𝝅번째 알파카의 개발 낙서장

screen

Rollup.js로 React 컴포넌트 라이브러리 개발기

projects

React

count

왜? 🔗

회사에서 할당받은 업무 중 하나로, 컴포넌트를 라이브러리화하여 npm으로 배포하는 업무를 맡게 됐다. 즉, react-bootstrap 같은 컴포넌트 라이브러리를 개발해야한다.

코드 배포 경험이라곤 예전에 JAVA 오픈소스 라이브러리 만든답시고 Maven에 한 번 배포해본 게 전부인 내게, 새로운 개발환경에서의 배포는 필연적인 시행착오를 불러왔다.

개발하면서 느꼈던건, 깊게 참고할만한 레퍼런스가 너무 없었고, 가져다 쓸만한 적절한 코드도 찾지 못 했다. 다행히 뭐 어찌저찌 시간 갈아가며 어느정도 기틀을 잡을 수 있었다.

나름 재밌기도 했고, 한 번 파볼만한 가치도 있는 것 같고, 인지도 높은 레퍼런스도 없는 것 같아서 내가 직접 한 번 만들어보기로 했다.




목표 🔗

  • 최소한의 번들을 위해 Create React App 미사용
  • TypeScript 기반의 React 라이브러리 개발환경을 구축
  • 스타일 코드는 SCSS 사용 (CSS-in-CSS)
  • Storybook을 통한 컴포넌트 테스트
  • npm 배포 및 타 프로젝트에서의 활용 가능 여부 확인

목표는 위와 같다. 수준급의 개발환경까지 제공하지는 못 하더라도, 적당히 활용 가능할만한 수준의 개발환경을 제공해주는 것이 궁극적인 목표다.




React Components Library Starter 🔗

이 프로젝트의 이름은 React Components Library Starter로 명명했다. 리액트 라이브러리 개발환경을 제공한다는 의미가 직관적으로 드러나길 원했다.

제품화된 소프트웨어나 솔루션이면 모를까, 이런 종류의 라이브러리는 그냥 봤을 때 "얘가 뭘 하는 라이브러리구나"라고 대충 감이 오는 게 제일 좋은 것 같다.

CRA를 사용하지 않으므로, 그냥 생짜 밑바닥에서부터 구축한다.

yarn을 기준으로 기술한다.



1. 환경 구성하기 🔗

BASH

0mkdir react-components-library-starter
1
2cd react-components-library-starter
3
4yarn init
5# question name (react-components-library-starter):
6# question version (1.0.0):
7# question description:
8# question entry point (index.js):
9# question repository url (https://github.com/itcode-dev/react-components-library-starter):
10# question author (RWB0104 <psj2716@gmail.com>):
11# question license (MIT):
12# question private: false
13
14mkdir src
  • 폴더를 생성한다.
  • 프로젝트를 초기화한다.
  • src 폴더를 생성한다. 소스코드의 최상위 폴더가 될 것이다.


2. React 설치 🔗

BASH

0yarn add -D react @types/react
이름 용도
react React 라이브러리
@types/react React 라이브러리 타입
  • React 관련 라이브러리를 설치한다.
  • 해당 라이브러리의 구동에 직접적으로 연관이 없는 대부분의 라이브러리는 -D 옵션을 지정하여 devDependencies로 설치한다.


3. TypeScript 설치 🔗

BASH

0yarn add -D typescript
이름 용도
typescript TypeScript 라이브러리
  • TypeScript 관련 라이브러리를 설치한다.

BASH

0vim tsconfig.json
  • TypeScript 빌드 설정을 위해, 설정파일 tsconfig.json을 생성한다.

JSON

0{
1 "compilerOptions": {
2 "target": "es5",
3 "esModuleInterop": true,
4 "forceConsistentCasingInFileNames": true,
5 "strict": true,
6 "skipLibCheck": true,
7 "jsx": "react",
8 "module": "ESNext",
9 "declaration": true,
10 "declarationDir": "./dist",
11 "sourceMap": false,
12 "outDir": "./dist",
13 "moduleResolution": "node",
14 "allowSyntheticDefaultImports": true,
15 "emitDeclarationOnly": true,
16 "removeComments": true
17 },
18 "include": [
19 "./src"
20 ],
21 "exclude": [
22 "./dist",
23 "./node_modules",
24 "./src/**/*.test.tsx",
25 "./src/**/*.stories.tsx",
26 ]
27}
  • tsconfig.json의 예시는 위와 같다.
    • declaration - *.d.ts 타입 파일 생성 여부
    • declarationDir - *.d.ts 출력 경로. 반드시 output과 동일하거나 하위 경로여야한다.
    • sourceMap - 번들링 분석을 위한 소스맵 코드 생성 여부
    • outDir - 출력 경로. 라이브러리의 빌드 결과물은 dist/에 생성된다.
  • 특별한 설정을 할 게 아니라면, 그냥 저대로 사용해도 무방하다.


4. SCSS 설치 🔗

BASH

0yarn add classnames style-inject
1yarn add -D postcss sass
이름 용도
classnames 클래스 속성 claaName 조인 활용을 위한 라이브러리
style-inject 스타일 태그 헤더 삽입기
postcss CSS 후처리기
sass SASS/SCSS 라이브러리


5. Rollup.js 빌더 설치 🔗

BASH

0yarn add -D rollup @rollup/plugin-babel @rollup/plugin-commonjs @rollup/plugin-node-resolve @rollup/plugin-typescript rollup-plugin-peer-deps-external rollup-plugin-postcss
이름 용도
rollup Rollup.js 코어
@rollup/plugin-babel Rollup.js와 Babel 연동 플러그인
@rollup/plugin-commonjs CommonJS -> ES6 코드로 변환하는 플러그인
@rollup/plugin-node-resolve 외부 라이브러리 사용 시, 해당 라이브러리를 설치한 프로젝트의 node_modules를 참조하도록 변환하는 플러그인
@rollup/plugin-typescript Rollup.js와 TypeScript 연동 플러그인
rollup-plugin-peer-deps-external peerDependencies 모듈을 번들링하지 않고 해당 라이브러리를 설치한 프로젝트의 node_modules를 참조하도록 변환하는 플러그인
rollup-plugin-postcss Rollup.js와 PostCSS 연동 플러그인
  • Rollup.js 관련 라이브러리를 설치한다.
  • 개발자의 니즈에 따라 다양한 플러그인을 추가할 수도 있다.

BASH

0vim rollup.config.js
  • Rollup.js 설정을 위해 rollup.config.js를 생성한다.

JS

0/**
1 * Rollup 설정 모듈
2 *
3 * @author RWB
4 * @since 2022.06.06 Mon 17:44:31
5 */
6
7import babel from '@rollup/plugin-babel';
8import commonjs from '@rollup/plugin-commonjs';
9import { nodeResolve } from '@rollup/plugin-node-resolve';
10import typescript from '@rollup/plugin-typescript';
11import peerDepsExternal from 'rollup-plugin-peer-deps-external';
12import postcss from 'rollup-plugin-postcss';
13
14const extensions = [ 'js', 'jsx', 'ts', 'tsx', 'mjs' ];
15
16const pkg = require('./package.json')
17
18const config = [
19 {
20 external: [ /node_modules/ ],
21 input: './src/index.ts',
22 output: [
23 {
24 dir: './dist',
25 format: 'cjs',
26 preserveModules: true,
27 preserveModulesRoot: 'src'
28 },
29 {
30 file: pkg.module,
31 format: 'es'
32 }
33 ,
34 {
35 name: pkg.name,
36 file: pkg.browser,
37 format: 'umd'
38 }
39 ],
40 plugins: [
41 nodeResolve({ extensions }),
42 babel({
43 exclude: 'node_modules/**',
44 extensions,
45 include: [ 'src/**/*' ]
46 }),
47 commonjs({ include: 'node_modules/**' }),
48 peerDepsExternal(),
49 typescript({ tsconfig: './tsconfig.json' }),
50 postcss({
51 extract: false,
52 inject: (cssVariableName) => `import styleInject from 'style-inject';styleInject(${cssVariableName});`,
53 modules: true,
54 sourceMap: false,
55 use: [ 'sass' ]
56 })
57 ]
58 }
59];
60
61export default config;
  • 설정 예시는 위와 같다.
  • ./src/index.ts 파일을 기준으로 결과물을 출력
    • CJS (기본)
    • ESM
    • UMD
  • CJS 모듈의 경우 preserveModules로 Tree Shaking을 지원
  • @rollup/plugin-babel@rollup/plugin-commonjs 보다 먼저 수행되어야함
    • 플러그인의 특성 상, 순서가 중요하게 작용할 가능성 있음


6. Storybook 설치 🔗

BASH

0npx storybook init --builder webpack5
1
2yarn add -D @storybook/preset-scss css-loader sass-loader style-loader react-dom
이름 용도
storybook Storybook CLI
@storybook/preset-scss Storybook의 webpack SCSS 설정 애드온
css-loader CSS 해석기
sass-loader SASS/SCSS를 CSS로 빌드하는 라이브러리
style-loader CSS 코드를 DOM에 삽입하는 라이브러리
react-dom React Dom 처리기
  • Storybook을 구동하기 위한 라이브러리를 설치한다.
    • .storybook/ - Storybook 설정 폴더
    • src/stories/ - Storybook 데모 폴더
  • 스타일 관련 로더들의 최신버전은 대부분 webpack5와 호환되고 있으므로, 반드시 Storybook의 빌더를 webpack5로 지정해야한다.
    • 기본 빌더는 webpack4로, 이 경우 로더들의 버전을 빌더와 호환되게끔 낮춰줘야한다.

설치만 하면 되는 건 아니고, 간단한 설정이 필요하다.

npx storybook init --builder webpack5를 수행하면 알아서 프로젝트에 Storybook을 설치해준다. 이 과정에서 최상단 경로에 .storybook 폴더를 생성한다.

.storybook/main.js에 아래와 같이 코드를 추가해준다.

JS

0module.exports = {
1 "stories": [
2 "../src/**/*.stories.mdx",
3 "../src/**/*.stories.@(js|jsx|ts|tsx)"
4 ],
5 "addons": [
6 "@storybook/addon-links",
7 "@storybook/addon-essentials",
8 "@storybook/addon-interactions",
9 // 추가
10 "@storybook/preset-scss"
11 ],
12 "framework": "@storybook/react",
13 "core": {
14 "builder": "@storybook/builder-webpack5"
15 }
16}
  • @storybook/preset-scss를 애드온 리스트에 추가하여 적용한다.


7. ESLint 설치 (Optional) 🔗

코드의 일정한 규칙은 코드의 가독성을 향상시켜준다. 물론 개발자가 온전히 수작업으로 코드 컨벤션을 준수할 수도 있지만, 사람이 하는 일이다보니 실수가 발생하기도 하며, 코드 컨벤션와 일치하지 않는 코드를 일일히 찾는 것은 코드 퍼포먼스와 거의 연관성이 없음에도 많은 작업량을 요구한다. 더군다나 개발자가 여러명일 경우, 각자의 주관으로 인해 코드 컨벤션이 쉽게 망가질 우려가 있다.

ESLint를 활용하면 개발자가 신경쓰지 않아도 코드 컨벤션을 준수할 수 있다.

하지만 이는 코드 품질을 준수하기 위한 것으로, 코드의 퍼포먼스와 큰 연관성이 없으며, ESLint의 유무와 개발은 전혀 관련이 없다. 만약 이런 것까지 굳이 신경쓰고 싶지 않다면 이 문단은 넘어가도 무방하다. 향후 라이브러리 개발에 어떠한 영향도 미치지 않는다.

BASH

0yarn add -D eslint @typescript-eslint/eslint-plugin @typescript-eslint/parser eslint-config-airbnb eslint-plugin-import eslint-plugin-jsx-a11y eslint-plugin-react eslint-plugin-react-hooks eslint-plugin-sort-keys-fix eslint-plugin-storybook
이름 용도
eslint ESLint 코어
@typescript-eslint/eslint-plugin ESLint TypeScript 환경 적용 플러그인
@typescript-eslint/parser ESLint TypeScript 파서 플러그인
eslint-config-airbnb Airbnb 규칙 설정
eslint-plugin-import import/export 규칙 플러그인
eslint-plugin-jsx-a11y JSX 요소 규칙 플러그인
eslint-plugin-react React 규칙 플러그인
eslint-plugin-react-hooks React Hook 규칙 플러그인
eslint-plugin-sort-keys-fix 객체 키 정렬 규칙 플러그인
eslint-plugin-storybook Storybook 규칙 플러그인
  • ESLint 및 관련 설정, 플러그인을 설치한다.

JS

0module.exports = {
1 env: {
2 browser: true,
3 node: true
4 },
5 extends: [ 'airbnb', 'airbnb/hooks', 'eslint:recommended', 'plugin:react/recommended', 'plugin:import/recommended', 'plugin:storybook/recommended' ],
6 ignorePatterns: [ '.storybook', '*.d.ts', 'node_modules', 'build', 'dist', '**/env/*.js' ],
7 overrides: [
8 {
9 files: [ '*.ts', '*.tsx' ],
10 rules: { 'no-undef': 'off' }
11 }
12 ],
13 parser: '@typescript-eslint/parser',
14 parserOptions: { warnOnUnsupportedTypeScriptVersion: false },
15 plugins: [ '@typescript-eslint', 'sort-keys-fix', 'prettier' ],
16 rules: {
17 '@typescript-eslint/ban-ts-comment': [
18 'error',
19 { 'ts-ignore': 'allow-with-description' }
20 ],
21 '@typescript-eslint/no-explicit-any': 'warn',
22 '@typescript-eslint/no-unused-vars': 'error',
23 'array-bracket-spacing': [
24 'error',
25 'always',
26 {
27 arraysInArrays: false,
28 objectsInArrays: false
29 }
30 ],
31 'brace-style': [ 'error', 'allman' ],
32 'comma-dangle': [ 'error', 'never' ],
33 'eol-last': [ 'error', 'never' ],
34 'import/extensions': 'off',
35 'import/named': 'off',
36 'import/no-anonymous-default-export': 'off',
37 'import/no-cycle': 'off',
38 'import/no-extraneous-dependencies': 'off',
39 'import/no-named-as-default': 'off',
40 'import/no-unresolved': 'off',
41 'import/order': [
42 'error',
43 {
44 alphabetize: {
45 caseInsensitive: true,
46 order: 'asc'
47 },
48 groups: [ 'external', 'builtin', 'internal', 'sibling', 'parent', 'index' ],
49 'newlines-between': 'always'
50 }
51 ],
52 indent: [ 'error', 'tab' ],
53 'jsx-a11y/control-has-associated-label': 'off',
54 'jsx-quotes': [ 'error', 'prefer-single' ],
55 'linebreak-style': 'off',
56 'max-len': 'off',
57 'no-restricted-exports': 'off',
58 'no-tabs': [ 'error', { allowIndentationTabs: true }],
59 'no-unused-vars': 'off',
60 'object-curly-newline': [ 'error', {
61 ExportDeclaration: 'never',
62 ImportDeclaration: 'never',
63 ObjectExpression: {
64 minProperties: 3,
65 multiline: true
66 },
67 ObjectPattern: 'never'
68 }],
69 'react-hooks/exhaustive-deps': 'warn',
70 'react/button-has-type': 'off',
71 'react/destructuring-assignment': 'off',
72 'react/function-component-definition': 'off',
73 'react/jsx-curly-brace-presence': [
74 'error',
75 {
76 children: 'never',
77 props: 'never'
78 }
79 ],
80 'react/jsx-filename-extension': 'off',
81 'react/jsx-indent': [ 'error', 'tab' ],
82 'react/jsx-props-no-spreading': 'off',
83 'react/jsx-sort-props': [
84 'error',
85 {
86 callbacksLast: true,
87 ignoreCase: true,
88 multiline: 'last',
89 noSortAlphabetically: false,
90 reservedFirst: false,
91 shorthandFirst: false,
92 shorthandLast: true
93 }
94 ],
95 'react/prop-types': 'off',
96 'react/react-in-jsx-scope': 'off',
97 'react/require-default-props': 'off',
98 'require-jsdoc': 'off',
99 'sort-keys-fix/sort-keys-fix': 'error'
100 },
101 settings: {
102 'import/parsers': { '@typescript-eslint/parser': [ '.ts', '.tsx', '.js' ] },
103 react: { version: 'detect' }
104 }
105};
  • .eslintrc.js에서 ESLint의 설정을 관리할 수 있으며, 그 예시는 위와 같다.
  • rules에 원하는 규칙을 추가하면 된다.


8. 프로젝트 설정 🔗

  • .npmignore
    • .npmignore.gitignore와 비슷하다. 다만, npm에 배포 시 제외할 파일을 선언한다는 점이 다르다.
    • 해당 리스트의 규칙과 일치하는 파일 및 폴더는 npm 배포 시 포함되지 않는다.

TXT

0.storybook/
1src/
2rollup.config.js
3tsconfig.json
4yarn.lock
  • package.json
    • name - npm 배포 시, 이 이름을 기준으로 배포를 수행한다.
      • 조직 하위에 배포할 경우, @org/name 형태로 입력한다.
    • version - 라이브러리 버전. 이미 올라간 버전은 재배포가 불가능하며, 배포 시마다 버전을 적절히 관리해야한다.
    • main - 해당 라이브러리의 기본(CJS) 스크립트
    • module - 해당 라이브러리의 ESM 스크립트
    • browser - 해당 라이브러리의 UMD 스크립트
    • types - 해당 라이브러리의 타입
    • private - npm 공개 여부
      • GitHub의 Repository와는 관계없다.
    • script - 프로젝트의 스크립트 명령어 목록
      • 빌드를 위해 rollup -c 명령어를 추가한다.

JSON

0{
1 "name": "@itcode-dev/react-components-library-starter",
2 "version": "3.0.1",
3 "main": "./dist/index.js",
4 "module": "./dist/index.es.js",
5 "browser": "./dist/index.umd.js",
6 "types": "./dist/index.d.ts",
7 "private": false,
8 "script": {
9 "build": "rollup -c"
10 }
11}



배포 🔗

라이브러리를 배포한다.

BASH

0npm login
1# username
2# password
3# email
4# email otp
5
6yarn publish --access public
  • npm login으로 로그인을 수행한다.
  • yarn publish --access public으로 배포를 수행한다.



설치 🔗

배포한 프로젝트를 직접 설치하여 사용해보자.

BASH

0npm i @itcode-dev/react-components-library-starter
1
2yarn add @itcode-dev/react-components-library-starter

위 명령어를 통해 라이브러리를 설치할 수 있다.

TSX

0import Button from '@itcode-dev/react-components-library-starter/dist/atom/Button';
1import Input from '@itcode-dev/react-components-library-starter/dist/atom/Input';

image

위와 같이 라이브러리를 활용할 수 있다.




여담 🔗

이번 프로젝트는 규모는 작았지만, 연구할 게 많은 프로젝트였다. 찾아볼 건 많은데 규모는 작다보니 재밌게 했던 것 같다.

이 프로젝트 덕분에 새로운 걸 많이 알아갈 수 있었다.

  • Storybook
  • classnames
  • npm 배포 흐름
  • rollup.js

그 밖에도 자잘자잘하게 얻은 게 많지 않나 싶다. 여러모로 보람찬 프로젝트였다.


그러고보니 저번에 자바 라이브러리도 배포한 적이 있던 거 같은데... npm랑 다르게 Maven은 배포 과정이 복잡했던걸로 기억한다.

지금 다시 하라고 하면 못 할거 같긴 한데.. 시간 날 때 그 것도 다시 한 번 정리해야 할 것 같다.