Rollup.js로 React 컴포넌트 라이브러리 개발기
프로젝트
⏰ 2022-06-10 14:51:25
D O W N

D O W N
회사에서 할당받은 업무 중 하나로, 컴포넌트를 라이브러리화하여 npm으로 배포하는 업무를 맡게 됐다. 즉, react-bootstrap 같은 컴포넌트 라이브러리를 개발해야한다.
코드 배포 경험이라곤 예전에 JAVA 오픈소스 라이브러리 만든답시고 Maven에 한 번 배포해본 게 전부인 내게, 새로운 개발환경에서의 배포는 필연적인 시행착오를 불러왔다.
개발하면서 느꼈던건, 깊게 참고할만한 레퍼런스가 너무 없었고, 가져다 쓸만한 적절한 코드도 찾지 못 했다. 다행히 뭐 어찌저찌 시간 갈아가며 어느정도 기틀을 잡을 수 있었다.
나름 재밌기도 했고, 한 번 파볼만한 가치도 있는 것 같고, 인지도 높은 레퍼런스도 없는 것 같아서 내가 직접 한 번 만들어보기로 했다.
목표는 위와 같다. 수준급의 개발환경까지 제공하지는 못 하더라도, 적당히 활용 가능할만한 수준의 개발환경을 제공해주는 것이 궁극적인 목표다.
이 프로젝트의 이름은 React Components Library Starter로 명명했다. 리액트 라이브러리 개발환경을 제공한다는 의미가 직관적으로 드러나길 원했다.
제품화된 소프트웨어나 솔루션이면 모를까, 이런 종류의 라이브러리는 그냥 봤을 때 "얘가 뭘 하는 라이브러리구나"라고 대충 감이 오는 게 제일 좋은 것 같다.
CRA를 사용하지 않으므로, 그냥 생짜 밑바닥에서부터 구축한다.
yarn을 기준으로 기술한다.
BASH
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
mkdir react-components-library-starter cd react-components-library-starter yarn init # question name (react-components-library-starter): # question version (1.0.0): # question description: # question entry point (index.js): # question repository url (https://github.com/itcode-dev/react-components-library-starter): # question author (RWB0104 <psj2716@gmail.com>): # question license (MIT): # question private: false mkdir src
BASH
1
yarn add -D react @types/react
이름 | 용도 |
---|---|
🔗 react | React 라이브러리 |
🔗 @types/react | React 라이브러리 타입 |
BASH
1
yarn add -D typescript
이름 | 용도 |
---|---|
🔗 typescript | TypeScript 라이브러리 |
BASH
1
vim tsconfig.json
JSON
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
{ "compilerOptions": { "target": "es5", "esModuleInterop": true, "forceConsistentCasingInFileNames": true, "strict": true, "skipLibCheck": true, "jsx": "react", "module": "ESNext", "declaration": true, "declarationDir": "./dist", "sourceMap": false, "outDir": "./dist", "moduleResolution": "node", "allowSyntheticDefaultImports": true, "emitDeclarationOnly": true, "removeComments": true }, "include": [ "./src" ], "exclude": [ "./dist", "./node_modules", "./src/**/*.test.tsx", "./src/**/*.stories.tsx", ] }
BASH
1 2
yarn add classnames style-inject yarn add -D postcss sass
이름 | 용도 |
---|---|
🔗 classnames | 클래스 속성 claaName 조인 활용을 위한 라이브러리 |
🔗 style-inject | 스타일 태그 헤더 삽입기 |
🔗 postcss | CSS 후처리기 |
🔗 sass | SASS/SCSS 라이브러리 |
BASH
1
yarn 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 연동 플러그인 |
BASH
1
vim rollup.config.js
JS
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62
/** * Rollup 설정 모듈 * * @author RWB * @since 2022.06.06 Mon 17:44:31 */ import babel from '@rollup/plugin-babel'; import commonjs from '@rollup/plugin-commonjs'; import { nodeResolve } from '@rollup/plugin-node-resolve'; import typescript from '@rollup/plugin-typescript'; import peerDepsExternal from 'rollup-plugin-peer-deps-external'; import postcss from 'rollup-plugin-postcss'; const extensions = [ 'js', 'jsx', 'ts', 'tsx', 'mjs' ]; const pkg = require('./package.json') const config = [ { external: [ /node_modules/ ], input: './src/index.ts', output: [ { dir: './dist', format: 'cjs', preserveModules: true, preserveModulesRoot: 'src' }, { file: pkg.module, format: 'es' } , { name: pkg.name, file: pkg.browser, format: 'umd' } ], plugins: [ nodeResolve({ extensions }), babel({ exclude: 'node_modules/**', extensions, include: [ 'src/**/*' ] }), commonjs({ include: 'node_modules/**' }), peerDepsExternal(), typescript({ tsconfig: './tsconfig.json' }), postcss({ extract: false, inject: (cssVariableName) => `import styleInject from 'style-inject';\nstyleInject(${cssVariableName});`, modules: true, sourceMap: false, use: [ 'sass' ] }) ] } ]; export default config;
BASH
1 2 3
npx storybook init --builder webpack5 yarn 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 처리기 |
설치만 하면 되는 건 아니고, 간단한 설정이 필요하다.
npx storybook init --builder webpack5를 수행하면 알아서 프로젝트에 Storybook을 설치해준다. 이 과정에서 최상단 경로에 .storybook 폴더를 생성한다.
.storybook/main.js에 아래와 같이 코드를 추가해준다.
JS
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
module.exports = { "stories": [ "../src/**/*.stories.mdx", "../src/**/*.stories.@(js|jsx|ts|tsx)" ], "addons": [ "@storybook/addon-links", "@storybook/addon-essentials", "@storybook/addon-interactions", // 추가 "@storybook/preset-scss" ], "framework": "@storybook/react", "core": { "builder": "@storybook/builder-webpack5" } }
코드의 일정한 규칙은 코드의 가독성을 향상시켜준다. 물론 개발자가 온전히 수작업으로 코드 컨벤션을 준수할 수도 있지만, 사람이 하는 일이다보니 실수가 발생하기도 하며, 코드 컨벤션와 일치하지 않는 코드를 일일히 찾는 것은 코드 퍼포먼스와 거의 연관성이 없음에도 많은 작업량을 요구한다. 더군다나 개발자가 여러명일 경우, 각자의 주관으로 인해 코드 컨벤션이 쉽게 망가질 우려가 있다.
ESLint를 활용하면 개발자가 신경쓰지 않아도 코드 컨벤션을 준수할 수 있다.
하지만 이는 코드 품질을 준수하기 위한 것으로, 코드의 퍼포먼스와 큰 연관성이 없으며, ESLint의 유무와 개발은 전혀 관련이 없다. 만약 이런 것까지 굳이 신경쓰고 싶지 않다면 이 문단은 넘어가도 무방하다. 향후 라이브러리 개발에 어떠한 영향도 미치지 않는다.
BASH
1
yarn 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 규칙 플러그인 |
JS
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106
module.exports = { env: { browser: true, node: true }, extends: [ 'airbnb', 'airbnb/hooks', 'eslint:recommended', 'plugin:react/recommended', 'plugin:import/recommended', 'plugin:storybook/recommended' ], ignorePatterns: [ '.storybook', '*.d.ts', 'node_modules', 'build', 'dist', '**/env/*.js' ], overrides: [ { files: [ '*.ts', '*.tsx' ], rules: { 'no-undef': 'off' } } ], parser: '@typescript-eslint/parser', parserOptions: { warnOnUnsupportedTypeScriptVersion: false }, plugins: [ '@typescript-eslint', 'sort-keys-fix', 'prettier' ], rules: { '@typescript-eslint/ban-ts-comment': [ 'error', { 'ts-ignore': 'allow-with-description' } ], '@typescript-eslint/no-explicit-any': 'warn', '@typescript-eslint/no-unused-vars': 'error', 'array-bracket-spacing': [ 'error', 'always', { arraysInArrays: false, objectsInArrays: false } ], 'brace-style': [ 'error', 'allman' ], 'comma-dangle': [ 'error', 'never' ], 'eol-last': [ 'error', 'never' ], 'import/extensions': 'off', 'import/named': 'off', 'import/no-anonymous-default-export': 'off', 'import/no-cycle': 'off', 'import/no-extraneous-dependencies': 'off', 'import/no-named-as-default': 'off', 'import/no-unresolved': 'off', 'import/order': [ 'error', { alphabetize: { caseInsensitive: true, order: 'asc' }, groups: [ 'external', 'builtin', 'internal', 'sibling', 'parent', 'index' ], 'newlines-between': 'always' } ], indent: [ 'error', 'tab' ], 'jsx-a11y/control-has-associated-label': 'off', 'jsx-quotes': [ 'error', 'prefer-single' ], 'linebreak-style': 'off', 'max-len': 'off', 'no-restricted-exports': 'off', 'no-tabs': [ 'error', { allowIndentationTabs: true }], 'no-unused-vars': 'off', 'object-curly-newline': [ 'error', { ExportDeclaration: 'never', ImportDeclaration: 'never', ObjectExpression: { minProperties: 3, multiline: true }, ObjectPattern: 'never' }], 'react-hooks/exhaustive-deps': 'warn', 'react/button-has-type': 'off', 'react/destructuring-assignment': 'off', 'react/function-component-definition': 'off', 'react/jsx-curly-brace-presence': [ 'error', { children: 'never', props: 'never' } ], 'react/jsx-filename-extension': 'off', 'react/jsx-indent': [ 'error', 'tab' ], 'react/jsx-props-no-spreading': 'off', 'react/jsx-sort-props': [ 'error', { callbacksLast: true, ignoreCase: true, multiline: 'last', noSortAlphabetically: false, reservedFirst: false, shorthandFirst: false, shorthandLast: true } ], 'react/prop-types': 'off', 'react/react-in-jsx-scope': 'off', 'react/require-default-props': 'off', 'require-jsdoc': 'off', 'sort-keys-fix/sort-keys-fix': 'error' }, settings: { 'import/parsers': { '@typescript-eslint/parser': [ '.ts', '.tsx', '.js' ] }, react: { version: 'detect' } } };
TXT
1 2 3 4 5
.storybook/ src/ rollup.config.js tsconfig.json yarn.lock
JSON
1 2 3 4 5 6 7 8 9 10 11 12
{ "name": "@itcode-dev/react-components-library-starter", "version": "3.0.1", "main": "./dist/index.js", "module": "./dist/index.es.js", "browser": "./dist/index.umd.js", "types": "./dist/index.d.ts", "private": false, "script": { "build": "rollup -c" } }
라이브러리를 배포한다.
BASH
1 2 3 4 5 6 7
npm login # username # password # email # email otp yarn publish --access public
배포한 프로젝트를 직접 설치하여 사용해보자.
BASH
1 2 3
npm i @itcode-dev/react-components-library-starter yarn add @itcode-dev/react-components-library-starter
위 명령어를 통해 라이브러리를 설치할 수 있다.
TSX
1 2
import Button from '@itcode-dev/react-components-library-starter/dist/atom/Button'; import Input from '@itcode-dev/react-components-library-starter/dist/atom/Input';
위와 같이 라이브러리를 활용할 수 있다.
이번 프로젝트는 규모는 작았지만, 연구할 게 많은 프로젝트였다. 찾아볼 건 많은데 규모는 작다보니 재밌게 했던 것 같다.
이 프로젝트 덕분에 새로운 걸 많이 알아갈 수 있었다.
그 밖에도 자잘자잘하게 얻은 게 많지 않나 싶다. 여러모로 보람찬 프로젝트였다.
그러고보니 저번에 자바 라이브러리도 배포한 적이 있던 거 같은데... npm랑 다르게 Maven은 배포 과정이 복잡했던걸로 기억한다.
지금 다시 하라고 하면 못 할거 같긴 한데.. 시간 날 때 그 것도 다시 한 번 정리해야 할 것 같다.
🏷️ Related Tag