안녕하세요. 정지현 입니다 :)
번들러 이해하기 1편에 이어 이번에는 여러 번들러들을 살펴 보려고 합니다.
각 번들러들의 용어나 특징들을 공식문서 위주로 이해해볼게요.
배경지식
번들러가 뭐야 ?
번들러 더 자세하게 !
------------- 👆 번들러 글 1편에 정리되어 있어요 -------------
webpack , rollup , esbuild , vite , parcel 각각 알아보기
정리
JS 프로젝트에서는 모듈화를 위한 많은 번들러가 존재합니다.
각각의 특징과 핵심 개념 , 장단점을 알아보고 비교해보도록 하겠습니다.
🔵 특징
- 오랫동안 사용되어 레퍼런스가 다양해 가장 안정적인 번들러 입니다.
서드파티 라이브러리 관리나 css 전처리, 이미지 에셋관리 등에 있어서 우수하고 생태계가 가장 풍부하여 다른 번들러들 보다 안정적입니다
- 모든 모듈을 함수로 래핑합니다
각 모듈을 함수로 감싸고 로더와 모듈 캐시를 구현하는 번들을 생성합니다. 런타임시 각 모듈 함수들이 평가되어 모듈 캐시를 채우게 됩니다.
- JS로 변환하기 위한 로더와 플러그인 설치가 필요합니다.
웹팩은 자바스크립트밖에 몰라요. 그래서 자바스크립트가 아닌 파일들은 웹팩이 이해할 수 있도록 변경해야 합니다.이 역할을 바로 로더가 해주는거죠.
로더 : 번들 되기 전 파일 단위를 처리
플러그인 : 번들된 결과물을 추가로 처리. 자바스크립트를 난독화 하는 등의 후처리에 사용.
- 하나의 시작점(entry point)으로부터 의존적인 모듈을 모두 찾아내서 하나의 결과물을 만듭니다.
app.js
import * as first from "./first.js"
first.printHello() // 3
first.js
export function printHello() {
console.log('Hello')
}
app.js 부터 시작해 해당 파일에 import된 first.js 파일을 찾은 뒤 하나의 파일로 만드는 방식입니다.
🔵 핵심 개념
Entry(엔트리)
엔트리 포인트는 웹팩이 내부 디펜던시 그래프(요거 위에 번들러 프로세스에서 다뤘었죠!)를 생성하기 위해 사용해야 하는 모듈입니다. 쉽게 말하면 의존성 그래프의 시작점을 웹팩에서는 Entry 라고 합니다.
// webpack.config.js
module.exports = {
entry: './src/index.js'
}
엔트리는 최초의 진입점이자 자바스크립트 파일 경로인데요. 웹팩을 실행하면 위의 코드에서 index.js를 대상으로 웹팩이 빌드를 수행하게 됩니다.
웹팩은 엔트리를 통해서 필요한 모듈을 로딩하고 종속성 그래프를 재귀적으로 빌드한 다음에 모든 모듈을 브라우저에 의해 로드되는 작은 수(보통 하나)의 번들로 묶습니다.
엔트리로 지정된 파일에는 당연히 웹 애플리케이션의 전반적인 구조와 내용이 담겨져 있어야 해요. 웹팩이 해당 파일을 가지고 모든 종속성을 이해하고 분석하기 때문에 애플리케이션을 동작시킬 수 있는 내용들이 있어야 합니다 !
SPA 프로젝트에서 app.js 파일 생각해보시면 될 것 같아요.
엔트리는 한 개가 될 수도 있지만 여러개가 될 수도 있어요.
// 항목을 구분하여 쓰는 경우
module.exports = {
entry: {
login: './src/Login.js',
main: './src/Main.js'},
output: {
filename: 'bundle.js',
},
};
// 배열로 쓰는 경우
module.exports = {
entry: ['./src/file_1.js', './src/file_2.js'],
output: {
filename: 'bundle.js',
},
};
엔트리를 여러개 쓰는 경우는 싱글 페이지 어플리케이션보다 멀티 페이지 어플리케이션에 적합합니다.
Output(출력)
output 속성은 웹팩으로 번들링을 한 후의 결과물의 파일 경로를 의미합니다.
out의 [name]은 번들의 파일명이 entry의 이름으로 동적 생성할 수 있도록 해줍니다.
// webpack.config.js
const path = require("path");
const webpack = require("webpack");
module.exports = {
mode: "development",
entry: {
wynter : './src/index.tsx',
},
output: {
filename: "[name].bundle.js",
path: path.resolve(__dirname, 'dist')
}
};
npm run build
를 하게 되면 wynter.bundle.js 파일이 생성됩니다 !
Loaders(로더)
로더는 웹팩이 번들링을 할 때 자바스크립트 파일이 아닌 웹 자원을 변환할 수 있도록 도와줍니다.
웹팩은 모든 파일을 모듈로 바라보는데요. 자바스크립트로 만든 모듈 뿐만 아니라 스타일 , 이미지, 폰트 까지도 전부 모듈로 봅니다.
이것이 가능한 건 웹팩의 로더 덕분인데요. 로더는 타입스크립트 같은 다른 언어를 자바스크립트 문법으로 변환해주거나 이미지를 data url 형식의 문자열로 변환해줍니다. 뿐만 아니라 css 파일을 자바스크립트에서 직접 로딩할 수 있도록 해줍니다. *( 올바른 로더를 적용해주지 않으면 에러가 발생해요 ! )
const path = require("path");
const webpack = require("webpack");
module.exports = {
entry:'./src/index.tsx',
module: {
rules: [
{
test: /\.(ts|tsx|js|jsx)$/,
use: "babel-loader", // ES5로 변환시켜주는 babel(트랜스파일러) 적용 - babel.config.js에 바벨 설정을 해줍니다.
exclude: /node_modules/,
},
{
test: /\.scss$/,
use: [
"style-loader", // creates style nodes from JS strings
"css-loader", // translates CSS into CommonJS
"sass-loader" // compiles Sass to CSS, using Node Sass by default
],
exclude: /node_modules/
},
],
},
};
짠 로더까지 적용을 해보았습니다.
Plugins(플러그인)
로더와 플러그인은 다릅니다. 플러그인은 웹팩의 기본적인 동작에 추가적인 기능을 제공하는 속성이에요.
로더는 번들 되기 전 파일 단위를 처리, 즉 파일을 해석하고 변환하는 과정에 기여하는 반면에
플러그인은 번들된 결과물을 추가로 처리, 즉 해당 결과물의 형태를 바꾸는 역할을 합니다.
const HtmlWebpackPlugin = require("html-webpack-plugin"); // 웹팩으로 빌드한 결과물로 HTML 파일을 생성해주는 플러그인
const path = require("path");
const webpack = require("webpack");
module.exports = {
entry:'./src/index.tsx',
module: {
rules: [
{
test: /\.(ts|tsx|js|jsx)$/,
use: "babel-loader",
exclude: /node_modules/,
},
{
test: /\.scss$/,
use: [
"style-loader",
"css-loader",
"sass-loader"
],
exclude: /node_modules/
},
],
},
plugins: [
new HtmlWebpackPlugin({
template: 'public/index.html',
// 메타 태그
meta: {
'theme-color': '#4285f4',
'description': 'webpack with wynter',
},
}),
new webpack.ProvidePlugin({ // 자주 사용되는 모듈을 미리 등록하여 매번 작성하지 않게 해줌
React: "react",
}),
],
resolve: {
alias: {
"@": path.resolve(__dirname, "..src/"),
},
extensions: [".js", ".ts", ".jsx", ".tsx", ".css", ".json"],
},
};
짠 플러그인까지 적용을 해보았습니다. 🎉
Mode(모드)
웹팩 버전 4 부터 mode 라는 개념이 추가되었습니다.
none : 모드 설정 안함
development : 개발 모드
production : 배포 모드
mode를 정의하면 웹팩의 실행 모드가 설정됩니다.
각 실행 모드에 따라 웹팩의 결과물 모습이 달리지는데용. 최적화 또한 mode에 따라 다르게 적용됩니다.
webpack.common.js
webpack.dev.js
webpack.prod.js
요런식으로 구성 파일을 따로 만들어서 사용할 수도 있어요 !
복잡한 내용은 아니라서 공식문서로 설명을 대체할게요.
최적화 (Code Splitting / lazy Loading)
코드가 많아지면 번들링된 결과물이 커지게 되면서 브라우저 성능에 영향을 미칩니다.
웹팩을 통하여 번들된 자바스크립트 파일의 크기를 줄일 수 있는데요, 그 방법은 여러가지가 있습니다.
Code Splitting
큰 파일을 한 번에 로드하는 것보다 작은 파일 여러개를 동시에 로드 하는 것이 속도면으로 유리합니다.
웹팩에서는 자동으로 중복되는 코드를 판단하여 별도의 청크로 분할하도록 설정할 수 있습니다.
Chunk란, 특정 스크립트와 모두의 부분집합이 되는 파일입니다.
코드를 분할하는 기준 각 프로젝트마다 달라집니다. 라우트 이동에 따라서 불러오는 컴포넌트를 기준으로 Chunk를 나눌 수도 있고, api를 호출하는 기준으로 나눌 수도 있겠죠.
또한 코드를 분할하는 방법도 크게 세 가지로 나뉩니다.
module.exports = {
optimization: {
splitChunks: { chunks: 'all' },
},
}
엔트리를 여러개로 분리
작은 프로젝트에서는 쓸만 한 방법이지만 복잡하거나 큰 프로젝트에서는 좋지 않습니다.
Dynamic import
해당 글에 웹팩을 사용하여 React 번들 사이즈를 줄이는 방법이 자세히 나와 있습니다.👀 - https://www.codemzy.com/blog/react-bundle-size-webpack-code-splitting
동적 import는 조건에 따라 코드를 불러오는 건데요. React나 Vue에서 널리 사용됩니다.
React에서는 React.lazy를 이용하여 동적으로 컴포넌트를 불러올 수 있습니다. React.lazy()를 사용하면 웹팩은 해당 경로에 대해 별도의 번들을 생성합니다.
참고로 현재 빌드된 결과를 시각적으로 확인할 수 있는 webpack-bundle-analyzer 플러그인도 있으니 활용해보면 좋을 것 같습니다.
🔵 장점
🔵 단점
🔵 특징
ES6 모듈 형식으로 빌드 결과물을 생성할 수 있는 번들러 입니다.
라이브러리나 패키지에 활용하기 유리합니다. (vue로 롤업을 이용해서 번들링을 진행하고 있어요.)
일단 트리쉐이킹이 가능하다는 것이 라이브러리에 가장 적합한 번들러인 것 같습니다. 사용자가 라이브러리 코드 일부만 사용했는데 전체 결과가 번들 결과물에 포함된다면 불필요하게 용량을 증가시키겠죠?
웹팩은 ES6 모듈에서만 지원 가능합니다.
자체 로더가 아니라 ES6를 사용합니다.
따라서 트리 쉐이킹이 가능합니다. 사실 왜 ES6를 사용하는게 트리 쉐이킹에 용이한지 이해가 안되었는데, 공식문서에 자세히 나와있네요. 후욱 ^^..
CommonJS를 사용하는 경우에는 전체 도구 또는 라이브러리를 가져와야 합니다. 그런데 ES 모듈을 사용하면 필요한 함수만 가져와서 쓸 수 있어요. (문법을 생각해보면 당연한 이야기네요)
롤업은 명시적 import , export를 사용하기 때문에 컴파일된 출력 코드에서 트리 쉐이킹을 하여 최소한의 것들만 번들링 할 수 있습니다.
코드를 동일한 수준으로 올리고(Scope Hoisting) 한 번에 번들링 합니다.
한 번에 하기때문에 속도는 웹팩보다 빠르고 번들링 결과물도 가볍습니다. 그러나 변수 충돌에 있어서는 웹팩보다 덜 안정적입니다.
진입점을 다르게 설정하여 번들링 가능합니다.
진입점이 다르기 때문에 중복해서 번들링될 수 있는 부분을 알아내고, 독립된 모듈로 분리가 가능합니다. 따라서 코드 스플리팅 측면에서 다른 번들러와 비교해 강점을 보입니다.
🔵 핵심 개념
롤업의 설정 파일은 선택 사항이지만 권장 됩니다.
웹팩 못지않게 많은 플러그인들이 존재하구요. 플러그인을 통해 번들링 전에 코드를 트랜스파일링 하거나, 써드 파티 모듈을 찾는데 사용할 수 있습니다.
[설정 파일 주요 항목]
input
주요 진입점 입니다.
output
빌드 결과물을 설정합니다. 아래 항목들을 포함합니다.
file : 생성된 결과물이 위치할 곳을 표시합니다.
format : 롤업은 여러 출력 형식을 제공합니다.
// CommonJS 스펙을 준수한 모듈로 번들링
// Node.js 에서만 쓸 용도의 라이브러리를 만들 때 용이
export default {
input: './src/main.js',
output: {
file: './build/bundle.min.js',
format: 'cjs', // build시 common js 모듈이 나온다
name: 'bundle'
}
}
// ES Module 스펙을 준수한 모듈로 번들링
export default {
input: './src/main.js',
output: {
file: './build/bundle.min.js',
format: 'es', // build시 es 모듈이 나온다
name: 'bundle'
}
}
// 브라우저 전용으로 모듈을 번들링
export default {
input: './src/main.js',
output: {
file: './build/bundle.min.js',
format: 'iife',
name: 'bundle'
}
}
[트랜스파일링 설정]
최신 버전의 ES 를 사용하기 위해 바벨 설정을 해줍니다.
npm install @babel/core @babel/preset-env rollup-plugin-babel --save-dev
import babel from 'rollup-plugin-babel';
export default {
input: './src/main.js',
output: {
file: './build/bundle.min.js',
format: 'iife',
name: 'bundle'
},
plugins: [
babel({
exclude: 'node_modules/**'
})
]
}
🔵 장점
🔵 단점
떠오르는 차세대 자바스크립트 번들러 라고 합니다.esbuild는 웹팩보다 100배 빠르다고 하는데요.
esbuild가 빠른 이유 중 하나는 GO 로 작성 되었다는 점입니다.
JavaScript는 인터프리터 언어이기 때문에 한줄한줄 기계어로 변환을 합니다. 반면에 GO는 컴파일 단계에서 미리 소스 코드를 전부 기계어로 변환해놓습니다.
또한 JavaScript는 싱글 스레드 기반이라 한 파일씩 순차적으로 처리되지만 GO의 경우에는 멀티 스레드 기반으로 동작 할 수 있습니다.
즉 esbuild는 코드 파싱과 출력, 소스맵 생성을 모두 병렬로 처리 하기 때문에 빠른 속도를 보장합니다.
🔵 특징
다른 번들러들과 다르게 많은 개발자 편의를 제공하지 않습니다. esbuild는 애초부터 자바스크립트를 위한 번들러 입니다. 그저 빌드 도구일 뿐이라 타입 체킹이나 HMR등 번들링과 상관 없는 기능들은 일체 없는 것이죠.
그래서 esbuild는 보통 프레임워크를 기반으로 하는 웹 개발 보다는 바닐라 스크립트 형태의 라이브러리등에 적합했습니다. 2020년에는 esbuild를 통해 개발 모드를 지원하고 실제 번들은 Webpack을 통해 제공하는 Snowpack 이라는 번들러가 나오기도 했습니다.
아래는 esbuild 에서 현재 제공하는 기능들입니다.
Extreme speed without needing a cache
ES6 and CommonJS modules
Tree shaking of ES6 modules
An API for JavaScript and Go
TypeScript and JSX syntax
Source maps
Minification
Plugins
🔵 핵심 개념
build script 구성 살펴보기
비교적 간단합니다 !
// build.js
#!/usr/bin/env node
require("esbuild")
.build({
entryPoints: ['./src/index.ts'],
outfile: 'dist/index.js',
bundle: true,
minify: true,
platform: 'browser',
format: 'esm',
sourcemap: true,
target: 'es6',
plugins: [nodeExternalsPlugin()],
logLevel: "info",
})
.catch(() => process.exit(1));
entryPoints: 번들링 알고리즘이 들어가게 되는 애플리케이션의 entry 포인트입니다.
outfile: 번들의 결과물입니다. 하나의 파일만 (문자열만) 가능합니다.
bundle: 번들링 여부 입니다.
minify: minification (자바스크립트 파일 축소) 여부입니다.
platform: 번들링된 파일이 어느 환경에서 실행될지를 설정 합니다.
format: 생성된 파일의 형태를 나타낸다. iife, cjs , esm 가능합니다.
sourcemap: 디버깅을 용이하게 해주는 소스맵 제공 여부를 설정합니다.
target: 어떤 플랫폼의 버전에서 사용할 수 있을지 명시합니다.
logLever : esbuild가 터미널에 경고 및 오류 메세지를 출력하는 수준을 설정합니다.
부분 컴파일 (Incremental Compilation)
코드가 변경 될 때, 같은 파일을 반복적으로 컴파일해야하는 경우, 부분 컴파일을 사용하면 성능 저하없이 효율적으로 빌드를 수행 할 수 있습니다. 이는 esbuild가 변경된 소스에 대해서만 작업을 수행하기 때문입니다.
🔵 장점
🔵 단점
타입체킹 같은 경우에는 별도로 tsc의 타입체킹 기능을 사용하거나, IDE에서 제공하는 타입 체킹을 사용하는 방법으로 보완할 수 있을 것 같습니다.
🔵 특징
Vue.js를 개발한 에반 유는 앞서 언급했던 Snowpack의 단점을 보완한 vite를 제작합니다. 처음에는 Vue를 위해 만들어졌으나 지금은 React등 다른 프레임워크와 라이브러리에도 지원 가능합니다.
esbuild와 다른 번들 도구에서 제공하는 기능들 (ex. 코드 스플리팅, 개발서버)을 하나로 모은 프론트엔드 번들 도구라고 생각하면 될 것 같습니다.
Dependencies와 SourceCode를 분리하여 빌드합니다.
dependencies는 대부분 개발하는 동안 변화가 거의 없는 의존성 모듈들 입니다. vite는 COMMONJS 모듈들을 ESM으로 변환하고 번들링을 진행하는데요, 사전 번들링 작업은 esbuild를 통해 이루어 집니다.
그러나 sourceCode는 우리가 작성 중이 코드로써 변화가 잦습니다 . 하지만 모든 코드가 동시에 로드될 필요는 없는데요. Vite는 브라우저의 요청에 따라 필요한 소스 코드를 변환해서 제공하면 됩니다. (브라우저가 번들러 작업의 일부를 인계 받는다고 생각하시면 됩니다. )
정리하면, esbuild로 미리 트랜스파일링 해놓은 뒤, 로컬에서 개발 서버를 띄우면 소스 코드를 불러오면서 의존성이 있는 패키지만 가져옵니다. 한 번 빌드한 결과는 캐싱을 해두어 다음 개발 빌드 때 바로 뜨게 됩니다.
ESM을 사용하여 매우 빠릅니다.
Vite는 ESM을 사용하여 매우 빠른데요. vite는 Native ESM을 이용해 소스 코드를 제공하도록 하고 있습니다. 즉, 브라우저가 곧 번들러라는 입니다. vite는 그저 브라우저의 판단 아래 특정 모듈에 대한 소스코드를 요청하면 이를 전달할 뿐입니다.
기본 ES 모듈보다 매우 빠른 Hot Module Replacement 제공합니다.
번들러가 아닌 ESM을 이용해서 제공하기 때문에 , 어떤 모듈이 수정되면 vite는 그저 수정된 모듈과 관련된 부분만을 교체할 뿐이고, 브라우저에서 해당 모듈을 요청하면 교체된 모듈을 전달 할 뿐입니다. 이 과정에서 ESM을 이용하기 때문에 앱 사이즈가 커져도 HMR을 포함한 갱신 시간에는 영향을 끼치지 않습니다.
Vite는 TypeScript , CSS로더 , HMR을 제공하면서도 복잡한 설정이 필요 없습니다.
🔵 장점
🔵 단점
🔵 특징
설정이 필요 없는( zero-configuration ) 웹 애플리케이션 번들러 입니다.
다른 빌더와 다르게 엔트리 포인트를 지정해 주는 것이 아니라 애플리케이션 진입을 위한 HTML 파일 자체를 읽기때문에 별도의 설정 없이도 빌드가 가능합니다.
동적 import()문을 사용해서 출력 번들을 분할할 수 있습니다. 이를 통해 초기 로드시 필요한 것들만 로드할 수 있습니다. 즉, 모든 자산을 한 번에 로드할 필요가 없으며 웹 애플리케이션을 사용하는 사용자는 더 빠르게 로드되는 페이지를 경험할 수 있습니다.
Assets 기반으로 번들링을 합니다.
entry point가 없고 HTML 파일을 읽으면서 JS, CSS, 이미지 에셋등을 직접 참조 합니다. 비슷한 유형의 에셋은 같은 번들로 출력하고 다른 유형은 자식 번들로 만들어 부모 번들에 참조합니다.
속도가 빠릅니다.
캐싱을 하므로 최고 빌드보다는 두 번째 빌드 속도부터 불꽃 튀게 빠릅니다. 전체 캐싱을 지원하고 병렬 처리를 지원하여 속도가 웹팩보다 빠릅니다.
자동 변환
Babel, PostCSSm PostHTML을 사용하는 코드는 자동으로 변환 됩니다. 즉, .babelrc, .postcssrc, .posthtml 같은 설정 파일들을 자동을 읽어와서 세팅을 해줍니다.
HMR
코드 스플리팅
난독화
🔵 장점
🔵 단점
번들러별 지원 범위에 대해 참고하기 좋은 사이트가 있어요.(https://bundlers.tooling.report/)
해당 사이트와 오늘 이해한 것 기준으로 정리를 해볼게요.
웹팩 vs 롤업 vs 바이트 vs 파셀
webpack
광범위한 개발 레퍼런스를 활용하고 많은 서드파티를 필요로 하는 복잡한 어플리케이션이라면.
rollup
ES6 모듈 형식으로 빌드 결과물을 출력하여 라이브러리나 패키지에 활용하고 싶다면. 트리 쉐이킹과 같이 효율성을 고려해야 하는 프로젝트라면.
vite
SPA를 생성하기 위한 Vue CLI/CRA를 대체할 거리를 찾고있다면.
parcel
복잡한 설정을 피하고 간단한 애플리케이션을 만들고 싶다면.
갈수록 설명이 추상적이고 짧아지는 느낌이네요.😅
아무래도 웹팩의 생태계가 더 커서 훨씬 더 많은 설명과 사례들을 참고할 수 있어서 그런 것 같습니다.
지금은 이론상으로만 각 번들로 들을 비교해 본 거라 각각의 장단점들이 크게 와닿지는 않는 것 같아요. 여러 프로젝트에 직접 사용해 보고 트러블슈팅도 경험해 보아야 확실히 알 수 있겠죠?
webpack
공식문서 - https://webpack.js.org/guides/code-splitting/#root
웹팩 번들 사이즈 줄이기 - https://www.codemzy.com/blog/react-bundle-size-webpack-code-splitting
https://juneyr.dev/2019-02-20/webpack-babel
rollup
롤업 환경 설정 - https://so-so.dev/tool/rollup/rollupjs-config/#why-not-webpack
vite
공식문서👍 - https://vitejs-kr.github.io/guide/why.html#the-problems
vite & svelte - https://yozm.wishket.com/magazine/detail/1620/
parcel
공식문서 - https://ko.parceljs.org/getting_started.html
https://d2.naver.com/helloworld/2838729
esbuild
공식문서👍 - https://blog.logrocket.com/getting-started-esbuild/
https://fe-developers.kakaoent.com/2022/220707-webpack-esbuild-loader/