Optimistic UI(이하 옵티미스틱 UI) 에 대해 알아보자!
기존의 버튼 인터랙션
1. 사용자가 버튼을 클릭
2. 버튼이 비활성화 상태로 바뀜
3. 서버에 신호가 전달되고
4. 서버에서 다시 페이지로 응답을 보낸다
5. 응답 결과를 보여주는 페이지가 새로 로딩된다.
이러한 기존의 버튼 인터랙션은 예측이 가능하고 오류가 나타날 가능성이 적다. 로딩상태일때 비활성화 상태의 버튼이 서버에 신호가 간 것을 확인시켜주고, 서버가 응답을 주면 페이지가 업데이트 되면서 인터렉션이 끝났음을 알려주는 직관성을 가진다. 하지만 이러한 기존의 방식은 다음과 같은 문제들을 가진다.
- 사용자의 인내심을 필요로 한다. 서버 최소 응답시간이 길어질 수록 페이지와 브랜드 이미지에 부정적인 영향을 줄 수 있다.
- 기존의 페이지가 업데이트가 되는 것이 아닌 새로운 페이지가 업로드 되는 방식으로, 작업중이던 사용자의 맥락을 끊는다. 의도하지 않은 맥락의 전환을 보여주기 때문에, 부정적일수 있다.

스피너와 SPA가 도입된 이후
1. 사용자가 버튼을 클릭
2. 버튼이 비활성화 되면서, 스피너가 버튼 위에 보이고 서버와 통신이 이뤄짐을 알려준다.
3. 서버에 신호가 전달되고
4. 서버에서 다시 페이지로 응답을 보낸다.
5. 응답에 따라 버튼과 페이지의 모습이 업데이트 된다.
페이지의 업데이트가 의도하지 않은 맥락의 변화를 일으키지 않고 유지하기에 인터렉션에 몰입하기 좋아진다. 이전 인터렉션의 두번째 문제점을 해결한 것이다. 하지만 여전히 사용자는 서버로부터의 응답을 기다려야한다. 2012년의 한 조사에 의하면 78% 소비자는 느리거나 신뢰가 떨어지는 웹사이트에 대해 부정적인 감정을 표현한다. 아래의 제이콥 닐슨의 연구에 의하면 응답시간에 따른 소비자의 인식변화를 확인할 수 있다.
1993년 제이콥 닐슨의 저서 Usability Engineering의 5장에 따르면:
- 0.1초는 사용자가 시스템이 순간적으로 반응하고 있다고 느낄수 있는 시간으로 결과를 표현하는 것만 필요함.
- 1.0초는 사용자가 지연을 알아차리더라도 사용자의 사고 흐름이 중단되지 않는 한계. 사용자는 데이터를 직접 조작하는 느낌을 잃는다.
- 10초는 사용자가 대화에 집중할 수 있는 한계. 컴퓨터가 완료될 때까지 기다리는 동안 다른 작업을 수행하기를 원할 것임으로, 작업이 완료될 것으로 예상되는 시간을 알려주는 피드백을 줘야한다. 지연 동안의 피드백은 응답 시간이 매우 가변적일 가능성이 있는 경우 특히 중요합니다.
서스펜스가 사용된다면, 시스템이 충돌하지 않고 문제를 해결하고 있음을 사용자에게 알립니다. 사용자가 대기할 것으로 예상되는 대략시간을 나타내 대기시간동안 다른 작업을 수행하게 할 수 있습니다.
서스펜스의 종류로는 완료율 표시기를 제공하거나, 완료후 제공될 수 있는 데이터베이스의 이름을 제공하거나, 최후의 수단은 시스템이 작동하고 있음만을 알려주는 서스펜스 종류일 수도 있습니다.
2014년 업데이트:
- 0.1초: 사용자가 UI에서 개체를 직접 조작하고 있다고 느끼는 제한시간.
- 1초: 사용자가 과도하게 컴퓨터를 기다릴 필요 없이 명령공간을 자유롭게 탐색하고 있다고 느끼는 제한시간. 컴퓨터가 명령에 대해 작동하고 있음을 느끼는 의미.
- 10초: 작업에 주의를 기울이는 사용자의 제한시간. 10초보다 느린 작업에는 완료율 표시기와 사용자가 작업을 중단할 수 있는 명확한 표지판이 필요한다.
이제는 결국 사용자가 기다리는 동안 진행중 지표를 보여주는 것만으로 만족하지 못한다. 그에 대한 해결책의 하나로 옵티미스틱 UI를 제시하고자 한다.
Optimistic UI
옵티미스틱 UI는 여전히 이전의 시나리오와 같은 사용자가 버튼을 클릭하는 동작으로 시작된다.
Optimistic(미래에 대해 희망과 확신이 있는) 의 뜻처럼 우리는 이러한 로직을 API가 안정적이고 응답이 예측가능한 수준일때, 유저가 처음에 취한 행동에 대한 응답으로 오류가 뜰 확률이 상당히 낮은 상황에 사용한다. 정확한 측정은 반복을 해봐야 알 수 있지만, 97-99%의 응답을 확신할 수 있다면, 우리는 새로운 버튼 인터렉션에 대해 완전히 새로운 이야기를 써 내려갈 수 있다.
사용자 측면
1. 사용자가 버튼을 클릭한다.
2. 버튼은 바로 성공일때와 같은 시각적 상태를 반영한다.
개발자 측면
1. 사용자가 버튼을 클릭한다.
2. 버튼은 바로 성공일때와 같은 시각적 상태를 반영한다.
3. 서버에 신호가 전달된다.
4. 서버에서 페이지로 응답을 보낸다.
5. 대부분의 경우 응답이 성공적일 것이므로, 사용자는 문제가 없다.
6. 실패한 경우에 시스템은 오류를 반환한다.
다음은 실제 프로젝트에서 적용된 "옵티미스틱 UI"의 예시를 살펴보겠다.

사용자가 버튼을 누른 상태에서, 여전히 서버의 응답을 기다리고 있지만 UI는 성공한 모드로 바뀌어 있다. 유저에게는 이미 성공한 UI를 제공하여 기다림을 보여주지 않으면서 매끄럽고 방해받지 않는 경험을 제공한다. 간단한 인터렉션에 응답시간이 실제로 16.85ms 밖에 걸리지 않지만, 이는 사용자가 UI를 직접 조작하고 있다고 느끼기에는 긴 시간일 수도 있다. 이때 옵티미스틱 UI의 적용으로 사용자는 대기시간을 인식하지 않고 낙관적인 업데이트를 통해 사이트에 대한 긍정적인 반응을 받는다.
다음은 위의 옵티미스틱 UI를 구현한 코드이다. react-query를 사용하면 다음과 같은 좋아요 버튼 mutate 상황에서 옵티미스틱 UI를 구현하기 편리하다.
// 디테일 페이지의 데이터를 api 요청하는 코드
const extractPostDetail = async () => {
const res = await getPostDetail(postId);
return res;
};
// postDetail 쿼리키를 활용해 post의 data를 postInfo 변수에 할당한다.
const postInfo = useQuery([queryKeys.postDetail, postId], extractPostDetail).data;
// mutation에서 사용한 toggleLike 함수, 실패상황에 대한 핸들링은 mutatation 함수에 위임하고,
// 여기서는 성공 상황에 대해서만 집중한다
const toggleLike = async () => {
if (postInfo?.isLiked === false) {
await likePost(postId);
} else {
await unlikePost(postId);
}
};
const { mutate: onToggleLike } = useMutation(toggleLike, {
onMutate: async () => {
// 기존의 좋아요상태 변경이 반영되지 않은 데이터를 snapshotOfPreviousPostDetail 변수에 저장
const snapshotOfPreviousPostDetail = queryClient.getQueryData<PostDetailData[]>([queryKeys.postDetail, postId]);
// mutation 요청하는 것을 캔슬
await queryClient.cancelQueries([queryKeys.postDetail]);
// setQueryData를 이용해 이전의 postDetail로 캐싱된 데이터의 isLiked 변수만 반대 boolean 값으로 변경
queryClient.setQueryData([queryKeys.postDetail, postId], () => {
return {
...postInfo,
isLiked: !postInfo?.isLiked,
};
});
// 이전 데이터를 onError 핸들링에 사용하기 위해 반영
return { snapshotOfPreviousPostDetail };
},
onSuccess: () => {
// 성공시 쿼리 invalidate를 실행시켜 refetch 통해 데이터 갱신
queryClient.invalidateQueries([queryKeys.postDetail], {
refetchType: 'all',
});
queryClient.invalidateQueries([queryKeys.postList], { refetchType: 'all' });
},
onError: (error, variables, context) => {
// 에러가 발생할 시 저장해둔 snapshot 데이터를 다시 setQuery 해서 기존 데이터로 복구
queryClient.setQueryData([queryKeys.postDetail, postId], () => {
return { ...context?.snapshotOfPreviousPostDetail };
});
},
});
사용자의 액션에 대한 빠른 응답
초기의 인터렉션 방식에서 우리는 버튼을 클릭한 후 버튼이 더이상 눌리지 않도록 비활성화 상태로 만들었고, 이를 통해 서버와 통신이 이루어지고 있음을 보여줬다. 하지만 이러한 인터페이스의 비활성화 상태는 수동적 기다림을 의미한다. 사용자는 아무 것도 할 수 없고 직접적인 통제가 불가능하다. 옵티미스틱 UI는 비활성화 상태를 통째로 건너뜀으로써 이러한 통제 불가능한 상황을 배제하고, 낙관적 결과로 소통한다.
잠재적 실패에 대응
물론 아주 낮은 확률로 서버의 오류상태를 핸들링 할 수 있는 방법 또한 고려해야한다.
예시 코드에선 onError에서 변경 전 상태를 저장해뒀던 snapshotOfPreviousPostDetail을 다시 set하는 방식으로 변경 전 상태로 되돌린다.
옵티미스틱 UI 디자인의 비관적 측면
옵티미스틱 UI 디자인이 black pattern 이라는 의견이 있다. 하지만 옵티미스틱 UI는 거짓말의 영역이라기보다 예측의 영역이다. 위의 예시에서 봤듯이, 응답은 16.85ms 라는 짧은 시간에 돌아오고 만약 실패의 경우에도 비슷한 수준을 유지할 것이다. 트위터의 경우 버튼의 상태를 되돌리는 것으로 이러한 응답의 실패상황을 핸들링하는데, 실제 적용한 프로젝트도 동일한 방법으로 구현이 되어있다.
이러한 실패 대응법에 추가로, 사용자에게 요청에 대한 실패상황이나, 오류를 알릴수 있는데 이는 알림을 나타내는 방식으로 구현하는 것이 일반적일 것이다. 하지만 이러한 알림의 방식은, 사용자의 맥락을 전환할 것이고 그렇게 새로운 맥락에서 다음 액션을 제공하는 방식으로 가이드를 제시해야한다. (오류상황을 알리면서, 취할 수 있는 해결책을 제시하지 않는 불친절한 방식은 페이지나 서비스 전체의 이미지에 부정적이다.)
물론 이러한 새로운 맥락을 만들어 내는 오류 대응법이 큰 형태의 웹사이트에서 오류를 핸들링하는 방식으로는 충분하지만, 버튼을 누르는 것 같은 단순한 액션에 대해서는 과잉일 수 있다. 옵티미스틱 UI는 실패에 대해 열려있어야하지만, 맥락에 따라 이루어져야한다.
옵티미스틱 UI 디자인의 적용
- API가 안정적이며 예측가능한 결과값을 보여주는지 확실하게 확인하라.
- 인터페이스는 서버에 요청이 보내지기 전에 잠재적 오류와 문제점을 발견해야 한다. 하지만 더 좋은 것은 API의 오류에 올 수 있는 모든 것을 제거해야 한다. UI 요소가 간단할수록 낙관적으로 만들기 간단하다.
- 낙관적 패턴을 성공 또는 실패 응답 이상을 기대할 수 없는 이분법적 요소들에 적용.
- API의 응답시간을 알고, 응답시간 자체가 매우 빠른 경우 대부분의 환경을 고려하더라도 시간이 2초 이내에 오는 경우
- 옵티미스틱 UI는 단순히 버튼 클릭에 관한 것만 아니다. 페이지 로딩을 비롯한 페이지의 라이프 사이클 동안 발생하는 다양한 인터렉션과 이벤트에 적용될 수 있다. skeleton UI도 같은 맥락이다. (서버가 성공응답을 보낼 것이라고 예측하고 유저에게 보이는 화면을 미리 보여준다)
느낀점
Optimistic UI에 대해 학습하고 적용하면서, Skeleton UI도 일부분이라는 사실에 신기함을 느꼈다. 매끄러운 상호작용과 긍정적인 UX를 위해서 필요한 부분에 적극적으로 도입해도 괜찮을 요소인것 같다는 생각이 들었다. 유저와의 인터렉션을 중요하게 생각하는 프론트엔드 개발자라고 스스로 생각하는 만큼, 데이터에 기반하여 유저경험을 개선하기 위한 이러한 시도들을 계속해서 해봐야겠다.
참고자료
True lies of Optimistic UI - 옵티미스틱 UI의 계산된 거짓말
이 글은 Denys Mishunov가 2016년 11월 SMASHING MAGAZINE에 게재한 글입니다. 피엑스디에서 저자의 서면 허락을 받고 번역, 게재하였으며, 저자의 허락없이 복사하여 사용하는 것은 절대 안됩니다. 원문 링
story.pxd.co.kr
https://www.nngroup.com/articles/response-times-3-important-limits/
Response Time Limits: Article by Jakob Nielsen
How users react to delays in a user interface, whether website or application. The 3 main response time limits are determined by human perceptual abilities.
www.nngroup.com
'개발 > 개발일지' 카테고리의 다른 글
| "Skeleton UI" 와 "사용자 경험 개선"의 은밀한 관계 (0) | 2023.02.20 |
|---|---|
| 왜 queryClient 설정에 useState를 사용할까? (1) | 2023.02.10 |
| 지역 중고거래 및 정보교류 커뮤니티 프로젝트 마이그레이션 후기(1차) (0) | 2023.01.30 |
| ErrorBoundary 심층 적용기 (1) | 2023.01.05 |
| [Trouble Shooting] useMutation을 활용한 생성, 수정, 삭제 (0) | 2022.08.30 |