CSS: Skeleton UI 적용기

두선아 Dusuna·2023년 3월 12일
3

CSS

목록 보기
1/2
post-thumbnail

CSS: Skeleton UI 적용기

도입 배경

목적 A. 이미지 랜더링 UX 개선

🔖 현재 자사의 서비스는 이미지를 서버에 업로드하고, 직접 보관하는 것이 아닌, 원 사이트의 이미지 링크를 통해 접근하는 방식을 사용하고 있습니다. 따라서 이미지의 용량을 줄이는 것이 불가능합니다.

데이터 패칭 과정을 통해 확인한 결과, 특정 이미지의 로딩 시간이 길어 UX가 저하되는 현상을 확인하였고, 그 원인으로 링크된 이미지 중 파일 크기가 10MB이상, 또는 가로 크기가 4000px 이상인 대용량 이미지가 있음을 알게되었습니다.

람다 등을 사용해 이미지 사이즈를 직접 줄일 수는 없는 현재 상황에서 페이지 로딩의 개선을 위해 썸네일 이미지 로딩의 UX 최적화가 반드시 필요하였고, 이틀 동안 관련 작업을 진행하였습니다.

목적 B. 페이지 전환을 자연스럽게 보여주기

🔖 React 프로젝트에서 이미지의 src가 바뀌며 이미지가 로딩될 때까지 이전 이미지가 그대로 남아있음으로서 오히려 페이지 전환이 어색하게 느껴지는 현상이 있었습니다.

페이지의 변경이 있을 때, 사용자에게 페이지 변환을 통해 게시글의 변경이 있음을 자연스럽게 알려주기 위해 기존 이미지를 지우고, 로딩을 시작하는 단계를 시각적으로 알리는 과정, 말하자면 인위적인 로딩, 깜박임이 필요합니다.


스켈레톤 UI란?

  • 데이터가 로드될 빈 영역을 먼저 잡아놓고, 데이터가 로딩되면 데이터를 채워넣는 형식입니다.
  • 레이아웃 쉬프트 현상을 최소화해, 랜더링 성능 개선을 할 수 있습니다.

    🔖 레이아웃 쉬프트(Layout Shift)는 웹 페이지에서 레이아웃이 예기치 않게 변경되는 현상을 의미합니다. 일반적으로 사용자가 페이지를 로드하는 동안 요소들이 이동하거나 새로운 요소들이 추가됨으로써 발생합니다. 이는 사용자 경험을 저하시키는 요소 중 하나입니다.

  • 디자인적으로 간단하고 직관적이며, UX을 향상시키는 데 유용합니다
    • 로딩 중 UX를 향상시키는 유사한 기능을 하는 UI에는 프로그래스바, 스피너 등이 있습니다.
    • 이전에 스켈레톤 UI를 적용해본 적이 없기 때문에 도입하면서 관련 내용을 정리해보려 합니다.

작업 계획:

다음 경우에 스켈레톤 UI를 사용하여 사용자 경험 개선을 노려볼 수 있습니다.

1. 이미지 및 리스트를 최초 로딩할 때

  • 리스트가 로딩 중일 때, 배경의 스켈레톤 애니메이션을 통해 로딩 중을 알립니다.
  • 리스트가 loading이 끝났을 때, 리스트를 보여줍니다.

2. 이미지 및 리스트가 변경되었을 때

  • 이미지의 변경이 있을 때 유저에게 이미지가 로딩된다는 것을 시각적으로 보여주기 위해,
    기존 이미지를 지웁니다.
  • loading이 false일 때만 이미지를 보여줍니다.
    1. 이미지가 로딩에 완료했을 때, loading을 false값으로 변경해 이미지를 보여줍니다.
    2. 이미지가 로딩에 실패했을 때, error를 true값으로 변경해 대체 이미지를 보여줍니다.
      1. 대체 이미지는 각 상황별로 사용합니다. ex) 카테고리가 4개일 때, 대체 이미지 4개
    3. 이미지가 로딩 중이지만, 이미지 용량이 커서 로딩에 500ms 이상 걸리는 특정 이미지가 있을 경우 사용자에게 로딩 중인 상황을 보여주는 것이 덜 답답할 수도 있습니다.
      1. loading 상태가 된 지 500ms가 지나면 loading을 false값으로 변경해, 이미지가 위쪽에서부터 로딩되는 과정을 보여줍니다.

기존 구현 내용

리스트 데이터

  • 리스트 데이터가 없을 때를 구분하기 위하여, 최초 로딩 진행 중일 때 loading 상태를 확인하였습니다. 최초 로딩 시까지 리스트가 화면에 나타나지 않는 공백이 있습니다.
    <Wrap>{!loading && <ItemList rowGap={40} listData={fetchData} />}</Wrap>

이미지 로딩

  • 각 아이템의 썸네일에 image의 src를 전달하여, 리랜더링 없이 아이템을 변경했습니다.
  • 화면 전환에 있어서, 텍스트가 먼저 변경되고 이미지가 뒤따라 로딩되면서 어색함이 있었습니다.
    • 텍스트보다 이미지가 로드되는 시간이 훨씬 길기 때문에, 로딩 과정에서 아이템이 바뀌고 있는 지, 이미 바뀐 아이템인지 시각적으로 알아보기 힘듭니다.
    • 관련 내용을 해결하기 위해 레퍼런스(dribble) 사이트의 UI를 참고하였을 때, 텍스트가 먼저 변경되고 이미지를 비워두었다가, 이미지가 로딩되면 채워지는 형식의 UX가 훨씬 자연스러움을 확인했습니다.

적용 방법

방법 A. metarial UI 등에서 제공하는 스켈레톤 기능을 사용합니다.

  • 장점
    • 편리하고 최적화된 UI를 구현할 수 있습니다.
  • 단점
    • 현재 회사 프로젝트에 metarial UI를 사용하고 있지 않습니다.
    • 해당 UI를 위해서 MUI를 추가하는 것은 맞지 않습니다.

방법 B. 직접 구현합니다.

  • 장점
    • 의존성 없이 작업할 수 있습니다.
    • 처음 구현해보는 UI를 직접 구현함으로서, 더 많이 이해할 수 있습니다.
  • 단점
    • 처음 구현해보는 것이기 때문에, 구현 시간이 오래 걸릴 수 있습니다.
    • 이전 토스트 UI등을 직접 구현하였을 때, 구현 시간보다도 버그 수정에 더 많은 시간과 노력을 들여야했었고, 버그 수정를 위해 팀원간의 추가적인 많은 소통을 필요로 했던 기억이 있습니다.
    • 예전 팀 프로젝트 이슈 내용 : https://github.com/cupicks/cupicks-fe/issues/73

방법 C. 스켈레톤 UI를 위한 라이브러리를 사용합니다.

  • 장점
    • 시간과 결과물을 생각한 현실적인 방안입니다.
  • React skeleton 라이브러리
    • react-content-loader
      • 커스텀 SVG : SVG를 직접 그릴 수 있음
    • react-loading-skeleton
      • 24.6 kB로 더 가벼움
  • 디자인을 위해 svg를 직접 그릴 수 있는 react-content-loader 라이브러리를 사용합니다.

작업 내용

  • 기존 loading 관련 useEffect에, setLoading(true)를 추가합니다.
    • 최초 랜더링 시, 데이터가 loaded되었는지 여부만 확인했던 기존 구현과 달리,
      매 패칭 시 loading여부를 확인하고, 스켈레톤 UI를 랜더링하겠습니다.

      useEffect(() => {
        setLoading(true);
      
        (function (){
      	  // 리스트 패칭이 완료되었다면
          setLoading(false);
        })()
      
      }, [location.pathname, location.search]);
  • 다음과 같이 의 children으로 svg를 그립니다.
    import ContentLoader from "react-content-loader";
    
    export default function SkeletonLoader() {
      return (
        <ContentLoader viewBox="0 0 250 330">
          <rect x="0" y="0" rx="0" ry="0" width="250" height="140" />
    
          <rect x="20" y="162" rx="8" ry="8" width="120" height="16" />
          <circle cx="220" cy="170" r="12" />
          <circle cx="200" cy="170" r="12" />
    
          <rect x="20" y="195" rx="8" ry="8" width="100" height="16" />
          <rect x="125" y="195" rx="8" ry="8" width="80" height="16" />
          <rect x="20" y="220" rx="8" ry="8" width="50" height="16" />
          <rect x="75" y="220" rx="8" ry="8" width="70" height="16" />
    
          <rect x="20" y="300" rx="8" ry="8" width="70" height="16" />
          <rect x="180" y="300" rx="8" ry="8" width="50" height="16" />
        </ContentLoader>
      );
    }

SVG, Scalable Vector Graphics란?

SVG 사용하기

  • SVG(Scalable Vector Graphics)는 XML 기반의 벡터 그래픽 이미지 포맷입니다. SVG를 사용하면 그래픽을 구성하는 모든 요소들이 벡터 형태로 저장되므로, 이미지를 확대 또는 축소할 때도 깨끗하고 선명한 화질을 유지할 수 있습니다. SVG를 그리는 방법은 아래와 같습니다.
    설명예시
    1HTML에 SVG 태그 추가<svg width="200" height="200"> … </svg>
    2그래픽 요소 추가<rect x="10" y="10" width="100" height="100" /> <circle cx="150" cy="150" r="50" fill="blue" />
    3CSS로 스타일링fill: 채우기 색상; stroke: 선 색상; stroke-width: 선 굵기;
  • 코드 : svg 태그 안에 svg 요소를 작성합니다.
  <svg width="200" height="200">
    <rect x="10" y="10" width="100" height="100" fill="red" />
    <circle cx="150" cy="150" r="50" fill="blue" />
  </svg>

SVG 요소

요소설명
<rect>사각형 그리기
<circle>원 그리기
<line>선 그리기
<polyline>폴리라인 그리기
<polygon>폴리곤 그리기
<path>경로 그리기
<ContentLoader viewBox="0 0 250 330">
  <rect x="0" y="0" rx="0" ry="0" width="250" height="140" />

  <rect x="20" y="162" rx="8" ry="8" width="120" height="16" />
  <circle cx="220" cy="170" r="12" />
  <circle cx="200" cy="170" r="12" />

  <rect x="20" y="195" rx="8" ry="8" width="100" height="16" />
  <rect x="125" y="195" rx="8" ry="8" width="80" height="16" />
  <rect x="20" y="220" rx="8" ry="8" width="50" height="16" />
  <rect x="75" y="220" rx="8" ry="8" width="70" height="16" />

  <rect x="20" y="300" rx="8" ry="8" width="70" height="16" />
  <rect x="180" y="300" rx="8" ry="8" width="50" height="16" />
</ContentLoader>
  • 상단 코드에서 x는 왼쪽 위를 기준으로 그림을 그리기 시작하는 위치, y는 오른쪽 위를 기준으로 그림을 시작하는 위치, rx와 ry는 radius를 뜻합니다. 또한 cx는원의 중심 x좌표, xy는 원의 중심 y 좌표이고, r은 반지름입니다.

  • 이를 표로 정리하면 다음과 같습니다.

    요소속성설명
    rectx사각형의 시작 x좌표. 좌측 상단 모서리의 x좌표
    recty사각형의 시작 y좌표. 좌측 상단 모서리의 y좌표
    rectwidth사각형의 너비
    rectheight사각형의 높이
    rectrx사각형의 x방향 라운드 코너 반지름
    rectry사각형의 y방향 라운드 코너 반지름
    circlecx원의 중심 x좌표
    circlecy원의 중심 y좌표
    circler원의 반지름
  • rect와 circle을 사용하여, 기존 아이템 리스트와 최대한 비슷하게 직접 그려봅니다.


적용 내용

  1. 리스트가 로딩 되기 전, 배경 이미지로 스켈레톤 UI 애니메이션을 적용합니다.
  • 컨텐츠 아이템을 2가지 타입의 리스트로 출력될 수 있도록 했습니다.

    • 메뉴가 확장되지 않아, 가로 컨텐츠가 5개일 때
    • 메뉴가 확장되어, 가로 컨텐츠가 4개일 때
  • 이미지의 placeholder로 스켈레톤 UI를 사용합니다.

    • 이미지가 로딩되고 있는 중일 때, 이미지를 잠시 대신해서 보여줄 rect를 추가로 만들었습니다.

      <div className="skeleton_box">
        <ContentLoader viewBox="0 0 250 140">
          <rect x="0" y="0" rx="0" ry="0" width="250" height="140" />
        </ContentLoader>
      </div>
    • 페이지를 이동해 리스트 데이터가 변경될 때, 이미지가 loading 상태나, error 상태가 될 때까지 해당 박스를 보여줍니다.


작업 결론

UX 개선

  • 외부 컨텐츠를 가져오기 위한, 개선할 수 없는 로딩 시간에 대해, 실제 컨텐츠의 레이아웃과 비슷하게 보이는 대체 컨텐츠를 먼저 보여줌으로써 UX를 개선하였습니다.
    • 페이지 이탈률과 사용자 불안감 해소에 도움

랜더링 개선

  • 브라우저 랜더링 과정에서 페이지 레이아웃이 변경되는 레이아웃 쉬프트(Layout Shift) 현상을 최소화할 수 있습니다.

UI 개선, 에러 핸들링

  • 이번 작업을 통해 외부 이미지 컨텐츠의 onError시 사용할 대체 이미지와, 로딩 시 나타날 placeholder 컴포넌트를 추가해 로딩, 에러에 대한 UI/UX를 개선하였습니다.
  • 실제로 사이트에 링크된 이미지들 중 여러 에러 이미지가 존재합니다. 어쩔 수 없이 console이 error 메시지로 지저분해지는 현상이 있습니다.

앞으로 여러 프로젝트를 진행하며, 경험이 쌓인다면😃

이번과 같이 추가될 UI 구현사항을 미리 고려하여 MUI와 같은 관련 라이브러리를 사용하거나, 어떤 라이브러리가 프로젝트에 적합한 지 경험을 통해 알 수 있을 것이라고 생각합니다.

profile
안녕하세요.

0개의 댓글