Algae-rithm 개발 블로그
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 seawiidwiid를 UI용 툴이라는 의미의 Widget와 합친 말장난입니다.

스코프를 사용하려면:

  1. npm에서 Organization(조직)을 먼저 생성해야 합니다
  2. 조직 이름이 스코프가 됩니다
  3. 같은 스코프 내에서는 패키지 이름이 중복되어도 괜찮습니다

버전의 의미: 첫 버전은 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.jsonfiles 필드를 사용하여 배포할 파일만 명시적으로 선언하는 whitelist 방식이 더 명확합니다:

"files": [
  "dist",
  "tailwind.preset.js"
]

이렇게 하면 npm 배포 시 dist/ 디렉토리와 tailwind.preset.js 파일만 포함되며, 나머지는 모두 제외됩니다. 이를 통해 불필요한 파일이 패키지에 포함되지 않아 패키지 크기를 줄일 수 있습니다.

TypeScript 환경을 위한 types 설정 팁

TypeScript를 사용하는 프로젝트에서 패키지를 사용할 때 타입 지원을 제공하려면:

  1. 타입 정의 파일 생성: 빌드 과정에서 .d.ts 파일을 생성해야 합니다
  2. types 필드 설정: package.json"types": "dist/index.d.ts"를 명시합니다
  3. 타입 내보내기: 패키지의 모든 타입을 적절히 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"
}

prepublishOnlypnpm 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가 먼저 실행되어 최신 빌드가 생성됩니다. 그 후 배포가 진행됩니다.

publishConfigaccess: "public"을 설정해두었기 때문에 별도로 --access=public 옵션을 주지 않아도 public으로 배포됩니다. 만약 설정하지 않았다면:

pnpm publish --access=public

이렇게 명시적으로 옵션을 지정해야 합니다.

배포가 완료되면 npm 패키지 페이지(https://www.npmjs.com/package/@wiid-get/design-system)에서 확인할 수 있습니다.

시행착오와 주의사항 (Q&A)

Q: "이미 배포된 버전은 덮어쓸 수 없다" – 실수했을 때 대처법

A: npm에서는 같은 버전을 다시 배포할 수 없습니다. 실수로 잘못된 내용을 배포했다면:

  1. 버전을 올려서 재배포: package.jsonversion을 올리고 다시 배포합니다

    pnpm version patch  # 0.0.1 -> 0.0.2
    pnpm publish
    
  2. deprecate 처리: 잘못된 버전을 deprecated로 표시할 수 있습니다

    pnpm deprecate @wiid-get/design-system@0.0.1 "This version has a bug, please use 0.0.2"
    
  3. 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

이 명령어는:

  1. package.jsonversion 필드를 업데이트합니다
  2. Git 커밋을 생성합니다 (커밋 메시지: "0.0.2")
  3. 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이 실행되면:

  1. 버전이 자동으로 올라갑니다
  2. CHANGELOG.md가 자동으로 업데이트됩니다
  3. Git 태그가 생성됩니다

이후 pnpm publish를 실행하면 새로운 버전으로 배포됩니다.

버전 관리는 패키지의 신뢰성과 사용자 경험에 직접적인 영향을 미칩니다. Semantic Versioning을 일관되게 따르면 사용자들이 안전하게 업데이트할 수 있고, 예상치 못한 breaking change로 인한 문제를 방지할 수 있습니다.

마치며

마치며: 첫 배포 소감

npm 패키지 배포 자체는 생각보다 간단했습니다. 특히 standard-version과 같은 도구를 활용하니 버전 관리와 CHANGELOG 작성이 자동화되어 배포 프로세스의 효율을 높일 수 있었습니다.

사실 디자인 시스템을 구축하며 까다로웠던 부분은 배포가 아니라 다음과 같은 기술적 문제들이었습니다.

  • 스타일 오염(Style Pollution): 디자인 시스템의 스타일이 의존 프로젝트의 기존 스타일을 의도치 않게 침범(Override)하거나 충돌하는 이슈.
  • 의존성 관리(Dependency Hell): peerDependencies로 설정한 라이브러리들이 빌드 결과물(dist)에서 적절히 분리되지 않아, 런타임에서 모듈을 찾지 못하는 이슈.
  • 로컬 개발 환경의 한계: npm에 매번 배포하지 않고도, 개발 중인 디자인 시스템이 실제 프로젝트에서 어떻게 동작하는지 실시간으로 검증하는 과정의 번거로움 (npm link 등의 활용 고민).

이 까다로웠던 고민들과 해결 과정은 이후 포스트에서 다루어 보도록 하겠습니다.

읽어주셔서 감사합니다.