빌드성능 개선하기 Webpack + ESbuild

그니·2021년 11월 2일
8

오랜만의 포스팅입니다 :)

이번에는 저희 프로젝트에서 빌드성능 개선한 후기를 포스팅 하려 합니다.

항상 말씀드리지만 가이드 같은것들은 워낙 좋은 글 들이 많아서 그걸 보시면 될것 같고 저는 실무를 하면서 겪을 수 있는 어려운 이슈에 대한 경험 및 고도화 위주로 포스팅 하려고 노력하는데요. 이번 빌드성능 개선 건은 정말.. 이미 크게 서비스되고 있는 환경에서 환경개선을 하려다 보니 고생을 너무 많이 한것 같네요 😥

자 그럼 시작합니다.

1. 빌드환경 개선의 필요성

처음에는 다들 그렇듯 작은 서비스로 시작하였습니다. 솔직히 이정도의 서비스까지 커질줄 몰랐는데 점점 런칭되는 서비스가 늘어나고 입점과 동시에 개편과 개선을 진행하면서 프로젝트가 상당히 거대해졌습니다. 그러다보니 지난번 청크파일 분리 등등 여러가지 개선방법을 다양하게 생각하게 되었고 빌드시간도 점점 늘어나게 되다보니 개선의 필요성을 느끼긴했었는데, 서비스가 더더욱 커지기전에 진행을 하는게 맞을것 같아 진행을 하게 되었습니다.

처음부터 제가 선견지명이 있었으면 구조작업을 이렇게 하지않고 서비스 별로 분리를했을텐데.. 몇번을 생각하긴하지만 우리의 시작은 항상 작은 서비스였으니까요^^

다들 이런 경험은 있으시죠? (저만 그런게 아니라고 말해주세요..)

2. 개선전 자료 리서칭

일단 개선전 리서칭을 진행해봤습니다. 대충 알고있는 방식들 외에도 좋은방식이 있을까 찾아봐야해요. 일단 PnP 방식을 재택하고 있는 Yarn2 (Berry), 네이티브 코드(Go)로 작성된 순수 esbuild로만 환경구성을 하는 방식이 눈에 제일 많이 들어왔습니다. 또 모노리스(Monolith) 기준으로 Travis CI의 github Actions처럼 node_modules의 캐싱처리가 젠킨슨CI 환경에서 처리되는 방식도 있었는데요. (Travis에선 package-lock.json파일의 해싱값을 가지고 캐싱하는 듯 합니다.) 이것저것 찾아보고 여러가지 검토 결과 결국 저희 프로젝트에서는 esbuild-loader를 활용하여 Webpack + ESbuild 방식으로 적용하는걸로 결정하게 되었습니다.

3. 해당 방식 선택이유

  1. 일단 현재 서비스중인 거대한 프로젝트라서 개편을 보수적으로 접근해야했습니다. 저희는 새로 시작하는 프로젝트나 규모가 작은 프로젝트의 개선부터 Yarn2, 순수 esbuild 환경으로 구성하기로 협의했습니다.

  2. 개선을 하려는 프로젝트는 현재 상황에서는 현실적으로 개편이 어려운 프로젝트인데 꼭 모노레포(monorepo)가 기준이 될 필요는 없었습니다. 현재 상황에 맞게 최선의 환경으로 개선을 하는게 목표였습니다.

  3. 지금 환경인 Webpack에서도 esbuild-loader 같은것들은 지원하니 loader들을 검토하여 추가 적용하는 방식으로 채택하였습니다.

  4. 저희 사내에서 사용되는 CI는 jenkins이지만 Travis CI와는 다른방식으로 캐싱처리를 하고있습니다. 해서 Travis CI 인프라적인 이유로 논외로 하였습니다.

3. 개선 시작

프로젝트 마다 환경이 다를테니 개선과정으로는 진행했던 방식을 대략적으로 설명하려고 합니다. 저희 프로젝트는 React CRA(create-react-app)로 구성되어있으니 해당 기준으로 서술합니다.

  1. 먼저 첫번째로는 smp(speed-measure-webpack-plugin)를 이용하여 현재 빌드속도를 체크합니다. 저희 프로젝트에서는 측정해보니 빠르면 1min 30secs 평균 약 1min 50secs ~ 2min 10secs까지 측정이 되었습니다. 아무리 거대해졌다해도 너무 느립니다.😩

  2. 일단 저희 프로젝트는 CRA2로 구성되어 eject가 된 상태였습니다. eject를 하게된다면 버전에 종속성을 갖게되어 자유롭게 CRA버젼업에는 어려움이 있습니다. 해서 프로젝트를 새로 구성하여 esbuild-loader를 적용하기로 하였습니다.

  3. CRA를 최신버젼인 4까지 버전업을 하였습니다. react의 버전도 16.X 버전에서 17.X버전으로 업데이트를 하였습니다.

  4. 프로젝트의 구조를 src 폴더부터 그대로 새로 구성한 프로젝트에 옮깁니다. 이때 저는 package.json에서 사용하던 모듈도 하나씩 옮기면서 안쓰는 모듈들을 모두 정리하였습니다.
    여기서 버전업을 하면서 모듈이 동작하지않는 것들이 하나씩 생길텐데요. 차근차근 migration을 진행해줬습니다.

  5. 프로젝트를 구동시켜봅니다. 여기서부터 어려움과 난관에 많이 봉착했는데요. 환경이 달라지다보니 로컬구동부터 쉽지 않았습니다. 구성하면서 있었던 이슈는 5번 항목에서 정리하여 작성하도록 하겠습니다.

  6. 우여곡절 끝에 로컬 구동을 시켰다면 이제 기본 동작을 확인합니다. 이때는 기간을 잡고 저희팀원들 다같이 브랜치를 하나 생성해서 확인 및 체킹하는 시간을 가졌습니다.

  7. CI/CD에 올려 배포해봅니다. 역시나 여기서도 이슈가 발생하는데요. 빌드가 되지않고 실패하는 이슈가 생겼습니다. 이때는 과감하게 빌드 스크립트를 수정해주시면 됩니다. 환경에 따라 다르겠지만 npm cache verify 또는 npm cache clean을 해주시거나 그래도 해결이 되지 않는다면 node_modules 폴더와 package-lock.json 파일을 강제로 삭제해주시면 거의 해결됩니다. (물론 사내의 CI/CD환경을 어느정도 파악을 하고 진행해주셔야합니다.)

4. 개선 진행시 이슈발생 및 해결

  1. smp(speed-measure-webpack-plugin)의 적용방식은 react-app-rewired craco 같은 모듈을 사용하신다면 관련 config.js에 아래와 같이 적용하시면 됩니다.

config-overrides.js 또는 craco.config.js

const smp = new SpeedMeasurePlugin()

module.exports = {
  webpack: smp.wrap({
    configure
  })
}
  1. CRA2에서 cross-env로 path 설정을 했던 부분이 CRA4로 버전업을 하다보니 이슈가 발생하였습니다. 해당 이슈로 고생을 조금했던 기억이 있는데요. 기존 package.json에서 cross-env로 src path 설정을 하셨다면 새로 구성하신 프로젝트에서는 jsconfig.json 파일을 생성해서 셋팅하시면 됩니다.

jsconfig.json

{
  "compilerOptions": {
    "baseUrl": "src"
  },
  "include": [
    "src"
  ]
}
  1. CRA가 버전업이 되면서 file-loader의 방식도 변경되었습니다. 기존에 부르던 방식을 아래 코드와 같이 수정해주셔야 합니다. 아니라면 file-loader의 옵션에서 esModule:false 값을 입력하여 CommonJS 모듈 구문의 활성화도 가능합니다.

example

// AS-IS
<img src={require('image.png')}/>
  
// TO-BE
<img src={require('image.png').default}/>
  1. 처음 구현시에는 react-app-rewired로 코드를 구현하여 esbuild-loader를 직접 주입시켰으나, 기존 로더를 지워줘야하는 등 코드가 상당히 복잡해지는 이슈가 발생하였습니다. 관련하여 craco, craco-esbuld등을 리서치하여 적용 시켜줬습니다. 해당 모듈 사용을 추천드립니다.

craco.config.js

const smp = new SpeedMeasurePlugin()
const cracoEsbuildPlugin = require('craco-esbuild')

module.exports = {
  plugins: [{ plugin: cracoEsbuildPlugin }],
  webpack: smp.wrap({
    configure
  })
}
  1. 저희 프로젝트에서는 svg를 리액트 컴포넌트화해서 사용하고 있는데요. svg-loader관련해서 rewireSvgReactLoader로 적용시켜주었습니다. 동일한 방식으로 사용하시는분들은 같은 모듈을 적용하셔도 될것 같고 svgr을 사용하실 분들은 svgr 방식으로 적용해주셔도 됩니다. 아래 두가지 방식중에 하나를 선택해서 적용해주시면 됩니다.

craco.config.js

const smp = new SpeedMeasurePlugin()
const cracoEsbuildPlugin = require('craco-esbuild')
const rewireSvgReactLoader = require('react-app-rewire-svg-react-loader')

module.exports = {
  plugins: [{ plugin: cracoEsbuildPlugin }],
  webpack: smp.wrap({
     configure: (webpackConfig, { paths }) => {
      // SvgReactLoader
      webpackConfig = rewireSvgReactLoader(webpackConfig, { paths })
       
      // @svgr
      webpackConfig.module.rules.unshift({
        test: /\.(svg)$/,
        exclude: /node_modules/,
        use: [
          {
            loader: '@svgr/webpack',
            options: { esModule: false }
          }
        ]
      })

      return webpackConfig
    }
  })
}

5. 개선 후 결과

위와 같은 우여곡절을 겪으며 개선한 결과는 놀라웠습니다. 일단 첨부 이미지부터 보시겠습니다.

개선 전 smp 측정값

개선 후 smp 측정값

기존 평균 1분50초 ~ 2분10초대의 빌드시간대에서 30초대로 줄었습니다.
각 로더들의 측정값도 줄었는데요. 기대했던 것보다 엄청난 개선이었습니다.

저희와 같이 지금 프로젝트 환경(Webpack)에서 개편이 쉽지않다고 생각하시는 분들은 글을 참고해서 천천히 빌드 개선을 해보시는것도 좋은 경험이 되실 것 같습니다🙂


끝으로 읽어주셔서 감사하고 오랜만의 포스팅이다보니 또 계절이 지나갔네요.
늦가을 일교차 추위 조심하시기 바랍니다 :)

profile
Front-end Developer, Fullstack Developer, Web Developer

0개의 댓글