- Published on
@wiid-get/design-system 패키지 npm 배포 썰
들어가며
디자인 시스템/UI 라이브러리를 배포하게 된 이유
여러 프로젝트(seawiid.com, shbusking.com, 그리고 기타 웹 도구 페이지들)를 관리하다 보니 공통으로 사용하는 UI 컴포넌트들의 디자인 일관성이 떨어지는 문제가 있었습니다. 작업 사이의 간격이 길다보니 어떤 디자인 컴포넌트가 있었는지 까먹고 다시 만드는 경우도 있었습니다.
이런 문제를 해결하기 위해 @wiid-get/design-system이라는 디자인 시스템 패키지를 만들었습니다. 이 패키지는 React 컴포넌트와 Tailwind CSS 설정을 포함하고 있으며, npm에 public으로 배포하여 여러 프로젝트에서 쉽게 설치하고 사용할 수 있도록 했습니다.
왜 굳이 npm 패키지로 관리하는가?
이전 직장에서처럼 모노레포(Monorepo) 환경이었다면 심볼릭 링크나 워크스페이스 기능을 통해 패키지 배포 없이도 공통 코드를 공유할 수 있었을 것입니다. 하지만 현재 제 프로젝트들(기술 블로그, seawiid.com 등)은 별도의 레포지토리(Multi-repo)로 격리되어 관리되고 있습니다.
저는 npm 배포를 통해 다음의 가치를 얻고자 했습니다.
- 독립적 라이프사이클: 모노레포는 공통 코드를 수정하면 연결된 모든 프로젝트가 즉시 영향을 받습니다. 하지만 npm은 버전(0.0.1, 1.0.0)을 가집니다. 라이브러리가 업데이트되어도 각 프로젝트(seawiid.com 등)는 준비가 되었을 때 원하는 버전을 선택해서 업데이트할 수 있습니다.
- 코드의 단일화: 물리적으로 떨어진 프로젝트 사이에서도 단일 진실 공급원(Single Source of Truth)을 유지.
- 책임 분리: UI 컴포넌트의 버그 수정과 각 서비스의 비즈니스 로직 수정을 명확히 분리하여 관리.
이 글에서는 npm에 패키지를 배포하는 과정에서 배운 점들을 회고 차원에서 정리해보려고 합니다. npm 배포 과정은 이 글을 참고했으며, 여기서는 실제 경험을 바탕으로 중요한 설정과 팁을 중심으로 다루겠습니다.
패키지의 이름과 버전 규칙
이름 짓기(Naming): 중복을 피하는 방법과 네이밍 규칙
npm 패키지 이름은 전 세계적으로 고유해야 합니다. 이미 사용 중인 이름은 사용할 수 없으므로, 패키지 이름을 정할 때는 다음 규칙을 따르는 것이 좋습니다:
- 소문자 사용: 패키지 이름은 소문자로 작성하는 것이 관례입니다
- 하이픈 사용: 단어 구분은 하이픈(
-)을 사용합니다 (예:my-design-system) - 간결하고 명확하게: 패키지의 용도를 한눈에 알 수 있도록 명명합니다
하지만 일반적인 이름은 이미 대부분 사용 중일 가능성이 높습니다. 이때 스코프(Scope)를 활용하면 나만의 네임스페이스를 만들 수 있습니다.
스코프(Scope) 활용: @아이디/패키지명으로 나만의 네임스페이스 만들기
스코프를 사용하면 @<조직 이름>/<패키지 이름> 형식으로 패키지를 배포할 수 있습니다. 예를 들어 @wiid-get/design-system처럼요.
scope 이름을 @wiid-get으로 정한 이유는 User ID seawiid의 wiid를 UI용 툴이라는 의미의 Widget와 합친 말장난입니다.
스코프를 사용하려면:
- npm에서 Organization(조직)을 먼저 생성해야 합니다
- 조직 이름이 스코프가 됩니다
- 같은 스코프 내에서는 패키지 이름이 중복되어도 괜찮습니다
버전의 의미: 첫 버전은 0.0.1부터 시작해도 괜찮을까?
npm은 Semantic Versioning(SemVer) 규칙을 따릅니다. 버전은 MAJOR.MINOR.PATCH 형식으로 구성됩니다:
- MAJOR: 호환되지 않는 API 변경 (예: 1.0.0 → 2.0.0)
- MINOR: 하위 호환성을 유지하면서 기능 추가 (예: 1.0.0 → 1.1.0)
- PATCH: 하위 호환성을 유지하면서 버그 수정 (예: 1.0.0 → 1.0.1)
첫 버전을 0.0.1부터 시작하는 것은 일반적입니다. 0.x.x 버전은 아직 안정화되지 않은 초기 개발 단계를 의미하며, API가 언제든 변경될 수 있다는 것을 나타냅니다. 1.0.0은 공식적으로 안정 버전으로 간주되며, 이후에는 SemVer 규칙을 엄격하게 따라야 합니다.
배포를 위한 package.json 핵심 설정
참고: 이 글에서는 패키지 매니저로
pnpm을 기준으로 설명합니다.npm이나yarn을 사용하시는 경우 명령어만 변경하시면 됩니다.
배포 시 꼭 포함해야 할 필수 필드
패키지를 배포하기 전에 package.json에 다음 필드들이 필수적으로 포함되어야 합니다:
{
"name": "@wiid-get/design-system",
"version": "0.0.1",
"description": "Design system package for React components",
"main": "dist/index.js",
"module": "dist/index.esm.js",
"types": "dist/index.d.ts"
}
- name: 패키지 이름 (스코프 포함)
- version: 패키지 버전 (SemVer 형식)
- description: 패키지에 대한 간단한 설명
- main: CommonJS(CJS) 진입점.
require()로 불러올 때 사용됩니다 - module: ES Module(ESM) 진입점.
import로 불러올 때 사용됩니다. 번들러가 트리 쉐이킹을 최적화할 수 있도록 ESM 형식으로 제공하는 것이 좋습니다 - types: TypeScript 타입 정의 파일 경로. TypeScript를 사용하는 프로젝트에서 자동완성과 타입 체크를 지원합니다
exports 필드 설정 (권장)
최신 Node.js 환경(ESM/CJS 하이브리드)에서는 main, module, types 외에 exports 필드를 사용하는 것이 권장됩니다. 이 설정을 추가하면 현대적인 번들러(Vite, Webpack 5+)에서 패키지를 더 정확하게 찾아낼 수 있습니다:
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.esm.js",
"require": "./dist/index.js"
}
}
exports 필드는 패키지의 진입점을 더 명확하게 정의하며, 조건부 export를 지원합니다. main, module, types 필드와 함께 사용할 수 있으며, exports가 우선순위가 높습니다.
불필요한 파일 제외하기
npm 배포 시 소스 코드, 테스트 파일, 설정 파일 등은 포함하지 않는 것이 좋습니다. 이를 위해 두 가지 방법이 있습니다:
방법 1: .npmignore 파일 사용
.gitignore와 유사하지만 npm 배포에만 적용됩니다:
src/
test/
*.test.js
.eslintrc
tsconfig.json
방법 2: files 필드 사용 (권장)
package.json의 files 필드를 사용하여 배포할 파일만 명시적으로 선언하는 whitelist 방식이 더 명확합니다:
"files": [
"dist",
"tailwind.preset.js"
]
이렇게 하면 npm 배포 시 dist/ 디렉토리와 tailwind.preset.js 파일만 포함되며, 나머지는 모두 제외됩니다. 이를 통해 불필요한 파일이 패키지에 포함되지 않아 패키지 크기를 줄일 수 있습니다.
TypeScript 환경을 위한 types 설정 팁
TypeScript를 사용하는 프로젝트에서 패키지를 사용할 때 타입 지원을 제공하려면:
- 타입 정의 파일 생성: 빌드 과정에서
.d.ts파일을 생성해야 합니다 - types 필드 설정:
package.json에"types": "dist/index.d.ts"를 명시합니다 - 타입 내보내기: 패키지의 모든 타입을 적절히 export해야 합니다
이렇게 설정하면 사용자가 패키지를 import할 때 자동완성과 타입 체크를 받을 수 있습니다.
publishConfig 설정 (스코프 패키지 필수)
scoped packages는 기본적으로 private으로 배포되기 때문에, public으로 배포하려면 publishConfig를 명시해야 합니다:
"publishConfig": {
"access": "public"
}
이렇게 설정하면 npm publish 명령어 실행 시 자동으로 public으로 배포됩니다. (private 패키지는 유료입니다!)
repository 정보 설정
package.json에 repository 정보를 작성하면 npm 페이지에서 자동으로 GitHub로 링크가 연결됩니다:
"repository": {
"type": "git",
"url": "git+https://github.com/dev-seawiid/design-system.git"
},
"bugs": {
"url": "https://github.com/dev-seawiid/design-system/issues"
},
"homepage": "https://github.com/dev-seawiid/design-system#readme"
이렇게 설정하면 npm 패키지 페이지에서 GitHub 저장소로 바로 이동할 수 있는 링크가 자동으로 생성됩니다.
빌드 스크립트 및 prepublishOnly 설정
소스 코드를 dist 폴더로 빌드하는 과정이 필수적입니다. 실수로 빌드하지 않은 구버전을 배포하는 것을 방지하기 위해 prepublishOnly 스크립트를 설정했습니다:
"scripts": {
"build": "your-build-command",
"prepublishOnly": "pnpm run build"
}
prepublishOnly는 pnpm publish 실행 전에 자동으로 실행되는 훅입니다. 이렇게 설정하면 배포 전에 항상 최신 빌드가 생성되어 실수를 방지할 수 있습니다.
npm 배포 실전 가이드
npm 계정 생성 및 터미널 로그인
먼저 npm 공식 웹사이트에서 계정을 생성해야 합니다. 계정 생성 후 터미널에서 로그인합니다:
pnpm login
npm 계정의 username, password, email을 입력하면 로그인이 완료됩니다. 최근 npm은 보안을 강화하기 위해 2FA(OTP) 인증을 요구하는 경우가 많습니다. 이 경우 터미널에서 OTP 코드를 입력해야 합니다.
스코프를 사용하는 경우, npm에서 해당 Organization(조직)을 먼저 생성해야 합니다. 웹사이트에서 조직을 생성한 후, 터미널에서 다시 로그인하면 해당 조직에 대한 권한이 부여됩니다.
빌드(Build) 과정의 중요성: 소스 코드가 아닌 '결과물'을 배포하는 이유
npm에 배포하는 것은 소스 코드가 아니라 빌드된 결과물입니다. 그 이유는:
- 번들링: 여러 파일을 하나로 합치거나 최적화된 형태로 변환합니다
- 트랜스파일링: TypeScript, JSX 등을 일반 JavaScript로 변환합니다
- 최적화: 코드 압축, 트리 쉐이킹 등을 통해 패키지 크기를 줄입니다
- 호환성: 다양한 환경(Node.js, 브라우저 등)에서 사용할 수 있도록 변환합니다
배포하기 전에 소스 코드를 빌드해야 합니다:
pnpm run build
빌드가 완료되면 dist/ 디렉토리에 배포 가능한 파일들이 생성됩니다. 빌드 결과물이 제대로 생성되었는지 확인한 후 배포를 진행하세요.
prepublishOnly 스크립트를 설정해두었다면, pnpm publish 실행 시 자동으로 빌드가 실행되므로 수동으로 빌드할 필요는 없습니다.
드디어 배포! pnpm publish
모든 준비가 완료되면 패키지를 배포할 수 있습니다:
pnpm publish
prepublishOnly 스크립트가 설정되어 있다면, pnpm publish 실행 시 자동으로 pnpm run build가 먼저 실행되어 최신 빌드가 생성됩니다. 그 후 배포가 진행됩니다.
publishConfig에 access: "public"을 설정해두었기 때문에 별도로 --access=public 옵션을 주지 않아도 public으로 배포됩니다. 만약 설정하지 않았다면:
pnpm publish --access=public
이렇게 명시적으로 옵션을 지정해야 합니다.
배포가 완료되면 npm 패키지 페이지(https://www.npmjs.com/package/@wiid-get/design-system)에서 확인할 수 있습니다.
시행착오와 주의사항 (Q&A)
Q: "이미 배포된 버전은 덮어쓸 수 없다" – 실수했을 때 대처법
A: npm에서는 같은 버전을 다시 배포할 수 없습니다. 실수로 잘못된 내용을 배포했다면:
버전을 올려서 재배포:
package.json의version을 올리고 다시 배포합니다pnpm version patch # 0.0.1 -> 0.0.2 pnpm publishdeprecate 처리: 잘못된 버전을 deprecated로 표시할 수 있습니다
pnpm deprecate @wiid-get/design-system@0.0.1 "This version has a bug, please use 0.0.2"72시간 이내: 배포 후 72시간 이내라면
pnpm unpublish명령어로 직접 삭제할 수 있습니다. 단, 이미 다른 프로젝트에서 사용 중이라면 의존성 문제가 생길 수 있으므로 신중해야 합니다
Q: 배포 전 미리 확인하기
A: 배포 전에 실제로 어떤 파일이 포함될지 확인하는 것이 좋습니다:
pnpm publish --dry-run
실제로 배포하지 않고 어떤 파일이 포함될지 확인할 수 있습니다:
pnpm publish --dry-run
pnpm pack
배포될 패키지를 .tgz 파일로 압축하여 로컬에 생성합니다. 이 파일을 압축 해제하여 내용을 확인할 수 있습니다:
pnpm pack
# @wiid-get-design-system-0.0.1.tgz 파일이 생성됨
# 압축 해제하여 확인
tar -xzf @wiid-get-design-system-0.0.1.tgz
Q: 버전 업데이트 시 pnpm version 명령어 사용하기
A: pnpm version 명령어를 사용하면 package.json의 버전을 자동으로 올리고 Git 태그도 생성합니다:
# Patch 버전 올리기 (0.0.1 -> 0.0.2)
pnpm version patch
# Minor 버전 올리기 (0.0.1 -> 0.1.0)
pnpm version minor
# Major 버전 올리기 (0.0.1 -> 1.0.0)
pnpm version major
이 명령어는:
package.json의version필드를 업데이트합니다- Git 커밋을 생성합니다 (커밋 메시지: "0.0.2")
- Git 태그를 생성합니다 (태그: "v0.0.2")
이후 pnpm publish를 실행하면 새로운 버전으로 배포됩니다.
standard-version을 활용한 자동화
더 고급 버전 관리를 원한다면 standard-version을 사용할 수 있습니다. 이 도구는 Conventional Commits 규칙을 따르는 커밋 메시지를 분석하여 자동으로 버전을 올려주고, CHANGELOG.md를 작성하며, 버전 태그도 생성해줍니다.
참고:
standard-version은 현재 공식적으로 유지보수가 중단된 상태입니다. 하지만 여전히 잘 작동하며, 혼자 작업하는 환경에서는 충분히 사용 가능합니다. 저는 현재 이 도구를 사용하고 있으며, 향후 팀 협업이 필요해지면release-please같은 대안으로 전환할 계획입니다. 여기서는 버전 관리의 중요성과 자동화의 이점에 대해 강조하고자 합니다.
package.json의 scripts에는 다음과 같이 설정했습니다:
"scripts": {
"release": "standard-version",
"release:dry": "standard-version --dry-run",
"release:patch": "standard-version --release-as patch",
"release:minor": "standard-version --release-as minor",
"release:major": "standard-version --release-as major"
}
사용법:
# 자동으로 버전 결정 (커밋 메시지 기반)
pnpm release
# 특정 버전으로 직접 지정
pnpm release:patch # 0.0.7 -> 0.0.8
pnpm release:minor # 0.0.7 -> 0.1.0
pnpm release:major # 0.0.7 -> 1.0.0
standard-version이 실행되면:
- 버전이 자동으로 올라갑니다
- CHANGELOG.md가 자동으로 업데이트됩니다
- Git 태그가 생성됩니다
이후 pnpm publish를 실행하면 새로운 버전으로 배포됩니다.
버전 관리는 패키지의 신뢰성과 사용자 경험에 직접적인 영향을 미칩니다. Semantic Versioning을 일관되게 따르면 사용자들이 안전하게 업데이트할 수 있고, 예상치 못한 breaking change로 인한 문제를 방지할 수 있습니다.
마치며
마치며: 첫 배포 소감
npm 패키지 배포 자체는 생각보다 간단했습니다. 특히 standard-version과 같은 도구를 활용하니 버전 관리와 CHANGELOG 작성이 자동화되어 배포 프로세스의 효율을 높일 수 있었습니다.
사실 디자인 시스템을 구축하며 까다로웠던 부분은 배포가 아니라 다음과 같은 기술적 문제들이었습니다.
- 스타일 오염(Style Pollution): 디자인 시스템의 스타일이 의존 프로젝트의 기존 스타일을 의도치 않게 침범(Override)하거나 충돌하는 이슈.
- 의존성 관리(Dependency Hell): peerDependencies로 설정한 라이브러리들이 빌드 결과물(dist)에서 적절히 분리되지 않아, 런타임에서 모듈을 찾지 못하는 이슈.
- 로컬 개발 환경의 한계: npm에 매번 배포하지 않고도, 개발 중인 디자인 시스템이 실제 프로젝트에서 어떻게 동작하는지 실시간으로 검증하는 과정의 번거로움 (npm link 등의 활용 고민).
이 까다로웠던 고민들과 해결 과정은 이후 포스트에서 다루어 보도록 하겠습니다.
읽어주셔서 감사합니다.