자바스크립트는 굉장히 오랜 시간동안 다양한 모듈 시스템을 지원해왔다.
하지만 최근에는 ES6의 ES 모듈 시스템으로 통합되고 있다. ES 모듈 시스템은 많이 알고있는 import/export
문법을 통한 모듈 노출 및 로드를 말한다.
ES 모듈 시스템은 2015년 ECMAScript에 소개되면서 2020년에 들어 대부분의 브라우저(IE는 미안하지만 빠져줘) JS 런타임이 지원하게 되었다.
타입스크립트에서는 top-level에 import
혹은 export
를 포함하고 있는 파일을 모듈로 간주한다.
자바스크립트 명세서에 따르면, 기본적으로 자바스크립트에서는 import
, export
구문이 없거나 top-level await
이 없다면 모듈로 보지 않고 하나의 스크립트로 간주한다. 스크립트 파일 내에서 선언된 변수와 타입은 global scope에 포함되어 공유된다.
어찌되었든 모듈은 각자의 스코프를 가지고 실행되기 때문에, 모듈 안에서 선언된 변수, 함수, 클래스 등은 외부 모듈에서 참조할 수 없다.
참조하기 위해서는 export
를 사용해서 명시적으로 노출시켜야 다른 모듈에서 참조할 수 있으며, 이러한 노출된 변수, 함수, 클래스 등을 다른 모듈에서 사용하기 위해서는 import
를 사용해야 한다.
예를 들어, 이 파일에서 노출하고 싶은 변수, 함수 등은 없지만 이 파일을 모듈로 만들고 싶으면
export {};
이렇게 export 문을 추가해주면 된다.
타입스크립트에서 module-based code를 작성하기 위해서는 세 가지를 고려해야 한다.
모듈에서 어떤 식별자를 어떻게 노출시키고 다른 모듈에서는 어떻게 import 할 수 있는지 알아봅시다. CommonJS 모듈 시스템에 대해선 별도로 언급하지 않습니다.
main export 선언은 export default
로 가능하다.
// @filename: hello.ts
export default function helloWorld() {
console.log("Hello, world!");
}
그 후에 import는 아래와 같이 한다.
import helloWorld from "./hello.js";
helloWorld();
다음과 같이 변수 혹은 함수 등을 하나씩 export할 수 있다.
// @filename: maths.ts
export var pi = 3.14;
export let squareTwo = 1.41;
export const phi = 1.61;
export class RandomNumberGenerator {}
export function absolute(num: number) {
if (num < 0) return num * -1;
return num;
}
아래와 같은 방식으로 import 한다.
import { pi, phi, absolute } from "./maths.js";
console.log(pi);
const absPhi = absolute(phi);
아래와 같이 노출된 변수 혹은 함수 등을 별칭으로 지정하여 import도 가능하다.
import { pi as π } from "./maths.js";
console.log(π);
아니면 모든 노출된 식별자들을 하나의 namespace에 담아서 import 할 수도 있다.
// @filename: app.ts
import * as math from "./maths.js";
console.log(math.pi);
const positivePhi = math.absolute(math.phi);
섞어서 사용도 가능
// @filename: maths.ts
export const pi = 3.14;
export default class RandomNumberGenerator {}
// @filename: app.ts
import RandomNumberGenerator, { pi as π } from "./maths.js";
RandomNumberGenerator;
//(alias) class RandomNumberGenerator
//import RandomNumberGenerator
console.log(π);
//(alias) const π: 3.14
//import π
어떠한 식별자도 포함하지 않고 그냥 파일(모듈)을 import 할 수도 있다.
// @filename: app.ts
import "./maths.ts";
console.log("3.14");
이러면 아무 식별자도 import 하지 않지만 maths.ts
의 모든 코드가 평가되기 때문에 다른 객체에 영향을 미치는 side-effect가 발생할 수 있다.
모든 타입들은 JS의 문법처럼 export, import 될 수 있다.
// @filename: animal.ts
export type Cat = { breed: string; yearOfBirth: number };
export interface Dog {
breeds: string[];
yearOfBirth: number;
}
// @filename: app.ts
import { Cat, Dog } from "./animal.js";
type Animals = Cat | Dog;
import type
을 사용하면 타입만 import 할 수 있다.
// @filename: animal.ts
export type Cat = { breed: string; yearOfBirth: number };
export type Dog = { breeds: string[]; yearOfBirth: number };
export const createCatName = () => "fluffy";
// @filename: valid.ts
import type { Cat, Dog } from "./animal.js";
export type Animals = Cat | Dog;
// @filename: app.ts
import type { createCatName } from "./animal.js";
const name = createCatName();
여기서 createCatName은 import type으로 import 되었기 때문에 값처럼 사용이 불가능하다.
// @filename: app.ts
import { createCatName, type Cat, type Dog } from "./animal.js";
export type Animals = Cat | Dog;
const name = createCatName();
타입스크립트 4.5부터 이렇게 type
을 앞에 붙여 한 번에 타입과 값을 import 할 수 있다.
CommonJS and ES Modules interop
There is a mis-match in features between CommonJS and ES Modules regarding the distinction between a default import and a module namespace object import. TypeScript has a compiler flag to reduce the friction between the two different sets of constraints with esModuleInterop.
CommonJS와 ES 모듈 사이에는 default import와 module namespace object import에 대한 차이점과 관련하여 기능이 일치하지 않는다..
TypeScript는 esModuleInterop이라는 컴파일러 플래그를 사용하여 두 가지 다른 제약 조건 세트 간의 마찰을 줄인다.
typescript 공식 문서: esModuleInterop
모듈 해석은 import나 require 문에서 문자열을 가져와 해당 문자열이 어떤 파일을 가리키는지 결정하는 과정이다.
TypeScript에는 Classic
과 Node
두 가지 해석 전략이 포함되어 있다.
컴파일러 옵션의 module
이 commonjs가 아닌 경우 기본값인 Classic
은 하위 호환성을 위해 포함되어 있습니다.
TypeScript 내에서 모듈 전략에 영향을 주는 많은 TSConfig 플래그는 다음과 같다.
typescript 공식 문서: moduleResolution
생성되는 JS 결과물에 대해 영향을 미치는 두 가지 옵션이 존재한다.
어떤 target을 사용할지는 ts 코드를 실행할 js runtime을 기준으로 결정하면 된다.
모든 모듈 간 통신은 모듈 로더를 통해 이루어지며 module 컴파일러 옵션은 어떤 모듈 시스템을 사용할지 결정한다.
런타임에서 모듈 로더는 모듈을 실행하기 전에 해당 모듈의 모든 종속성을 찾아서 실행하는 역할을 한다.
ES 모듈이 표준 이전에 만들어진 namespaces
라는 TS 모듈이 있다. 여러 유용한 기능을 가지고 있고 DefinitelyTyped에서도 활발히 쓰이고 있다.
namespaces
의 대부분의 기능은 ES 모듈에도 존재하기 때문에 아직 deprecated되지 않았지만 자바스크립트의 방향성을 따라가는 것이 좋을 것 같다.