Skip to content
On this page

2021-01-01

Title
2021-01-01
Category
2021
Tags
Aliases
2021-01-01
Created
3 years ago
Updated
last year

TypeScript + React + Storybook으로 디자인 시스템 구축하기

Design System vs Component Library

☝ 디자인 시스템은 계속 진화하는 재사용 가능한 구성 요소 모음이고, 일관성과 속도를 보장하는 규칙을 따르는 모든 제품을 개발하기 위한 단일 진실 공급원(SSOT)이다.

  • 디자인 시스템은 컴포넌트 라이브러리를 넘어서 디자인 원칙, 스타일 가이드, 패턴, 톤, 규칙과 명세서 등을 포함한다.

프로젝트 생성

  • .gitignore , package.json 생성

Storybook 설치하기

bash
npx -p @storybook/cli sb init --type react
npx -p @storybook/cli sb init --type react
  • storybook 시작하기
bash
yarn storybook
yarn storybook

React peer dependencies

@storybook/reactreact , react-dom 을 peer-dependency로 가지므로 react, react-dom 을 설치해야 실행할 수 있다. 설치하지 않으면 다음의 에러가 발생한다.

bash
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에 같은 라이브러리를 도 추가해도 무방하다.

bash
yarn add -D react react-dom
yarn add -D react react-dom
json
// 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"
}

storybookjs/storybook

Yarn

Duplicate same dependency in package.json devDependencies and peerDependencies?

TypeScript로 이전하기

  • typescript, react-docgen-typescript-loader 설치

    bash
    yarn 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 변경

    javascript
    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,
    		},
    	},
    };
    
    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"]
    }
    

TypeScript

Rollup으로 번들링하기

☝ 웹팩이 애플리케이션을 위한 번들러라면, 롤업은 라이브러리를 위한 번들러다.

webpack and Rollup: the same but different

Rollup 설정

  • 사용하는 플러그인
json
"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
javascript
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이란, 컴포넌트들에서 사용하고 있는 타입 정보들을 지니고 있는 파일.

이는 다음 명령어로 생성을 할 수 있다.

bash
tsc --emitDeclarationOnly
tsc --emitDeclarationOnly

이 명령어를 실행하기 전에 tsconfig.json 을 수정해주어야 한다.

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 커맨드를 추가한다.

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

package.json에서 module, types, files 를 추가한다. name은 scope를 사용한다.

json
{
	"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"]
}

배포 명령어

bash
npm publish --access public # scope를 사용할 때
npm 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

Released under the MIT License.