프로그램이 커지면 코드를 여러 파일로 나누고, 다른 사람이 만든 패키지를 가져다 쓰게 됩니다. TypeScript는 JavaScript 표준 모듈 시스템인 ES 모듈을 그대로 쓰고, 그 위에 타입 정보를 함께 주고받는 방법을 더합니다. 이 장에서는 import와 export, npm 패키지와 package.json, @types 타입 선언 패키지, 모듈 관련 tsconfig 옵션, 빌드 방법을 살펴봅니다.
import와 export
파일 하나가 모듈 하나입니다. 다른 파일에서 쓰게 할 값과 타입에는 export를 붙이고, 쓰는 쪽에서는 import로 가져옵니다.
// src/math.ts
export const PI = 3.14159;
export function circleArea(radius: number): number {
return PI * radius * radius;
}
export interface Range {
min: number;
max: number;
}
export default function clamp(value: number, range: Range): number {
return Math.min(range.max, Math.max(range.min, value));
}
// src/index.ts
import clamp, { circleArea, PI } from "./math.js";
import type { Range } from "./math.js";
import * as math from "./math.js";
const range: Range = { min: 0, max: 10 };
console.log(clamp(15, range)); // 10
console.log(circleArea(2), PI);
console.log(math.circleArea(1));
몇 가지 눈여겨볼 점이 있습니다.
- 이름 있는 내보내기(
export function ...)는 중괄호로, 기본 내보내기(export default)는 중괄호 없이 가져옵니다. 이름이 고정되는 이름 있는 내보내기가 자동 완성과 이름 바꾸기에 유리해 더 많이 쓰입니다. import type은 타입만 가져온다는 표시입니다. 컴파일 결과에서 완전히 지워지므로 불필요한 실행 시점 의존을 만들지 않습니다.module이 nodenext일 때 상대 경로에는 확장자를 적어야 하며, 원본이 .ts여도 컴파일 결과 파일 이름인 .js로 적습니다. TypeScript가 이를 원본 .ts 파일과 연결해 줍니다.
npm 패키지와 package.json
package.json은 프로젝트의 이름, 의존성, 스크립트를 담는 파일입니다. 실행할 때 필요한 패키지는 dependencies, 개발할 때만 필요한 도구는 devDependencies에 넣습니다.
npm install zod # 실행에 필요한 패키지
npm install --save-dev vitest # 개발 도구
npm install # package.json 기준으로 모두 설치
npm outdated # 새 버전이 있는 패키지 확인
{
"name": "hello-ts",
"type": "module",
"main": "dist/index.js",
"scripts": {
"build": "tsc",
"test": "vitest"
}
}
"type": "module"은 이 패키지의 .js 파일을 ES 모듈로 다루라는 뜻입니다. npm install을 실행하면 dependencies와 devDependencies 항목이 자동으로 추가되며, 버전은 ^가 붙은 범위로 기록되어 호환되는 범위 안의 업데이트를 허용합니다. package-lock.json은 실제로 설치된 정확한 버전을 기록하므로 저장소에 함께 커밋합니다.
타입 선언과 @types
TypeScript는 .d.ts 선언 파일을 통해 JavaScript 라이브러리의 타입을 압니다. 요즘 많은 패키지는 자체 선언 파일을 포함하고 있어 설치만 하면 바로 타입이 보입니다. 선언이 없는 패키지는 커뮤니티가 관리하는 DefinitelyTyped 저장소의 @types/패키지이름을 따로 설치합니다.
npm install express
npm install --save-dev @types/express @types/node
@types/node는 fs, path, process 같은 Node.js 내장 모듈의 타입입니다. Node.js 내장 모듈은 node: 접두사로 가져오는 것이 명확합니다.
import { readFile } from "node:fs/promises";
import path from "node:path";
const file = path.join(process.cwd(), "package.json");
const pkg = JSON.parse(await readFile(file, "utf8")) as { name: string };
console.log(pkg.name);
최상위 await는 ES 모듈에서만 쓸 수 있으므로 module이 nodenext 같은 ES 모듈 출력 설정이어야 합니다. as는 타입 단언으로, 컴파일러에게 "이 값은 이 타입이다"라고 알려 줄 뿐 실제로 검사하지는 않습니다. 외부 입력은 검증 라이브러리로 확인하는 편이 안전합니다.
모듈 관련 tsconfig 옵션
Node.js 백엔드라면 module: nodenext, Vite 같은 번들러를 쓰는 프런트엔드라면 module: esnext와 moduleResolution: bundler를 쓰는 것이 일반적입니다.
빌드하기
tsc는 타입 검사와 JavaScript 출력을 함께 합니다. 라이브러리를 배포한다면 declaration을 켜서 .d.ts도 함께 내보내고, package.json에 진입점을 적습니다.
{
"name": "my-lib",
"type": "module",
"exports": {
".": {
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
}
},
"files": ["dist"]
}
애플리케이션에서는 esbuild, Vite 같은 번들러가 TypeScript를 빠르게 변환합니다. 이런 도구는 대부분 타입을 지우기만 하고 검사하지 않으므로, CI에서 tsc --noEmit을 반드시 함께 실행합니다.
정리
- 파일 하나가 모듈이며,
export로 내보내고 import로 가져옵니다. 타입만 가져올 때는 import type을 씁니다. nodenext에서는 상대 경로 import에 .js 확장자를 적습니다.- 실행 의존성은
dependencies, 개발 도구는 devDependencies에 넣고 잠금 파일도 커밋합니다. - 타입이 없는 라이브러리는
@types/패키지이름을 설치합니다. - 번들러는 타입을 검사하지 않으므로
tsc --noEmit을 별도로 실행합니다.
댓글 0개
로그인 · 로그인하면 댓글을 남길 수 있습니다.