Webpack Complete Guide | Loaders, Plugins, Code Splitting & Optimization
이 글의 핵심
Webpack 5 is the battle-tested bundler behind millions of production apps. This guide covers the full configuration — loaders, plugins, code splitting, tree shaking, persistent caching, and Module Federation for micro-frontends.
Core Concepts
Entry point(s)
→ webpack reads imports/requires recursively
→ applies Loaders to transform each file type
→ Plugins run on the compilation
→ Output: bundled JavaScript + assets
| Concept | Role |
|---|---|
| Entry | Where webpack starts building the dependency graph |
| Output | Where to write the bundle files |
| Loader | Transforms files (TS→JS, SCSS→CSS, PNG→data-URL) |
| Plugin | Hooks into the build process (HTML generation, env vars, etc.) |
| Mode | development or production — sets defaults |
| Chunk | A bundle fragment (created by code splitting) |
Installation
npm install -D webpack webpack-cli webpack-dev-server
webpack.config.ts
import path from 'path';
import webpack from 'webpack';
import HtmlWebpackPlugin from 'html-webpack-plugin';
import MiniCssExtractPlugin from 'mini-css-extract-plugin';
import { BundleAnalyzerPlugin } from 'webpack-bundle-analyzer';
const isDev = process.env.NODE_ENV !== 'production';
const config: webpack.Configuration = {
// Mode sets optimizations automatically
mode: isDev ? 'development' : 'production',
// Entry — where webpack starts
entry: {
main: './src/index.tsx',
// Multiple entry points create separate bundles
// admin: './src/admin/index.tsx',
},
// Output
output: {
path: path.resolve(__dirname, 'dist'),
filename: isDev ? '[name].js' : '[name].[contenthash:8].js', // Cache busting
chunkFilename: isDev ? '[name].chunk.js' : '[name].[contenthash:8].chunk.js',
assetModuleFilename: 'assets/[name].[contenthash:8][ext]',
publicPath: '/', // Base URL for all assets
clean: true, // Clean dist/ before each build
},
// Module resolution
resolve: {
extensions: ['.tsx', '.ts', '.js', '.json'],
alias: {
'@': path.resolve(__dirname, 'src'),
'@components': path.resolve(__dirname, 'src/components'),
},
},
// Source maps
devtool: isDev ? 'eval-source-map' : 'source-map',
// Loaders
module: {
rules: [
// TypeScript / TSX
{
test: /\.(ts|tsx)$/,
use: 'ts-loader',
exclude: /node_modules/,
},
// CSS Modules
{
test: /\.module\.css$/,
use: [
isDev ? 'style-loader' : MiniCssExtractPlugin.loader,
{
loader: 'css-loader',
options: { modules: { localIdentName: '[name]__[local]--[hash:base64:5]' } },
},
],
},
// Global CSS
{
test: /\.css$/,
exclude: /\.module\.css$/,
use: [isDev ? 'style-loader' : MiniCssExtractPlugin.loader, 'css-loader'],
},
// SCSS
{
test: /\.scss$/,
use: [
isDev ? 'style-loader' : MiniCssExtractPlugin.loader,
'css-loader',
'sass-loader',
],
},
// Images and fonts
{
test: /\.(png|jpg|jpeg|gif|svg|webp)$/i,
type: 'asset',
parser: {
dataUrlCondition: { maxSize: 8 * 1024 }, // Inline if < 8KB
},
},
{
test: /\.(woff|woff2|eot|ttf|otf)$/i,
type: 'asset/resource',
},
],
},
// Plugins
plugins: [
// Generate index.html with injected script tags
new HtmlWebpackPlugin({
template: './public/index.html',
favicon: './public/favicon.ico',
}),
// Extract CSS to separate file (production)
...(isDev ? [] : [
new MiniCssExtractPlugin({
filename: 'css/[name].[contenthash:8].css',
}),
]),
// Inject environment variables
new webpack.DefinePlugin({
'process.env.API_URL': JSON.stringify(process.env.API_URL),
'process.env.VERSION': JSON.stringify(process.env.npm_package_version),
}),
// Bundle size visualization (run with ANALYZE=true npm run build)
...(process.env.ANALYZE ? [new BundleAnalyzerPlugin()] : []),
],
// Code splitting
optimization: {
splitChunks: {
chunks: 'all',
cacheGroups: {
// Vendor libraries into separate chunk
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
chunks: 'all',
},
// React-specific chunk
react: {
test: /[\\/]node_modules[\\/](react|react-dom)[\\/]/,
name: 'react',
chunks: 'all',
priority: 10,
},
},
},
// Keep runtime chunk separate for better caching
runtimeChunk: 'single',
},
// Dev server
devServer: {
port: 3000,
hot: true, // Hot Module Replacement
historyApiFallback: true, // SPA routing
proxy: {
'/api': { target: 'http://localhost:8000', changeOrigin: true },
},
},
};
export default config;
Loaders in Detail
TypeScript with Babel
npm install -D ts-loader typescript
# Or for faster builds:
npm install -D babel-loader @babel/core @babel/preset-env @babel/preset-typescript @babel/preset-react
// babel-loader (faster than ts-loader, no type checking during build)
{
test: /\.(ts|tsx)$/,
use: {
loader: 'babel-loader',
options: {
presets: [
'@babel/preset-env',
['@babel/preset-react', { runtime: 'automatic' }],
'@babel/preset-typescript',
],
},
},
},
// Run tsc --noEmit separately for type checking
SVG as React Components
npm install -D @svgr/webpack
{
test: /\.svg$/,
use: [
{
loader: '@svgr/webpack',
options: { icon: true },
},
],
},
import Logo from './logo.svg'; // Now a React component
<Logo width={24} height={24} />
Tree Shaking
// package.json — tell webpack which files have side effects
{
"sideEffects": false
// Or list files that DO have side effects:
// "sideEffects": ["*.css", "./src/polyfills.js"]
}
// ✅ Tree-shakeable exports
export function add(a: number, b: number) { return a + b; }
export function multiply(a: number, b: number) { return a * b; }
// If you only import add:
import { add } from './math';
// multiply is removed from the bundle (tree shaken)
// ❌ Prevents tree shaking
export default {
add: (a: number, b: number) => a + b,
multiply: (a: number, b: number) => a * b,
};
// webpack can't determine which methods are used at build time
Code Splitting
Dynamic Imports
// Route-based code splitting
const Dashboard = React.lazy(() => import('./pages/Dashboard'));
const Settings = React.lazy(() => import('./pages/Settings'));
function App() {
return (
<Suspense fallback={<Spinner />}>
<Routes>
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/settings" element={<Settings />} />
</Routes>
</Suspense>
);
}
// Named chunks — the comment controls the chunk file name
const Chart = React.lazy(() =>
import(/* webpackChunkName: "chart" */ './components/Chart')
);
// Prefetch — load when browser is idle (user likely to navigate here soon)
import(/* webpackPrefetch: true */ './pages/About');
// Preload — load immediately (needed for current navigation)
import(/* webpackPreload: true */ './components/Header');
Persistent Caching
Webpack 5’s built-in persistent cache dramatically speeds up subsequent builds:
// webpack.config.ts
export default {
cache: {
type: 'filesystem', // Cache to disk
buildDependencies: {
config: [__filename], // Invalidate cache if config changes
},
cacheDirectory: '.webpack-cache',
},
};
# First build: ~15s
# Subsequent builds (cache hit): ~2s
Environment-Specific Configs
// webpack.common.ts — shared config
export const commonConfig = { ... };
// webpack.dev.ts
import { merge } from 'webpack-merge';
import { commonConfig } from './webpack.common';
export default merge(commonConfig, {
mode: 'development',
devtool: 'eval-source-map',
devServer: { hot: true, port: 3000 },
});
// webpack.prod.ts
import { merge } from 'webpack-merge';
import { commonConfig } from './webpack.common';
import CssMinimizerPlugin from 'css-minimizer-webpack-plugin';
export default merge(commonConfig, {
mode: 'production',
devtool: 'source-map',
optimization: {
minimizer: [
'...', // Keep Terser (default JS minifier)
new CssMinimizerPlugin(), // Also minify CSS
],
},
});
// package.json
{
"scripts": {
"dev": "webpack serve --config webpack.dev.ts",
"build": "webpack --config webpack.prod.ts",
"analyze": "ANALYZE=true webpack --config webpack.prod.ts"
}
}
Module Federation (Micro-Frontends)
Module Federation lets separate deployed apps share code at runtime:
// App A (host) — webpack.config.ts
import { ModuleFederationPlugin } from 'webpack/container';
new ModuleFederationPlugin({
name: 'host',
remotes: {
// Load components from App B at runtime
userApp: 'userApp@https://user.example.com/remoteEntry.js',
},
shared: {
react: { singleton: true, requiredVersion: '^18.0.0' },
'react-dom': { singleton: true, requiredVersion: '^18.0.0' },
},
}),
// App B (remote) — webpack.config.ts
new ModuleFederationPlugin({
name: 'userApp',
filename: 'remoteEntry.js', // Entry point for host apps
exposes: {
'./UserProfile': './src/components/UserProfile',
'./UserList': './src/components/UserList',
},
shared: { react: { singleton: true } },
}),
// Host app — dynamic import from App B
const UserProfile = React.lazy(() => import('userApp/UserProfile'));
function App() {
return (
<Suspense fallback={<Spinner />}>
<UserProfile userId={1} /> {/* Loaded from App B's deployment */}
</Suspense>
);
}
Bundle Analysis
npm install -D webpack-bundle-analyzer
# Run with analysis
ANALYZE=true npm run build
# Opens browser with interactive treemap of your bundle
Common optimizations from analysis:
- Large
moment.js→ replace withdate-fns(tree-shakeable) - Entire icon library → import only used icons
- Duplicate packages at different versions → dedupe with
resolve.alias
Related posts: