TS 탐구생활 - TS의 모듈에 관한 작은 정보들

목차

JS에서는 ES2015부터 import/export를 이용하는 모듈 시스템을 정식으로 지원한다. TS에서도 당연히 이를 공유하는데 TS에서 다른 점들을 몇 가지 메모하였다.

JS에서의 모듈에 대해서는 JS 탐구생활 - require와 import 그리고 JS의 모듈 시스템을 참고할 수 있다.

1. export

JS의 다른 객체들과 같이 타입도 export할 수 있다. 이는 다른 파일에서 import하여 사용할 수 있다.

// types.ts
export type Person = {
  name: string;
  age: number;
};
// index.ts
import { Person } from "./types";

*를 이용하면 모듈 전체를 불러와서 마치 모듈의 내용을 복사한 것처럼 사용할 수 있다. 다음 예시에서는 types.ts를 import해서 types라는 이름으로 사용한다.

// index.ts
import * as types from "./types";
 
const person: types.Person = {
  name: "witch",
  age: 20,
};

이렇게 하면 서로 다른 모듈에 같은 이름의 인터페이스나 네임스페이스가 있어도 병합되지 않고 충돌을 일으키지 않는다는 장점이 있다.

1.1. type export

그리고 import/export하는 대상이 값이 아닌 타입이라는 것을 type 키워드를 이용해서 명시할 수 있다. 이를 type import/export라고 한다.

// types.ts
type Person = {
  name: string;
  age: number;
};
 
export type { Person };
// index.ts
import type { Person } from "./types";

일반적으로는 TS가 import/export되는 값이 타입인지 여부를 알고 있기 때문에 이를 사용할 필요가 없다. 하지만 몇몇 경우에 필요하고 이점이 있으니 도입된 개념인데 이에 대해서는 따로 글을 작성할 예정이다.

1.2. export all

export *를 이용하면 모듈의 모든 export를 다른 모듈에서 사용할 수 있다.

export * as types from "./types";

이렇게 import해서 사용할 수 있다.

import { types } from "./types";

2. cjs와 esm의 호환

원래 commonJS 방식으로 export된 객체는 es2015 방식으로 import할 수 없다. 그래도 ts에서는 이를 해결하기 위한 몇 가지 방법을 제공한다.

2.1. commonjs export

commonJS에서는 exports 객체를 이용해서 파일 내에서 export할 수 있는 값들을 정의한다.

// commonJS
// 여러 객체를 내보낼 때
exports.a = 1;
exports.b = 2;
exports.c = 3;
 
// 하나의 객체를 내보낼 때
const obj = {
    a: 1,
    b: 2,
    c: 3
};
 
module.exports = obj;

이런 exports 객체와 비슷한 es2015 문법은 default export이다. 그런데 둘이 호환되지는 않는다. cjs 스타일로 export하고 es2015 스타일로 import할 수는 없다.

2.2. export = 문법

이 부분을 해결하기 위해서 ts에서는 export =라는 문법을 이용해서 모듈에서 export되는 단일 객체를 지정할 수 있도록 한다. 만약 라이브러리의 index.d.ts등에 들어갔는데 export = ...가 써있다면 그 라이브러리는 commonJS 모듈 시스템을 따르지만 모듈을 사용하는 곳에서는 import를 사용하여 ES2015 스타일로 모듈을 가져와 사용할 수 있도록 export =를 써서 export를 해놓은 것이다.

// types.ts
class Person {
  name: string;
  age: number;
}
 
export = Person;

그런데 이렇게 export된 모듈을 가져오기 위해서는 importrequire가 묘하게 섞인 문법이 사용된다. 참고로 ./types 파일에서 export =로 내보낸 객체는 하나뿐이므로 다른 이름을 사용해서 import해도 된다.

import Person = require("./types");

이렇게 모듈을 사용한 코드는 컴파일러에 의해 commonjs, AMD, ES6 모듈 문법 등으로 알아서 컴파일된다. 이 모듈은 컴파일 커맨드에서 --module키워드를 사용해 지정할 수 있다.

tsc --module commonjs index.ts

2.3. esModuleInterop

그런데 위와 같이 모듈을 가져오려면 importrequire를 동시에 써야 해서 어색하다. 이럴 때 tsconfig.json에서 esModuleInterop을 true로 설정하면 commonJS 모듈을 import할 때 es2015 모듈 문법을 자연스럽게 사용할 수 있다.

{
  "compilerOptions": {
    "esModuleInterop": true
  }
}

이렇게 설정하면 위에서 importrequire를 동시에 써서 모듈을 가져오던 코드를 다음과 같이 작성할 수 있게 된다. 컴파일러가 알아서 이 코드를 변환하여 두 가지 모듈 시스템을 호환시켜 주기 때문이다.

import Person from "./types";

이렇게 되는 원리에 대해서는 ES모듈방식과 CommonJS 모듈 방식을 섞어 사용하기(esModuleInterop)를 참고할 수 있다.

3. 스크립트 파일과 모듈 파일

파일 내부의 최상위 스코프에 import, export 키워드가 없으면 현재 파일의 타입 정의를 전역으로 사용할 수 있게 되는 스크립트 파일로 인식된다. 반면 import, export 키워드가 있을 시 모듈 파일이다.

// 스크립트 파일
interface Person {
  name: string;
  age: number;
}

반면 이런 식으로 export를 하면 모듈 파일이 된다. 주의할 점은 export가 최상위 스코프에 있는 게 아니라 네임스페이스 내에 있는 등 최상위가 아닌 다른 스코프에 있다면 스크립트 파일이라는 점이다.

// 모듈 파일
export interface Person {
  name: string;
  age: number;
}

스크립트 파일에 있는 타입과 같은 이름의 타입이 다른 모듈 파일에 있다면 주의해야 한다. 해당 타입을 그냥 사용할 때와 import해서 사용할 때의 타입 내용이 달라질 수 있기 때문이다.

예를 들어서 Person이라는 타입이 스크립트 파일에 있다면 이를 import하지 않고도 사용할 수 있다. 그런데 person.ts라는 모듈 파일에서도 Person타입을 export하고 있다면 Person 타입을 그냥 사용할 때와 import해서 사용할 때의 타입 내용이 달라질 수 있다.

참고

https://www.typescriptlang.org/ko/docs/handbook/modules.html

조현영 - 타입스크립트 교과서

https://www.typescriptlang.org/tsconfig#esModuleInterop