Skip to main content

Command Palette

Search for a command to run...

번들링, 빌드, 그리고 TypeScript 변환의 모든 것

Published
•11 min read•View as Markdown

프론트엔드 개발을 하다 보면 webpack, 번들링, 빌드, TypeScript, Babel 등의 용어를 자주 접하게 됩니다.

하지만 이들의 정확한 차이와 관계를 이해하는 것은 쉽지 않습니다.

이 글에서는 이러한 개념들을 체계적으로 정리하고, 실제로 어떻게 동작하는지 상세히 알아보겠습니다.

1. 번들링 vs 빌드: 어떤 개념이 더 클까?

빌드가 더 큰 개념입니다

빌드(Build)는 소스 코드를 프로덕션 환경에서 실행 가능한 형태로 변환하는 전체 프로세스를 의미합니다.

반면 **번들링(Bundling)은 여러 파일을 하나 또는 몇 개의 파일로 합치는 특정 작업을 가리킵니다.

번들링이란?

번들링은 여러 개의 파일을 하나로 합치는 과정입니다:

src/
  ├── index.js
  ├── utils.js
  └── components/
      ├── Header.js
      └── Footer.js

        ↓ 번들링

dist/
  └── bundle.js  (모든 파일이 합쳐짐)

빌드 프로세스에 포함되는 작업들

빌드는 훨씬 더 많은 작업을 포함합니다:

  • 번들링 - 파일 합치기

  • 트랜스파일링 - Babel로 ES6+ → ES5 변환

  • 압축/최적화 - 코드 minification, uglification

  • 코드 스플리팅 - 필요에 따라 여러 번들로 나누기

  • 에셋 처리 - 이미지, 폰트, CSS 처리

  • 환경 변수 주입 - production/development 설정

  • 소스맵 생성 - 디버깅을 위한 맵 파일 생성

  • 해싱 - 캐시 무효화를 위한 파일명 해싱

실제 webpack 설정 예시

// webpack.config.js
module.exports = {
  // 빌드 프로세스 설정
  mode: 'production',

  // 번들링 시작점
  entry: './src/index.js',

  output: {
    filename: '[name].[contenthash].js' // 해싱
  },

  module: {
    rules: [
      { test: /\.js$/, use: 'babel-loader' }, // 트랜스파일링
      { test: /\.css$/, use: ['style-loader', 'css-loader'] } // CSS 처리
    ]
  },

  optimization: {
    minimize: true, // 압축
    splitChunks: { /* ... */ } // 코드 스플리팅
  }
};

관계 정리

빌드 (Build)
  ├── 번들링 (Bundling) ← 핵심 작업
  ├── 트랜스파일링
  ├── 최적화
  ├── 압축
  └── 기타 변환 작업들

2. Webpack을 왜 '번들러'라고 부를까?

Webpack은 실제로는 빌드 도구의 역할을 하는데, 왜 '번들러'라고 부를까요?

Webpack은 2012년에 등장했을 때 "모듈 번들러(Module Bundler)"로 소개되었습니다.

당시 여러 script 태그로 파일을 관리하던 문제를 해결하기 위해, **"여러 파일을 하나로 합치는 것"**이 핵심 가치였습니다.

이미 존재하던 Grunt, Gulp 같은 태스크 러너와 차별화하기 위해 "의존성 그래프 기반 모듈 번들링"이라는 독특한 정체성을 강조했고, 그래서 '번들러'라는 이름이 붙었습니다.

// Webpack의 핵심 아이디어: 모든 것을 모듈로
import Header from './Header.js'
import utils from './utils.js'
import './styles.css'  // CSS도 모듈!
import logo from './logo.png'  // 이미지도 모듈!

// → 의존성 그래프를 만들고 번들링

실제로는 "빌더"가 더 정확한 표현이지만, "번들링"이 가장 핵심적이고 독특한 기능이기 때문에 그 이름으로 불립니다.

3. TSX에서 JS로: 상세한 변환 과정

.tsx 파일이 .js 파일로 변환되는 과정을 단계별로 살펴보겠습니다.

ts-loader의 역할

먼저, Webpack에서 TypeScript 파일을 처리하는 ts-loader의 역할을 이해해야 합니다.

ts-loader는 내부적으로 **TypeScript 컴파일러(tsc)**를 호출합니다. 실제 변환 작업은 tsc가 수행하고, ts-loader는 Webpack과 tsc 사이의 중개자(bridge) 역할만 합니다:

// webpack.config.js
module.exports = {
  module: {
    rules: [
      {
        test: /\.tsx?$/,
        use: 'ts-loader',  // TypeScript Compiler를 호출
        exclude: /node_modules/
      }
    ]
  },
  resolve: {
    extensions: ['.tsx', '.ts', '.js']
  }
};
// tsconfig.json (ts-loader가 이 설정을 따름)
{
  "compilerOptions": {
    "target": "ES5",           // ES5 JavaScript로 변환
    "module": "ESNext",        // 모듈 시스템
    "strict": true,            // 엄격한 타입 체크
    "jsx": "react",            // JSX 처리 방법
    "sourceMap": true          // 소스맵 생성
  }
}

이제 실제 변환 과정을 살펴보겠습니다.

원본 TSX 파일

// src/components/UserCard.tsx
import React, { useState } from 'react';

interface UserProps {
  name: string;
  age: number;
  isAdmin?: boolean;
}

const UserCard: React.FC<UserProps> = ({ name, age, isAdmin = false }) => {
  const [count, setCount] = useState<number>(0);

  const handleClick = (): void => {
    setCount(prev => prev + 1);
  };

  return (
    <div className="user-card">
      <h2>{name}</h2>
      <p>Age: {age}</p>
      {isAdmin && <span>Admin</span>}
      <button onClick={handleClick}>
        Clicked {count} times
      </button>
    </div>
  );
};

export default UserCard;

1단계: TypeScript 컴파일러 (타입 제거)

TypeScript의 모든 타입 정보를 제거합니다:

// 타입 제거 후
import React, { useState } from 'react';

const UserCard = ({ name, age, isAdmin = false }) => {
  const [count, setCount] = useState(0);

  const handleClick = () => {
    setCount(prev => prev + 1);
  };

  return (
    <div className="user-card">
      <h2>{name}</h2>
      <p>Age: {age}</p>
      {isAdmin && <span>Admin</span>}
      <button onClick={handleClick}>
        Clicked {count} times
      </button>
    </div>
  );
};

export default UserCard;

제거된 것들:

  • interface UserProps

  • : React.FC<UserProps>

  • : string, : number, : boolean

  • <number> (제네릭)

  • : void

2단계: JSX 변환

JSX 문법을 일반 JavaScript 함수 호출로 변환합니다:

// JSX → JavaScript
import React, { useState } from 'react';

const UserCard = ({ name, age, isAdmin = false }) => {
  const [count, setCount] = useState(0);

  const handleClick = () => {
    setCount(prev => prev + 1);
  };

  return React.createElement(
    'div',
    { className: 'user-card' },
    React.createElement('h2', null, name),
    React.createElement('p', null, 'Age: ', age),
    isAdmin && React.createElement('span', null, 'Admin'),
    React.createElement(
      'button',
      { onClick: handleClick },
      'Clicked ', count, ' times'
    )
  );
};

export default UserCard;

JSX 변환 규칙:

<div className="box">Hello</div>
↓
React.createElement('div', { className: 'box' }, 'Hello')

<Component prop={value}>Children</Component>
↓
React.createElement(Component, { prop: value }, 'Children')

3단계: ES6+ → ES5 변환 (선택적)

tsconfig.json의 target 설정에 따라 추가 변환이 이루어집니다:

// target: "ES5"인 경우
"use strict";

var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};

Object.defineProperty(exports, "__esModule", { value: true });
var react_1 = __importDefault(require("react"));

var UserCard = function (_a) {
    var name = _a.name, age = _a.age, _b = _a.isAdmin, isAdmin = _b === void 0 ? false : _b;

    var _c = react_1.default.useState(0), 
        count = _c[0], 
        setCount = _c[1];

    var handleClick = function () {
        setCount(function (prev) { return prev + 1; });
    };

    return react_1.default.createElement(
        "div",
        { className: "user-card" },
        react_1.default.createElement("h2", null, name),
        react_1.default.createElement("p", null, "Age: ", age),
        isAdmin && react_1.default.createElement("span", null, "Admin"),
        react_1.default.createElement(
            "button",
            { onClick: handleClick },
            "Clicked ", count, " times"
        )
    );
};

exports.default = UserCard;

변환된 것들:

  • const → var

  • Arrow functions → function

  • Destructuring → 명시적 변수 할당

  • import → require

  • export → exports

전체 변환 흐름도

┌─────────────────────────────────┐
│  UserCard.tsx                   │
│  - TypeScript                   │
│  - JSX                          │
│  - ES6+ 문법                    │
└────────────┬────────────────────┘
             │
             ↓ ts-loader (TypeScript Compiler)
             │
┌────────────┴────────────────────┐
│  중간 단계                       │
│  - TypeScript 타입 제거          │
│  - 여전히 JSX                    │
└────────────┬────────────────────┘
             │
             ↓ JSX Transform
             │
┌────────────┴────────────────────┐
│  중간 단계                      │
│  - React.createElement()        │
│  - 여전히 ES6+                  │
└────────────┬────────────────────┘
             │
             ↓ Target 변환 (선택적)
             │
┌────────────┴────────────────────┐
│  UserCard.js                    │
│  - 순수 JavaScript              │
│  - 브라우저 실행 가능            │
└─────────────────────────────────┘

4. 변환 과정의 담당자들

각 변환 단계를 누가 담당하는지 명확히 정리해보겠습니다.

핵심은 Loader는 실제 변환을 하지 않고, 컴파일러/트랜스파일러를 호출만 한다는 것입니다.

TypeScript Compiler (tsc)의 역할

담당 작업:

  1. 타입 체크

  2. 타입 제거

  3. JSX 변환

  4. ES6+ → target 문법 변환

// 입력: .tsx
interface User { name: string }
const App: React.FC = () => <div>Hello</div>;

// 출력: .js
const App = () => React.createElement('div', null, 'Hello');

ts-loader의 실제 역할

ts-loader는 실제 변환을 하지 않고, TypeScript Compiler를 호출하는 역할만 합니다:

// ts-loader 내부 동작 (개념적)
const typescript = require('typescript');

function tsLoader(source) {
  // TypeScript 컴파일러 호출
  const result = typescript.transpileModule(source, {
    compilerOptions: { /* tsconfig.json 설정 */ }
  });

  return result.outputText; // JavaScript 반환
}

변환 흐름: TypeScript만 사용

┌──────────────────────┐
│   Component.tsx      │
│   - TypeScript       │
│   - JSX              │
│   - ES6+             │
└──────────┬───────────┘
           │
           │ Webpack
           ↓
    ┌──────────────┐
    │  ts-loader   │  (단순히 tsc 호출)
    └──────┬───────┘
           │
           ↓
    ┌─────────────────────────┐
    │ TypeScript Compiler     │
    │ (tsc)                   │
    │                         │
    │ 1. 타입 체크 & 제거      │
    │ 2. JSX → createElement  │
    │ 3. ES6+ → target 문법   │
    └──────┬──────────────────┘
           │
           ↓
    ┌──────────────┐
    │ Component.js │
    │ (ES5/ES6)    │
    └──────────────┘

변환 흐름: TypeScript + Babel 조합

┌──────────────────────┐
│   Component.tsx      │
└──────────┬───────────┘
           │
           │ Webpack
           ↓
    ┌──────────────┐
    │  ts-loader   │
    └──────┬───────┘
           │
           ↓
    ┌─────────────────────────┐
    │ TypeScript Compiler     │
    │                         │
    │ 1. 타입 체크 & 제거      │
    │ 2. JSX → createElement  │
    │ 3. target: ESNext 유지  │
    └──────┬──────────────────┘
           │
           ↓
    ┌──────────────┐
    │ babel-loader │
    └──────┬───────┘
           │
           ↓
    ┌─────────────────────────┐
    │ Babel                   │
    │                         │
    │ 1. ES6+ → ES5           │
    │ 2. Polyfill 추가        │
    │ 3. 브라우저 호환성 처리  │
    └──────┬──────────────────┘
           │
           ↓
    ┌──────────────┐
    │ Component.js │
    │ (ES5)        │
    └──────────────┘

각 도구의 역할 정리

도구역할특징
TypeScript Compiler (tsc)타입 체크 + 타입 제거 + JSX 변환 + 문법 변환타입 안정성 보장, 느림
ts-loadertsc를 호출하는 Webpack 로더tsc의 래퍼
Babel문법 변환 + Polyfill유연함, 플러그인 많음
babel-loaderBabel을 호출하는 Webpack 로더Babel의 래퍼

핵심: Loader는 실제 변환을 하지 않고, 해당 컴파일러/트랜스파일러를 호출만 합니다!

5. Babel vs TypeScript Compiler: 속도만 다를까?

많은 개발자들이 Babel과 TypeScript Compiler의 차이를 단순히 "속도" 정도로만 생각합니다.

하지만 실제로는 훨씬 더 근본적인 차이가 있습니다.

타입 체크 여부 (가장 중요한 차이)

TypeScript Compiler (tsc):

interface User {
  name: string;
  age: number;
}

const user: User = {
  name: "John",
  age: "30"  // ❌ 에러: string은 number에 할당 불가
};

// 컴파일 실패!

Babel (@babel/preset-typescript):

interface User {
  name: string;
  age: number;
}

const user: User = {
  name: "John",
  age: "30"  // ⚠️ 에러를 발견하지 못함!
};

// 컴파일 성공! (런타임에 버그 발생 가능)

지원하는 TypeScript 기능

TypeScript Compiler:

// ✅ 모든 TypeScript 기능 지원

// const enum (인라인 최적화)
const enum Direction {
  Up,
  Down
}
const dir = Direction.Up;  // → const dir = 0;

// namespace
namespace Utils {
  export function log() {}
}

// legacy decorators
@sealed
class MyClass {}

Babel:

// ❌ 일부 TypeScript 기능 미지원

// const enum - 일반 enum처럼 처리 (최적화 안 됨)
const enum Direction {
  Up,
  Down
}

// namespace - 지원 안 함
namespace Utils {  // ❌ 에러
  export function log() {}
}

Babel이 지원하지 않는 TypeScript 기능:

  • const enum (최적화된 형태)

  • namespace

  • Legacy decorators

  • export = / import = (CommonJS 스타일)

Polyfill 처리

TypeScript Compiler:

// tsconfig.json
{
  "compilerOptions": {
    "target": "ES5",
    "lib": ["ES2015"]
  }
}

const promise = new Promise();
// ❌ Promise polyfill을 수동으로 추가해야 함

Babel:

// babel.config.js
{
  "presets": [
    ["@babel/preset-env", {
      "useBuiltIns": "usage",  // 자동 polyfill 추가
      "corejs": 3
    }]
  ]
}

const promise = new Promise();
// ✅ 필요한 polyfill 자동으로 추가됨!

브라우저 타겟팅

TypeScript Compiler:

{
  "compilerOptions": {
    "target": "ES5"  // 단순히 ES 버전만 지정
  }
}

Babel:

{
  "presets": [
    ["@babel/preset-env", {
      "targets": {
        "chrome": "58",
        "ie": "11",
        "> 1%",
        "not dead"
      }
    }]
  ]
}

// 특정 브라우저에 맞춰 최적화!

비교 표

항목TypeScript CompilerBabel
타입 체크✅ 수행❌ 수행 안 함
속도느림 (타입 체크 때문)빠름
TypeScript 기능100% 지원~90% 지원
Polyfill수동 추가 필요자동 추가 가능
브라우저 타겟팅제한적 (ES 버전만)정교함 (browserslist)
플러그인제한적매우 풍부
설정 파일tsconfig.json.babelrc / babel.config.js
목적TypeScript → JavaScript모던 JS → 레거시 JS

핵심 차이점 요약

TypeScript Compiler (tsc)

  • ✅ 타입 체크로 안전성 보장

  • ✅ 모든 TS 기능 100% 지원

  • ❌ 빌드 속도 느림

  • ❌ 제한적인 변환 옵션

Babel

  • ✅ 빠른 빌드 속도

  • ✅ 유연한 플러그인 생태계

  • ✅ 정교한 브라우저 타겟팅

  • ✅ 자동 Polyfill 주입

  • ❌ 타입 체크 불가

  • ❌ 일부 TS 기능 미지원

6. 실무 전략: 어떤 조합을 선택할까?

전략 1: Babel만 사용

추천 상황: 빠른 빌드가 최우선인 경우

// 개발: Babel로 빠르게 변환
// 타입 체크: 별도로 tsc --noEmit 실행

// webpack.config.js
{
  test: /\.tsx?$/,
  use: 'babel-loader'
}

// package.json
{
  "scripts": {
    "dev": "webpack serve",
    "type-check": "tsc --noEmit --watch"
  }
}

장점

  • 빠른 HMR (Hot Module Replacement)

  • 유연한 변환 옵션

단점

  • 타입 체크 별도 실행 필요

전략 2: tsc만 사용

추천 상황: 설정 단순화와 타입 안전성이 중요한 경우

// webpack.config.js
{
  test: /\.tsx?$/,
  use: 'ts-loader'
}

// tsconfig.json
{
  "compilerOptions": {
    "target": "ES5"
  }
}

장점

  • 설정 단순

  • 타입 안전성 보장

단점

  • 느린 빌드

  • 제한적인 변환

전략 3: 하이브리드

추천 상황: 빌드 속도와 타입 안전성 모두 중요한 경우

// webpack.config.js
{
  test: /\.tsx?$/,
  use: [
    {
      loader: 'babel-loader',
      options: {
        presets: ['@babel/preset-env']
      }
    },
    {
      loader: 'ts-loader',
      options: {
        transpileOnly: true  // 타입 체크 스킵
      }
    }
  ]
}

// package.json
{
  "scripts": {
    "dev": "webpack serve",
    "type-check": "tsc --noEmit --watch",
    "build": "tsc --noEmit && webpack"
  }
}

장점

  • 빠른 빌드

  • Babel의 유연성 활용

단점

  • 설정 복잡

전략 4: esbuild

추천 상황: 초고속 빌드가 필요하고 최신 도구를 선호하는 경우

// 둘 다 대체하는 초고속 빌드 도구
{
  test: /\.tsx?$/,
  use: 'esbuild-loader'
}

장점

  • 매우 빠름 (Go로 작성)

단점

  • 타입 체크 안 함

  • 일부 기능 제한

결론

Webpack과 관련된 빌드 프로세스를 이해하는 것은 프론트엔드 개발자에게 필수적입니다.

핵심을 다시 정리하면:

  1. 빌드는 번들링을 포함하는 더 큰 개념입니다.

  2. Webpack은 의존성 그래프 기반 모듈 번들링이라는 핵심 정체성 때문에 '번들러'라고 불립니다.

  3. ts-loader는 TypeScript Compiler를 호출하는 중개자입니다.

  4. TSX → JS 변환은 타입 제거, JSX 변환, 문법 변환의 3단계로 이루어집니다.

  5. Babel과 TypeScript Compiler는 속도뿐만 아니라 기능, 목적, 철학이 다릅니다.

각 도구의 특성을 이해하고 프로젝트의 요구사항에 맞는 전략을 선택하는 것이 중요합니다.

타입 안전성이 중요하다면 TypeScript Compiler를, 빌드 속도와 유연성이 중요하다면 Babel을, 또는 두 가지를 조합하여 사용할 수 있습니다.


📝 참고사항: 이 글은 AI 어시스턴트 Claude(Anthropic)와의 대화를 바탕으로 작성되었습니다. 기술적 정확성을 위해 노력했지만, 실제 프로젝트에 적용하기 전에 공식 문서를 참고하시기 바랍니다.

More from this blog

LangGraph의 astream_events: 에이전트 실행을 실시간으로 들여다보기

💡 한 줄 요약: astream_events()는 LangGraph 그래프 실행 중 발생하는 모든 이벤트(LLM 토큰, 툴 호출, 노드 시작/종료 등)를 실시간 스트림으로 받을 수 있는 비동기 API입니다. 들어가며 AI 에이전트가 응답을 생성하는 데 25초가 걸린다고 상상해보세요. 사용자는 평균 8초 후에 페이지를 이탈합니다. 하지만 스트리밍을 도입

Jun 20, 20266 min read

LLM은 어떻게 글을 읽고 쓰는가 — 입력부터 출력까지

대화형 AI에게 질문을 던지면, 마치 사람처럼 문장을 이해하고 답을 써 내려가는 것처럼 보인다.하지만 그 안에서 벌어지는 일은 생각보다 단순하고, 또 생각보다 기계적이다.이 글에서는 두 가지 질문에 답해 본다.LLM은 어떻게 글을 만들어내는가,그리고 우리가 입력한 프롬프트는 모델에 어떤 모습으로 들어가는가. 1부. LLM은 어떻게 글을 만들어내는가 핵심은

Jun 10, 20264 min read

useEffect 지옥이란 무엇이며, 어떻게 안전하게 사용하는가

React를 어느 정도 사용하다 보면 한 번쯤은“useEffect 지옥에 빠졌다”는 말을 듣거나 직접 느껴본 적이 있을 것이다. useEffect가 계속 늘어나고 의존성 배열은 점점 길어지고 왜 실행되는지 이해하기 어려워지며 eslint-disable-next-line 이 늘어나는 상태 하지만 많은 사람들이 오해한다.useEffect 지옥은 useEffect가 많아서 생기는 문제가 아니다. 이 글에서는 ‘useEffect 지옥’의 ...

Jan 12, 20263 min read

Zod란 무엇인가: TypeScript에서 런타임 검증과 타입 안정성을 동시에 해결하는 방법

TypeScript 프로젝트를 하다 보면 반드시 마주치는 문제가 있다.바로 “타입은 있는데, 데이터는 믿을 수 없다”는 것이다. TypeScript는 컴파일 타임에는 강력하지만,런타임에 들어오는 데이터에 대해서는 아무런 보장을 해주지 않는다. API 요청 데이터 사용자 입력(Form) 환경 변수 (process.env) 외부 JSON / 설정 파일 이 모든 것은 타입 시스템 바깥에서 들어온다. 이 문제를 해결하기 위해 등장한 라이브러...

Jan 10, 20263 min read

Flatpak을 이해하기 위한 배경 지식

배포판, 라이브러리 충돌, 그리고 OSTree까지 리눅스 데스크톱에서 Flatpak이 표준처럼 자리 잡은 데에는 분명한 이유가 있다.그 이유를 이해하려면 단순히 “Flatpak은 패키징 시스템이다”를 넘어서,리눅스 배포판 구조, 라이브러리 버전 충돌, OSTree라는 기술까지 함께 이해해야 한다. 이 글에서는 다음 질문에 답해본다. 리눅스에서 말하는 배포판이란 무엇인가? 라이브러리 버전 충돌은 왜 발생하는가? Flatpak은 이 문제를 어...

Jan 8, 20265 min read

Dev note

32 posts