오늘날의 웹 애플리케이션은 특히 JavaScript 부분이 상당히 클 수 있습니다. 2018년 중반 현재 HTTP Archive에 따르면 휴대기기에서 JavaScript의 중간 전송 크기는 약 350KB입니다. 이는 전송 크기일 뿐입니다. JavaScript는 네트워크를 통해 전송될 때 압축되는 경우가 많으므로 브라우저에서 압축을 해제한 후의 실제 JavaScript 양은 상당히 많습니다. 리소스 처리와 관련해서는 압축이 관련이 없으므로 이 점을 지적하는 것이 중요합니다. 압축된 JavaScript가 약 300KB일 수 있지만 압축 해제된 JavaScript 900KB는 파서와 컴파일러에게 여전히 900KB입니다.
JavaScript는 처리하는 데 많은 비용이 드는 리소스입니다. 다운로드 후 비교적 사소한 디코딩 시간만 발생하는 이미지와 달리 JavaScript는 파싱, 컴파일된 후 최종적으로 실행되어야 합니다. 바이트별로 계산하면 JavaScript가 다른 유형의 리소스보다 비용이 더 많이 듭니다.
JavaScript 엔진의 효율성을 개선하기 위해 지속적으로 개선이 이루어지고 있지만 JavaScript 성능 개선은 언제나 개발자의 몫입니다.
이를 위해 JavaScript 성능을 개선하는 기법이 있습니다. 코드 분할은 애플리케이션 JavaScript를 청크로 분할하고 이러한 청크를 필요한 애플리케이션 경로에만 제공하여 성능을 개선하는 기법입니다.
이 기법은 작동하지만 JavaScript가 많은 애플리케이션의 일반적인 문제인 사용되지 않는 코드를 포함하는 문제를 해결하지는 않습니다. 트리 셰이킹은 이 문제를 해결하려고 시도합니다.
트리 쉐이킹이란 무엇인가요?
트리 셰이킹은 불량 코드 제거의 한 형태입니다. 이 용어는 Rollup에 의해 대중화되었지만, 데드 코드 제거라는 개념은 이미 오래전부터 존재해 왔습니다. 이 개념은 webpack에서도 사용되며, 이 도움말에서는 샘플 앱을 통해 이를 보여줍니다.
'트리 셰이킹'이라는 용어는 애플리케이션과 종속 항목을 트리와 같은 구조로 생각하는 사고방식에서 비롯되었습니다. 트리의 각 노드는 앱에 고유한 기능을 제공하는 종속 항목을 나타냅니다. 최신 앱에서는 이러한 종속 항목이 다음과 같은 정적 import 문을 통해 가져옵니다.
// Import all the array utilities!
import arrayUtils from "array-utils";
앱이 초기 단계(묘목)에 있으면 종속 항목이 거의 없을 수 있습니다. 또한 추가하는 종속 항목을 전부 또는 대부분 사용합니다. 하지만 앱이 성숙해짐에 따라 종속 항목이 더 추가될 수 있습니다. 문제를 더 복잡하게 만드는 것은 오래된 종속 항목이 사용되지 않지만 코드베이스에서 삭제되지 않을 수 있다는 점입니다. 그 결과 앱이 사용되지 않는 JavaScript가 많이 포함된 상태로 제공됩니다. 트리 셰이킹은 정적 import 문이 ES6 모듈의 특정 부분을 가져오는 방식을 활용하여 이 문제를 해결합니다.
// Import only some of the utilities!
import { unique, implode, explode } from "array-utils";
이 import 예와 이전 예의 차이점은 "array-utils" 모듈에서 모든 것(많은 코드일 수 있음)을 가져오는 대신 이 예에서는 특정 부분만 가져온다는 점입니다. 개발자 빌드에서는 전체 모듈이 가져와지므로 아무것도 변경되지 않습니다. 프로덕션 빌드에서 명시적으로 가져오지 않은 ES6 모듈의 내보내기를 제거하도록 webpack을 구성하여 프로덕션 빌드를 더 작게 만들 수 있습니다. 이 가이드에서는 이 작업을 수행하는 방법을 알아봅니다.
나무를 흔들 기회 찾기
설명을 위해 트리 셰이킹의 작동 방식을 보여주는 샘플 단일 페이지 앱이 제공됩니다. 원하는 경우 클론하여 따라할 수 있지만 이 가이드에서는 모든 단계를 함께 다루므로 클론하지 않아도 됩니다 (실습을 선호하는 경우 제외).
샘플 앱은 검색 가능한 기타 효과 페달 데이터베이스입니다. 쿼리를 입력하면 효과 페달 목록이 표시됩니다.
이 앱을 구동하는 동작은 공급업체 (예: Preact 및 Emotion)과 앱별 코드 번들 (webpack에서 '청크'라고 부름)
위 그림에 표시된 JavaScript 번들은 프로덕션 빌드이므로 난독화를 통해 최적화됩니다. 앱별 번들의 경우 21.1KB는 나쁘지 않지만 트리 셰이킹이 전혀 발생하지 않는다는 점에 유의해야 합니다. 앱 코드를 살펴보고 이 문제를 해결하기 위해 무엇을 할 수 있는지 알아보겠습니다.
모든 애플리케이션에서 트리 셰이킹 기회를 찾으려면 정적 import 문을 찾아야 합니다. 기본 구성요소 파일 상단 근처에 다음과 같은 줄이 표시됩니다.
import * as utils from "../../utils/utils";
다양한 방법으로 ES6 모듈을 가져올 수 있지만 다음과 같은 모듈에 주의해야 합니다. 이 특정 줄은 'utils 모듈에서 import 모든 것을 가져와 utils라는 네임스페이스에 넣어'라고 말합니다. 여기서 물어야 할 중요한 질문은 '이 모듈에 얼마나 많은 항목이 있나요?'입니다.
utils 모듈 소스 코드를 살펴보면 코드 줄이 약 1,300개 있습니다.
이 모든 것이 필요한가요? utils 모듈을 가져오는 기본 구성요소 파일을 검색하여 해당 네임스페이스의 인스턴스가 몇 개 나오는지 다시 확인해 보겠습니다.
utils 네임스페이스는 기본 구성요소 파일 내에서 세 번만 호출됩니다.
알고 보니 utils 네임스페이스는 애플리케이션에서 세 곳에만 표시됩니다. 어떤 함수에 표시될까요? 기본 구성요소 파일을 다시 살펴보면 정렬 드롭다운이 변경될 때 여러 기준으로 검색 결과 목록을 정렬하는 데 사용되는 utils.simpleSort라는 하나의 함수만 있는 것으로 보입니다.
if (this.state.sortBy === "model") {
// `simpleSort` gets used here...
json = utils.simpleSort(json, "model", this.state.sortOrder);
} else if (this.state.sortBy === "type") {
// ..and here...
json = utils.simpleSort(json, "type", this.state.sortOrder);
} else {
// ..and here.
json = utils.simpleSort(json, "manufacturer", this.state.sortOrder);
}
내보내기가 많은 1,300줄 파일에서 하나만 사용됩니다. 이로 인해 사용되지 않는 JavaScript가 많이 전송됩니다.
이 예시 앱은 다소 인위적이지만, 이러한 합성 시나리오가 프로덕션 웹 앱에서 발생할 수 있는 실제 최적화 기회와 유사하다는 사실은 변하지 않습니다. 트리 셰이킹이 유용할 수 있는 기회를 확인했으므로 실제로 어떻게 수행할까요?
Babel이 ES6 모듈을 CommonJS 모듈로 변환하지 않도록 하기
Babel은 필수 도구이지만 트리 셰이킹의 효과를 관찰하기가 약간 더 어려워질 수 있습니다. @babel/preset-env를 사용하는 경우 Babel은 ES6 모듈을 더 광범위하게 호환되는 CommonJS 모듈로 변환할 수 있습니다. 즉, import 대신 require하는 모듈입니다.
CommonJS 모듈의 경우 트리 셰이킹이 더 어렵기 때문에 CommonJS 모듈을 사용하기로 결정하면 webpack에서 번들에서 무엇을 삭제해야 하는지 알 수 없습니다. 이 문제를 해결하려면 ES6 모듈을 그대로 두도록 @babel/preset-env를 명시적으로 구성해야 합니다. babel.config.js 또는 package.json 등 Babel을 구성하는 곳 어디에서나 약간의 추가 작업을 해야 합니다.
// babel.config.js
export default {
presets: [
[
"@babel/preset-env", {
modules: false
}
]
]
}
@babel/preset-env 구성에 modules: false를 지정하면 Babel이 원하는 대로 작동하므로 webpack이 종속 항목 트리를 분석하고 사용하지 않는 종속 항목을 제거할 수 있습니다.
부작용 고려
앱에서 종속 항목을 제거할 때 고려해야 할 또 다른 측면은 프로젝트의 모듈에 부작용이 있는지 여부입니다. 부작용의 예는 함수가 자체 범위 외부의 항목을 수정하는 경우입니다. 이는 실행의 부작용입니다.
let fruits = ["apple", "orange", "pear"];
console.log(fruits); // (3) ["apple", "orange", "pear"]
const addFruit = function(fruit) {
fruits.push(fruit);
};
addFruit("kiwi");
console.log(fruits); // (4) ["apple", "orange", "pear", "kiwi"]
이 예에서 addFruit는 범위 밖에 있는 fruits 배열을 수정할 때 부작용을 일으킵니다.
부작용은 ES6 모듈에도 적용되며 이는 트리 셰이킹 컨텍스트에서 중요합니다. 예측 가능한 입력을 받아 자체 범위 외부의 항목을 수정하지 않고 예측 가능한 출력을 생성하는 모듈은 사용하지 않는 경우 안전하게 삭제할 수 있는 종속 항목입니다. 독립형 모듈식 코드 조각입니다. 따라서 '모듈'이라고 합니다.
webpack의 경우 프로젝트의 package.json 파일에 "sideEffects": false를 지정하여 패키지와 종속 항목에 부작용이 없음을 지정하는 힌트를 사용할 수 있습니다.
{
"name": "webpack-tree-shaking-example",
"version": "1.0.0",
"sideEffects": false
}
또는 webpack에 부작용이 없는 특정 파일을 알려줄 수 있습니다.
{
"name": "webpack-tree-shaking-example",
"version": "1.0.0",
"sideEffects": [
"./src/utils/utils.js"
]
}
후자의 예에서는 지정되지 않은 파일에 부작용이 없는 것으로 가정합니다. package.json 파일에 이를 추가하지 않으려면 module.rules를 통해 webpack 구성에서 이 플래그를 지정할 수도 있습니다.
필요한 항목만 가져오기
Babel에 ES6 모듈을 그대로 두라고 지시한 후에는 utils 모듈에서 필요한 함수만 가져오기 위해 import 구문을 약간 조정해야 합니다. 이 가이드의 예에서는 simpleSort 함수만 있으면 됩니다.
import { simpleSort } from "../../utils/utils";
전체 utils 모듈이 아닌 simpleSort만 가져오므로 utils.simpleSort의 모든 인스턴스를 simpleSort로 변경해야 합니다.
if (this.state.sortBy === "model") {
json = simpleSort(json, "model", this.state.sortOrder);
} else if (this.state.sortBy === "type") {
json = simpleSort(json, "type", this.state.sortOrder);
} else {
json = simpleSort(json, "manufacturer", this.state.sortOrder);
}
이 예에서 트리 셰이킹이 작동하는 데 필요한 것은 이 정도면 충분합니다. 다음은 종속 항목 트리를 흔들기 전의 webpack 출력입니다.
Asset Size Chunks Chunk Names
js/vendors.16262743.js 37.1 KiB 0 [emitted] vendors
js/main.797ebb8b.js 20.8 KiB 1 [emitted] main
트리 셰이킹이 성공한 후의 출력은 다음과 같습니다.
Asset Size Chunks Chunk Names
js/vendors.45ce9b64.js 36.9 KiB 0 [emitted] vendors
js/main.559652be.js 8.46 KiB 1 [emitted] main
두 번들 모두 크기가 줄었지만 main 번들의 이점이 가장 큽니다. utils 모듈의 사용하지 않는 부분을 제거하면 main 번들이 약 60% 축소됩니다. 이렇게 하면 스크립트가 다운로드하는 데 걸리는 시간뿐만 아니라 처리 시간도 줄어듭니다.
나무를 흔들어 보세요!
트리 셰이킹의 효과는 앱과 종속 항목, 아키텍처에 따라 달라집니다. 기능을 사용해 보세요. 이 최적화를 실행하도록 모듈 번들러를 설정하지 않았다는 것을 확실히 알고 있다면 시도해 보고 애플리케이션에 어떤 이점이 있는지 확인해 보세요.
트리 셰이킹으로 인해 성능이 크게 향상될 수도 있고 그렇지 않을 수도 있습니다. 하지만 프로덕션 빌드에서 이 최적화를 활용하고 애플리케이션에 필요한 항목만 선택적으로 가져오도록 빌드 시스템을 구성하면 애플리케이션 번들을 최대한 작게 유지할 수 있습니다.
이 도움말의 품질을 크게 개선해 준 Kristofer Baxter, Jason Miller, Addy Osmani, Jeff Posnick, Sam Saccone, Philip Walton에게 특별히 감사드립니다.