모듈
모듈 (Modules)
코드가 커지면서 파일 경계를 넘어 코드를 주고받는 일은 필수가 돼요. 모듈은 그걸 가능하게 하는 JavaScript의 표준 방식입니다. 이 장에서는 ES 모듈의 import/export 문법과 그 전신인 CommonJS 문법을 중심으로, TypeScript에서 모듈을 정의하고 내보내고 불러오는 방법을 살펴볼게요.
출처: TypeScript 핸드북
JavaScript는 코드를 모듈화하는 여러 방식의 오랜 역사를 갖고 있어요. 2012년부터 존재해 온 TypeScript는 그중 많은 형식을 지원해 왔지만, 시간이 지나면서 커뮤니티와 JavaScript 명세는 ES 모듈(또는 ES6 모듈)이라는 형식으로 수렴했습니다. 아마도 import/export 문법으로 알고 있을 거예요.
ES 모듈은 2015년에 JavaScript 명세에 추가되었고, 2020년 즈음에는 대부분의 웹 브라우저와 JavaScript 런타임에서 폭넓게 지원되었습니다.
이 핸드북은 ES 모듈과 그 인기 있던 전신인 CommonJS module.exports = 문법을 함께 다룰 거예요. 다른 모듈 패턴에 대한 정보는 Modules 참조 섹션에서 찾을 수 있습니다.
JavaScript 모듈이 어떻게 정의되는가
TypeScript에서는 ECMAScript 2015에서와 마찬가지로, 최상위 import 또는 export를 포함하는 모든 파일이 모듈로 간주됩니다.
반대로 최상위 import나 export 선언이 없는 파일은 그 내용이 전역 스코프에서 사용 가능한(따라서 모듈에서도 사용 가능한) 스크립트로 취급됩니다.
모듈은 전역 스코프가 아니라 자체 스코프 안에서 실행됩니다. 즉 모듈 안에서 선언한 변수, 함수, 클래스 등은 export 형식 중 하나로 명시적으로 내보내지 않는 한 모듈 밖에서는 보이지 않아요. 반대로 다른 모듈에서 내보낸 변수, 함수, 클래스, 인터페이스 등을 사용하려면 import 형식 중 하나로 가져와야 합니다.
비-모듈 (Non-modules)
시작하기 전에 TypeScript가 무엇을 모듈로 보는지 이해하는 게 중요해요. JavaScript 명세는 import 선언, export, 또는 최상위 await가 없는 JavaScript 파일을 모듈이 아니라 스크립트로 취급해야 한다고 선언합니다.
스크립트 파일 안에서 변수와 타입은 공유되는 전역 스코프에 선언된다고 간주되며, 여러 입력 파일을 하나의 출력 파일로 합치기 위해 outFile 컴파일러 옵션을 쓰거나, HTML에서 여러 <script> 태그로 이 파일들을 (올바른 순서로!) 로드할 것이라고 가정합니다.
현재 import나 export가 없는 파일인데 모듈로 취급되길 원한다면, 다음 줄을 추가하세요.
export {};
이러면 파일이 아무것도 내보내지 않는 모듈로 바뀝니다. 이 문법은 모듈 target과 무관하게 동작해요.
TypeScript에서의 모듈
TypeScript에서 모듈 기반 코드를 작성할 때 고려할 세 가지 핵심 사항이 있습니다.
- 문법(Syntax): 무엇을 import·export 하기 위해 어떤 문법을 쓸까?
- 모듈 해석(Module Resolution): 모듈 이름(또는 경로)과 디스크의 파일 사이의 관계는 무엇인가?
- 모듈 출력 target(Module Output Target): 방출되는 JavaScript 모듈은 어떤 모습이어야 할까?
ES 모듈 문법
파일은 export default를 통해 주요 export를 선언할 수 있어요.
// @filename: hello.ts
export default function helloWorld() {
console.log("Hello, world!");
}
이 파일은 다음과 같이 import 됩니다.
// @filename: hello.ts
export default function helloWorld() {
console.log("Hello, world!");
}
// @filename: index.ts
// ---cut---
import helloWorld from "./hello.js";
helloWorld();
기본 export와 더불어, default를 생략한 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 문법으로 사용할 수 있어요.
// @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;
}
// @filename: app.ts
// ---cut---
import { pi, phi, absolute } from "./maths.js";
console.log(pi);
const absPhi = absolute(phi);
// ^?
추가적인 import 문법
import는 import {old as new} 같은 형식으로 이름을 바꿀 수 있어요.
// @filename: maths.ts
export var pi = 3.14;
// @filename: app.ts
// ---cut---
import { pi as π } from "./maths.js";
console.log(π);
// ^?
위 문법들을 하나의 import 안에서 섞어 쓸 수도 있습니다.
// @filename: maths.ts
export const pi = 3.14;
export default class RandomNumberGenerator {}
// @filename: app.ts
import RandomNumberGenerator, { pi as π } from "./maths.js";
RandomNumberGenerator;
// ^?
console.log(π);
// ^?
내보낸 모든 객체를 * as name을 사용해 하나의 네임스페이스에 담을 수 있어요.
// @filename: maths.ts
export var pi = 3.14;
export let squareTwo = 1.41;
export const phi = 1.61;
export function absolute(num: number) {
if (num < 0) return num * -1;
return num;
}
// ---cut---
// @filename: app.ts
import * as math from "./maths.js";
console.log(math.pi);
const positivePhi = math.absolute(math.phi);
// ^?
import "./file"를 사용하면 현재 모듈에 변수를 포함시키지 않고 파일을 import 할 수 있어요.
// @filename: maths.ts
export var pi = 3.14;
// ---cut---
// @filename: app.ts
import "./maths.js";
console.log("3.14");
이 경우 import는 아무것도 하지 않습니다. 다만 maths.ts의 모든 코드가 평가되어, 다른 객체에 영향을 줄 수 있는 부수 효과(side-effects)를 일으킬 수도 있어요.
TypeScript 고유의 ES 모듈 문법
타입도 JavaScript 값과 같은 문법으로 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;
TypeScript는 타입의 import를 선언하는 두 가지 개념으로 import 문법을 확장했습니다.
import type
타입만 import 할 수 있는(그것 만) 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
// @errors: 1361
import type { createCatName } from "./animal.js";
const name = createCatName();
인라인 type import
TypeScript 4.5는 또한 개별 import에 type을 접두사로 붙여 해당 참조가 타입임을 나타낼 수 있게 허용합니다.
// @filename: animal.ts
export type Cat = { breed: string; yearOfBirth: number };
export type Dog = { breeds: string[]; yearOfBirth: number };
export const createCatName = () => "fluffy";
// ---cut---
// @filename: app.ts
import { createCatName, type Cat, type Dog } from "./animal.js";
export type Animals = Cat | Dog;
const name = createCatName();
이 둘을 함께 쓰면 Babel, swc, esbuild 같은 비-TypeScript 트랜스파일러가 어떤 import를 안전하게 제거할 수 있는지 알 수 있습니다.
CommonJS 동작을 가진 ES 모듈 문법
TypeScript에는 CommonJS 및 AMD require와 직접적으로 대응하는 ES 모듈 문법이 있습니다. ES 모듈을 사용한 import는 대부분의 경우 그 환경들의 require와 같지만, 이 문법은 TypeScript 파일에서 CommonJS 출력과 1:1 대응을 보장해 줍니다.
/// <reference types="node" />
// @module: commonjs
// ---cut---
import fs = require("fs");
const code = fs.readFileSync("hello.ts", "utf8");
이 문법에 대해 더 알고 싶다면 모듈 참조 페이지를 참고하세요.
CommonJS 문법
CommonJS는 npm의 대부분의 모듈이 배포되는 형식입니다. 위의 ES 모듈 문법으로 작성하더라도 CommonJS 문법이 어떻게 동작하는지 간단히 이해하면 디버깅하기가 훨씬 쉬워져요.
내보내기 (Exporting)
식별자는 module이라는 전역 객체의 exports 프로퍼티를 설정해 내보내집니다.
/// <reference types="node" />
// ---cut---
function absolute(num: number) {
if (num < 0) return num * -1;
return num;
}
module.exports = {
pi: 3.14,
squareTwo: 1.41,
phi: 1.61,
absolute,
};
그러면 이 파일들은 require 문으로 import 할 수 있어요.
// @module: commonjs
// @filename: maths.ts
/// <reference types="node" />
function absolute(num: number) {
if (num < 0) return num * -1;
return num;
}
module.exports = {
pi: 3.14,
squareTwo: 1.41,
phi: 1.61,
absolute,
};
// @filename: index.ts
// ---cut---
const maths = require("./maths");
maths.pi;
// ^?
혹은 JavaScript의 구조 분해(destructuring) 기능을 사용해 조금 단순화할 수도 있어요.
// @module: commonjs
// @filename: maths.ts
/// <reference types="node" />
function absolute(num: number) {
if (num < 0) return num * -1;
return num;
}
module.exports = {
pi: 3.14,
squareTwo: 1.41,
phi: 1.61,
absolute,
};
// @filename: index.ts
// ---cut---
const { squareTwo } = require("./maths");
squareTwo;
// ^?
CommonJS와 ES 모듈 상호운용
CommonJS와 ES 모듈 사이에는 기본 import와 모듈 네임스페이스 객체 import의 구분과 관련해 기능상 불일치가 있습니다. TypeScript는 esModuleInterop이라는 컴파일러 플래그를 통해 두 제약 세트 사이의 마찰을 줄여 줍니다.
TypeScript의 모듈 해석 옵션
모듈 해석은 import 또는 require 문에서 문자열을 받아 그 문자열이 어떤 파일을 가리키는지 결정하는 과정입니다.
TypeScript는 두 가지 해석 전략을 포함합니다: Classic과 Node. Classic은 컴파일러 옵션 module이 commonjs가 아닐 때의 기본값으로, 하위 호환성을 위해 포함되어 있습니다. Node 전략은 Node.js가 CommonJS 모드에서 동작하는 방식을 재현하되, .ts와 .d.ts에 대한 추가 검사가 들어갑니다.
TypeScript 내부에서 모듈 전략에 영향을 주는 TSConfig 플래그는 많습니다: moduleResolution, baseUrl, paths, rootDirs.
이 전략들이 어떻게 동작하는지에 대한 전체적인 내용은 Module Resolution 참조 페이지에서 확인할 수 있습니다.
TypeScript의 모듈 출력 옵션
방출되는 JavaScript 출력에 영향을 주는 두 가지 옵션이 있습니다.
target: 어떤 JS 기능을 다운레벨링(downlevel, 더 오래된 런타임에서 동작하도록 변환)할지, 무엇을 그대로 둘지를 결정합니다.module: 모듈들이 서로 상호작용하는 데 어떤 코드를 사용할지 결정합니다.
어떤 target을 쓸지는 TypeScript 코드를 실행할 JavaScript 런타임에서 사용 가능한 기능에 따라 결정됩니다. 지원하는 가장 오래된 웹 브라우저, 실행할 Node.js의 최소 버전이 될 수도 있고, Electron처럼 런타임의 특수한 제약에서 비롯될 수도 있어요.
모듈 간의 모든 통신은 모듈 로더를 통해 일어나며, 컴파일러 옵션 module이 어떤 로더를 쓸지 결정합니다. 런타임에서 모듈 로더는 모듈을 실행하기 전에 그 모듈의 모든 의존성을 찾아 실행하는 역할을 담당합니다.
예를 들어 다음은 ES 모듈 문법을 사용하는 TypeScript 파일로, module에 대한 몇 가지 다른 옵션을 보여줍니다.
// @filename: constants.ts
export const valueOfPi = 3.142;
// @filename: index.ts
// ---cut---
import { valueOfPi } from "./constants.js";
export const twoPi = valueOfPi * 2;
ES2020
// @showEmit
// @module: es2020
// @noErrors
import { valueOfPi } from "./constants.js";
export const twoPi = valueOfPi * 2;
CommonJS
// @showEmit
// @module: commonjs
// @noErrors
import { valueOfPi } from "./constants.js";
export const twoPi = valueOfPi * 2;
UMD
// @showEmit
// @module: umd
// @noErrors
import { valueOfPi } from "./constants.js";
export const twoPi = valueOfPi * 2;
ES2020은 사실상 원래의
index.ts와 동일하다는 점에 주의하세요.
사용 가능한 모든 옵션과 그것들이 방출하는 JavaScript 코드가 어떤 모습인지는 TSConfig 참조의 module에서 볼 수 있습니다.
TypeScript 네임스페이스
TypeScript에는 ES 모듈 표준보다 먼저 존재했던 namespaces라는 자체 모듈 형식이 있습니다. 이 문법은 복잡한 정의 파일을 만드는 데 유용한 기능이 많으며, DefinitelyTyped에서 여전히 활발히 사용됩니다. 비록 deprecated는 아니지만, 네임스페이스의 대부분 기능은 ES 모듈에 존재하므로 JavaScript의 방향에 맞추기 위해 ES 모듈 사용을 권장합니다. 네임스페이스에 대해 더 알고 싶다면 네임스페이스 참조 페이지를 참고하세요.