Optimistic UI
Optimistic UI는 사용자 경험을 개선하기 위한 중요한 프론트엔드 패턴으로, 이에 대해 학습하고자 작성하게 되었습니다.
Optimistic UI는 서버 응답을 기다리지 않고 사용자의 액션이 성공할 것이라고 "낙관적으로 가정"하여 UI를 즉시 업데이트하는 방법입니다. 예를 들어 ‘좋아요’ 버튼을 클릭하면 서버 응답을 기다리지 않고 바로 하트가 채워지는 것이 있습니다.
장단점
Optimistic UI를 적용한다면 즉각적인 피드백을 기반으로 사용자는 클릭과 동시에 결과를 볼 수 있게 되어 앱의 반응성이 좋다는 느낌을 줄 수 있는데, 이는 결과적으로 사용자 경험을 크게 향상시킵니다.
하지만 구현을 하기 위해선 적절한 에러 처리와 상태 관리를 해야하기 때문에 구현의 복잡도는 증가합니다. 만약 로직이 실패했다면 롤백을 해주어야 하는 등의 처리가 필요하기 때문에 클라이언트와 서버의 상태를 체계적으로 관리하는 것이 핵심이라고 생각합니다.
사용 예시
Optimistic UI는 적용하기 좋은 경우는 SNS의 좋아요나 팔로우 기능처럼 성공룔이 매우 높으면서 실패가 심각한 문제를 발생시키지 않는 경우에 사용하기 적합하다고 생각합니다. 일반적으로 실패할 확률이 적기 때문에 낙관적으로 처리해도 문제가 될 가능성이 적을 것이고, 좋아요 등의 기능은 사용자에게 즉각적인 피드백을 전달하는 것이 사용자 경험의 측면에서 중요하기 때문에 이러한 경우에 사용할 수 있을 것입니다.
- SNS 좋아요/팔로우: 성공률이 매우 높음
- 쇼핑 장바구니 추가: 즉시 피드백이 중요
- 간단한 설정 변경: 토글, 스위치 등
- 댓글/리뷰 작성: 사용자 참여도 향상
하지만 결제 처리와 같이 실패를 했을 때 심각한 문제를 야기하는 기능에서는 사용을 지양하는 것이 맞다고 생각합니다. 예를 들어 결제를 완료했다고 생각했는데, 결제 처리가 이루어지지 않는 등의 상황에서는 치명적인 결함으로 다가올 수 있다고 생각하기 때문에 사용하지 않는 것이 좋다고 생각합니다. 또한 네트워크의 영향을 많이 받거나 파일의 크기가 큰 경우에는 실패 가능성이 높은데, 이러한 경우 또한 지양해야 한다고 생각합니다.
- 결제 처리: 실패 시 심각한 문제
- 회원가입/로그인: 보안이 중요
- 파일 업로드: 실패 가능성이 높음
- 관리자 기능: 정확성이 우선
적용
저는 다음과 같은 방식으로 Optimistic UI를 적용해보았습니다. 저는 ‘좋아요’ 기능을 Optimistic UI를 적용해보았습니다.
먼저 액션 정의 부분에서는 기존의 likeSuccess 액션 외에 두 가지 액션을 추가로 정의했습니다. likeOptimistic은 즉시 UI 업데이트를 위한 액션이고, likeRollback은 실패 시 원상복구를 위한 액션입니다.
case likeSuccess(isLiked: Bool) // 서버 확인 후 최종 업데이트
case likeOptimistic(isLiked: Bool) // 즉시 UI 업데이트 (새로 추가)
case likeRollback(isLiked: Bool) // 실패 시 롤백 (새로 추가)
로직 구현에서는 총 4단계로 처리 과정을 나누어 구현했습니다.
1단계에서는 사용자가 좋아요 버튼을 클릭하는 순간 서버 응답을 기다리지 않고 즉시 UI를 업데이트합니다. 현재 좋아요 상태의 반대값으로 likeOptimistic 액션을 보내고, 동시에 로딩 상태도 설정해서 중복 요청을 방지합니다.
2단계에서는 서버 응답을 받은 후 로컬 상태와 서버 상태를 동기화합니다. 여기서 중요한 점은 서버 응답이 예상한 값과 다를 때만 추가로 likeSuccess 액션을 보낸다는 것입니다. 만약 서버 응답이 우리가 낙관적으로 설정한 값과 일치한다면 이미 UI가 올바른 상태이므로 추가 작업이 불필요합니다.
3-4단계에서는 실패 상황을 처리합니다. 서버에서 실패 응답을 받거나 네트워크 에러가 발생하면 likeRollback 액션을 통해 원래 상태로 되돌립니다. 이때 저장해둔 currentLikeStatus 값을 사용해서 정확히 이전 상태로 복원합니다.
전체적인 흐름은 "버튼 클릭 → 서버 요청 시작 && UI 변경 → 서버 응답 → 성공 시 유지, 실패 시 롤백"입니다. 핵심은 서버의 응답이 오기 전에 미리 UI를 변경해두고, 추후에 받은 응답을 기준으로 롤백 여부를 결정하는 것입니다.
private func handleLikeOptimistic(store: StoreDetailStore) async {
let currentLikeStatus = store.state.entity.imageCarousel.isLiked
let newLikeStatus = !currentLikeStatus
await MainActor.run {
// 1️⃣ 즉시 UI 업데이트 (Optimistic)
store.send(.likeOptimistic(isLiked: newLikeStatus))
store.send(.setLikeLoading(isLoading: true))
}
let request = StoreLikeRequest(like_status: newLikeStatus)
do {
let result = try await NetworkManager.shared.fetch(
StoreRouter.like(query: StoreIDRequest(id: store.state.storeID), request: request),
successType: StoreLikeResponse.self,
failureType: CommonMessageResponse.self
)
await MainActor.run {
store.send(.setLikeLoading(isLoading: false))
if let success = result.success {
// 2️⃣ 서버 응답과 로컬 상태 동기화
if success.like_status != newLikeStatus {
store.send(.likeSuccess(isLiked: success.like_status))
}
// 일치하면 이미 optimistic으로 업데이트했으므로 추가 작업 불필요
} else if let failure = result.failure {
// 3️⃣ 실패 시 원래 상태로 되돌리기
store.send(.likeRollback(isLiked: currentLikeStatus))
}
}
} catch {
// 4️⃣ 네트워크 에러 시 롤백
await MainActor.run {
store.send(.setLikeLoading(isLoading: false))
store.send(.likeRollback(isLiked: currentLikeStatus))
}
}
}
이런 방식으로 구현하면 사용자는 좋아요 버튼을 클릭하는 순간 바로 하트가 채워지는 것을 볼 수 있어서 앱이 매우 반응성이 좋다고 느끼게 됩니다. 만약 네트워크 상황이 좋지 않더라도 사용자는 자신의 액션에 대한 즉각적인 피드백을 받을 수 있기 때문에 전체적인 사용자 경험이 크게 개선됩니다.
결론
기술은 결국 사용자를 위해 존재하는 것이고, Optimistic UI 또한 사용자의 편의와 만족도를 높이기 위한 수단일 뿐, 그 자체가 목적은 아니라고 생각합니다.
결국 개발자는 사용자에게 어떠한 경험을 제공하는 것이 적절한 지에 대해 고민하는 자세를 통해 더 나은 사용자 경험을 제공해야 한다고 생각합니다. 각 프로젝트의 특성과 사용자의 니즈를 충분히 고려함으로써 빠른 반응성이 중요한 상황인지, 아니면 확실한 결과 확인이 더 중요한 상황인지를 판단하고, 그에 맞는 기술을 선택하는 것이 개발자가 가져야하는 덕목이라고 생각하게 되었습니다.
'iOS' 카테고리의 다른 글
[iOS] Decimal을 사용해야 하는 이유 (0) | 2025.04.02 |
---|---|
[iOS] Xcode의 빌드 과정 (0) | 2025.01.16 |
[UIKit] UILabel.text와 문자열 보간법의 옵셔널 표현의 차이 (0) | 2025.01.03 |
[iOS] SwiftLint 설치 및 적용하기 (0) | 2023.12.23 |
[iOS] 협업 시 하나의 Bundle Identifier로 설정하기 (Provisioning Profile, Certificate 공유) (0) | 2023.11.21 |