node 라이브러리 npm 배포

이경택·2025년 3월 24일
1

main

개요

파이썬 프로젝트를 진행하면서 csv 파일을 만들고 그 파일을 읽고 파싱해서 원하는 데이터를 추출해내는 로직이 있었다.
라이브러리를 배포해보고 싶은 것이 이 프로젝트를 만든 가장 큰 이유이고 파이썬에서 사용했던 로직을 참고삼아 라이브러리로 만들어 보았고 그 과정을 정리해보았다.

프로젝트 생성 및 초기화

mkdir parse-csv-file
cd parse-csv-file

package.json 생성

npm init -y

  • -y 플래그는 프로젝트에 대한 기본 양식 설정을 다 생략하고 default로 만들겠다는 뜻, yes 라는 뜻
{
  "name": "parse-csv-file",
  "version": "1.0.0",
  "main": "index.js",
  "type": "module",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "description": ""
}
  • name - NPM에 배포할 때 사용되는 이름
  • version - 배포할 때 사용되는 버전, Semantic Version 체계 사용
  • type - 값으로module / commonjs 를 사용할 수 있으며 불러오기/내보내기 옵션 설정할 때 사용
    • commonjs = CJS - require/module.exports를 사용하고 동기적으로 동작

    • module = ESM - import/export를 사용하고 비동기적으로 동작 (Top-level Await 지원)

      ESM에서는 CJS를 import 할 수 있지만 CJS에서는 Top-level Await을 지원하지 않기 때문에 ESM을 require 할 수 없다

  • main - 라이브러리를 사용할 때 기본 진입점. index.js 를 기본 진입점으로 사용
  • license - 라이브러리의 라이센스를 의미
  • keywords - npm에 보여질 키워드
  • author - 만든이
  • description - 라이브러리 설명

exports 필드 명시하기

exports 필드는 node 12버전에 추가된 필드로 cjsesm 을 동시에 지원

{
  "name": "parse-csv-file",
  "version": "1.0.0",
  "main": "index.js",
  "type": "module",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "description": ""
  "exports": {
    "./lib": {
	    "types": "./dist/lib/index.d.ts", // typescript를 사용하는 경우 사용될 파일을 명시한 conditional 필드
	    "require": "./dist/lib/index.cjs", // cjs 환경에서 사용될 파일을 명시한 conditional 필드
	    "import": "./dist/lib/index.js" // esm 환경에서 사용될 파일을 명시한 conditional 필드
	  }
	}
}

exports 필드는 기존에 main 필드를 참고해서 라이브러리를 불러왔었는데 esm이나 commonjs 두 개의 방식을 모두 지원하는 경우에는 main에서 두 파일을 모두 적을 수가 없어 나온 필드
exports가 있으면 main, module, types 등보다 exports 필드를 우선시 함.

exports 필드는 모두 . 으로 시작되는 상대 경로로 작성

해당 경로는 라이브러리의 subPath 를 의미하고 그 안의 객체에는 import, require, types, default 같은 conditional 필드 존재

현재의 경우에는 최상위 경로에는 아무것도 없고 lib 폴더 내부에 코드가 존재하므로 ./lib 으로 표시

수정
exports 문에 ./lib 경로로 export를 해줘도 실제 빌드해서 만드는 파일은 dist파일 바로 아래 경로에 있기 때문에 exports를 ./ 경로로 수정

타입스크립트 지원

npm i typescript @types/node -D

typescript와 node의 타입을 지원해주는 @types/node 설치

tsconfig.json 파일 생성

{
  "compilerOptions": {
    "target": "es6" /* 최신 브라우저는 es6을 대부분 지원한다. */,
    "module": "ES6" /* 모듈 시스템을 지정한다. */,
    "lib": ["es5", "es6", "dom"] /* 타입스크립트가 어떤 버전의 JS의 빌트인 API를 사용할 건지에 대한 것을 명시해 준다. */,
    "declaration": true /* 타입스크립트가 자동으로 타입정의 (d.ts) 파일을 생성해 준다. */,
    "outDir": "dist" /* 컴파일된 결과물을 어디에 저장할지에 대한 것을 명시해 준다. */,
    "strict": true /* 타입스크립트의 엄격한 모드를 활성화한다. */
    "moduleResolution": "bundler" /* 모듈 해석 방법을 지정한다. */
  },
  "include": ["src/lib/index.ts"] /* 컴파일할 대상을 명시해 준다. */
}

이 후 npm run tsc 로 컴파일을 해도 되지만 build script로 작성

{
  "name": "parse-csv-file",
  "version": "1.0.0",
  "main": "index.js",
  "type": "module",
  "scripts": {
	  "build": "npm run build:tsc",
    "build:tsc": "npm run tsc"
    "tsc": "tsc" // npm run tsc 가 동작하지 않을 땐 직접 tsc 입력
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "description": ""
  "exports": {
    "./lib": {
	    "types": "./dist/lib/index.d.ts", // typescript를 사용하는 경우 사용될 파일을 명시한 conditional 필드
	    "require": "./src/index.cjs", // cjs 환경에서 사용될 파일을 명시한 conditional 필드
			"import": "./src/index.js", // esm 환경에서 사용될 파일을 명시한 conditional 필드
	  }
	} 
}

npm run build 를 실행하면 tsconfig.json에 명시해 준 outDir 경로에 dist 폴더가 생성되고 dist 폴더 안에 index.jsindex.d.ts 파일이 생성 됨

{
  "name": "parse-csv-file",
  "version": "1.0.4",
  "main": "dist/index.js",
  "type": "module",
  "scripts": {
    "build": "npm run build:tsc",
    "build:tsc": "npm run tsc",
    "tsc": "tsc"
  },
  "keywords": [],
  "author": "",
  "license": "MIT",
  "exports": {
    "./lib": {
      "types": "./dist/lib/index.d.ts",
      "require": "./dist/lib/index.cjs",
      "import": "./dist/lib/index.js"
    }
  }
}

폴더와 컴파일 된 결과물들에 맞춰서 main, types, exports 필드도 변경

컴파일 된 결과물인 dist/index.jsmain에 넣어줌

exports 필드 안에는 types 필드를 추가해 index.d.ts 파일 명시

import 필드에는 ./dist/lib/index.js 파일 명시

esbuild 파일 추가

라이브러리 코드를 수정할 때마다 esm, cjs 를 둘 다 고칠 수는 없기 때문에 esbuild 라이브러리를 이용해 한 번에 컴파일

npm i esbuild -D

build.js 파일 작성

// build.js
import esbuild from "esbuild";
// 공통으로 사용할 옵션들
// https://esbuild.github.io/api/#build 에서 다양한 옵션들을 확인할 수 있다.
const baseConfig = {
  entryPoints: ["src/lib/index.ts"], // 컴파일할 파일
  outdir: "dist", // 컴파일된 파일이 저장될 경로
  bundle: true, // 번들링 여부
  sourcemap: true, // 소스맵 생성 여부
  platform: "node" // 코드 내부에서 fs 모듈을 불러와 쓰는데 명시 안하면 빌드 시 에러
};
Promise.all([
  // 한 번은 cjs
  esbuild.build({
    ...baseConfig,
    format: "cjs",
    outExtension: {
      ".js": ".cjs",
    },
  }),
  // 한 번은 esm
  esbuild.build({
    ...baseConfig,
    format: "esm",
  }),
]).catch(() => {
  console.log("Build failed");
  process.exit(1);
});

커맨드 라인에서도 사용 가능하지만 스크립트 파일로 작성도 가능.

다양한 옵션들을 조정하기 위해 build.js 파일 생성 및 옵션 설정

{
  "scripts": {
    "prepack": "npm run build",
    "build": "npm run clean && npm run build:tsc && npm run build:js",
    "build:tsc": "npm run tsc --emitDeclarationOnly",
    "build:js": "node build.js",
    "clean": "rm -rf dist"
  }
}

배포하기 전에 확실히 build 명령어를 실행하고 배포하기 위해 prepack 스크립트 추가

prepack 스크립트는 npm publish를 실행하기 전에 실행되는 스크립트

빌드 전 dist 폴더를 삭제하는 스크립트와 tsc 명령어가 js 파일도 생성해주니 tsc에
—emitDeclarationOnly 플래그로 d.ts 파일만 생성하도록 수정

esbuild는 타입스크립트 d.ts 컴파일을 지원하지 않아 tsc와 esbuild를 같이 사용해야 함
https://esbuild.github.io/content-types/#es-module-interop

TypeScript 구성 옵션 declaration(즉, 파일 생성 .d.ts)은 지원되지 않습니다. 
TypeScript로 라이브러리를 작성하고 컴파일된 JavaScript 코드를 다른 사람이 사용할 수 있는
패키지로 게시하려는 경우 유형 선언도 게시하고 싶을 것입니다. 
이는 esbuild가 어떤 유형 정보도 유지하지 않기 때문에 esbuild가 대신 할 수 있는 일이 아닙니다.
TypeScript 컴파일러를 사용하여 생성하거나 직접 수동으로 작성해야 할 가능성이 큽니다.

https://esbuild.github.io/content-types/#es-module-interop

files 필드 추가

// package.json
{
	...,
	"files": [
		"dist",
		"src"
	]
}

라이브러리에 포함될 파일 또는 폴더 명시해주는 필드

배포

npm publish 를 통해 빌드 및 배포

결과물

npm: parse-csv-file

참고
https://junghyeonsu.com/posts/deploy-simple-util-npm-library/#es-module-%EC%A7%80%EC%9B%90%ED%95%98%EA%B8%B0

profile
한 줄로 소개 할 수 없는 개발자

0개의 댓글