Tree Shaking

앱의 어느곳에서도 사용되지 않는 코드들을 코드베이스에 추가하는 경우가 있다. 이런 dead code들을 제거하여 최종 번들 크기를 줄이고 불필요한 로딩 타임을 줄일수 있다. 코드베이스에 추가되어 있는 dead code들을 최종 번들중에 제거하는 과정을 tree-shaking이라 한다.

math 같은 모듈들에 대해 tree-shaking을 적용하는것은 간단하지만, tree-shaking을 하기 까다로운 몇몇 상황들이 존재한다.

컨셉

tree-shaking은 최종 자바스크립트 번들에서 실제로 사용되지 않는 코드들을 제거하기 위해 사용된다. 이 과정이 올바르게 수행될 경우 클라이언트 측에서 다운로드해야하는 번들 크기를 줄여 로딩 속도, 구분 분석, 실행 시간을 줄일 수 있다. 대부분의 모던 앱 들은 webpack 혹은 rollup과 같은 모듈 번들러를 통해 빌드되기 때문에 tree-shaking이 자동으로 적용된다.

우리개 개발하는 앱과 내부의 의존 관계를 Abstract Syntax Tree (우리는 이 Tree를 흔들어 불필요한 것들을 떨궈내야 한다)로 볼 수 있다. 트리의 각 노드는 우리의 앱에 기능을 제공하는 “의존성” 이라고 볼 수 있다. tree-shaking에서는 입력된 파일 모두를 그래프로 처리한다. 그래프의 각 노드에서 part라 불리는 최 상의 구문이 된다. tree-shaking은 그래프를 앱의 진입점부터 모든 실제 순회 경로를 표시하는 탐색 과정이라고 볼 수 있다.

각 컴포넌트들은 심볼들을 선언할 수 있고 참조할 수 있으며 다른 파일에 의존할 수 있다. 각각의 part(파일들)는 사이드 이펙트가 있을수도, 없을수도 있다. 예를 들어 let firstName = 'Jane' 구문의 경우 사이드 이펙트가 없기 때문에 firstName을 참조하는 다른 코드가 없는지만 확인하면 된다. 하지만 let firstName = getName() 구문의 경우 firstName을 필요로 하는 코드가 없더라도 getName()의 호출로 이어지는 내용이 변경되지 않고서는 제거할 수 없기 때문이다.

Imports

ES2015의 모듈 구문(import, export)으로 정의된 모듈들만 tree-shaking이 가능하다. 모듈을 import하는 방법이 tree-shaking 가능 여부를 결정짓게 된다.

tree-shaking은 앱의 시작이 되는 파일부터 새로운 섹션들에 닿게 될 때 까지 사이드 이펙트로 관련된 트리의 각 노드 끝 부분까지 탐색한다. 이 탐색 과정이 끝나면 자바스크립트 번들에는 이렇게 탐색된 부분들만 포함된다. 이 과정에서 탐색되지 않은 부분은 번들에 포함되지 않고 남게 된다. 아래 utilities.js파일 예시를 보자.

export function read(props) {⁣⁣
    return props.book⁣⁣
}⁣⁣
⁣⁣
export function nap(props) {⁣⁣
    return props.winks⁣⁣
}

이 모듈을 index.js에서 아래처럼 사용하고 있다.

import { read } from 'utilities';⁣⁣
⁣⁣
eventHandler = (e) => {⁣⁣
    read({ book: e.target.value })⁣⁣
}

위의 예제에서 nap()은 import되어 사용되지 않기 때문에 번들에 포함되지 않게 된다.

Side Effects

ES6 모듈을 import하는 경우 해당 모듈은 즉시 실행된다. 이 때 참조하는 코드가 export하는 것을 참조하지 않더라도 그 코드 내부에서 전역에 무언가 영향을 줄 수 있다 (예를 들면 폴리필 추가, 전역 스타일시트 추가 등..) 이를 side effect라 한다. 모듈이 export하는 것을 참조하지 않고 있더라도 위와 같은 특이 동작들로 인해 tree-shaking이 불가해지는 경우가 있다.

이런 상황에 대해 더욱 자세히 알고 문제를 해결하고 싶다면 webpack 문서 중 tree-shaking에 대한 설명을 읽어 보자.