logo
홈블로그소개
3,207

Built with Next.js, Bun, Tailwind CSS and Shadcn/UI

ReactTypescriptJavaScript

React 라이브러리 패키지 만들기 — tsup 빌드부터 Verdaccio 로컬 배포까지

Toma
2026년 4월 22일
목차
📦 React 라이브러리 패키지 만들기 — tsup 빌드부터 Verdaccio 로컬 배포까지
🗺️ 전체 흐름 한눈에 보기
🔧 tsup — "번들러 설정 없이 패키지를 빌드한다"
tsup이 뭔가요?
패키지 빌드에서 필요한 출력물
tsup 설정
package.json exports 설정
⚠️ tsup DTS 이슈 — 가장 많이 막히는 부분
문제 상황
해결 방법
🏠 Verdaccio — "내 컴퓨터 안의 npm registry"
Verdaccio가 뭔가요?
대안 비교
Verdaccio 설치 및 실행
사용자 등록
🚀 실전: 빌드 → 배포 → 설치
1단계: 패키지 .npmrc 설정
2단계: 빌드 & 배포
3단계: 테스트 프로젝트에서 설치
4단계: 검증
🔄 GitHub Packages로 마이그레이션
📋 체크리스트
💬 마치며
이전 포스트Claude Code의 Advisor 전략

목차

📦 React 라이브러리 패키지 만들기 — tsup 빌드부터 Verdaccio 로컬 배포까지
🗺️ 전체 흐름 한눈에 보기
🔧 tsup — "번들러 설정 없이 패키지를 빌드한다"
tsup이 뭔가요?
패키지 빌드에서 필요한 출력물
tsup 설정
package.json exports 설정
⚠️ tsup DTS 이슈 — 가장 많이 막히는 부분
문제 상황
해결 방법
🏠 Verdaccio — "내 컴퓨터 안의 npm registry"
Verdaccio가 뭔가요?
대안 비교
Verdaccio 설치 및 실행
사용자 등록
🚀 실전: 빌드 → 배포 → 설치
1단계: 패키지 .npmrc 설정
2단계: 빌드 & 배포
3단계: 테스트 프로젝트에서 설치
4단계: 검증
🔄 GitHub Packages로 마이그레이션
📋 체크리스트
💬 마치며

📦 React 라이브러리 패키지 만들기 — tsup 빌드부터 Verdaccio 로컬 배포까지

"이 컴포넌트, 다른 프로젝트에서도 쓰고 싶은데..." 라는 생각을 해본 적 있다면, 이 글이 도움이 될 것이다.

이 글에서는 React + TypeScript로 만든 컴포넌트를 npm 패키지로 만들고, 로컬에서 실제 설치 흐름까지 검증하는 전 과정을 다룬다. 단순히 npm publish만 다루는 것이 아니라, 왜 이런 선택을 했는지, 어디서 막히는지, 그리고 어떻게 해결했는지까지 함께 정리했다.


🗺️ 전체 흐름 한눈에 보기

mermaid
flowchart LR
  A["소스 코드 (src/)"] --> B["tsup 빌드 (dist/)"]
  B --> C["Verdaccio 로컬 publish"]
  C --> D["테스트 프로젝트 npm install"]
  D --> E["검증 완료 → GitHub Packages"]

패키지 제작은 크게 두 단계다.

  1. 빌드: 소스 코드를 배포 가능한 형태로 변환 (tsup)
  2. 배포 검증: 실제 npm install 흐름을 로컬에서 테스트 (Verdaccio)

🔧 tsup — "번들러 설정 없이 패키지를 빌드한다"

tsup이 뭔가요?

tsup은 TypeScript/JavaScript 패키지를 빌드하는 도구다. 내부적으로 esbuild를 사용하기 때문에 매우 빠르고, 설정이 거의 없어도 동작한다.

💡 Webpack/Vite와 뭐가 다른가요?
Webpack과 Vite는 앱을 빌드하는 도구다. HTML, CSS, 이미지를 번들링하고 개발 서버를 띄우는 데 최적화되어 있다. tsup은 라이브러리를 빌드하는 도구다. ESM/CJS 형식의 JS 파일과 .d.ts 타입 선언 파일을 만드는 데 특화되어 있다.

패키지 빌드에서 필요한 출력물

패키지를 배포하면 사용자는 이 파일들을 쓰게 된다.

파일형식역할
dist/index.jsESMVite, 최신 번들러용
dist/index.cjsCJSNode.js, 구형 번들러용
dist/index.d.tsTypeScript 선언자동완성, 타입 체크

tsup 설정

typescript
// tsup.config.ts
import { defineConfig } from 'tsup'

export default defineConfig({
  entry: ['src/index.ts'],      // 진입점
  format: ['esm', 'cjs'],       // ESM + CJS 동시 출력
  dts: false,                   // ← 이게 핵심 (아래 DTS 이슈 섹션 참고)
  sourcemap: true,
  external: [
    'react', 'react-dom',
    'three', '@react-three/fiber', '@react-three/drei',
    'konva', 'react-konva',
  ],
})

external에 나열된 패키지들은 번들에 포함되지 않는다. peer dependency로 선언된 것들은 여기에 넣어야 한다. 그렇지 않으면 번들 크기가 폭발적으로 커지고, 사용자 프로젝트와 버전 충돌이 일어난다.

package.json exports 설정

json
{
  "main": "./dist/index.cjs",
  "module": "./dist/index.js",
  "types": "./dist/index.d.ts",
  "exports": {
    ".": {
      "types": "./dist/index.d.ts",
      "import": "./dist/index.js",
      "require": "./dist/index.cjs"
    },
    "./utils": {
      "types": "./dist/utils.d.ts",
      "import": "./dist/utils.js",
      "require": "./dist/utils.cjs"
    }
  }
}

exports 필드는 최신 번들러가 우선적으로 읽는다. main/module은 구형 도구를 위한 fallback이다.

💡 서브패스(./utils)는 왜 만들었나?
이 패키지는 Three.js와 Konva를 포함하기 때문에 번들 크기가 크다. 타입과 유틸 함수만 필요한 경우 /utils 진입점에서 가져오면 렌더러 코드가 포함되지 않아 초기 번들 크기를 대폭 줄일 수 있다. 실제로 React.lazy()와 함께 사용하면 1,465 KB → 506 KB (-62%) 절감 효과가 있었다.


⚠️ tsup DTS 이슈 — 가장 많이 막히는 부분

문제 상황

tsup.config.ts에서 dts: true로 설정하면 tsup이 자동으로 .d.ts 파일을 생성해준다. 그런데 프로젝트 루트에 tsconfig.json의 baseUrl이 설정되어 있으면 오류가 발생한다.

javascript
Error: Could not resolve "../types" from "src/components/FloorViewer.tsx"

tsup의 DTS 생성 워커가 상위 디렉토리의 tsconfig.json****을 탐색하면서 baseUrl 등의 경로 설정을 잘못 해석하는 것이 원인이다. 모노레포 구조이거나 패키지 디렉토리 외부에 tsconfig가 존재하면 이 문제가 더 자주 발생한다.

해결 방법

dts: false로 tsup DTS를 끄고, TypeScript 컴파일러를 별도로 실행한다.

json
// package.json
{
  "scripts": {
    "build": "tsup && tsc --project tsconfig.json --emitDeclarationOnly --outDir dist"
  }
}
typescript
// tsup.config.ts
export default defineConfig({
  entry: ['src/index.ts', 'src/utils.ts'],
  format: ['esm', 'cjs'],
  dts: false,   // tsup DTS 비활성화
  sourcemap: true,
  external: [ /* peer deps */ ],
})
json
// tsconfig.json (패키지 전용)
{
  "compilerOptions": {
    "declaration": true,
    "emitDeclarationOnly": true,
    "outDir": "dist",
    "rootDir": "src"
  }
}

⚠️ 주의: tsc --emitDeclarationOnly는 JS를 출력하지 않고 .d.ts만 만든다. tsup이 JS를 만들고, tsc가 타입 선언만 만드는 방식으로 역할을 분리하는 것이다.

빌드 결과물은 아래와 같다.

javascript
dist/
├── index.js        ← tsup (ESM)
├── index.cjs       ← tsup (CJS)
├── index.d.ts      ← tsc (타입 선언)
├── utils.js        ← tsup (ESM, 서브패스)
├── utils.cjs       ← tsup (CJS, 서브패스)
└── utils.d.ts      ← tsc (타입 선언, 서브패스)

🏠 Verdaccio — "내 컴퓨터 안의 npm registry"

Verdaccio가 뭔가요?

Verdaccio는 로컬에서 실행하는 private npm registry다. 쉽게 말하면 npmjs.com을 내 컴퓨터 안에 똑같이 띄우는 것이다.

javascript
일반 npm publish:  패키지 → npmjs.com → 전 세계 공개
Verdaccio:         패키지 → localhost:4873 → 내 컴퓨터 전용

💡 왜 로컬 registry가 필요한가?
npm pack으로 .tgz를 만들어 로컬 경로로 설치하는 방법도 있지만, 실제 npm install @scope/package-name 흐름과 완전히 같지는 않다. Verdaccio는 실제 registry 프로토콜을 사용하기 때문에 publish → install 흐름을 100% 동일하게 재현할 수 있다.

대안 비교

방법장점단점적합한 상황
npm link설정 없음, 즉시 사용심볼릭 링크 기반 — peer dependency 버전 충돌, React 중복 인스턴스 문제빠른 개발 중 임시 테스트
npm pack • 로컬 경로 설치설정 없음실제 registry 흐름과 다름, 버전 관리 불편단순 파일 포함 여부 확인
Verdaccio실제 publish/install 흐름 동일, peer dep 정상 처리초기 설치 및 서버 실행 필요출시 전 최종 검증 (권장)
GitHub Packages실제 배포 환경과 동일GitHub 조직/토큰 설정 필요, 인터넷 연결 필수최종 배포

npm link의 가장 큰 문제는 React 중복 인스턴스다. 패키지 안의 React와 사용자 프로젝트의 React가 서로 다른 인스턴스로 인식되면서 훅 관련 오류가 발생한다. Verdaccio는 실제 node_modules에 설치되기 때문에 이 문제가 없다.

Verdaccio 설치 및 실행

bash
# 전역 설치 (최초 1회)
npm install -g verdaccio

# 실행 (기본 포트: 4873)
verdaccio

# 브라우저에서 http://localhost:4873 확인

설정 파일 위치: ~/.config/verdaccio/config.yaml

사용자 등록

npm adduser는 대화형 입력을 요구해서 스크립트에서 쓰기 어렵다. REST API로 직접 등록하는 것이 편하다.

bash
curl -X PUT http://localhost:4873/-/user/org.couchdb.user:testuser \
  -H "Content-Type: application/json" \
  -d '{"name":"testuser","password":"testpass","email":"test@example.com","type":"user"}'

# 응답 예시
# {"ok":"user 'testuser' created","token":"eyJhbGci..."}

응답의 token 값을 저장해둔다.


🚀 실전: 빌드 → 배포 → 설치

1단계: 패키지 .npmrc 설정

패키지 저장소 루트에 .npmrc 파일을 만든다.

javascript
# building-visualization/.npmrc
@energyx:registry=http://localhost:4873
//localhost:4873/:_authToken=eyJhbGci...

⚠️ **.npmrc**를 반드시 **.gitignore**에 추가하자. authToken이 포함되어 있기 때문이다.

2단계: 빌드 & 배포

bash
# 패키지 디렉토리에서
bun run build
# 내부적으로: tsup && tsc --emitDeclarationOnly --outDir dist

npm publish --registry http://localhost:4873
# 또는 .npmrc에 registry가 설정되어 있으면
npm publish

버전을 올릴 때는 package.json의 version 필드를 수동으로 수정하거나 npm version patch를 사용한다.

3단계: 테스트 프로젝트에서 설치

테스트 프로젝트(예: bv-test) 루트에 .npmrc 추가:

javascript
# bv-test/.npmrc
@energyx:registry=http://localhost:4873
//localhost:4873/:_authToken=eyJhbGci...
bash
# bun은 Verdaccio와 캐시 충돌이 발생할 수 있어 npm 권장
npm install @energyx/building-visualization

⚠️ bun 사용 시 주의: bun은 자체 캐시 레이어를 사용하기 때문에 Verdaccio와 충돌이 발생하는 경우가 있다. Verdaccio 검증 단계에서는 npm을 사용하는 것을 권장한다.

4단계: 검증

typescript
// 테스트 프로젝트의 App.tsx
import { lazy, Suspense, useState } from 'react'
import {
  toScene,
  WALL_HEIGHT,
  type ViewMode,
  type ZoneBase,
  type ZoneVisual,
} from '@energyx/building-visualization/utils'  // 서브패스에서 타입/유틸 정적 import

// 렌더러만 lazy로 동적 import — 번들 분리
const FloorViewer = lazy(() =>
  import('@energyx/building-visualization').then(m => ({ default: m.FloorViewer }))
)

실제 앱에서 렌더링이 되고, 타입 자동완성이 뜨고, 에러가 없으면 검증 완료다.


🔄 GitHub Packages로 마이그레이션

Verdaccio 검증이 끝나면 GitHub Packages로 전환하는 건 간단하다. **.npmrc**의 registry URL 두 줄만 바꾸면 된다.

diff
# 패키지 .npmrc
- @energyx:registry=http://localhost:4873
- //localhost:4873/:_authToken=<verdaccio-token>
+ @energyx:registry=https://npm.pkg.github.com
+ //npm.pkg.github.com/:_authToken=${GITHUB_TOKEN}
diff
# 사용자 프로젝트 .npmrc
- @energyx:registry=http://localhost:4873
- //localhost:4873/:_authToken=<verdaccio-token>
+ @energyx:registry=https://npm.pkg.github.com
+ //npm.pkg.github.com/:_authToken=${GITHUB_TOKEN}

package.json의 publishConfig도 함께 업데이트:

json
{
  "publishConfig": {
    "registry": "https://npm.pkg.github.com",
    "access": "restricted"
  }
}

GitHub Personal Access Token 발급 경로:

GitHub → Settings → Developer settings → Personal access tokens → Fine-grained tokens

  • 배포 측: write:packages 권한
  • 설치 측: read:packages 권한

✅ 핵심: Verdaccio와 GitHub Packages는 .npmrc 두 줄만 다르다. 로컬에서 동작하면 GitHub Packages에서도 동일하게 동작한다. 이것이 Verdaccio로 검증하는 이유다.


📋 체크리스트

  • tsup.config.ts 작성 — format: ['esm', 'cjs'], dts: false, external 목록
  • tsconfig.json 타입 선언 전용 설정
  • package.json exports 필드 구성
  • bun run build 성공 확인 → dist/ 파일 목록 검토
  • Verdaccio 실행 → 사용자 등록 → token 저장
  • 패키지 .npmrc 작성 → .gitignore 추가
  • npm publish 성공 확인
  • 테스트 프로젝트 .npmrc 작성 → npm install 성공
  • 타입 자동완성 동작 확인
  • GitHub Packages 마이그레이션 시 .npmrc registry URL 교체

💬 마치며

패키지 제작에서 가장 어려운 부분은 사실 "어디서 무엇을 써야 하는지" 파악하는 것이다. tsup은 빌드 도구고 Verdaccio는 배포 검증 도구다. 역할이 다르다.

tsup DTS 이슈처럼 처음 마주치면 당황스러운 문제들도 있지만, 원인을 이해하면 해결책이 명확하다. dts: false + tsc --emitDeclarationOnly 조합을 기억해두자.

Verdaccio 검증 단계를 거치면 GitHub Packages나 npmjs.com으로 옮길 때 .npmrc 두 줄만 바꾸면 된다. 이 흐름 자체가 패키지 배포의 본질이다.