TypeScript와 webpack 통합하기

TypeScript와 webpack 통합하기

TypeScript는 타입이 있는 JavaScript의 상위 집합(superset)이라서, 그대로 두면 일반 JavaScript로 컴파일되어요. 이 가이드에서는 webpack에 TypeScript를 통합하는 방법을 함께 하나씩 살펴볼게요.

출처: webpack 공식문서 — TypeScript

본문

기본 설정 (Basic Setup)

먼저 TypeScript 컴파일러와 로더(loader)를 설치해요.

npm install --save-dev typescript ts-loader

이제 디렉터리 구조와 설정 파일을 이렇게 바꿔볼게요.

project

 webpack-demo
  ├── package.json
  ├── package-lock.json
+ ├── tsconfig.json
- ├── webpack.config.js
+ ├── webpack.config.ts
  ├── /dist
  │   ├── bundle.js
  │   └── index.html
  ├── /src
- │   ├── index.js
+ │   └── index.ts
  └── /node_modules

tsconfig.json

JSX를 지원하고 TypeScript를 ES5까지 내려 컴파일하도록 설정해 볼게요.

{
  "compilerOptions": {
    "outDir": "./dist/",
    "noImplicitAny": true,
    "module": "esnext",
    "moduleResolution": "bundler",
    "target": "esnext",
    "jsx": "react-jsx",
    "allowJs": true
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules"]
}

tsconfig.json의 각 옵션을 더 자세히 알고 싶다면 TypeScript 문서를 확인해 보세요. webpack 설정 자체가 궁금하다면 configuration concepts 문서를 참고하면 돼요.

이제 webpack이 TypeScript를 처리하도록 설정해 볼게요. 먼저 필요한 의존성을 설치해요.

npm install --save-dev ts-node @types/node

webpack.config.ts

import path from "node:path";
import { fileURLToPath } from "url";
import webpack from "webpack";

// in case you run into any TypeScript error when configuring `devServer`
import "webpack-dev-server";

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

const config: webpack.Configuration = {
  entry: "./src/index.ts",
  module: {
    rules: [
      {
        test: /\.tsx?$/,
        use: "ts-loader",
        exclude: /node_modules/,
      },
    ],
  },
  resolve: {
    extensions: [".tsx", ".ts", ".js"],
  },
  output: {
    filename: "bundle.js",
    path: path.resolve(__dirname, "dist"),
  },
};

export default config;

Typescript file로 설정을 작성하는 방법에 대한 더 자세한 내용은 해당 문서를 참고하세요.

이렇게 하면 webpack이 ./index.ts를 *진입점(entry)*으로 삼고, .ts.tsx 파일을 전부 ts-loader로드해서, 현재 디렉터리에 bundle.js 파일을 내보내게 돼요.

이제 ./index.ts에서 lodash를 가져오는 방식을 조정해 볼게요. lodash 정의에는 기본 내보내기(default export)가 없어서, import 문을 다음과 같이 바꿔줘야 해요. 먼저 TypeScript 정의를 설치해요.

npm install --save-dev @types/lodash

그리고 파일 맨 위에 있는 import 문을 이렇게 수정해요.

./index.ts

- import _ from 'lodash';
+ import * as _ from 'lodash';

  function component() {
    const element = document.createElement('div');

    element.innerHTML = _.join(['Hello', 'webpack'], ' ');

    return element;
  }

  document.body.appendChild(component());

webpack.config.ts에서 TypeScript를 쓰는 세 가지 방법 (Ways to Use TypeScript)

webpack.config.ts에서 TypeScript를 사용하는 방법은 세 가지가 있어요.

  1. Node.js 내장 타입 제거(type stripping) 기능 이용 (권장):
webpack -c ./webpack.config.ts

Node.js의 내장 type-stripping으로 설정을 불러오려 시도하고, 그다음 interpretrechoir를 이용해 설정 파일을 불러오려 시도해요. 이 경우 tsxts-node 같은 도구가 필요할 수 있어요.

  1. Node.js용 커스텀 --import/--require 사용:
NODE_OPTIONS='--import=tsx --no-experimental-strip-types'  webpack -c ./webpack.config.ts
NODE_OPTIONS='--require=ts-node/register --no-experimental-strip-types'  webpack -c ./webpack.config.ts

--no-experimental-strip-types 플래그는 Node.js 버전 22.7.0부터 필요해요.

  1. Node.js ≥ v22.7.0의 내장 transform types 기능 사용:

enum 선언이나 매개변수 속성(parameter properties)처럼 JavaScript 코드 생성이 필요한, 지울 수 없는 TypeScript 문법을 변환할 때는 이렇게 해요.

NODE_OPTIONS='--experimental-transform-types' webpack --disable-interpret -c ./webpack.config.ts

TypeScript 경로 별칭 (TypeScript Path Aliases)

5.105.0+

tsconfig.jsoncompilerOptions.pathscompilerOptions.baseUrl로 import 별칭을 만든다면, webpack 5.105부터는 webpack이 resolve.tsconfig를 통해 그 별칭을 바로 읽어 올 수 있어요. 이 기능이 tsconfig-paths-webpack-plugin을 대체하게 되었으니, 더 이상 그 플러그인은 쓰지 않는 게 좋아요.

resolve.tsconfigboolean | string | object를 받아요.

webpack.config.ts

export default {
  resolve: {
    tsconfig: true, // automatically find tsconfig.json
  },
};

문자열을 넘기면 특정 파일을 가리킬 수 있어요. 모노레포에서 유용하죠.

export default {
  resolve: {
    tsconfig: "./tsconfig.app.json",
  },
};

객체를 넘기면 TypeScript project references도 함께 처리할 수 있어요.

export default {
  resolve: {
    tsconfig: {
      configFile: "./tsconfig.json",
      references: "auto", // inherit references from tsconfig, or pass an array of paths
    },
  },
};

tsconfig.json

{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@/*": ["src/*"]
    }
  }
}

이렇게 해 두면 @/components/Button이 별도 플러그인 없이, resolve.alias에 별칭을 중복해서 적어 넣지 않아도 src/components/Button으로 해석되어요.

tsconfig-paths-webpack-plugin에서 마이그레이션하기

지금 tsconfig-paths-webpack-plugin을 쓰고 있다면, 내장된 resolve.tsconfig 옵션으로 바꿀 수 있어요.

- import TsconfigPathsPlugin from 'tsconfig-paths-webpack-plugin';

  export default {
    resolve: {
-     plugins: [new TsconfigPathsPlugin()],
+     // Auto-find tsconfig.json in the project root
+     tsconfig: true,
+
+     // Or explicitly point to one
+     // tsconfig: './tsconfig.app.json'
    },
  };

그리고 프로젝트에서 패키지를 제거해요.

npm uninstall tsconfig-paths-webpack-plugin

로더 (Loader)

이 가이드에서는 ts-loader를 사용해요. 다른 web 어셋을 import하는 것 같은 webpack 기능을 활성화하기가 조금 더 쉽거든요.

참고로 이미 babel-loader로 코드를 트랜스파일하고 있다면, 추가 로더 대신 @babel/preset-typescript를 써서 JavaScript와 TypeScript 파일을 모두 Babel이 처리하게 할 수도 있어요. 다만 ts-loader와 달리 내부의 @babel/plugin-transform-typescript 플러그인은 타입 검사를 수행하지 않는다는 점을 기억해 두세요.

소스 맵 (Source Maps)

소스 맵 자체를 더 알고 싶다면 development guide 문서를 확인해 보세요.

소스 맵을 활성화하려면, TypeScript가 컴파일된 JavaScript 파일에 인라인 소스 맵을 출력하도록 설정해 줘야 해요. TypeScript 설정에 다음 줄을 추가해 볼게요.

tsconfig.json

  {
    "compilerOptions": {
      "outDir": "./dist/",
+     "sourceMap": true,
      "noImplicitAny": true,
      "module": "esnext",
      "moduleResolution": "bundler",
      "target": "esnext",
      "jsx": "react-jsx",
      "allowJs": true,
  },
    "include": ["src/**/*"],
    "exclude": ["node_modules"]
  }

이제 webpack에 이 소스 맵을 추출해서 최종 bundle에 포함시키라고 알려줘요.

webpack.config.ts

 import path from "node:path";
 import { fileURLToPath } from 'url';

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

  export default {
    entry: './src/index.ts',
+   devtool: 'inline-source-map',
    module: {
      rules: [
        {
          test: /\.tsx?$/,
          use: 'ts-loader',
          exclude: /node_modules/,
        },
      ],
    },
    resolve: {
      extensions: [ '.tsx', '.ts', '.js' ],
    },
    output: {
      filename: 'bundle.js',
      path: path.resolve(__dirname, 'dist'),
    },
  };

devtool 옵션에 대한 더 자세한 내용은 devtool 문서를 참고하세요.

클라이언트 타입 (Client types)

TypeScript 코드 안에서 webpack 특유의 기능, 예를 들어 import.meta.webpack 같은 것을 쓸 수 있어요. webpack은 이런 기능들을 위한 타입도 제공하므로, TypeScript reference 지시어를 추가해 선언해 주세요.

/// <reference types="webpack/module" />
console.log(import.meta.webpack); // without reference declared above, TypeScript will throw an error

프로젝트 전체에서 이 타입을 사용하려면, tsconfig.jsoncompilerOptions.typeswebpack/module을 추가해요.

  {
    "compilerOptions": {
      "types": [
+       "webpack/module"
      ]
    }
  }

서드파티 라이브러리 사용하기 (Using Third Party Libraries)

npm에서 서드파티 라이브러리를 설치할 때는 반드시 그 라이브러리의 타입 정의(typing definition)도 함께 설치하는 게 중요해요.

예를 들어 lodash를 쓰고 싶다면, 타입 정의를 설치하기 위해 다음 명령을 실행하면 돼요.

npm install --save-dev @types/lodash

npm 패키지가 이미 선언 타입을 번들에 포함하고 있다면, 그에 해당하는 @types 패키지를 받을 필요는 없어요. 자세한 내용은 TypeScript changelog blog를 참고하세요.

다른 어셋 가져오기 (Importing Other Assets)

TypeScript와 함께 코드가 아닌 어셋을 사용하려면, 그 import에 대한 타입을 지연(defer)시켜 줘야 해요. 이를 위해 프로젝트 안에서 TypeScript의 커스텀 정의를 의미하는 custom.d.ts 파일이 필요해요. .svg 파일에 대한 선언을 이렇게 만들어 볼게요.

custom.d.ts

declare module "*.svg" {
  const content: any;
  export default content;
}

여기서는 .svg로 끝나는 모든 import를 지정해서 SVG용 새 모듈을 선언하고, 그 모듈의 contentany로 정의했어요. URL임을 더 명확히 하고 싶다면 타입을 string으로 정의할 수도 있어요. CSS, SCSS, JSON 등 다른 어셋에도 같은 개념이 적용돼요.

빌드 성능 (Build Performance)

빌드 도구에 대한 자세한 내용은 Build Performance 가이드를 참고하세요.

더 알아보기