콘텐츠 대표 이미지 - 타입스크립트, JS 라이브러리랑 '썸' 탈 때 필수템!d.ts 선언 파일 완전 정복 가이드

타입스크립트, JS 라이브러리랑 '썸' 탈 때 필수템!
d.ts 선언 파일 완전 정복 가이드

자바스크립트 생태계와 타입스크립트의 완벽한 콜라보를 위한 비밀 병기, .d.ts 파일을 파헤쳐보자. 이제 '타입 정의를 찾을 수 없습니다' 에러는 안녕!

들어가며: 그놈의 'Could not find a declaration file' 에러!

안녕! 타입스크립트로 코딩 좀 해본 친구들이라면 아마 이 경험, 다들 한 번쯤은 있을 거야.
새로운 프로젝트에 투입돼서, 혹은 사이드 프로젝트를 하다가 '오, 이거 완전 쩌는 자바스크립트 라이브러리인데?' 싶어서 냉큼 `npm install some-awesome-js-lib`를 날렸지.

기분 좋게 `import` 구문까지 딱 썼는데... 이게 웬걸?
우리의 친절한 VS Code가 빨간 밑줄을 쫙 그어주면서 싸늘한 메시지를 날리는 거야.


// 🚨 TypeScript 컴파일러의 경고 🚨
Could not find a declaration file for module 'some-awesome-js-lib'.
'c:/.../node_modules/some-awesome-js-lib/index.js' implicitly has an 'any' type.

Try `npm i --save-dev @types/some-awesome-js-lib` if it exists or add a new declaration (.d.ts) file containing `declare module 'some-awesome-js-lib';`

아... 이 순간, 타입스크립트의 달콤함에 취해있던 우리는 잠시 현실을 마주하게 돼.
맞다, 자바스크립트는 원래 타입이 없었지.

타입스크립트(이하 TS)는 코드의 안정성과 예측 가능성을 위해 모든 변수와 함수, 객체의 '타입'을 알아야만 해. 그래야 우리 대신 오타를 잡아주고, 어떤 함수에 어떤 값을 넣어야 하는지 똑똑하게 알려줄 수 있으니까. 그런데 타입 정보가 싹 빠진 순수한 자바스크립트(이하 JS) 코드를 만나면, TS 컴파일러는 그야말로 '멘붕'에 빠지는 거야. '이 라이브러리, 정체가 뭐야? 어떻게 써야 해?' 하고 우리에게 되묻는 거지.

바로 이 문제, 즉 **타입이 없는 JS 세상과 타입이 필수인 TS 세상 사이의 간극을 메워주는 다리** 역할을 하는 것이 바로 오늘 우리가 깊게 파헤쳐 볼 **선언 파일(`.d.ts`)**이야.

이 글을 끝까지 읽고 나면, 넌 더 이상 저 빨간 줄 앞에서 당황하지 않고, 오히려 "아, 타입 정의가 없다고? 내가 만들어주지!"라며 자신감 있게 키보드를 두드릴 수 있게 될 거야. 심지어는 네가 만든 타입 정의 파일을 공유해서 다른 개발자들에게 도움을 주는 멋진 경험을 할 수도 있고 말이야. 마치 재능 공유 플랫폼 **재능넷**에서 자신의 스킬을 나누는 전문가처럼!

자, 그럼 TS와 JS의 성공적인 '썸'을 위한 필수 아이템, `.d.ts`의 세계로 함께 떠나볼까?

그래서 .d.ts가 정확히 뭔데? (feat. 족보 or 사용 설명서)

좋아, `.d.ts` 파일이 '다리' 역할을 한다는 건 알겠어. 근데 구체적으로 그게 뭘까?
`.d.ts`에서 'd'는 **Declaration(선언)**의 약자야. 말 그대로 '선언'만 담겨있는 파일이라는 뜻이지.

여기서 핵심은 **'선언'만** 있다는 점이야.
실제 기능이 구현된 코드는 단 한 줄도 없어. 함수의 몸통 `{...}` 이나 변수의 초기값 `= 'hello'` 같은 건 찾아볼 수 없다는 거지.

대신 이 파일에는 오직 **'타입 정보'**만 담겨 있어.
마치 이런 느낌이랄까?

  • "이 라이브러리에는 `createUser`라는 함수가 있는데, `name`(문자열)과 `age`(숫자)를 받고, `User` 객체를 반환해."
  • "여기 `User` 객체는 `id`(숫자), `name`(문자열), `isActive`(불리언)라는 속성을 가지고 있어."
  • "전역적으로 `MY_API_KEY`라는 상수가 있는데, 이건 무조건 문자열이야."

이런 식으로 JS 코드의 '형태'나 '구조', 즉 **'타입 시그니처(Type Signature)'**를 설명해주는 거야.
TS 컴파일러는 이 `.d.ts` 파일을 읽고 "아하! `some-awesome-js-lib`는 이렇게 생겨먹었구나. 이제부터 내가 개발자의 코드를 검사할 때 이 정보를 기준으로 삼아야겠다!"라고 똑똑하게 행동하기 시작해.

백문이 불여일견! 코드로 비교해보면 느낌이 확 올 거야.

JS vs TS vs .d.ts 비교

1. 순수 자바스크립트 파일 (`utils.js`) - 실제 로직이 담겨있음


function calculateTax(price) {
  return price * 0.1;
}

const PI = 3.14;

module.exports = {
  calculateTax,
  PI,
};

2. 타입스크립트 파일 (`utils.ts`) - 타입과 로직이 함께 있음


export function calculateTax(price: number): number {
  return price * 0.1;
}

export const PI: number = 3.14;

3. 선언 파일 (`utils.d.ts`) - 오직 타입 정보만! 구현은 없음


export declare function calculateTax(price: number): number;

export declare const PI: number;

차이가 느껴져? `.d.ts` 파일에는 `{}`나 `= 3.14` 같은 실제 '값'이나 '구현'이 전혀 없어.
대신 `declare`라는 키워드가 붙어서 "이런 게 어딘가에 **선언되어 있으니**, 너(TS 컴파일러)는 그냥 그렇다고 믿고 타입 체크에 사용해!"라고 알려주는 거지.

이런 선언을 **'앰비언트 선언(Ambient Declaration)'**이라고도 불러. '주변 어딘가에 존재한다'는 뉘앙스야. 그 '어딘가'는 바로 실제 코드가 담긴 `.js` 파일이거나, 브라우저 환경 자체(e.g., `window`, `document`)일 수도 있어.

JS 라이브러리 (some-lib.js) 구현 코드 (블랙박스) 선언 파일 (some-lib.d.ts) 타입 정보 (설명서) 나의 TS 코드 (my-app.ts) 타입을 알고 사용 의 타입을 설명해줌 를 읽고 참조함 .d.ts 파일의 역할

결론적으로 `.d.ts` 파일은 JS로 만들어진 결과물에 대한 **'타입 사용 설명서'** 또는 **'족보'**라고 생각하면 완벽해. 이 설명서 덕분에 우리는 TS 환경에서도 JS 라이브러리의 기능을 100% 활용하면서, 동시에 타입 체크의 안정성까지 누릴 수 있게 되는 거지.

일단 찾아보자! DefinitelyTyped, 우리들의 희망

자, 이제 `.d.ts` 파일의 정체를 알았으니, 우리가 마주했던 '선언 파일을 찾을 수 없다'는 에러로 다시 돌아가 보자.
TS 컴파일러가 친절하게 해결책도 제시해줬었지?


Try `npm i --save-dev @types/some-awesome-js-lib` if it exists...

이게 바로 첫 번째이자 가장 쉬운 해결책이야.
"혹시 `@types/some-awesome-js-lib`라는 패키지가 있는지 찾아보고, 있으면 설치해봐!" 라는 뜻이지.

여기서 등장하는 `@types`가 바로 오늘의 첫 번째 영웅, **DefinitelyTyped** 프로젝트야.

💡 DefinitelyTyped란?

전 세계의 TS 개발자들이 힘을 합쳐 만든, **JS 라이브러리들의 `.d.ts` 파일을 모아놓은 거대한 저장소(repository)**야. React, Lodash, jQuery, Express 등 우리가 아는 거의 모든 유명 JS 라이브러리의 타입 선언 파일이 여기에 있다고 봐도 무방해. Microsoft가 직접 관리하며, 수많은 기여자들 덕분에 계속해서 업데이트되고 있지.

우리가 `npm`에서 `@types/` 접두사가 붙은 패키지를 설치하면, 바로 이 DefinitelyTyped 저장소에 있는 타입 선언 파일을 내려받는 거야.

사용법은 정말 간단해. 예를 들어, 유명한 유틸리티 라이브러리인 `lodash`를 JS 프로젝트에서 쓴다고 가정해보자. TS에서 `lodash`를 쓰려면 타입이 필요하겠지? 그럼 터미널에 이렇게 입력하는 거야.


# 1. lodash 라이브러리 원본 설치 (런타임에 필요)
npm install lodash

# 2. lodash의 타입 선언 파일 설치 (개발 시에만 필요)
npm install --save-dev @types/lodash

여기서 중요한 포인트!
타입 선언 파일은 실제 프로그램이 동작할 때는 필요 없어. 오직 개발 과정에서 TS 컴파일러가 타입 체크를 할 때만 필요하지. 그래서 일반 의존성(`dependencies`)이 아니라, 개발용 의존성(`devDependencies`)으로 설치하기 위해 `--save-dev` (또는 `-D`) 옵션을 붙여주는 거야. 이건 거의 국룰이니까 꼭 기억해둬!

이렇게 설치만 하면 마법 같은 일이 벌어져.
별도의 설정 없이도, TS 컴파일러는 자동으로 `node_modules/@types/` 폴더를 스캔해서 `lodash`의 타입 정보를 찾아내. 그리고 우리는 마치 `lodash`가 처음부터 TS로 만들어진 라이브러리인 것처럼 자동 완성 기능과 타입 체크의 혜택을 누릴 수 있게 돼.


import _ from 'lodash';

// _.chunk를 입력하면 자동으로 (array, size) 인자를 보여줌
const chunkedArray = _.chunk(['a', 'b', 'c', 'd'], 2); 
// => [['a', 'b'], ['c', 'd']]

// 만약 두 번째 인자에 문자열을 넣으면?
const wrong = _.chunk(['a', 'b', 'c', 'd'], 'two');
//                                            ^
// 🚨 에러! 'string' 형식의 인수는 'number' 형식의 매개 변수에 할당될 수 없습니다.

이게 바로 `.d.ts` 파일의 위력이야! 우리는 `lodash`의 소스 코드를 한 줄도 보지 않았지만, `@types/lodash` 덕분에 `chunk` 함수의 사용법을 정확히 알 수 있었고, 실수도 방지할 수 있었지.

요즘에는 많은 라이브러리들이 아예 **자체적으로 `.d.ts` 파일을 포함해서 배포**하기도 해. 이런 라이브러리들은 `@types` 패키지를 따로 설치할 필요도 없어. 그냥 라이브러리만 설치하면 끝! 라이브러리의 `package.json` 파일에 `"types": "./dist/index.d.ts"` 같은 필드가 있는지 확인해보면 알 수 있어. 이게 가장 이상적인 베스트 케이스지.

하지만... 세상이 언제나 우리 뜻대로 되진 않잖아?
만약 `@types`에도 없고, 라이브러리 자체에도 타입 파일이 없다면?
그때가 바로 우리가 직접 칼을 뽑아 들 차례야.

없으면 만든다! 내 손으로 만드는 커스텀 .d.ts 파일

자, 이제부터가 진짜야. "남이 만들어준 거 쓰는 건 쉽지. 내가 직접 만드는 게 진짜 실력이지!"라고 생각하는 너를 위한 챕터. 지금부터 우리는 황무지에서 직접 샘을 파는 개척자가 될 거야.

커스텀 `.d.ts` 파일을 만드는 과정은 크게 두 단계로 나눌 수 있어.

  1. **1단계: 파일 생성 및 TS 설정** - "TS야, 내가 만든 타입 파일 좀 읽어줘!"
  2. **2단계: 타입 선언 작성** - "이 라이브러리는 이렇게 생겼어!"

4.1. 파일 생성 및 TS 설정 (feat. tsconfig.json)

먼저, 우리 프로젝트에 타입 선언 파일을 둘 공간을 마련해야 해. 보통 프로젝트 루트에 `types`나 `declarations` 같은 이름의 폴더를 만드는 걸 추천해. 깔끔하잖아.


my-project/
├── node_modules/
├── src/
│   └── index.ts
├── types/  <-- 요기!
│   └── some-awesome-js-lib.d.ts  <-- 여기에 만들자
├── package.json
└── tsconfig.json

자, 이제 `types/some-awesome-js-lib.d.ts` 파일을 만들었어. 근데 이걸로 끝이 아니야.
TS 컴파일러는 기본적으로 `node_modules/@types`만 쳐다보기 때문에, 우리가 만든 `types` 폴더의 존재를 알려줘야 해. 이 역할을 하는 게 바로 `tsconfig.json` 파일이야.

`tsconfig.json` 파일의 `compilerOptions` 안에 `typeRoots`라는 옵션을 추가해주면 돼.


// tsconfig.json
{
  "compilerOptions": {
    "target": "es6",
    "module": "commonjs",
    "strict": true,
    // ... 기타 옵션들
    
    "typeRoots": [
      "./node_modules/@types", // 기존 @types 폴더도 계속 사용하고
      "./types"                // 우리가 만든 types 폴더도 추가로 사용해!
    ]
  },
  "include": ["src", "types"] // types 폴더도 컴파일 대상에 포함시켜주면 더 확실해
}

`typeRoots`는 TS가 타입 정의를 찾을 폴더 목록이야. 기존의 `@types` 경로와 함께 우리 커스텀 폴더 경로를 배열에 추가해주는 거지. 순서는 상관없어. 이렇게 설정하면 이제 TS는 `types` 폴더 안에 있는 `.d.ts` 파일들도 열심히 읽어 들일 거야.

이제 모든 준비는 끝났어. `some-awesome-js-lib.d.ts` 파일을 열고 본격적으로 타입을 선언해보자.

4.2. 타입 선언의 기초: 변수, 함수, 객체, 클래스

가장 먼저 배울 건 특정 라이브러리가 아니라, 전역(global) 공간에 존재하는 변수나 함수를 타이핑하는 방법이야. 예를 들어 레거시 프로젝트에서 `

핵심은 `declare` 키워드야. "이건 어딘가에 이미 구현되어 있으니, 타입만 알아둬!" 라는 신호지.

기본적인 전역 타입 선언

// 변수 (상수) 선언


// JS 환경에 `const MY_GLOBAL_CONFIG = { version: '1.0.0' };` 가 있다고 가정
declare const MY_GLOBAL_CONFIG: {
  version: string;
  apiKey?: string; // ?는 optional(있을 수도 있고 없을 수도 있음)을 의미
};

// 함수 선언


// JS 환경에 `function showMessage(msg, duration) { ... }` 가 있다고 가정
declare function showMessage(message: string, durationInMs?: number): void;
// void는 함수가 아무것도 반환하지 않음을 의미

// 객체(인터페이스) 선언


// API 응답으로 오는 User 데이터의 형태를 정의
declare interface ApiUser {
  id: number;
  email: string;
  profile: {
    nickname: string;
    avatarUrl: string;
  };
}

// 클래스 선언


// JS 환경에 `class ImageSlider { ... }` 가 있다고 가정
declare class ImageSlider {
  constructor(elementId: string, options: SliderOptions);

  next(): void;
  prev(): void;
  getCurrentIndex(): number;

  readonly version: string; // readonly는 읽기 전용 속성
}

// 클래스 생성자에 들어갈 옵션 객체의 타입도 정의해주면 완벽!
declare interface SliderOptions {
  autoplay: boolean;
  speed: number;
  loop?: boolean;
}

어때? `declare`를 붙인다는 것만 빼면 우리가 평소에 TS에서 타입 정의하던 것과 거의 똑같지?
이렇게 선언해두면, 이제 우리 TS 코드 어디서든 `MY_GLOBAL_CONFIG`나 `showMessage` 같은 것들을 타입 에러 없이, 심지어 자동 완성의 도움을 받으며 사용할 수 있게 돼.

4.3. 모듈 선언: `declare module '모듈이름'`

자, 이제 진짜 실전이야. 우리가 `npm`으로 설치한 JS 라이브러리의 타입을 정의하는 방법.
요즘 대부분의 JS 라이브러리는 '모듈(Module)' 시스템을 기반으로 동작해. `import`와 `export`를 사용하는 바로 그거 말이야. 그래서 타입 선언도 모듈 단위로 해줘야 해.

이때 사용하는 마법의 주문이 바로 `declare module '모듈이름' { ... }` 구문이야.

여기서 '모듈이름'은 우리가 `import` 할 때 쓰는 그 문자열을 그대로 적어주면 돼. 예를 들어 `import slider from 'super-slider'` 라면, 모듈 이름은 `'super-slider'`가 되는 거지.

이제 이 `declare module` 블록 안에서, 해당 모듈이 `export` 하는 것들을 똑같이 `export` 키워드를 사용해서 타입으로 선언해주면 돼. 몇 가지 흔한 케이스를 살펴보자.

모듈 타입 선언 예제

케이스 1: 여러 함수를 `export` 하는 모듈

만약 `my-utils`라는 라이브러리가 이렇게 생겼다면:


// my-utils.js
exports.padLeft = function(str, len) { /* ... */ };
exports.capitalize = function(str) { /* ... */ };

선언 파일은 이렇게 작성해:


// my-utils.d.ts
declare module 'my-utils' {
  export function padLeft(text: string, length: number): string;
  export function capitalize(text: string): string;
}

// 사용하는 쪽 (my-app.ts)
import { padLeft, capitalize } from 'my-utils';
const padded = padLeft('hello', 10);

케이스 2: `export default`로 단일 값을 내보내는 모듈

이번엔 `cool-chart` 라이브러리가 클래스 하나를 기본으로 내보낸다고 해보자:


// cool-chart.js
class CoolChart { /* ... */ }
module.exports = CoolChart;

선언 파일은 `export default`를 사용해:


// cool-chart.d.ts
declare module 'cool-chart' {
  interface ChartOptions {
    // ...
  }
  export default class CoolChart {
    constructor(element: HTMLElement, options: ChartOptions);
    render(): void;
  }
}

// 사용하는 쪽 (my-app.ts)
import Chart from 'cool-chart'; // default export는 중괄호 없이 import
const myChart = new Chart(document.getElementById('chart'), { /* ... */ });

케이스 3: `default`와 일반 `export`가 섞인 모듈 (아주 흔함!)

예를 들어 `axios`처럼, 기본 객체도 있고 유틸리티 타입도 함께 `export`하는 경우야.


// super-api.js
function createInstance() { /* ... */ }
const instance = createInstance();
instance.VERSION = '1.2.3';
module.exports = instance;
module.exports.default = instance; // CommonJS와 ES 모듈 호환성을 위해

이런 복잡한 구조는 이렇게 표현할 수 있어:


// super-api.d.ts
declare module 'super-api' {
  // 함수이면서 동시에 객체인 경우, 이렇게 선언할 수 있어.
  interface SuperApiInstance {
    (url: string, config?: ApiConfig): Promise<ApiResponse>; // 호출 시그니처
    
    // 객체로서 가지는 속성들
    get(url: string, config?: ApiConfig): Promise<ApiResponse>;
    post(url: string, data?: any, config?: ApiConfig): Promise<ApiResponse>;
  }

  // export 할 값의 타입을 정의
  const api: SuperApiInstance;
  
  // 최종적으로 default export
  export default api;
}

// 사용하는 쪽 (my-app.ts)
import superApi from 'super-api';
superApi.get('/users');
superApi('/users'); // 이렇게 함수처럼 호출도 가능

처음엔 좀 헷갈릴 수 있지만, 핵심은 간단해. **"JS 라이브러리의 `export` 구조를 `.d.ts` 파일에서 `export declare`로 똑같이 흉내 낸다"**는 것만 기억하면 돼. 라이브러리의 문서를 보거나, 실제 JS 파일을 살짝 뜯어보면 어떤 구조로 `export`하는지 금방 파악할 수 있을 거야.

4.4. 고급 기술 및 알아두면 좋은 것들

기본기를 다졌으니, 이제 당신을 '타입 마스터'로 만들어 줄 몇 가지 고급 기술을 알아보자.

1. 네임스페이스 (Namespace)

`namespace`는 관련된 타입들을 하나의 그룹으로 묶어주는 역할을 해. 옛날 JS 라이브러리들이 전역 `window` 객체에 자기만의 객체를 하나 만들고 그 안에 모든 기능을 넣었던 방식(e.g., `jQuery.ajax`, `jQuery.each`)을 타이핑할 때 아주 유용해.


// my-legacy-lib.d.ts
declare namespace MyLegacyLib {
  export const version: string;
  export function doSomething(): boolean;
  
  export interface Options {
    // ...
  }
}

// 사용하는 쪽 (my-app.ts)
const options: MyLegacyLib.Options = { /* ... */ };
MyLegacyLib.doSomething();

`declare module`이 파일 기반의 모듈을 위한 것이라면, `declare namespace`는 전역 공간에 존재하는 객체 기반의 라이브러리를 위한 것이라고 생각하면 쉬워.

2. 모듈 보강 (Module Augmentation)

이건 정말 강력한 기능이야! 이미 타입 정의가 존재하는 모듈에 **내가 원하는 타입을 추가**하고 싶을 때 사용해. 예를 들어, Express.js 프레임워크를 사용하는데, 모든 요청(Request) 객체에 `currentUser`라는 속성을 추가해서 사용하고 싶다고 가정해보자.

이때 `express`의 기존 타입 정의를 수정하는 게 아니라, 내 프로젝트에서만 유효한 '보강'을 하는 거야.


// types/express.d.ts

// 1. express의 원래 타입들을 가져오기 위해 import
import 'express'; 

// 2. 이미 존재하는 'express-serve-static-core' 모듈을 다시 선언
declare module 'express-serve-static-core' {
  // 3. Request 인터페이스를 찾아서 새로운 속성을 추가!
  interface Request {
    currentUser?: User; // User 타입은 어딘가에 정의되어 있어야 함
  }
}

이렇게만 해두면, 이제 내 프로젝트의 모든 Express 라우터 핸들러에서 `req.currentUser`에 접근할 때 타입 에러 없이 자동 완성까지 지원받을 수 있어. 기존 라이브러리를 내 입맛에 맞게 확장하는, 정말 우아한 방법이지.

실전 투입! .d.ts, 이럴 때 이렇게 쓴다

이론은 충분히 배웠으니, 이제 실제 현장에서 마주칠 법한 시나리오들을 통해 배운 내용을 복습하고 응용해보자.

TypeScript 선언 파일(.d.ts) 활용 시나리오 시나리오 1: 타입 없는 NPM 패키지 'simple-carousel.js' 라이브러리처럼 '@types'에도 타입 정의가 없는 경우입니다. // types/simple-carousel.d.ts declare module 'simple-carousel' { interface Options { speed?: number; } export default class Carousel { constructor(id: string, opt: Options); play(): void; } } 시나리오 2: 전역 변수/객체 <script>로 로드한 'googleAnalytics'처럼 'window' 객체에 추가된 전역 변수를 타이핑합니다. // types/global.d.ts declare global { interface Window { googleAnalytics: { send(event: string): void; }; } } 시나리오 3: JS가 아닌 파일 Import '*.css', '*.svg', '*.png' 등 JS가 아닌 에셋 파일을 import할 때 사용합니다. // types/assets.d.ts declare module '*.css' { const classes: { [key: string]: string }; export default classes; } declare module '*.svg' { const content: any; export default content; }

시나리오 1: 타입이 전혀 없는 NPM 패키지 `super-slider.js`

문서를 보니 이 라이브러리는 `new SuperSlider('#slider', { loop: true })` 처럼 사용하고, `play()`, `stop()` 메소드를 제공한대. 그럼 우리의 임무는 이걸 `.d.ts`로 옮기는 거야.

  1. `types/super-slider.d.ts` 파일을 생성한다.
  2. `tsconfig.json`에 `typeRoots` 설정이 되어 있는지 확인한다.
  3. 파일에 모듈 선언을 작성한다.

// types/super-slider.d.ts

declare module 'super-slider' {
  // 생성자에 들어갈 옵션 객체의 타입을 먼저 정의해주자.
  interface SliderOptions {
    loop?: boolean;
    speed?: number;
    autoplay?: boolean;
  }

  // 라이브러리가 default로 export하는 클래스를 선언한다.
  export default class SuperSlider {
    constructor(selector: string, options?: SliderOptions);

    play(): void;
    stop(): void;
    goTo(index: number): void;
    getCurrentIndex(): number;
  }
}

이제 우리 코드에서 `import Slider from 'super-slider'`를 하면, TS는 이 선언 파일을 보고 `Slider`가 클래스라는 것, 생성자에 어떤 인자를 넣어야 하는지, 어떤 메소드를 사용할 수 있는지 모두 알게 된다. 완벽해!

시나리오 2: `

마케팅팀의 요청으로 모든 페이지에 GA나 Amplitude 같은 분석 스크립트를 심었어. 이 스크립트는 `window.analytics`라는 객체를 만들고, 우리는 `window.analytics.track('page_view')` 같은 코드를 호출해야 해.

이럴 땐 `declare global`을 사용해서 전역 `Window` 인터페이스를 '보강'해주면 돼.


// types/global.d.ts

declare global {
  interface Window {
    analytics?: { // 스크립트 로딩 전일 수 있으니 optional(?)로 선언
      track: (eventName: string, properties?: Record<string, any>) => void;
      identify: (userId: string, traits?: Record<string, any>) => void;
    };
  }
}

// Record<string, any>는 키가 문자열이고 값이 아무 타입이나 올 수 있는 객체를 의미해.
// 더 정확하게 타입을 정의할 수 있다면 더 좋겠지!

주의: 이 파일을 제대로 인식시키려면 `tsconfig.json`의 `include` 배열에 `types` 폴더가 포함되어 있는지 확인해야 해. 그리고 이 파일에는 `import`나 `export` 구문이 없어야 전역으로 적용돼.

시나리오 3: CSS 모듈이나 이미지 파일 `import`하기

React나 Vue 같은 최신 프레임워크에서는 JS 파일 안에서 CSS나 SVG 파일을 `import`하는 경우가 많아. 하지만 TS는 기본적으로 JS/TS 파일 외에는 모듈로 인식하지 못해서 에러를 뿜지.

이것도 `.d.ts`로 해결할 수 있어. 특정 확장자를 가진 파일들을 모듈로 선언해버리는 거야.


// types/assets.d.ts

// CSS 모듈(*.module.css)을 위한 타입 선언
declare module '*.module.css' {
  const classes: { readonly [key: string]: string };
  export default classes;
}

// 일반 CSS 파일을 위한 타입 선언
declare module '*.css' {
  const content: string;
  export default content;
}

// 이미지 파일들을 위한 타입 선언
declare module '*.svg' {
  // React에서 SVG를 컴포넌트처럼 쓸 때
  import * as React from 'react';
  export const ReactComponent: React.FunctionComponent<React.SVGProps<SVGSVGElement>>;
  const src: string;
  export default src;
}

declare module '*.png';
declare module '*.jpg';
declare module '*.jpeg';
declare module '*.gif';

이렇게 와일드카드(`*`)를 사용해서 선언해두면, 이제 `import styles from './MyComponent.module.css'`나 `import logo from '../assets/logo.svg'` 같은 코드를 에러 없이 자유롭게 사용할 수 있게 돼.

🔥 .d.ts 작성 베스트 프랙티스

  1. **`any`는 최후의 보루다:** `any`를 남발하면 TS를 쓰는 의미가 사라져. 귀찮더라도 최대한 구체적인 타입을 작성하려고 노력하자. 정 타입을 알 수 없을 땐 `any`보다 타입-세이프한 `unknown`을 먼저 고려해봐.
  2. **라이브러리의 API를 존중하라:** 내 맘대로 타입을 만드는 게 아니라, 실제 라이브러리의 사용법과 구조를 최대한 정확하게 반영해야 해. 문서는 기본, 필요하면 소스 코드도 참고하자.
  3. **JSDoc 주석을 적극 활용하라:** 타입 선언에 주석을 잘 달아두면, 나중에 코드를 사용하는 시점에 VS Code 같은 툴에서 풍부한 힌트를 얻을 수 있어. `/** ... */` 형식의 JSDoc 주석을 추천!
  4. **기여를 두려워하지 마라:** 네가 만든 `.d.ts` 파일이 꽤 괜찮다면, DefinitelyTyped에 Pull Request를 보내서 기여하는 걸 고려해봐. 전 세계 개발자들에게 도움을 주는 엄청나게 보람 있는 경험이 될 거야!

이제 당신도 타입 마스터! .d.ts와 함께하는 쾌적한 코딩

와, 정말 긴 여정이었어! 하지만 여기까지 꼼꼼히 따라온 너라면 이제 더 이상 `.d.ts` 파일 앞에서, 혹은 '타입 정의를 찾을 수 없다'는 에러 메시지 앞에서 쫄지 않을 거야.

오늘 우리가 배운 것들을 다시 한번 정리해볼까?

  • `.d.ts` 파일은 JS로 작성된 코드의 **타입 정보만을 담은 '설명서'**다.
  • 대부분의 유명 라이브러리는 `npm install --save-dev @types/라이브러리명`으로 **DefinitelyTyped**에서 타입 정의를 쉽게 얻을 수 있다.
  • 타입 정의가 없다면, `types` 폴더를 만들고 `tsconfig.json`에 `typeRoots`를 설정한 뒤, **직접 `.d.ts` 파일을 작성**할 수 있다.
  • 모듈 라이브러리는 `declare module '모듈명' { ... }` 구문을, 전역 객체는 `declare namespace`나 `declare global`을 사용한다.
  • `any`를 피하고, 최대한 **정확하고 구체적인 타입을 정의**하는 것이 핵심이다.

선언 파일을 작성하는 것은 처음에는 조금 번거롭고 귀찮은 작업처럼 느껴질 수 있어. 하지만 이 작은 노력 하나가 가져다주는 이점은 상상 이상이야.

**견고한 자동 완성(IntelliSense), 컴파일 시점의 에러 발견, 손쉬운 리팩토링, 그리고 그 자체로 훌륭한 문서 역할까지.**
`.d.ts`는 타입스크립트라는 언어의 장점을 극대화하고, 자바스크립트라는 거대한 생태계를 안전하게 항해할 수 있도록 도와주는 필수적인 나침반과도 같아.

이건 단순히 기술 하나를 더 배우는 걸 넘어, 코드의 품질과 개발 경험 자체를 한 단계 끌어올리는 중요한 '역량'이야. 이런 전문적인 스킬을 갈고닦는 과정에서 어려움을 겪거나 전문가의 도움이 필요하다면, **재능넷** 같은 플랫폼에서 경험 많은 개발자 멘토를 찾아보는 것도 좋은 방법이 될 수 있겠지.

이제 너는 JS 라이브러리를 만나도 자신 있게 타입의 옷을 입혀줄 수 있는 '타입 스타일리스트'가 되었어.
두려워하지 말고, 세상의 모든 자바스크립트에 타입의 날개를 달아주러 가보자고!

Happy Typing!

댓글 작성

이 글에 대한 여러분의 생각을 들려주세요

댓글 0