본글에서는 react, vite를 이용해서 라이브러리를 만드는 방법에 대해 작성하고자 합니다

Bundle?

웹 개발 및 프론트엔드 개발에서 주로 사용되는 용어로, 일반적으로 여러 파일을 하나로 결합 하는 작업을 나타낸다

이렇게 하나의 파일로 결합하는 것을 "번들링" 이라고도 하는데 번들링은 주로 JavaScript 및 CSS 파일을 하나의 파일로 결합하는 것을 의미한다.

이를 통해 웹 애플리케이션의 성능을 최적화하고 관리를 더 쉽게 만드는데 도움이 된다. 

왜 Vite 인가?

번들링 시, Rollup 기반의 다양한 빌드 커맨드를 사용할 수 있습니다.

이는 높은 수준으로 최적화된 정적(Static) 리소스들을 배포할 수 있게끔 하며, 미리 정의된 설정(Pre-configured)을 제공합니다.

또한 스토리북에서도 vite를 기반으로 연동할수 있어 편리하게 스토리북까지 이용가능합니다.

Vite Config & Build

config 구성하는 방법은 다음과 같습니다

1. vite project 생성

pnpm create vite {package name}

2. package install

# react
pnpm add react react-dom

# vite config package
pnpm add -D vite vite-plugin-dts @vitejs/plugin-react vite-tsconfig-paths rollup-preserve-directives glob @vanilla-extract/vite-plugin

# vanilla-extract package
pnpm add -D @vanilla-extract/css

3. vite.config.ts 설정

import { extname, relative, resolve } from 'path';
import { fileURLToPath } from 'url';
import { vanillaExtractPlugin } from '@vanilla-extract/vite-plugin';
import react from '@vitejs/plugin-react';
import { glob } from 'glob';
import preserveDirectives from 'rollup-preserve-directives';
import { defineConfig } from 'vite';
import dts from 'vite-plugin-dts';
import tsconfigPaths from 'vite-tsconfig-paths';
import packageJson from './package.json';

export default defineConfig({
  plugins: [
    react(),
    dts({
      rollupTypes: true, // index.d.ts로 병합
    }),
    tsconfigPaths(),
    vanillaExtractPlugin({
      identifiers: 'debug',
    }),
    preserveDirectives(), // use client 유지
  ],
  build: {
    lib: {
      entry: resolve(__dirname, 'src/index.ts'),
      formats: ['es'],
    },
    rollupOptions: {
      external: [
        ...Object.keys(packageJson.peerDependencies || {}),
        ...Object.keys(packageJson.dependencies || {}),
        'react/jsx-runtime',
      ],
      input: Object.fromEntries(
        glob
          .sync(['src/**/*.{ts,tsx}'], {
            ignore: ['src/**/*.d.ts', 'src/**/*.stories.{ts,tsx}'],
          })
          .map(file => {
            return [
              relative('src', file.slice(0, file.length - extname(file).length)),
              fileURLToPath(new URL(file, import.meta.url)),
            ];
          }),
      ),
      output: {
        chunkFileNames: 'chunk/[name].js', // 외부모듈 관련 파일 chunk/모듈명.js 로 생성
        assetFileNames: 'theme.css', // 전체 css는 dist/theme.css에 생성됩니다
        entryFileNames(info) {
          if (!info.exports.length) {
            return 'rmdir/[name].js';
          }
          return '[name].js'; // 모든 파일의 이름을 [파일명].js로 지정합니다
        },
      },
    },
  },
});

4. package.json 수정

{
  "name": "@breadlee/ui",
  "version": "0.0.0",
  "sideEffects": false,
  "type": "module",
  "exports": {
    ".": {
      "import": "./dist/index.js",
      "types": "./dist/index.d.ts"
    },
    "./theme.css": "./dist/theme.css",
    "./reset.css": "./dist/reset.css"
  },
  "main": "dist/index.js",
  "module": "dist/index.js",
  "types": "dist/index.d.ts",
  "scripts": {
    "prebuild": "[ ! -d dist ] && mkdir -p dist || rm -rf dist/*",
    "build": "vite build",
    "postbuild": "rm -rf dist/rmdir",
    ...
  },
  "dependencies": {
  	...
  }
  "peerDependencies": {
	...
    "react": "^19.0",
    "react-dom": "^19.0"
  },
  "peerDependenciesMeta": {
 	...
  }
}

 

5. build

pnpm build

 

사용

pnpm add @breadlee/ui

import '@breadlee/ui/reset.css';
import '@breadlee/ui/theme.css';
import { ThemeProvider } from 'next-themes';
import { Typography } from '@breadlee/ui';

export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  return (
    <html lang='ko'>
      <body>
        <ThemeProvider>
        	{children}
            <Typography>test</Typography>
        </ThemeProvider>
      </body>
    </html>
  );
}

input 의 value를 변경하기 위해선 다음과 같이 코드를 작성합니다

const ControlledComponent = () => {
  const [value, setValue] = useState('');

  const handleSubmit = () => {
    onSubmit(value);
  };
  

  return (
    <form onSubmit={handleSubmit}>
      <input type='text' value={value} onChange={e => setValue(e.target.value)} />
      <button type='submit'>Submit</button>
    </form>
  );
};

const UnControlledComponent = () => {
  const inputRef = useRef();

  const handleSubmit = () => {
    const value = inputRef.current.value;
    onSubmit(value);
  };

  return (
    <form onSubmit={handleSubmit}>
      <input type='text' ref={inputRef} />
      <button type='submit'>Submit</button>
    </form>
  );
}

ControlledComponent에선 state를 사용하여 value를 변경중이고 UnControlledComponent는 input tag의 기본기능을 활용하여 value를 변경합니다

Controlled Component vs UnControlled Component

구분 Controlled Component UnControlled Component
정의 컴포넌트의 상태가 React에 의해 제어되는 컴포넌트 컴포넌트의 상태가 DOM 자체에 의해 제어되는 컴포넌트
데이터 흐름 단방향 데이터 흐름 (부모 -> 자식) 양방향 데이터 흐름 (DOM -> React)
장점 - 상태를 명확하게 관리할 수 있어 디버깅이 용이함
- 입력값에 대한 즉각적인 유효성 검사가 가능함
- 모든 상태 변화가 React의 상태로 관리되므로 예측 가능성이 높음
- 초기 설정이 간단하고 빠름
- 외부 라이브러리와의 통합이 쉬움
단점 - 코드가 복잡해질 수 있음
- 성능에 영향을 줄 수 있음 (특히 많은 양의 데이터 처리 시)

- 상태를 추적하기 어려움
- 입력값의 유효성 검사가 어려움
- 초기값 설정이 복잡할 수 있음

사용 예시 - 폼 데이터가 복잡하고, 입력값에 대한 즉각적인 피드백이 필요한 경우 - 간단한 폼이나 초기값이 중요하지 않은 경우

 

react-hook-form 사용이유?

UnControlled Component를 사용하면 react에 의해 제어되지않아 데이터 관리가 어려울수 있습니다

form내에서 사용되는  input, select, textarea 같은 컴포넌트는 submit시 value값들을 가져오는데 어려움을 겪을수 있는데요

하지만 https://react-hook-form.com/과 같이 라이브러리를 이용한다면 좀더 수월하게 UnControlled Component를 다룰수 있고

form의 최적화도 할수 있게 됩니다

 

참고

Virtual DOM

Virtual DOM은 UI의 이상적인 또는 “가상”적인 표현을 메모리에 저장하고 ReactDOM과 같은 라이브러리에 의해 “실제” DOM과 동기화하는 프로그래밍 개념입니다

VirtualDom

 

리액트의 렌더링 과정

리액트의 렌더링 프로세스는 어플리케이션 트리 안에 있는 모든 컴포넌트들이 현재 자신들이 가지고 있는 props와 state의 값을 기반으로 어떻게 UI를 구성하고 이를 바탕으로 어떤 DOM 결과를 브라우저에 제공할 것인지 계산하는 일련의 과정을 의미하며 총 3 가지로 나뉩니다

  1. 렌더링을 유발하는 단계로 createRoot의 실행 혹은 state를 업데이트하게 되면 발생한다.
  2. 렌더링단계로 컴포넌트를 호출하는 단계이다. createRoot로 렌더링이 발생했다면 루트 컴포넌트를 호출하고 state의 업데이트로 인한 렌더링이라면 해당 state가 속해있는 컴포넌트를 호출한다.
  3. 커밋 단계로 변경사항들을 실제 DOM에 적용하는 작업을 진행한다. 첫 커밋이라면 appendChild를 사용해서 스크린에 있는 모든 노드를 생성한다. 만약 첫 커밋이 아니라면 최소한의 작업을 통해 변경사항만을 실제 DOM에 적용한다. 그리고 이 변경사항은 렌더링 중에 계산된다.

렌더링이 일어나는 이유

  1. 최초 렌더링 
  2. 리렌더링
    1. state가 변경되었을때
    2. props가 변경되었을때
    3. 컴포넌트의 key props가 변경되었을때

JSX 문법이 해석 되는과정

1. 컴포넌트 작성

function Component(){
  return(
    <div classname="title">
    	<h1>HELLO</h1>
    </div>
    );
}

2. 컴파일

function Component(){
  return React.createElement("div", {
    className: 'title'
  }, React.createElement("h1", null, "HELLO"));
}

// React elements 는 DOM 요소를 virtual DOM으로 표현
// virtual DOM은 메모리에 저장되며 실제 렌더되지 않으므로 연산비용이 적음
const element = {
  type: 'div',
  props: {
    className: 'title',
    children: [
      {
        type: 'h1',
       	children: 'HELLO',
      }
      ]
  }
};

3. Reconciliation (재조정)
4. 변경사항 커밋

 ReactDOM.render(element, document.getElementById('title'));

Diffing Algorithm & Reconciliation (재조정)

 

기존의 DOM 트리를 새로운 트리로 변환하기 위하여 최소한의 연산을 하는 알고리즘을 사용합니다
이때 알아낸 조작 방식은 알고리즘 O(n^3) 의 복잡도를 가지고 있다. 만약,이 알고리즘을 React에 적용한다면, 1000개의 엘리먼트가 있다는 가정하에 실제 화면에 표시하기 위해 1000^3인 10억번의 비교 연산을 해야합니다
이는 너무 비싼 연산이기에 React는 두 가지 가정을 가지고 시간 복잡도 O(n)의 새로운 Heuristic Algorithm을 구현했습니다

  1. 각기 서로 다른 타입을 가진 두 요소는 다른 트리를 구축한다.
  2. 개발자가 제공하는 key 프로퍼티를 통해 자식 요소의 변경 여부를 표시할 수 있다

=> 여러 번 렌더링을 거쳐도 변경되지 말아야 하는 자식 요소가 무엇인지 알아낼 수 있다.
렌더링 프로세스 단계에서 Diffing Algorithm을 통해 변경사항을 체크하고
Reconciliation(재조정)을 통해 DOM을 다시 업데이트 시켜주게 됩니다

ReactFiber

React 16 이전에는 Stack Reconciler를 사용했으며 동작방식은 모든작업을 스택으로 처리하고 동기적으로 이루어져 작업이 오래걸린다면
메인 쓰레드에 과부하가 걸릴 정도의 고연산 작업이라면 유저 경험을 심각하게 저해시킬 정도의 렌더링 문제가 유발될 수 있었습니다
그래서 ReactFiber가 도입되었고 이를 증분 렌더링 이라고 합니다.
주 역할은 다음과 같습니다

  • 연산을 멈추고 다시 수행할 수 있는 기능
  • 각기 역할마다 다른 우선순위를 부여할 수 있는 기능
  • 이전에 완료된 연산을 재사용할 수 있는 기능
  • 필요가 없어진 연산을 중간에 취소하는 기능

ReactFiber Tree

FiberTree

  • type: 생성된 function Component
  • child: 해당 Component의 가장 왼쪽에 있는 자식 노드
  • sibling: 해당 Component의 형제 노드

Fiber Tree는 Fiber Tree, WokinProgress Tree 총 두개로 구성되어있습니다

  • FiberTree: 현재 모습을 담은 트리
  • WorkInProgressTree: 작업 중인 상태를 나타내는 트리

리액트 파이버의 작업이 끝나면 리액트는 단순히 포인터만 변경해 현재 트리를 workInProgress트리로 바꿔버립니다. (이러한 기술을 더블 버퍼링이라고 하며 커밋 단계에서 수행됩니다.)
두 개의 트리가 존재하기 때문에 리액트는 미처 렌더가 끝나지 않은 모습을 노출시키지 않을 수 있고. 리액트 파이버 트리의 작업 단계는 다음과 같습니다.

  1. 모든 작업은 현재 UI 렌더링을 위해 존재하는 current 트리를 기준으로 시작된다.
  2. 만약 업데이트가 발생할 경우 리액트에서 새로 받은 데이터로 workInProgress 트리를 빌드하기 시작한다.
  3. 이 workInProgress 트리를 빌드하는 작업이 마무리되면 다음 렌더링에 이 트리를 사용한다.
  4. 이 workInProgress 트리가 UI에 최종적으로 렌더링되어 반영이 완료되면 current가 workInProgress 트리로 변경된다.

 

FiberTree 와 WorkInProgressTree

React Fiber 도입 효과

  1. 성능을 향상시켰습니다. 재조정하는 동안 다른 작업을 중지하지 않기 때문에 React는 필요할 때마다 작업을 일시 중지하거나 렌더링을 시작할 수 있게 됐습니다.
  2. 훨씬 깔끔한 방식으로 오류를 처리할 수 있게 됐습니다. 자바스크립트 런타임 오류가 발생할 때마다 흰색 화면을 표시하는 대신 Error Boundary를 설정하여 문제가 발생할 경우 백업 화면을 표시할 수 있게 됐습니다.
  3. ※ 리액트 파이버의 도입으로 Error Boundary, Suspense, React.Lazy, Fragment 그리고 Concurrency Mode가 가능해졌습니다.

참고

브라우저

웹에서 페이지를 찾아서 보여주고, 사용자가 하이퍼링크를 통해 다른 페이지로 이동할 수 있도록 하는 프로그램으로
브라우저는 유저가 선택한 자원을 서버로 부터 받아와서 유저에게 보여준다. 이 자원은 페이지 외에도 이미지, 비디오 등의 컨텐츠들도 포함된다. 받아온 자원들을 렌더링 과정을 통해 유저에게 보여주게 된다.
브라우저의 종류는 chrome, safari, firefox 등이 있다
 

Dom

Document Object Model의 약자로 문서 객체 모델을 의미한다.
문서 객체란 html, head, body와 같은 태그들을 javascript가 이용할 수 있는 (메모리에 보관할 수 있는) 객체를 의미한다.

Dom Tree



브라우저 렌더링 과정

  1. HTML 파일과 CSS 파일을 파싱해서 각각 Tree를 만든다. (DOM, CSSOM 을 생성) (Parsing)
  2. 두 Tree를 결합하여 Rendering Tree를 만든다. (Style)
  3. Rendering Tree에서 각 노드의 위치와 크기를 계산한다. (Layout)
  4. 계산된 값을 이용해 각 노드를 화면상의 실제 픽셀로 변환하고, 레이어를 만든다. (Paint)
  5. 레이어를 합성하여 실제 화면에 나타낸다. (Composite)

렌더링 과정

 

리플로우와 리페인트중 어느것이 더 부하가 클까?

  • 리플로우 (reflow)
  • DOM 요소의 기하학적 속성이 변경될때, 브라우저 사이즈가 변할때, 스타일시트가 로딩되었을때 발생하는 변화들을 다시 계산 해주는 작업을 뜻하고 레이아웃(Layout) 이라고도 한다.
  • 리페인트 (repaint)
  • 변경된 요소를 실제로 화면에 그려주는 작업을 리페인트라고 한다. 그래서 리플로우가 발생하면 필연적으로 리페인트가 실행된다.

리페인트도 굉장히 무거운 작업이긴 하지만 리플로우 처럼 모든 요소들에 대한 기하학적 정보들을 계산해주는 작업은 아니기 때문에 리플로우 보다는 상대적으로 훨씬 가벼운 작업이다
 

Dom을 직접조작하는것은 느릴까?

과거 IE와 같은 브라우저에서는 DOM은 일반적으로 객체 대신 노드(Node)를 사용하여 계층 구조를 형성하고 이를 탐색하며 메모리를 관리하는 방식을 사용해 자바스크립트와 DOM 간의 데이터 교환과 상호작용이 비효율적이었다
 
하지만 모던 브라우저(Chrome, Safari)는 DOM도 자바스크립트와 완전히 동일한 메모리 모델을 쓰기 때문에 자바스크립트의 객체를 다루는 것과 속도 차이는 거의 없다
 

Code Review를 AI가 해주면 얼마나 좋을까라는 의문에서 여러 글을 서치하고 한번 POC를 진행해보았습니다

 

Github action을 활용하고 순서는 다음과 같습니다

 

Openai api key 발급

https://openai.com/blog/openai-api 접속 하여 secret key를 발급받습니다

Dashboard > APIkeys > Create new secret key

 

Dashboard > Usage 에서 비용을 확인할수있습니다

 

Secret  key 등록

repository에 발급받은 secretkey를 등록해줍니다

Github > repository > settings > secrets and variables > actions > New repository secret

 

 

Action설정

.github/workflows/main.yml 파일 생성

name: AI Code Reviewer

on:
  pull_request:
    types:
      - opened
      - synchronize
permissions: write-all
jobs:
  review:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout Repo
        uses: actions/checkout@v3

      - name: AI Code Reviewer
        uses: leeyc924/ai-codereviewer@main
        with:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # The GITHUB_TOKEN is there by default so you just need to keep it like it is and not necessarily need to add it as secret as it will throw an error. [More Details](https://docs.github.com/en/actions/security-guides/automatic-token-authentication#about-the-github_token-secret)
          OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
          OPENAI_API_MODEL: "gpt-4-1106-preview" # Optional: defaults to "gpt-4"
          exclude: "**/*.json, **/*.md" # Optional: exclude patterns separated by commas

 

간단한 poc를 위해 오픈소스인 https://github.com/freeedcom/ai-codereviewer 를 fork로 가져와 runner로 활용하였습니다

 

pr요청을 하면 다음과같이 AI가 review를 하게 됩니다

jobkorea를 다니며 "Vanilla-Extract 도입기"를 작성하였다.

https://techblog.jobkorea.co.kr/vanilla-extract-%EB%8F%84%EC%9E%85%EA%B8%B0-b61c38aaef36

본 글은 PWA를 이용한 회원관리 앱만들기와 이어집니다

 

서론

회원관리앱으로 몇달간 회원관리를 해본 결과 몇몇 문제점들이 발견되었고 이를 개선하고자 본 프로젝트를 이어서 진행하게되었습니다.

문제점으로는 다음과 같았습니다

1. mui 기반으로 수많은 커스텀을 진행하여야했고 코드가 지저분해져 유지보수에 어려움이 있었습니다

2. PWA 앱이여서 브라우저 히스토리 삭제시 로그인 풀리는 현상이 있었습니다.

3. nextjs app router에는 pwa가 제대로 지원하지않아 기능을 추가하기 어려웠습니다

4. 게임참여관리를 1~4부로 나누어 관리하였는데 게임을 참여한 날짜만 중요하다는 것을 깨닫고 entity 수정을 해야했습니다

 

Migration 과정

FRONT END

asis
tobe

 

기존 nextjs를 걷어내고 react  vite 기반으로 migration 하는 과정을 거쳤습니다

 

UI UX 변경점으로는 다음과 같습니다

- Mui를 걷어내고 leeyc design system 디자인 시스템을 적용

- 다크모드 지원 추가

- 홈화면 신규회원 목록, 참여왕 추가

- 게임 목록 ui ux개편

 

ASIS

 

TOBE

 

 

'SideProject > 회원관리 앱만들기' 카테고리의 다른 글

PWA를 이용한 회원관리 앱만들기  (4) 2024.02.13

storybook은 React, Angular, Vue 등의 분리된 UI 컨포넌트를 체계적이고 효율적으로 구축할 수 있는 개발 도구입니다

UI 컨포넌트 라이브러리의 문서화(documentation)를 위해 사용할 수도 있고 디자인 시스템(Design system)을 개발하기 위한 플랫폼으로 사용할 수도 있습니다.

Directory

vite 기반으로 프로젝트를 구성하였습니다

.storybook: storybook config 관련 파일들이 있습니다

stories:

  • Components: @breadlee/ui 에서 개발한 컴포넌트들의 story들입니다 
  • Display: Icon, Palette 등 display용도의 story입니다

storybook-static: storybook build시 생성되는 정적 파일들입니다

Config

// .storybook/main.js
import { mergeConfig } from "vite";
import { resolve } from 'path';
const UI_PATH = resolve("../../packages/ui");

/** @type { import('@storybook/react-vite').StorybookConfig } */
const config = {
  stories: ["../stories/**/*.stories.@(js|jsx|mjs|ts|tsx|mdx)"],
  addons: [
    "@storybook/addon-links",
    "@storybook/addon-essentials",
    "@storybook/addon-interactions",
  ],
  framework: {
    name: "@storybook/react-vite",
    options: {},
  },
  docs: {
    autodocs: true,
  },
  async viteFinal(config) {
    return mergeConfig({
      ...config,
      resolve: {
        alias: [
          {
            find: '@styles',
            replacement: `${UI_PATH}/src/styles`
          },
          {
            find: "@components",
            replacement: `${UI_PATH}/src/components`,
          },
          {
            find: "@types",
            replacement: `${UI_PATH}/src/types`,
          },
          {
            find: "@hooks",
            replacement: `${UI_PATH}/src/hooks`,
          },
          {
            find: "@icons",
            replacement: resolve("../../packages/icons/dist"),
          },
        ],
      },
      define: {
        "process.env": {},
      },
      css: {
        postcss: null,
      },
    });
  },
};

export default config;

Example

// Components/TextField.stories.tsx
import { TextField, TextFieldProps } from '@components';
import { Meta, StoryObj } from '@storybook/react';

const story: Meta<TextFieldProps> = {
  component: TextField,
  tags: ['autodocs'],
  parameters: {},
};

export default story;

export const Default: StoryObj<TextFieldProps> = {
  args: {
    placeholder: 'TextField',
  },
};

palette는 mdx로 작성할수도있지만 직접 story를 작성하였습니다

// Display/Palette.stories.tsx
import { Typography } from '@components';
import { Meta, StoryObj } from '@storybook/react';
import { palette } from '@styles';
import { useEffect, useRef, useState } from 'react';

interface PaletteProps {
  color?: keyof typeof palette;
}

const ColorItem = ({ className, name }: { name: string; className: string }) => {
  const [color, setColor] = useState<string>('');
  const ref = useRef<HTMLDivElement>(null);
  useEffect(() => {
    if (ref.current) {
      const computedStyle = getComputedStyle(ref.current);
      const color = computedStyle.getPropertyValue('background-color');
      const regex = /(\d+), (\d+), (\d+)/;
      const match = color.match(regex);
      if (!match) return;

      const r = parseInt(match[1]).toString(16).padStart(2, '0').toUpperCase();
      const g = parseInt(match[2]).toString(16).padStart(2, '0').toUpperCase();
      const b = parseInt(match[3]).toString(16).padStart(2, '0').toUpperCase();

      setColor(`#${r}${g}${b}`);
    }
  }, [className]);

  return (
    <div key={name} style={{ display: 'flex', flexDirection: 'column', flex: 1, gap: 5 }}>
      <div
        ref={ref}
        style={{
          backgroundColor: className,
          width: '100%',
          height: '48px',
          boxShadow: 'rgba(0, 0, 0, 0.1) 0 1px 3px 0',
          border: '1px solid hsla(203, 50%, 30%, 0.15)',
        }}
      ></div>
      <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center' }}>
        <Typography color="Gray800" variant="D2">
          {name}
        </Typography>
        <Typography color="Gray800" variant="D2">
          {color}
        </Typography>
      </div>
    </div>
  );
};

const ColorItemList = ({ title }: { title: string }) => {
  const colors = Object.entries(palette).filter(([name]) => name.includes(title));

  return (
    <div style={{ display: 'flex', flexDirection: 'column', gap: 10 }}>
      <div>
        <Typography color="Gray900" variant="H4">
          {title}
        </Typography>
      </div>
      <div style={{ display: 'flex', flex: 1 }}>
        {colors.map(([name, className]) => (
          <ColorItem className={className} key={name} name={name} />
        ))}
      </div>
    </div>
  );
};

const Palette = () => {
  return (
    <div style={{ display: 'flex', flexDirection: 'column', gap: 30 }}>
      <ColorItemList title="Primary" />
      <ColorItemList title="Secondary" />
      <ColorItemList title="Tertiary" />
      <ColorItemList title="Error" />
      <ColorItemList title="Background" />
      <ColorItemList title="Surface" />
      <ColorItemList title="Blue" />
      <ColorItemList title="Gray" />
      <ColorItemList title="Black" />
      <ColorItemList title="White" />
    </div>
  );
};

const story: Meta<PaletteProps> = {
  component: Palette,
  parameters: {
    docs: {
      description: {
        component: 'Display palette',
      },
    },
  },
};

export default story;

export const Default: StoryObj = {
  render() {
    return <Palette />;
  },
};

Publish

배포는 chromatic을 활용하여 배포해주었습니다

https://65d092e55038add0e921289f-xwvphhjnrf.chromatic.com/

 

@storybook/cli - Storybook

 

65d092e55038add0e921289f-xwvphhjnrf.chromatic.com

 

 

자세한 소스는 여기서 확인가능합니다

https://github.com/leeyc924/leeyc-package/tree/main/apps/storybook

 

 

 

'SideProject > Design System' 카테고리의 다른 글

6. utils  (2) 2024.02.18
5. ui  (5) 2024.02.13
4. icon  (5) 2024.02.08
3. npm publish  (2) 2024.02.02
2. eslint, tsconifg 설정  (3) 2024.02.01
#!/bin/bash

echo "remove node_modules:"
echo "  \$ROOT/node_modules"
rm -r "node_modules"
for d in packages/*/node_modules; do
  echo "  $d"
  rm -r "$d"
done

for d in apps/*/node_modules; do
  echo "  $d"
  rm -r "$d"
done

pnpm store prune

pnpm install

turborepo project에서 node_modules파일들을 전부 삭제후 다시 install해주는 스크립트

매번 프로젝트마다 utility 함수를 만들기는 매우 번거로운 일입니다.

 

Directory

 

typescript 로 작성하였으며 src하위에 추가할 예정입니다

Bundle

tsup을 이용하여 간단하게 번들링하였습니다

// tsup.config.js
import { defineConfig } from 'tsup';

export default defineConfig({
  entry: ['src/index.ts'],
  format: ['esm'],
  dts: true,
  splitting: false,
  sourcemap: true,
  clean: true,
  minify: true,
});

 

parse.ts를 예를 들어 살펴 보겠습니다

// src/parse.ts

export function parseToNumber<T>(value: T, defaultValue = 0): number {
  const parsed = Number(value);

  if (Number.isNaN(parsed)) {
    return defaultValue;
  }

  return parsed;
}

value parameter를 number타입으로 변경 시켜주는 함수입니다

 

 

자세한 소스는 여기서 확인가능합니다

https://github.com/leeyc924/leeyc-package/tree/main/packages/utils

'SideProject > Design System' 카테고리의 다른 글

7. Storybook  (3) 2024.02.23
5. ui  (5) 2024.02.13
4. icon  (5) 2024.02.08
3. npm publish  (2) 2024.02.02
2. eslint, tsconifg 설정  (3) 2024.02.01

+ Recent posts