[TYPESCRIPT] Ambient Declaration을 직접 작성해야 하는 순간과 d.ts 파일이 남기는 영향

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는 단순한 타입 보조 파일이 아니라, 타입 구조의 일부로 남습니다. 그 점을 인지하고 작성하는 편이 나중에 되돌아보기 수월합니다.