안동민 개발노트

본문 시작

앰비언트 모듈 선언

declare module과 와일드카드 선언으로 타입 정보가 없는 외부 모듈·파일 자산·전역 스크립트의 형태를 컴파일러에 알립니다.

앞선 8장 1절과 2절에서 .d.ts 파일의 역할과 작성 방법을 살펴봤습니다.

그중 declare 키워드로 타입 정보만 선언하는 방식을 앰비언트 선언(Ambient Declarations)이라고 부릅니다.

그리고 앰비언트 선언 중에서 특정 모듈 타입을 정의하는 방식이 앰비언트 모듈 선언(Ambient Module Declarations)입니다.

앰비언트 모듈 선언은 주로 두 가지 경우에 사용됩니다.

타입스크립트 프로젝트에서 타입 정의가 없는 자바스크립트 모듈을 사용할 때.

웹팩(Webpack)이나 Vite 같은 번들러를 사용할 때 자바스크립트가 아닌 파일(예: 이미지, CSS 파일)을 모듈처럼 import할 때.

이 절에서는 앰비언트 모듈 선언의 구체적인 사용법과 중요성에 대해 더 깊이 알아보겠습니다.


declare module 'module-name' 구문

앰비언트 모듈 선언의 핵심은 declare module 'module-name' 구문입니다.

여기서 'module-name'은 bare 패키지 이름이나 와일드카드 패턴입니다. 아래 파일은 최상위 import/export가 없는 script 문맥이며, 모듈 파일 안의 declare module은 기존 모듈 확장으로 해석될 수 있습니다.

Example: some-legacy-library.d.ts
declare module 'some-legacy-library' {
  // 이 블록 안에 'some-legacy-library' 모듈의 타입을 정의합니다.

  export interface Options {
    debugMode: boolean;
    logLevel: 'info' | 'warn' | 'error';
  }

  export function initialize(options: Options): void;
  export function fetchData<T>(url: string): Promise<T>;
  export const version: string;

  // 기본(default) 내보내기가 있다면 이렇게 선언합니다.
  export default class SomeClient {
    constructor(apiKey: string);
    sendRequest(data: any): Promise<any>;
  }
}

위 선언을 프로젝트에 포함하면 호출의 정적 타입을 검사할 수 있습니다. 실제 패키지 구현은 별도로 필요하며 fetchData<T>의 타입 인자만으로 서버 응답 구조가 런타임 검증되지는 않습니다.

app.ts
import { initialize, fetchData, version } from 'some-legacy-library';
import SomeClient from 'some-legacy-library'; // default export 가져오기

initialize({ debugMode: true, logLevel: 'info' });
console.log(`Library Version: ${version}`);

async function loadData() {
  const data = await fetchData<{ name: string; value: number }>('/api/data');
  console.log(data.name, data.value);
}

const client = new SomeClient('my-api-key');
client.sendRequest({ type: 'report' });

// initialize({}); // Error: 'Options' 형식에 'debugMode' 및 'logLevel' 속성이 없습니다.

컴파일러는 import 'some-legacy-library' 구문을 만나면, node_modules에서 실제 some-legacy-library 모듈을 찾는 것 외에도, 해당 모듈 이름에 해당하는 .d.ts 파일을 찾아 타입 정보를 얻습니다.

@types는 타입 선언의 배포 경로입니다. 그 안에는 최상위 export를 갖는 모듈 선언, 전역 선언, 모듈 확장 등 여러 형태가 들어갈 수 있으므로 declare module 한 형태와 동일시하지 않습니다.


와일드카드 모듈 선언

특정 파일 확장자를 가진 모든 파일에 대해 공통된 타입을 선언하고 싶을 때 와일드카드 문자 (*) 를 사용할 수 있습니다.

이는 이미지, CSS, JSON 등 웹팩과 같은 번들러가 모듈처럼 처리하는 비-자바스크립트/타입스크립트 파일을 임포트할 때 특히 유용합니다.

types/custom-file-types.d.ts
// types/custom-file-types.d.ts 또는 src/declarations.d.ts
declare module '*.png' {
  const value: string; // 일반적으로 이미지 파일은 번들링 후 URL 문자열로 처리됩니다.
  export default value;
}

declare module '*.svg' {
  // SVG 파일을 React 컴포넌트로 가져오는 경우 (예: @svgr/webpack)
  import * as React from 'react';
  export const ReactComponent: React.FunctionComponent<React.SVGProps<SVGSVGElement> & { title?: string }>;
  const src: string;
  export default src;
}

declare module '*.module.css' {
  const content: { [className: string]: string }; // CSS Module을 사용하는 경우
  export default content;
}

declare module '*.json' {
  const value: any; // JSON 파일은 객체로 직접 로드될 수 있습니다.
  export default value;
}

이 선언은 아래 export 형식을 실제로 제공하는 loader 설정을 전제로 합니다. SVG의 URL과 named 컴포넌트, CSS Modules의 클래스 맵은 설정에 따라 다르며 일반 CSS와 같지 않습니다. JSON의 any 선언은 예시지만 구조 검사를 포기하므로 실제 JSON import에는 resolveJsonModule 등 구체적인 해석을 우선 검토합니다.

app.ts
import logo from './logo.png'; // logo의 타입은 string
import { ReactComponent as Icon } from './docs/common/fallback-image.svg'; // named component export 사용
import styles from './styles/main.module.css'; // CSS Modules의 클래스 이름 맵

console.log(logo); // 예: "/logo.png"
// Icon은 React 컴포넌트로 사용할 수 있습니다. URL은 별도의 default import로 가져옵니다.
console.log(styles.container); // 예: "main_container__abc123"

와일드카드 모듈 선언은 번들러 환경에서 파일 기반의 에셋을 모듈처럼 다룰 때 발생하는 타입 오류를 해결하는 표준적인 방법입니다.

ambient declaration 라우팅

DECLARATION SCOPE · FLOWCHART

ambient declaration 라우팅

bare package, wildcard asset, global script는 specifier 소유권과 declaration 파일의 script/module scope에 따라 서로 다른 선언 경로를 택합니다.

ambient declaration 라우팅 선언 대상이 bare package, wildcard asset, classic global script, module augmentation 중 무엇인지 질문해 올바른 ambient declaration scope로 라우팅합니다. ROUTING QUESTION누가 이름을 소유하는가?specifier · asset · globalPACKAGEbare modulepackage-owned exportsASSETwildcard moduleplugin export shapeSCRIPTglobal declarationclassic script scopeMODULE FILEdeclare globalexport {} + augmentation패키지파일 확장자전역 스크립트모듈 확장AMBIENT TYPES MIRROR AN EXISTING LOADER OR SCRIPT CONTRACT
  1. bare package

    package가 소유한 export 표면 또는 정확한 ambient external module로 선언합니다.

  2. wildcard asset

    실제 plugin이 내보내는 URL·component·class map 중 하나와 맞춥니다.

  3. 전역 script

    script scope의 top-level declaration으로 기존 전역 값을 설명합니다.

  4. module 확장

    module 파일에서는 export {}와 declare global 또는 진짜 augmentation을 사용합니다.

@types, ambient module, global, augmentation을 같은 개념으로 묶지 않고 선언 파일의 scope를 먼저 판별합니다.


전역 모듈과 스크립트 파일의 앰비언트 선언

간혹 모듈 시스템을 사용하지 않고 전역 스코프에 변수나 함수를 노출하는 자바스크립트 파일(스크립트 파일)의 타입을 선언해야 할 때가 있습니다.

이 경우 declare module 대신 declare namespace 또는 단순히 declare var/function/class를 사용합니다.

전역 스크립트 타입 선언 (global-script-types.d.ts)
global-script-types.d.ts
// 이 파일은 HTML <script> 태그로 로드되는 자바스크립트 파일에 대한 타입 정의입니다.

// 전역 변수 선언
declare var MY_APP: {
  version: string;
  debugMode: boolean;
};

// 전역 함수 선언
declare function initializeApp(config: { theme: string }): void;

// 전역 객체 확장 (예: window 객체에 추가되는 속성)
interface Window {
  _paq?: Array<any>; // Matomo (Piwik) 같은 분석 스크립트가 추가하는 전역 변수
}

// 네임스페이스를 사용하여 관련된 전역 객체를 그룹화
declare namespace MyGlobalUtils {
  function formatCurrency(amount: number): string;
  class DataLoader {
    load(url: string): Promise<any>;
  }
}

이 파일은 최상위 import/export가 없는 script 문맥이므로 포함된 선언이 전역에 추가됩니다. 모듈 파일에서는 declare global로 전역 확장을 명시해야 하며, 어느 경우도 실제 전역 값을 생성하지는 않습니다.

아래 그림은 자산 import의 선언과 실제 loader가 동일한 export 형식을 제공해야 한다는 별도 경계를 보여줍니다.

asset import의 타입·로더 이중 계약

ASSET LOADER · DATA FLOW

asset import의 타입·로더 이중 계약

하나의 asset import는 TypeScript wildcard 선언과 실제 bundler·loader가 같은 export shape를 제공해야 소비자가 안전하게 사용할 수 있습니다.

asset import의 타입·로더 이중 계약 asset import가 TypeScript declaration과 configured bundler loader의 두 경로로 나뉘어 동일 export shape gate에 수렴한 뒤 consumer와 runtime smoke test로 이어집니다. SOURCEasset import./logo.asset-url.svgTYPE WORLDwildcard 선언default: stringRUNTIME WORLDbundler / loaderdefault URL exportPARITY GATE · 2 IN같은 export shapeURL · component 혼합 금지VERIFYconsumer smokebuild + runtime타입 해석실제 로드검증THE DECLARATION AND THE CONFIGURED LOADER MUST EXPORT THE SAME SHAPE
  1. asset import

    한 파일 specifier가 두 계약의 공통 입력입니다.

  2. TypeScript 선언

    wildcard declaration이 default URL 같은 정적 모양을 설명합니다.

  3. 실제 loader

    설정된 plugin이 런타임에 동일한 export를 생성합니다.

  4. shape 일치

    URL string과 React component 등 서로 다른 계약을 섞지 않습니다.

  5. smoke test

    실제 build와 import 사용으로 drift를 확인합니다.

JSON·global CSS·CSS Modules·SVG plugin은 각기 다른 loader 계약이므로 만능 any 선언으로 합치지 않습니다.

앰비언트 선언은 실제 런타임 모듈의 표면을 타입으로 옮기는 작업입니다.


앰비언트 모듈 선언의 중요성

  • 타입 안전성 확보: 자바스크립트 라이브러리나 외부 리소스에 대한 타입 정보를 제공하여, 타입스크립트 컴파일러가 잠재적인 오류를 잡아내고 코드 자동 완성을 제공할 수 있도록 합니다.
  • 개발 생산성 향상: IDE의 자동 완성, 매개변수 힌트, 타입 오류 검출 등의 기능을 활용하여 개발자가 더 빠르고 정확하게 코드를 작성할 수 있게 합니다.
  • 유지보수성 증대: 타입 정보를 통해 코드의 의도와 기대되는 데이터 형태를 명확히 하여, 다른 개발자가 코드를 이해하고 변경하기 쉽게 만듭니다.
  • 레거시 코드 통합: 기존 자바스크립트 코드베이스를 타입스크립트로 점진적으로 전환할 때, 핵심적인 역할을 합니다.

앰비언트 모듈 선언은 타입스크립트가 방대한 자바스크립트 생태계와 조화롭게 공존할 수 있게 하는 핵심 메커니즘입니다.

declare module 'module-name'과 와일드카드 선언(*.ext)을 통해 외부 모듈의 타입을 명확히 정의함으로써, 타입스크립트의 강력한 타입 검사 기능을 자바스크립트 프로젝트 전반에 걸쳐 확장할 수 있습니다.