devjaewon.com

Next.js 기반 웹 성능 개선

by Jaewon Kim

최근 지인으로부터 Next.js로 개발된 프로젝트의 성능 개선을 부탁 받았다. 미국에서 운영하는 웹 서비스를 개편하고 팀을 리빌딩 하면서 PHP에서 Next.js로 전환 했는데 원하는 성능까지 개선하는데 어려움을 겪고 있는듯 보였다.

1

현황 파악

먼저 현재의 개괄적인 성능지표를 파악하기 위해서 Lighthouse 를 사용했는데, 로드 성능은 FCP 5.2초, LCP 12.7초가 걸리고 있었으며 총점 26점으로 생각보다 심각한 성능을 보여주고 있었다.

2

문제는 성능도 성능이지만 SEO에도 치명적인 영향을 끼치는 것으로 보였다.

서비스 BM 상 SEO = 매출이 되는 서비스였는데 SEO 관련 메타정보는 개편시 모두 동일하게 적용되어 있었고 영향을 줄만한 부분이 성능만 남은 상황에서 SEO 지표가 반 넘게 계속해서 떨어지는 추세을 보여주고 있었다. (아마 이 부분 때문에 도움을 요청한게 아닐까 싶다)

3

Next.js의 일부 기능은 과한 최적화라고 여겨질 정도로 많은 최적화가 기본적으로 탑재되어 있기 때문에 어떻게 성능이 이렇게까지 안좋을 수 있을까? 라는 의문과 함께 오밀조밀 살펴보기 시작했다.

최적화란

내가 무엇부터 시작해야할지 생각해보기 위해서 먼저 최적화를 한다는게 무엇인지 생각해봤다. 최적화란 특정 지표가 고객이 원하는 수준까지 도달하지 못할 때, 해당 프로세스를 specific 하게 개선함으로써 목표 수준에 도달하는 것을 의미한다고 생각하는데

어떤 지표를 세울지는 고객의 니즈를 보고 결정하면 되고, 방법론을 선택함에 있어서의 접근은 일반적으로 두가지만 집중하면 된다고 생각한다.

  1. 병목지점을 확인하기
    • ex. 의존성, 동시성, 병렬성, ... 등
  2. 연산량 줄이기
    • ex. 파싱 및 인코딩, 시간복잡도, 캐싱, ... 등

병목지점 찾기

작업단위를 하나하나 분해해서 보기 위해 Chrome DevTools를 사용했다.

병목 A. FCP와 LCP가 동시에 일어나는 기현상

4

SSG 환경에서 window resize 이벤트를 이용해 요소를 조건부 렌더링 하는 부분과 Media Query 내에 root 요소에display: none을 주는 부분이 교묘하게 맞물려서 전체 첫 렌더트리 생성을 막는 것이 가장 큰 문제였다.

또한 static generation은 pc로 해두고 모바일에서 js를 통해 렌더링 하다보니 오히려 하드웨어 스펙이 더 떨어지는 모바일에서 재 렌더링 및 hydration 비용으로 인해 더 느려지는 것도 문제가 되고 있었다.

어떤 해결책이 서비스에 적합할까?

문제 자체를 해결할 수 있는 방법은 다양했는데, 그 중 현재 서비스 형태에 적합한 방법이 무엇일지 고민이 되었다. 결론부터 말하자면 가장 좋은 방법은 PC와 모바일의 환경이 아예 분리된 두 버전의 SSG를 운용하되 소스코드는 재사용 가능한 형태로 만드는 것이라 생각했다.

PC와 모바일이 서로 다른 UI/UX를 가지고 있어서 View Layer에서는 하나의 소스로 커버하기 어려운 구조였지만 Service Layer는 재사용되는 부분이 많았고, 그렇다고 한쪽을 타협하고 반응형으로 가기에는 서비스 특성상 양쪽의 트래픽이 많이 차이나지 않아서 아쉬운 지점들이 있었다.

두 버전의 SSG를 제공해보자

Next.js에서 동일한 경로에 대해 두가지 버전의 SSG를 제공할 수 있는 기능은 제공하지 않았기 때문에 하나의 서버에서 기능을 구현하기 위해서는 두 버전을 다른 경로로 static 파일을 생성해둔 뒤 미들웨어나 프록시에서 rewrite 하거나 아예 별도의 캐시서버를 두는 방식을 사용해야 했었는데, 현재 팀 규모에 비해 구현비용이 크거나 유지보수의 복잡성이 불필요하게 증가해보였다.

따라서 깔끔하게 env 기반으로 두 버전의 pod 을 운영한 뒤, user-agent 기반으로 mobile로 생성된 static 서버와 pc로 생성된 static 서버로 밸런싱하는 L7을 앞단에 두어 해결했고, UX에 영향가지 않는 선에서 반응형 컴포넌트는 dyanamic import 를 통해 로드시점을 조절해 fcp 에 영향을 가지 않도록 수정했다.

병목 B. Hydration 오류와 reportError

2

뷰는 정상적으로 나오는 것 처럼 보였지만 리얼 환경의 특이 케이스가 아닌 개발환경에서도 수많은 오류가 발생하고 있었다.

찾아보니 React Hydration 오류가 많은 부분을 차지하고 있었는데, time 처리, pc/m 단일소스로 처리.. 등등 과정에서 발생하고 있었다.

이 부분은 선택의 여지 없이 모든 에러를 해결했다. 다만 처음 알았던 사실이 Hydration 오류가 발생할 때 reportError를 사용한다는 점, 그리고 이 task가 많이 쌓이게 되면 병목이 될수도 있다는 점이었다.ref: reportError()

1차 개선결과

휴.. 큰 문제는 해결했다.
fcp 5초 → 2초, lcp 13초 → 3초로 개선되었다.
하지만 여전히 아쉬운 성능이고 Blocking Time 이 길게 나오고 있었다.

2

다음 포스트에 이어서

거의 일주일에 걸쳐 최적화를 했던터라 중요한 것만 적는다고 해도 글이 길어지는 듯 하여 이 부분에 대한 개선은 다음 포스트에서 다뤄보려 한다. 그런데 요즘 창업의 고통을 몸소 겪다보니 글을 언제 다시 이어서 쓸 수 있을지 몰라서 결론만 미리 스포하려 한다.

성능 97 달성

2

SEO 회복

2

사실 SEO 회복은 이 이외에도 다양한 노력을 겸했기 때문에 성능만이 원인이였는지는 분명하진 않지만, 기존에 심각했던 성능지표를 생각하면 영향이 있었을거라 생각한다.

양자 컴퓨터가 상용화 되면 더 이상 성능을 신경쓰지 않고 비지니스와 고객에게 조금 더 집중할 수 있는 날이 올수도 있을까 문득 드는 잡생각을 끝으로, 이제 다시 할일을 하러 가봐야겠다.

포스트 마침.

by Jaewon Kim