2021-01-01
TypeScript + React + Storybook으로 디자인 시스템 구축하기
Design System vs Component Library
☝ 디자인 시스템은 계속 진화하는 재사용 가능한 구성 요소 모음이고, 일관성과 속도를 보장하는 규칙을 따르는 모든 제품을 개발하기 위한 단일 진실 공급원(SSOT)이다.
- 디자인 시스템은 컴포넌트 라이브러리를 넘어서 디자인 원칙, 스타일 가이드, 패턴, 톤, 규칙과 명세서 등을 포함한다.
프로젝트 생성
.gitignore
,package.json
생성
Storybook 설치하기
npx -p @storybook/cli sb init --type react
npx -p @storybook/cli sb init --type react
- storybook 시작하기
yarn storybook
yarn storybook
React peer dependencies
@storybook/react
가 react
, react-dom
을 peer-dependency로 가지므로 react, react-dom 을 설치해야 실행할 수 있다. 설치하지 않으면 다음의 에러가 발생한다.
Error: Cannot find module 'react-dom/package.json'
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
yarn add -D react react-dom
// package.json
"peerDependencies": {
"react": "17.0.1",
"react-dom": "17.0.1",
"styled-components": "5.2.1"
}
// package.json
"peerDependencies": {
"react": "17.0.1",
"react-dom": "17.0.1",
"styled-components": "5.2.1"
}
Duplicate same dependency in package.json devDependencies and peerDependencies?
TypeScript로 이전하기
typescript, react-docgen-typescript-loader 설치
bashyarn add -D 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
변경javascriptmodule.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, }, }, };
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
추가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"] }
{ "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"] }
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를 번들링된 결과에 포함하지 않음
}
"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',
},
],
};
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
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"]
}
{
"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",
"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"]
}
{
"name": "@younho9/design-system",
"module": "dist/index.js",
"types": "dist/types/index.d.ts",
"files": ["/dist"]
}
배포 명령어
npm publish --access public # scope를 사용할 때
npm publish --access public # scope를 사용할 때
참고자료
Do you think your component library is your design system? Think again
TypeScript와 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