Webpack Complete Guide | Loaders, Plugins, Code Splitting & Optimization

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
ConceptRole
EntryWhere webpack starts building the dependency graph
OutputWhere to write the bundle files
LoaderTransforms files (TS→JS, SCSS→CSS, PNG→data-URL)
PluginHooks into the build process (HTML generation, env vars, etc.)
Modedevelopment or production — sets defaults
ChunkA 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 with date-fns (tree-shakeable)
  • Entire icon library → import only used icons
  • Duplicate packages at different versions → dedupe with resolve.alias

Related posts: