우리가 만든 컴포넌트들을 다른 프로젝트에서도 사용 할 수 있게 해주려면 패키지를 만들어서 npm에 퍼블리시를 해주어야 합니다. (또는, 로컬 패키지로 설정해서 사용하거나 git 레포로 설치하는 방법도 존재합니다.)

라이브러리를 배포하려면, 우리가 보통 웹 애플리케이션을 webpack / parcel 과 같은 도구로 번들링하는 것 처럼 라이브러리도 번들링을 해주어야 합니다. 물론, 번들링이 필수적인 것은 아닙니다. 단순히 TypeScript 또는 Babel로 코드를 트랜스파일해서 등록을 할 수도 있습니다.

하지만 현재 우리 상황에서는 그냥 코드 트랜스파일만 해서 배포 할 수는 없습니다. 그 이유는 우리가 svg를 컴포넌트로 사용하기 위해서 svgr 이란 것을 사용하고있는데, 이를 사용하려면 webpack 또는 rollup을 꼭 사용해야만 합니다.

우리가 webpack을 쓸 수도 있는데 rollup을 쓰는 이유는 webpack은 ES Module 형태로 번들을 할 수 없습니다. webpack을 사용 할 때에는 일반적으로 commonjs 형태로 번들링을 하게 되는데, commonjs로 번들링한 라이브러리를 나중에 다른 프로젝트에서 사용하게 되면 Tree-shaking이 지원되지 않습니다.

가령, 우리가 등록한 라이브러리를 설치하고 그 중에서 Button 컴포넌트만 사용을 했다고 가정을 해봅시다. Tree-shaking이 되었다면 프로젝트를 빌드했을 때, Dialog컴포넌트를 사용하지 않았기 때문에 빌드된 파일에도 Dialog 관련 코드가 포함되지 않게 됩니다.

반면, Tree-shaking이 되지 않았다면 Dialog를 사용하지 않았음에도 불구하고 관련 코드가 결과물 안에 포함되어버립니다.

5-1. Rollup으로 라이브러리 번들하기

자, 이제 Rollup으로 우리가 만든 라이브러리를 번들해서 배포 할 준비를 해줍시다.

rollup에 필요한 패키지 설치하기

우선 Rollup을 사용하기 위하여 필요한 패키지들을 설치해주세요.

yarn add --dev rollup rollup-plugin-babel rollup-plugin-node-resolve rollup-plugin-peer-deps-external rollup-plugin-commonjs @svgr/rollup rollup-plugin-url
# 또는 npm install --save-dev rollup rollup-plugin-babel rollup-plugin-node-resolve rollup-plugin-peer-deps-external rollup-plugin-commonjs @svgr/rollup rollup-plugin-url

rollup 패키지를 비롯하여 다양한 plugin들을 설치해줬습니다.

  • rollup-plugin-babel: rollup에서 babel 을 사용 할 수 있게 해주는 플러그인입니다.
  • rollup-plugin-node-resolve: node_modules에서 써드파티 모듈을 사용하는 용도로 사용하며, js 이외의 확장자 (ts, tsx) 파일을 불러오기 위해서도 이 플러그인을 필요로 합니다.
  • rollup-plugin-peer-deps-external: peerDependency로 설치된 라이브러리의 코드가 번들링된 결과에 포함되지 않고, import 구문으로 불러와서 사용할 수 있게 만들어줍니다.
  • rollup-plugin-commonjs: CommonJS 형태로 이루어진 모듈의 코드를 ES6로 변환하여 결과물에 포함될 수 있게 해줍니다. (현재 우리 프로젝트에서는 필수로 적용해야하는 플러그인은 아닙니다)
  • @svgr/rollup: SVG를 컴포넌트 형태로 불러와서 사용 할 수 있게 해줍니다.
  • rollup-plugin-url: data-URI형태로 svg, png, jpg 파일 등을 불러와서 사용 할 수 있게 해줍니다. @svgr/rollup 플러그인을 사용 할 때, rollup-plugin-url과 함께 사용을 해야만 import { ReactComponent as icon } from './icon.svg' 형태의 코드를 사용 할 수 있습니다.

peerDependency 설정하기

그 다음에는 react와 react-dom을 peerDependency 로 설치해주세요. 해당 모듈은 현재 프로젝트에 이미 설치되어있긴 하지만, 설치되어있는 이유는 devDependency에 있는 @storybook/react 에서 의존하고 있기 때문입니다.

yarn add --peer react react-dom
# 또는 npm install --save react react-dom

package.json 을 열어보세요. peerDependencies 값이 다음과 같이 채워져있나요?

  "peerDependencies": {
    "@emotion/core": "^10.0.22",
    "react": "^16.12.0",
    "react-dom": "^16.12.0",
    "react-spring": "^8.0.27"
  }

yarn을 사용하지 않고 npm을 사용하신 분들은 peerDependencies 가 아닌 dependencies 로 설치가 되어있을 것입니다. 그런 경우에는, 그냥 텍스트를 바로 peerDependencies로 교체하시면 됩니다.

package.json 수정하기

package.json 의 상단부에서 name 값을 react-uikit-username 으로 변경하세요. username 부분엔 여러분의 아이디를 넣으세요.

그 다음에는, main 을 지우시고, module 값을 "dist/index.js" 로 설정하세요. 이는 우리가 ESModule 형태로 빌드한 결과물을 저장 할 경로를 의미합니다. 나중에 우리가 만든 패키지를 설치하고 불러오게 되면 이 module 값을 참조하여 코드를 불러오게 됩니다.

{
  "name": "react-uikit-sample",
  "version": "1.0.0",
  "module": "dist/index.js",

rollup.config.js 작성하기

rollup을 사용하게 될 때 단순히 명령어로 옵션을 정해줄 수도 있지만 더욱 편리한 설정을 위하여 설정파일을 만들어서 작업할수도 있습니다.

프로젝트의 루트 디렉터리에 rollup.config.js 를 생성하고, 다음 코드를 입력해보세요.

rollup.config.js

import commonjs from 'rollup-plugin-commonjs';
import resolve from 'rollup-plugin-node-resolve';
import babel from 'rollup-plugin-babel';
import pkg from './package.json';
import external from 'rollup-plugin-peer-deps-external';
import svgr from '@svgr/rollup';
import url from 'rollup-plugin-url';
import peerDepsExternal from 'rollup-plugin-peer-deps-external';

const extensions = ['.js', '.jsx', '.ts', '.tsx']; // 어떤 확장자를 처리 할 지 정함

// babel-preset-react-app를 사용한다면 BABEL_ENV를 필수로 설정해야함.
process.env.BABEL_ENV = 'production';

export default {
  input: './src/index.ts', // 어떤 파일부터 불러올지 정함.
  plugins: [
    peerDepsExternal() /* peerDependencies로 설치한 라이브러리들을 external 모듈로 설정
                               즉, 번들링된 결과에 포함시키지 않음 */,
    resolve({ extensions }), // node_modules 에서 모듈을 불러올 수 있게 해줌. ts/tsx 파일도 불러올 수 있게 해줌
    commonjs({
      include: 'node_modules/**'
    }), // CommonJS 형태로 만들어진 모듈도 불러와서 사용 할 수 있게 해줌. 현재 프로젝트 상황에서는 없어도 무방함
    babel({ extensions, include: ['src/**/*'], runtimeHelpers: true }), // Babel을 사용 할 수 있게 해줌
    url(), // 미디어 파일을 dataURI 형태로 불러와서 사용 할 수 있게 해줌.
    svgr() // SVG를 컴포넌트로 사용 할 수 있게 해줌.
  ],
  output: [
    {
      file: pkg.module, // 번들링한 파일을 저장 할 경로
      format: 'es' // ES Module 형태로 번들링함
    }
  ]
};

그 다음에는 babel 을 위한 설정을 해주어야 합니다. .babelrc 파일을 루트경로에 생성한 뒤, 다음 코드를 입력하세요.

.babelrc

{
  "presets": [["react-app", { "flow": false, "typescript": true }]]
}

참고로, .babelrc 파일을 만들지 않아도 babel rollup 플러그인 옵션 객체에 presets 값을 넣어주셔도 상관 없습니다.

빌드를 하기 전에, src 에 index.ts 파일을 만들어주세요. 이 파일에서는 우리가 만들었던 컴포넌트를 불러와서, 바로 내보내줍니다.

src/index.ts

export { default as Button } from './Button/Button';
export { default as ButtonGroup } from './ButtonGroup/ButtonGroup';
export { default as Dialog } from './Dialog/Dialog';
export { default as Icon } from './Icon/Icon';

이제 한번 빌드를 해봅시다! 다음 명령어를 입력해보세요.

yarn rollup -c
# 또는 ./node_modules/rollup/dist/bin/rollup -c

이 때 react-spring이 존재하지 않는다는 에러가 발생한다면 다시 한번 설치해주세요: yarn add --peer react-spring

image.png

위와 같은 결과물이 나타날것입니다.

이 명령어를 package.json 에서 스크립트로 지정해봅시다.

yarn build (또는 npm build) 라고 입력해서 잘 작동하는지 확인해보세요.

성공적으로 빌드가 됐다면, dist/index.js 파일이 생성됐을 것입니다.

TypeScript declaration 파일 만들기

declaration이란, 우리가 만든 컴포넌트들에서 사용하고 있는 타입 정보들을 지니고 있는 파일을 의미합니다. 이는 다음 명령어로 생성을 할 수 있는데요

tsc --emitDeclarationOnly

이 명령어를 실행하기 전에 tsconfig.json 을 수정해주어야 합니다.

{
  "compilerOptions": {
    "target": "es5",
    "lib": ["dom", "dom.iterable", "esnext"],
    "skipLibCheck": true,
    "esModuleInterop": true,
    "allowSyntheticDefaultImports": true,
    "strict": true,
    "forceConsistentCasingInFileNames": true,
    "module": "esnext",
    "moduleResolution": "node",
    "resolveJsonModule": true,
    "jsx": "react",
    "declaration": true,
    "declarationDir": "dist/types"
  },
  "include": ["src"],
  "exclude": ["**/*.stories.tsx"]
}

위와 같이 declaration 값을 true 로 바꾸고 declarationDir 경로를 "dist/types" 로 정해주면 되는데요, 이 두가지 값을 추가하고 나면 기존에 있던 몇가지 옵션이 충돌나게 됩니다.

  • allowJs: 자바스크립트와 혼용을 하고 있다면 declaration 파일을 못만듭니다. 이 옵션을 제거해주세요.
  • noEmit: 결과물을 만들지 않는다는 옵션입니다. 이 옵션을 제거해주세요.
  • isolatedModules: 아무 값도 내보내지 않는 파일을 방지하는 옵션입니다. 이 옵션을 제거해주세요.

추가적으로, stories.tsx 확장자는 모두 무시하도록 exclude 옵션을 설정하셔야 합니다.

이제, package.json 을 열어서 다음과 같이 build:types 스크립트를 추가해보세요.

"scripts": {
  "storybook": "start-storybook -p 6006",
  "build-storybook": "build-storybook",
  "build": "rollup -c",
  "build:types": "tsc --emitDeclarationOnly"
},

그 다음에 yarn build:types 를 입력해보세요.

dist/types/index.d.ts 파일이 잘 만들어졌나요?

파일이 잘 만들어졌다면 package.json에서 definition 파일의 경로를 명시해주세요.

{
    "name": "react-uikit-sample",
    "version": "1.0.0",
    "module": "dist/index.js",
    "license": "MIT",
    "types": "dist/types/index.d.ts",
  ...

나중에 이 패키지를 설치해서 사용하게 될 때 TypeScript 컴파일러에서 자동으로dist/types/index.d.ts 에서 선언한 타입을 참고하게 된답니다.

5-2. npm에 등록하기

이제 드디어! 우리가 만든 패키지에 npm에 등록해볼 차례입니다.

https://www.npmjs.com/ 에 들어가서 우측 상단 JOIN 버튼을 눌러서 회원가입을 하세요. 이메일 인증을 해야 하므로 실제 사용중인 이메일을 사용하시기 바랍니다. 이미 계정이 있으신분들은 기존 계정 사용하시면 됩니다.

회원가입후, 이메일 인증을 하신다음에 다음 명령어를 통해 로그인해보세요.

yarn login
# 또는 npm login

명령어를 입력하시면 계정명과 이메일을 물어봅니다. 회원가입하실때 사용하셨던 정보들을 입력하세요.

npm 에 패키지를 등록 할 때에는 어떤 파일들을 패키지 안에 넣을지 정해주어야 합니다. 패키지 안에는 src 안에 있는 파일들을 넣어줄 필요가 없지요. 특정 파일들을 포함시키지 않기 위해서 .gitignore 처럼 .npmignore 파일을 사용 할 수도 있습니다.

하지만, 그 대신에 package.json 파일에서 files 값을 명시하는 것을 권장드립니다. .gitignore 를 하게된다면 무시해야 할 파일을 하나하나 넣어줘야 하는 반면에 files 값을 명시하면 여기에서 지정한 경로만 패키지에 포함이 됩니다.

{
  "name": "react-uikit-sample",
  "version": "1.0.0",
  "module": "dist/index.js",
  "license": "MIT",
  "types": "dist/types/index.d.ts",
  "files": [
    "/dist"
  ],

이렇게 하면 패키지안에 /dist 경로만 포함이 됩니다.

참고로 README, package.json 파일은 따로 입력하지 않아도 무조건 포함됩니다.

이제 등록을 해봅시다!

주의: npm에 패키지를 등록하면 72시간 이내에만 삭제 할 수 있습니다. 패키지를 제거하고나면 24시간 이후에만 해당 이름을 재사용 할 수 있습니다.

yarn publish
# 또는 npm publish

위 명령어를 입력하고 나면 npm 비밀번호를 한번 물어보게 되고, 절차가 끝나면 npmjs.com 에서 확인해볼 수 있습니다.

등록한 패키지를 npmjs.com 에서 검색해보거나, 로그인 후 상단 유저메뉴 - Packages 를 열어보시면 됩니다.

image.png

성공적으로 등록이 되었다면 이렇게 npm에서 확인해보실 수 있습니다. 지금은 README가 작성되어있지 않기 때문에 "Unable to find a readme..." 라고 나와있는데요,

README.md 파일을 추가하시고 다시 publish 를 하시면 저 페이지에서 정보가 나타나게 됩니다.

패키지 npm 에서 받아서 사용해보기

이번에는 새 프로젝트를 만들어서 우리가 만든 패키지가 잘 작동하는지 확인해봅시다.

npx create-react-app sampleapp --typescript
cd sampleapp
yarn add react-uikit-sample react-spring @emotion/core

우리가 만든 라이브러리는 react-spring, @emotion/core 를 peerDependency로 갖고 있기 때문에 꼭 함께 설치를 해주셔야합니다.

그 다음에는, App 에서 다음과 같이 Button 컴포넌트를 사용해봅시다.

import React from 'react';
import { Button } from 'react-uikit-sample';

const App: React.FC = () => {
  return <Button>버튼</Button>;
};

export default App;

image.png
image.png

우리가 이전에 만들었던 definition 파일이 제대로 작동하는 것을 확인하세요.

그 다음에는, yarn start 명령어를 입력하여 브라우저상에서 버튼이 잘 나타나는지 확인해보세요.

image.png

고생하셨습니다! 이제 여러분들은 npm에 리액트 컴포넌트 라이브러리를 등록하는 방법도 배우셨습니다!

참고: 이 페이지처럼 Storybook 페이지를 배포하고 싶다면, yarn build-storybook 또는 npm run build-storybook 명령어를 사용하셔서 빌드를 하고 결과물을 surge, netlify, s3 등에 배포하시면 됩니다. 참고로 이 명령어는 Storybook 초기 설정을 할 때 자동으로 생성이 됩니다.