teklog

Next.js 성능 최적화

2023/07/09

n°37

category : Next.js

img


회사에서 서비스 리뉴얼 이후, 또다시 리뉴얼에 가까운 보수 작업이 있었다. 3가지 이유가 있었다. 1 기획 변경으로 큰 변화가 있었고, 2 심각한 버그가 연이어 터졌으며, 3 코드 컨벤션에 맞지 않거나 비효율적인 로직을 리팩터링 해야했기 때문이다. 22년 4분기에서 23년 1분기는 새 프로젝트를 론칭하느라 정신없었다면, 23년 2분기는 뜯어 고치느라 정신이 없었다.



img

내가 지운 코드가 우리팀에서 제일 많았다..


고된 작업이지만, 남는 것도 많아 기억할 만한 부분만 간단히 메모.



성능 최적화의 체크리스트


  • 불필요한 로직
  • 불필요한 파일
  • 데이터 fetching과 렌더링
  • 캐싱



불필요한 로직


코드의 가독성, 로직의 간결함 뿐만 아니라 성능과도 연계될 수 있다. 여러 곳에서 동일한 결과를 보여주어야 하는 간단한 카운트다운 로직임에도 추상화가 낮은 코드가 있었다. 가독성이 떨어져 원리를 파악하는데 시간이 걸렸다. 남겨두면 추후에 문제가 반복될 것 같아, 아예 공용 카운트다운 함수를 다시 만들었다. 어처구니 없게도 이미 카운트다운 패키지를 사용하고 있었다. 코드를 만든 작업자가 패키지를 확인하지 않은 것인지 모르겠지만, 좋지 않은 선택이었다. 결국 패키지에 내장된 카운트다운 로직에 끝나는 타겟 날짜의 params만 받도록 공용 컴포넌트를 만들었고, 이 카운트다운 컴포넌트를 여러 곳에서 재사용할 수 있었다.


 로직은 최대한 간결하게 작성하는 것이 좋겠다. 위 사례의 기존 카운트다운 함수에는 setTimeOut, let 재할당, for문이 사용되었다. 스코프가 복잡해 가독성도 떨어졌지만, 잦은 버그와 느린 실행 시간, 여러 파일에서 중복된 로직의 사용으로 성능 상 크게 좋지 않았다. 



불필요한 파일


불필요한 패키지 설치, import, 파일 생성, 함수/변수 생성은 하지 않는 게 좋다. 단순히는 번들링 사이즈가 커지기 때문이다. 별거 아닌 것처럼 보여도 쌓이면 개선하기 까다롭다. 특별한 규칙 없이 마구잡이로 설치된 패키지나 선언된 함수/변수는 코드 일관성에 좋지 않다. 중복된 유틸이나 패키지에 너무 많은 컴포넌트가 의존하다 보면, 시간이 지나면서 혼돈은 계속 자라난다. 리팩터링 작업에서 불필요한 것은 모두 지우고, 사용되지 않는 패키지, 유틸 등을 모두 지우니, 그 후로 작업하기도 수월해졌다.




데이터 fetching과 렌더링


서버사이드 렌더링이 널리 쓰이면서 신경쓸 부분이 많아졌다. 기획이나 api 성능에 따라 서버사이드/클라이언트 사이드 중 어느 쪽에서 api를 호출하는 것이 나을지 판단해야한다. 그리고 가능하다면 SSG나 ISR을 활용한다. 서버사이드 렌더링으로 데이터를 fetch할 경우에 상황에 따라 초기 서버 응답속도가 많이 느려질 수 있기 때문이다. 빌드 타임에 미리 렌더링된 페이지를 불러오는 게 나을 수 있다. 최근 Next.js 버전에서는 SSR에서도 서스팬스를 사용할 수 있어 streaming을 할 수 있다고 한다. 이 영상에 잘 설명해주셨다. 서스팬스를 활용하여 TTFB를 줄이면서 기존 ssr의 이점은 가져가는 방법도 있게 되었다.



캐싱


캐싱을 적극 활용한다. 특히 대용량 이미지와 같은 LCP 개선에도 좋지만, 세밀하게 캐싱을 설정해두면 많은 부분 성능 개선을 달성할 수 있다. 시간을 내서 거의 모든 페이지의 서버사이드 캐싱, 리액트 쿼리 캐싱, 다국어 캐싱까지 적용했는데 극적인 효과를 볼 수 있었다. (아주 극적인 케이스로는 17s -> 10s -> 0.2s로 개선하였다. 다행히도 릴리즈 이전이었다.) 캐싱된 요청은 네트워크탭에서 (cached from memory)라고 적히며 로딩 속도가 0s인 걸 확인할 수 있다. 워터폴 또한 발생하지 않고 캐싱된 수많은 데이터가 병렬적으로 로드된 걸 볼 수 있다.

 한가지 주의점은 실시간 혹은 일정 주기마다 업데이트해야 하는 데이터나 비밀번호를 포함한 여러 post 요청에는 캐시 설정에 유의해야 한다. cacheTime, staleTime, revalidate 등의 추가적인 설정이 필요하다. 



개선을 위한 도구들


  • 개발자 도구 
  1. 라이트하우스
  2. 퍼포먼스
  3. 커버리지



라이트하우스

성능(속도)과 SEO, 접근성 등 다양한 지표를 확인할 수 있는 크롬의 성능지표 도구이다. FCP, LCP, CLS 등 속도를 확인할 수 있는 각종 지표를 제공한다. 진단 결과에서 개선사항을 꽤 구체적으로 알려주는 편이어서, 이를 최대한 반영하면 좋다. View Original Trace 버튼을 클릭하면 Performance 탭으로 이동한다. 라이트하우스 검사를 바탕으로 성능 데이터를 살필 수 있다.


img


퍼포먼스

img

라이트하우스에서 확인한 지표들을 시간별로 상세히 볼 수 있다. 어떤 순서로 로드가 되는지, 얼마나 걸리는지, 어디서 병목이 발생하는지, 뎁스까지 확인해볼 수 있다. 라이트하우스 진단으로 최적화 범위나 대상을 정확히 알기 힘들 때 퍼포먼스탭을 살피며 찾아갈 수 있다.


img

오래 걸리는 작업에는 다음과 같은 메시지로 확인할 수 있다. 하단의 탭을 참고해가며 어느 단계에서 시간이 제일 많이 소모되는지 확인해보자. 예를 들어 페이지에서 시간 지연이 발생하는 스크립트를 탐색할 수 있을 것이다. 혹은 데이터를 가공하는 for문에서 시간지체가 발생한다면, 해당 로직에 문제가 있다는 사실을 알 수 있을 것이다. 위 이미지에선 과도한 이미지 로딩과 html 파싱 과정에서 스크립트를 컴파일 하는 과정이 오래 걸리는 것으로 보인다. 그렇다면 컴파일 속도를 줄일 방법을 찾아 최적화를 진행하면 될 것이다.


커버리지


img


커버리지 탭에선 사용되지 않는 스크립트를 알 수 있다. 항목 클릭 시, source 탭에서 해당 항목의 코드로 이동한다. 무턱대고 unused bytes가 높은 스크립트를 삭제해선 안된다. 이벤트 핸들러처럼 커버리지 기록 시 사용되지 않은 코드도 있기 때문이다. 라이트하우스, 퍼포먼스와 함께 확인하는 용도로 사용한다. 앞서 얘기한 불필요하게 커진 스크립트에서 사용하지 않는 패키지, 함수, 변수 등을 찾아볼 수 있을 것이다.




  • 빌드 분석툴 (@next/build-analyzer)


img

@next/bundle-analyzer


번들링 사이즈를 분석해주는 툴이다. README에 적힌 대로 설치를 진행한 이후, ANALYZE=true npm run build를 입력하면 위처럼 프로젝트의 번들링 사이즈를 상세히 알 수 있다. 불필요하게 용량을 차지하고 있는 라이브러리나 페이지, 컴포넌트를 확인할 수 있다.



결론


최적화는 시간과 인력 등 한정된 자원 때문에 후순위로 밀려나기 쉬운 것 같다. 그렇다면 처음부터 성능을 고려하여 좋은 코드를 만드는 것이 현실적이겠다. 불필요한 의존성은 제거하고, 적절한 추상화를 이룬다. 실행 시간을 많이 잡아먹거나 반복되는 로직은 줄이려고 노력할 필요가 있다. 잘 작성된 코드는 리팩터링 또한 수월할 수 있기 때문이다.


읽어주셔서 감사합니다.