Webpack → Vite: 번들러 마이그레이션 이야기

Webpack → Vite: 번들러 마이그레이션 이야기

개발 경험과 확장성. 두 마리 토끼를 잡기 위한 마이그레이션 경험기

Chanhee Lee — 이찬희 @hiddenest
로컬에서 빌드를 돌리다보면 느린 속도에 답답함을 느끼곤 합니다. 많은 이유가 있지만 하나만 꼽는다면 기능이 늘며 코드 베이스가 큰 폭으로 증가했기 때문입니다. 당장 서비스를 시작한 2016년보다 2.7배 정도 많은 기능을 제공하고 있습니다. 기능이 늘어나도 개발 경험이 불편해지는 것은 정말로 막고 싶었습니다.
이번 글에서는 Airbridge 대시보드의 번들러를 ‘Webpack’에서 ‘Vite’로 마이그레이션하면서 겪었던 점을 공유하려고 합니다.

Webpack, 그리고 번들러 춘추천국시대

기존에는 Webpack을 configuration을 직접 작성하여 쓰고 있었습니다. 서비스 출시 당시에는 CRA(Create React App)도 없었고, 지금처럼 다른 번들러들이 많지도 않아서 다른 선택지가 없었습니다. (그리고 필자도 없었습니다)
Webpack은 지금도 많이 쓰이는 번들러로 여러 장점을 가지고 있습니다.
10년 동안 개발, 관리되며 Plugin, Loader 등 생태계가 잘 갖춰져 있다
Code Splitting이나 Tree Shaking 등을 잘 지원한다
모듈을 IIFE 방식으로 묶어주어 여러 브라우저를 지원할 수 있다
다양한 Boilerplate에서 쓰고 있어 자료가 많다 (CRA 등)
...
하지만, 시간이 지나고 웹이 발전하면서 많은 것이 바뀌기 시작했습니다. ES Module을 브라우저에서 지원하여 <script type="module">로 불러올 수 있게 되었고, 에버그린 브라우저(Evergreen Browser)가 아닌 브라우저들을 지원하지 않는 비율이 급격하게 늘어났습니다.
그중에서도 번들러 쪽은 최근 1~2년 사이에 급격한 변화의 바람이 불었습니다. Native 바람이 불며 바야흐로 ‘번들러 춘추전국시대’가 열린 것입니다.

Native를 빌려

보통 번들링을 하며 다양한 작업이 동시에 이뤄집니다. 예를 들어, TS → JS로 트랜스파일링하거나, 플러그인과 폴리필(Polyfill)을 적용합니다. 대부분의 번들러는 NodeJS 기반으로 돌아가며, 번들링 중 진행되는 트랜스파일링 등 역시 대부분 NodeJS로 돌아갑니다. 문제는 NodeJS는 싱글 스레드 구조다보니 이로 인한 처리 한계가 발생합니다.
위의 동작을 Native 영역에서 돌려 성능을 높이려는 시도들이 나타나기 시작했습니다. Golang을 기반으로 하는 esbuild, Rust를 기반으로 하는 SWC가 대표적입니다. 특히 esbuild는 안정성이 높아 다른 번들러에서 Vite, Snowpack 등에서 래핑하여 사용하고 있습니다.

1차 시도 — esbuild만을 사용한 번들링

어떤 번들러를 선택할지 고민하다 esbuild를 코어로 사용하는 번들러를 사용하면 좋겠다고 생각했습니다. (당시 기준으로) 안정성이 높고 생태계가 잘 구성되어 있었기 때문입니다. 하지만 그전에 esbuild 자체의 성능을 확인해보고 싶었습니다. 그리고 esbuild만을 사용할 수 있는지도 알아보고 싶었습니다.
esbuild 패키지를 설치한 뒤 빌드 스크립트를 작성하기 시작했습니다. 설정 방식은 Rollup과 비슷한 느낌을 많이 받았습니다. 기본적인 entry, output 등을 설정한 뒤, Sass나 SVGR 등 필요한 플러그인을 추가로 설치해 적용했습니다.
기초적인 설정을 한 후 빌드를 돌렸는데 정말 큰 충격을 받았습니다. 기존에 210초 정도 걸리던 빌드가 2.16초만에 끝났습니다. 수치만 보았을 때 약 100배 빠른 빌드 속도를 보인 것입니다.
16-Inch Macbook Pro i7, 2018 Late 기준
하지만 아쉽게도 esbuild를 그대로 사용할 수는 없었습니다. 그 자체로는 해결하기 어려운 문제가 있었기 때문입니다.
먼저, EmotionJS로 인해 문제가 발생했습니다. Emotion을 사용하면서 개발 편의를 위해 @emotion/babel-plugin을 적용했는데, 이로 인해 빌드 시 Babel을 먼저 돌린 후에 esbuild를 실행해야만 했었습니다. Babel 설정을 더했더니 빌드 시간이 25초 정도로 늘어났습니다.
또한, HMR(Hot-Module Replacement)을 공식 지원하지 않아 어려움이 있었습니다. esbuild는 어떤 모듈이 변경되었는지 알려주지 않습니다. SDK팀의 수범님께서 web-dev-serveresbuild를 활용해서 HMR을 구현해주셨지만, 완전히 문제를 해결할 수는 없었습니다. 전체 컴포넌트가 Re-rendering되어 변경된 컴포넌트에만 HMR을 적용하려고 하였으나 공수가 부족했습니다. 무엇보다 플러그인을 작성하고 유지/보수/관리하는 것 역시 비용이 너무 크다고 판단했습니다.
위의 테스트를 진행하며 이점과 함께 한계 사항을 파악할 수 있었습니다. 이를 토대로 어떤 번들러를 선택하면 좋을지 정리해보았습니다.
Babel 트랜스파일 지원
Dev-Server, HMR 속도가 빨라야 함
Soft-Landing이 가능한지, 생태계가 잘 구축되어 있는지 등
...그리고 고민 끝에 Vite를 사용하기로 결정했습니다.

2차 시도 — Vite를 사용한 번들링

Vite는 빠르고 간결한 개발 경험에 초점을 맞춘 번들러입니다. 원래는 Vue를 위해 만들어졌으나, 지금은 React 등 다른 프레임워크와 라이브러리도 지원합니다. 기본적으로 ES Modules를 사용하며, Rollup처럼 쉽게 설정할 수 있습니다. HMR 등 부가적인 기능 역시 기본적으로 지원합니다.
가장 마음에 들었던 컨셉은 Dependencies(패키지)와 Source Code(소스 코드)를 분리하여 빌드하는 점이었습니다.  패키지는 설치 후에는 내용이 바뀌지 않습니다. 반면  소스 코드는 빈번하게 바뀝니다.
이 점에 착안하여 패키지는 esbuild로 미리 트랜스파일링해놓은 뒤, 로컬에서 개발 서버를 띄우면, 소스 코드를 불러오면서 의존성이 있는 패키지만 가져옵니다. 한 번 빌드한 결과는 캐싱을 해두어 다음 개발 빌드 때 바로 뜹니다.
Bundle-Based Dev Server vs ESM-Based Dev Server (출처: Vite 홈페이지)
마이그레이션 과정은 빠르고 편하게 진행되었습니다. 먼저, CRA와 비슷하게 Vite에서 제공하는 React + TypeScript 템플릿으로 기반을 만들었습니다.
yarn create vite 프로젝트명 --template react-ts
Bash
복사
그 뒤에 SVGR, tsconfig-paths 등 필요한 플러그인을 더하고, 기존에 사용하던 port를 적용했습니다. 설정 파일을 TypeScript로 쓸 수 있어서(Node v16 이상 권장), 모르는 부분은 쉽게 Definition을 찾아 읽으며 작성했습니다. 기존 300줄이 넘던 설정 파일이 50줄 내외로 줄어들었습니다.

마이그레이션 중에 만난 이슈들

환경 변수

먼저 흔하게 사용하던 환경 변수(process.env.*)에서 문제가 생겼습니다. 설정 완료 후 아무 생각없이 빌드를 돌렸는데, ‘process를 찾을 수 없다’는 에러가 뜬 것입니다. 무슨 문제인지 들여다보았더니, Vite에서는 import.env.VITE_* 만을 코드 내의 환경 변수로 사용할 수 있다는 것을 알게 되었습니다.
모든 환경 변수의 이름을 변경하기에는 코드 수정으로 인한 사이드 이펙트가 걱정되었기 때문에, Vite 설정 파일의 define을 사용하여 다른 string으로 치환하는 방법을 사용했습니다.
define: { 'process.env.NODE_ENV': `"${mode}"`, 'process.env.API_PROXY': `"${process.env.API_PROXY || ''}"`, ... },
JavaScript
복사

빌드 시 메모리 부족 문제

Netlify에서 빌드를 돌리는 중에 알 수 없는 이유로 빌드가 계속 실패했습니다. 딱히 변경한 것이 없었기에 의아해서 이것저것 찾아보기 시작했습니다. 문제 자체는 Node에 할당된 메모리가 적어서 발생했습니다. 그렇다면 무엇이 메모리를 잡아먹었을까요?
곰곰이 생각하다 ‘Sourcemap’ 옵션을 켠 것이 원인이 아닌지 고민하게 되었습니다. 그래서 옵션을 끄고 빌드를 돌렸더니 문제 없이 동작했습니다(...)
하지만 Sourcemap을 지울 수는 없으므로, 빌드 실행 전에 Node의 메모리 한도를 늘려주어 이슈를 해결했습니다. 다만 여전히 좀 찜찜합니다
export NODE_OPTIONS='--max-old-space-size=6400' yarn build
Bash
복사

그 외의 자잘한 이슈들...

사용하고 있는 라이브러리 중 ‘React Virtualized’에서 ESM import 이슈가 있어 전체 빌드가 안되는 문제가 있었습니다. 다행히 다른 사람들도 동일한 이슈를 겪고 있었고, UMD 폴더 내의 파일로 빌드되도록 바꿔주는 별도의 esbuild 플러그인을 만들어 주입했습니다.
빌드 커맨드는 같아도 번들러가 바뀐다면 Netlify에서는 빌드 오류가 발생했습니다. 이 문제는 마땅한 해결 방법이 없어서, 기존 프로젝트를 삭제한 뒤 새로 만들어 문제를 해결했습니다. 참고로 Vercel은 문제가 없습니다.

번들러 마이그레이션 이후, 앞으로의 아이템

Vite로 옮긴 후 빌드 시간이 기존 Webpack 대비 절반 이상 줄어들었습니다. Netlify의 빌드 평균 소요 시간을 보면, 기존에 평균 250초 걸리던 빌드가... 평균 90초. 즉 36% 수준으로 줄어들었습니다.
빌드 시간이 줄며 비용을 절감한 곳도 많습니다. Netlify를 예로 들면, 같은 시간에 더 많은 빌드를 돌릴 수 있게 되다보니, 시간 초과로 인한 추가 과금도 줄어들었습니다.
팀원들도 빠른 개발 빌드 속도에 만족한다는 의견을 많이 주었습니다. 특히, 빌드 설정을 쉽게 할 수 있다보니 팀원들이 더 많은 시도를 하기 시작한 것도 좋았습니다. 설정 파일이 쉽고 템플릿처럼 만들 수 있다는 점이 정말 큰 것 같았습니다. 이를 활용해 목적에 특화된 빌드 환경을 구성하거나(유닛 / E2E 테스트 등), 슬랙 봇에 연동해 테스트 환경을 생성할 수 있게 되었습니다. 보다 자세한 내용은 다른 글에서 자세히 공유드리고자 합니다.
빌드 속도를 빠르게 만들기 위해 할 수 있는 다른 작업도 많을 것입니다.
Emotion을 Babel 의존성 없는 스타일 라이브러리로 바꾼다면?
Monorepo로 구조를 변경하는 것도 선택지가 될 수 있을까? (Yarn Workspace, Nx...)
SWC가 더 안정화되어 Babel(혹은 Vite까지)을 대신할 수 있게 된다면?
5년 넘게 쓰던 Webpack을 걷어내는 일을 잘 할 수 있을지 걱정이 많았습니다. ‘5년’이라는 시간, 그리고 ‘잘 돌아가던 것’이라는 현재 상황이 있었기 때문입니다. 그래서 더 많이 팀원들과 이야기를 나누고, 작업을 작게 쪼개서 실험을 하며 적용했습니다. 그랬더니 생각보다 두렵지 않았습니다. 마이그레이션으로 인한 장애도 발생하지 않았습니다. 의견 교환과 일을 작게 쪼개는 것의 중요성을 다시금 새기게 되었습니다. 특히 같이 고민해주신 수범님께 이 글을 빌려 감사의 말씀을 드립니다.
또한, 트렌드의 변화 또는 생태계의 발전으로 인해, 현재 메이저로 쓰고 있는 도구들도 언젠가 형장의 이슬로 사라질 수 있다는 것도 배우게 되었습니다. 물론 그렇다고 해서 라이브러리나 도구의 의존성을 0%로 줄일 필요는 없다고 생각합니다. 다만, 지나친 강결합은 피하고 변화의 흐름에 귀를 기울인다면 괜찮지 않을까요?
마지막으로 교훈이 있다면, ‘좋은 접근성이 좋은 환경을 만들어준다’는 점입니다. 쉽게 이해할 수 있는 코드, 조금 더 빨라진 빌드 시간 등이, 현재의 상태를 더 들여다보고 개선할 수 있게 만들어줍니다. 결과적으로 팀의 업무 효율도 높아지고 개인 역시 더욱 성장할 수 있을 것입니다. 접근성을 높이기 위한 작업에 더 신경써야겠다고 반성했습니다.
살아남았기에 할 수 있는 고민이 있습니다. 경쟁에 밀렸거나 성장하지 못했다면, 위의 것들을 문제로 느낄 수 있었을지 잘 모르겠습니다.
팀과 제품이 성장하며 무엇이 도태되고 또 무엇이 나타날까요? 개발자로서 살짝 기대가 되는 것은 어쩔 수 없나봅니다.
ᴡʀɪᴛᴇʀ
Chanhee Lee @hiddenest Frontend Software Engineer
유니콘부터 대기업까지 쓰는 제품. 같이 만들어볼래요? 에이비일팔공에서 함께 성장할 다양한 직군의 동료들을 찾고 있어요! → 더 알아보기