오늘날 새로운 프로젝트를 시작한다고 할 경우 가장 빠르게 인기를 얻고 webpack에 비해서 뛰어난 성능을 갖고 있으며 다양한 프레임워크를 지원해주는 vite 을 사용하지 않아야 하는 이유가 있나? 어떠한 차이가 있어서 그런 것일까? 에 대한 궁금증이 있었기 때문에 vite와 webpack의 차이점에 대해서 분석 해보았습니다.
vite
가 성능이 뛰어난 이유는 무엇일까요? vite는 esbuild
, Rollup
, SWC
들의 장점들을 최대한 활용하여 등장한 번들링 툴이기 때문입니다.AST(Abstract Syntax Tree)가 무엇일까?
vite와 webpack와 같은 번들러나 트랜스파일러는 AST를 기반으로 코드의 의존성과 변경 사항을 분석하고 최적화하는 작업을 수행합니다.
AST는 코드 텍스트에서 트리 구조의 데이터 스트럭쳐를 만들어 냅니다. 간단한 예시를 통해서 설명을 해보겠습니다.
function square(n) {
return n * n
}
// 위의 코드를 트리 구조의 데이터 스트럭쳐로 변환
{
"type": "Program",
"body": [
{
"type": "FunctionDeclaration",
"id": {
"type": "Identifier",
"name": "square"
},
"params": [
{
"type": "Identifier",
"name": "n"
}
],
"body": {
"type": "BlockStatement",
"body": [
{
"type": "ReturnStatement",
"argument": {
"type": "BinaryExpression",
"operator": "*",
"left": {
"type": "Identifier",
"name": "n"
},
"right": {
"type": "Identifier",
"name": "n"
}
}
}
]
}
}
],
"sourceType": "module"
}
위와 같이 코드를 트리 구조의 데이터 스트럭처로 만들어 내는 과정을 AST processing이라고 합니다. AST processing 과정을 간략하게 소개 해보겠습니다.
우선 렉시컬 분석을 통해서 코드의 문자들을 읽어서 정해진 룰에 따라서 이들을 토큰으로 만들어 합치는 것을 이야기합니다. 이 후 신택스 분석 과정에서 렉시컬 분석을 통해서 나온 결과로 나온 토큰 목록을 트리 구조로 만들며, 구조적 언어적으로 문제가 있을 경우에는 에러를 내뱉습니다.
위와 같은 트리 구조의 데이터 스트럭처를 만들어내고 분석을 하고 비교하여 변환하는 과정이 현대에서 사용하고 있는 많은 도구들 예를 들어
Eslint
, babel
, Rollup
, esbuild
등에서 실제로 이루어지고 있는 작업입니다.그렇다면 지금까지 AST를 설명한 이유는 무엇일까요? Webpack와 Vite의 AST 처리 방식 차이는 빌드 성능에 영향을 미치기 때문입니다.
Webpack의 처리 방법
Webpack은 JavaScript 애플리케이션에서 모든 모듈을 정적으로 분석하고 의존성 그래프를 구성하여 번들링을 수행하는 도구입니다. 이 과정에서 Babel이나 Terser와 같은 도구를 사용해 AST(Abstract Syntax Tree)을 활용하여 코드 변환이나 최적화를 수행합니다.
Webpack의 AST 처리
Webpack 자체는 AST를 직접 생성하거나 조작하지 않지만, 로더(Loader)와 플러그인(Plugin)을 통해 AST 기반의 작업을 위임합니다. 예를 들어,
babel-loader
는 Babel을 사용해 코드를 파싱하여 AST를 생성하고, 변환 후 JavaScript 코드를 생성합니다. Terser는 Webpack 번들링 결과물을 최적화하기 위해 AST를 생성하고 이를 변환합니다.Webpack의 번들링 과정은 다음과 같습니다:
-
코드 → 의존성 그래프 생성:
Webpack은 엔트리 파일에서 시작해 의존성 그래프를 재귀적으로 생성하며, 모든 모듈의 관계를 정적으로 분석합니다. -
의존성 그래프 → 번들 생성:
생성된 그래프를 기반으로 로더를 사용해 코드 변환 작업을 수행하고, 플러그인을 통해 최적화를 진행합니다. -
최종 JavaScript 코드 생성:
변환된 모듈을 하나 또는 여러 개의 번들 파일로 묶어 브라우저에서 실행 가능한 JavaScript 코드를 출력합니다.
HMR(Hot Module Replacement) 이란?
HMR은 "Hot Module Replacement"의 약자로, 개발 중 애플리케이션 전체를 다시 로드하지 않고, 변경된 모듈만을 갱신하는 기능입니다. 이를 통해 상태를 유지하면서 빠르게 변경 사항을 반영할 수 있어 개발 시간을 절약할 수 있습니다.
HMR의 특징:
- 전체 페이지 새로고침 없이 변경된 코드만 교체.
- React와 같은 라이브러리와 결합 시 상태를 유지하며 갱신 가능.
- 변경된 코드와 연관된 모듈을 정확히 다시 컴파일.
Webpack에서 HMR 처리 방법
HMR은 Webpack이 제공하는 런타임 기능으로, 변경 사항을 감지하여 필요한 모듈만 교체합니다. 처리 과정은 아래와 같습니다:
-
파일 변경 감지 및 재컴파일:
파일 시스템 감시 도구를 사용해 변경 사항을 감지하고, 변경된 모듈 및 관련 의존성을 다시 컴파일합니다. -
HMR Runtime 업데이트 알림:
컴파일된 새로운 모듈 정보를 HMR 런타임으로 전달합니다. -
브라우저에서 새 모듈 교체:
런타임은 기존 모듈을 새로운 모듈로 교체합니다. React와 같은 프레임워크는 추가적으로 DOM 상태를 업데이트합니다.
HMR 처리의 한계
변경된 부분만 교체한다고 해도 복잡한 의존성 그래프에서는 그래프 전체를 다시 분석하고 재구성해야 하므로 번들링 시간이 길어질 수 있습니다. Webpack은 변경된 모듈에서 AST를 다시 생성하고 전체 모듈이 아닌 변경된 모듈 전체를 교체하는 구조를 가집니다. 따라서 초대형 프로젝트에서는 HMR이 느려질 수 있습니다.
Webpack은 번들링과 모듈화 도구로서, HMR을 통해 개발 중 효율성을 극대화할 수 있는 강력한 기능을 제공합니다. 그러나 대규모 프로젝트에서는 번들링 속도를 최적화하거나 대안을 고려하는 것도 중요합니다.
Vite의 처리 방법
// Vite(esbuild)의 AST 처리
코드 → 바이너리 AST 생성 → 변환 → JavaScript 코드 생성
vite은 기본적으로 개발 환경에서 ESM(ECMAScript Modules)을 기본으로 활용하여 브라우저가 모듈을 직접 요청하도록 설계되었습니다. Vite가 기본적으로 ESM을 사용하는 것이 갖는 성능의 차이는 어떤 것이 있는 지 자세하게 알아보겠습니다.
ESM이 AST 파싱 시 성능면에서 뛰어난 이유
- 비동기적 모듈 로딩을 지원한다.
ESM은 브라우저에서 네이티브로 지원되며 비동기 방식의
import
로딩을 통해 필요한 모듈을 병렬로 가져옵니다.- 정적분석이 가능하도록 설계되어있다.
정적 분석은 프로그램의 코드를 실행하지 않고, 코드를 분석하여 그 구조, 동작, 의존성, 오류 등을 파악할 수 있는 방법입니다. 이에 대한 간단한 예시를 설명해보겠습니다.
const x = 10;
console.log(y); // ReferenceError: y is not defined
정적 분석은 위와 같이 코드가 실행되지 않더라도 y가 정의되지 않았다는 오류를 파악할 수 있는 것을 이야기 합니다.
Vite는 이러한 정적 분석을 활용하여 코드 의존성을 분석하여 최적의 번들 구조를 생성하는 용도로 사용하거나 Tree Shaking을 이용하여 불필요한 코드를 제거하여 성능을 개선하고 있습니다.
- 모듈 단위 캐싱에서 유리하다.
브라우저에서 ESM을 요청할 경우 일반적인 HTTP 요청과 동일하게 HTTP 캐싱 전략을 적용하여 사용합니다. 예를 들어 HTTP 헤더(예:
Cache-Control
, ETag
, Last-Modified
)를 기반으로 변경되지 않은 모듈은 캐시에서 가져오므로 네트워크 요청을 줄이고 성능을 향상시킬 수 있습니다.Vite의 HMR 처리
파일 시스템 감시 도구를 사용해 변경된 파일을 즉시 감지한다.
↓
변경된 모듈만 esbuild로 처리한다.
- esbuild를 사용하여 AST를 생성하고 필요한 변환을 수행한다.
↓
변경된 모듈만 브라우저에 전달한다
- 브라우저는 ESM을 사용해 변경된 모듈만 다시 요청하며, 전체 페이지를 다시 로드하지 않습니다
↓
모듈 업데이트 적용
위에서 설명한 것 처럼 Vite은 esbuild를 활용하여 AST를 처리하고 esbuild는 필요한 모듈만 개별적으로 파싱하여 AST를 생성합니다. ESM을 활용하여 브라우저의 모듈 로딩을 그대로 사용하고 코드에서 변경 사항이 생겼을 경우, HMR 시 변경된 모듈만을 처리하여 빌드를 진행합니다.
마무리
오늘날 webpack도 ESM을 지원하여 전반적인 성능을 개선하였습니다. 또한 위에서 Vite는 기본적으로 사용하는 esbuild를 사용하여 AST 처리를 진행하는 것을 webpack에서도 추가적인 작업을 수행한다면 가능합니다.
const path = require("path");
const { EsbuildPlugin } = require("esbuild-loader");
module.exports = {
entry: {
app: "./src/index.tsx",
},
output: {
module: true,
library: {
type: 'module'
}
},
experiments: {
outputModule: true
},
module: {
rules: [
{
test: /\.[jt]sx?$/,
loader: "esbuild-loader",
options: {
target: "es6",
},
exclude: /node_modules/,
},
{
test: /\.jsx?$/,
loader: "esbuild-loader",
options: {
target: "es6",
loader: "jsx",
},
exclude: /node_modules/,
},
{
test: /\.tsx?$/,
loader: "esbuild-loader",
options: {
target: "es6",
loader: "tsx",
},
exclude: /node_modules/,
}
},
resolve: {
alias: {
"@": path.resolve(__dirname, "./src"),
"@components": path.resolve(__dirname, "./src/components"),
},
extensions: [".ts", ".tsx", ".js", ".jsx"],
},
output: {
filename: "[name].bundle.js",
path: path.join(__dirname, "dist"),
clean: true,
},
optimization: {
minimizer: [
new EsbuildPlugin({
target: "es6",
css: true,
}),
],
},
};
하지만 위의 코드에서 확인할 수 있듯이 기본적으로 esbuild로 작업을 수행하고 ESM을 기본으로 동작하는 Vite에 비해서는 DX 경험이 저하될 수 밖에 없는 것이 사실입니다. 또한 기본적으로 webpack의 설계 방식 또한 제한되는 부분이 많습니다.
Vite는 개발 환경과 빌드 환경을 분리하여 개발 환경에서는 esbuild를 사용하고 빌드 시에는 rollup을 사용하여 각각의 최적화된 방식으로 동작을 하지만 webpack의 경우 개발 및 프로덕션 환경에서 동일한 번들링 메커니즘을 사요하므로 개발 서버에서도 불필요한 번들링 작업이 수행될 수 밖에 없습니다.
이러한 이유들로 인해서 오늘 날 새로운 프로젝트를 시작한다고 할 경우 많은 개발자들이 Vite를 이용하여 프로젝트를 시작하는 것이 굉장히 많은 인기를 얻게 되었습니다.