본문 바로가기

앰비언트 모듈 선언

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는 타입 선언의 배포 경로입니다.

반드시 실행되는 코드와 선언만 있는 코드를 구별하기

위 some-legacy-library는 실제 패키지가 존재하고 표시된 export를 제공한다고 가정한 설명용 이름입니다. .d.ts만 저장해서 패키지를 설치한 효과를 얻는 실습은 아닙니다. loadData는 함수 정의만 있고 호출이 없으므로 위 코드 자체가 /api/data 요청을 실행한다는 뜻도 아닙니다. fetchData<T>의 결과를 믿기 전에 실제 응답이 그 구조인지 검증해야 합니다.

외부 라이브러리 없이 선언의 문맥만 확인하려면 독립 폴더에 다음 두 파일을 만듭니다. 이 확인은 타입 검사만 수행하며 네트워크·번들러를 실행하지 않습니다.

types/lesson-source.d.ts
declare module 'lesson-source' {
  export function readCount(): number;
}
app.ts
import { readCount } from 'lesson-source';
const count: number = readCount();
// const label: string = readCount(); // 반환 타입과 맞지 않음
tsconfig.json
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "strict": true,
    "noEmit": true,
    "types": []
  },
  "files": ["types/lesson-source.d.ts", "app.ts"]
}

package.json은 { "type": "commonjs" }로 두고 tsc -p .를 실행합니다. 정상 코드에는 타입 진단이 없고, 주석을 해제한 문자열 대입에는 진단이 납니다. 모듈의 타입 설명이 포함됐다는 결과이며 실제 lesson-source 구현이 존재하거나 readCount()가 실행됐다는 증거는 아닙니다.

선언 파일의 위치를 바꾸기

lesson-source.d.ts의 맨 위에 export {};를 추가하면 왜 원래와 다른 뜻이 될까요? 같은 확장자로 가져온 SVG를 선언에서는 named ReactComponent로 읽는데 로더는 default URL만 제공한다면, 타입 설명과 실행 구성 중 어디를 확인해야 할지도 적어 보세요.

선언 문맥과 실행 export 확인

최상위 export가 있는 파일은 모듈 문맥이고, 그 안의 declare module은 기존 모듈 확장으로 해석됩니다. 이 실습에는 확장할 실제 모듈이 없으므로 원래의 새 ambient 모듈 설명과 같지 않습니다. SVG의 경우 선언을 추가하는 것만으로 named 컴포넌트가 생성되지 않습니다. 실제로 URL만 쓰려면 default URL 계약에 맞추거나, named 컴포넌트가 필요하면 그 export를 제공하는 로더 구성을 준비해야 합니다.

문맥의 차이는 공식 ambient module 설명에서 대조할 수 있습니다.

@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"

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

실제 bare 모듈·와일드카드 자산·전역 스크립트와 module 파일의 확장을 구분합니다.

TYPE / RUNTIME / CONTRACT

외부 모듈·자산·전역의 선언 문맥을 구분한다

선언할 대상과 파일의 script/module 문맥을 먼저 확인합니다. 같은 declare 문법이라도 새 외부 모듈 설명과 기존 모듈 확장, 전역 설명은 역할이 다릅니다.

외부 모듈·자산·전역의 선언 문맥을 구분한다이 절의 d.ts 예제에서 최상위 import/export가 없는 script와 있는 module 문맥을 구분합니다. script 아래의 모듈·자산 설명과 전역 설명은 대상에 따른 선택이며 실행 순서가 아닙니다. 확장은 기존 모듈을 전제로 합니다.없음있음모듈·자산전역최상위import/export?script 문맥새 선언 설명모듈·자산 설명declare module전역 값 설명namespace · varmodule 문맥기존 선언 확장모듈 확장augmentation
외부 모듈·자산·전역의 선언 문맥을 구분한다이 절의 d.ts 예제에서 최상위 import/export가 없는 script와 있는 module 문맥을 구분합니다. script 아래의 모듈·자산 설명과 전역 설명은 대상에 따른 선택이며 실행 순서가 아닙니다. 확장은 기존 모듈을 전제로 합니다.없음있음모듈·자산전역최상위import/export?script 문맥새 선언 설명모듈·자산 설명declare module전역 값 설명namespace · varmodule 문맥기존 선언 확장모듈 확장augmentation

이 절의 d.ts 예제에서 최상위 import/export가 없는 script와 있는 module 문맥을 구분합니다. script 아래의 모듈·자산 설명과 전역 설명은 대상에 따른 선택이며 실행 순서가 아닙니다. 확장은 기존 모듈을 전제로 합니다.

BARE MODULE

some-legacy-library의 API를 선언한다

최상위 import/export가 없는 선언 파일에서 Options, initialize, fetchData<T>, version, default SomeClient를 설명합니다. initialize는 debugMode와 logLevel을 요구합니다.

FILE CONTEXT

모듈 파일이면 기존 모듈 확장이다

최상위 import/export가 있으면 declare module은 기존 모듈 augmentation이 됩니다. 새 ambient 모듈 선언을 모듈 파일로 바꾸면 의미가 달라집니다.

WILDCARD

로더가 제공하는 export를 설명한다

*.png·*.svg·*.module.css 패턴은 자산 import의 타입입니다. SVG 선언 블록 내부의 React import는 바깥 선언 파일을 모듈로 바꾸지 않습니다.

GLOBAL SCRIPT

기존 전역 이름과 객체를 설명한다

MY_APP, initializeApp, Window._paq, MyGlobalUtils는 script 문맥의 전역 선언입니다. 모듈 파일에서 전역을 확장하려면 declare global을 사용합니다.

PROGRAM

선언 파일이 프로그램에 포함돼야 한다

include/files 등으로 선언을 타입 검사에 포함합니다. fetchData<{name:string;value:number}>는 응답에 타입을 부여하지만 실제 응답 구조를 검증하지 않습니다.

@TYPES

배포 경로와 선언 문법을 구분한다

@types 패키지는 모듈 선언·전역 선언·확장 등 여러 형태의 타입을 배포합니다. @types를 declare module 한 문법과 동일한 개념으로 읽지 않습니다.

어느 선언 문맥도 라이브러리·로더·전역 값의 실제 구현을 생성하지 않습니다.


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

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

이 경우 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 형식을 제공해야 한다는 별도 경계를 보여줍니다.

PNG URL, SVG의 default URL 및 named 컴포넌트, CSS Modules 맵과 JSON any의 서로 다른 계약을 설명합니다.

TYPE / RUNTIME / CONTRACT

자산 선언의 export 형식을 실제 로더와 맞춘다

본문은 PNG URL, SVG URL과 named ReactComponent, CSS Modules 클래스 맵을 선언합니다. 같은 import가 실행에서도 그 형식을 제공하도록 로더 설정을 맞춰야 합니다.

자산 선언의 export 형식을 실제 로더와 맞춘다하나의 import에는 정적 선언 해석과 실제 로더 처리가 각각 있습니다. 양쪽의 export 이름과 값 형식을 대조해야 합니다. 타입 경로를 통과했다는 사실만으로 로더가 named 컴포넌트를 만든다고 판단하지 않습니다.같은 자산 importPNG · SVG · CSS타입 해석선언 파일 포함선언한 exportURL · 컴포넌트·맵실제 로더파일 처리제공한 exportURL · 컴포넌트·맵두 계약이 일치?실제 사용으로 확인타입 경로실행 경로
자산 선언의 export 형식을 실제 로더와 맞춘다하나의 import에는 정적 선언 해석과 실제 로더 처리가 각각 있습니다. 양쪽의 export 이름과 값 형식을 대조해야 합니다. 타입 경로를 통과했다는 사실만으로 로더가 named 컴포넌트를 만든다고 판단하지 않습니다.같은 자산 importPNG · SVG · CSS타입 해석선언 파일 포함선언한 exportURL · 컴포넌트·맵실제 로더파일 처리제공한 exportURL · 컴포넌트·맵두 계약이 일치?실제 사용으로 확인타입 경로실행 경로대조

하나의 import에는 정적 선언 해석과 실제 로더 처리가 각각 있습니다. 양쪽의 export 이름과 값 형식을 대조해야 합니다. 타입 경로를 통과했다는 사실만으로 로더가 named 컴포넌트를 만든다고 판단하지 않습니다.

PNG

default URL 문자열

logo.png의 default import logo는 string입니다. 실제 빌드가 해당 URL을 제공해야 하며 선언만으로 이미지 처리나 파일 출력이 수행되지는 않습니다.

SVG

URL과 컴포넌트의 이름을 구분한다

본문 *.svg는 default src:string과 named ReactComponent를 함께 선언합니다. Icon은 named 컴포넌트 별칭입니다. 이런 두 export를 실제로 제공하는 설정이 필요하며 SVGR의 기본 단독 설정과 같다고 가정하지 않습니다.

CSS MODULES

클래스 이름을 문자열로 매핑한다

*.module.css의 default styles는 className→string 맵입니다. styles.container는 변환된 클래스 이름을 가리킵니다. 일반 CSS의 side-effect import와 이 맵 계약은 다릅니다.

JSON

any 선언은 구조 검사를 포기한다

본문 *.json의 any는 어떤 사용도 허용할 수 있습니다. 실제 JSON import에는 resolveJsonModule 등 구체적인 해석을 검토하고 실행 환경의 JSON 로드 방식도 맞춥니다.

타입 오류가 없어도 로더의 실제 export가 다르면 사용이 실패할 수 있습니다. 선택한 로더로 빌드하고 URL·컴포넌트·클래스 맵을 실제로 사용하는 지점을 확인합니다.

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


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

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

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

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