빌드 속도 개선

minseok baek·2024년 8월 16일

프로젝트

목록 보기
12/20

문제의 발견

빌드 속도 최적화 작업 이후, Husky와 Vercel의 Git-Action 기능 덕분에 별도의 빌드 테스트를 진행하지 않았다. Vercel의 빌드 과정에서 별다른 문제가 없었고, 커밋 시점에서 ESLint와 테스트 코드까지 점검하므로 빌드 속도에 대해 크게 신경 쓰지 않았다.

그러나 최종 작업 단계에서 예상치 못한 문제가 발생했다. 작업이 거의 완료된 시점에서 빌드 속도를 다시 측정해본 결과, 번들의 초기 로딩 속도가 이전에 비해 눈에 띄게 느려진 것을 발견했다. 이 문제는 특정 페이지에 국한되지 않고, 리팩토링을 거친 모든 페이지에서 공통적으로 나타났으며, 각 페이지에서 50~70KB 정도의 로딩 속도 증가가 확인되었다.

해결 과정

운이 좋게도, 문제를 발견했을 때 메인 브랜치로 머지되지 않은 상태였기 때문에 일부 페이지는 여전히 이전 빌드 결과를 유지하고 있었다. 이를 통해 문제의 원인을 파악하는 데 중요한 단서를 얻을 수 있었다.

리팩토링을 거친 모든 페이지에서 로딩 속도가 공통적으로 50KB씩 증가한 점을 고려했을 때, 문제는 공통적으로 사용되는 코드나 구성 요소에서 발생했을 가능성이 높다고 판단했다. 이러한 요소로는 레이아웃, shared/components, entities 등이 있었다. 그러나 만약 문제가 레이아웃에 있었다면, 모든 페이지에 동일한 영향을 미쳤을 것이므로 이 가능성은 배제했다.

다음으로 entities를 점검해본 결과, 메인 브랜치에서도 이미 entities에서 데이터를 가져오는 구조였기 때문에 이 역시 문제가 아니었다. 결국 남은 원인으로는 shared/component가 가장 유력해 보였다.

shared/component의 메인 버튼을 이전 빌드에서 정상적으로 동작하던 페이지에 적용해본 결과, 빌드 속도가 비약적으로 상승하는 것을 확인했다. 메인 버튼은 단순히 스타일링이 적용된 버튼 컴포넌트였기 때문에, 로딩 속도에 이렇게 큰 영향을 미칠 것이라고는 예상하지 못했다.

개발의 편의성을 위해 스타일링 속성을 전달하는 부분이 성능에 악영향을 미쳤을 가능성을 의심하여 sx = {} Props를 제거하고 다시 빌드를 확인해보았지만, 빌드 속도에는 변화가 없었다.

문제의 원인 발견

다음으로 의심했던 부분은 특정 컴포넌트 자체의 문제가 아닌, re-export 방식을 사용하여 모듈을 가져오는 방식에 문제가 있는지 확인하는 것이었다. shared/component/button/mainbutton을 직접 가져오는 방식으로 모듈을 로드했을 때, 빌드 속도가 정상으로 돌아왔다. 이를 통해 모듈을 불러오는 방식에서 차이가 있음을 발견했다.

export default 로 명시적으로 모듈을 내보내면 해당 모듈만 로드되지만,
export * from 방식으로 모듈을 불러올 경우, 특정 모듈만 사용하더라도 번들러는 모든 모듈을 로드해야 한다. 이런 동작 때문에 초기 로딩 속도가 비약적으로 상승하는 문제가 발생한 것이다.

이 문제를 해결하는 방법은 비교적 간단하다. export * fromexport { default as * } 와 같이 명시적으로 가져올 수 있는 방법으로 변경한다면 문제를 해결할 수 있다. 그러나 이 방법은 현재의 문제만 해결할 뿐, export * from 의 편리함을 포기해야 하는 단점이 있다.

하지만 유명한 프로젝트들에서도 export * from 방식을 자주 사용하는 것을 목격했기 때문에, 이 문제에 대한 다른 해결책을 찾아보았다.

해결방안 및 새로운 문제 등장

추가적인 조사를 통해 Tree Shaking이라는 기법을 알게 되었다. 이 기법은 번들러가 불필요한 코드를 제거하여 최적화하는 방법으로, 사용되지 않는 코드(Dead Code)를 식별하여 제거함으로써, 초기 로딩 속도를 개선할 수 있다. Tree Shaking을 활용하면 export * from 방식의 편리함을 유지하면서도 성능 문제를 해결할 수 있다.

Tree Shaking을 적용한 결과 대부분의 페이지가 정상화되었지만, qna/edit/[id] 및 qna/detail/[id] 페이지의 로딩 속도는 여전히 개선되지 않았다. 이 문제를 해결하기 위해 번들 분석 도구(analyze)를 사용해 분석한 결과, 페이지 내에 특정 모듈이 유독 큰 비율을 차지하고 있는 것을 발견했다. 해당 모듈은 /node_modules/markdown-it/index.mjs 였다.

해당 모듈은 첫로딩속도에 4분의1을 차지 하는 영향을 주는 만큼 dynamic import을 사용하여 모듈을 로드하기로 결정했다. 다행히 모듈을 비동기로 전환한 후 첫 로딩 속도가 예상대로 감소했다. 그러나 모듈이 필요한 시점에 로드되기 때문에, 사용자가 화면을 보는 순간 레이아웃이 조정되는 현상이 발생했다. 이문제를 해결하기 위해 로딩 시 spinner 와 모듈 로딩 메시지를 표시하여 사용자에게 모듈 로드 중임을 알리고, 혼란을 최소화 했다.

실험 결과 : SSR 방식 결정

디벨롭 모드 기준으로 tanstack query 로드 시간이 653ms인 상황에서, 두 가지 dynamic 설정을 적용하여 웹페이지의 네트워크 탭에서 청크 파일 로딩속도를 측정한 결과는 다음과 같았다.

dynamic : ssr false의 경우
MarkdownEditor 청크 파일 828ms
Markdownit라이브러리 로드 속도 272ms

dynamic : ssr true의 경우
markdownEditor 청크 파일 1.49초
Markdownit 라이브러리 로드 속도 353ms

두 설정 간의 로드 시간 차이를 분석한 결과 ssr: false 설정이 시각적인 차이도 크지 않고, 성능 면에서도 더 우수하다는 결론에 도달했다. 따라서 최종적으로 ssr : false를 선택하기로 결정했다.

Route (pages)                              Size     First Load JS
┌ ○ /                                      3.2 kB          148 kB
├   /_app                                  0 B             141 kB
├ ○ /404                                   551 B           142 kB
├ ○ /admin                                 4.3 kB          156 kB
├ ○ /coverLetter                           3.2 kB          148 kB
├ ○ /coverLetter/[id]/edit                 1.3 kB          150 kB
├ ○ /coverLetter/new                       1.48 kB         150 kB
├ ○ /interview                             2.23 kB         158 kB
├ ○ /join                                  1.77 kB         143 kB
├ ○ /login                                 1.53 kB         143 kB
├ ○ /myProfile                             2.28 kB         144 kB
├ ƒ /qna/detail/[id]                       983 B           145 kB
├ ○ /qna/edit/[id]                         4.57 kB         146 kB
├ ƒ /qna/list                              1.89 kB         150 kB
└ ○ /qna/question                          5.48 kB         150 kB
+ First Load JS shared by all              141 kB
  ├ chunks/framework-a6b69b549137c0cc.js   45.2 kB
  ├ chunks/main-bba3ba64503b38d8.js        36.8 kB
  ├ chunks/pages/_app-35e7642f49ab36ce.js  57.1 kB
  └ other shared chunks (total)            2.29 kB

ƒ Middleware                               26.7 kB

○  (Static)   prerendered as static content
ƒ  (Dynamic)  server-rendered on demand
profile
성장은 점진적 과부하, 매주 회고를 목표로 시작했지만 그때 그때 컨셉이 달라요. 시행착오를 통해 저만의 방식을 찾아가는중입니다.

0개의 댓글