Tips & Tricks (업데이트: 2026. 6. 2.)

Claude Code로 npm 패키지 만들고 배포하기

Claude Code로 npm 패키지를 만들고 tsup, exports, 타입 선언, npm pack 검증, CI 배포까지 정리합니다.

Claude Code로 npm 패키지 만들고 배포하기

Claude Code에게 “npm 패키지를 만들어 줘”라고 말하면 기본 파일은 금방 나옵니다. 하지만 실제로 공개할 패키지는 구현 코드만으로 끝나지 않습니다. package.json의 진입점, exports, 타입 선언, README, npm pack 결과, CI 배포, 공개 권한까지 맞아야 합니다. 이 중 하나라도 빠지면 로컬에서는 잘 되는데 사용자의 프로젝트에서는 import가 깨지거나, 불필요한 파일이 registry에 올라갈 수 있습니다.

이 글은 TypeScript 문자열 유틸리티 패키지를 예제로 사용합니다. Claude Code는 무조건 믿고 공개시키는 도구가 아니라, 파일을 만들고 설정을 점검하며 위험을 설명하게 만드는 페어 프로그래머로 다룹니다. 최신 기준은 npm 공식 package.json 문서, scoped public packages, npm pack, trusted publishing, Claude Code 공식 문서를 확인하세요.

먼저 패키지 설계를 적는다

처음 프롬프트가 모호하면 Claude Code는 보기 좋은 boilerplate를 만들지만 공개 품질까지 보장하지 못합니다. 패키지 이름, 사용자, 런타임, 모듈 형식, 검증 명령, 공개 정책을 먼저 전달하세요. 특히 type: "module", main, exports, types는 서로 연결되어 있으므로 나중에 고치는 것보다 처음부터 같이 설계하는 편이 안전합니다.

항목이 글의 결정Claude Code가 확인할 것
패키지명@acme/string-kitscoped public 공개에 --access public이 필요한지
사용자Node.js와 TypeScript 사용자ESM import와 CJS require가 모두 되는지
빌드tsup으로 ESM, CJS, 선언 파일 출력exports가 가리키는 파일이 dist에 있는지
공개 파일dist, README.md, LICENSE만 포함npm pack --dry-run에 예상 밖 파일이 없는지
배포GitHub Actions + npm Trusted Publishing장기 npm token 의존을 줄였는지

흐름을 먼저 그리면 Claude Code가 구현 코드만 만들고 검증을 빼먹는 일을 줄일 수 있습니다.

flowchart LR
  A["설계 메모"] --> B["package.json"]
  B --> C["src/index.ts"]
  C --> D["Vitest"]
  D --> E["tsup build"]
  E --> F["npm pack dry-run"]
  F --> G["CI publish"]

CLI 패키지를 만들 계획이라면 Claude Code CLI 도구 개발을 이어서 보세요. 프롬프트와 검증 습관은 Claude Code 생산성 팁과 함께 보면 좋습니다.

최소 프로젝트 만들기

복잡한 저장소에 바로 넣기 전에 깨끗한 디렉터리에서 확인하세요. 작은 패키지는 실패 원인이 설정인지, 빌드 도구인지, 워크스페이스인지 빨리 구분할 수 있습니다.

mkdir string-kit
cd string-kit
npm init -y
npm install -D typescript tsup vitest @types/node
mkdir src scripts

package.json은 공개 계약입니다. main은 CJS 사용자, module은 일부 구형 bundler, types는 TypeScript, exports는 현대적인 진입점 제어를 담당합니다. files를 좁히면 테스트 fixture, 초안, 로컬 설정, sourcemap이 잘못 포함될 위험을 낮출 수 있습니다.

{
  "name": "@acme/string-kit",
  "version": "0.1.0",
  "description": "Small TypeScript string utilities used as an npm package example.",
  "type": "module",
  "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"
    },
    "./package.json": "./package.json"
  },
  "files": ["dist", "README.md", "LICENSE"],
  "sideEffects": false,
  "scripts": {
    "build": "tsup",
    "test": "vitest run",
    "docs": "node scripts/write-readme.mjs",
    "test:pack": "npm pack --dry-run",
    "prepublishOnly": "npm run test && npm run build && npm run test:pack"
  },
  "keywords": ["string", "typescript", "utilities"],
  "license": "MIT",
  "devDependencies": {
    "@types/node": "^22.15.0",
    "tsup": "^8.5.0",
    "typescript": "^5.8.0",
    "vitest": "^3.2.0"
  }
}

TypeScript는 타입 검사에 집중하고 출력은 tsup에 맡깁니다. 이렇게 하면 distexports의 불일치를 Claude Code가 찾기 쉬워집니다.

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "ESNext",
    "moduleResolution": "Bundler",
    "strict": true,
    "declaration": true,
    "declarationMap": true,
    "skipLibCheck": true,
    "noEmit": true
  },
  "include": ["src", "tsup.config.ts"]
}

실제 구현과 테스트

공개용 글에서 가장 위험한 것은 실행되지 않는 의사 코드입니다. 아래 코드는 네 가지 함수를 내보냅니다. slugify는 제목을 영문 slug로 만들고, truncate는 표시 길이를 제한하고, interpolate는 간단한 템플릿 치환을 하고, byteLength는 UTF-8 바이트 수를 계산합니다.

export function slugify(input: string): string {
  return input
    .normalize("NFKD")
    .replace(/[\u0300-\u036f]/g, "")
    .toLowerCase()
    .trim()
    .replace(/[^a-z0-9]+/g, "-")
    .replace(/^-+|-+$/g, "");
}

export function truncate(input: string, maxLength: number, suffix = "..."): string {
  if (!Number.isInteger(maxLength) || maxLength < 1) {
    throw new RangeError("maxLength must be a positive integer");
  }
  if (suffix.length >= maxLength) {
    throw new RangeError("suffix must be shorter than maxLength");
  }
  if (input.length <= maxLength) return input;
  return `${input.slice(0, maxLength - suffix.length)}${suffix}`;
}

export function interpolate(template: string, values: Record<string, string | number>): string {
  return template.replace(/\{\{\s*([\w.-]+)\s*\}\}/g, (match, key: string) => {
    return Object.hasOwn(values, key) ? String(values[key]) : match;
  });
}

export function byteLength(input: string): number {
  return new TextEncoder().encode(input).length;
}

테스트는 정상 동작, 경계값, 예외, Unicode를 포함합니다. Claude Code에 “테스트 추가”라고만 하지 말고, 악센트 문자, 알 수 없는 placeholder, 잘못된 길이, UTF-8 바이트 수를 확인하라고 구체적으로 지시하세요.

import { describe, expect, it } from "vitest";
import { byteLength, interpolate, slugify, truncate } from "./index";

describe("slugify", () => {
  it("turns a title into an npm-friendly slug", () => {
    expect(slugify("Hello npm Package!")).toBe("hello-npm-package");
  });

  it("removes accents before replacing separators", () => {
    expect(slugify("Crème brûlée utils")).toBe("creme-brulee-utils");
  });
});

describe("truncate", () => {
  it("keeps short text unchanged", () => {
    expect(truncate("short", 10)).toBe("short");
  });

  it("adds a suffix inside the requested length", () => {
    expect(truncate("Claude Code package", 12)).toBe("Claude Co...");
  });

  it("rejects invalid lengths", () => {
    expect(() => truncate("abc", 2)).toThrow(RangeError);
  });
});

describe("interpolate", () => {
  it("replaces known placeholders and keeps unknown ones", () => {
    expect(interpolate("Hi {{ name }}, ship {{pkg}} {{missing}}", {
      name: "Masa",
      pkg: "@acme/string-kit",
    })).toBe("Hi Masa, ship @acme/string-kit {{missing}}");
  });
});

describe("byteLength", () => {
  it("counts UTF-8 bytes", () => {
    expect(byteLength("npm")).toBe(3);
    expect(byteLength("日本語")).toBe(9);
  });
});

tsup와 README 생성

tsup 설정은 작게 유지합니다. outExtension은 ESM을 .js, CJS를 .cjs로 내보내 package.json의 진입점과 맞춥니다. 이 글에서는 sourcemap: false를 사용합니다. 공개 패키지에 내부 소스맵을 넣을지는 의도적으로 결정해야 하기 때문입니다.

import { defineConfig } from "tsup";

export default defineConfig({
  entry: ["src/index.ts"],
  format: ["esm", "cjs"],
  dts: true,
  clean: true,
  sourcemap: false,
  minify: false,
  target: "es2022",
  outDir: "dist",
  outExtension({ format }) {
    return { js: format === "esm" ? ".js" : ".cjs" };
  },
});

README는 코드와 쉽게 어긋납니다. 작은 생성 스크립트만 있어도 설치 방법과 사용 예제를 API 변경에 맞춰 유지할 수 있습니다.

import { writeFile } from "node:fs/promises";

const fence = String.fromCharCode(96).repeat(3);
const readme = `# @acme/string-kit

Small TypeScript string utilities packaged with tsup.

## Install

${fence}bash
npm install @acme/string-kit
${fence}

## Usage

${fence}ts
import { slugify, truncate } from "@acme/string-kit";

console.log(slugify("Hello npm Package!"));
console.log(truncate("Claude Code package", 12));
${fence}
`;

await writeFile(new URL("../README.md", import.meta.url), readme);

npm pack으로 공개 전 검증

npm publish 직전에 처음 확인하면 늦습니다. npm pack --dry-run으로 실제 tarball에 가까운 파일 목록을 보고, README, LICENSE, 타입 선언, ESM/CJS 출력이 들어갔는지 확인합니다.

npm run docs
npm test
npm run build
npm pack --dry-run
node -e "import('./dist/index.js').then((m)=>console.log(m.slugify('Hello ESM')))"
node -e "const m=require('./dist/index.cjs'); console.log(m.slugify('Hello CJS'))"

더 안전하게 보려면 생성된 .tgz를 다른 디렉터리에 설치합니다. 이 검사는 로컬 src가 옆에 있어서 우연히 성공하는 문제를 잡아줍니다.

npm pack
mkdir ../string-kit-smoke
cd ../string-kit-smoke
npm init -y
npm install ../string-kit/acme-string-kit-0.1.0.tgz
node -e "import('@acme/string-kit').then((m)=>console.log(m.truncate('Claude Code package', 12)))"

실무 사용 사례는 세 가지입니다. 첫째, 여러 웹앱과 관리자 화면에서 같은 문자열 규칙을 공유할 수 있습니다. 둘째, 콘텐츠 운영에서 description, 카드 제목, 릴리스 노트를 같은 함수로 정리할 수 있습니다. 셋째, 디자인 시스템이나 CLI가 작은 유틸리티를 별도 패키지로 배포해 애플리케이션이 SemVer 기준으로 업그레이드할 수 있습니다.

GitHub Actions로 배포

CI 배포는 리뷰 가능한 경로를 만듭니다. npm Trusted Publishing을 사용하면 지원되는 CI에서 OIDC로 공개할 수 있어 장기 npm token을 줄일 수 있습니다. npm 쪽에서 trusted publisher를 먼저 설정하고, release 이벤트에서만 공개하도록 제한하세요.

name: package

on:
  push:
    branches: [main]
  pull_request:
  release:
    types: [published]

permissions:
  contents: read
  id-token: write

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 22
          registry-url: https://registry.npmjs.org
          cache: npm
      - run: npm ci
      - run: npm run docs
      - run: npm test
      - run: npm run build
      - run: npm pack --dry-run

  publish:
    if: github.event_name == 'release'
    needs: test
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 22
          registry-url: https://registry.npmjs.org
          cache: npm
      - run: npm ci
      - run: npm run docs
      - run: npm test
      - run: npm run build
      - run: npm publish --access public

버전 설명과 변경 기록까지 관리하려면 Claude Code와 Changesets 버전 관리를 함께 사용하세요. patch, minor, major 판단을 Claude Code가 리뷰하기 쉬워집니다.

자주 생기는 실수

첫 번째는 exports 없이 main만 두는 것입니다. 오래된 예제에서는 보일 수 있지만 현대 TypeScript 사용자에게는 진입점이 불명확합니다. 두 번째는 files를 좁히지 않아 테스트 데이터, 초안, 소스맵을 공개하는 것입니다. 세 번째는 README가 오래되는 것입니다. 함수명이 바뀌었는데 문서가 그대로라면 패키지 신뢰도가 바로 떨어집니다.

네 번째는 Unicode 지원을 과하게 약속하는 것입니다. 이 예제의 truncate는 JavaScript 문자열 길이를 사용하므로 emoji 표시 폭까지 완벽히 처리하지 않습니다. 다섯 번째는 Claude Code에게 공개 결정까지 맡기는 것입니다. 패키지명, scope, 2FA, Trusted Publishing, 최종 release 승인만큼은 사람이 확인해야 합니다.

Claude Code 프롬프트 템플릿

Create a TypeScript npm package.

Goal:
- Package name: @acme/string-kit
- Support both ESM import and CJS require
- Use tsup to emit dist/index.js, dist/index.cjs, and dist/index.d.ts
- Include README generation, Vitest tests, and npm pack verification

Constraints:
- Only touch package.json, tsconfig.json, tsup.config.ts, src, scripts, and .github/workflows
- Do not use pseudocode; the project must run after npm install
- Do not publish source maps or unnecessary test files in the package tarball

Acceptance criteria:
- npm test passes
- npm run build passes
- npm pack --dry-run output is summarized
- ESM import and CJS require smoke tests are shown
- List the human release checks before npm publish

CTA: 배포 품질을 템플릿으로 남기기

npm 패키지는 한 번 공개하면 끝이 아닙니다. 버전, README, CI Node 버전, 의존성, registry 권한이 계속 바뀝니다. 먼저 무료 Claude Code 치트시트로 안전한 프롬프트와 검증 명령을 손에 익히세요. 반복 사용할 템플릿은 ClaudeCodeLab 제품에서 확인할 수 있습니다. 팀의 CLAUDE.md, CI, 공개 권한, 리뷰 기준을 정리해야 한다면 Claude Code 교육 및 상담이 적합합니다.

이 글의 흐름을 실제로 Windows 임시 디렉터리에서 시험한 결과, npm install, npm test, npm run build, npm pack --dry-run, ESM/CJS node -e 스모크 테스트가 통과했습니다. Masa의 운영에서는 Claude Code에게 npm pack 출력을 설명하게 하는 것만으로 README 오래됨, 선언 파일 누락, tarball 내용 오류를 공개 전에 꽤 잘 잡을 수 있었습니다.

정리

Claude Code는 npm 패키지 작성을 빠르게 해 주지만, 공개 계약은 명확히 적어야 합니다. package.json, exports, 타입, tsup, 테스트, README, npm pack, CI를 하나의 흐름으로 묶으세요. 배포 직전에는 dist를 봤는지, tarball을 봤는지, 사용자가 쓰는 진입점으로 import했는지만 확인해도 실패 가능성이 크게 줄어듭니다.

#Claude Code #npm #繝代ャ繧ア繝シ繧ク蜈ャ髢・ #TypeScript #OSS
무료

무료 PDF: Claude Code 치트시트

이메일을 입력하면 명령, 리뷰 습관, 안전한 워크플로를 정리한 PDF를 받을 수 있습니다.

개인정보를 안전하게 관리하며 스팸을 보내지 않습니다.

Masa

작성자 소개

Masa

Claude Code 실무 워크플로와 팀 도입을 검증하는 엔지니어입니다.