๋ณธ๋ฌธ์œผ๋กœ ๊ฑด๋„ˆ๋›ฐ๊ธฐ

"Storybook" ํƒœ๊ทธ๋กœ ์—ฐ๊ฒฐ๋œ 1๊ฐœ ๊ฒŒ์‹œ๋ฌผ๊ฐœ์˜ ๊ฒŒ์‹œ๋ฌผ์ด ์žˆ์Šต๋‹ˆ๋‹ค.

๋ชจ๋“  ํƒœ๊ทธ ๋ณด๊ธฐ

ยท ์•ฝ 7๋ถ„

TypeScript + React + Storybook์œผ๋กœ ๋””์ž์ธ ์‹œ์Šคํ…œ ๊ตฌ์ถ•ํ•˜๊ธฐ#

Design System vs Component Library#

โ˜ ๋””์ž์ธ ์‹œ์Šคํ…œ์€ ๊ณ„์† ์ง„ํ™”ํ•˜๋Š” ์žฌ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ ๊ตฌ์„ฑ ์š”์†Œ ๋ชจ์Œ์ด๊ณ , ์ผ๊ด€์„ฑ๊ณผ ์†๋„๋ฅผ ๋ณด์žฅํ•˜๋Š” ๊ทœ์น™์„ ๋”ฐ๋ฅด๋Š” ๋ชจ๋“  ์ œํ’ˆ์„ ๊ฐœ๋ฐœํ•˜๊ธฐ ์œ„ํ•œ ๋‹จ์ผ ์ง„์‹ค ๊ณต๊ธ‰์›(SSOT)์ด๋‹ค.

  • ๋””์ž์ธ ์‹œ์Šคํ…œ์€ ์ปดํฌ๋„ŒํŠธ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ๋„˜์–ด์„œ ๋””์ž์ธ ์›์น™, ์Šคํƒ€์ผ ๊ฐ€์ด๋“œ, ํŒจํ„ด, ํ†ค, ๊ทœ์น™๊ณผ ๋ช…์„ธ์„œ ๋“ฑ์„ ํฌํ•จํ•œ๋‹ค.

ํ”„๋กœ์ ํŠธ ์ƒ์„ฑ#

  • .gitignore , package.json ์ƒ์„ฑ

Storybook ์„ค์น˜ํ•˜๊ธฐ#

npx -p @storybook/cli sb init --type react
  • storybook ์‹œ์ž‘ํ•˜๊ธฐ
yarn storybook

React peer dependencies#

@storybook/react ๊ฐ€ react , react-dom ์„ peer-dependency๋กœ ๊ฐ€์ง€๋ฏ€๋กœ react, react-dom ์„ ์„ค์น˜ํ•ด์•ผ ์‹คํ–‰ํ•  ์ˆ˜ ์žˆ๋‹ค. ์„ค์น˜ํ•˜์ง€ ์•Š์œผ๋ฉด ๋‹ค์Œ์˜ ์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค.

Error: Cannot find module 'react-dom/package.json'

ํ•˜์ง€๋งŒ, dependency ์— ์ง์ ‘ ์ถ”๊ฐ€ํ•˜๋ฉด, ๊ฐœ๋ฐœํ•œ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์„ค์น˜ํ•œ ์œ ์ €๊ฐ€ react , react-dom ์—ญ์‹œ ์„ค์น˜ ๋ฐ›๊ฒŒ ๋œ๋‹ค. (๊ฒŒ๋‹ค๊ฐ€ ์ •ํ•ด์ง„ ๋ฒ„์ „์œผ๋กœ)

๋”ฐ๋ผ์„œ ์ปดํฌ๋„ŒํŠธ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์ž‘์„ฑํ•˜๋Š” ์ž‘์„ฑ์ž ์ž…์žฅ์—์„œ๋Š” storybook์„ ์‹คํ–‰ํ•˜๋Š” ๊ฐœ๋ฐœ ์‹œ์—๋งŒ ์ด ์˜์กด์„ฑ์ด ํ•„์š”ํ•˜๋ฏ€๋กœ devDependency์— ๋ช…์‹œํ•ด์•ผํ•œ๋‹ค.

๋˜ํ•œ, ์ด ์ปดํฌ๋„ŒํŠธ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ์‚ฌ์šฉ์ž ์ž…์žฅ์—์„œ๋Š” react , react-dom ๊ณผ ํ•จ๊ป˜ ์‚ฌ์šฉํ•ด์•ผ ํ•˜๊ธฐ ๋•Œ๋ฌธ์— ๋”ฐ๋ผ์„œ peerDependency์—๋„ ๋ช…์‹œํ•ด์•ผํ•œ๋‹ค.

peerDependency๋Š” ์ด ์˜์กด์„ฑ๊ณผ ํ•จ๊ป˜ ์‚ฌ์šฉํ•ด์•ผ ํ•œ๋‹ค๋Š” ๋œป์ด๊ณ , ์œ ์ €๊ฐ€ ์ง์ ‘ ์„ค์น˜ ํ•ด์•ผํ•˜๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค.

devDependency๋Š” ํ”„๋กœ์ ํŠธ์˜ ๋กœ์ปฌ์—๋งŒ ์„ค์น˜๋˜๊ณ  ๋ฐฐํฌ์‹œ์—๋Š” ์œ ์ €๊ฐ€ ๋‹ค์šด๋กœ๋“œํ•˜์ง€ ์•Š๊ธฐ๋•Œ๋ฌธ์— peerDependency์™€ devDependency์— ๊ฐ™์€ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ๋„ ์ถ”๊ฐ€ํ•ด๋„ ๋ฌด๋ฐฉํ•˜๋‹ค.

yarn add -D react react-dom
// package.json"peerDependencies": {  "react": "17.0.1",  "react-dom": "17.0.1",  "styled-components": "5.2.1"}

storybookjs/storybook

Yarn

Duplicate same dependency in package.json devDependencies and peerDependencies?

TypeScript๋กœ ์ด์ „ํ•˜๊ธฐ#

  • typescript, react-docgen-typescript-loader ์„ค์น˜

    yarn add -D typescript react-docgen-typescript-loader
  • stories typescript ๋ฒ„์ „์œผ๋กœ ๋ณ€๊ฒฝ

    create-react-app typescript ํ…œํ”Œ๋ฆฟ์— sb init ์œผ๋กœ ์ƒ์„ฑํ•œ ts ๋ฒ„์ „ stories๋กœ ํ…Œ์ŠคํŠธ

  • .storybook/main.js ๋ณ€๊ฒฝ

    module.exports = {    stories: [        '../stories/**/*.stories.mdx',        '../stories/**/*.stories.@(js|jsx|ts|tsx)',    ],    addons: ['@storybook/addon-links', '@storybook/addon-essentials'],    typescript: {        check: false,        checkOptions: {},        reactDocgen: 'react-docgen-typescript',        reactDocgenTypescriptOptions: {            shouldExtractLiteralValuesFromEnum: true,            propFilter: (prop) =>                prop.parent ? !/node_modules/.test(prop.parent.fileName) : true,        },    },};
  • tsconfig.json ์ถ”๊ฐ€

    {    "compilerOptions": {        "target": "es5",        "lib": ["dom", "dom.iterable", "esnext"],        "allowJs": true,        "skipLibCheck": true,        "esModuleInterop": true,        "allowSyntheticDefaultImports": true,        "strict": true,        "forceConsistentCasingInFileNames": true,        "noFallthroughCasesInSwitch": true,        "module": "esnext",        "moduleResolution": "node",        "resolveJsonModule": true,        "isolatedModules": true,        "noEmit": true,        "jsx": "react-jsx"    },    "include": ["stories"]}

TypeScript

Rollup์œผ๋กœ ๋ฒˆ๋“ค๋งํ•˜๊ธฐ#

โ˜ ์›นํŒฉ์ด ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์„ ์œ„ํ•œ ๋ฒˆ๋“ค๋Ÿฌ๋ผ๋ฉด, ๋กค์—…์€ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์œ„ํ•œ ๋ฒˆ๋“ค๋Ÿฌ๋‹ค.

Webpack and Rollup: the same but different

Rollup ์„ค์ •#

  • ์‚ฌ์šฉํ•˜๋Š” ํ”Œ๋Ÿฌ๊ทธ์ธ
"devDependencies": {  "babel-preset-react-app": "10.0.0", // create-react-app์—์„œ ์‚ฌ์šฉํ•˜๋Š” babel ์„ค์ •  "rollup": "2.35.1",  "rollup-plugin-babel": "4.4.0", // babel ์‚ฌ์šฉ์„ ์œ„ํ•œ ํ”Œ๋กœ๊ทธ์ธ  "rollup-plugin-cleaner": "1.0.0", // build ์ „์— dist ํด๋” ์‚ญ์ œ  "rollup-plugin-commonjs": "10.1.0", // CommonJS์˜ ๋ชจ๋“ˆ ์ฝ”๋“œ๋ฅผ ES6๋กœ ๋ณ€ํ™˜ํ•˜์—ฌ ๊ฒฐ๊ณผ๋ฌผ์— ํฌํ•จ  "rollup-plugin-node-resolve": "5.2.0", // ์จ๋“œํŒŒํ‹ฐ ๋ชจ๋“ˆ์„ ์‚ฌ์šฉํ•˜๊ธฐ์œ„ํ•œ ์šฉ๋„  "rollup-plugin-peer-deps-external": "2.2.4", // peerDependencies๋ฅผ ๋ฒˆ๋“ค๋ง๋œ ๊ฒฐ๊ณผ์— ํฌํ•จํ•˜์ง€ ์•Š์Œ}
  • rollup.config.js
import commonjs from 'rollup-plugin-commonjs';import cleaner from 'rollup-plugin-cleaner';import resolve from 'rollup-plugin-node-resolve';import babel from 'rollup-plugin-babel';import external from 'rollup-plugin-peer-deps-external';import peerDepsExternal from 'rollup-plugin-peer-deps-external';
import pkg from './package.json';
const extensions = ['.js', '.jsx', '.ts', '.tsx'];
process.env.BABEL_ENV = 'production';
export default {    input: './src/index.ts',    plugins: [        cleaner({targets: ['./dist/']}),        peerDepsExternal(),        resolve({extensions}),        commonjs({            include: 'node_modules/**',        }),        babel({            extensions,            include: ['src/**/*'],            presets: [['react-app', {flow: false, typescript: true}]],            runtimeHelpers: true,        }),    ],    output: [        {            file: pkg.module,            format: 'es',        },    ],};

tsconfig.json & package.json ์„ค์ •#

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 ์ปค๋งจ๋“œ๋ฅผ ์ถ”๊ฐ€ํ•œ๋‹ค.

"build": "rollup -c && tsc --emitDeclarationOnly",

package.json์—์„œ module, types, files ๋ฅผ ์ถ”๊ฐ€ํ•œ๋‹ค. name์€ scope๋ฅผ ์‚ฌ์šฉํ•œ๋‹ค.

{    "name": "@younho9/design-system",    "module": "dist/index.js",    "types": "dist/types/index.d.ts",    "files": ["/dist"]}

๋ฐฐํฌ ๋ช…๋ น์–ด

# npm publishnpm publish --access public # scope๋ฅผ ์‚ฌ์šฉํ•  ๋•Œ

์ฐธ๊ณ ์ž๋ฃŒ#

Do you think your component library is your design system? Think again

TypeScript์™€ Storybook์„ ์‚ฌ์šฉํ•œ ๋ฆฌ์•กํŠธ ๋””์ž์ธ ์‹œ์Šคํ…œ ๊ตฌ์ถ•ํ•˜๊ธฐ

๋””์ž์ธ ์‹œ์Šคํ…œ ์†Œ๊ฐœ

storybookjs/design-system

storybookjs/storybook

How to create a react component library with TypeScript, rollup.js and Storybook

Building a Design System Package With Storybook, TypeScript, and React in 15 Minutes

Creating and publishing scoped public packages | npm Docs