https://github.com/XionWCFM/component-lib
완성된 프로젝트는 위 레포지토리 링크에서 확인할 수 있습니다.
만드는 데 사용할 스택
NPM , vite , rollup , typescript, storybook, tailwindcss , react 를 사용합니다.
바닥부터 차근차근 만들어나가보도록 합시다.
적절한 폴더를 하나 생성해준 뒤 initialize를 해주겠습니다.
yarn init -y
{
"name": "new-vite",
"version": "1.0.0",
"main": "index.js",
"license": "MIT"
}
name의 경우에는 자신의 폴더명으로 생성될 것입니다.
패키지매니저는 크게 중요하지 않으니 자신이 사용하는 패키지매니저를 이용해주세요
저는 yarn 4.1 버전을 사용할 것이지만 다른 패키지 매니저를 사용해도 아무 상관없습니다.
yarn add -D vite typescript eslint prettier
먼저 개발하는데에 필요한 종속성들을 설치해주겠습니다.
{
"compilerOptions": {
"outDir": "./dist/",
"noImplicitAny": true,
"module": "ESNext",
"target": "es5",
"jsx": "react-jsx",
"allowJs": true,
"moduleResolution": "node",
"importHelpers": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"esModuleInterop": true,
"declaration": true,
"strictNullChecks": true,
"skipLibCheck": true
},
"include": ["src/**/*"],
"exclude": ["**/*.stories.ts", "**/*.stories.tsx", "stories/**/*"]
}
tsconfig.json은 취향껏 작성해주셔도 좋지만 저는 저렇게 작성해주도록 하겠습니다.
조금 유념하시면서 작성해야하는 부분은
outDir / jsx / allowSyntheticDefaultImports / esMouduleInterop 입니다.
이부분은 궁금하시면 찾아보시길 바라요
yarn add -D @types/react @types/react-dom autoprefixer postcss tailwindcss react react-dom
이번엔 리액트와 테일윈드를 설치해주겠습니다.
테일윈드 외에 다른 css 도구를 사용하는 경우에는 사용하는 도구를 설치하세요
npx tailwindcss init
yarn dlx tailwindcss init
npx 혹은 yarn dlx 명령어를 통해 tailwindcss 초기 설정을 수행합니다.
성공적으로 수행되었다면 tailwind.confg.js 설정 파일이 생겨있을 것입니다.
npx storybook@latest init
yarn dlx storybook@latest init
storybook을 설치해주겠습니다. 간단한 명령어만 입력하면 사용가능합니다.
"scripts": {
"storybook": "storybook dev -p 6006",
"build-storybook": "storybook build"
}
성공적으로 수행되었다면 package.json의 script에 다음과 같이 스크립트가 생겨있는걸 볼 수 있습니다.
"@chromatic-com/storybook": "^1.2.22",
"@storybook/addon-essentials": "^8.0.0",
"@storybook/addon-interactions": "^8.0.0",
"@storybook/addon-links": "^8.0.0",
"@storybook/addon-onboarding": "^8.0.0",
"@storybook/blocks": "^8.0.0",
"@storybook/react": "^8.0.0",
"@storybook/react-vite": "^8.0.0",
"@storybook/test": "^8.0.0",
글을 작성한 시점에서의 버전과 차이는 있을 수 있지만 여러가지 개발의존성들이 생긴것 또한 확인할 수 있을 것입니다.
yarn add -D @storybook/builder-vite
빌더를 웹팩에서 vite로 변경해주도록 하겠습니다.
이 작업은 수행하지 않아도 좋습니다.
import type { StorybookConfig } from '@storybook/react-vite';
import { join, dirname } from 'path';
/**
* This function is used to resolve the absolute path of a package.
* It is needed in projects that use Yarn PnP or are set up within a monorepo.
*/
function getAbsolutePath(value: string): any {
return dirname(require.resolve(join(value, 'package.json')));
}
const config: StorybookConfig = {
stories: ['../stories/**/*.mdx', '../stories/**/*.stories.@(js|jsx|mjs|ts|tsx)'],
addons: [
getAbsolutePath('@storybook/addon-onboarding'),
getAbsolutePath('@storybook/addon-links'),
getAbsolutePath('@storybook/addon-essentials'),
getAbsolutePath('@chromatic-com/storybook'),
getAbsolutePath('@storybook/addon-interactions')
],
framework: {
name: getAbsolutePath('@storybook/react-vite'),
options: {}
},
docs: {
autodocs: 'tag'
},
core: {
builder: '@storybook/builder-vite' // 👈 The builder enabled here.
}
};
export default config;
.storybook/main.ts 에 들어가 core 키에 builder를 builder-vite로 추가해주겠습니다.
tailwindcss를 storybook에서 적절히 사용해주기 위해서는 약간의 설정이 필요합니다.
/** @type {import('tailwindcss').Config} */
module.exports = {
content: ['./stories/**/*.{js,ts,jsx,tsx,mdx}', './src/**/*.{js,ts,jsx,tsx,mdx}'],
theme: {
extend: {}
},
plugins: []
};
tailwind.config.js의 content 부분을 다음과 같이 수정하겠습니다.
content는 tailwind가 적용되어야할 범위를 지정하는 것이라고 생각하면 편합니다.
불필요한 파일들까지 전부 watch 하면 프로젝트가 커지게 될 수록 성능이 저하될 수 있으니
필요한 콘텐츠들만 포함시켜주면 되겠습니다. src 폴더를 만들고 관리할 예정이니 저렇게 넣었지만
만약 자신만의 폴더 컨벤션이 있다면 그 폴더를 사용해주면 되겠습니다.
:root {
--primary: rgb(189, 40, 189);
}
@tailwind base;
@tailwind components;
@tailwind utilities;
src/tailwind.css
src 폴더를 생성하고 css 파일을 만들어주겠습니다.
:root에는 css 변수를 하나 지정해주었는데요 이 행동에 대한 설명은 이후에 하겠습니다.
import type { Preview } from '@storybook/react';
import '../src/tailwind.css';
const preview: Preview = {
parameters: {
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/i
}
}
}
};
export default preview;
.storybook/preview.ts 파일로 진입하여 아까 만들어둔 css 파일을 임포트합니다.
이 행동을 통하여 스토리북에서 tailwindcss를 사용한다는 것을 이해할 수 있게됩니다.
{
"parser": "@typescript-eslint/parser",
"extends": ["eslint:recommended", "plugin:@typescript-eslint/recommended", "plugin:react/recommended"],
"plugins": ["@typescript-eslint", "react"],
"env": {
"browser": true,
"node": true,
"es6": true
},
"parserOptions": {
"ecmaFeatures": {
"jsx": true
},
"ecmaVersion": 2018,
"sourceType": "module"
},
"rules": {}
}
아까 빼먹고갔었는데 .eslintrc를 생성하여 eslint 설정도 해주겠습니다
이부분은 꼭 제 설정을 따라할 필요는 없고 자신이 사용하는 설정을 사용해도 좋습니다
const config: StorybookConfig = {
stories: [
'../stories/**/*.mdx',
'../stories/**/*.stories.@(js|jsx|mjs|ts|tsx)',
'../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'
],
더불어 .storybook/main.ts에 진입하여 stories의 탐색범위에 src 폴더도 지정해주겠습니다.
여기에서의 stories 설정은 아까 tailwind.config.js에서의 content 설정과 유사하다고 볼 수 있습니다.
theme: {
extend: {
colors: {
primary: 'var(--primary)'
}
}
},
tailwind.config.js로 진입하여 theme를 확장해주겠습니다. extend 뒤에 색상을 primary를 추가 정의하며
이 primary 색상은 아까 정의한 css 변수를 따라가게 하겠습니다.
이제 src/components 폴더를 생성하고 button.tsx를 생성합니다.
export const XionButton = () => {
return <button className=" px-4 py-2 bg-primary rounded-sm text-white">안녕하세요</button>;
};
내용은 다음과 같이 간단한 버튼이며 테일윈드를 통해 스타일을 정의할 뿐입니다.
import type { Meta, StoryObj } from '@storybook/react';
import { fn } from '@storybook/test';
import { XionButton } from './button';
// More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export
const meta = {
title: 'Xion/Button',
component: XionButton,
parameters: {
// Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/configure/story-layout
layout: 'centered'
},
// This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/writing-docs/autodocs
tags: ['autodocs'],
// More on argTypes: https://storybook.js.org/docs/api/argtypes
argTypes: {
backgroundColor: { control: 'color' }
},
// Use `fn` to spy on the onClick arg, which will appear in the actions panel once invoked: https://storybook.js.org/docs/essentials/actions#action-args
args: { onClick: fn() }
} satisfies Meta<typeof XionButton>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Small: Story = {
args: {
label: 'Button'
}
};
이제 스토리를 작성해주겠습니다.
button.stories.tsx를 만들고 다음과 같이 작성합니다.
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {}
}
};
postcss.config.js를 생성하고 다음과같이 작성합니다.
postcss는 tailwindcss의 짝궁처럼 같이 쓰이는 도구입니다.
아까 tailwindcss를 설치할 때 같이 설치했던 것을 기억하실거에요
이제 여기까지 수행한 뒤 스토리북 서버를 다시 실행해보겠습니다.
yarn storybook
디자인이 조금 많이 못생겼지만 중요한 것은 storybook에서 tailwindcss가 적용되었다는 것이며
큰 문제없이 나타나고 있다는 것입니다.
import { XionButton } from './components/button';
export { XionButton };
src 폴더에 진입점을 만들어주겠습니다.
src/index.ts를 만들고 다음과 같이 작성해주세요
이 진입점을 통하여 빌드를 수행할것이기 때문입니다.
빌드 도구는 여러가지 선택지가 존재하는데요 이번에는 rollup을 이용하여 빌드하도록 하겠습니다.
빌드도구는 취향에 따라 여러가지 선택지가 있으니 취향껏 쓰세요
yarn add -D @rollup/plugin-commonjs @rollup/plugin-node-resolve rollup r
ollup-plugin-terser rollup-plugin-typescript2 tslib
롤업을 이용하여 빌드할예정이니 다음과 같이 롤업과 빌드에 필요한 플러그인들을 설치해주겠습니다.
import resolve from '@rollup/plugin-node-resolve';
import commonjs from '@rollup/plugin-commonjs';
import typescript from 'rollup-plugin-typescript2';
import { terser } from 'rollup-plugin-terser';
export default {
input: 'src/index.ts', // 라이브러리의 진입점
output: [
{
file: 'dist/bundle.esm.js', // ESM 형태로 빌드 결과
format: 'es'
},
{
file: 'dist/bundle.cjs.js', // CommonJS 형태로 빌드 결과
format: 'cjs'
}
],
plugins: [resolve(), commonjs(), typescript(), terser()] // 플러그인 설정
};
rollup.config.mjs를 생성합니다.
mjs를 이용하는 이유는 module import 방식을 사용한다는 것을 명시하기 위함입니다.
파일명을 건드리는게 너무 싫다 하시는 분들은 다른 선택지도 있으니 이것저것 시도해보시기 바랍니다.
"rollup": "rollup -c"
package.json으로 가서 다음과 같이 script를 추가해줍니다.
성공적으로 빌드가 되었다면 dist 폴더가 생성되며 그 안에 빌드 결과물이 저장되었을 것입니다.
빌드 결과물을 확인해볼까요?
분명 우리는 세줄짜리 컴포넌트밖에 작성하지않았는데 빌드결과물은 엄청나게 많습니다.
이것은 react와 react-dom 코드가 함께 섞여있기 때문인데요
우리의 디자인시스템을 사용하는 측에서 react 와 react-dom은 당연히 있을것이라고 기대하기 때문에
빌드할때 이를 포함하는것은 비효율적일것입니다.
rollup 진영에는 이런 상황을 위한 플러그인을 제공하고 있습니다.
yarn add -D rollup-plugin-peer-deps-external
import PeerDepsExternalPlugin from 'rollup-plugin-peer-deps-external';
plugins: [PeerDepsExternalPlugin(), resolve(), commonjs(), typescript(), terser()] // 플러그인 설정
rollup.config.mjs로 돌아가서 다음과 같이 import를 작성하고 플러그인에 피어뎁스플러그인을 추가해주겠습니다.
"peerDependencies": {
"react": ">=16.0.0",
"react-dom": ">=16.0.0"
},
그리고 package.json에서 동료의존성을 지정해줄게요
>=는 이상이라는 의미입니다. 리액트 버전이 16이상이면 okay 라는 의미에요
다시 빌드를 돌려보죠
import{jsx as r}from"react/jsx-runtime";var t=function(){return r("button",{className:" px-4 py-2 bg-primary rounded-sm text-white",children:"안녕하세요"})};export{t as XionButton};
확연히 코드가 짧아졌고 우리가 작성한 코드만 남은것을 확인할 수 있습니다.
CSS도 빌드하기
디자인시스템에서 CSS를 관리하는 방법을 크게 나누면 두가지로 생각합니다.
1. css 를 별도로 빌드하여 배포하기
2. 컴포넌트에 각각 styles 혹은 css를 미리 입혀둔 상태로 배포하기
각각의 장단점이 있는 방식인데요
먼저 1번 방식의 경우 사용자 입장에서 css를 따로 import 하는 수고가 생깁니다.
반면 사용자 입장에서 css를 입힐지 안입힐지를 취사선택할 수 있어지고
또 특정 css를 오버라이딩하는 것이 용이해진다는 장점도 있습니다.
한편 2번 방식의 경우 사용자는 그냥 컴포넌트만 import 하면 되기에 상대적으로 수고가 적습니다.
각 방식은 각자 좋다고 생각하는 방식을 선택하시면 되겠습니다.
저는 1번 방식을 이용해주도록 하겠습니다.
tailwindcss는 build time css이며 자체적으로 빌드할 수 있는 수단을 제공합니다.
"build:css": "tailwindcss build src/tailwind.css -o dist/xds.css --minify",
다음과 같은 스크립트를 작성하고 실행시켜보세요
dist/xds.css 는 생성될 위치와 파일명을 지정하는 것입니다.
마음에 드는 파일명으로 변경해도 좋아요
"exports": {
".": {
"types": "./dist/index.d.ts",
"require": "./dist/bundle.cjs.js",
"import": "./dist/bundle.esm.js",
"default": "./dist/bundle.esm.js"
},
"./xds": "./dist/xds.css"
},
이제 라이브러리 사용자입장에서의 진입점을 만들어주겠습니다.
npm 배포에 대한 다른 좋은 글들이 이미 많으니 배포 방법은 굳이 다루진 않겠습니다.
저는 xion-test-comp라는 이름으로 npm에 배포를 해두었습니다.
npm i xion-test-comp
import "xion-test-comp/xds";
import { XionButton } from "xion-test-comp";
<XionButton />
이런 형태로 사용해줄 수 있습니다.
@tailwind base;
@tailwind components;
@tailwind utilities;
:root {
--primary: rgb(76, 41, 183);
}
아까와 같이 css 변수 형태로 디자인 시스템을 제어하면 재밌는 점이
이렇게 css 변수를 오버라이딩 하는 것을 통하여 각 프로젝트마다 커스터마이징이 가능하다는 것인데요
물론 이러한 방법을 사용하게되면 전역 css 스코프가 더럽혀지기 때문에 장단이 있습니다.
마치며
스토리북과 npm을 이용하여 디자인 시스템을 구축하는 기초적인 방법을 다루었습니다.
이제 기반이 될 것들은 모두 갖추어졌기 때문에 여기에서 선택적으로
스토리북 크로마틱 배포 | npm 프라이빗 배포 그리고 그 배포 과정들을
ci / cd를 통하여 자동화하기 등등을 수행할 수 있을 것입니다.
이런 컴포넌트 라이브러리를 만드는 과정을 담은 아티클들이 1, 2년 전에 작성된 경우가 많아
2024년의 모던한 버전으로 작성해두고 싶다는 생각이 있었는데요
모쪼록 도움이 되셨으면 좋겠습니다.
감사합니다.
'frontend' 카테고리의 다른 글
내가 쓰는 프론트엔드 코딩 컨벤션과 네이밍 컨벤션 폴더구조 (0) | 2024.05.10 |
---|---|
next.js mdx fastrefresh not working problem (4) | 2024.04.21 |
늦은 밤에 도메인 죽은 썰 푼다 (1) | 2024.01.19 |
짧은 zed editor 사용후기 (2) | 2024.01.15 |
react-native expo router 시작해보기 (0) | 2024.01.07 |