FE 프로젝트 스펙이 꽤나 레거시해서 버전 업그레이드 작업이 예정돼있다.
만약 담당하게 된다면 직접 작업해보고
담당하지 않더라도 서포트 하고 싶어 조금씩 리서치 해두려 한다.
지금 상태는 버전 문제도 있지만 프로젝트 규모가 꽤 많이 커져서 콜드스타트 시간이 너-무 오래 걸린다.
빌드 방식도 바꾸고 싶은 욕심이 생겨서 vite 에 대해 알아봤는데
겸사겸사 모듈 시스템과 번들러 개념도 정리했다.
js는 처음부터 대규모 애플리케이션을 염두에 두고 설계된 언어가 아니었다.
대부분 버튼 클릭 시 alert를 출력하거나, 간단한 폼 검증 같은 매우 간단한 작업에만 사용했다.
<script>
alert('Hello World')
</script>
이 당시에는
파일은 보통 1개였고 / 모든 코드가 전역 스코프에 존재했으며 / 파일 분리나 모듈 시스템 같은 개념이 없었다.
“코드를 나눈다”는 생각 자체가 필요 없었다.
웹이 점점 복잡해지고,
js가 Node.js 기반 서버 환경에서도 사용되기 시작하면서 문제가 발생했다.
파일 하나에 코드가 수천 줄씩 작성되자
코드를 기능 단위로 분리하는 것과 공통 로직을 재사용하고 싶은 니즈가 생겼다.
그래서 등장한 것이 CommonJS다.
[CommonJS]
// mathUtils.js
function sum(a, b) {
return a + b
}
module.exports = { sum }
// main.js
const { sum } = require('./mathUtils')
console.log(sum(1, 2))
CommonJS는
require, module.exports를 사용하고서버에서는 파일을 디스크에서 바로 읽을 수 있기 때문에, 동기 로딩 방식이 큰 문제가 되지 않았다.
CommonJS를 브라우저 환경에 그대로 적용하려고 하면 문제가 발생한다.
브라우저는 파일 시스템 접근이 불가능해서 모든 js 파일은 네트워크 요청을 거쳐야 한다. 이때 require() 기반의 동기 로딩은 치명적이다.
const config = require('./config') // 동기 방식이라서 네트워크 응답을 기다려야 함
이런 코드가 여러 개 쌓이면 페이지 로딩 속도가 급격히 느려진다.
그래서 등장한 방식이 AMD (Asynchronous Module Definition) 이다.
[AMD]
define(['mathUtils'], function (mathUtils) {
console.log(mathUtils.sum(1, 2))
})
AMD 방식은
그러나 AMD 방식도 실무에 적용해보니 문제가 있었다.
문제 1) 네트워크 요청이 너무 많다
js 파일 하나당 HTTP 요청 1번이 수행되는데 파일 수가 많아지면 그만큼 요청이 많아진다.
// 이런 구조라면? 최소 5번 이상의 request가 발생하여 네트워크 비용이 증가한다
main.js
├─ authService.js
├─ userApi.js
├─ validationUtils.js
├─ dateUtils.js
문제 2) 응답이 하나라도 늦으면 전체 실행이 늦어진다
define(['authService', 'userApi', 'dateUtils'], function (...) {
// 하나라도 늦으면 실행 지연
})
문제 3) 코드 가독성과 유지보수성이 좋지 않다.
콜백 기반 구조여서 / 모듈이 많아질수록 중첩이 증가하고 / 개발 경험(DX)가 안좋아진다.
결과적으로 AMD도 범용화되지 못했고 번들러가 등장했다.
다양한 시행착오 끝에 개발자들은 하나의 결론에 도달했다.
“개발할 때는 나누고, 배포할 때는 하나로 묶자”
이 역할을 수행하는 도구가 번들러(Bundler) 다.
번들러를 간단하게 도식화하면 아래와 같다.
개발 중
├─ authService.js
├─ userApi.js
├─ dateUtils.js
배포 시
└─ bundle.js
// bundle.js (개념적 예시)
(function () {
function sum(a, b) {
return a + b
}
console.log(sum(1, 2))
})()
네트워크 요청 최소화하면서 브라우저 로딩 성능 개선하고
모듈 시스템의 단점을 보완할 수 있다. 번들러에 대해 좀 더 자세하게 알아보자.
번들러는 단순히 파일을 합치는 도구가 아니다.
주요 역할은 다음과 같다.
대표적으로 Webpack, Rollup, Parcel, Vite 가 있다.
Webpack은 사실상 오랫동안 프론트엔드 번들러의 표준이었다.
Webpack의 기본 동작 방식은
import를 따라가며 의존성 그래프 생성// entry.js
import App from './App'
import Header from './components/Header'
모든 의존성을 하나의 번들로 처리하기 때문에
이 상황을 체감하고 있는 입장에서 DX가 매우 좋지 않다는 것에 공감한다🥲
Vite는 이런 질문에서 출발했다.
“왜 개발 중에도 전체를 번들링해야 할까?”
Vite의 주요 특징으로는
<!-- index.html -->
<script type="module" src="/main.js"></script>
// main.js
import App from './App.js'
index.html을 읽다가<script type="module" src="/main.js">를 발견하면 main.js를 ES Module 방식으로 실행한다main.js 파일을 해석하다가 import App from './App.js' 코드를 만나면App.js에 의존하고 있구나”라고 판단하고App.js를 추가로 네트워크 요청한다
ESM?
JavaScript의 공식 표준 모듈 시스템이다.
import/export문법을 사용- 브라우저가 모듈 간 의존 관계를 직접 해석한다
- 필요한 파일을 자동으로 네트워크 요청
ESM 을 활용하면 번들러가 모든 파일을 미리 묶지 않아도, 브라우저가
모듈 로딩을 직접 처리할 수 있다.
Vite는 이 ESM 지원을 전제로 설계된 개발 도구다.
Vite는 “개발 중에는 번들링을 하지 않는다” 구조덕에 아래와 같은 장점을 갖는다.
번들 기반 개발 서버(ex. webpack)

ESM 기반 개발 서버(Vite)

3-1) Vite의 빌드 방식... Rollup
Vite는 개발 단계와 빌드 단계를 명확히 분리한다.
배포 시에는 내부적으로 Rollup을 사용해 최적화된 결과물을 생성한다.
Rollup?
- ESM 기반 번들러
- Tree Shaking에 강함
- 불필요한 코드를 제거한 깔끔한 번들 생성에 적합
개발 속도는 ESM으로, 빌드 결과물의 품질은 Rollup으로 보장한다.
3-2) 의존성 Pre-bundle... esbuild
Vite는 개발 서버를 시작할 때
node_modules에 있는 외부 라이브러리들을 대상으로 Pre-bundle 과정을 수행한다.
직접 작성한 소스 코드(src/ 내부)는 개발 중에 수시로 수정되고 HMR 대상이기 때문에
번들링하지 않는 것이 유리하지만
외부 라이브러리 코드는 변동성이 없으므로
이 Pre-bundle 단계에서 esbuild가 사용된다.
esbuild?
- Go 언어로 작성된 초고속 빌드 도구
- JavaScript / TypeScript 변환 속도가 매우 빠름
Vite는 esbuild를 활용해 초기 로딩 성능을 개선하고 개발 서버 응답 속도 향상시킨다.
정리하면 Vite는
ESM을 활용한 개발 서버 구조Rollup 기반의 안정적인 빌드esbuild를 이용한 빠른 Pre-bundle을 조합하여 기존 번들러의 한계를 보완한 현대적인 프론트엔드 개발 도구라고 볼 수 있다.
| 항목 | Webpack | Vite |
|---|---|---|
| 개발 방식 | 전체 번들 후 실행 | ESM 기반, 필요 시 로드 |
| 초기 속도 | 느림 | 매우 빠름 |
| HMR | 가능하지만 무거워질 수 있음 | 빠르고 가벼움 |
| 설정 난이도 | 높음 | 낮음 |
| 적합한 경우 | 레거시, 복잡한 설정 | 신규 프로젝트 |
그래서 vite가 무조건적인 정답이냐고 한다면 그건 아니다.
webpack 은 복잡한 설정을 컨트롤 할 수 있고 긴 사용기간만큼 안정적이다.
프로젝트 성격에 따라서 적합한 번들러를 선택하면 된다.