9.3 번들러 통합 — Vite, webpack, Rollup + TypeScript
번들러와 TypeScript
번들러는 여러 모듈을 하나(또는 여러)의 파일로 묶어주는 도구입니다. TypeScript와 함께 사용하려면 TS → JS 변환 설정이 필요합니다.
| 번들러 | TS 처리 방식 | 특징 |
|---|---|---|
| Vite | esbuild (내장) | 빠른 개발 서버, HMR |
| webpack | ts-loader / babel-loader | 성숙한 생태계, 설정 복잡 |
| Rollup | @rollup/plugin-typescript | 라이브러리 번들링에 최적 |
| Parcel | 내장 (자동 감지) | 무설정, 초보자 친화 |
Vite + TypeScript
프로젝트 생성
npm create vite@latest my-app -- --template react-ts
# 또는 Vue
npm create vite@latest my-app -- --template vue-ts
# 또는 바닐라 TS
npm create vite@latest my-app -- --template vanilla-ts
생성된 tsconfig 구조
// tsconfig.json (앱 코드용)
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true, // Vite가 번들링하므로 tsc는 emit 안 함
"jsx": "react-jsx",
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
},
"include": ["src"],
"references": [{ "path": "./tsconfig.node.json" }]
}
// tsconfig.node.json (vite.config.ts 등 Node.js 도구용)
{
"compilerOptions": {
"composite": true,
"skipLibCheck": true,
"module": "ESNext",
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true,
"strict": true
},
"include": ["vite.config.ts"]
}
vite.config.ts 주요 설정
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import path from 'path';
export default defineConfig({
plugins: [react()],
// 경로 별칭
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),
},
},
// 빌드 설정
build: {
target: 'es2020',
outDir: 'dist',
sourcemap: true,
rollupOptions: {
output: {
manualChunks: {
vendor: ['react', 'react-dom'],
router: ['react-router-dom'],
},
},
},
},
// 환경변수 접두사
envPrefix: 'VITE_',
});
Vite에서 환경변수 타입 정의
// src/vite-env.d.ts
/// <reference types="vite/client" />
interface ImportMetaEnv {
readonly VITE_API_URL: string;
readonly VITE_APP_TITLE: string;
}
interface ImportMeta {
readonly env: ImportMetaEnv;
}
// 사용
const apiUrl = import.meta.env.VITE_API_URL; // string
webpack + TypeScript
설치
npm install --save-dev webpack webpack-cli ts-loader typescript
webpack.config.ts
import path from 'path';
import webpack from 'webpack';
import HtmlWebpackPlugin from 'html-webpack-plugin';
const config: webpack.Configuration = {
entry: './src/index.tsx',
output: {
path: path.resolve(__dirname, 'dist'),
filename: '[name].[contenthash].js',
clean: true,
},
resolve: {
extensions: ['.tsx', '.ts', '.js'],
alias: {
'@': path.resolve(__dirname, 'src'),
},
},
module: {
rules: [
{
test: /\.(ts|tsx)$/,
use: 'ts-loader',
exclude: /node_modules/,
},
{
test: /\.css$/,
use: ['style-loader', 'css-loader'],
},
],
},
plugins: [
new HtmlWebpackPlugin({
template: './public/index.html',
}),
],
optimization: {
splitChunks: {
chunks: 'all',
},
},
};
export default config;
ts-loader vs babel-loader 비교
ts-loader (권장: 타입 체크 포함):
{
test: /\.tsx?$/,
use: 'ts-loader',
exclude: /node_modules/,
}
babel-loader + @babel/preset-typescript (타입 체크 없음, 빠름):
npm install --save-dev babel-loader @babel/core @babel/preset-typescript @babel/preset-env
{
test: /\.tsx?$/,
use: {
loader: 'babel-loader',
options: {
presets: [
'@babel/preset-env',
'@babel/preset-typescript',
['@babel/preset-react', { runtime: 'automatic' }],
],
},
},
}
ts-loader 옵션 최적화
{
test: /\.tsx?$/,
use: [
{
loader: 'ts-loader',
options: {
transpileOnly: true, // 타입 체크 건너뜀 (빠름)
// 타입 체크는 fork-ts-checker-webpack-plugin이 별도 수행
},
},
],
}
npm install --save-dev fork-ts-checker-webpack-plugin
import ForkTsCheckerWebpackPlugin from 'fork-ts-checker-webpack-plugin';
{
plugins: [
new ForkTsCheckerWebpackPlugin(), // 별도 프로세스에서 타입 체크
],
}
Rollup + TypeScript
라이브러리 배포에 적합합니다. Tree-shaking이 뛰어납니다.
설치
npm install --save-dev rollup @rollup/plugin-typescript rollup-plugin-dts
rollup.config.ts
import { defineConfig } from 'rollup';
import typescript from '@rollup/plugin-typescript';
import dts from 'rollup-plugin-dts';
import resolve from '@rollup/plugin-node-resolve';
import commonjs from '@rollup/plugin-commonjs';
export default defineConfig([
// 메인 빌드 (CJS + ESM 동시 출력)
{
input: 'src/index.ts',
external: ['react', 'react-dom'], // peer dependency 제외
output: [
{
file: 'dist/index.cjs',
format: 'cjs',
sourcemap: true,
},
{
file: 'dist/index.mjs',
format: 'es',
sourcemap: true,
},
],
plugins: [
resolve(),
commonjs(),
typescript({ tsconfig: './tsconfig.json' }),
],
},
// 타입 선언 파일 번들
{
input: 'dist/types/index.d.ts',
output: [{ file: 'dist/index.d.ts', format: 'es' }],
plugins: [dts()],
},
]);
package.json 라이브러리 설정
{
"name": "my-library",
"version": "1.0.0",
"main": "./dist/index.cjs",
"module": "./dist/index.mjs",
"types": "./dist/index.d.ts",
"exports": {
".": {
"import": "./dist/index.mjs",
"require": "./dist/index.cjs",
"types": "./dist/index.d.ts"
}
},
"scripts": {
"build": "rollup -c rollup.config.ts --configPlugin typescript",
"typecheck": "tsc --noEmit"
},
"peerDependencies": {
"react": ">=18"
}
}
Parcel + TypeScript
설정 없이 TypeScript를 사용할 수 있습니다.
npm install --save-dev parcel
{
"scripts": {
"dev": "parcel src/index.html",
"build": "parcel build src/index.html"
}
}
Parcel은 tsconfig.json을 자동으로 인식합니다. 소규모 프로젝트나 프로토타이핑에 적합합니다.
isolatedModules 옵션
번들러와 함께 사용할 때 중요한 옵션입니다.
{
"compilerOptions": {
"isolatedModules": true
}
}
각 파일을 독립적으로 트랜스파일할 수 있도록 강제합니다. esbuild, swc, Babel은 파일 단위로 처리하기 때문에 이 옵션이 필요합니다.
// isolatedModules 위반 패턴들
// ❌ const enum은 다른 파일에서 인라인 불가
const enum Direction { Up, Down } // 오류
// ❌ 타입 재export는 'export type' 필요
export { SomeType }; // 오류
export type { SomeType }; // ✅
// ❌ 네임스페이스는 타입만 포함해야 함
namespace MyNs {
export const value = 1; // 오류 (런타임 값 포함)
}
번들 최적화 공통 전략
Tree-shaking 활성화
// ✅ Named export 사용 (tree-shaking 가능)
export function add(a: number, b: number) { return a + b; }
export function subtract(a: number, b: number) { return a - b; }
// ❌ default export 객체 (tree-shaking 어려움)
export default { add, subtract };
동적 import로 코드 분할
// 필요할 때 로드 (lazy loading)
const { PDFViewer } = await import('./components/PDFViewer');
// React에서
const LazyComponent = React.lazy(() => import('./HeavyComponent'));
고수 팁
번들러 선택 가이드
신규 React/Vue 프로젝트 → Vite (가장 빠른 개발 경험)
레거시 CRA 프로젝트 → webpack (기존 설정 유지)
라이브러리 배포 → Rollup (최적의 tree-shaking)
서버사이드 Node.js → esbuild 직접 사용 (가장 빠름)
프로토타입/소규모 → Parcel (무설정)
tsc + 번들러 역할 분리 원칙
tsc: 타입 체크만 (noEmit: true)
번들러: 트랜스파일 + 번들링 + 최적화