발단
제가 자주 사용하는 유틸리티 함수들이 있습니다.
기존에는 단순히 코드를 복사 & 붙여넣기하는데요, 어느날 “해당 코드를 배포해서 사용하시진 않으시나요?”라는 질문을 받았습니다. 많은 사람들이 사용해줄 지는 모르겠지만 패키지를 만들어 배포하는 경험을 해보는 것이 좋을 것 같아서 배포를 해보았습니다.
디렉토리 설정
우선 제가 기존에 가지고 있던 유틸리티 함수 파일이 있습니다.
// common-utils.ts export function isEmptyArray() { ... } export function isNotEmptyArray() { ... }
저는
@einere/common-utils
라는 패키지명으로 배포하기위해, 다음과 같이 폴더를 만들었습니다.common-utils - src - index.ts // 모듈 진입점 - common-utils.ts // 모듈 정의 파일 - .gitignore // GitHub 에 올리지 않을 파일들을 명시 - .npmignore // NPM 에 올리지 않을 파일들을 명시
package.json 및 README.md 설정
그리고 디렉토리 내에서 CLI를 이용해
pakcage.json
설정을 해줍니다.$ npm init --scope=einere
--scope
에 해당하는 값은 조직의 이름입니다. 기본적인 정보를 입력해주면, 다음과 같이
package.json
파일이 만들어집니다. 여기서, 필요한 추가 속성을 추가해줍니다.{ "name": "@einere/common-utils", "version": "1.0.0", "description": "Einere's common utilities", "type": "module", // 해당 패키지가 ESM을 사용하도록 지정합니다. "main": "./dist/index.js", // 패키지의 진입점입니다. "types": "./dist/index.d.ts", // 패키지의 타입 정의 파일의 위치입니다. "files": [ "dist" // npm 배포 시 'dist' 폴더의 내용만 포함합니다. ], "exports": { ".": { "types": "./dist/index.d.ts", "default": "./dist/index.js" } }, "scripts": { "build": "tsc", "prepublishOnly": "npm run build", // npm에 배포하기 직전, 빌드를 합니다. }, "publishConfig": { "access": "public" // NPM에 배포하기 위해서는 공개로 지정해야 합니다. }, "devDependencies": { "typescript": "^5.8.3" }, // ... }
그리고 루트 경로에
README.md
파일을 만들어줍니다. 내용은 자유롭게 채워주세요.진입점 구현
진입점인
index.ts
를 구현해줍니다.// index.ts export * from './common-utils.js';
노드 환경에서 모듈을 가져올 때 확장자를 명시하지 않으면 모듈을 제대로 가져오지 못합니다. 그래서 위와 같이 파일의 확장자를 명시해줍니다.
그런데 왜
.js
일까요? 그 이유는 컴파일 결과물 때문입니다. 만약 확장자를 .ts
로 해버리면 어떻게 될까요?// 빌드 결과물 dist - index.d.ts - index.js - common-utils.d.ts - common-utils.js
// dist/index.js export * from "./common-utils.js"; // 이 경우에는 제대로 유틸리티 함수를 가져옵니다. // dist/index.d.ts // d.ts 파일은 TSC에 의해 확장자 변경이 이루어지지 않습니다. // 결국, 존재하지 않는 ts 파일을 가져오려고 합니다. export * from "./common-utils.ts";
dist/index.js
와 dist/index.d.ts
둘 다 제대로 동작하게끔 하기 위해 .js
확장자를 사용한 것 입니다.만약 이게 싫다면, rollup과 같은 번들러를 같이 사용해야 합니다.
Typescript 설정
이제 TS 환경을 위해 typescript 를 개발 의존성으로 설치한 후,
tsconfig.json
파일을 만들어줍니다.$ npm install -D typescript
{ // 컴파일 과정과 그 결과물에 대한 설정들입니다. "compilerOptions": { "target": "ESNext", // 결과물의 JS 버전을 최신 버전으로 지정합니다. "module": "ESNext", // 결과물의 모듈 시스템을 ESM로 지정합니다. "moduleResolution": "node10", // 컴파일 시, 모듈 해석 방식을 지정합니다. "esModuleInterop": true, // CJS 모듈을 ESM 방식으로 import 할 수 있도록 돕습니다. "forceConsistentCasingInFileNames": true, "strict": true, // 엄격한 타입 체크 "skipLibCheck": true, // 라이브러리의 타입 체크를 건너뜁니다. "declaration": true, // .d.ts 타입 정의 파일을 생성합니다. "outDir": "./dist", // 빌드 결과물이 저장될 디렉토리를 지정합니다. "rootDir": "./src" }, "include": [ "src/**/*.ts" // src 폴더 내의 모든 .ts 파일을 컴파일 대상에 포함합니다. ], "exclude": [ "node_modules", "dist" // dist 폴더는 컴파일 대상에서 제외합니다. ] }
저는 패키지를 ESM로 지정하고 싶어서
module
속성은 ESNext
로 설정하고 moduleResolution
속성은 node10
으로 설정했습니다.두 속성을 각각
NodeNext
, nodenext
로 해도 무방한 것 같습니다.빌드
이제 필요한 패키지를 설치하고 빌드를 해줍니다.
$ npm install && npm run build
그러면 위에서 언급한 것과 동일하게, 다음과 같이
dist
폴더가 만들어집니다.dist - index.d.ts - index.js - common-utils.d.ts - common-utils.js
// dist/index.js export * from './common-utils.js'; // dist/index.d.ts export * from "./common-utils.js"; // js 파일을 가져오게끔 되어있음에 유의!
로컬 테스트
만들어진 패키지가 잘 동작하는지 테스트하기 위해
common-utils
폴더 밖에 새로운 폴더를 만들어줍니다.$ cd .. && mkdir common-utils-playground && cd common-utils-playground
그리고
package.json
파일을 새로 만들어줍니다.// common-utils-playground/package.json { "name": "common-utils-playground", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "build": "npx tsc", "start": "node dist/index.js" }, "devDependencies": { "typescript": "^5.8.3" }, "type": "module" // ESM을 사용합니다. }
TS를 사용하기 위해
tsconfig.json
도 간단하게 만들어줍니다. 모듈 해결 방법으로는 node10
를 지정해줍니다.{ "compilerOptions": { "target": "ESNext", "module": "ESNext", "moduleResolution": "node10", "esModuleInterop": true, "forceConsistentCasingInFileNames": true, "strict": true, "skipLibCheck": true, "rootDir": "." }, "include": ["./**/*.ts"], "exclude": ["node_modules"] }
이제 common-utils 폴더로 돌아가 다음 명령어를 실행하여 심볼릭 링크를 만들어줍니다.
$ cd ../common-utils $ npm link
다시 common-utils-playground 로 돌아와서 @einere/common-utils 패키지를 연결해줍니다.
$ cd ../common-utils-playground $ npm install $ npm link @einere/common-utils
그리고 간단한 테스트 코드를 작성해줍니다.
// index.ts import { isEmptyArray, isNotEmptyArray } from '@einere/common-utils'; if(isEmptyArray([])) { console.debug('foo!') } if(isNotEmptyArray([1, 2])) { console.debug('bar!') }
빌드 후, 결과물을 node로 실행해서 결과를 확인합니다.
$ npm run build && npm run start foo! bar!
잘 되네요!
NPM에 배포하기
이제 NPM에 배포만 하면 됩니다.
common-utils 폴더로 돌아와서 npm에 로그인한 뒤, 배포합니다.
$ npm login $ npm publish
다음과 같은 프롬프트가 나오며 인증에 성공하면 정상적으로 배포됩니다.
npm notice npm notice 📦 @einere/common-utils@1.0.0 npm notice === Tarball Contents === npm notice 0B README.md npm notice 2.3kB dist/common-utils.d.ts npm notice 4.3kB dist/common-utils.js npm notice 35B dist/index.d.ts npm notice 35B dist/index.js npm notice 906B package.json npm notice === Tarball Details === npm notice name: @einere/common-utils npm notice version: 1.0.0 npm notice filename: einere-common-utils-1.0.0.tgz npm notice package size: 2.6 kB npm notice unpacked size: 7.5 kB // ... Press ENTER to open in the browser... + @einere/common-utils@1.0.0
마지막으로 GitHub 레포지토리를 생성하고, remote 연동을 해주면 끝입니다.
시험삼아 제 블로그 코드에서 테스트 해 보니 잘 되네요. (Next.js SSR 환경)
import { isEmptyArray } from "@einere/common-utils"; export default function HomePage() { if (isEmptyArray([])) { console.log("foo"); } // ... }

배포된 패키지는 https://www.npmjs.com/package/@einere/common-utils 에서 확인하실 수 있습니다.
참고
만약 ESM 뿐 만 아니라, CJS도 지원하고 싶다면 위 글을 참고하면 좋을 것 같습니다.
후기
이렇게 처음으로 공개 패키지를 배포해보았습니다.
처음에는 TS 및 검증용 Node 환경 세팅, Common JS 모듈까지 지원하느라 삽질을 엄청나게 했는데요, 결국 원점으로 돌아가서 ESM만 지원하는 것으로 결정하고 이에 맞춰 차근차근 다시 설정해나가니 꽤나 수월하게 작업이 진행되었습니다.
이번 경험을 통해 TS의 몇몇 컴파일러 옵션들과 노드의 모듈 시스템에 대해 더 잘 알게 되었습니다. 특히나 컴파일된
.d.ts
파일 내부의 가져오기 구문도 신경써야 한다는 것도 알았네요…한가지 아쉬운 점은 소스 코드를 작성할 때 컴파일 결과물을 미리 예측하여, 일부러
.js
를 붙여야 한다는 게 조금 거슬리네요.. 이 문제를 해결하려면 번들러를 도입해야 할 것 같은데, 요건 나중에 해볼까 합니다.여러분들도 나만의 작은 패키지를 배포해보시는건 어떨까요? 😎