기능 구현이 끝난 후 프론트엔드 성능이 사용자 경험에 영향을 미칠 수 있다는 것을 알고, 서비스의 품질 향상을 위해 LightHouse와 번들 분석기를 이용해 성능 지표를 측정하며 개선하고자 했습니다. LightHouse를 통해 성능을 측정하였을 때, FCP와 LCP 지표가 낮게 나온 것을 확인하였고, 이를 개선하였습니다. 먼저 해당 지표들의 의미를 알아보고, 개선점을 살펴보겠습니다.
FCP와 LCP
FCP(First Contetnful Paint)는 유저가 페이지에 처음 진입했을 때, 콘텐츠가 처음 렌더되기 시작하는 시점을 뜻합니다. 즉, 페이지 콘텐츠가 모두 렌더 되지 않아도 일부가 렌더 되기 시작했다면, 이 시점을 측정합니다. 이때 콘텐츠는 이미지나 텍스트, <svg> 요소, 내용이 있는 <canvas> 요소를 뜻합니다.
이에 반해 LCP(Largest Contentful Paint)는 뷰포트에서 가장 크고 주요한 내용이 화면에 렌더링 완료되는 시점을 결정합니다. web.dev에 따르면, 좋은 사용자 경험을 위해서는 FCP는 1.8초 이내, LCP는 페이지가 로드를 시작할 때 2.5초 이내에 발생해야 합니다.
FCP와 LCP를 개선하기 위해서는 최대한 빠르게 로딩을 시작할 수 있는 환경을 만들어야 합니다. 따라서, 초기에 페이지를 렌더링 할 때 로드하는 자바스크립트 번들의 사이즈를 줄이고, 이미지를 포함하고 있는 경우, 이미지 레이지 로딩을 통해 이미지 로드의 부담을 줄이고자 했습니다. 마지막으로, 페이지 전환 또는 새로운 콘텐츠 로드가 예상되는 경우, preload 및 prefetch를 활용해 미리 최대한 빠르게 로드를 시작하도록 하였습니다.
번들 사이즈 최적화
번들사이즈 감소
vite-bundle-analyzer를 통해 애플리케이션 시작 시 로딩하는 파일들의 크기를 분석하여 불필요한 패키지들을 정리하고, 큰 부분을 차지하던 Lodash의 경우, 트리쉐이킹을 위해 default import로 필요한 함수만 받아오도록 변경하였습니다. lodash-es는 번들이 더 커지는 문제가 있어 사용하지 않았습니다.
dynamic import 적용
페이지 기반 dynamic import를 통해 필요한 시점에 자바스크립트를 로드하도록 했습니다. 이를 통해 초기에 로드하는 자바스크립트 번들 사이즈를 감소시켰습니다. 초기에는 페이지 및 node_modules 하위 패키지들을 모두 분할했지만, 네트워크 통신이 늘어나 오히려 로딩 속도가 느려지는 문제가 있었습니다. 따라서, 이를 삭제하고 페이지 기반으로만 분할하여 초기 화면에서 필요한 코드만 동적으로 import 하도록 했습니다.
이미지 레이지 로딩
이미지의 경우, 뷰포트에 렌더링되는 이미지만 고화질로 로드하도록 레이지 로딩을 적용하였습니다. 우선적으로 loading=lazy를 활용하고, 이를 지원하지 않는 브라우저를 위해 IntersectionObserver를 사용한 LazyImg 컴포넌트를 제작하여 활용하였습니다. useObserver라는 커스텀 훅에서 옵저버를 생성하여 공유하고, 뷰포트와 intersecting 하는 경우, 이미지의 src를 dataset.src로 변경하였습니다. 초기 src는 저해상도를, dataset.src에는 고해상도의 이미지를 저장했습니다.
이때, 유의할 점은 초기 화면에 나오는 이미지는 레이지 로딩하지 않아야한다는 것입니다. 특히, LCP 이미지는 레이지 로드하면 안 되는데, 불필요한 리소스 로드 지연을 유발하기 때문입니다. 따라서 캐러셀의 첫 페이지가 아닌 경우에만 LazyImg 컴포넌트를 사용하였습니다.
화면 사이즈에 따라 index를 활용해 일정 카드 UI까지만 eagarly loading 하고 그 이후는 lazy loading 하도록 하였습니다. 이외에도 옵저버를 각 카드 UI에 붙여서 판단하거나, 실제로 뷰포트를 계산해 내부에 있는지 확인하는 방법도 있었지만, 옵저버를 카드 개수만큼 생성하고 제거하는 것이 비효율적이고, 매번 계산하는 비용이 있기 때문에 해당 방법을 선택하였습니다.
Prefetch 및 Preload 적용하기
preload와 prefetch는 필요한 리소스를 미리 다운로드하는 최적화 방법입니다. 둘의 차이를 살펴보자면, preload는 현재 페이지에서 즉시 필요로 하는 리소스를 우선적으로 가져오는 작업이고, prefetch는 다음 페이지 이동과 같이 사용자가 곧 사용할 리소스를 미리 가져오는 작업입니다.
페이지 기반으로 React Router의 loader를 적용해 페이지 진입 이전에 데이터를 prefetch하고, 컴포넌트 내부에서는 TanStack Query의 useQuery를 이용해 캐싱된 데이터를 사용합니다. 이를 통해 페이지 진입 시 초기 렌더인 경우 loader를 사용해 빠르게 데이터를 가져오고, 리렌더하는 경우 캐싱된 데이터를 사용합니다. 검색의 경우, 추천 검색어에 hover 할 때 해당 검색 결과를 prefetch 하여 유저가 실제로 클릭할 시에 캐싱된 데이터를 사용하도록 했습니다. 이때 Suspense를 사용해 비동기 요청과 화면 렌더링을 동시에 시작하도록 하여 render-as-you-fetch를 구현했고, 사용자에게 최대한 빠르게 화면을 출력하면서 데이터를 가져올 수 있었습니다. 이 부분은 여기에서 조금 더 자세히 다루고 있습니다.
💭 React Router의 loader과 TanStack Query의 Prefetching
loader와 prefetchQuery 모두 데이터가 필요한 시점 이전에 리소스를 로딩하여 사용자 경험을 향상합니다. 이를 통해 애플리케이션이 빠르고 사용자의 동작에 대해 더 빠르게 반응하도록 합니다.
라우팅과 연동해 라우트 기반으로 미리 리소스를 로드하고 싶다면, React Router의 loader를 사용할 수 있습니다. 만약 데이터 fetching, 캐싱, 업데이트, 사용자 인터랙션처럼 다양한 조건에서 데이터를 prefetch 하기를 원하는 경우 TanStack Query가 강력한 해결책이 될 수 있습니다.
둘의 이점을 모두 활용하는 방법도 있는데, 앞서 설명한 최적화 방법처럼 loader를 사용해 라우트 기반으로 데이터를 로드하고, TanStack Query로 좀 더 세부적인 prefetching과 데이터 관리를 하는 방법입니다.
끝내며
사용자 경험 향상을 위한 웹 성능 관련 지표를 살펴보고 이를 개선하는 작업을 진행하면서 페이지에 필요한 네트워크 요청 시 로딩을 최적화하기 위해 고려해야 할 요소들을 알게 되었습니다. 이를 통해 FCP 0.6초, LCP 1.5초 개선하여 초기 로딩 속도를 단축하는 성과를 도출했습니다. 글을 시작할 때 보았던 web.dev에서 말하는 좋은 사용자 경험을 위한 FCP 및 LCP 지표를 만족하여 뿌듯했습니다.
이 과정에서 상황에 따른 적절한 처리가 중요하다는 것을 다시 한 번 생각해 보는 계기가 되었습니다. 예를 들어, 이미지 레이지 로딩은 초기 로딩 시 필요한 이미지만을 로드하여 성능을 향상할 수 있지만, LCP인 경우 사용하지 말아야 합니다. 또 React Router의 loader는 페이지 전환 이전에 필요한 데이터를 가져올 수 있지만, 렌더링 시 매번 fetch를 한다는 단점이 있고, 이를 보완하기 위해 TanStack Query의 ensureQueryData와 useQuery를 사용할 수 있습니다. 그리고 요소를 prefetch 할 때 사용자에게 빠르게 반응하는 페이지를 보여줄 것인지, 또는 이미지가 모두 준비된 매끄러운 UI를 제공할 것인지에 따라 Promise를 await 할 지의 여부를 결정할 수도 있습니다. 이렇듯 상황에 따른 적절한 최적화 방법을 찾아 사용자에게 쾌적하고 만족스러운 사용 경험을 줄 수 있는 방법을 찾아나가야 할 것입니다.
마지막으로, 아쉬웠던 점은 현재 third-party api를 사용하고 있기 때문에 성능에 큰 영향을 미칠 수 있는 이미지 압축, webP, AVIF와 같은 웹 페이지에 최적화된 형식을 사용하지 못했던 점입니다. 추후 백엔드에서 간단한 작업뿐만 아니라 이미지를 직접 관리한다면 더 효율적인 개선이 가능할 것으로 보입니다.
'Projects' 카테고리의 다른 글
[NutriNotes] 프론트엔드 개인 프로젝트 최종 회고(프론트&백&DB&AWS배포까지 완!료!) (13) | 2023.11.27 |
---|---|
[NutriNotes] 시맨틱 태그 적용하기 (0) | 2023.08.22 |
[NutriNotes] 프론트엔드 프로젝트 중간 회고 (2) | 2023.08.03 |