전역 라이브러리(Global Library)의 타입 정의 템플릿

전역 라이브러리(Global Library)의 타입 정의 템플릿

전역(global) 라이브러리는 따로 import를 쓰지 않고도 브라우저 전역 스코프에서 바로 꺼내 쓸 수 있는 라이브러리를 말해요. 이런 라이브러리를 위한 .d.ts 파일을 어떻게 만들어야 하는지, 그리고 이 템플릿을 쓰기 전에 그 라이브러리가 정말 "전역"인지 꼭 확인해야 하는 이유까지 함께 살펴볼게요.

출처: TypeScript 공식문서

본문

Global Libraries (전역 라이브러리)

전역(global) 라이브러리란, 어떤 형태의 import도 쓰지 않고 전역 스코프에서 바로 접근할 수 있는 라이브러리를 뜻해요. 많은 라이브러리가 전역 변수를 하나 또는 그 이상 그대로 노출해서 쓰게 만들죠.

예를 들어 jQuery를 쓴다면, 특별한 import 없이 그냥 $ 변수를 참조해서 바로 사용할 수 있어요:

$(() => {
  console.log("hello!");
});

보통 전역 라이브러리의 문서를 보면, HTML의 <script> 태그로 어떻게 불러다 쓰는지 안내가 적혀 있곤 해요:

<script src="http://a.great.cdn.for/someLib.js"></script>

오늘날 인기 있는 전역 라이브러리들 대부분은 사실 UMD 라이브러리로 작성돼 있어요(아래에서 다시 다룰게요). 그런데 UMD 라이브러리의 문서는 전역 라이브러리 문서와 구분하기가 상당히 어려워요. 그래서 전역 선언 파일(declaration file)을 작성하기 전에, 그 라이브러리가 정말 전역 라이브러리가 맞는지, 어쩌면 UMD인지 먼저 확인해두는 게 좋아요.

코드만 보고 전역 라이브러리인지 알아보기 (Identifying a Global Library from Code)

전역 라이브러리의 코드는 보통 아주 단순해요. 전역 "Hello, world" 라이브러리는 대략 이런 모양이죠:

function createGreeting(s) {
  return "Hello, " + s;
}

아니면 이렇게 window에 직접 걸어주는 형태일 수도 있어요:

window.createGreeting = function (s) {
  return "Hello, " + s;
};

전역 라이브러리 코드를 들여다보면 대개 이런 특징이 눈에 들어와요:

  • 최상위 레벨의 var 문이나 함수 선언이 존재한다
  • window.someName 같은 형태로 값을 하나 이상 할당한다
  • documentwindow 같은 DOM 기본 객체가 있다고 그냥 가정한다

반면에 이런 것들은 안 보이는 게 정상이에요:

  • requiredefine 같은 모듈 로더를 체크하거나 사용한다
  • var fs = require("fs"); 형태의 CommonJS/Node.js 스타일 import가 있다
  • define(...) 호출이 있다
  • 라이브러리를 require/import 하는 방법을 설명하는 문서가 있다

전역 라이브러리의 예시 (Examples of Global Libraries)

전역 라이브러리를 UMD 라이브러리로 바꾸는 건 보통 어렵지 않아서, 요즘 인기 있는 라이브러리 중에 아직 전역 스타일을 유지하는 경우는 손에 꼽을 정도로 드물어요. 다만 규모가 작으면서 DOM이 꼭 필요하거나(또는 의존성이 전혀 없는) 라이브러리는 여전히 전역 스타일로 남아 있을 수 있어요.

전역 라이브러리 템플릿 (Global Library Template)

아래처럼 생긴 .d.ts 예시를 하나 볼게요:

// Type definitions for [~THE LIBRARY NAME~] [~OPTIONAL VERSION NUMBER~]
// Project: [~THE PROJECT NAME~]
// Definitions by: [~YOUR NAME~] <[~A URL FOR YOU~]>

/*~ If this library is callable (e.g. can be invoked as myLib(3)),
 *~ include those call signatures here.
 *~ Otherwise, delete this section.
 */
declare function myLib(a: string): string;
declare function myLib(a: number): number;

/*~ If you want the name of this library to be a valid type name,
 *~ you can do so here.
 *~
 *~ For example, this allows us to write 'var x: myLib';
 *~ Be sure this actually makes sense! If it doesn't, just
 *~ delete this declaration and add types inside the namespace below.
 */
interface myLib {
  name: string;
  length: number;
  extras?: string[];
}

/*~ If your library has properties exposed on a global variable,
 *~ place them here.
 *~ You should also place types (interfaces and type alias) here.
 */
declare namespace myLib {
  //~ We can write 'myLib.timeout = 50;'
  let timeout: number;

  //~ We can access 'myLib.version', but not change it
  const version: string;

  //~ There's some class we can create via 'let c = new myLib.Cat(42)'
  //~ Or reference e.g. 'function f(c: myLib.Cat) { ... }
  class Cat {
    constructor(n: number);

    //~ We can read 'c.age' from a 'Cat' instance
    readonly age: number;

    //~ We can invoke 'c.purr()' from a 'Cat' instance
    purr(): void;
  }

  //~ We can declare a variable as
  //~   'var s: myLib.CatSettings = { weight: 5, name: "Maru" };'
  interface CatSettings {
    weight: number;
    name: string;
    tailLength?: number;
  }

  //~ We can write 'const v: myLib.VetID = 42;'
  //~  or 'const v: myLib.VetID = "bob";'
  type VetID = string | number;

  //~ We can invoke 'myLib.checkCat(c)' or 'myLib.checkCat(c, v);'
  function checkCat(c: Cat, s?: VetID);
}

더 알아보기