Tech윙잇 FE 개발 및 빌드 환경 개선기 (feat. Webpack)

조회수 929

안녕하세요, 윙잇 개발팀 커뮤니티셀 프론트엔드 개발자 김우조입니다.
최근 윙잇 개발팀의 규모가 커지면서 더욱 탄탄한 기반을 다지기 위해 팀 내부 개선 작업들을 진행하고 있습니다.
그 중, 윙잇 프론트엔드 프로젝트의 고질적 문제점이었던 개발 및 빌드 환경(webpack, babel, …)을 개선하기 위해 공부하고 개선한 경험을 공유하고자 합니다.


기존 윙잇 Front-end 개발 및 빌드 환경


*webpack config의 마지막 수정 날짜 


현 시점(2022. 05)을 기준으로 webpack 메이저 버전은 어떻게 될까요? 🤔

2020.10.10에 릴리즈된 webpack5입니다. 그럼에도 불구하고 윙잇의 프론트엔드 빌드 환경은 4년전에 CRA(Create React App) 기반으로 프로젝트를 만들고 eject한 webpack3을 사용하고 있었습니다. 빌드 시 사용하고 있는 loader, plugin 등도 4년 전의 패키지 라이브러리를 사용하고 있었죠.



비교적 최근 ES의 문법을 사용할 수 없었어요

기존의 노후화된 빌드 환경은 Node 버전을 올렸음에도 불구하고 Optional chaining 같이 비교적 최근의 문법을 사용하게 되면 Syntax error를 피하지 못하였습니다.




빌드가 오래 걸렸어요

윙잇 프로젝트는 1년 전과는 비교할 수 없을 정도로 매우 빠르게 성장했습니다.
기능들이 다양하게 추가되고 A/B 테스트 환경, API Mocking 환경 등 여러 환경 구성 및 사용 라이브러리들이 증가하면서 빌드하는 코드의 양도 어마어마하게 커졌습니다.

1년 전, 제가 쓰는 맥북 환경에서 Production 빌드 시간은 평균 60초였습니다.
최근에는 100 ~ 120초로 2배 가까이의 시간이 걸리게 되었습니다. 심지어 Jenkins 배포 자동화 환경 스테이지 중 프론트엔드 빌드 시간만 평균 200초 가까이 소요되었습니다.


*Local에서 진행한 production 빌드 시간

개발 환경에서 만큼은 빌드 시간을 줄여 보고자 하여 development 웹팩 설정을 최소화했을 때, 맥북 환경 기준 30초 정도로 단축하여 진행했지만 개발 모드 결과물이었고 여전히 배포할 때는 긴 시간이 소요되었습니다.



어떻게 개발 및 빌드 환경을 개선할까?

번들링 도구 최신화 하기
사용하고 있던 대부분의 loader 및 plugin 라이브러리들, 특히 babel-loader의 경우 webpack 버전에 대한 의존성을 가져 기존의 babel-core를 사용하는 라이브러리는 deprecated 되고 @babel/core로 개편되었습니다. 그리고 webpack3에서 webpack4로 업데이트했을 때 빌드 속도의 향상을 안내한 여러 사례들이 있습니다.

What’s new in Webpack 4
How to cut your Webpack build time in half

이러한 이유로 첫 번째, 번들링 도구의 최신화를 진행하기로 했습니다.


Smaller = Faster

앞에서 말씀드렸듯이 윙잇은 CRA 도구를 활용해서 만들어진 프로젝트입니다.
CRA는 손쉽게 프로젝트를 구성하고 개발할 수 있도록 만든 도구로써 저희가 사용하지 않는 loader나 plugin들이 다수 포함되어 있었습니다. 빌드 속도를 지연시키는 주요 원인이었기 때문에 사용하지 않는 plugin이나 loader들을 프로젝트에서 제거하기로 했습니다.


*출처: https://webpack.js.org/guides/build-performance


결과적으로 이 단계에서 빠르고 확실한 결과를 만들 수 있는 작업들은 아래와 같았습니다.

  1. 번들링 도구 최신화
  2. 사용하지 않는 loader, plugin and library packages 제거

그리고 CRA 기반의 빌드 환경을 특별하게 수정한 내역이 없으므로, 마이그레이션하는 것보다 리빌딩하는 것이 효율적이라고 판단했습니다.


Webpack 리빌딩

디렉토리 구조
webpack에서 소개하는 예제는 프로젝트의 root 경로에 webpack.config.js 파일을 만들어서 자동으로 webpack 설정을 탐색하여 번들링을 수행하도록 하지만, 윙잇 프로젝트의 구조는 webpack 관련된 부분이 config와 script 디렉토리 하위에 나눠져 있었습니다. 오히려 나눠져 있는 구조가 보기 편했기 때문에 아래와 같은 구조를 따르기로 했습니다.


config 디렉토리는 필요한 번들링 설정과 번들링에 사용하는 모듈들을 정의해놓고 scripts 디렉토리에 번들링을 수행하는 스크립트 파일을 작성하여 수행하도록 했습니다.


packages.json 수정하기
우선적으로 babel-core 같은 deprecated package들을 webpack5 버전에서 사용할 수 없으므로 예전 버전의 package들은 업그레이드하고 사용하지 않을 package들은 전부 삭제했습니다. 아래는 변경된 의존 모듈들의 일부분입니다. 


사용하지 않을 의존 모듈들 삭제


의존 모듈들 최신화
그 외 CRA에서 사용하는 proxy 설정 등, 당장 사용하지 않을 부분들은 전부 삭제했습니다.


Webpack 구성의 변경점
webpack의 버전을 업그레이드하면서 loader, plugin의 사용법이나 패키지 자체 등이 변경된 부분이 있었습니다. 또한, webpack에서 자체적으로 제공하는 기능들도 추가되어 기존 설정을 그대로 사용하기보다 webpack5에 맞게, 그리고 필요한 것만 사용하도록 설정을 다시 했습니다.

Babel 및 polyfill
Babel은 ES6 이상의 문법을 지원하지 않는 브라우저를 위해 ES5로 구문 변환해주는 자바스크립트 컴파일러입니다.

윙잇은 하위 브라우저 지원을 위해 babel/preset-env 프리셋과 CRA 에서 제공하는 babel-preset-react-app 프리셋을 적용하고 있었습니다. 하지만 Babel은 새롭게 정의된 함수나 객체에 대해서는 변환해줄 수 없으므로 하위 브라우저에 맞게 새롭게 정의할 필요가 있습니다. polyfill이죠.

기존에는 polyfill을 모아두는 js 파일을 만들어 전역으로 정의하고 빌드 시 함께 주입하고 있었습니다. 여기서 발생할 수 있는 대표적인 문제점으로 polyfill에 의한 전역 스코프 오염이 있었습니다. 전역 스코프가 오염되면 최신 브라우저 환경에서는 지원하는 메소드 등이 비효율적이게 동작할 수 있으며, 중복 오염이 있을 경우 이슈 트래킹이 어려울 수 있었기 때문에 개선할 여지가 있었습니다. 

이 기회에 위와 같은 문제점을 예방하기 위해 설정한 부분은 아래와 같습니다. runtime 환경에서 전역 스코프의 요소와 중복되지 않는 부분만 polyfill 스크립트를 호출할 수 있게 helper가 동작하도록 babel 설정했습니다. @babel/plugin-transform-runtime@babel/runtime-corejs3 패키지를 babel 구성에 추가하여 전역 스코프의 오염 없이 polyfill을 주입할 수 있었습니다.


윙잇의 FE 개발자들은 ECMA Script의 최신 문법들을 사용할 수 있게 되었답니다. 🙌


Webpack Loaders

윙잇 webpack은 CRA 기반답게 범용적으로 이루어져 있었습니다. 사용하지 않는 스펙에 대한 loader들도 세팅되어 있었는데, 대표적으로 post cssscss를 처리하기 위한 loader들이 있었습니다. 윙잇의 스타일은 현재 base를 담당하는 css 파일을 제외하고는 EmotionJS로 이루어져있기 때문에 위 loader는 제거했습니다.

그 외 file-loader, url-loader 같이 파일을 불러오거나 용량이 작은 파일은 문자열로 변환하는 로더는 webpack5에서 지원하는 asset type으로 전환했습니다.

file-loaderurl-loader는 webpack5에서는 deprecated loader입니다.
https://webpack.js.org/guides/asset-modules/  

eslint-loader는 실제 코드를 전부 검사하다보니 매우 많은 시간을 소요했습니다. IDE 환경에서 체크하는 것으로 충분하다고 판단, 번들링 과정에서 제외하였습니다.


Webpack Plugins

Minify plugin 변경
plugin의 대표적인 변경점은 minify 플러그인을 uglify에서 terser로 변경한 것입니다. 둘 다 번들 파일을 난독화하고 압축하는 프로세스를 진행하는데 변경한 큰 이유로는 아래와 같습니다.


*terser-webpack-plugin vs uglifyjs-webpack-plugin 트렌드 그래프


  1. UglifyJS는 2버전부터 ES6+ 지원을 중단하였으며 현재 유지보수도 이뤄지고 있지 않습니다. 그에 반해 Terser는 꾸준한 유지보수가 진행 중입니다.
  2. UglifyJS에 비해 Terser가 압축율은 비슷하면서도 최대 3배 이상의 매우 빠른 압축 속도를 보여주며 webpack 공식 문서에서도 TerserPlugin을 대표적으로 소개하고 있습니다.
    참고: Terser vs. UglifyJS - Dramatic Improvements

 윙잇 프로젝트로 비교한 결과는 아래와 같았습니다. 

case: uglifyjs-webpack-plugin                             case: terser-webpack-plugin
bundle size: 3.12MiB / avg. time: 46s                    bundle size: 3.15MiB / avg. time: 33s 


Compression plugin 삭제
번들링 결과물을 gzip으로 압축하는 플러그인인 CompressionPlugin 또한 제거했습니다.  프론트엔드 서버에서 gzip 압축을 진행해도 캐싱하기 때문에 큰 부하가 없을 것이라고 판단하고, 빌드 시간을 줄이고자 SSR 서버에서 compression 패키지를 사용하는 것으로 대체했습니다. 

ReactRefreshWebpackPlugin 추가
개발 환경을 개선하기 위해 추가한 플러그인입니다. HMR(Hot Module Replacement)을 사용하는 webpack-dev-server 환경에서 페이지의 새로고침 없이 변경된 부분을 반영하는 플러그인입니다. 상태를 유지한 상태로 코드 수정이 필요한 경우 매우 유용하여 개발하는 시간을 좀 더 효율적으로 운용할 수 있게 되었습니다.


Output
이번 작업의 목표는 빌드 속도와 개발 환경의 개선이었습니다. 개발 환경은 주관적이지만 개인적으로 많이 개선됐다고 생각됩니다. 객관적인 수치 상으로 표현할 수 있는 빌드 속도의 차이는 아래와 같습니다.


끝으로

이제서야 빌드 환경을 쌓아가는 게 어려운 일이 아니라고 생각됩니다만, 처음에는 4년 동안 유지보수되지 않던 webpack을 변경하는데에 두려움이 있었습니다.

현업 프론트엔드 개발자로서 당연히 알고 있어야 할 지식이지만 화면을 구성하는 것이 아닌 환경을 구성하는 것이기 때문에 어렵다는 인식이 저도 모르게 자리 잡고 있었을지도 모릅니다.

하지만 신입으로 입사 후 꼭 하고 싶었던 일 중 하나였고 개선 후에는 좋은 지식이 되어 기반을 탄탄하게 만들어준 것 같습니다. 하고 싶은 일을 할 수 있도록 지원해준 윙잇 개발팀 여러분이 있었기에 재미있게 진행할 수 있었던 것 같습니다.

이 글을 읽고 계신분들 중 프론트엔드 개발자지만 webpack과는 아직 친해지지 않은 분들께서 좀 더 친해질 수 있는 계기가 되었으면 좋겠습니다!

보다 좋은 번들러를 찾고 효율적인 번들링을 수행하도록 하는 것도 또 하나의 과제이며, SSR 개발 환경 구축과 monorepo 환경 도입, 프론트엔드의 CI/CD 개선도 생각하고 있습니다.

여러분과 함께 지식을 공유하고 같이 성장할 수 있는 기회가 있으면 더더욱 좋겠다고 생각합니다! 감사합니다. 😄