본문 바로가기

프로그래밍 공부내용/자바스크립트(JS)

기깔나게 webpack build 속도 최적화하기

안녕하세요!

오늘은 webpack 빌드속도를 개선했던 이야기를 해보려고 합니다.

 

저희 '땡쿠' 프로젝트는 CRA를 사용하지 않고 webpack을 이용해서 react project를 빌드해서 사용했었습니다.

 

'왜 CRA를 쓰지 않았나요' 에 관한 이야기는 다음에 하기로 하고,

 

오늘은 '어떻게 webpack을 사용했나요' 에 초점을 맞춰볼까 합니다.

 

 

 

우리는 잘 하고 있었나?

예전에 '부탄' 이라는 나라가 행복지수가 가장 높은 곳으로 소개된 적이 있습니다.

부탄의 행복한 모습(http://thetomorrow.kr/archives/2219)

 

지금은 그렇지 않은데요.. 기술이 발달하면서 부탄사람들이 다른 사람들은 어떻게 사나 알게 되면서 행복지수가 많이 감소했다고 해요.

 

왜 이런 이야기로 시작하냐고 하면...

 

저희 프로젝트가 그랬습니다...

 

웹펙으로 빌드하면서 빌드시간이 32초정도 걸렸습니다.

 

초기 빌드시간.. 답도없이 길다

웹팩 이야기를 하다가 다른 팀의 빌드 시간을 들었더니 10초가 안걸리더군요.

 

그걸 듣고 "아차!" 했습니다. "나는 웹팩을 잘 못 쓰고 있었구나."

 

분명 최적화 하는 방법이 있을 것이기에 이를 공부해야겠다고 생각했습니다.

 

Devtool Options

webpack 번들링 중 source-map에 관한 옵션입니다.

 

소스맵(=source map)이 무엇인가하면, 빌드한 파일과 원본 파일을 맵핑해주는 겁니다.

 

성능 최적화를 위해 HTML, CSS, JS와 같은 웹 자원들을 압축합니다.

 

이때 압축한 파일 어디에서 오류가 생겼는지를 쉽게 파악하기 위해서 소스맵을 만듭니다.

 

이 소스맵을 만드는 방식에 따라서 빌드시간에 차이가 나게 됩니다.

 

웹팩에서 제공해주는 아래 표를 보면 이해가 가실겁니다.

devtool 성능 production 설명
(none) false를 사용 build: 가장 빠름 rebuild: 가장 빠름 yes 최대 성능을 갖춘 프로덕션 빌드를 위해 추천하는 옵션입니다.
eval build: 빠름 rebuild: 가장 빠름 no 최대 성능을 갖춘 개발 빌드를 위해 추천하는 옵션입니다.
source map build: 가장 느림 rebuild: 가장 느림 yes 고품질 소스맵을 포함한 프로덕션 빌드를 위해 추천하는 옵션입니다.

속도가 어느 정도인지 빠름, 느림 정도로는 이해가 가지 않아서 직접 비교 해봤습니다.

 

false
eval
source map

false, eval, source map 순으로 41초 32초 49초가 걸렸습니다.

 

production으로 추천되지 않는 eval은 dev환경에서 사용해서 17초 정도를 아낄 수 있었습니다.

 

production에서는 default 로 설정되는 none을 사용해 빌드시간을 8초정도 아낄 수 있었습니다.

 

로더 (module rules)

1. ts-loader와 babel-loader

ts-loader와 babel-loader가 둘다 타입관련한 업무를 하고 있기 때문에 한개만 사용해도 충분합니다.

 

ts-loader는 ts -> js로 변환해 주는 역할이고, babel은 js(es6) -> js(es5) 로 변환해 주는 역할이라 같이 사용해야 하지만

//bable config
module.exports = {
  presets: [
    ['@babel/preset-react', { runtime: 'automatic', importSource: '@emotion/react' }],
    ['@emotion/babel-preset-css-prop'],
    '@babel/preset-env',
    '@babel/preset-typescript',
  ],
  plugins: ['@emotion/babel-plugin'],
};

에 babel-preset을 추가해서 polyfill을 이용해 비슷한 역할을 해 줄 수 있습니다.

 

둘은 동작방식에 차이가 좀 있습니다.

 

1) ts loader는 컴파일 결과를 화면에 보여줍니다.

2) babel은 타입 주석을 제거한다.

https://babeljs.io/docs/en/#type-annotations-flow-and-typescript

이 주석제거에 관해서 약간의 차이가 있습니다.

import { Coupon, CouponDetail } from '../../types/coupon';

const CouponDetail = ({ couponId }: { couponId: number }) => {
    ...
    return (
        ...
        <CouponDetailReservation couponDetail={couponDetail as CouponDetail} />
        ...
    )
}

이런 컴포넌트가 있을 때 문제가 무엇일까요?

 

ts-loader와 함께 있을 때는 뱉지 않는 오류를 babel-loader만 사용했을 때는 뱉어줍니다.

 

as CouponDetail 의 타입으로써의 CouponDetail과 컴포넌트명인 CouponDetail을 구별하지 못하는 모습을 보입니다.

 

아마 타입주석만 체크하기 때문에 ts-loader가 컴파일시에 타입체크를 실행하는 방식과는 차이가 있어서 생기는 문제인 듯 합니다.

 

해당 타입 에러를 제거한 뒤 실행했더니

 

(devtool=false, module rules = bable-loader만 사용)

로 10초 가량 시간을 단축할 수 있었습니다.

 

하지만 emotion에서 prop을 내려주기 위해서는 type을 이용해야 했기 때문에 loader는 그대로 사용하기로 했습니다.

 

2. css Loader

이모션(emotion)을 사용해서 css-in-js를 하고 있기 때문에 css 와 style-loader가 필요 없었는데 포함 돼 있더군요.

 

이미 사용하지 않기 때문에 시간에 유의미한 차이는 없지만 번들할때 사용하지 않기 때문에 용량면에서 이득이 있을 겁니다.

 

3. 이미지파일

module: {
    rules: [
      {
        test: /\.(png|jpe?g|gif|svg|ico|webp)$/i,
        type: 'asset/resource',
        generator: {
          filename: '[name][ext]',
        },
      },
    ],
  },

로 file-loader 대신 asset/resource 설정해 줬습니다.

 

webpack5부터 file-loader가 내장이기 때문에 해당 방식으로 처리해주는게 좋습니다.

 

미니마이저

웹팩은 번들사이즈를 줄이기 위해서 minimize를 실행합니다.

 

minimize하는 시간도 빌드시간입니다.

 

dev에서는 작은 번들사이즈를 통한 빠른 로드에 대한 이점이 필요없기 때문에 우리는 사이즈를 포기하고 시간을 택해보도록 합시다.

//webpack.config.js
optimization: { minimize: false }

를 추가하면 minimize 옵션을 사용하지 않습니다.

 

 

적용했더니 dev기준 10초정도 줄었다...!

 

production에서는 적용하지 않을 것이기 때문에 기억해 둡시다.

 

캐싱

실질적으로 가장 많은 시간을 줄여줬던 옵션입니다.

filesystem cache

모듈과 청크파일을 생성할때도 캐싱을 할 수 있습니다.

이를 이용해서 시간을 단축 할 수 있었습니다.

 

dev에서는 원래 ‘cache: { type: 'memory' }’ 가 default 설정이고(cache:true랑 같다고 봐도 무방)

prod에서는 ‘false’가 기본설정이라고 합니다.

 

//webpack.config.js
cache: { type: 'filesystem' }

를 추가해 주면 캐싱을 시작합니다.

 

자 실험해 볼까요?

 

 

오잉 아무 차이가 없다?

 

당연하죠 캐싱이니까 두번째 실행부터 빨라질 겁니다.

 

 

2초로 엄청나게 빨라진 것을 볼 수 있습니다.

 

하지만 진짜 빨라진 것일까요? 이것은 변경점이 없는 Happy Case가 아닐까요?

 

궁금해서 파일을 하나 만든다음 다른 파일에서 import 해서 사용하도록 해 보았습니다.

 

트리쉐이킹을 당하지 않고 잘 섞여들어간 파일이 있을 때 어느정도 효율인지 봅시다.

 

 

돌려보면 4초정도로 아까보다는 느려졌지만 확실히 캐싱을 통해서 빌드를 개선하고 있었습니다.

 

 

빌드 상태를 보면 cache-able한 모듈들을 파악하고 이를 이용하는 모습도 볼 수 있습니다.

 

파일단위로 변경점을 확인하고 캐싱하는 것으로 보입니다.

 

 

돌아보기

위의 과정을 전부 jenkins를 통해서 ec2환경에서 최대한 prod와 dev 환경과 비슷하게 실행하면서 비교해봤습니다.

 

결론적으로

 

cache:filesystem, minimizer, loader, source-map의 옵션을 적용해서 시간을 줄였는데요,

 

 

젠킨스에서 확인한 모습

마지막까지 적용하니까 웹팩을 포함한 전체 빌드시간이 62초 -> 22초 까지 줄어든 모습을 볼 수 있었습니다.

 

항상 젠킨스가 다 돌 때까지 기다리면서 하염없이 기다리던 시간은 이제 바로 개발할 수 있을 정도의 짧은 빌드타임으로 바뀌었습니다!!

 

아마 cra였다면 craco와 같은 도구를 추가 설치해서 설정을 바꿔줘야 했을겁니다..

 

아쉬우신분들은 speed-measure-webpack-plugin을 이용해서 각 단계별로 웹펙에서 시간을 얼마나 잡아먹는지 볼 수 있습니다.

 

 

여러분도 프로젝트에 적합한 webpack설정을 통해서 더 나은 생산성을 향해 가보시길 바랍니다.