Front-end

npm에 라이브러리 배포하기!

kjyook 2023. 11. 9. 14:55
728x90

클론코딩을 진행하다가 갑자기 해보고 싶은 일이 생겨서 하게 된 일이 있었다. npm에 나의 라이브러리를 배포해 보고 싶었다. 물론 지금 배포해 본 라이브러리는 아무도 안 쓸 수도 있고, 별로 쓸모없는 라이브러리일 수 있지만, 나중에 내가 어떤 컴포넌트를 굉장히 확장성 있게 잘 만들었다고 생각이 들 때 손쉽게 배포할 수 있게 미리 연습해 보기 위해서 진행해 봤다.

개발환경 구축하기

내가 원하는 라이브러리명으로 새 폴더를 만들고, 그 폴더에서 작업한다.
(이건 필수사항은 아니지만 나는 이때 내가 만든 repository를 클론 해서 받아왔다~)

npm init을 터미널에 입력해 초기 세팅을 한다. 이때 package.json 파일이 만들어지는데 한 문장을 추가해야 한다.

{
...,
"type":"module",
}

이는 프로젝트 내에서 ES modules 방식을 사용할 때 쓰는 코드이다.

나는 ts로 코딩할 거고, ts도 배포하기 위해 typescript를 설치한다.

npm i -D typescript

모듈 방식에 대하여

뒤에 ts모듈을 개발 후 빌드하고 퍼블리싱하는 과정을 설명하기에 앞서서 내가 직접 dist 디렉토리에 빌드하고 퍼블리싱 하는 이유는 설명해야 할 것 같아서 설명을 하도록 하겠다. Node.js에는 모듈 방식에는 2가지가 존재하는데 CommonJS와 ES Modules 방식이 존재한다.

 

CommonJS

//add.js
module.exports.add = (x,y) => x+y;

//main.js
const { add } = require('./add');

add(1,2);

 

ES Modules

//add.js
export function add(x,y) {
  return x + y
}

//main.js
import { add } from './add.js';

add(1,2);

이렇게 두 개의 모듈 시스템이 존재한다. 나는 이때까지 ESM 방식만 봐왔고 CJS는 이번에 라이브러리를 배포하는 과정에서 처음 봤다.
그러면 왜 두 모듈 시스템을 모두 지원해야 하는가?라는 질문이 있을 수 있다.

 

두 모듈 시스템을 모두 지원해야 하는 이유

cjs는 동기적으로 작동하고, esm은 top-level await를 지원하기에 비동기적으로 동작한다. 따라서 esm에서는 cjs를 import 할 수 있지만, cjs에서는 esm을 require 할 수 없다. cjs는 top-level await를 지원할 수 없기 때문이다. 또한 두 모듈 시스템은 동작이 달라 호환되기 어렵다.

우선 모듈 시스템의 지원도 브라우저 환경에서의 퍼포먼스에 영향을 준다. 브라우저 환경에서는 페이지 렌더링을 빠르게 하는 것이 중요한데, 이때 JS는 로딩되어 실행되는 동안 페이지 렌더링을 중단시키는 요소들 중 하나이다. 따라서 JS 번들 사이즈를 줄여서 렌더링 시간을 최소화하는 것이 종요하다. 이를 위해 필요한 것이 Tree-shaking이라고 한다. Tree-shaking이란 필요하지 않은 코드와 사용되지 않은 코드를 삭제하여 JS 번들의 크기를 가볍게 만드는 것이다. cjs는 require/module.exports에 아무런 제약이 없지만, esm은 최상위 스코프에서만 export를 할 수 있다. 고로 빌드 단계에서 정적 분석을 통해 모듈 간의 의존 관계를 파악할 수 있고, Tree-shaking을 쉽게 할 수 있다.


이다음에는 직접 모듈을 개발해야 한다. 개발하는 모듈은 src폴더 내에 index.ts 파일에 작성하는 것이 기본값이지만, 이는 마음대로 수정해도 상관이 없다. 다만 package.json의 "main"에 해당 파일 주소를 정확히 입력해 줘야 나중에 빌드하거나 퍼블리싱할 때 문제가 생기지 않는다.

ts 파일 빌드하기

위와 같은 이유로 cjs, esm 방식 모두 파일을 빌드해서 퍼블리싱할 거다. 물론 ts까지! 아까 package.json에 "type": "module"이라는 코드를 적은 적이 있는데 이는 esm방식을 사용하겠다는 말을 뜻한다. 그러면 cjs는요?? 그래서 이를 위한 코드는 따로 작성해줘야 한다.

그리고 알아두면 좋은 사실이 하나 더 있는데 JS 파일은 확장자가 모두. js이다. 하지만 cjs인지, esm인지 확장자를 통해 알릴 수도 있다.

  • . js 파일의 모듈 시스템은 package.json의 type에 따라 결정된다
    • type의 기본값은 CommonJS로, 이때. js는 cjs로 해석된다
    • 다른 값으로는 module이 있다. 이때. js는 esm으로 해석된다
  • . cjs는 항상 cjs로 해석된다
  • . mjs는 항상 esm으로 해석된다
  • ts에서도 마찬가지로 규칙이 적용된다. ( j만 t로 바꾸면 된다! )

딴 소리가 길었는데 그래서 나는 ts 파일을 컴파일하여 dist 폴더에 저장되도록 한 다음, tsconfig를 이용하여 ts 파일을 js로 변환할 거다. 이때 두 번의 컴파일을 해야 한다. CommonJS에 맞도록 한번, ES Modules에 맞도록 한번 총 두 번의 컴파일을 해야한다.

 

"프로젝트 루트"에 3개의 tsconfig 파일을 만들어야 한다. 나는 module 방식으로 ts 코드를 작성했으므로 추가로 cjs를 위한 tsconfig 파일을 만들었다. 만약 본인이 commonjs 방식을 사용했다면 반대로 하면 될 거 같다.

tsconfig-base.json
tsconfig-cjs.json
tsconfig.json

 

base에는 tsconfig-cjs와 tsconfig에서 상속받아 사용할 공통적인 설정들을 담을 것이다.

tsconfig-base.json

{
  "compilerOptions": {
    "target": "ES6",
    "allowJs": true,
    "allowSyntheticDefaultImports": true,
    "baseUrl": "src",
    "declaration": true,
    "esModuleInterop": true,
    "inlineSourceMap": false,
    "listEmittedFiles": false,
    "listFiles": false,
    "moduleResolution": "node",
    "noFallthroughCasesInSwitch": true,
    "pretty": true,
    "resolveJsonModule": true,
    "rootDir": "src",
    "skipLibCheck": true,
    "strict": true,
    "traceResolution": false,
    "jsx":"react"
  },
  "compileOnSave": false,
  "exclude": ["node_modules", "dist"],
  "include": ["src"]
}

 

여기서 compilerOption에 "jsx": "react"를 반! 드! 시! 써줘야 한다... 내가 이걸 안 썼다가 하루종일 고생했다.

 

tsconfig-cjs.json

{
    "extends": "./tsconfig-base.json",
    "compilerOptions": {
      "module": "commonjs",
      "outDir": "dist/cjs",
      "target": "es6"
    }
  }

 

tsconfig.json

{
    "extends": "./tsconfig-base.json",
    "compilerOptions": {
      "module": "esnext",
      "outDir": "dist/mjs",
      "target": "es6",
      "moduleResolution": "node",
      "allowSyntheticDefaultImports": true
    },
  }

처럼 3개의 파일을 모두 작성하고 나면 다시 package.json으로 가서 build 코드를 작성해 주면 된다.

 

package.json

{
  "scripts": {
    "build": "rm -fr dist/* && tsc -p tsconfig.json && tsc -p tsconfig-cjs.json"
  },
}

 

이때 하나 주의하면 좋은 것은 명령어인 npm run build를 칠 때 window 터미널에서는 하면 안 된다. rm이라는 명령어를 읽지 못한다.

이 명령어가 실행되면 dist 디렉터리를 지우고 tsconfig 파일을 이용하여 컴파일을 두 번 진행할 것이다.

이후 bash에 명령어만 몇 번 입력하면 끝난다.

cp dist/mjs/index.d.ts dist

rm -rf dist/*/index.d/ts

cat >dist/cjs/package.json
{
    "type": "commonjs"
}

cat >dist/mjs/package.json
{
    "type": "module"
} 

 

밑의 cat명령어 이후에는 package.json 파일에 입력하는 상태가 되는데 밑의 코드만 입력하고 종료하면 된다.

이제 마지막으로 package.json을 한 번만 수정해 주면 모든 일이 끝난다.

{
  ...,
  "main": "dist/cjs/index.js",
  "module": "dist/mjs/index.js",
  "types": "dist/index.d.ts",
  "exports": {
    ".": {
      "import": "./dist/mjs/index.js",
      "require": "./dist/cjs/index.js"
    }
  },
}

이렇게 추가해 주면 각 모듈이 환경에 맞게끔 불러오기가 가능해진다.


npm에 배포하기

배포하기 전에 필요 없이 올라가는 파일들이 없도록 ignore 해줘야 한다. 예를 들어 node_modules가 또 올라갈 필요는 없지 않으냐?

. npmignore

**/*
!/dist/**

dist 폴더 내의 파일과 루트 디렉터리 내의 파일만 퍼블리시될 거다.

이후에는 npm에서 가입을 하고 터미널에서 로그인해야 한다.

npm login

이젠 진짜 배포만 하면 된다!!

npm publish

끝!!!

 

 


참고한 블로그

https://toss.tech/article/commonjs-esm-exports-field

 

CommonJS와 ESM에 모두 대응하는 라이브러리 개발하기: exports field

Node.js에는 두 가지 Module System이 존재합니다. 토스 프론트엔드 챕터에서 운영하는 100개가 넘는 라이브러리들은 그것에 어떻게 대응하고 있을까요?

toss.tech

토스는 신인가... 나도 가고 싶다 ㅠ_ㅠ

728x90