트리쉐이킹(Tree Shaking)은 말 그대로 나무를 흔들어 잔가지를 털어내듯 불필요한 코드를 제거하는 것을 의미합니다. 웹 개발을 할 때, 애플리케이션의 규모가 커지면서 코드의 양이 방대해지고, 다양한 라이브러리를 가져다 사용하게 되면 불필요한 코드를 그대로 가져가는 경우가 생각보다 많이 생깁니다. 이런 불필요한 코드들을 찾아내어 제거하면 웹 사이트 성능 최적화에 큰 도움이 됩니다. 특히 JavaScript는 다음과 같은 이유로 가능하면 트리쉐이킹을 해주는 것이 좋습니다.
요즘은 과거 HTML 위주의 단순한 웹 페이지와는 비교도 안 될 정도로 규모 있고 화려한 인터랙션을 자랑하는 웹 애플리케이션들이 많습니다. 웹 사이트에서 인터랙션이 많아졌다는 것은 그만큼 JavaScript의 비중이 높아졌다는 뜻이기도 합니다. 실제로 http archive의 자료를 보면, 2011년도에 비해 웹 애플리케이션의 JavaScript 파일 크기의 중윗값이 데스크톱에서는 478.6% 증가했고, 모바일에서는 무려 796.6%나 증가했습니다.
이 자료에서의 파일 크기는 네트워크를 오고 갈 때의 크기로, 압축되어 있는 상태에서의 크기입니다. 실제로 파일을 사용할 땐 압축을 해제한 후에 사용해야 하는 것을 생각하면, 실제 JavaScript 파일의 크기는 훨씬 클 것입니다.
JavaScript 파일의 크기만 커진 것이 아닙니다. JavaScript 파일을 요청하는 HTTP 요청 수 또한 데스크탑에서 155.6%, 모바일에서 425.0% 증가했습니다. 크기가 훨씬 커진 JavaScript 파일이 늘어난 요청 횟수만큼 더 오가는 것이니, 네트워크 리소스 소모가 그만큼 커졌다는 것을 알 수 있습니다.
JavaScript 파일 크기의 증가, 요청 횟수의 증가는 그만큼 파일이 오고 가는 동안 화면 표시가 늦어진다는 것을 뜻하고, 네트워크 속도가 느린 환경에서는 더 큰 병목현상을 유발합니다. 따라서 트리쉐이킹을 통해 파일 크기를 가능한 줄이는 것이 최적화에 도움이 됩니다.
JavaScript 파일이 실행되기 위해서는 여러 과정을 거치게 됩니다. 다운로드부터 필요한 경우에는 우선 요청을 보내어 파일을 다운받아 온 다음 압축을 해제해야 합니다. 그다음에는 JavaScript 코드를 파싱하여 DOM 트리를 생성합니다. 파싱이 끝나면 컴파일하여 컴퓨터가 이해할 수 있는 언어로 바꿔줘야 합니다. 이 컴파일 과정까지 거쳐야지 비로소 코드를 실행할 수 있습니다. 이처럼 코드 실행까지 거쳐야 하는 과정이 많기 때문에 JavaScript는 다른 리소스에 비해서 실행까지 상대적으로 많은 시간을 소모하게 됩니다.
실제로 JavaScript 파일의 크기가 커진 만큼, 파일의 실행 시간 또한 증가한 것을 알 수 있습니다. 데스크톱에서는 측정한 기간이 얼마 되지 않아 확인하기 어렵지만, 모바일에서는 222.2%만큼 실행 시간이 길어져 2.9초의 시간이 소요되는 것을 확인할 수 있습니다.
JavaScript 파일의 실행은 CPU에 크게 영향을 받는데, 그렇다 보니 사양이 천차만별인 모바일 환경에서 그 영향이 더욱 두드러집니다. 실제로 휴대폰의 사양에 따라 소모 시간이 크게 차이 나는 것을 아래 도표를 통해 확인할 수 있습니다.
앞서 공부한 최적화의 개념에서, 페이지 로드 시간이 3초를 넘어가면 53%의 사용자가 이탈한다고 했습니다. JavaScript 파일을 요청하고 다운받아 오는 시간을 제외하고서 파일을 실행하는 데만 2.9초가 걸린다면 파일을 실행하는 동안에만 이미 50% 이상의 사용자가 이탈할 것이라고 예상할 수 있습니다. 기기 환경에 따라서 2.9초의 몇 배의 시간을 파일 실행에만 사용할 수도 있는 만큼 이탈률은 그만큼 커질 수 있습니다. 이러한 상황을 최대한 줄이기 위해서라도 트리쉐이킹을 통한 최적화가 필요합니다.
웹팩 4버전 이상을 사용하는 경우에는 ES6 모듈(import
, export
를 사용하는 모듈)을 대상으로는 기본적인 트리쉐이킹을 제공합니다. Create React App을 통해 만든 React 애플리케이션도 웹팩을 사용하고 있기 때문에 트리쉐이킹이 가능합니다. 웹팩을 사용하는 환경에서 효과적으로 트리쉐이킹을 수행하는 방법에 대해서 알아봅시다.
import
구문을 사용해서 라이브러리를 불러와서 사용할 때, 라이브러리 전체를 불러오는 것이 아니라 필요한 모듈만 불러오면 번들링 과정에서 사용하는 부분의 코드만 포함시키기 때문에 트리쉐이킹이 가능해집니다.
예시를 들어보겠습니다. React를 사용하는 애플리케이션에서 React를 통째로 불러온 다음 그 안에 무엇이 있는지 console.log
를 사용해 확인해보겠습니다.
확인 결과, 아래와 같이 React의 모든 코드가 불려 온 것을 확인할 수 있습니다.
이렇게 모든 코드를 불러오면, 이 중에서 실제로 사용하는 코드는 얼마 되지 않더라도 번들링할때 이 모든 코드를 같이 빌드하게 됩니다. 불필요한 코드가 포함되는 것이죠. 이를 방지하기 위해서 import
해올 때 아래와 같이 실제로 사용할 코드만 불러와 주면 됩니다.
import { useState, useEffect } from 'react'
그러면 불러오지 않은 코드는 빌드할 때 제외되므로 코드의 크기를 줄일 수 있게 됩니다.
Babel은 자바스크립트 문법이 구형 브라우저에서도 호환이 가능하도록 ES5 문법으로 변환하는 라이브러리입니다. 이 때 ES5문법은 import
를 지원하지 않기 때문에 commonJS 문법의 require로 변경시키는데, 이 과정은 트리쉐이킹에 큰 걸림돌이 됩니다. require
는 export
되는 모든 모듈을 불러오기 때문입니다. 1번에서 작성한 것처럼 필요한 모듈만 불러오기 위한 코드를 작성해도 소용이 없어지는 것입니다.
이를 방지하기 위해서 Barbelrc 파일에 다음과 같은 코드를 작성해주면 ES5로 변환하는 것을 막을 수 있습니다.
{
“presets”: [
[
“@babel/preset-env”,
{
"modules": false
}
]
]
}
반대로, modules 값을 true로 설정하면 항상 ES5 문법으로 변환하므로 주의해서 작성해야 합니다.
웹팩은 사이드 이펙트를 일으킬 수 있는 코드의 경우, 사용하지 않는 코드라도 트리쉐이킹 대상에서 제외시킵니다.
const crews = ['kimcoding', 'parkhacker']
const addCrew = function (name) {
crews.push(name)
}
위 코드에서 addCrew
함수는 함수 외부에 있는 배열인 crews
를 변경시키는 함수입니다. 해당 함수는 외부에 영향을 주지도 받지도 않는 함수, 순수 함수가 아니기 때문에 트리쉐이킹을 통해 제외하는 경우 문제가 생길 수도 있다고 판단해 웹팩은 이 코드를 제외시키지 않습니다.
이럴 때 package.json 파일에서 sideEffects
를 설정하여 사이드 이펙트가 생기지 않을 것이므로 코드를 제외시켜도 됨을 웹팩에게 알려줄 수 있습니다. 다음과 같이 작성하면 애플리케이션 전체에서 사이드 이펙트가 발생하지 않을 것이라고 알려줍니다.
{
"name": "tree-shaking",
"version": "1.0.0",
"sideEffects": false
}
혹은 아래와 같이 작성하여 특정 파일에서는 발생하지 않을 것임을 알려줄 수 있습니다.
{
"name": "tree-shaking",
"version": "1.0.0",
"sideEffects": ["./src/components/NoSideEffect.js"]
}
보통 3번까지 작성하면 트리쉐이킹이 잘 작동합니다. 그런데 트리쉐이킹이 적용되지 않는 라이브러리가 있다면, 해당 라이브러리가 어떤 문법을 사용하고 있는지 확인해볼 필요가 있습니다. 모듈에 따라서 ES5로 작성된 모듈이 있을 수도 있기 때문입니다. ES5 문법을 사용하는 모듈을 통째로 사용하는 상황이라면 상관없지만, 일부만 사용하는 경우라면 해당 모듈을 대체할 수 있으면서 ES6를 지원하는 다른 모듈을 사용하는 것이 트리쉐이킹에 유리합니다. ES6 문법을 사용하는 모듈을 사용하면 해당 모듈에서도 필요한 부분만 import 해서 사용하지 않는 코드는 빌드할 때 제외되기 때문입니다.