우리가 만든 컴포넌트들을 다른 프로젝트에서도 사용 할 수 있게 해주려면 패키지를 만들어서 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를 사용하지 않았음에도 불구하고 관련 코드가 결과물 안에 포함되어버립니다.
자, 이제 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들을 설치해줬습니다.
import
구문으로 불러와서 사용할 수 있게 만들어줍니다.import { ReactComponent as icon } from './icon.svg'
형태의 코드를 사용 할 수 있습니다.그 다음에는 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 의 상단부에서 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을 사용하게 될 때 단순히 명령어로 옵션을 정해줄 수도 있지만 더욱 편리한 설정을 위하여 설정파일을 만들어서 작업할수도 있습니다.
프로젝트의 루트 디렉터리에 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
위와 같은 결과물이 나타날것입니다.
이 명령어를 package.json 에서 스크립트로 지정해봅시다.
yarn build
(또는 npm build
) 라고 입력해서 잘 작동하는지 확인해보세요.
성공적으로 빌드가 됐다면, dist/index.js
파일이 생성됐을 것입니다.
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"
로 정해주면 되는데요, 이 두가지 값을 추가하고 나면 기존에 있던 몇가지 옵션이 충돌나게 됩니다.
추가적으로, 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
에서 선언한 타입을 참고하게 된답니다.
이제 드디어! 우리가 만든 패키지에 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 를 열어보시면 됩니다.
성공적으로 등록이 되었다면 이렇게 npm에서 확인해보실 수 있습니다. 지금은 README가 작성되어있지 않기 때문에 "Unable to find a readme..." 라고 나와있는데요,
README.md 파일을 추가하시고 다시 publish 를 하시면 저 페이지에서 정보가 나타나게 됩니다.
이번에는 새 프로젝트를 만들어서 우리가 만든 패키지가 잘 작동하는지 확인해봅시다.
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;
우리가 이전에 만들었던 definition 파일이 제대로 작동하는 것을 확인하세요.
그 다음에는, yarn start
명령어를 입력하여 브라우저상에서 버튼이 잘 나타나는지 확인해보세요.
고생하셨습니다! 이제 여러분들은 npm에 리액트 컴포넌트 라이브러리를 등록하는 방법도 배우셨습니다!
참고: 이 페이지처럼 Storybook 페이지를 배포하고 싶다면,
yarn build-storybook
또는npm run build-storybook
명령어를 사용하셔서 빌드를 하고 결과물을 surge, netlify, s3 등에 배포하시면 됩니다. 참고로 이 명령어는 Storybook 초기 설정을 할 때 자동으로 생성이 됩니다.
우선, 너무나 잘 정리된 글 감사합니다!
궁금한 점이 있어서 댓글 남깁니다! webpack v5 버젼 부터는 commonjs 에 대한 번들링시 트리쉐이킹을 지원하는 걸로 알고있습니다.
그렇다면 rollup 과 webpack 어느걸 써도 상관없는 걸까요? ( 번들 사이즈에 크게 차이가 없을까요? -> 이건 제가 직접해봐야 아는 것이겠지만 ... ) 혹시나 이런 고민하셨는지 궁금해서 여쭤봐요!