본문 바로가기
SPA/React

[React] Tree Shaking으로 최적화하기

by J4J 2021. 8. 31.
300x250
반응형

안녕하세요. J4J입니다.

 

이번 포스팅은 tree shaking으로 최적화하는 방법에 대해 적어보는 시간을 가져보려고 합니다.

 

 

 

Tree Shaking이란?

 

tree shaking은 나무 흔들기라는 뜻으로 소스코드에서는 명시되어 있지만 실제로 프로그램 실행에 영향을 주지 않는 코드들을 빌드 단계 때 제거하는 것을 의미합니다.

 

tree shaking을 이용하여 프로그램 실행에 영향을 주지 않는 코드들을 제거했을 때 돌아오는 이점은 번들 파일의 크기 최적화입니다.

 

서비스의 규모가 커짐에 따라 프로그래밍되는 코드들도 많아지고 결국 번들 파일의 크기가 커지는 것은 당연한 상황이 됩니다.

 

그리고 번들 파일의 크기가 커지게 되면 브라우저가 번들 파일을 로드할 때 더 많은 시간을 소비하게 만듭니다.

 

이런 상황에서 tree shaking을 이용하면 번들 파일을 최적화하여 사이즈를 줄일 수 있을 것이고 결과적으로 런타임 단계 때 번들 파일이 로드되는 시간이 줄어들어 사용자들이 더 빠른 속도로 서비스를 사용할 수 있게 됩니다.

 

 

 

tree shaking은 모듈 번들러 중에 하나인 rollup에 의해 점점 인기(?)를 얻게 되었다고 합니다.

 

비록 리액트에서 주로 사용되는 모듈 번들러는 webpack이지만 webpack을 사용하더라도 tree shaking을 적용할 수 있기에 적용 방법을 보여드리도록 하겠습니다.

 

 

 

Tree Shaking 설정

 

webpack 공식 문서를 확인해보면 tree shaking 설정을 위해 해 줘야 되는 행동은 4가지가 있습니다.

 

  • ES6 모듈 포맷 사용 (ex, import / export)
  • commonjs로 컴파일되는 것 방지하기 (@babel/preset-env 설정 등을 이용)
  • package.json에 sideEffects 속성 사용
  • webpack production mode 사용

 

 

 

[ 1. ES6 모듈 포맷 사용 ]

 

간단한 예제 파일을 만들어 보겠습니다.

 

App.jsx와 동일한 경로에 다음과 같이 food.jsx파일을 생성해보겠습니다.

 

export const getPizza = () => {
    console.log('this is getPizza');
    return 'pizza';
}

export const getChicken = () => {
    console.log('this is getChicken');
    return 'chicken';
}

 

 

 

그리고 App.jsx는 다음과 같이 코드를 작성해보겠습니다.

 

import * as React from 'react';
import { getPizza } from './food';

const App = () => {

    React.useEffect(() => {
        getPizza();
    }, [])

    return (
        <div>
            <h2>Tree Shaking Test...</h2>
        </div>
    )
}

export default App;

 

 

 

코드를 보시면 commonjs에서 사용되는 require, module.exports가 아닌 import, export를 이용하여 모듈화, 모듈 로드 등을 해주고 있습니다.

 

이처럼 import, export만 사용해주면 되고 또한 최근 리액트로 만들어지는 프로젝트들은 대부분 ES6 모듈 포맷을 사용하기 때문에 크게 수정할 필요가 없는 부분이라고 생각됩니다.

 

 

 

[ 2. commonjs로 컴파일 되는 것 방지하기 ]

 

해당 부분은 babel설정을 다음과 같이 수정해주시면 됩니다.

 

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

 

 

반응형

 

 

[ 3. package.json에 sideEffects 속성 사용 ]

 

일반적으로 sideEffects가 발생될 것이라고 생각되는 코드들은 번들 파일이 실행될 때 영향을 미칠 수 있을 것이라고 판단하여 tree shaking이 적용되지 않습니다.

 

하지만 프로그램 실행에 영향을 미치지 않는다면 tree shaking대상으로 지정해주는 것이 올바른 선택입니다.

 

 

 

sideEffects가 발생될 것이라고 판단되는 코드들을 모두 tree shaking대상으로 지정해주기 위해서는 package.json에 다음과 같이 설정해주시면 됩니다.

 

{
  "name": "...",
  "sideEffects": false,
}

 

 

 

만약 일부 파일들에만 지정을 해주고 싶다면 다음과 같이 설정해주시면 됩니다.

 

{
  "name": "...",
  "sideEffects": [
    "./src/food.jsx",
    "*.css"
  ],
}

 

 

 

[ 4. webpack production mode 사용 ]

 

해당 설정은 webpack 설정 파일에서 다음과 같이 설정해주시면 됩니다.

 

module.exports = {
  // 개발모드, development or production
  mode: "production",
  
  ...
  
};

 

 

 

위의 4가지 설정을 모두 해줬다면 tree shaking 설정이 모두 완료되어 번들 파일 크기가 줄어드는 것을 확인하실 수 있습니다.

 

 

 

Webpack Mode 빌드 비교

 

tree shaking에 대해 공부를 하면서 차이점을 자세히 확인해보기 위해 여러 비교를 해봤습니다.

 

가장 먼저 webpack mode 설정 비교입니다.

 

 

 

우선 폴더 내부에 존재하는 파일들은 App.jsx, index.jsx와 위에서 생성한 food.jsx만 있습니다.

 

그리고 food에는 getPizza, getChicken이 모듈화가 되어 있지만 App.jsx에서는 getPizza만 가져와 사용하는 것을 볼 수 있습니다.

 

다른 말로는 프로그램 실행에 getChicken은 필요가 없는 소스코드입니다.

 

tree shaking이 정상적으로 적용되었다면 getChicken과 관련된 소스코드는 번들 파일에 존재하면 안 됩니다.

 

해당 비교를 webpack mode를 development와 production으로 변경해보면서 테스트해보겠습니다.

 

 

 

  • webpack mode: development
// bundle.js

/*! exports provided: getPizza, getChicken */
(function(module, __webpack_exports__, __webpack_require__) {
"use strict";
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"getPizza\", function() { return getPizza; });\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"getChicken\", function() { return getChicken; });\nvar getPizza = function getPizza() {\n  console.log('this is getPizza');\n  return 'pizza';\n};\nvar getChicken = function getChicken() {\n  console.log('this is getChicken');\n  return 'chicken';\n};\n\n//# sourceURL=webpack:///./src/food.jsx?");
}),

 

번들 크기

 

 

 

  • webpack mode: production
// bundle.js

function(e,t,n){
   "use strict";
   n.r(t);
   var r=n(0),l=n(2),a=function(){
     return r.useEffect((function(){
       console.log("this is getPizza")}),[]),
       r.createElement("div",null,r.createElement("h2",null,"Tree Shaking Test..."))};
   n.n(l).a.render(r.createElement(a,null),document.querySelector("#root"))
}

 

번들 크기

 

 

 

테스트해본 결과 development일 땐 번들 파일에서 getChicken과 관련된 코드를 확인할 수 있었지만 production일 땐 번들 파일에서 실제 사용되고 있는 getPizza와 관련된 코드만 확인할 수 있었습니다.

 

또한 번들 크기도 드라마틱하다고 할 수 있을 정도로 줄어든 것도 확인할 수 있었습니다.

 

 

 

함수 로드 방식 비교

 

tree shaking 관련된 내용을 공부하던 도중 알게 되었던 사실은 import를 해오는 방식에 따라서도 tree shaking 적용이 달라질 수도 있다는 것이었습니다.

 

위의 App.jsx파일처럼 실제로 사용될 getPizza만 불러오게 될 경우 getChicken은 번들 파일에서 사라지지만 import * as food from './food'와 같이 모든 함수를 불러올 경우 getChicken은 번들 파일에 나타난다는 것이었습니다.

 

해당 방식도 App.jsx 파일을 수정해보며 직접 비교를 해보겠습니다.

 

 

728x90

 

 

  • 모든 함수 로드하기
import * as React from 'react';
import * as food from './food';

const App = () => {

    React.useEffect(() => {
        food.getPizza();
    }, [])

    return (
        <div>
            <h2>Tree Shaking Test...</h2>
        </div>
    )
}

export default App;

 

// bundle.js

function(e,t,n){
   "use strict";
   n.r(t);
   var r=n(0),l=n(2),a=function(){
     return r.useEffect((function(){
       console.log("this is getPizza")}),[]),
       r.createElement("div",null,r.createElement("h2",null,"Tree Shaking Test..."))};
   n.n(l).a.render(r.createElement(a,null),document.querySelector("#root"))
}

 

번들 크기

 

 

 

  • 사용될 함수만 로드하기
import * as React from 'react';
import { getPizza } from './food';

const App = () => {

    React.useEffect(() => {
        getPizza();
    }, [])

    return (
        <div>
            <h2>Tree Shaking Test...</h2>
        </div>
    )
}

export default App;

 

// bundle.js

function(e,t,n){
   "use strict";
   n.r(t);
   var r=n(0),l=n(2),a=function(){
     return r.useEffect((function(){
       console.log("this is getPizza")}),[]),
       r.createElement("div",null,r.createElement("h2",null,"Tree Shaking Test..."))};
   n.n(l).a.render(r.createElement(a,null),document.querySelector("#root"))
}

 

번들 크기

 

 

 

테스트해본 결과 전혀 차이가 없었다는 것을 알 수 있었습니다.

 

정보가 잘못된 것이었는지 여기저기 찾아보다가 Webpack 4의 Tree Shaking에 대한 이해라는 다른 분의 블로그에서 이유를 확인할 수 있었습니다.

 

간단하게 이유를 말씀드리면 webpack 4 이후로는 자동으로 최적화가 된다는 것이었습니다.

 

제 webpack 버전이 4였기 때문에 자동으로 최적화가 되어 동일한 결과가 나오는 것으로 보입니다.

 

 

 

SideEffects 적용 비교

 

위의 비교들을 해보면서 sideEffects가 적용된 사례도 비교해보고 싶은 생각이 들어서 어떤 사례가 있을지를 찾아보았습니다.

 

여기저기 검색하던 도중 이곳에서 예시를 찾을 수 있었고 저만의 코드를 작성하여 테스트해봤습니다. (갓 스택오버플로우...)

 

모든 파일들은 App.jsx와 동일한 위치에 생성해보겠습니다.

 

 

 

[ 1. beef.jsx 파일 생성 ]

 

export default 'beef';

 

 

 

[ 2. pork.jsx 파일 생성 ]

 

export default 'pork';

 

 

 

[ 3. meat.jsx 파일 생성 ]

 

import beef from './beef';
import pork from './pork';

pork = pork + beef;

export {
    beef,
    pork,
};

 

 

 

[ 4. App.jsx 파일 수정 ]

 

import * as React from 'react';
import { getPizza } from './food';
import { pork } from './meat';

const App = () => {

    React.useEffect(() => {
        getPizza();
        console.log(pork());
    }, [])

    return (
        <div>
            <h2>Tree Shaking Test...</h2>
        </div>
    )
}

export default App;

 

 

 

 

위와 같은 상황은 meat.jsx파일에서 beef와 pork를 re-export 하는 상황입니다.

 

다만 여기서 pork = pork + beef라는 구문이 있지만 실행 결과에는 영향을 미치지 않는 구문입니다.

 

즉, sideEffects설정을 한 것이 tree shaking에 영향을 준다면 해당 코드는 번들 파일에서 보이면 안 되는 것이라는 의미를 가지게 됩니다.

 

package.json에서 sideEffects 설정을 변경해보며 테스트해보겠습니다.

 

 

 

  • sideEffects 설정 제거
{
  "name": "...",
  // "sideEffects": false,
}

 

// bundle.js

function(e,t,n){
     "use strict";
     n.r(t);
     var r=n(0),l=n(2);
     pork="porkbeef";
     var a=function(){
         return r.useEffect((function(){console.log("this is getPizza"),console.log("pork")}),[]),
         r.createElement("div",null,r.createElement("h2",null,"Tree Shaking Test..."))};
     n.n(l).a.render(r.createElement(a,null),document.querySelector("#root"))
}

 

번들 크기

 

 

 

  • sideEffects 설정 추가
{
  "name": "...",
  "sideEffects": false,
}

 

 function(e,t,n){
     "use strict";
     n.r(t);
     var r=n(0),l=n(2),a=function(){
         return r.useEffect((function(){console.log("this is getPizza"),console.log("pork")}),[]),
         r.createElement("div",null,r.createElement("h2",null,"Tree Shaking Test..."))};
     n.n(l).a.render(r.createElement(a,null),document.querySelector("#root"))
}

 

번들 크기

 

 

 

sideEffects 설정을 했더니 pork="porkbeef"라는 구문이 사라진 것을 확인할 수 있었고 그에 따라 번들 파일의 크기도 미세하게나마 줄어든 것을 확인할 수 있었습니다.

 

이처럼 sideEffects도 tree shaking에 영향을 미친다는 것을 확인할 수 있었습니다.

 

 

 

파일 구성

 

파일 구성

 

 

 

참조

 

webpack 공식 문서

Webpack 4의 Tree Shaking에 대한 이해

What Does Webpack 4 Expect From A Package With sideEffects: false

 

 

 

 

 

 

 

이상으로 tree shaking으로 최적화하는 방법에 대해 간단하게 알아보는 시간이었습니다.

 

읽어주셔서 감사합니다.

 

 

 

728x90
반응형

댓글