🔖 현재 자사의 서비스는 이미지를 서버에 업로드하고, 직접 보관하는 것이 아닌, 원 사이트의 이미지 링크를 통해 접근하는 방식을 사용하고 있습니다. 따라서 이미지의 용량을 줄이는 것이 불가능합니다.
데이터 패칭 과정을 통해 확인한 결과, 특정 이미지의 로딩 시간이 길어 UX가 저하되는 현상을 확인하였고, 그 원인으로 링크된 이미지 중 파일 크기가 10MB이상, 또는 가로 크기가 4000px 이상인 대용량 이미지가 있음을 알게되었습니다.
람다 등을 사용해 이미지 사이즈를 직접 줄일 수는 없는 현재 상황에서 페이지 로딩의 개선을 위해 썸네일 이미지 로딩의 UX 최적화가 반드시 필요하였고, 이틀 동안 관련 작업을 진행하였습니다.
🔖 React 프로젝트에서 이미지의 src가 바뀌며 이미지가 로딩될 때까지 이전 이미지가 그대로 남아있음으로서 오히려 페이지 전환이 어색하게 느껴지는 현상이 있었습니다.
페이지의 변경이 있을 때, 사용자에게 페이지 변환을 통해 게시글의 변경이 있음을 자연스럽게 알려주기 위해 기존 이미지를 지우고, 로딩을 시작하는 단계를 시각적으로 알리는 과정, 말하자면 인위적인 로딩, 깜박임이 필요합니다.
🔖 레이아웃 쉬프트(Layout Shift)는 웹 페이지에서 레이아웃이 예기치 않게 변경되는 현상을 의미합니다. 일반적으로 사용자가 페이지를 로드하는 동안 요소들이 이동하거나 새로운 요소들이 추가됨으로써 발생합니다. 이는 사용자 경험을 저하시키는 요소 중 하나입니다.
다음 경우에 스켈레톤 UI를 사용하여 사용자 경험 개선을 노려볼 수 있습니다.
<Wrap>{!loading && <ItemList rowGap={40} listData={fetchData} />}</Wrap>
react-content-loader
react-loading-skeleton
react-content-loader
라이브러리를 사용합니다.setLoading(true)
를 추가합니다.최초 랜더링 시, 데이터가 loaded되었는지 여부만 확인했던 기존 구현과 달리,
매 패칭 시 loading여부를 확인하고, 스켈레톤 UI를 랜더링하겠습니다.
useEffect(() => {
setLoading(true);
(function (){
// 리스트 패칭이 완료되었다면
setLoading(false);
})()
}, [location.pathname, location.search]);
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>
);
}
설명 | 예시 | |
---|---|---|
1 | HTML에 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" /> |
3 | CSS로 스타일링 | fill: 채우기 색상; stroke: 선 색상; stroke-width: 선 굵기; |
<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>
요소 | 설명 |
---|---|
<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은 반지름입니다.
이를 표로 정리하면 다음과 같습니다.
요소 | 속성 | 설명 |
---|---|---|
rect | x | 사각형의 시작 x좌표. 좌측 상단 모서리의 x좌표 |
rect | y | 사각형의 시작 y좌표. 좌측 상단 모서리의 y좌표 |
rect | width | 사각형의 너비 |
rect | height | 사각형의 높이 |
rect | rx | 사각형의 x방향 라운드 코너 반지름 |
rect | ry | 사각형의 y방향 라운드 코너 반지름 |
circle | cx | 원의 중심 x좌표 |
circle | cy | 원의 중심 y좌표 |
circle | r | 원의 반지름 |
rect와 circle을 사용하여, 기존 아이템 리스트와 최대한 비슷하게 직접 그려봅니다.
컨텐츠 아이템을 2가지 타입의 리스트로 출력될 수 있도록 했습니다.
이미지의 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 상태가 될 때까지 해당 박스를 보여줍니다.
이번과 같이 추가될 UI 구현사항을 미리 고려하여 MUI와 같은 관련 라이브러리를 사용하거나, 어떤 라이브러리가 프로젝트에 적합한 지 경험을 통해 알 수 있을 것이라고 생각합니다.