9.1 패키지 개발에 도움이 되는 도구
9.1.1 패키지를 업로드하는 또 다른 방법, JSR
JSR(JavaScript Registry): https://jsr.io/
npm과 비슷한 역할을 하는 자바스크립트 레지스트리다. npm의 슈퍼셋으로서 npm의 모든 기능을 지원하면서 추가적인 기능을 제공한다.
JSR 탄생 배경
npm은 Node.js와 함께 쓰이기 위해 만들어졌으며, Node.js에서 동작하는 CLI 도구이자 JS 생태계에서 만들어지는 패키지를 업로드하기 위한 레지스트리로 사용됐다. 대부분 CommonJS 모듈 시스템 기반에서 동작할 수 있도록 제작되었고, npm과 Node.js의 규격에 맞춰 package.json을 작성하면 손쉽게 업로드가 가능했다.
그러나 JavaScript 생태계에 많은 변화가 있었다.
| 변화 | 설명 |
|---|---|
| ESModule의 대두 | ES6에서 표준화된 모듈 시스템이 브라우저와 Node.js 모두에서 지원되기 시작 |
| 타입스크립트의 가파른 성장 | 타입 안정성에 대한 수요 증가로 TypeScript가 사실상 표준으로 자리잡음 |
| 새로운 런타임의 등장 | Deno, Bun, Cloudflare Workers(workerd) 등 Node.js 외의 런타임 등장 |
이러한 변화에 맞춰 JS 생태계는 새로운 도구와 시스템을 필요로 하기 시작했다. 타입스크립트를 원활하게 처리하며, 여러 JS 런타임에서 동작할 수 있는 유연성을 갖춘 시스템이 필요하다는 것을 의미한다.
JSR이 제공하는 기능
| 기능 | 설명 |
|---|---|
| 타입스크립트 네이티브 지원 | npm에서는 tsc 같은 도구로 자바스크립트로 컴파일하여 업로드해야 했지만, JSR에서는 타입스크립트 파일만 있어도 바로 업로드 가능하다. |
| ESModule 전용 | ESModule만 지원하며 CommonJS 모듈은 배포할 수 없다. 이는 모던 JavaScript 생태계로의 전환을 촉진한다. |
| 더 쉬운 배포 | CommonJS/ESModule 동시 지원 여부, Rollup/Vite 같은 번들러 설정, exports 설정, 타입스크립트 설정 등 복잡한 설정 없이 JSR에서 정의한 몇 가지 규칙만 지키면 손쉽게 배포 가능하다. |
| 다양한 런타임 지원 | Node.js, Deno, Bun, Cloudflare Workers(workerd) 등 다양한 런타임에서 동작한다. |
JSR의 추가 특징
스코프 기반 패키지 네이밍
JSR은 모든 패키지가 스코프를 필수로 가진다. npm의 선택적 스코프(@scope/package)와 달리, JSR에서는 @scope/package-name 형식이 필수다. 이를 통해 패키지 이름 충돌을 방지하고 소유권을 명확히 한다.
# JSR 패키지 설치 예시
npx jsr add @std/path
deno add @std/pathnpm과의 호환성
JSR 패키지는 npm에서도 사용할 수 있다. JSR은 패키지를 npm 호환 형식으로 자동 변환하여 제공하므로, 기존 npm 기반 프로젝트에서도 JSR 패키지를 설치할 수 있다.
자동 문서 생성
타입스크립트의 JSDoc 주석과 타입 정보를 기반으로 API 문서를 자동 생성한다. 별도의 문서화 작업 없이도 패키지 사용자에게 상세한 문서를 제공할 수 있다.
JSR 점수(JSR Score)
각 패키지의 품질을 평가하는 점수 시스템을 제공한다. 다음 항목들을 기준으로 점수가 매겨진다.
- 타입 선언 포함 여부
- 문서화 수준 (JSDoc 주석)
- README 파일 존재 여부
- 모든 런타임 호환성 명시 여부
npm에서 JSR 패키지 사용하기
JSR에 배포한 패키지는 npm 레지스트리에 직접 존재하지 않지만, npm 생태계에서도 사용할 수 있다. 예를 들어 @my/a라는 패키지를 JSR에 배포한 경우를 살펴보자.
설치 방법
각 패키지 매니저별로 다음과 같이 설치할 수 있다.
# npm
npx jsr add @my/a
# yarn
yarn dlx jsr add @my/a
# pnpm
pnpm dlx jsr add @my/a
# Deno
deno add @my/a설치 후 생성되는 파일
package.json
{
"name": "my-project",
"type": "module",
"dependencies": {
"@my/a": "npm:@jsr/my__a@^0.4.0"
}
}.npmrc
@jsr:registry=https://npm.jsr.ioJSR의 npm 호환성 메커니즘
위 설정을 통해 알 수 있는 JSR의 동작 방식은 다음과 같다.
| 항목 | 설명 |
|---|---|
| 원본 레지스트리 | @my/a라는 이름 그대로 올라간 레지스트리는 JSR 레지스트리다. npm 레지스트리에는 해당 패키지가 존재하지 않는다. |
| npm 호환 스코프 | JSR에서 자체적으로 구축한 npm 레지스트리 스코프인 @jsr 내부에 @jsr/my__a라는 이름으로 배포된다. |
| 패키지명 변환 규칙 | @jsr은 JSR이 가지고 있는 고유 스코프이며, my__a는 JSR에 배포한 패키지명(@my/a)을 기반으로 만들어진 npm 전용 패키지명이다. (/ → __로 변환) |
| 별도 레지스트리 | npm 공식 레지스트리(https://registry.npmjs.org/)가 아닌, JSR이 npm 호환을 위해 만든 별도 레지스트리(https://npm.jsr.io/)에 배포된다. |
JSR의 자동 빌드 처리
패키지 설치 후 node_modules를 살펴보면 다음과 같은 특징을 확인할 수 있다.
- 별도의 Rollup 설정 없이도
exports필드가 올바르게 작성되어 있음 - 타입스크립트 파일이
.d.ts파일과 함께 잘 컴파일되어 포함됨
이는 JSR이 배포 과정에서 Node.js 시스템에서도 잘 작동할 수 있도록 트랜스파일과 번들링 작업을 개발자를 대신해서 미리 수행하기 때문이다.
JSR과 Deno의 관계
JSR은 Deno에서 현재 권장하고 있는 패키지 배포 방식이다. Deno 진영에서 많이 사용하고 있지만, npm에서도 사용할 수 있게 상호운용성을 보장하기 때문에 각광받는 도구다. 별도의 복잡한 설정 없이 배포할 수 있다는 장점이 있다.
JSR에 대한 모든 정보를 다루려면 Deno에 대한 이해가 필수적이다.
Deno란?
Deno는 Node.js의 창시자인 **라이언 달(Ryan Dahl)**이 Node.js의 설계적 후회를 바탕으로 만든 새로운 JavaScript/TypeScript 런타임이다.
| 특징 | 설명 |
|---|---|
| 보안 중심 | 기본적으로 파일, 네트워크, 환경 접근 등에 대한 권한이 제한돼 있다. 필요한 권한은 --allow-read, --allow-net 등의 플래그로 명시적으로 부여해야 한다. |
| 타입스크립트 기본 지원 | 별도의 설정 없이 타입스크립트를 바로 실행할 수 있다. tsc 컴파일 과정이 필요 없다. |
| ESModule 사용 | Node.js의 CommonJS 대신 ESModule을 기본으로 사용한다. |
| URL 기반 모듈 | npm 같은 중앙 저장소 대신 URL을 통해 직접 모듈을 불러올 수 있다. |
| 브라우저 호환성 | 가능한 한 브라우저 API와 호환되는 API를 제공한다. (fetch, Web Crypto API 등) |
| 단일 실행 파일 | Node.js와 달리 단일 실행 파일로 배포되어 설치가 간편하다. |
Deno에서 JSR 패키지 사용하기
Deno에서는 JSR 패키지를 더욱 간편하게 사용할 수 있다.
// deno.json에서 import map 설정
{
"imports": {
"@std/path": "jsr:@std/path@^0.224.0"
}
}
// 코드에서 사용
import { join } from "@std/path";JSR 패키지 배포하기
JSR에 패키지를 배포하려면 jsr.json 또는 deno.json 파일이 필요하다.
{
"name": "@my/awesome-lib",
"version": "1.0.0",
"exports": "./mod.ts"
}배포 명령어:
# Deno CLI 사용
deno publish
# npx 사용
npx jsr publishJSR vs npm 비교
| 항목 | npm | JSR |
|---|---|---|
| 모듈 시스템 | CommonJS + ESModule | ESModule 전용 |
| 타입스크립트 | 컴파일 후 업로드 필요 | 네이티브 지원 |
| 스코프 | 선택 사항 (@scope/pkg) | 필수 (@scope/pkg) |
| 런타임 지원 | 주로 Node.js | Node.js, Deno, Bun, Workers |
| 문서 생성 | 별도 도구 필요 | 자동 생성 |
| 품질 점수 | 없음 | JSR Score 제공 |
| 번들링 | 개발자가 직접 설정 | 자동 처리 |
JSR의 한계와 고려사항
- ESModule 전용: CommonJS를 사용하는 레거시 프로젝트에서는 직접 사용이 어려울 수 있다.
- 생태계 규모: npm에 비해 아직 패키지 수가 적다.
- Deno 의존성: 일부 고급 기능은 Deno 환경에서 더 잘 동작한다.
- 타입 추론 한계: 복잡한 제네릭이나 조건부 타입의 경우 자동 생성된 문서가 정확하지 않을 수 있다.
9.1.2 복잡한 번들 프로세스를 한 번에 수행하는 도구, tsup
여러 사용자 환경을 지원하기 위해 Rollup, Vite, Babel, esbuild 등 다양한 도구를 사용해 정교하게 번들 결과물을 만들어왔다. 이러한 설정은 매우 복잡하고 정교함을 요구하기 때문에 가벼운 수준의 패키지를 만들기에는 조금 부담스러울 수도 있다.
이러한 경우 tsup을 활용해볼 수 있다. tsup은 타입스크립트 기반의 패키지를 esbuild를 통해 번들링하고, rollup-plugin-dts 또는 @microsoft/api-extractor를 통해 타입스크립트 타입을 추출해서 .d.ts 파일도 만들어준다.
@microsoft/api-extractor란?
Microsoft에서 만든 오픈소스 패키지로, 타입스크립트 기반 패키지를 더욱 효과적으로 관리할 수 있도록 다양한 기능을 제공한다.
| 기능 | 설명 |
|---|---|
| API 릴리스 태그 관리 | @alpha, @beta, @public 같은 태그로 아직 내보낼 준비가 되지 않은 API를 체계적으로 관리 |
| 타입 내보내기 누락 감지 | export 해야 할 타입을 누락한 경우 경고 |
| 의도치 않은 타입 내보내기 감지 | 내부용으로만 사용해야 할 타입이 외부로 노출되는 경우 감지 |
| 단일 .d.ts 파일 생성 | 여러 개의 .d.ts 파일을 하나의 파일로 롤업하여 깔끔하게 관리 |
tsup 소개
tsup은 esbuild를 기반으로 타입스크립트 기반 패키지를 번들링하도록 만들어졌다.
지원 파일 형식: .js, .json, .mjs, .ts, .tsx
CSS 지원 현황 (2026년 3월 기준)
tsup의 CSS 지원은 여전히 실험적(experimental) 상태다. CSS 파일을 번들링할 수는 있지만, CSS Modules이나 PostCSS 플러그인 지원 등 고급 기능은 제한적이다. 프로덕션 환경에서 복잡한 CSS 처리가 필요하다면 별도의 CSS 빌드 파이프라인을 구성하는 것이 권장된다.
// tsup.config.ts - CSS 번들링 활성화
import { defineConfig } from "tsup";
export default defineConfig({
entry: ["src/index.ts"],
format: ["cjs", "esm"],
dts: true,
// CSS 번들링 (실험적 기능)
injectStyle: true, // CSS를 JS에 주입
});tsup 기본 사용법
설치
npm install tsup -D기본 설정 (tsup.config.ts)
import { defineConfig } from "tsup";
export default defineConfig({
entry: ["src/index.ts"],
format: ["cjs", "esm"], // CommonJS와 ESModule 동시 출력
dts: true, // .d.ts 파일 생성
clean: true, // 빌드 전 출력 디렉토리 정리
minify: true, // 코드 압축
sourcemap: true, // 소스맵 생성
});실행
npx tsuptsup vs Vite 비교
tsup은 Vite 대신 라이브러리 번들링에 사용할 수 있지만, 몇 가지 차이점이 있다.
| 항목 | tsup | Vite (라이브러리 모드) |
|---|---|---|
| 주요 용도 | 라이브러리/패키지 번들링 | 애플리케이션 + 라이브러리 |
| 번들러 기반 | esbuild | esbuild (dev) + Rollup (build) |
| 코드 스플리팅 | 기본 활성화 (chunk- 파일 생성) | 설정에 따라 제어 가능 |
| browserslist 지원 | 미지원 | 지원 |
| 폴리필 삽입 | 미지원 | 플러그인으로 지원 |
| 설정 복잡도 | 매우 간단 | 상대적으로 복잡 |
tsup의 코드 스플리팅 이슈
tsup은 기본적으로 코드 스플리팅이 활성화되어 있어, entry로 지정한 파일들을 살펴보면 굳이 모듈 단위로 분리하지 않아도 되는 내용까지 모두 chunk-로 시작하는 별도의 파일로 구성된다.
dist/
├── index.js
├── index.mjs
├── chunk-ABCD1234.js # 자동 생성된 청크
├── chunk-EFGH5678.js # 자동 생성된 청크
└── index.d.ts실제 패키지 이용에는 크게 영향이 없으나, 번들링 결과물이 여러 개 생긴다는 차이점이 있다.
해결 방법과 트레이드오프
// tsup.config.ts
export default defineConfig({
// ...
splitting: false, // 코드 스플리팅 비활성화
});splitting: false 옵션으로 해결할 수 있지만, 이 경우 중복 코드가 발생해 번들 크기가 커진다는 단점이 있다. 패키지의 특성에 따라 적절히 선택해야 한다.
tsup의 한계
| 한계 | 설명 |
|---|---|
| browserslist 미지원 | 지원 환경에 맞게 자동으로 트랜스파일하지 않는다. tsup은 지정한 설정에 따라 번들하는 것에만 초점이 맞춰져 있다. |
| 폴리필 미삽입 | core-js 같은 폴리필을 빌드 시 자동으로 삽입하지 않는다. 필요한 경우 별도로 처리해야 한다. |
| CSS 처리 제한 | CSS 번들링은 실험적 기능이며, PostCSS 플러그인 등 고급 기능 지원이 제한적이다. |
언제 tsup을 선택해야 할까?
| 적합한 경우 | 부적합한 경우 |
|---|---|
| 간단한 타입스크립트 라이브러리 | 복잡한 CSS 처리가 필요한 경우 |
| 빠른 빌드 속도가 중요한 경우 | 다양한 브라우저 지원이 필요한 경우 |
| 설정을 최소화하고 싶은 경우 | 폴리필 자동 삽입이 필요한 경우 |
| Node.js 전용 패키지 | 레거시 브라우저 지원이 필수인 경우 |
결론: tsup은 간단하고 경량화된 타입스크립트 기반 패키지의 번들링에 적합한 도구다. 복잡한 설정 없이 빠르게 라이브러리를 번들링하고 싶을 때 좋은 선택이다.
9.1.3 구성 파일의 표준, cosmiconfig
cosmiconfig는 .eslintrc, .prettierrc.json, .stylelintrc.yml과 같은 구성 파일을 로드하고 파싱하는 것을 도와주는 도구다. 다양한 도구들이 일관된 방식으로 구성 파일을 처리할 수 있도록 표준화된 방법을 제공한다.
cosmiconfig의 주요 기능
| 기능 | 설명 |
|---|---|
| 다양한 파일 형식 지원 | .json, .yaml, .yml, .js, .ts, .cjs, .mjs 등 다양한 형식의 구성 파일을 자동으로 인식하고 파싱 |
| 파일 탐색 순서 | package.json의 특정 필드, .{name}rc, .{name}rc.json, .{name}rc.yaml, {name}.config.js 등 순서대로 탐색 |
| 디렉토리 순회 | 현재 디렉토리에서 시작해 상위 디렉토리로 올라가며 구성 파일을 찾음 |
| 캐싱 지원 | 동일한 파일을 반복해서 읽지 않도록 캐싱 기능 제공 |
사용 예시
import { cosmiconfig } from "cosmiconfig";
// 'myapp'이라는 이름으로 구성 파일 탐색
const explorer = cosmiconfig("myapp");
// 탐색 순서:
// 1. package.json의 "myapp" 필드
// 2. .myapprc
// 3. .myapprc.json
// 4. .myapprc.yaml
// 5. .myapprc.yml
// 6. .myapprc.js
// 7. .myapprc.ts
// 8. myapp.config.js
// 9. myapp.config.ts
const result = await explorer.search();
// result = { config: { ... }, filepath: '/path/to/.myapprc' }cosmiconfig을 사용하는 주요 도구들
- ESLint
- Prettier
- Stylelint
- Babel
- Jest
- Tailwind CSS
- PostCSS
9.1.4 성능 분석을 위한 도구, Tinybench
Tinybench는 자바스크립트 코드의 성능을 측정하기 위해 만들어진 경량 벤치마크 도구다.
Tinybench 등장 배경
기존에 널리 사용되던 Benchmark.js는 마지막 업데이트가 2017년으로 상당히 오래되었으며, 2024년 4월 12일 이후로는 더 이상 유지보수되지 않는다. 이에 따라 최근에는 Tinybench를 더욱 선호하는 추세다.
특히 Vitest가 벤치마크 테스트 기능을 추가하기 위해 Tinybench를 채택했다.
Vitest 벤치마크 기능 상태 (2026년 3월 기준)
Vitest의 벤치마크 기능은 v1.0 (2023년 12월) 릴리스 이후 안정화(stable) 상태가 되었다. 더 이상 실험적 기능이 아니며, 프로덕션 환경에서도 사용할 수 있다.
Tinybench vs Benchmark.js
| 항목 | Tinybench | Benchmark.js |
|---|---|---|
| 번들 크기 | ~2KB | ~40KB |
| 유지보수 | 활발함 | 중단됨 (2017~) |
| TypeScript 지원 | 네이티브 지원 | 타입 정의 별도 설치 |
| ESModule 지원 | 지원 | 미지원 |
| Vitest 통합 | 공식 지원 | 미지원 |
성능 측정 예시
직접 만든 isEmpty 함수와 lodash의 _isEmpty 함수의 성능을 비교해보자.
테스트 대상 함수
// 직접 구현한 isEmpty
function isEmpty(value: unknown): boolean {
if (value == null) return true;
if (Array.isArray(value) || typeof value === "string") {
return value.length === 0;
}
if (typeof value === "object") {
return Object.keys(value).length === 0;
}
return false;
}
// lodash의 isEmpty
import _isEmpty from "lodash/isEmpty";Tinybench를 이용한 벤치마크 코드
import { Bench } from "tinybench";
import _isEmpty from "lodash/isEmpty";
// 직접 구현한 isEmpty
function isEmpty(value: unknown): boolean {
if (value == null) return true;
if (Array.isArray(value) || typeof value === "string") {
return value.length === 0;
}
if (typeof value === "object") {
return Object.keys(value).length === 0;
}
return false;
}
// 테스트 데이터
const testData = {
emptyArray: [],
emptyObject: {},
emptyString: "",
filledArray: [1, 2, 3, 4, 5],
filledObject: { a: 1, b: 2, c: 3 },
};
const bench = new Bench({ time: 1000 }); // 1초 동안 반복 실행
bench
.add("isEmpty (직접 구현)", () => {
isEmpty(testData.emptyArray);
isEmpty(testData.emptyObject);
isEmpty(testData.emptyString);
isEmpty(testData.filledArray);
isEmpty(testData.filledObject);
})
.add("_isEmpty (lodash)", () => {
_isEmpty(testData.emptyArray);
_isEmpty(testData.emptyObject);
_isEmpty(testData.emptyString);
_isEmpty(testData.filledArray);
_isEmpty(testData.filledObject);
});
await bench.warmup(); // 워밍업
await bench.run(); // 벤치마크 실행
console.table(bench.table());벤치마크 결과 예시
┌─────────┬──────────────────────┬───────────────┬────────────────────┬──────────┬─────────┐
│ (index) │ Task Name │ ops/sec │ Average Time (ms) │ Margin │ Samples │
├─────────┼──────────────────────┼───────────────┼────────────────────┼──────────┼─────────┤
│ 0 │ isEmpty (직접 구현) │ '37,037,037' │ 0.02699 │ '±0.82%' │ 37037 │
│ 1 │ _isEmpty (lodash) │ '26,455,026' │ 0.03781 │ '±0.91%' │ 26455 │
└─────────┴──────────────────────┴───────────────┴────────────────────┴──────────┴─────────┘결과 분석
| 지표 | isEmpty (직접 구현) | _isEmpty (lodash) | 분석 |
|---|---|---|---|
| ops/sec (초당 실행 횟수) | 37,037,037 | 26,455,026 | isEmpty가 약 40% 더 많이 실행됨 |
| Average Time (평균 소요 시간) | 0.02699ms | 0.03781ms | isEmpty가 약 29% 더 빠름 |
| Margin (오차 범위) | ±0.82% | ±0.91% | 두 테스트 모두 1% 이내로 안정적 |
결론: 직접 구현한
isEmpty()함수가 lodash의_isEmpty보다 약 40% 정도 성능이 더 우수하다. 이는 lodash가 다양한 엣지 케이스와 호환성을 위한 추가 로직을 포함하고 있기 때문이다. 단순한 용도라면 직접 구현이 유리하고, 안정성과 엣지 케이스 처리가 중요하다면 lodash가 더 나은 선택일 수 있다.
Vitest를 이용한 벤치마크 테스트
Tinybench를 기반으로 동작하는 Vitest의 벤치마크 기능을 사용하면 테스트 코드와 벤치마크 코드를 일관된 방식으로 작성할 수 있다.
설정 (vitest.config.ts)
import { defineConfig } from "vitest/config";
export default defineConfig({
test: {
benchmark: {
include: ["**/*.bench.ts"],
reporters: ["default"],
},
},
});벤치마크 테스트 파일 (isEmpty.bench.ts)
import { bench, describe } from "vitest";
import _isEmpty from "lodash/isEmpty";
// 직접 구현한 isEmpty
function isEmpty(value: unknown): boolean {
if (value == null) return true;
if (Array.isArray(value) || typeof value === "string") {
return value.length === 0;
}
if (typeof value === "object") {
return Object.keys(value).length === 0;
}
return false;
}
// 테스트 데이터
const testData = {
emptyArray: [],
emptyObject: {},
emptyString: "",
filledArray: [1, 2, 3, 4, 5],
filledObject: { a: 1, b: 2, c: 3 },
};
describe("isEmpty 성능 비교", () => {
bench("isEmpty (직접 구현)", () => {
isEmpty(testData.emptyArray);
isEmpty(testData.emptyObject);
isEmpty(testData.emptyString);
isEmpty(testData.filledArray);
isEmpty(testData.filledObject);
});
bench("_isEmpty (lodash)", () => {
_isEmpty(testData.emptyArray);
_isEmpty(testData.emptyObject);
_isEmpty(testData.emptyString);
_isEmpty(testData.filledArray);
_isEmpty(testData.filledObject);
});
});실행 명령어
npx vitest bench실행 결과 예시
✓ isEmpty.bench.ts > isEmpty 성능 비교
name hz min max mean p75 p99 p995 p999 rme samples
· isEmpty (직접 구현) 37,123,456 0.0234 0.1523 0.0269 0.0256 0.0412 0.0523 0.0891 ±0.82% 18562 fastest
· _isEmpty (lodash) 26,543,210 0.0312 0.2134 0.0377 0.0356 0.0567 0.0723 0.1234 ±0.91% 13272
BENCH Summary
isEmpty (직접 구현) - isEmpty.bench.ts > isEmpty 성능 비교
1.40x faster than _isEmpty (lodash)Vitest 벤치마크 주요 옵션
bench(
"테스트명",
() => {
// 벤치마크 대상 코드
},
{
time: 1000, // 벤치마크 실행 시간 (ms)
iterations: 100, // 최소 반복 횟수
warmupTime: 100, // 워밍업 시간 (ms)
warmupIterations: 5, // 워밍업 반복 횟수
},
);벤치마크 작성 시 주의사항
| 주의사항 | 설명 |
|---|---|
| 워밍업 필수 | JIT 컴파일러 최적화를 위해 반드시 워밍업 단계를 거쳐야 함 |
| 일관된 환경 | CPU 부하, 메모리 상태 등이 결과에 영향을 미치므로 일관된 환경에서 테스트 |
| 충분한 샘플 수 | 통계적으로 유의미한 결과를 위해 충분한 샘플 수 확보 필요 |
| 마이크로벤치마크 한계 | 실제 애플리케이션 성능과 마이크로벤치마크 결과가 다를 수 있음 |
| Dead Code Elimination 주의 | 최적화로 인해 실행되지 않는 코드가 있을 수 있으므로 결과를 사용하도록 함 |
9.1.5 손쉬운 코드 마이그레이션을 도와주는 jscodeshift
패키지를 유지보수하다 보면 major 버전 업데이트는 피할 수 없는 숙명이다. 그러나 major 업데이트는 사용자 입장에서는 반가운 일이 아니다. 많은 사람이 메이저 버전을 업데이트하기를 주저하며, 충분한 공감대가 없는 주버전 업데이트는 사용자의 불만을 증폭시킬 뿐만 아니라 장기적으로는 사용자의 이탈로 이어진다.
Codemod란?
Codemod는 대규모 코드베이스를 자동으로 수정하는 도구를 의미한다. 주로 다음과 같은 작업을 수행할 때 사용한다.
| 용도 | 설명 |
|---|---|
| API 변경 | 함수명, 매개변수, 반환값 등의 변경을 자동으로 적용 |
| 타입/인터페이스 변경 | TypeScript 타입 정의 변경 사항을 일괄 적용 |
| 레거시 코드 모던화 | 옛날 문법을 최신 문법으로 변환 (예: var → const/let) |
| 코드 스타일 변경 | 일관된 코드 스타일로 변환 |
JavaScript 생태계에서 이러한 Codemod를 만들어주는 도구 중 하나가 jscodeshift다. Facebook에서 개발했으며, **AST(Abstract Syntax Tree, 추상 구문 트리)**를 기반으로 코드를 분석하고 변환한다.
jscodeshift 설치
npm install -g jscodeshift예시: React 컴포넌트 마이그레이션
React 16에서 17로 업그레이드할 때 React.createElement 대신 새로운 JSX 변환을 사용하도록 import 문을 변경하는 codemod 예시를 살펴보자.
변환 전
import React from "react";
function Button({ children }) {
return <button>{children}</button>;
}변환 후
// React를 명시적으로 import하지 않아도 됨 (React 17+)
function Button({ children }) {
return <button>{children}</button>;
}Codemod 작성 (remove-react-import.js)
// jscodeshift transform 파일
module.exports = function transformer(file, api) {
const j = api.jscodeshift;
const root = j(file.source);
// 'react'에서 React를 import하는 구문 찾기
const reactImports = root.find(j.ImportDeclaration, {
source: { value: "react" },
});
// JSX를 사용하는지 확인
const hasJSX = root.find(j.JSXElement).length > 0;
// React를 직접 참조하는 곳이 있는지 확인 (React.useState 등)
const hasReactReference =
root.find(j.MemberExpression, {
object: { name: "React" },
}).length > 0;
// JSX만 사용하고 React를 직접 참조하지 않는 경우 import 제거
if (hasJSX && !hasReactReference) {
reactImports
.filter(path => {
const specifiers = path.node.specifiers;
// default import만 있는 경우 (import React from 'react')
return specifiers.length === 1 && specifiers[0].type === "ImportDefaultSpecifier";
})
.remove();
}
return root.toSource();
};Codemod 실행
# 단일 파일에 적용
jscodeshift -t remove-react-import.js src/components/Button.jsx
# 디렉토리 전체에 적용
jscodeshift -t remove-react-import.js src/
# dry-run (실제 변경 없이 미리보기)
jscodeshift -t remove-react-import.js src/ --dry
# TypeScript 파일 지원
jscodeshift -t remove-react-import.js src/ --parser=tsx실전 예시: PropTypes를 TypeScript로 변환
// propTypes-to-typescript.js
module.exports = function transformer(file, api) {
const j = api.jscodeshift;
const root = j(file.source);
// PropTypes import 찾기
const propTypesImport = root.find(j.ImportDeclaration, {
source: { value: "prop-types" },
});
// 컴포넌트의 propTypes 정의 찾기
const propTypesAssignments = root.find(j.AssignmentExpression, {
left: {
property: { name: "propTypes" },
},
});
// PropTypes를 TypeScript interface로 변환하는 로직
// ... (복잡한 변환 로직)
// PropTypes import 제거
propTypesImport.remove();
// propTypes 할당문 제거
propTypesAssignments.remove();
return root.toSource();
};jscodeshift의 주요 API
| API | 설명 |
|---|---|
j(source) | 소스 코드를 AST로 파싱 |
.find(type, filter) | 특정 노드 타입 찾기 |
.filter(callback) | 조건에 맞는 노드 필터링 |
.replaceWith(node) | 노드 교체 |
.insertBefore(node) | 노드 앞에 삽입 |
.insertAfter(node) | 노드 뒤에 삽입 |
.remove() | 노드 제거 |
.toSource() | AST를 소스 코드로 변환 |
유용한 Codemod 리소스
- react-codemod: React 팀에서 제공하는 공식 codemod 모음
- next-codemod: Next.js 버전 업그레이드를 위한 codemod
- AST Explorer (https://astexplorer.net): AST 구조를 시각적으로 확인할 수 있는 도구
9.2 패키지 개발에 도움이 되는 팁
프론트엔드 앱 개발에도 유용한가?
이 섹션의 팁들은 패키지(라이브러리) 개발에 초점이 맞춰져 있지만, 프론트엔드 애플리케이션 개발에도 상당 부분 적용 가능하다.
팁 패키지 개발 앱 개발 ESModule 사용 필수 권장 (대부분의 번들러가 기본 지원) 트리 셰이킹 최적화 매우 중요 중요 (번들 크기 최적화) 배럴 파일 최소화 필수 권장 (빌드 속도 및 번들 크기 개선) exports 필드 필수 불필요 (내부 모듈 구조) sideEffects 설정 필수 유용 (CSS 등 사이드 이펙트 명시) 특히 트리 셰이킹 최적화와 배럴 파일 관리는 대규모 프론트엔드 앱에서 빌드 시간과 번들 크기에 직접적인 영향을 미치므로 알아두면 좋다.
9.2.1 선택이 아닌 필수, ESModule
브라우저 환경에 최적화된 ESModule
ESModule은 브라우저 환경에서 다음과 같은 장점을 제공한다.
| 장점 | 설명 |
|---|---|
| 정적 분석 가능 | 모듈을 빌드 시점에 분석해서 트리 셰이킹과 캐싱 등 다양한 최적화를 통해 빌드 결과물을 최적화할 수 있다. |
| 비동기 로딩 | 비동기로 모듈을 불러오므로 네트워크 지연을 줄이고 로딩 속도를 개선할 수 있다. |
| 불변성 | import, export 키워드를 사용해 모듈을 불러오고 내보내는데, 이는 재정의가 불가능하다. 이로 인해 더 안정적인 코드 동작이 보장된다. |
이중 패키지(Dual Package)의 어려움
CommonJS와 ESModule을 동시에 지원하는 이중 패키지는 다음과 같은 문제가 있다.
- 패키지 크기 증가: 두 가지 형식의 번들을 모두 포함해야 함
- Dual Package Hazard: 동일한 패키지의 CJS/ESM 버전이 동시에 로드될 수 있음
- 유지보수 복잡성: 두 모듈 시스템에 대한 테스트와 설정 관리 필요
정말 필요한 경우가 아니라면 이중 패키지는 지양하는 것이 좋다. 단일 모듈 시스템(ESModule)으로의 전환이 개발자와 사용자 모두에게 더 나은 선택이 될 것이다.
ESModule 패키지에서의 올바른 트리 셰이킹 방법
패키지의 최적화를 위해 배럴 파일을 제거하고 exports 필드를 통해 모듈을 명시적으로 분리할 것을 권장한다.
배럴 파일의 문제점
// src/index.ts (배럴 파일)
export * from "./Button";
export * from "./Input";
export * from "./Modal";
export * from "./utils";
// ... 수십 개의 re-export배럴 파일은 편리함을 제공하지만 다음과 같은 문제가 있다.
- 내부적으로 불필요한 의존성을 추가
- 사용하지 않는 코드까지 포함되어 번들 크기 증가
- 트리 셰이킹을 방해
권장 방식: exports 필드 활용
{
"name": "my-ui-library",
"exports": {
".": "./dist/index.js",
"./button": "./dist/components/Button.js",
"./input": "./dist/components/Input.js",
"./modal": "./dist/components/Modal.js",
"./utils": "./dist/utils/index.js"
}
}사용자가 필요로 하는 모듈만 가져올 수 있도록 구조를 명확하게 설계할 수 있다.
// 사용자 코드
import { Button } from "my-ui-library/button"; // Button만 번들에 포함반드시 배럴 파일을 사용해야 한다면?
| 권장 사항 | 설명 |
|---|---|
| 명시적 export | export * from 대신 특정 모듈만 명시적으로 내보내기 |
| sideEffects 설정 | package.json에서 sideEffects: false 또는 사이드 이펙트가 있는 파일만 명시 |
| 진입점 분리 | 배럴 파일을 제공하더라도 나눌 수 있는 파일은 최대한 진입점을 분리해 제공 |
{
"sideEffects": false
}
// 또는 CSS처럼 사이드 이펙트가 있는 파일만 명시
{
"sideEffects": ["*.css", "*.scss"]
}9.2.2 package.json 올바르게 작성하기
main과 exports
CLI 패키지처럼 모듈을 외부로 노출하지 않는 경우를 제외하고는 두 필드를 함께 설정하는 것을 권장한다.
| 필드 | 용도 | 지원 환경 |
|---|---|---|
| main | 패키지의 기본 진입점 | Node.js (모든 버전), Webpack, Rollup 등 번들러 |
| exports | 세밀한 진입점 제어 | Node.js 12.7+, 최신 번들러 |
main 필드
{
"main": "./dist/index.js"
}- Node.js와 Webpack, Rollup 같은 번들러에서 패키지의 진입점으로 사용된다.
- 오래된 Node.js 버전이나 CommonJS 환경에서는 이
main필드를 기준으로 패키지를 해석한다. - 패키지의 하위 호환성을 위해 main을 명시하는 것이 좋다.
exports 필드
{
"exports": {
".": {
"import": "./dist/index.mjs",
"require": "./dist/index.cjs",
"types": "./dist/index.d.ts"
},
"./utils": {
"import": "./dist/utils.mjs",
"require": "./dist/utils.cjs",
"types": "./dist/utils.d.ts"
},
"./package.json": "./package.json"
}
}- CommonJS뿐만 아니라 ESModule 및 최신 Node.js 환경에서 모듈의 외부로 노출되는 파일 및 경로를 정의한다.
main보다 세밀하고 강력한 제어를 제공한다.- 패키지의 하위 경로를 지정하거나, 모듈 시스템 혹은 브라우저/서버 환경에 따라 진입 경로를 나누어 정의할 수 있다.
조건부 exports
{
"exports": {
".": {
"browser": "./dist/browser.js",
"node": "./dist/node.js",
"import": "./dist/index.mjs",
"require": "./dist/index.cjs",
"default": "./dist/index.js"
}
}
}| 조건 | 설명 |
|---|---|
import | ESModule로 import할 때 |
require | CommonJS로 require할 때 |
node | Node.js 환경 |
browser | 브라우저 환경 (번들러가 해석) |
types | TypeScript 타입 정의 |
default | 위 조건에 해당하지 않을 때 (항상 마지막에 위치) |
packageManager와 engines
프로젝트에서 사용해야 하는 패키지 관리 도구와 Node.js 환경을 명시적으로 설정하는 데 사용되며, 프로젝트 개발의 일관성을 지키는 데 유용하다.
{
"packageManager": "pnpm@8.15.0",
"engines": {
"node": ">=18.0.0",
"npm": ">=9.0.0"
}
}| 필드 | 설명 |
|---|---|
| packageManager | 프로젝트에서 사용 중인 패키지 관리 도구와 버전을 명시해 다른 개발자가 프로젝트를 복제하거나 설치할 때 동일한 패키지 관리 도구를 사용하도록 요구한다. Corepack과 함께 사용하면 자동으로 올바른 버전이 설치된다. |
| engines | 패키지 관리 도구뿐만 아니라 프로젝트에서 필요로 하는 Node.js 및 기타 도구의 버전을 명시할 수 있다. npm install 시 버전이 맞지 않으면 경고를 표시한다. |
추가적으로 고려해야 할 필드
| 필드 | 설명 |
|---|---|
| type | "module" 또는 "commonjs". 패키지의 모듈 시스템을 지정 |
| files | npm에 배포할 때 포함할 파일/폴더 목록. .npmignore보다 명시적 |
| keywords | npm 검색에 사용되는 키워드 배열 |
| repository | 소스 코드 저장소 URL |
| $schema | JSON Schema를 지정해 IDE에서 자동완성 지원 ("https://json.schemastore.org/package.json") |
9.2.3 올바른 트랜스파일과 폴리필 적용하기
browserslist 설정
browserslist는 지원할 브라우저 범위를 정의하는 표준 설정이다. Babel, Autoprefixer, PostCSS 등 여러 도구가 이 설정을 참조한다.
{
"browserslist": ["> 1%", "last 2 versions", "not dead", "not ie 11"]
}모든 패키지가 트랜스파일과 폴리필이 필요한 것은 아니다
| 고려사항 | 설명 |
|---|---|
| 모던 브라우저 네이티브 지원 | ES2020+ 문법은 대부분의 모던 브라우저에서 이미 지원된다. 불필요한 트랜스파일은 번들 크기만 증가시킨다. |
| 사용자 애플리케이션과의 충돌 | 패키지에 포함된 폴리필이 사용자 앱의 폴리필과 충돌할 수 있다. |
| 소수 브라우저를 위한 오버헤드 | 1% 미만의 브라우저를 지원하기 위해 모든 사용자에게 추가 코드를 전달하는 것은 비효율적이다. |
권장 접근 방식
- 목표 지원 환경 확인: 실제 사용자 통계를 기반으로 지원 범위 결정
- 패키지 크기와 성능 최적화: 트랜스파일 범위를 최소화
- 기능 테스트: 지원 브라우저에서 실제 동작 확인
팁: 라이브러리는 트랜스파일하지 않은 ESModule 소스를 제공하고, 최종 애플리케이션에서 필요에 따라 트랜스파일하도록 하는 것이 현대적인 접근 방식이다.
9.2.4 dependencies는 신중하게 추가
정말 필요한 패키지만 포함하기
| 원칙 | 설명 |
|---|---|
| 핵심 기능에 필요한 의존성만 포함 | 부가적인 기능을 위한 의존성은 peerDependencies나 optionalDependencies로 분리 |
| 간단한 기능은 직접 구현 | 작은 유틸리티 함수를 위해 무거운 라이브러리를 추가하지 않기 |
| 적합한 범주로 패키지 구분 | dependencies, devDependencies, peerDependencies를 명확히 구분 |
추가될 패키지의 모듈 시스템 검토
- ESModule만 지원하는 패키지를 CommonJS 프로젝트에서 사용할 수 있는지 확인
package.json의type필드와exports필드 확인- 필요시 동적 import 사용 고려
안전한 패키지인지 확인
| 점검 항목 | 방법 |
|---|---|
| 패키지 출처 확인 | npm 페이지에서 저자, 저장소, 다운로드 수 확인 |
| 보안 취약점 점검 | npm audit, Snyk, Socket.dev 등 도구 활용 |
| 하위 의존성 보안 점검 | 전이적 의존성(transitive dependencies)도 검토 |
| 권한 최소화 | postinstall 스크립트 등 의심스러운 동작 확인 |
| 최신 버전 사용 | 정기적인 업데이트로 보안 패치 적용 |
최적화된 패키지 선택
# 패키지 크기 확인
npx package-size lodash lodash-es ramda
# 의존성 트리 확인
npm ls --all
# 번들 크기 분석
npx bundlephobia lodash| 확인 항목 | 설명 |
|---|---|
| 의존성 체인 확인 | 하나의 패키지가 수십 개의 하위 의존성을 가져오는지 확인 |
| 모듈 최적화 확인 | 트리 셰이킹이 가능한지, sideEffects: false 설정 여부 |
| 경량화된 패키지 선택 | 동일 기능의 경량 대안이 있는지 검토 (예: moment → dayjs) |
의존성 선택 체크리스트
□ 직접 구현 가능한가? (100줄 이하라면 직접 구현 고려)
□ 모듈 시스템이 호환되는가? (ESM/CJS)
□ 안전한 패키지인가? (출처, 유지보수 상태, 보안)
□ 최적화됐는가? (번들 크기, 트리 셰이킹)
□ 라이선스가 호환되는가? (MIT, Apache 등)9.2.5 코드에 신뢰를 주는 테스트 코드와 벤치마크 테스트
| 테스트 유형 | 설명 | 도구 예시 |
|---|---|---|
| 단위 테스트 | 개별 함수나 모듈의 동작을 검증 | Jest, Vitest, Mocha |
| 통합 테스트 | 여러 모듈이 함께 동작하는지 검증 | Jest, Vitest |
| 성능 테스트/벤치마크 | 코드의 성능을 측정하고 비교 | Tinybench, Vitest bench |
| E2E 테스트 | 실제 사용 환경에서 전체 흐름 검증 | Playwright, Cypress |
테스트 커버리지 목표
- 핵심 기능: 90% 이상
- 엣지 케이스: 주요 케이스 포함
- 에러 처리: 예외 상황 테스트
9.2.6 올바른 문서 작성법
좋은 문서는 패키지의 채택률과 기여도에 직접적인 영향을 미친다.
| 문서 | 역할 |
|---|---|
| README.md | 프로젝트의 첫인상을 책임지는 문서. 라이브러리의 목적, 주요 기능, 설치 방법, 기본 사용법을 간결하고 직관적으로 작성 |
| CONTRIBUTING.md | 기여자를 위한 가이드. 기여 방법, 코드 스타일, 테스트 방법, 이슈 작성 가이드 등을 포함 |
| CHANGELOG.md | 변경 사항을 기록하는 문서. 새롭게 출시되는 버전마다 추가된 기능, 수정된 버그, 중요한 변경 사항을 명확히 작성 |
README.md
필수 구성 요소
- 패키지의 목적: 무엇을 하는 라이브러리인지 한 문장으로 설명
- 설치 방법: npm, yarn, pnpm 등 다양한 방법 제공
- 사용법: 기본적인 코드 예시
- API 문서: 주요 함수/클래스 설명 (또는 별도 문서 링크)
- 기여/문제 보고: 이슈 템플릿, 기여 가이드 링크
- 라이선스: 명확한 라이선스 표기
배지(Badges) 활용
| 카테고리 | 배지 예시 |
|---|---|
| 패키지 정보 | npm 버전, 다운로드 수, 라이선스, 번들 크기 |
| 현재 상태 | CI/CD 상태, 테스트 커버리지, 의존성 상태 |
| 기여 독려 | 기여자 수, "contributions welcome" 배지 |
| 커뮤니티 | Discord, Slack 등 외부 지원 채널 |
   CONTRIBUTING.md
포함해야 할 내용
- 개발 환경 설정: 프로젝트 클론, 의존성 설치, 빌드 방법
- 코드 스타일: ESLint/Prettier 설정, 네이밍 컨벤션
- 테스트 방법: 테스트 실행 명령어, 커버리지 요구사항
- PR 가이드라인: 브랜치 전략, 커밋 메시지 형식
- 행동 강령(Code of Conduct): 커뮤니티 규칙 (선택사항이나 권장)
CHANGELOG.md
Keep a Changelog란?
Keep a Changelog는 변경 로그 작성을 위한 표준 형식이다. 2014년 Olivier Lacan이 제안했으며, 많은 오픈소스 프로젝트에서 채택하고 있다.
핵심 원칙
- 사람을 위해 작성한다: Git 로그가 아닌, 사용자가 이해할 수 있는 언어로 작성
- 최신 버전이 맨 위에: 역순으로 정렬
- 버전별로 그룹화: 각 버전에 릴리스 날짜 포함
- 변경 유형별로 분류: 일관된 카테고리 사용
변경 유형 카테고리
| 카테고리 | 설명 |
|---|---|
Added | 새로운 기능 추가 |
Changed | 기존 기능의 변경 |
Deprecated | 곧 삭제될 기능 (사용 중단 예정) |
Removed | 삭제된 기능 |
Fixed | 버그 수정 |
Security | 보안 취약점 수정 |
CHANGELOG.md 템플릿
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
### Added
- 새로운 기능 설명
### Changed
- 변경된 기능 설명
## [1.2.0] - 2026-03-20
### Added
- `useAsync` 훅 추가 (#123)
- TypeScript 5.0 지원
### Changed
- `fetchData` 함수의 기본 타임아웃을 5초에서 10초로 변경
### Fixed
- Safari에서 발생하던 스크롤 버그 수정 (#456)
## [1.1.0] - 2026-02-15
### Added
- SSR 지원 추가
- `retry` 옵션 추가
### Deprecated
- `legacyMode` 옵션은 다음 메이저 버전에서 삭제 예정
### Security
- XSS 취약점 수정 (CVE-2026-XXXX)
## [1.0.0] - 2026-01-01
### Added
- 최초 릴리스
- 기본 API 제공 (`create`, `update`, `delete`)
- React 18 지원
[Unreleased]: https://github.com/user/repo/compare/v1.2.0...HEAD
[1.2.0]: https://github.com/user/repo/compare/v1.1.0...v1.2.0
[1.1.0]: https://github.com/user/repo/compare/v1.0.0...v1.1.0
[1.0.0]: https://github.com/user/repo/releases/tag/v1.0.0자동화 도구
| 도구 | 설명 |
|---|---|
| conventional-changelog | Conventional Commits 기반으로 CHANGELOG 자동 생성 |
| release-it | 버전 관리, CHANGELOG 생성, 릴리스 자동화 |
| changesets | 모노레포에서 버전 관리와 CHANGELOG 생성 |
| semantic-release | CI/CD에서 완전 자동화된 릴리스 |
# conventional-changelog 사용 예시
npx conventional-changelog -p angular -i CHANGELOG.md -s