개발/Today I Learned

웹 최적화 : 자바스크립트 번들 사이즈 줄이기 (feat. JavaScript Bundle Diet)

devmomori 2022. 5. 29. 01:49

최근 토스에서 SLASH 22 두 번째 개발자 콘퍼런스를 홍보하는 것을 보고 SLASH 21에 있던 영상들을 다시 한번 빠르게 훑어봤다. 이전에는 잘 알지 못해서 북마크만 해두고 '나중에 봐야지' 하고 남겨두었던 이한 님의 JavaScript Bundle Diet 영상을 보는 순간 현재 회사에서 맡고 있는 프로젝트의 번들 사이즈를 줄이고 싶다는 열망이 솟아났다.

 

중복 패키지를 제거하자

맡고 있는 프로젝트는 Vue로 되어 있다. Vue Cli를 통해 프로젝트를 구성하게 되면 Webpack Bundle Analayzer가 이미 포함되어 있기 때문에 빌드 시 --report 명령어를 추가해준다면 report.html을 통해서 다음과 같은 차트를 확인할 수 있다.

React에서는 라이브러리 설치 후, 웹팩에서 설정해주는 작업이 필요한 듯 하다.

 

// build 후 report.html을 생성한다.
vue-cli-service build --report

 

Bundle Analayzer를 통해 확인할 수 있는 사이즈는 3가지다.
[관련 Github Issue - 사이즈에 관한 질문 링크]
- stat: 최적화하지 않은 날 것의 번들 사이즈
- parsed: webpack minification이 적용된 번들 사이즈
- gzip: 압축된 사이즈

 

report.html created after build with option '--report'

 

문제의 원인

모노레포(yarn workspaces)로 구성되어 있는 프로젝트이며, yarn을 통해 패키지를 설치했다. JavaScript Bundle Diet 영상에서도 나왔듯, yarn이 공식문서에서 설명하는 것과는 다르게 완벽하게 중복된 라이브러리의 설치를 방지하지 못했다. 

 

내 경우에는 core-js 라는 라이브러리가 중복되어 있음을 확인할 수 있었다. 하나는 root/core-js, 또 다른 하나는 @vue/babel-preset-app/node_modules/core-js 이다.

 

영상에서는 yarn deduplicate를 사용하여 중복된 라이브러리를 제거하라 하였지만, 나는 다른 방법으로 해당 라이브러리의 중복을 제거했다. (사실문제를 해결하기 전에 정답을 보고 싶지 않아서 '용도가 겹치는 중복된 라이브러리를 제거하는 게 번들 용량을 줄이는 첫 번째 시작입니다'라는 말을 듣자마자 영상을 멈추고 답을 찾아나가기 시작했다) 

 

처음에는 각각의 node_modules 폴더로 들어가 core-js의 패키지 버전을 확인했다.

  • core-js: v3.22.0
  • @vue/~/core-js: v3.20.2

 

나중에는 yarn list를 통해 설치된 라이브러리의 의존성들을 확인하고, yarn why <package name> 명령어를 통해 해당 라이브러리가 어떤 이유로 설치되었는지도 확인하였다. 결과로, 파트 내부에서 사용하는 코어 라이브러리, Vue/bable-preset-app, 그리고 Cypress 스냅샷 라이브러리의 core-js 버전이 서로 일치하지 않음을 터미널로 확인할 수 있었다.

 

버전이 다른 것을 확인한 후, 아주 단순하게 생각했다. "@vue/~/core-js의 버전을 높이면 되겠네!"  

 

버전을 올리니 ts-loader와 관련된 의존성 문제가 터졌고, 이를 해결하기 위해 Vue의 TypeScript 버전을 높이니, Webpack 버전의 문제가 발생하였다. 버전을 올리는 것은 정말 많은 것을 고려해야 한다는 것을 깨달았다. 해당 문제를 해결하기 위해서 Cypress 스냅샷 테스트 라이브러리 내부의 core-js 버전을 낮추기로 결정하였다.

 

그럼 해당 core-js의 버전을 어떻게 낮출 수 있을까?

 

yarn : Selective dependency resolutions

라이브러리의 버전을 선택적으로 관리할 수 있는 방법이 있는데 바로 yarn의 Selective dependency resolutions다. 해당 문서의 예시 코드는 굉장히 불친절하다. 처음에는 어떻게 쓰는 건가 했는데, yarnpkg/rfcs 문서를 읽는 것이 더 도움이 되었다. 해당 RFC 문서에서 어떤 경우에서 resolution을 통해 문제를 해결할 수 있는지 자세히 설명하고 있으며 여러 가지 예시들도 적혀있다.

 

한 예시로 다음을 살펴보자. DevDependencies는 다음과 같이 설정되어 있다.

  // example
  "devDependencies": {
    "@vue/cli-service": "2.0.3",
    "typescript": "2.3.2"
  }

그렇다면 yarn.lock 파일은 이러한 형태로 구성이 되어있다

"typescript@>=2.0.0 <2.3.0":
  version "2.2.2"
  resolved "https://registry.yarnpkg.com/typescript/-/typescript-2.2.2.tgz#606022508479b55ffa368b58fee963a03dfd7b0c"

typescript@2.3.2:
  version "2.3.2"
  resolved "https://registry.yarnpkg.com/typescript/-/typescript-2.3.2.tgz#f0f045e196f69a72f06b25fd3bd39d01c3ce9984"

이제 다른 버전을 가진 TypeScript를 node_modules 폴더에서 확인할 수 있다. 필자가 겪은 core-js 문제와 정말 비슷하지 않은가?

  • typescript@2.3.2 in node_modules/typescript
  • typescript@2.2.2 in node_modules/@vue/cli/node_modules.

 

해당 상황에서 TypeScript의 버전을 2.3.2로 고정하던지 2.1.0으로 낮추는 방법이 있을 수 있겠다.

 

(단순히 resolutions를 사용하여 버전을 관리하는 예시일 뿐이다. 버전을 내리거나 올림으로써 오는 사이드 이펙트가 있을 수 있다.)

  "devDependencies": {
    "@vue/cli-service": "2.0.3",
    "typescript": "2.3.2"
  },
  "resolutions": {
    "@vue/cli-service/**/typescript": "2.3.2"
  }
  
  or
  
  "resolutions": {
    "typescript": "2.1.0"
  }

더 자세한 예시는 RFC 문서에서 확인하자.

 

참고사항

이렇게 Selective dependency resolutions를 통해 문제를 해결했지만 RFC에서 설명해주지 않은 한 가지로 인해 삽질을 한 것이 있다. yarn workspaces 구조에서는 각각의 내부 package.json에서 resolutions를 사용하더라도 원하는 버전이 설치되지 않는 것이다. workspaces 구조에서는 root에 존재하는 pacakge.json에 resolutions를 적용해줘야 한다. 

yarn why 명령어를 통해 각 라이브러리의 버전을 확인하면 workspaces로 인해 라이브러리들이 호이스팅이 되었다는 메시지를 확인할 수 있는데 각 모노레포를 패키지로 보기 때문에 루트에서 resolutions를 적용해야 하는 것 같다.

 

이 과정을 통해 번들 사이즈를 약 27% 감소시킬 수 있었다.

 


 

더 줄일 수 있는 방법은? (Chunk 분리 및 Dynamic Import)

중복된 패키지의 제거를 통해 번들 사이즈를 줄였다면, 영상 내용처럼 자주 사용하는 패키지와 그렇지 않은 컴포넌트들의 chunk를 나눠볼 수도 있다. 또한, Dynamic Import를 통해 필요할 때 원하는 파일을 받아올 수 있다면 그만큼 처음 파일을 받는 시간을 줄일 수도 있을 것이다.  prefetch를 통해 여유 있는 시간에 원하는 파일을 미리 받아 놓아 페이지의 이동을 최적화할 수도 있겠다.

 

참고사항

prefetch와 관련하여, Vue-cli를 통해 프로젝트를 만들게 되면 기본적으로 prefetch 기능이 추가되어 있는데 해당 작업을 따로따로 관리해주고 싶다면 vue.config.js 설정에서 prefetch 기능을 제거해주면 된다. 또, 해당 기능이 무조건적으로 장점이 되지 않았다는 JeongWoo Ahn 님의 [Vue.js] Lazy load 적용하기2 아티클도 읽어보면 많은 도움이 될 것 같다. 회사 프로젝트에도 prefetch 기능이 기본적으로 적용되어 있기 때문에 이와 관련해서는 chunk 분리와 prefetch 기능의 전후 테스트를 통해 웹 최적화를 진행해 나아가야겠다.

 

 


영상을 통해 새로운 것을 배우고 프로젝트에 적용하여 실제로 번들 사이즈를 줄인 것은 꽤나 즐거운 경험이었다. 많은 시간을 쏟고 삽질을 했지만 패키지의 중복을 제거하였고, 여러 웹팩 설정을 통한 최적화 방법과 몰랐던 yarn과 관련한 명령어 그리고 활용법을 배울 수 있었다. 삽질이 길어질 때 전달되는 파트원의 조언은 정말 많은 도움이 되었다.

 

웹 최적화와 프로젝트 번들 다이어트는 이제 시작이다. 번들 살, 더 빼보자.

 

 

 

 

Refereces