본문으로 건너뛰기
Advertisement

8.1 ESM vs CJS — 모듈 시스템 완전 이해

모듈 시스템이란?

JavaScript에는 두 가지 주요 모듈 시스템이 있습니다.

  • CJS (CommonJS): Node.js에서 기본으로 사용하던 방식 (require / module.exports)
  • ESM (ES Modules): 브라우저와 Node.js 모두 지원하는 표준 방식 (import / export)

TypeScript는 두 방식을 모두 지원하지만, 올바르게 설정하지 않으면 런타임 오류가 발생합니다.


CJS (CommonJS)

기본 문법

// math.js (CJS)
function add(a, b) {
return a + b;
}
module.exports = { add };

// main.js (CJS)
const { add } = require('./math');
console.log(add(1, 2)); // 3

TypeScript에서 CJS

// tsconfig.json: "module": "CommonJS"

// math.ts
export function add(a: number, b: number): number {
return a + b;
}

// main.ts
import { add } from './math';
// 컴파일 결과: const { add } = require('./math');

ESM (ES Modules)

기본 문법

// math.ts (ESM)
export function add(a: number, b: number): number {
return a + b;
}

export default function multiply(a: number, b: number): number {
return a * b;
}

// main.ts (ESM)
import multiply, { add } from './math.js'; // ESM에서는 .js 확장자 필수!
console.log(add(1, 2)); // 3
console.log(multiply(2, 3)); // 6

package.json 설정

{
"type": "module"
}

"type": "module" 설정 시 .js 파일이 ESM으로 처리됩니다.


tsconfig.json module 옵션

{
"compilerOptions": {
"module": "CommonJS", // Node.js CJS
"module": "ESNext", // 최신 ESM
"module": "NodeNext", // Node.js ESM (Node16과 동일)
"module": "Preserve", // 입력 형식 그대로 유지 (TS 5.4+)
"moduleResolution": "node", // 전통적인 Node 해석
"moduleResolution": "bundler", // Vite/webpack 등 번들러용 (TS 4.9+)
"moduleResolution": "NodeNext" // Node.js ESM 해석
}
}

module vs moduleResolution

옵션역할
moduleTypeScript가 출력할 모듈 형식
moduleResolutionimport 경로를 어떻게 해석할지

ESM에서의 파일 확장자 규칙

ESM 모드(NodeNext)에서 TypeScript는 .ts 파일을 작성하지만 import는 .js로 해야 합니다.

// ✅ 올바른 방법 (NodeNext 모드)
import { add } from './math.js'; // .ts 파일이지만 .js로 import

// ❌ 틀린 방법
import { add } from './math'; // 확장자 없음 — 오류 발생
import { add } from './math.ts'; // .ts 확장자 — 오류 발생

왜 .js로 써야 하나요?

TypeScript는 .ts → .js로 컴파일하므로, 런타임에는 .js 파일이 존재합니다. NodeNext 모드는 이 런타임 해석을 그대로 따릅니다.


CJS ↔ ESM 인터롭 (Interop)

ESM에서 CJS 모듈 가져오기

// CJS 라이브러리를 ESM에서 사용
import express from 'express'; // default import
import { Router } from 'express'; // named import
import * as fs from 'fs'; // namespace import

esModuleInterop 옵션

{
"compilerOptions": {
"esModuleInterop": true // 권장: CJS ↔ ESM 호환성 개선
}
}

esModuleInterop: true 없이는 import express from 'express'가 오류를 냅니다.

// esModuleInterop: false (구형)
import * as express from 'express';
const app = express();

// esModuleInterop: true (권장)
import express from 'express';
const app = express();

allowSyntheticDefaultImports

esModuleInterop: true를 설정하면 자동으로 활성화됩니다. default export가 없는 CJS 모듈도 import X from '...'로 가져올 수 있게 해줍니다.


moduleResolution: "bundler" 완전 이해

TypeScript 4.9에서 추가된 bundler 모드는 Vite, webpack, esbuild 등 번들러 환경에 최적화되어 있습니다.

{
"compilerOptions": {
"module": "ESNext",
"moduleResolution": "bundler",
"allowImportingTsExtensions": true // .ts/.tsx 확장자 허용
}
}
// bundler 모드: 확장자 없이 import 가능
import { add } from './math'; // ✅ 번들러가 처리
import { Button } from './Button'; // ✅ .tsx도 자동 해석

모드별 비교표

모드확장자 필요사용 환경
node불필요구형 Node.js CJS
NodeNext필요 (.js)Node.js ESM
bundler불필요Vite, webpack, Next.js

실전 설정 예시

Node.js CJS 프로젝트 (Express API)

{
"compilerOptions": {
"target": "ES2022",
"module": "CommonJS",
"moduleResolution": "node",
"esModuleInterop": true,
"outDir": "./dist",
"rootDir": "./src",
"strict": true
}
}

Node.js ESM 프로젝트

{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"outDir": "./dist",
"rootDir": "./src",
"strict": true
}
}
// package.json
{
"type": "module",
"scripts": {
"build": "tsc",
"start": "node dist/index.js"
}
}

Vite / Next.js 프론트엔드 프로젝트

{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "bundler",
"jsx": "react-jsx",
"strict": true,
"noEmit": true
}
}

흔한 실수와 해결책

오류 1: Cannot use import statement in a module

SyntaxError: Cannot use import statement in a module

원인: package.json에 "type": "module" 없이 ESM 문법 사용

해결: "type": "module" 추가 또는 module: CommonJS로 변경


오류 2: ERR_REQUIRE_ESM

Error [ERR_REQUIRE_ESM]: require() of ES Module ...

원인: CJS 코드에서 ESM 전용 패키지를 require()로 불러옴

해결:

// 동적 import 사용
const { default: chalk } = await import('chalk');

오류 3: 확장자 관련 오류 (NodeNext 모드)

Relative import paths need explicit file extensions in ECMAScript imports.

해결: import 경로에 .js 확장자 추가

import { helper } from './helper.js';  // ✅

고수 팁

1. 프로젝트 유형별 권장 설정

백엔드 (Node.js):  module: NodeNext + moduleResolution: NodeNext
프론트엔드: module: ESNext + moduleResolution: bundler
라이브러리: module: NodeNext (dual CJS/ESM 패키징 고려)

2. dual package (CJS + ESM 동시 지원)

// package.json
{
"exports": {
".": {
"import": "./dist/esm/index.js",
"require": "./dist/cjs/index.js"
}
}
}

3. verbatimModuleSyntax (TS 5.0+)

{
"compilerOptions": {
"verbatimModuleSyntax": true
}
}

타입 전용 import를 명시적으로 표시하도록 강제합니다.

import type { User } from './types';     // ✅ 타입 전용
import { type User, getUser } from './api'; // ✅ 혼합
Advertisement