본문 바로가기

개발/개발일지

"Skeleton UI" 와 "사용자 경험 개선"의 은밀한 관계

들어가며

머리를 맞았다! 시작은 "실서비스가 아닌걸 어떻게 유저데이터를 모아요? 다른 방법 없어요?" 라는 종택님의 질문에 나는 "아..맞네..?" 라는 대답을 하고선, 고속도로가 뚫리는 기분이 들었다. 데이터는 '유저데이터만 있는게 아니었지!' 에서 이 글은 시작된다. 

 

선생님들,,, 유저경험 개선 된다면서요?

프로젝트의 홈 화면에서는 게시물 목록 API를 사용하여 화면을 구성한다. 특별한 비즈니스 로직이 포함되어 있지 않기에, 사용자 환경이 양호하다면 API 응답 지연시간이 거의 존재하지 않는다. 네트워크탭에서 설정할 수 있는 네트워크 설정에 따른 화면의 연출을 보여준다.

네트워크 제한 없을 때 화면

[제한 없음 모드] skeleton 처럼 보이는 로딩 UI 가 잠시 화면에 나타났다 사라지면서 깜빡임을 연출한다. 실제로 요청한 화면이 빠르게 노출 되면서 스켈레톤이 보여지는 시간이 너무 짧았기 때문에 오히려 불필요한 느낌을 받는다. 스켈레톤인지 모르는 경우에는 그냥 깜빡임이나 오류화면으로 느껴질지도 모르겠다.

빠른 3G 모드

[빠른 3G모드] 빠른 3G 모드로 변경했더니, 이전 상태보다 skeleton 이 뚜렷하게 보인다.

 

느린 3G 모드

[느린 3G 모드] skeleton이 확실히 유저경험을 개선해주는 듯하다. 즉각적으로 요청이 가고 있다는 사실을 보여주면서 유저 이탈률을 낮추는데 도움이 될 것 같다.

사용하면....유저 경험 개선된다고 했잖아요......

API 응답시간이 짧은 경우에는 skeleton을 보여주지 않는게 더 좋은 사용자 경험을 줄 수 있지 않을까?

 

참고자료) Progress Indicator에 대한 UX 리서치

더보기

좋은 Progress Indicator는 사용자에게 긍정적인 경험을 줄 수 있고 사용자가 작업을 끝까지 수행할 수 있도록 만들 수 있습니다.

Progress Indicator와 관련된 주요 지침은 다음과 같습니다.

  1. 약 1초 이상 걸리는 작업에는 Progress Indicator를 사용하십시오.
  2. Loop Animation은 빠른 동작에만 사용하십시오.
  3. Percent-done Animation은 10초 이상 걸리는 작업에 사용하십시오.
  4. Static Indicator는 사용하지 마십시오.

로드하는데 1초 미만이 소요되는 모든 항목의 경우 반복되는 애니메이션을 사용하면 주의가 산만해집니다.
사용자는 화면에서 어떤 일이 발생했는지 따라갈 수 없고, 화면에 깜빡이는 내용에 대해 불안을 느낄 수 있습니다.

출처: https://www.nngroup.com/articles/progress-indicators/

 

 


로직을 정리해보자!

서버에 데이터를 요청해서 받아오는 상황 (isFetchingNextPage, 로딩상황) => 기준시간 동안 아무런 화면을 보여주지 않는다 => (기준시간 내에 로딩이 완료되면) 성공 UI를 보여준다. //  (기준시간이 지나도 로딩 상황이라면) 요청이 수행되고 있다는 의미의 Skeleton UI를 노출해서 유저에게 확인을 시켜준다. 이후 완료시 Skeleton UI를 제거하고 성공 UI를 보여준다. 

 

  • 핵심은 기준시간이야!

일반적인 네트워크환경에서 api 응답시간은 16ms 정도 소요된다.
빠른 3G 환경에서는 585ms, 느린 3G 환경에서는 2.07s의 응답시간이 소요된다. 

네트워크 탭에서 설정할 수 있는 3가지 환경에서 API 응답시간은 각각 평균 16ms(제한없음), 585ms(빠른 3G), 2.07s(느린 3G) 가 소요된다. 위의 환경별 영상을 보면, 빠른 3G 환경부터 어느정도 Skeleton이 의미 있는 것으로 인식되었기 때문에 빠른 3G 환경에서는 Skeleton을 살리고 제한없음 환경에서는 사용되지 않는 200ms를 설정했다.

 

 

  • 추가 판단근거

카카오페이 프론트엔드 팀의 Firebase Performance Monitoring의 성능지표를 참고했다. (https://tech.kakaopay.com/post/skeleton-ui-idea/#progress-indicator%EC%97%90-%EB%8C%80%ED%95%9C-ux-%EB%A6%AC%EC%84%9C%EC%B9%98)

 

카카오페이팀의 Firebase Performance Monitoring 네트워크 요청 응답 시간 지표
세부 정보를 보니 75%의 사용자들은 192ms 이내에 응답
90% 의 사용자들이 296ms 이내에 응답

 위의 자료들을 통해 카카오페이팀에서는 200ms 정도의 Skeleton 지연이 있다면, 75% 이상의 유저들은 부자연스러운 깜빡임을 느끼지 않을수 있고, 나머지 약 96초동안 15% 정도의 사용자들이 Skeleton을 보게될텐데 스켈레톤을 200ms 지연시킬 경우 전체 사용자 중 75%는 기존에 느끼던 덜그럭거림을 느끼지 않을 수 있지만, 기존에 덜그럭거림을 느끼지 못하던 15%의 사용자들은 덜그럭거리는 스켈레톤 뷰를 보게 된다는 가정을 했다. 15%의 유저의 경험은 조금 나빠질 수 있지만 75% 유저의 경험을 개선할 수 있다면 의미 있는 시도가 아닐까 싶다.

(물론 카카오페이팀에서 말했듯이 실 서비스에서는 다양한 시나리오를 세워보고 고민하면서, 실험과 검증을 통해 지속해서 개선 프로세스를 진행해야겠지만) 카카오페이팀에서 추가 예시로 제시했던 공지사항 API는 평균 100ms의 응답시간을 가졌고, 우리 프로젝트에서는 16ms의 응답을 가졌기 때문에 상대적으로 응답시간이 짧고 덜 복잡한 프로젝트에서는 더 많은 유저들이 부자연스러운 Skeleton을 경험하지 않을 것이라는 판단으로 200ms 를 기준으로 정했다.

 


지연로딩 구현하기

처음 구현한 코드는 다음과 같다. 

useEffect(() => {
	const fetchNextPageAndShowSkeleton = async () => {
    //다음 페이지가 있고 뷰포트에 들어갔을때 200ms 가 지난 후 스켈레톤을 보여준다 (지연)
		if (inView && hasNextPage) {
			const timerId = setTimeout(() => {
				setShowSkeleton(true);
				console.log('변경');
			}, 200);
            //다음페이지를 붙이는 비동기 동작을 Promise 반환을 기다리며, 완료시에 setTimeout을 취소하고 스켈레톤을 안보이게 한다.
			await fetchNextPage();
			clearTimeout(timerId);
			setShowSkeleton(false);
		}
	};
	fetchNextPageAndShowSkeleton();

	return () => {
		setShowSkeleton(false);
	};
}, [inView, hasNextPage, fetchNextPage]);

...

{isFetchingNextPage ? showSkeleton ? <SkeletonList /> : null : <div className="inviewDiv" ref={ref} />}

 

원하던 대로 작동도 하고 사용부가 많지 않아서 아직 문제는 없어 보이지만 더 많은 페이지에서 지연로직을 사용하게 된다면, 매번 번거로운 코드 작성이 발생하고 수정되야할때 일일히 모든 코드를 수정해야하는 등 문제가 많아질 것 같아서 고민을 하게 되었다. 

 

우리가 어떤 개발자입니까...? 공통로직은 분리해서 재사용 할수 있는 개발자 아임니까..!

유틸성 컴포넌트를 분리하여, 데이터 패칭로직과 skeleton 지연 로직을 분리했다. 하나의 useEffect에서 하나의 동작을 맡으면서 분명해지고 이해하기 쉬워졌다. 이때 중요한 것은, Qurey 문의 suspense 옵션을 true로 해줘야 한다는 것이다. (usePostList hook의 비동기 결과에 postList 컴포넌트가 의존하기 때문에 jsx 요소 반환이 동기적이지 않다. 따라서 Suspense를 PostList 컴포넌트와 함께 직접 사용할 수 없다. 이를 suspense 옵션을 true로 설정하여, Suspense가 포착하고 기다릴 수 있는 Promise를 자동으로 던진다.)

* props로 setTimeout의 지연시간을 넘겨서 사용한다면, 페이지별 API 응답 속도에 따른 시간 조절도 가능할 것 같다.

//skeleton 지연을 위한 유틸성 컴포넌트

const DeferredComponent = ({ children }: PropsWithChildren<{}>) => {
//기존의 showSkeleton 과 같은 동작을 하는 state
  const [isDeferred, setIsDeferred] = useState(false);

  useEffect(() => {
  //200ms 지연해준다.
    const timeoutId = setTimeout(() => {
      setIsDeferred(true);
    }, 200);
    return () => clearTimeout(timeoutId);
  }, []);

  if (!isDeferred) {
    return null;
  }

  return <>{children}</>;
};


// 데이터 패칭하는 로직
useEffect(() => {
		if (inView && hasNextPage) {
			fetchNextPage();
		}
	}, [inView]);




<Suspense fallback={
	<DeferredComponent>
		<SkeletonList/>
	</DeferredComponent>
	}>
	<PostList
		postFilterObj={postFilterObj}
		searchKeyword={searchKeyword}
		AgreementToMissingInfo={AgreementToMissingInfo!}
	/>
</Suspense>

 

유틸성 컴포넌트를 사용한 방식도 충분히 원하던 기능을 하지만, 확장성과 유연성을 고려하여 고차 컴포넌트(Higher-Order Component) 로 코드를 변경했다. export 구문에 고차컴포넌트를 감싸면서 사용부에서 따로 컴포넌트 구성을 해줄 필요가 없게 되었다.

//hoc component

import { ComponentType, PropsWithChildren, useEffect, useState } from 'react';

const withDeferred = <P extends object>(WrappedComponent: ComponentType<P>) => {
	const DeferredComponent = (props: PropsWithChildren<P>) => {
		const [isDeferred, setIsDeferred] = useState(false);

		useEffect(() => {
			const timeoutId = setTimeout(() => {
				setIsDeferred(true);
			}, 200);
			return () => clearTimeout(timeoutId);
		}, []);

		if (!isDeferred) {
			return null;
		}

		return <WrappedComponent {...props} />;
	};

	return DeferredComponent;
};

export default withDeferred;


//사용부
export default DeferredComponent(SkeletonList);

 

이제 약 75% 이상의 유저들은 불필요한 Skeleton으로 인한 화면의 버벅거림을 보지 않을수 있게 되었다. 어쩌면 그 이상일지도 모른다.

 

 


마치며

사용자 경험 향상을 위해 무한스크롤로 구현된 화면에 적용 했던 Progress Indicator(=Skeleton)을 제한없이 적용하면서, 어느 순간에는 많은 유저들의 사용자 경험을 해치고 있었을지도 모른다. 이번 기회를 통해 개선을 위해 개발한 기능들이 모든 순간에 긍정적이진 않을 수 있으며, 미세한 차이를 고려하는 것에서 사용자 경험이 달라질 수 있다는 것을 경험했다.

 

또한 사용자 경험에 대한 연구들을 통해 여러 인사이트를 얻고, 데이터를 활용해 가설을 세우고 이를 기반으로 사용자 경험을 개선하려는 시도를 통해 프론트엔드 개발자로서 사용자와 가장 가까운 곳에서 소통한다는 것의 의미를 다시 상기할 수 있었다. 비록 모든 사용자가 다른 환경을 가지고 있기에 모두에게 동일한 사용자경험을 제공할 수는 없지만, 이러한 시도들을 통해 개선해 나간다면 더 좋은 사용자 경험을 만들어 내는 개발자가 되리라 믿는다.

 

 


참고자료

https://tech.kakaopay.com/post/skeleton-ui-idea/#progress-indicator%EC%97%90-%EB%8C%80%ED%95%9C-ux-%EB%A6%AC%EC%84%9C%EC%B9%98

 

무조건 스켈레톤 화면을 보여주는게 사용자 경험에 도움이 될까요? | 카카오페이 기술 블로그

카카오페이에서 프론트엔드 개발을 하며 스켈레톤 UI와 사용자 경험 향상에 대해 고민한 내용을 공유합니다.

tech.kakaopay.com

https://www.nngroup.com/articles/progress-indicators/

 

Progress Indicators Make a Slow System Less Insufferable

Users are more satisfied and will wait longer when a site uses wait animations such as percent-done bars and spinners to explain >1 s response times delays.

www.nngroup.com