Optimizing Core Web Vitals on a Next.js app
Vercel이 개발한 Next.js는 React 메타 프레임웍으로 React 개발 경험을 향상시킨다. Next.js는 프로덕션 에서 바로 사용 가능한 수준의 앱을 만들 수 있게 해 주고. SSG, 쉬운 설정, 빠른 새로고침, 이미지 최적화, 파일 시스템 라우팅, 최적화된 코드 스플리팅 및 번들링 기능을 제공한다.
3P 의존 모듈을 사용하는 React +Next.js 앱을 최적화 하는 과정을 살펴보기 위해 우리는 Next.js 기반의 영화 앱을 만들었다. 이 앱은 TMDB의 기능을 모두 구현한 클라이언트이다. 포괄적이며 분류된 영화 목록을 검색하거나 탐색하고, 세부 정보를 확인하고, 멤버십 및 인증을 통해 개인별 즐겨찾기를 관리할 수 있는 등 다양한 기능들을 구현했다.
그 후 Next.js 영화 앱은 여러 성능 트윅을 진행하고 전반적인 사용자 경험 관점에서 유익했던 점을 식별하고 벤치마크했다. 이 글에서는 이 과정에서 이루었던 모든 성능 향상들과 각 트윅에 대해서 이야기 해 보고, 적용 결과에 대해서도 살펴보도록 하자.
누적된 성능 향상들
우리는 모든 최적화를 적용한 후 상당한 총체적 성능 향상을 달성할 수 있었다. 이는 자연스럽게 사용자 경험 향상으로 이어진다. 아래 지표들은 성능을 최적화하기 위한 모든 수정 사항을 적용하기 전과 후에 캡쳐되었다.
사용자 관점에서 이루어진 전체적인 향상들을 이해하기 위해 이어진 실제 페이지 로딩 경험 비교를 확인해 보자.
- 개선 전
- 일부 최적화 적용 후
- 모든 촤적화 적용 후
전반적인 성능 향상 외에도 관련 페이지에 대한 코드를 변경한 후 Lighthouse및 WPT를 사용하여 지표들을 캡쳐했다. 이 테스트들은 서버의 슬립 상태에 따른 어떠한 렉이나 변경 이전과 이후의 어떤 조건들을 고려해 여러 번 반복하여 수행해 신뢰할 수 있는 지표를 얻을 수 있도록 했다.
이런 내용들을 기억한 상태에서 이제 전반적인 성능 향상에 기여했던 변경 사항들이 어떻게 진행되었는지 이야기 해 보자.
대체된 패키지들
처음에 영화 앱에서 필요로 하는 각기 다른 기능들을 빠르게 구현하기 위해 여러 3P React 컴포넌트들을 사용했다. 우리는 이 3P 리소스들 모두 개별적으로 가벼운 패키지로 대체해가며 지표들을 분석해보았다. 특히 무겁거나 메인 스레드를 블록하는 것들에 대해서 진행했다.
이런 시도들의 대부분이 지표 측정치를 낮추는 데 매우 효과적이었다.
- SVG 아이콘을 사용하기 위해 Font-Awesome대신 @svgr/webpack을 사용하여 페이지 속도를 34%이상 향상시켰고, LCP는 23%, TBT는 51% 향상되었다.
- react-burger-menu를 사용하는 대신 커스텀 컴포넌트를 만들어 사용하고, react-sicky-box에서 resize-observer-polyfill을 제거하여 전체적인 번들 크기가 gzipped 기준 34.28kB 감소하였다.
- react-select 대신 react-select-search를 사용하여 LCP가 35%향상, CLS는 완전히 해결되었으며 TBT는 18% 향상되었다.
- react-slick 대신 react-glider를 사용하여 TBT가 77% 향상되었다.
- 브라우저 네이티브 smooth scrolling대신 react-scroll을 사용하여 여러 브라우저에서 스크롤 기능을 제공했다.
- react-rating 대신 react-stars를 사용하여 TBT를 33% 향상시켰다.
이어서 어떤게 어떻게 바뀌고 왜 바꿨는지 확인해 보자.
SVG 아이콘 라이브러리
영화 앱에서 필요한 모든 아이콘은 SVG를 사용하기로 했다. 처음에는 많이 쓰이고 또 CSS로 커스터마이징 한 벡터 그래픽 아이콘을 사용하려고 Font-Awesome을 적용했다.
그러나 Font-Awesome은 라이브러리 자체를 로딩할 때 전송되는 크기가 크기 때문에 로딩 속도에 영향을 준다. 이는 Lighthouse 성능 점수에도 악영향을 주고 있다.
우리는 Font-Awesome을 @svgr/webpack으로 대체했다. 또 페이지에서 여러 아이콘들을 사용하는 경우 아래 예제 코드와 같이 라이브러리 자체를 가져오는 대신 필요한 아이콘만 개별적으로 가져다 쓰도록 변경했다.
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
// replaced by
import HeartIcon from 'public/assets/svgs/icons/heart.svg'
import PollIcon from 'public/assets/svgs/icons/poll.svg'
import CalendarIcon from 'public/assets/svgs/icons/calendar.svg'
import DotCircleIcon from 'public/assets/svgs/icons/dot-circle.svg'
이는 Lighthouse점수를 전반적으로 향상시키는 데 도움이 되었다. 아래는 적용 전과 후의 점수 캡쳐 화면이다. 또 200KiB 이상의 전송량이 줄어든 것을 확인할 수 있다.
Before | After |
---|---|
앱 메뉴
앱의 초기 버전에서는 좌측 햄버거 메뉴 구현을 위해 react-burger-menu를 사용했다. 해당 컴포넌트에는 CSS스타일과 애니메이션 효과들이 포함되어 있었다. 아래는 해당 라이브러리를 사용했던 앱에 대해 번들 분석을 한 결과이다.
우린 react-burger-menu의 모든 기능이 필요 없다 판단하였고 직접 만들어 제공할수도 있을 것이라 생각했다. 이는 필요 기능에는 차이가 없지만 번들 크기를 줄이는 데 큰 도움이 되었다. 번들 분석 트리맵에서 볼 수 있듯이. 좌측 햄버거 메뉴의 청크는 변경 전 gzipped 기준 6.73kB 였으나 변경 후 879B 로 감소했다.
Parsed Size도 32.74kB에서 2.14kB로 감소했다. 해당 개선은 다운로드 시간 및 청크 구문 분석 시간 모두 줄이는 데 도움이 되었다.
정렬 기능을 위한 드롭다운
영화 앱은 영화들을 장르나 출연 배우에 따라 정렬할 수 있다. 사용자는 인기도, 추천 수, 출시일, 제목을 기준으로 영화를 정렬할 수 있는데 이전에는 react-select 컴포넌트를 사용하여 구현했다. 해당 컴포넌트는 다중 선택, 검색, 애니메이션, emotion을 활용한 스타일링을 지원한다. 컴포넌트의 번들 크기는 27.2kB이며 7개의 의존 모듈도 포함되어 있다.
하지만 영화 앱에서는 단일 선택 드롭다운 기능만 필요하고 스타일링 기능은 필요 없었으므로 이를 react-select-search 컴포넌트로 대체했다. 이 컴포넌트는 가벼우며 (gzipped 기준 3.2kB)의존 모듈도 없다. 또 필요에 따라 다중 선택과 스타일링 기능을 이용할 수 있었다.
아래는 컴포넌트의 변경과 그에 따른 Lighthouse 성능 개선으로 인한 변경 사항이다.
Before | After |
---|---|
출연진 캐러셀
우리는 React-Slick 컴포넌트를 사용하여 영화 상세 페이지에서 출연진들의 사진과 이름을 가로로 스와이프하여 볼 수 있도록 했다. React-Slick 컴포넌트는 꽤 무겁고(14.7kB) 의존 모듈도 5개나 된다.
우린 더 가벼운 react-glider 라이브러리를 사용했다. 해당 라이브러리는 번들 크기도 작고. CSS인라이닝도 지원한다.
기능의 변경 없이 번들 사이즈가 14.7kB 에서 3.4kB 로 78% 나 크게 향상되었다. 또 미래에 CSS Scroll Snap을 지원하도록 컴포넌트를 다시 만들수도 있다는 장점도 있었다.
스크롤 컴포넌트
영화 앱은 영화 목록 페이지네이션을 구현하여 여러 페이지에서 목록을 볼 수 있도록 했다. 각 이전 다음 페이지 버튼이 클릭될 때 화면은 맨 위로 스크롤이 뒤어야 한다. 이 스크롤을 부드럽게 처리하기 위해 우리는 네이티브 스크롤 옵션을 사용했다.
window.scroll({ top: 0, left: 0, behavior: 'smooth' })
// and
document
.querySelector(`.${SCROLL_TO_ELEMENT}`)
?.scrollIntoView({ behavior: 'smooth' })
하지만 smooth scroll 기능은 크로스브라우징 이슈가 있다.
수직 스크롤에 애니메이션을 적용하기 위해 react-scroll (gzipped 기준 6.8kB) 라이브러리를 사용했다. 이는 다음 비교에서 볼 수 있듯이 약간의 성능 저하는 있지만 모든 브라우저에서 부드러운 스크롤을 수행할 수 있게 되었다.
성능 지표 | FCP(s) | Speed Index(s) | LCP(s) | TTI(s) | TBT(ms) | CLS | 성능(%) |
---|---|---|---|---|---|---|---|
Before | 0.8 | 3.93 | 2.63 | 1.73 | 16.66 | 0 | 94.33 |
After | 0.92 | 3.78 | 2.9 | 2.26 | 66 | 0 | 92.8 |
% Change | 15 | 3.81 | 10.26 | 39.63 | 296.15 | 0 |
레이팅 컴포넌트
원래 사용하고 있던 React-rating은 레이팅 기능에 여러 다양한 심볼을 사용할 수 있도록 해 준다 (예: 별, 원, 엄지척 등). 하지만 영화 앱에서는 별표만 사용하고 다른 기능은 필요하지 않았다. 다른 기능을 위한 비용이 2.6kB 되었다.
react-stars 컴포넌트가 요구사항에 딱 맞는 기능만 가지고 있었다. 이 컴포넌트는 gzipped 기준 2kB 여서 우린 해당 컴포넌트를 인라이닝 CSS와 함께 사용했다.
라이브러리의 크기가 크게 차이나지는 않지만 react-rating은 컴포넌트에 SVG아이콘을 사용하는 반면, react-stars 컴포넌트는 ”★” 기호를 사용한다. 영화 목록 컴포넌트에서 컴포넌트가 20번 이상 반복되어 렌더링되다 보니 아이콘/이미지의 크기도 크기 절감에 기여하게 되었다. 이는 변경 전후의 Lighthouse 점수에서 확인할 수 있다.
Before | After |
---|---|
다른 지표들이 크게 변경되진 않았지만 TBT(33%)의 차이가 크게 벌어졌다. 이는 react-rating 컴포넌트의 청크가 메인 스레드에서 제외된 것에 기인한 것으로 보인다.
최적화를 위한 다른 기법들
위와 같이 대체 라이브러리를 찾아 적용하고 테스트하는 것은 성능 분석 및 최적화 프로젝트의 일부였다. 우린 그것 말고도 성능을 향상시키는 것으로 알려진 여러 메커니즘들도 시도해보았다. 여러 시도 들에서 효과가 있던 것과 없던 것 모두 아래에서 살펴보자.
코드 스플리팅
우리는 메뉴 컴포넌트에 대해 코드 스플리팅과 지연 로딩을 적용했다. 모바일에서는 닫힌 상태로 렌더되기 때문에 사용자가 실제로 필요할 때에만 추가적인 동작을 하게 구현할 수 있었다. 처음에는 좌측 버거 메뉴 사이드바 컴포넌트를 지연로딩 했고. 성능을 관찰해보았다. 그 다음 이를 위에서 언급한대로 커스텀 컴포넌트로 대체하였고 역시 지연 로딩을 적용하였다.
또 React의 lazy()나 suspense() 래퍼와 유사하게 동작하는 LazyLoadingErrorBoundary 컴포넌트를 적용하였다. 이로 인해 메뉴 컴포넌트는 페이지 로드 후에 로드되게 되었으며 FCP와 LCP에 큰 변화는 없지만, 다음 비교에서 볼 수 있듯이 TBT가 78% 만큼 상당히 감소했다.
성능 지표 | FCP(s) | Speed Index(s) | LCP(s) | TTI(s) | TBT(ms) | CLS | 성능(%) |
---|---|---|---|---|---|---|---|
Before | 0.86 | 4.2 | 3.46 | 2.53 | 70 | 0 | 87.66 |
After | 0.83 | 3.63 | 3.3 | 1.73 | 20 | 0 | 90.33 |
% Change | 3.48 | 13.57 | 4.62 | 31.62 | 71.42 | 0 |
중요 리소스 인라이닝과 중요하지 않은 리소스 지연 처리
Lighthouse 감사는 아래 사항에 대해 조취를 취할 것을 지속적으로 제안하였다.
CSS는 렌더 블록 리소스이기 때문에 페이지 렌더 전에 로드가 완료되어야 한다. 일부 CSS는 초기 페이지 로드에 표시되는 컨텐츠의 스타일 지정을 위해 필요한 경우가 있다. 이것은 페이지를 최적화하기 위해 인라이닝 처리 되어야 하는 중요한 CSS이다. 또한 처음에는 필요하지 않기 때문에 지연될 수 있는 CSS도 존재할 것이다.
최적화 과정의 일환으로 다크/라이트 모드 전환에 필요한 CSS를 중요한 리소스로 분류하여 인라인 처리하였다.
Next.js 문서에 따르면 node_modules 내의 일부 CSS를 /pages/_app.js
에 포함시켜야 했다. 우리는 react-glider와 react-modal-video의 CSS를 포함시켜야 했는데. 이를 _app.js에 포함시키면 앱의 모든 페이지에서 쓰지 않는데도 앱의 렌더를 차단시킬 수 있다.
각 컴포넌트가 필요한 CSS는 인라이닝 처리할 수 있다. 예를 들어 최적화 후. case 컴포넌트는 Glider 컴포넌트와 필요 CSS를 함께 가지고 있도록 리펙토링 되었다.
<div ref={ref} className='cast'>
<Glider hasArrows slidesToShow={slidesToShow} slidesToScroll={1} itemWidth={GLIDER_ITEM_WIDTH}>
{cast.map(person => <PersonLink key={person.id} person={person} baseUrl={baseUrl} />)}
</Glider>
</div>
<style jsx>{`/*CSS Classes required for Glider*/`}</style>
이로 인해 FCP, LCP, TTI 각 2% ~ 5%의 향상이 측정되었으며 페이지 성능은 79% 에서 81% 로 향상되었다.
이미지 가로세로비율 할당
지금까지 논의한 개선 사항들은 다양한 페이지에서 FCP, LCP, TBT, TTI를 개선하는데 도움이 되었다. 이제 Lighthouse 보고서의 마지막 측정 항목 개선 항목인 누적 레이아웃 이동 (CLS)에 대해서 이야기 해 보자. 이에 대한 자세히 알고 싶다면 필자의 CLS최적화를 참고하기 바란다. 최적화 적용 전 영화 상세 페이지에서 Lighthouse 보고서에는 CLS의 원인을 보여주고 있다.
CLS 점수가 0.016으로 임계치보다는 낮지만. 모바일 3g연결에서 페이지를 로드할 때에는 CLS가 발생하였다. 따라서 우리는 해당 엘리먼트에 최적화 작업을 진행해 CLS가 발생하지 않도록 하였다. 이미지의 사이즈를 직접 지정하는 대신, aspect-ratio-boxes 기법을 적용하였다. 이로 인해 이미지가 들어갈 공간을 미리 확보하여 이미지가 로드 완료되었을 때 레이아웃이 움직이는것을 예방할 수 있다. 이 기법을 활용하여 CLS점수를 0으로 만들었다. 해당 측정 제안이 사라졌고 사용자 경험이 눈에 띄개 개선되었다.
노트: 브라우저가 aspect-ratio를 지원하면 더 좋지만 아직 그 기능이 폭넓게 구현되지 않아 위의 방법을 사용했다.
Preconnects
Preconnects는 브라우저에게 페이지에서 곧 사용될 리소스들에 대한 힌트를 줄 수 있는 기능이다. rel="preconnect"
를 사용하면 브라우저는 다른 도메인에 대한 연결을 미리 만들어 빠르게 처리할 수 있도록 해 둔다. 이 힌트는 연결을 미리 준비하기 때문에 리소스 로드 속도를 개선시킬 수 있다.
우리는 아래와 같이 preconnect 속성을 리소스들에 추가하였다.
<link rel='preconnect' href='https://image.tmdb.org' />
<link rel='preconnect' href='https://api.themoviedb.org' />
<link rel='preconnect' href='https://www.google-analytics.com' />
<link rel='preconnect' href='https://content-autofill.googleapis.com' />
아래 표와 같이 큰 차이는 없지만 식별 가능한 수준의 향상이 관측되었다.
성능 지표 | FCP(s) | Speed Index(s) | LCP(s) | TTI(s) | TBT(ms) | CLS | 성능(%) |
---|---|---|---|---|---|---|---|
Before | 0.9 | 3.9 | 3.43 | 2.93 | 60 | 0 | 88 |
After | 0.83 | 3.5 | 2.86 | 2.63 | 53.33 | 0 | 93.33 |
% Change | 7.77 | 10.25 | 16.61 | 10.23 | 6.67 | 0 |
API호출 순서 최적화
영화 앱은 TMDB의 클라이언트이기 때문에 영화 목록, 장르, 출연자, 연관 이미지 등의 정보를 얻기 위해 여러 API를 호출한다. API호출 순서 최적화르 위해 사용되는 원칙은 페이지의 메인영역을 위한 데이터를 받아오는 것이 다른 API호출때문에 지연되지 않도록 하는것이다. 이를 염두에 두고 순서를 아래와 같이 변경하였다.
순 서 | Before | After |
---|---|---|
1 | 영화 포스터에 대한 API를 호출하는 중에 장르나 구성에 대한 메타데이터를 받아오고 있어서 포스터 API가 지연된다 | 사이드 메뉴에 필요한 메타데이터와 영화 포스터 데이터를 동시에 조회한다 |
2 | 영화 포스터 데이터를 받아온다 | 다운로드 받은 영화 포스터 데이터로 홈 페이지를 렌더한다 |
3 | 다운로드 받은 영화 포스터 데이터로 홈 페이지를 렌더한다 |
API응답 preload
사용자가 영화 앱에 처음 방문했을 때 영화 API를 유명한 순으로 1페이지를 호출할 것이라는 것을 알고 있다. 실제 목록은 TMDB API로부터 받아오는데 이 때 두 가지 파라미터 장르 = “유명한”, 페이지 = “1” 를 전달하게 될 것이다. 따라서 해당 파라미터를 포함하는 API를 아래와 같이 preload 처리할 수 있다.
<link
rel="preload"
as="fetch"
href="https://api.themoviedb.org/3/movie/popular?api_key=844dba0bfd8f3a4f3799f6130ef9e335&page=1"
crossorigin="true"
/>
이 코드는 사용자가 다른 페이지에서는 무엇을 할 지 예측할 수 없기 때문에 홈 페이지에서만 사용하게 된다. 만약 사용자가 해당 데이터를 사용하지 않으면 불필요하게 리소스를 낭비하는 것으로 크롬 개발자 도구에서 알려준다. - “The resource https://api.themoviedb.org/3/movie/popular?api_key=844dba0bfd8f3a4f3799f6130ef9e335&page=1 was preloaded using link preload but not used within a few seconds from the window’s load event. Please make sure it has an appropriate as value and it is preloaded intentionally.”
이로 인해 LCP와 TTI는 각각 12%, 7.76% 증가하였고 전체적인 성능 증가는 91% 에서 94% 로 개선되었다.
TMDB 로고와 트레이드마크 preload
TMDB의 로고와 트레이드마크는 모든 페이지에서 보여지므로 preloading을 적용할 수 있다. 미디어 쿼리를 사용하여 preload처리하였다.
<link
rel="preload"
href="{LOGO_IMAGE_PATH}"
as="image"
media="(min-width: 80em)"
/><link
rel="preload"
href="{DARK_TMDB_IMAGE_PATH}"
as="image"
media="(prefers-color-scheme: dark) and (min-width: 80em)"
/><link
rel="preload"
href="{LIGHT_TMDB_IMAGE_PATH}"
as="image"
media="(prefers-color-scheme: light) and (min-width: 80em)"
/>
결과적으로 5~6% 정도의 FCP 및 Speed Index 속도가 개선되었다.
사이트를 반응형으로 만들기
영화 앱은 UI 래퍼를 렌더하기 위해 Next.js 의 SSR을 사용한다. 앱이 데스크톱과 모바일에서 모두 접속할 수 있기 때문에 반응형 디자인이 필수적이다. 그런데 반응형 디자인과 SSR을 조합하는게 조금 까다로운데. 이유는
- 컨텐츠가 렌더링 되는 서버에서는 클라이언트의
window
요소를 알 수 없다. 따라서 클라이언트의 상세 정보를 알기 위해 window.matchmedia()같은 것을 사용할 수 없다. 또 클라이언트 힌트는 모든 브라우저에서 지원하지 않는다. - CSS 미디어 쿼리를 사용할 경우 데스크탑이던 모바일이던 상관 없이 모든 요소가 렌더링된다.
이런 문제들을 해결하기 위해 우리는 @artsy/fresnel 라이브러리를 사용했다. 서버에서 특별한 CSS breakpoint와 함께 렌더링을 양쪽 다 하긴 하지만 breakpoint에 매칭되는 컴포넌트만 마운팅된다. 따라서 중복 마크업과 불필요한 렌더링을 피할 수 있었다. 다음 이미지는 동일한 컨텐츠에 대해 변경 전화 후의 마크업 차이를 나타내고 있다.
Before | After |
---|---|
다음은 변경 후 관찰된 Lighthouse 성능의 변화이다.
성능 지표 | FCP(s) | Speed Index(s) | LCP(s) | TTI(s) | TBT(ms) | CLS | 성능(%) |
---|---|---|---|---|---|---|---|
Before | 0.93 | 3.73 | 2.6 | 2.63 | 60 | 0.001 | 94.33 |
After | 1.06 | 3.23 | 2.66 | 2.66 | 63.33 | 0 | 95 |
% Change | 13.97 | 13.4 | 2.3 | 1.14 | 5.55 | 100 |
FCP, LCP, TTI, TBT에서 약간의 성능 감소가 있었지만 Speed Index와 성능이 향상되었다. @artsy/fresnel 사용으로 청크 크기가 증가했지만 마크업을 줄이면 좋은 절충안이 될 수 있다.
구글 애널리틱스 적용
구글 애널리틱스를 포함하여 사이트가 사용자와 어떻게 소통하고 있는지를 알 수 있도록 했다. 다만 이로 인해 약간의 성능 저하가 예상되었다. 코드 변경에 따른 성능의 변화를 추적하기 위해 여러 프로세스들에 대해 변화량을 캡쳐했다. 예상대로 추가 컴포넌트로 인해 약간의 성능 저하가 있었다.
성능 지표 | FCP(s) | Speed Index(s) | LCP(s) | TTI(s) | TBT(ms) | CLS | 성능(%) |
---|---|---|---|---|---|---|---|
Before | 0.8 | 3.4 | 2.53 | 1.8 | 26.66 | 0.001 | 95.66 |
After | 0.95 | 3.7 | 2.93 | 2.13 | 35 | 0 | 92.75 |
% Change | 18.75 | 8.82 | 15.61 | 18.05 | 31.28 | 0 |
도움이 되지 않았던 항목들
Lighthouse 리포트의 피드백에 따라 여러 대체 항목과 아이디어들을 시도해 보았지만 성능상의 이점이 없어 포기한 것들도 있다. 아래는 그 요약이다.
- 이미지 지연로딩을 위해 react-lazyload 패키지를 사용하고 있었다. 그런데 이 것이 스크롤과 레이팅 컴포넌트와 함께 메인 스레드가 오랫동안 실행되도록 만들었다.
우리는 이것을 브라우저 네이티브 지연 로딩으로 대체했고 후속 테스트를 통해 TBT 가 10ms에서 117ms 로 증가하여 LCP가 무시할 수 있는 수준으로 감소했음을 확인했다. 네이티브 이미지 지연 로딩은 뷰포트 근처의 몇 이미지를 로드하는 반면 react-lazyload는 뷰포트 내에 있는 이미지만 로드하여 TBT에서 이런 차이를 일으킨 것으로 보인다.
요즘에는 이런 기능을 위해 Next.js Image Component를 활용할 수 있다.
- 이미지에 aspect-ratio를 지정하기 전에 CLS이슈를 해결하기 위해 이미지에 크기를 지정했다. 그 방법도 CLS를 감소시키기 위해 제안되는 방법이긴 하지만 aspect-ratio 설정이 없이 그 방법만으로는 이슈가 해결되지 않았다.
- LCP를 줄이기 위해 SSR을 도입했지만 오히려 성능이 감소되었다. 이는 페이지를 렌더링하는데 필요한 영화데이터를 TMDB API호출을 통해 가져오기 때문일 수 있다. 이로 인해 서버에서 API요청/응답이 처리되는 시간이 늘어나 응답이 느려진 것이다.
도움이 될 수 있는 아이디어들
아래 항목들은 미래에 시도해 보려고 하는 성능 향상 항목들이다. 여기에는 개별 컴포넌트를 더 가벼운 것으로 대체하는것 부터 본격적으로 SSR을 구현하는 것까지 다양하게 있다. 다음은 앱의 성능에 긍정적인 영향을 미치는지 확인하기 위해 확인해볼 수 있는 사항들이다.
- preloading을 포함한 반응형 이미지 구현에 관련된 논의
- 서비스 워커를 이용한 캐시
- 현재 _app.js에는 redux 관련된 액션, 리듀서와 같은 로직들이 불필요하게 포함되어 있다. 개별 페이지들에 방문했을 때 이런것들이 다 필요없는데도 말이다. 우리는 redux 로직을 위한 코드 스플리팅을 통해 이런 코드들을 제거하려고 하고 있다.
- redux 없이 SSR을 구현하고 SSR캐싱을 적용한다.
- react-modal-video를 가벼운 대체 패키지로 바꾼다.
- react-slider 대신 keen-slider를 사용한다.
- react-lazyload대신 react-cool-inview를 사용한다.
- 다양한 React loading pattern을 활용하여 3P 라이브러리들에 지연 로딩 및 코드 스플리팅을 적용한다.
- Image-post-precessing을 사용하여 hero image같은 것을 preload처리한다.
- SVG로딩 스피너를 CSS애니메이션으로 대체한다.
- 이미지 렌더링에 내부 자바스크립트를 사용하는 컴포넌트 대신 HTML과 CSS를 활용하는 가벼운 컴포넌트를 사용한다.
결론
성능 최적화는 지금까지도 지속되고 있는 프로세스이다. 지난 6개월 동안 우린 이런 변경사항들을 적용하여 많은 모법 사례들을 테스트 해 보았다. 사실 얼마든지 더 할수도 있긴 하지만 어느 시점에서는 성능 최적화보다 다른 대안을 찾는 것이 나을 수 있다. 사이트에 새로운 기능이 추가될 때 마다 성능 검토는 반복되어 이뤄질 것이다. 그러나 우리는 이런 과정에서 얻은 교훈들을 캡쳐하여 이 글을 읽는 분들 뿐만 아니라 미래를 위한 매뉴얼 역할을 하고 싶었다.
이 글에 기여한 Anton Karlovskiy와 Leena Sohoni-Kasture에게 특별한 감사를 전합니다.