icon
7장 : 모듈과 네임스페이스

CommonJS와의 상호 운용성


타입스크립트는 ES 모듈(import/export)을 기본 모듈 시스템으로 강력하게 지원하지만, Node.js 환경에서는 오랫동안 CommonJS 모듈 시스템(require/module.exports)이 지배적이었습니다. 많은 기존 Node.js 프로젝트와 npm 패키지들이 여전히 CommonJS를 사용하고 있으며, 타입스크립트 프로젝트를 개발할 때는 이러한 CommonJS 모듈들과 상호 운용해야 할 필요가 자주 발생합니다.

타입스크립트는 tsconfig.json 파일의 module 컴파일러 옵션을 통해 ES 모듈 코드를 CommonJS를 포함한 다양한 모듈 형식으로 트랜스파일(Transpile)할 수 있게 하며, 반대로 CommonJS 모듈을 타입스크립트 프로젝트에서 가져와 사용할 수 있도록 지원합니다.


module 컴파일러 옵션

tsconfig.json 파일의 compilerOptions.module 설정은 타입스크립트 코드를 어떤 모듈 시스템으로 컴파일할지 결정합니다.

  • "ESNext" 또는 "ES2015" 이상: ES 모듈(import/export) 구문을 그대로 유지하거나 최신 자바스크립트 표준에 맞춰 컴파일합니다. 웹팩(Webpack), Vite 등 최신 번들러와 함께 사용하기에 적합합니다.
  • "CommonJS": importexport 구문을 CommonJS의 requiremodule.exports 구문으로 변환합니다. Node.js 환경에서 직접 실행할 때 적합합니다.

예시: module 옵션에 따른 컴파일 결과

원본 TypeScript 코드 (myModule.ts)

myModule.ts
// myModule.ts
export const greeting = "Hello";

export function sayHello(name: string): void {
  console.log(`${greeting}, ${name}!`);
}

export default class Greeter {
  greet(name: string): void {
    console.log(`Greetings from Greeter, ${name}!`);
  }
}

tsconfig.json 설정

{
  "compilerOptions": {
    "target": "es2018",
    "module": "CommonJS", // 또는 "ESNext"
    "rootDir": "./src",
    "outDir": "./dist",
    "strict": true
  }
}

컴파일 결과 (dist/myModule.js) - module: "CommonJS" 일 때

"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.sayHello = exports.greeting = void 0;
exports.greeting = "Hello";
function sayHello(name) {
    console.log(`${exports.greeting}, ${name}!`);
}
exports.sayHello = sayHello;
var Greeter = /** @class */ (function () {
    function Greeter() {
    }
    Greeter.prototype.greet = function (name) {
        console.log("Greetings from Greeter, ".concat(name, "!"));
    };
    return Greeter;
}());
exports.default = Greeter;

export 구문이 exports.member = ... 또는 exports.default = ... 형태로 변환된 것을 볼 수 있습니다.

컴파일 결과 (dist/myModule.js) - module: "ESNext" 일 때

// target이 es2018이므로, es2018의 문법을 따르되 모듈은 esnext를 따릅니다.
export const greeting = "Hello";
export function sayHello(name) {
    console.log(`${greeting}, ${name}!`);
}
export default class Greeter {
    greet(name) {
        console.log(`Greetings from Greeter, ${name}!`);
    }
}

import/export 구문이 그대로 유지되는 것을 볼 수 있습니다. 이는 브라우저 환경이나 Node.js의 --experimental-modules 플래그를 사용하거나, 번들러에 의해 처리될 때 유용합니다.


CommonJS 모듈 가져오기

타입스크립트 프로젝트에서 CommonJS 모듈을 가져와 사용하는 것은 매우 자연스럽습니다. 타입스크립트 컴파일러는 .js 파일이나 .d.ts (타입 정의 파일)를 기반으로 CommonJS 모듈의 타입을 추론하려고 시도합니다.

1. require 구문 사용 (타입스크립트 런타임이 CommonJS인 경우)

module 옵션이 "CommonJS"로 설정된 경우, import 문은 내부적으로 require로 변환됩니다. 하지만 타입스크립트 소스 코드에서는 여전히 import 구문을 사용하는 것이 일반적입니다.

tsconfig.json

{
  "compilerOptions": {
    "module": "CommonJS",
    // ...
  }
}
// myUtility.js (CommonJS 모듈)
module.exports = {
  add: (a, b) => a + b,
  subtract: (a, b) => a - b
};
// app.ts
import { add, subtract } from './myUtility'; // TypeScript는 .js 파일의 export를 이해하여 타입을 추론

console.log(add(10, 5));
console.log(subtract(10, 5));

만약 myUtility.js에 대한 타입 정의 파일 (myUtility.d.ts)이 없다면, TypeScript는 해당 모듈의 타입으로 any를 추론할 수 있습니다. 명시적인 타입 안전성을 위해서는 declare module 또는 @types 패키지를 통해 타입 정의를 제공하는 것이 좋습니다.

2. esModuleInterop 옵션

CommonJS와 ES 모듈 간의 호환성을 높이기 위해 tsconfig.jsoncompilerOptions.esModuleInterop: true를 설정하는 것이 강력히 권장됩니다. 이 옵션을 활성화하면, CommonJS 모듈의 export default와 같은 동작을 ES 모듈의 import default 구문으로 자연스럽게 가져올 수 있도록 컴파일러가 추가적인 헬퍼 코드를 생성합니다.

{
  "compilerOptions": {
    "module": "CommonJS",
    "esModuleInterop": true, // 이것을 true로 설정
    // ...
  }
}

esModuleInterop: true가 없을 때의 문제점:

CommonJS 모듈은 exports.default = ... 형태가 아닌 module.exports = ... 형태로 기본 내보내기를 합니다. esModuleInterop: false인 상태에서 import SomeLib from 'some-lib'와 같이 CommonJS 모듈의 기본 내보내기를 가져오려 하면, 실제 CommonJS 모듈이 default 속성을 가지지 않기 때문에 런타임 오류(undefined가 되는 등)가 발생할 수 있습니다.

esModuleInterop: true로 설정하면, TypeScript는 컴파일된 JavaScript 코드에 __importDefault 헬퍼 함수를 추가하여 이러한 비호환성을 자동으로 처리해줍니다.

// date-fns (CommonJS로 배포되는 라이브러리의 일부)
// 실제 내용은 CommonJS 문법으로 되어있지만, 타입스크립트에서는 ES 모듈처럼 가져올 수 있음
import { format } from 'date-fns';
import addDays from 'date-fns/addDays'; // CommonJS 모듈의 default export

const today = new Date();
console.log(format(today, 'yyyy-MM-dd'));
console.log(addDays(today, 7));

@types 패키지를 통한 타입 지원

대부분의 인기 있는 자바스크립트 라이브러리(CommonJS로 배포되든 ES 모듈로 배포되든)는 DefinitelyTyped 프로젝트를 통해 타입 정의 파일(.d.ts 파일)을 제공합니다. 이 타입 정의 파일들은 @types/<패키지명> 형태로 npm에 배포되며, 이를 설치하면 타입스크립트가 해당 라이브러리의 타입을 인식하여 타입 안전성을 확보할 수 있습니다.

예시: lodash 라이브러리 사용 시

npm install lodash # lodash 라이브러리 설치
npm install --save-dev @types/lodash # lodash의 타입 정의 파일 설치

이제 타입스크립트 코드에서 lodash를 타입 안전하게 사용할 수 있습니다.

import _ from 'lodash'; // lodash는 기본 내보내기를 하는 CommonJS 모듈이므로, esModuleInterop: true 필요

const arr = [1, 2, 3, 4, 5];
console.log(_.sum(arr)); // 15
console.log(_.shuffle(arr)); // [4, 1, 5, 2, 3] (예시)

Node.js 환경에서 타입스크립트 실행 시 고려사항

Node.js 버전 12 이상부터는 --experimental-modules 플래그를 통해 ES 모듈을 지원하기 시작했으며, 버전 14 이상부터는 안정화되었습니다. Node.js 프로젝트에서 타입스크립트를 사용할 때는 다음과 같은 방식들을 고려할 수 있습니다.

  1. CommonJS로 컴파일 후 Node.js에서 실행 (가장 일반적): tsconfig.jsonmodule"CommonJS"로 설정하고, 컴파일된 .js 파일을 Node.js에서 직접 실행합니다.

    # tsconfig.json: "module": "CommonJS"
    tsc
    node dist/index.js
  2. ES 모듈로 컴파일 후 Node.js ESM에서 실행: tsconfig.jsonmodule"ESNext" 또는 "Node16" 등으로 설정하고, package.json"type": "module"을 추가하거나 .mjs 확장자를 사용하여 Node.js가 해당 파일을 ES 모듈로 인식하게 합니다.

    // package.json
    {
      "type": "module"
    }
    // tsconfig.json
    {
      "compilerOptions": {
        "module": "ESNext", // 또는 "Node16", "NodeNext"
        "target": "ESNext",
        "outDir": "./dist",
        "esModuleInterop": true,
        "moduleResolution": "node" // Node.js 환경에서는 "node"가 일반적
      }
    }
    tsc
    node dist/index.js # Node.js 런타임이 ES 모듈을 인식함

    이 방식은 최신 Node.js 환경에서 ES 모듈의 이점을 활용할 수 있게 하지만, 모든 CommonJS 모듈과의 호환성을 보장하기 어려울 수 있습니다.


CommonJS와의 상호 운용성은 타입스크립트 개발에서 피할 수 없는 중요한 부분입니다. tsconfig.jsonmoduleesModuleInterop 옵션을 적절히 설정하고, 필요하다면 @types 패키지를 활용하여 CommonJS 라이브러리에 대한 타입 정의를 추가함으로써, 타입스크립트의 타입 안전성을 유지하면서도 광범위한 자바스크립트 생태계를 활용할 수 있습니다.