타입스크립트 라이브러리 프로젝트 진행하면서 겪은 시행착오

Nowon LeeNowon Lee
3 min read

계기

리액트 프론트엔드 프로젝트에 참여해서 캘린더 컴포넌트를 작성했던 적이 있었다.

외부 라이브러리로 제공되는 캘린더 컴포넌트는 디자인을 변경하거나 라이브러리에 없는 필요한 기능을 추가하는데 시간이 많이 소요됐던 적이 있었다. 디자인에 관계없이 캘린더 기능을 제공해주는 리액트 훅이 있었으면 좋겠다고 생각했다.

깃허브에서 찾아볼 수 있는 라이브러리 저장소는 여러 패키지로 이뤄진 단일 저장소로 구성되어있는 경우가 많았다.

  • 실제 라이브러리

  • 예시 프로젝트

  • 공식문서

모노레포를 도입함으로써 다음과 같은 이점을 확보할 수 있다는 것을 알게 되었다.

  • 프로젝트를 일관성있게 관리하고 여러 프로젝트 간 버전 관리를 편리하게 관리할 수 있다.

  • 프로젝트에서 외부 라이브러리를 도입할 때 라이브러리의 버전을 보다 쉽게 관리할 수 있다.

리액트 Hooks 라이브러리와 테스트할 수 있는 예제 프로젝트를 한 프로젝트에 관리하기 위해 모노레포를 선택했다. 모노레포를 처음 도입했을 때는 다음과 같은 의문점이 있었다.

  • 프로젝트 폴더 구조와 외부 의존성을 어떻게 관리해야 하는가?

  • 각각 프로젝트에 대해 명령어를 실행시키는 방법이 있을까?

  • 작성한 라이브러리를 예시 프로젝트에서 사용하려면 어떻게 해야 하는가?

  • pnpm이 라이브러리 작성과 빌드에도 관여해야 하는가?

프로젝트 관리

모노레포를 구성하기 위해 pnpm 워크스페이스를 도입할 수 있다. pnpm init으로 package.json 파일을 생성한다. 폴더 구조는 다음과 같이 설정했다.

  • packages/awesome-ts-library는 라이브러리 프로젝트를 세팅했다.

  • app/example는 예시 프로젝트를 세팅했다.

libs/
├── packages/
│   ├── awesome-ts-library/
│   │   ├── src/
│   │   ├── package.json
│   │   └── tsup.config.ts
│   └── another-package/
├── app/
│   ├── example/
│   │   ├── src/
│   │   ├── package.json
│   │   └── vite.config.ts
├── pnpm-workspace.yml
└── package.json

pnpm-workspace.yml로 어떤 프로젝트가 워크스페이스에 포함되어야 하는지 설정할 수 있다. 패키지명에 느낌표(!)를 붙여 워크스페이스에서 제외할 프로젝트를 설정할 수 있다.

packages/*는 packages 폴더 내 프로젝트를 전부 포함하겠다는 의미이다.

packages:
  - "packages/*"
  - "app/*"
  - "!**/test/**"

라이브러리 프로젝트 세팅

Vite로 리액트 프로젝트를 빠르게 세팅할 수 있지만 다음과 같은 이슈가 있었다.

  • 라이브러리 빌드하기 위해 여러 모듈 시스템에 대응해야 한다.

  • 타입스크립트 프로젝트의 경우 타입 정의 파일도 생성해야 한다.

어플리케이션이 아니라 라이브러리를 작성하는 프로젝트기 때문에 create-vite 대신 다른 방법을 찾아봤다. tsup을 도입했을 때 다음과 같은 이점을 챙길 수 있다.

  • 코드 스플리팅

  • 외부 의존성 관리

  • 모듈 시스템 지원

  • 타입 파일 생성

import { defineConfig } from "tsup";

export default defineConfig({
  entry: ["./src/index.ts"],
  clean: true,
  splitting: true,
  dts: true,
  format: ["cjs", "esm"],
  external: ["react", "date-fns"],
});

라이브러리 프로젝트 폴더에서 package.json에는 다음과 같은 속성들을 추가해야 한다.

  • type: 모듈 시스템을 지정한다.

  • types: 타입스크립트 타입 정의 파일을 지정할 수 있다.

  • exports: 패키지가 공개하는 파일을 정의할 수 있다.

    • import: ES 모듈 프로젝트 환경에서의 엔트리 파일

    • require: Common JS 프로젝트 환경에서의 엔트리 파일

  • files: 패키지에 포함할 파일을 명시할 수 있다.

{
  "name": "@nwleedev/awesome-ts-library",
  "author": "Nowon Lee",
  "version": "0.1.0",
  "type": "module",
  "types": "dist/index.d.ts",
  "exports": {
    ".": {
      "import": "./dist/index.js",
      "require": "./dist/index.cjs"
    }
  },
  "license": "MIT",
  "files": ["dist/*"]
}

프로젝트를 계속 하면서 pnpm과 tsup이 각각 다음과 같은 역할로 나눠지는 것을 알 수 있었다.

  • pnpm: 워크스페이스 기능을 통해 프로젝트 폴더 구조와 의존성, 명령어 등을 세팅한다.

  • tsup: 타입스크립트 프로젝트의 빌드를 담당한다.

    • pnpm 스크립트를 통해 tsup 빌드를 실행시킬 수 있다.

명령어 실행

pnpm 워크스페이스에서는 --filter를 통해 특정 프로젝트에서만 명령어를 실행시키도록 할 수 있다. 예를 들어 다음과 같은 명령어로 라이브러리를 빌드할 수 있다.

pnpm --filter ./packages/awesome-ts-library run build

만약 여러 프로젝트에 걸쳐 일관된 외부 패키지를 설치하게 하려면 다음과 같은 명령어를 입력할 수 있다.

pnpm add react date-fns -w

루트 위치의 package.json에 스크립트를 추가해서 명령어를 실행시키도록 할 수 있다.

// 루트 package.json
{
  "scripts": {
    "dev:app": "pnpm --filter ./app/example dev",
    "build:lib": "pnpm --filter ./packages/awesome-ts-library run build",
    "build:all": "pnpm -r run build"
  }
}

예시 프로젝트에서 불러오기

빌드가 끝난 라이브러리는 예제 프로젝트에서 다음과 같이 사용할 수 있다.

예제 프로젝트는 create-vite 명령어로 빠르게 세팅할 수 있었다.

{
  "ts-library": "workspace://*"
}
import Libs from "ts-library";

const libs = Libs();
0
Subscribe to my newsletter

Read articles from Nowon Lee directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Nowon Lee
Nowon Lee