TypeScript를 쓰다 보면, 타입이 없는 라이브러리를 만나게 되는 순간이 있습니다. 내부에서 만든 유틸 스크립트일 수도 있고, 외부에서 받아온 오래된 패키지일 수도 있습니다.
처음에는 any로 처리하고 넘어갑니다. 일단 동작은 하니까요. 하지만 이 코드가 여러 파일로 퍼지기 시작하면, 타입 안정성은 금방 무너집니다.
이때 등장하는 게 Ambient Declaration, 즉 d.ts 파일입니다. 런타임 코드를 바꾸지 않고, 타입 시스템에만 정보를 추가하는 방식입니다.
Ambient Declaration이 필요한 상황
대표적인 경우는 다음과 같습니다.
- 타입 정의가 없는 JS 라이브러리를 사용할 때
- 전역 객체(window, global)에 속성을 추가했을 때
- 빌드 타임에만 존재하는 환경 변수를 타입으로 선언해야 할 때
이 상황에서는 구현 코드가 아니라, “타입 정보만” 제공해야 합니다.
가장 단순한 모듈 선언
타입이 없는 라이브러리를 import할 때, 컴파일 에러가 먼저 발생합니다.
Cannot find module 'legacy-lib'
이 경우 프로젝트에 d.ts 파일을 하나 추가합니다.
// types/legacy-lib.d.ts
declare module 'legacy-lib' {
export function doSomething(input: string): number;
}
이 파일은 JS로 컴파일되지 않습니다. 오직 타입 시스템에만 영향을 줍니다.
전역 타입 확장
Express 같은 프레임워크를 쓰다 보면, request 객체에 값을 추가하는 경우가 있습니다. 런타임에서는 문제없지만, 타입은 모르는 상태입니다.
// types/express.d.ts
import 'express';
declare module 'express-serve-static-core' {
interface Request {
user?: {
id: string;
role: string;
};
}
}
이 코드는 런타임 코드를 건드리지 않습니다. 하지만 프로젝트 전반의 Request 타입이 확장됩니다.
이 방식은 강력하지만, 어디서 타입이 확장됐는지 추적하기 어렵다는 특성이 있습니다.
환경 변수 타입 정의
Node 환경에서는 process.env가 기본적으로 string | undefined입니다. 실제로는 특정 키만 쓰고 있고, 값도 제한되어 있습니다.
// types/env.d.ts
declare namespace NodeJS {
interface ProcessEnv {
NODE_ENV: 'development' | 'production';
DB_HOST: string;
}
}
이 선언 이후에는 process.env.NODE_ENV가 리터럴 유니온으로 추론됩니다. 런타임에는 아무 변화가 없지만, 컴파일 단계에서 실수를 줄일 수 있습니다.
declare global을 사용할 때
브라우저 전역 객체를 확장하는 경우도 있습니다.
// types/global.d.ts
export {};
declare global {
interface Window {
__APP_VERSION__: string;
}
}
export {}를 추가하는 이유는 이 파일을 모듈로 취급하기 위해서입니다. 그렇지 않으면 전역 오염이 의도치 않게 확장될 수 있습니다.
d.ts 파일이 남기는 영향
Ambient Declaration은 구현과 분리되어 있기 때문에, 코드를 읽는 사람은 “이 타입이 실제로 어디서 오는지” 바로 알기 어렵습니다.
특히 다음 상황에서는 주의가 필요합니다.
- 여러 d.ts 파일에서 동일한 타입을 확장하는 경우
- 런타임과 타입 정의가 어긋나는 경우
- 타입 정의만 바꾸고 구현은 수정하지 않은 경우
Ambient Declaration은 타입 시스템을 설계하는 작업에 가깝습니다. JS 코드보다 더 오래 남는 경우도 많습니다.
운영 코드에서 겪는 차이
d.ts를 잘 정리한 프로젝트에서는, 외부 라이브러리 사용이 비교적 안정적입니다. 타입 추론이 자연스럽고, IDE 지원도 잘 따라옵니다.
반대로 아무 기준 없이 선언을 추가하다 보면, 전역 타입이 예측 불가능하게 확장됩니다. 이 경우 문제를 찾는 데 시간이 더 걸립니다.
Ambient Declaration은 런타임을 건드리지 않고 타입 시스템을 보완하는 방법입니다. 필요한 순간에는 가장 현실적인 해결책이 됩니다.
다만 이 파일은 눈에 잘 띄지 않으면서 프로젝트 전체에 영향을 줍니다. 구현 코드만큼이나 어디에, 어떤 의도로 선언했는지가 중요해집니다.
d.ts는 단순한 타입 보조 파일이 아니라, 타입 구조의 일부로 남습니다. 그 점을 인지하고 작성하는 편이 나중에 되돌아보기 수월합니다.
'개발 > Typescript' 카테고리의 다른 글
| [TYPESCRIPT] 모듈 보강을 쓰게 되는 순간과 타입 경계가 흔들리는 지점들 (0) | 2026.02.21 |
|---|---|
| [TYPESCRIPT] DefinitelyTyped와 @types를 쓰면서 겪게 되는 현실적인 지점들 (0) | 2026.02.20 |
| [TYPESCRIPT] 선언 병합을 쓰게 되는 순간과 그 이후에 남는 것들 (0) | 2026.02.18 |
| [TYPESCRIPT] Extract와 ReturnType을 쓰다 보면 생기는 활용과 한계 (0) | 2026.02.17 |
| [TYPESCRIPT] as const를 쓰기 시작하면 타입이 고정되는 지점들이 보이기 시작한다 (0) | 2026.02.16 |
