프론트엔드 성능 최적화 - 3. 코드 스플리팅 & 트리 쉐이킹

2021. 8. 29. 01:39웹 프론트엔드 깊게 이해하기/성능 최적화

이 글은 아래 링크의 내용을 바탕으로 작성되었습니다.

https://medium.com/humanscape-tech/react에서-해보는-코드-스플리팅-code-splitting-56c9c7a1baa4

https://ui.toast.com/weekly-pick/ko_20180716

 

자바스크립트 코드 분할

 

자바스크립트 코드를 분할하는 방식에는 정말 여러가지가 있지만, 그 중 가장 간단하고 유용하다고 생각되는 방식을 적용해보겠습니다. 그것은 바로 동적 import + React.lazy 메서드 + React.Suspense 컴포넌트를 조합해서 사용하는 것입니다. 참고로 동적 import 를 사용하기 위해서는 @babel/plugin-syntax-dynamic-import 패키지가 필요하고 이를 바벨에 적용하는 것 또한 필요하지만, 만일 바벨에 @babel/preset-env 프리셋을 적용했다면 따로 설치 및 설정할 필요는 없습니다.

 

예전에는 코드 분할을 여러개의 엔트리 자바스크립트 파일을 두는 식으로 수행해야 했었지만 이제는 이 동적 import 구문의 도움을 받아 웹팩이 동적 import 로 가져오는 스크립트를 자동으로 따로 번들링하기 때문에 단순히 메인 번들에 포함하고 싶지 않은 부분은 동적 import 를 통해 가져오는 것만으로 코드 스플리팅을 적용할 수 있게 되었습니다.

 

게다가 만일 리액트를 사용하고 있다면 React.lazy 메서드를 통해 동적으로 렌더링 되는 컴포넌트를 사용할 수 있습니다. 이 컴포넌트는 또한 React 에서 자체적으로 제공하는 Suspense 컴포넌트를 이용해서, import 가 완료되기 전에는 다른 컨텐츠를 보여주고 import 완료 후에 동적으로 컴포넌트를 보여주는 일을 수행할 수 있습니다.

 

 

위와 같은 식으로 코드를 적용할 수 있습니다. 이때 저는 이용자가 처음 웹사이트에 방문할 때 보여질 Home 컴포넌트는 그냥 동기적으로 가져오도록 두고, Search 컴포넌트는 Suspense 컴포넌트화하여 라우터에 넘겼습니다. 이렇게 하면 브라우저는 /search 경로에 방문하기 전까지는 Home 컴포넌트를 렌더링하는데 필요한 자원만을 Load 하게 됩니다.

 

 

Idle 시간을 활용한 자원 다운로드

 

위와 같이 페이지별로 필요한 자원을 완전히 분리해서 각 페이지에 접근할 때만 그 페이지에서 활용되는 자원을 다운로드 받게 할수도 있지만, 이용자가 초기 페이지에 머무는 시간을 좀 더 활용해볼 수 있습니다.

 

구글 RAIL 모델에 따르면, 이용자가 페이지에 접속하고 나서 페이지가 렌더링 된 후 최소 50ms 동안은 이용자가 실제적으로 앱과의 상호작용을 하기까지의 '인지적 지연'이 발생합니다. 구글은 이 시간을 적극 활용해서 추후에 필요할 수도 있는 자원을 다운로드 받거나 어플리케이션을 이용하기 위해 필요한 처리를 수행할 것을 권장합니다.

 

이 예시 어플리케이션은 어플리케이션에 대해 설명하는 홈페이지와 gif 이미지를 검색할 수 있는 검색페이지로 페이지가 나뉩니다. 이때 검색페이지에서는 검색을 위해 @giphy api 를 사용할 수 있게 해주는 패키지를 사용하고 있습니다. 이 패키지의 크기가 생각보다 크기 때문에 검색 페이지에 접근함과 동시에 이 패키지를 다운로드 받는 것보다, 이용자가 홈페이지에 머무는 시간을 활용해서 미리 다운로드 받아봅시다.

 

 

이 말만 들으면 그냥 미리 받고자 하는 코드를 동적으로 import 하는 구문을 최상단 앱 컴포넌트에 추가만 하면 될 것처럼 느껴집니다. 하지만 이렇게 하면 앱의 초기 렌더링을 방해하게 됩니다.

 

 

첫 로드 시점을 녹화해보면 LCP 지점 이전에 총 두번의 'Evaluate Script' 과정이 일어난 것을 확인하실 수 있습니다. 자바스크립트는 앱의 렌더링을 중단시키는 블록 리소스이기 때문에 자바스크립트 코드가 비동기적으로 다운로드가 완료됨과 동시에 브라우저의 메인스레드는 렌더링 프로세스를 도중에 멈추고 해당 스크립트를 평가하는 작업에 들어갑니다. 따라서 앱의 초기 렌더링을 방해하지 않으면서 자바스크립트와 같은 블록 리소스 자원을 미리 다운로드 받기 위해서는 LCP 이후에 다운로드가 수행되도록 하는 것이 좋습니다.

 

 

그 방법은 정말 간단합니다. 아래와 같이 window.onload 이벤트에 기존의 동적 import 구문을 넣어주시기만 하면됩니다. 이러면 앱은 어플리케이션의 초기 'Load' 이벤트가 끝난 후에야 해당 자바스크립트 자원에 대한 동적 import 를 수행할 것입니다.

 

 

이제 다시 초기 로드 시점을 녹화해보면 두번째 'Evaluate Script' 과정이 LCP 이후로 이동한 것을 확인해보실 수 있습니다.

 

 

트리 쉐이킹 환경 설정

 

사용하지 않는 코드가 번들에 포함하지 않게 하는 것도 중요합니다. 이는 웹팩이 '알아서' 해주는 것이라 생각하기 쉽지만, 몇 가지 주의해야 할 사항들이 존재합니다.

 

첫번째로, babel 이 프로젝트에서 사용되는 ES6 모듈로된 패키지들을 CommonJS 모듈로 바꿔버릴 수 있다는 점입니다. 웹팩은 CommonJS 모듈(즉 require 를 쓰는 모듈)에 대해서는 트리 쉐이킹을 적용할 수 없습니다. 따라서 바벨이 이 모듈들을 그대로 ES6 모듈인채로 두도록 만들 필요가 있습니다.

 

설정 방법은 간단합니다. 만일 babel-preset-env 모듈을 사용하고 있다면 해당 프리셋을 적용하는 부분에서 "modules" 옵션을 꺼주시면 됩니다.

 

// package.json

"babel": {
  "presets": [
    [
      "@babel/preset-env",
      {
        "modules": false
      }
    ],
    "@babel/preset-react"
  ],
}

 

 

트리 쉐이킹 적용

 

기본적인 트리 쉐이킹의 원칙은 "필요한 것만 가져다 쓰기" 입니다. 예를 들어 foo 라는 패키지가 있고 해당 패키지의 foo.search 라는 메서드가 필요하다면 foo 를 import 하는게 아니라 구조분해할당을 이용해 search 메서드만을 가져오는 식으로 코드를 작성하는 것입니다.

 

import { search } from 'foo'

 

하지만 이 방식만을 너무 맹신해서는 안됩니다. 제가 이전에 말씀드렸듯, CommonJS 문법을 쓰는 모듈은 웹팩에서 기본적으로 트리쉐이킹이 적용되지 않습니다. lodash 같은 라이브러리가 대표적인 예입니다. 위와 같이 메서드 하나만 가져오려고 시도해도 이 같은 경우엔 패키지 안의 모든 내용이 번들에 포함될 것입니다.

 

이 경우 트리 쉐이킹이 되는 다른 라이브러리를 알아보거나, 해당 라이브러리가 폴더별로 다른 기능을 제공한다면 특정 모듈이 존재하는 폴더의 내용을 import 하도록 지정하는 것이 좋습니다.

 

import search from 'foo/search'

 

 

코드 스플리팅 & 트리 쉐이킹 완료 여부 확인

 

웹팩을 이용한 작업은 항상 '내가 제대로 한게 맞나'를 좀처럼 확인하기가 쉽지 않다는 단점이 있습니다. 위에서 코드 스플리팅과 트리 쉐이킹을 적용했다고 쳐도, 이게 실제로 잘 적용이 되었는지를 확인하기 위해서는 일일히 빌드 결과물을 뒤져봐야 합니다.

 

전체 빌드 결과를 시각적으로 한눈에 볼 수 있다면 어떨까요? 이를 위해 제작된 것이 'webpack-bundle-analyzer' 입니다.

 

npm install --save-dev webpack-bundle-analyzer
or
yarn add -D webpack-bundle-analyzer

 

설치 후 BundleAnalyzerPlugin 을 webpack.config.js 에 추가시켜줍니다.

 

const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer');

 

이러면 웹팩 번들링이 수행될 때마다 localhost:8888 주소에서 번들링 결과를 시각적으로 확인해보실 수 있습니다.

 

 

이 전체 트리맵을 보면서 각각의 번들과 청크에 들어가서는 안될 패키지나 코드가 들어가지는 않았는지, 각각의 번들과 청크의 크기가 본인이 생각하기에 알맞은지를 체크해볼 수 있습니다. 키워드 검색을 지원하기 때문에 전체 번들, 청크 파일들 안에 중복된 코드가 있는지도 한번 검사해볼 수 있습니다. 사이드바 옵션을 통해 webpack 이 수행한 파일 압축 이전에는 용량이 어느 정도였는지, 압축 이후에는 용량이 어느 정도인지, 압축 처리 후 gzip 형식으로 파일을 변환 시켰을 때는 용량이 어느 정도인지를 미리 확인해볼 수 있습니다.