선언 파일 배포하기(Publishing)

선언 파일 배포하기(Publishing)

지금까지 이 가이드를 따라 선언 파일(declaration file)을 잘 작성했다면, 이제 그걸 실제로 npm에 배포할 차례예요. 크게 두 가지 방법이 있는데, 둘 중 어느 쪽이 우리 상황에 맞는지 하나씩 살펴볼게요.

출처: TypeScript 공식문서

본문

선언 파일을 npm에 올리는 두 가지 방법

선언 파일을 npm에 배포하는 방법은 크게 두 갈래예요.

  • 여러분의 npm 패키지에 번들해서 함께 올리는 방법
  • npm의 @types organization에 올리는 방법

여기서 기준이 되는 원칙이 하나 있어요. 타입이 소스 코드에서 생성되는 경우라면(예: declaration 컴파일 옵션으로 뽑아내는 경우), 그 타입을 소스 코드와 함께 올리는 게 맞아요. TypeScript 프로젝트든 JavaScript 프로젝트든 declaration으로 타입을 생성할 수 있어요.

반대로 타입이 소스에서 자동 생성되지 않는다면, DefinitelyTyped에 타입을 제출하는 걸 권장해요. 그러면 DefinitelyTyped가 그것을 npm의 @types organization으로 배포해 줘요.

npm 패키지 안에 선언 포함하기

패키지에 main .js 파일이 있다면, package.json 안에 메인 선언 파일의 위치도 함께 지정해 줘야 해요. types 프로퍼티를 번들된 선언 파일을 가리키도록 설정하면 돼요. 예를 들면 이렇게요.

{
    "name": "awesome",
    "author": "Vandelay Industries",
    "version": "1.0.0",
    "main": "./lib/main.js",
    "types": "./lib/main.d.ts"
}

참고로 "typings" 필드는 types와 같은 의미예요. types 대신 typings를 써도 무방해요.

의존성(Dependencies)

의존성 관리는 전부 npm이 담당해요. 중요한 건, 우리가 의존하는 모든 선언 패키지를 package.json"dependencies" 섹션에 제대로 표시해 두는 거예요.

예를 들어 Browserify와 TypeScript를 사용하는 패키지를 작성했다고 상상해 볼게요.

{
    "name": "browserify-typescript-extension",
    "author": "Vandelay Industries",
    "version": "1.0.0",
    "main": "./lib/main.js",
    "types": "./lib/main.d.ts",
    "dependencies": {
        "browserify": "latest",
        "@types/browserify": "latest",
        "typescript": "next"
    }
}

여기서 우리 패키지는 browserifytypescript 패키지에 의존해요. browserify는 자체 선언 파일을 npm 패키지에 번들해서 올리지 않기 때문에, 대신 @types/browserify에 의존해서 선언을 가져와야 해요. 반면 typescript는 자기 선언 파일을 직접 패키징해서 올리기 때문에, 추가로 필요한 의존성이 없었죠.

우리 패키지는 위 패키지들의 선언을 그대로 노출해요. 그래서 browserify-typescript-extension 패키지를 쓰는 모든 사용자는 이 의존성들도 함께 갖고 있어야 해요. 그래서 "dependencies"를 쓴 거예요. "devDependencies"가 아니라는 점이 핵심이에요. 만약 devDependencies에 넣어 버리면, 소비자(consumer)들이 그 패키지들을 수동으로 설치해야만 하거든요.

만약 우리가 만들고 있는 게 라이브러리로 쓰일 게 아니라 명령줄 애플리케이션뿐이라면, 그때는 devDependencies를 써도 무방해요.

주의할 점(Red flags)

/// <reference path="..." />

선언 파일 안에서는 /// <reference path="..." />쓰지 마세요. 이렇게 하면 안 돼요.

/// <reference path="../typescript/lib/typescriptServices.d.ts" />
....

대신 /// <reference types="..." />를 사용하세요.

/// <reference types="typescript" />
....

자세한 내용은 Consuming dependencies 섹션을 다시 살펴보세요.

의존하는 선언 패키징하기

여러분의 타입 정의가 다른 패키지에 의존한다면:

  • 다른 패키지의 선언을 우리 것과 합치지 마세요. 각각 별도의 파일로 유지해야 해요.
  • 우리 패키지 안에 그 선언을 복사해 넣지도 마세요.
  • 그 패키지가 자기 선언 파일을 패키징하지 않는다면, npm 타입 선언 패키지에 의존하세요.

typesVersions로 버전 선택하기

TypeScript는 package.json 파일을 열어 어떤 파일을 읽어야 할지 판단할 때, 먼저 typesVersions라는 필드를 살펴봐요.

폴더 리다이렉트(* 사용)

typesVersions 필드가 있는 package.json은 대략 이렇게 생겼어요.

{
    "name": "package-name",
    "version": "1.0.0",
    "types": "./index.d.ts",
    "typesVersions": {
        ">=3.1": { "*": ["ts3.1/*"] }
    }
}

package.json은 TypeScript에게 "먼저 현재 TypeScript 버전을 확인하라"고 말해 줘요. 버전이 3.1 이상이라면, TypeScript는 패키지를 기준으로 여러분이 import한 경로를 계산해서 패키지의 ts3.1 폴더에서 읽어요. { "*": ["ts3.1/*"] }가 바로 그 뜻이에요. path mapping을 알고 있다면, 그 방식과 똑같이 동작한다고 보면 돼요.

위 예시에서 TypeScript 3.1에서 실행 중일 때 "package-name"에서 import한다면, TypeScript는 [...]/node_modules/package-name/ts3.1/index.d.ts(및 기타 관련 경로)에서 해석을 시도해요. package-name/foo에서 import한다면 [...]/node_modules/package-name/ts3.1/foo.d.ts[...]/node_modules/package-name/ts3.1/foo/index.d.ts를 찾아보게 되죠.

그렇다면 이 예시에서 TypeScript 3.1이 아닌 버전이면 어떻게 될까요? typesVersions의 어떤 필드도 매칭되지 않으면 TypeScript는 types 필드로 폴백(fall back)해요. 그래서 여기서 TypeScript 3.0과 그 이전 버전은 [...]/node_modules/package-name/index.d.ts로 리다이렉트돼요.

파일 리다이렉트

한 번에 하나의 파일에 대해서만 해석을 바꾸고 싶다면, 정확한 파일 이름을 지정해서 TypeScript에 알려 줄 수 있어요.

{
    "name": "package-name",
    "version": "1.0.0",
    "types": "./index.d.ts",
    "typesVersions": {
        "<4.0": { "index.d.ts": ["index.v3.d.ts"] }
    }
}

TypeScript 4.0 이상에서는 "package-name"을 import하면 ./index.d.ts로 해석되고, 3.9 이하에서는 "./index.v3.d.ts"로 해석돼요.

여기서 주의할 점이 하나 있어요. 리다이렉션은 패키지의 외부(external) API에만 영향을 줘요. 프로젝트 내부의 import 해석은 typesVersions의 영향을 받지 않아요. 예를 들어 위 예시의 d.ts 파일 하나가 import * as foo from "./index"를 포함하고 있다면, 이것은 여전히 index.d.ts로 매핑돼요. index.v3.d.ts가 아니에요. 반면 다른 패키지에서 import * as foo from "package-name"으로 import하면, 그 패키지는 index.v3.d.ts를 받게 돼요.

매칭 동작(Matching behavior)

TypeScript가 어떤 컴파일러·언어 버전과 매칭되는지를 결정하는 방식은 Node의 semver ranges를 사용해요.

여러 필드

typesVersions는 여러 필드를 지원해요. 각 필드 이름이 매칭할 범위로 지정되죠.

{
    "name": "package-name",
    "version": "1.0",
    "types": "./index.d.ts",
    "typesVersions": {
        ">=3.2": { "*": ["ts3.2/*"] },
        ">=3.1": { "*": ["ts3.1/*"] }
    }
}

범위는 겹칠 가능성이 있기 때문에, 어떤 리다이렉트가 적용될지는 순서에 따라 달라져요. 위 예시에서 >=3.2>=3.1 둘 다 TypeScript 3.2 이상을 지원하지만, 순서를 뒤집으면 동작이 달라질 수 있어요. 그래서 위 예시는 아래와 동일하지 않아요.

{
    "name": "package-name",
    "version": "1.0",
    "types": "./index.d.ts",
    "typesVersions": {
        // NOTE: this doesn't work!
        ">=3.1": { "*": ["ts3.1/*"] },
        ">=3.2": { "*": ["ts3.2/*"] }
    }
}

@types로 배포하기

@types organization 아래의 패키지들은 types-publisher tool을 사용해 DefinitelyTyped에서 자동으로 배포돼요. 여러분의 선언을 @types 패키지로 배포하고 싶다면, DefinitelyTyped에 pull request를 제출하세요. 자세한 내용은 contribution guidelines 페이지에서 확인할 수 있어요.

더 알아보기