라이브러리 구조 — 구조별 선언 파일 작성법

라이브러리 구조 — 구조별 선언 파일 작성법

타입스크립트로 남의 라이브러리를 쓰려면, 그 라이브러리에 맞는 선언 파일(declaration file) 을 만들어야 해요. 그런데 여기서 막히는 지점이 하나 있어요. 선언 파일을 어떻게 구조화할지는, 정작 그 라이브러리가 어떻게 사용되는지에 달려 있다는 거예요. JavaScript에서는 라이브러리를 소비하는 방법이 다양하니까, 선언 파일도 그 사용 방식에 맞춰서 써 줘야 해요. 이 가이드에서는 흔하게 등장하는 라이브러리 패턴을 어떻게 알아차리는지, 그리고 그 패턴에 맞는 선언 파일을 어떻게 작성하는지 차근차근 다뤄볼게요.

각 주요 라이브러리 구조 패턴에는 Templates 섹션에 대응하는 파일이 하나씩 있어요. 그 템플릿에서 시작하면 훨씬 빠르게 감을 잡을 수 있답니다.

출처: TypeScript 공식문서

본문

라이브러리의 종류 파악하기 (Identifying Kinds of Libraries)

먼저 타입스크립트 선언 파일이 표현할 수 있는 라이브러리의 종류부터 훑어볼게요. 각 종류가 어떻게 쓰이고, 어떻게 작성되며, 실제 세상에는 어떤 예시 라이브러리들이 있는지 간단히 살펴볼 거예요.

선언 파일을 작성하는 첫 단계는 바로 이 라이브러리의 구조를 파악하는 것이에요. 우리는 구조를 알아내기 위한 힌트를, 사용 방식(usage)과 코드 양쪽 모두에서 얻을 수 있어요. 라이브러리의 문서와 정리 상태에 따라 어느 쪽이 더 쉬울지 다르니까, 편한 쪽을 골라 쓰면 돼요.

무엇을 찾아봐야 할까요? (What should you look for?)

타이핑하려는 라이브러리를 보면서 스스로에게 물어볼 질문이에요.

  • 라이브러리를 어떻게 얻나요?
    • 예를 들어 npm으로만 받을 수 있나요, 아니면 CDN에서만 받을 수 있나요?
  • 어떻게 import 하게 되나요?
    • 전역 객체를 추가하나요? 아니면 requireimport/export 문을 사용하나요?

원문: What should you look for?

다른 종류의 라이브러리들에 대한 짧은 예시 (Smaller samples for different types of libraries)

아래에서 라이브러리 유형을 구분해서 하나씩 살펴볼게요. 각 패턴이 실제 코드로 어떻게 보이는지, 그리고 모듈·전역·UMD라는 이름이 정확히 무엇을 뜻하는지 감을 잡아봐요.

모듈 라이브러리 (Modular Libraries)

거의 모든 현대적인 Node.js 라이브러리는 모듈 계열(module family) 에 속해요. 이런 라이브러리들은 모듈 로더가 있는 JS 환경에서만 동작해요. 예를 들어 express는 Node.js에서만 동작하고, 반드시 CommonJS의 require 함수를 이용해 불러와야 하죠.

ECMAScript 2015(ES2015, ECMAScript 6, ES6라고도 불러요), CommonJS, RequireJS 모두 모듈을 가져오는 방식이 비슷해요. JavaScript CommonJS(Node.js)에서는 이렇게 써요.

var fs = require("fs");

타입스크립트나 ES6에서는 import 키워드가 같은 일을 해요.

import * as fs from "fs";

모듈 라이브러리의 문서를 보면 대개 이 중 한 줄이 등장해요.

var someLib = require("someLib");

또는

define(..., ['someLib'], function(someLib) {

});

전역 모듈에서와 마찬가지로, UMD 모듈의 문서에서도 이런 예시를 볼 수 있으니 실제 코드나 문서를 꼭 확인해 보세요.

코드로 모듈 라이브러리 판별하기 (Identifying a Module Library from Code)

모듈 라이브러리는 대개 다음 항목 중 적어도 일부를 갖고 있어요.

  • 조건 없는 require 또는 define 호출
  • import * as a from 'b'; 또는 export c; 같은 선언
  • exports 또는 module.exports에 대한 할당

반면에 다음은 거의 갖고 있지 않아요.

  • windowglobal의 프로퍼티에 대한 할당
모듈용 템플릿 (Templates For Modules)

모듈에는 네 가지 템플릿이 있어요: module.d.ts, module-class.d.ts, module-function.d.ts, module-plugin.d.ts.

먼저 module.d.ts를 읽어 보면 이 템플릿들이 모두 어떻게 동작하는지 개요를 잡을 수 있어요.

그 다음, 모듈이 함수처럼 호출될 수 있다면 module-function.d.ts 템플릿을 쓰면 돼요.

const x = require("foo");
// Note: calling 'x' as a function
const y = x(42);

모듈이 new로 생성될 수 있다면 module-class.d.ts 템플릿을 써요.

const x = require("bar");
// Note: using 'new' operator on the imported variable
const y = new x("hello");

모듈을 import 했을 때 다른 모듈들을 변경한다면 module-plugin.d.ts 템플릿을 써요.

const jest = require("jest");
require("jest-matchers-files");

원문: Templates For Modules

전역 라이브러리 (Global Libraries)

전역 라이브러리(global library) 는 어떤 형태의 import도 없이, 전역 스코프에서 바로 접근할 수 있는 라이브러리예요. 많은 라이브러리가 전역 변수 하나 이상을 노출해서 쓰도록 만들죠. 예를 들어 jQuery를 쓴다면, 별다른 준비 없이 $ 변수를 그냥 참조해서 쓸 수 있어요.

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

전역 라이브러리 문서에는 보통 HTML <script> 태그로 라이브러리를 사용하는 방법이 안내되어 있어요.

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

요즘 인기 있는 전역 접근형 라이브러리는 사실 대부분 UMD 라이브러리로 작성돼요(아래에서 바로 다룰게요). UMD 라이브러리의 문서는 전역 라이브러리의 문서와 구분하기가 어려워요. 그러니 전역 선언 파일을 쓰기 전에, 그 라이브러리가 실제로는 UMD가 아닌지 꼭 확인해야 해요.

코드로 전역 라이브러리 판별하기 (Identifying a Global Library from Code)

전역 라이브러리의 코드는 대개 아주 단순해요. 전역 "Hello, world" 라이브러리는 이런 모습일 거예요.

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

또는 이렇게 생겼을 수도 있어요.

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

// Node
global.createGreeting = function (s) {
  return "Hello, " + s;
};

// Potentially any runtime
globalThis.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)

global.d.ts 템플릿 파일은 myLib라는 예시 라이브러리를 정의해요. 그 안의 "Preventing Name Conflicts" 각주를 꼭 읽어 보세요.

원문: Global Library Template

UMD

UMD 모듈은 모듈(import를 통해)로도, 그리고 모듈 로더가 없는 환경에서는 전역으로도 사용할 수 있는 모듈이에요. Moment.js 같은 많은 인기 라이브러리가 이런 방식으로 작성돼요. 예를 들어 Node.js나 RequireJS에서는 이렇게 쓰죠.

import moment = require("moment");
console.log(moment.format());

반면 순수 브라우저 환경에서는 이렇게 써요.

console.log(moment.format());
UMD 라이브러리 판별하기 (Identifying a UMD library)

UMD 모듈은 모듈 로더 환경이 존재하는지를 확인해요. 이건 눈에 잘 띄는 패턴이라서 금방 찾을 수 있어요. 대략 이런 모습이에요.

(function (root, factory) {
    if (typeof define === "function" && define.amd) {
        define(["libName"], factory);
    } else if (typeof module === "object" && module.exports) {
        module.exports = factory(require("libName"));
    } else {
        root.returnExports = factory(root.libName);
    }
}(this, function (b) {

라이브러리 코드에서 typeof define, typeof window, typeof module을 확인하는 코드가 보인다면, 특히 파일 맨 위에 있다면 거의 확실히 UMD 라이브러리예요.

UMD 라이브러리 문서에는 require를 보여주는 "Using in Node.js" 예시와, <script> 태그로 스크립트를 로드하는 "Using in the browser" 예시가 함께 등장하는 경우가 많아요.

UMD 라이브러리 예시 (Examples of UMD libraries)

요즘 인기 라이브러리는 대부분 UMD 패키지로 제공돼요. jQuery, Moment.js, lodash 등이 대표적이죠.

템플릿 (Template)

module-plugin.d.ts 템플릿을 사용하면 돼요.

원문: UMD

의존성 소비하기 (Consuming Dependencies)

라이브러리가 가질 수 있는 의존성에는 여러 종류가 있어요. 이 절에서는 그것들을 선언 파일 안으로 어떻게 가져오는지 보여드릴게요.

전역 라이브러리에 대한 의존성 (Dependencies on Global Libraries)

라이브러리가 전역 라이브러리에 의존한다면, /// <reference types="..." /> 지시문을 사용해요.

/// <reference types="someLib" />

function getThing(): someLib.thing;

모듈에 대한 의존성 (Dependencies on Modules)

라이브러리가 모듈에 의존한다면 import 문을 사용해요.

import * as moment from "moment";

function getThing(): moment;

UMD 라이브러리에 대한 의존성 (Dependencies on UMD libraries)

전역 라이브러리에서 (From a Global Library)

전역 라이브러리가 UMD 모듈에 의존한다면 /// <reference types 지시문을 사용해요.

/// <reference types="moment" />

function getThing(): moment;
모듈 또는 UMD 라이브러리에서 (From a Module or UMD Library)

모듈 또는 UMD 라이브러리가 UMD 라이브러리에 의존한다면 import 문을 사용해요.

import * as someLib from "someLib";

UMD 라이브러리에 대한 의존성을 선언할 때 /// <reference 지시문을 사용하면 안 돼요!

원문: Consuming Dependencies

각주 (Footnotes)

이름 충돌 방지하기 (Preventing Name Conflicts)

전역 선언 파일을 작성할 때 전역 스코프에 타입을 아주 많이 정의할 수도 있어요. 하지만 이건 프로젝트에 선언 파일이 여러 개 있을 때 해결할 수 없는 이름 충돌을 일으킬 수 있으니, 되도록 피하는 게 좋아요.

따라서 따르기 쉬운 규칙 하나를 기억해 두세요. 라이브러리가 정의하는 전역 변수로 네임스페이스된 타입만 선언하는 거예요. 예를 들어 라이브러리가 전역 값 cats를 정의한다면, 이렇게 써야 해요.

declare namespace cats {
  interface KittySettings {}
}

이렇게 쓰면 안 되고요.

// at top-level
interface CatsKittySettings {}

이 지침을 지키면 라이브러리를 나중에 UMD로 전환할 때도 선언 파일 사용자들을 깨뜨리지 않아요.

원문: Preventing Name Conflicts

ES6가 모듈 호출 시그니처에 미치는 영향 (The Impact of ES6 on Module Call Signatures)

Express 같은 많은 인기 라이브러리는 import 했을 때 호출 가능한 함수(callable function) 로 스스로를 노출해요. 전형적인 Express 사용법은 이렇게 생겼죠.

import exp = require("express");
var app = exp();

그런데 ES6 규격을 따르는 모듈 로더에서는 최상위 객체(여기서는 exp로 import 됨)가 프로퍼티만 가질 수 있어요. 최상위 모듈 객체는 절대 호출 가능할 수 없어요.

여기서 가장 흔한 해결책은 호출/생성 가능한 객체에 default export를 정의하는 거예요. 모듈 로더는 보통 이 상황을 자동으로 감지해서 최상위 객체를 default export로 바꿔 줘요. 타입스크립트는 당신의 tsconfig.json"esModuleInterop": true가 있다면 이 처리를 대신 해 줘요.

원문: The Impact of ES6 on Module Call Signatures

더 알아보기