๋ณธ๋ฌธ ๋ฐ”๋กœ๊ฐ€๊ธฐ
React-Native

React Native, Web ํฌ๋กœ์Šค ํ”Œ๋žซํผ ๋””์ž์ธ ์‹œ์Šคํ…œ Storybook ๊ตฌ์ถ•ํ•˜๊ธฐ

by LasBe 2026. 4. 11.
๋ฐ˜์‘ํ˜•

๐Ÿ“’ React Native, Web ํฌ๋กœ์Šค ํ”Œ๋žซํผ ๋””์ž์ธ ์‹œ์Šคํ…œ Storybook ๊ตฌ์ถ•ํ•˜๊ธฐ


React Native๋กœ ์•ฑ์„ ๋งŒ๋“ค๊ณ , ๊ฐ™์€ ๋””์ž์ธ์„ ์›น์•ฑ์—๋„ ์ ์šฉํ•ด์•ผ ํ•˜๋Š” ์ƒํ™ฉ์ด ๋ฐ˜๋ณต ๋˜์–ด, ํŒ€๊ณผ ํ•จ๊ป˜ react-native-web ๊ธฐ๋ฐ˜์˜ ํฌ๋กœ์Šค ํ”Œ๋žซํผ ๋””์ž์ธ ์‹œ์Šคํ…œ์„ ๋งŒ๋“ค์—ˆ์Šต๋‹ˆ๋‹ค.  ์ปดํฌ๋„ŒํŠธ๋Š” ์ž˜ ๋‚˜์™”๋Š”๋ฐ, ๋ฌธ์ œ๊ฐ€ ํ•˜๋‚˜ ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค.

 
22๊ฐœ ์ปดํฌ๋„ŒํŠธ๊ฐ€ ์ „๋ถ€ Compound Component(ํ•ฉ์„ฑ ์ปดํฌ๋„ŒํŠธ) ํŒจํ„ด์ด๋ผ "์–ด๋–ป๊ฒŒ ์กฐํ•ฉํ•ด์„œ ์“ฐ๋Š” ๊ฑด๋ฐ?"๋ผ๋Š” ์งˆ๋ฌธ์— ์ฝ”๋“œ๋งŒ ๋ณด์—ฌ์ฃผ๊ธฐ์—” ํ•œ๊ณ„๊ฐ€ ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค. ๊ฒฐ๊ตญ Storybook์„ ์ง์ ‘ ์…‹์—…ํ–ˆ๊ณ , ๊ทธ ๊ณผ์ •์—์„œ webpack ์„ค์ •๊ณผ ๊ฝค ์˜ค๋ž˜ ์‹ธ์› ์Šต๋‹ˆ๋‹ค. ๊ทธ ๊ฒฝํ—˜์„ ๊ณต์œ ํ•ด๋ณด๋ ค ํ•ฉ๋‹ˆ๋‹ค.
 
ํ™˜๊ฒฝ ์ •๋ณด

  • React Native, Expo
  • Storybook 8.6.4 (@storybook/react-webpack5)
  • react-native-web 0.19.10
  • @storybook/addon-react-native-web 0.0.27
  • Yarn Workspaces (๋ชจ๋…ธ๋ ˆํฌ)

 

๐Ÿ“Œ ์™œ ํฌ๋กœ์Šค ํ”Œ๋žซํผ ๋””์ž์ธ ์‹œ์Šคํ…œ์ด ํ•„์š”ํ–ˆ๋Š”๊ฐ€

์•ฑ ๋‚ด์—์„œ ์›น๋ทฐ๋ฅผ ํ†ตํ•œ ์›น์•ฑ ๊ธฐ๋Šฅ์ด ์ ์  ๋Š˜์–ด๋‚˜๊ณ  ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค. ์•ฑ์€ React Native๋กœ, ์›น์•ฑ์€ ๋ณ„๋„๋กœ ๋งŒ๋“ค๋ฉด ๊ฐ™์€ ๋””์ž์ธ ์ŠคํŽ™์ธ๋ฐ ๊ตฌํ˜„์ด ๋‘ ๋ฒŒ์ด ๋ฉ๋‹ˆ๋‹ค. ๋””์ž์ธ ๋ถˆ์ผ์น˜๋Š” ๋‹น์—ฐํ•˜๊ณ , ์ˆ˜์ •์‚ฌํ•ญ ๋ฐ˜์˜๋„ ๋‘ ๋ฒˆ ํ•ด์•ผ ํ•˜์ฃ .
 

react-native-web์ด ์ด ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•ด์คฌ์Šต๋‹ˆ๋‹ค. React Native ์ปดํฌ๋„ŒํŠธ๋ฅผ ๊ทธ๋Œ€๋กœ ์›น์—์„œ ๋ Œ๋”๋งํ•  ์ˆ˜ ์žˆ์œผ๋‹ˆ, ๋””์ž์ธ ์‹œ์Šคํ…œ์„ ํ•˜๋‚˜๋งŒ ๋งŒ๋“ค๋ฉด ์•ฑ๊ณผ ์›น ๋‘˜ ๋‹ค ์“ธ ์ˆ˜ ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค. ์‹ค์ œ๋กœ Expo Web(expo-web)์œผ๋กœ ์›น์•ฑ ํ”„๋กœ์ ํŠธ๋ฅผ ๊ตฌ์ถ•ํ•˜๊ณ  ๋””์ž์ธ ์‹œ์Šคํ…œ์„ ์ ์šฉํ•˜๋‹ˆ, ์•ฑ ๋‚ด ์›น์•ฑ ๊ธฐ๋Šฅ ๊ฐœ๋ฐœ ์‹œ๊ฐ„์ด ๊ฝค ์ค„์—ˆ์Šต๋‹ˆ๋‹ค.

 
๋””์ž์ธ ์‹œ์Šคํ…œ ์ž์ฒด๋Š” ํŒ€์ด ํ•จ๊ป˜ ๊ฐœ๋ฐœํ–ˆ์Šต๋‹ˆ๋‹ค. 22๊ฐœ ์ปดํฌ๋„ŒํŠธ, 12๊ฐœ ์ปค์Šคํ…€ ํ›…, 5๊ฐœ Context Provider๊นŒ์ง€ ๊ตฌ์กฐ๊ฐ€ ์ž˜ ์žกํ˜”๋Š”๋ฐ, ๋ฌธ์„œํ™” ํ™˜๊ฒฝ์€ ์—†์—ˆ์Šต๋‹ˆ๋‹ค.
 

Compound Component ํŒจํ„ด ํŠน์„ฑ์ƒ Button.Label, Button.Icon ๊ฐ™์€ ์„œ๋ธŒ ์ปดํฌ๋„ŒํŠธ๋ฅผ ์–ด๋–ป๊ฒŒ ์กฐํ•ฉํ•˜๋Š”์ง€ ์‹œ๊ฐ์ ์œผ๋กœ ๋ณด์—ฌ์ฃผ๋Š” ํ™˜๊ฒฝ์ด ํ•„์š”ํ–ˆ์Šต๋‹ˆ๋‹ค.

 

๐Ÿ“Œ ์™œ React Native Storybook์ด ์•„๋‹ˆ๋ผ Storybook Web์ธ๊ฐ€

๐Ÿ”Ž ์ฒ˜์Œ์—” React Native Storybook์œผ๋กœ ์‹œ์ž‘

์ฒ˜์Œ ๋– ์˜ฌ๋ฆฐ ๊ฑด ๋‹น์—ฐํžˆ @storybook/react-native์˜€์Šต๋‹ˆ๋‹ค. React Native ํ”„๋กœ์ ํŠธ๋‹ˆ๊นŒ RN์šฉ Storybook์„ ์“ฐ๋Š” ๊ฒŒ ์ž์—ฐ์Šค๋Ÿฝ์ฃ .

๊ทธ๋Ÿฐ๋ฐ ์‹ค์ œ๋กœ ์จ๋ณด๋‹ˆ ๋ถˆํŽธํ•จ์ด ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค. ์ปดํฌ๋„ŒํŠธ๋ฅผ ํ™•์ธํ•˜๋ ค๋ฉด ์‹œ๋ฎฌ๋ ˆ์ดํ„ฐ๋ฅผ ์‹คํ–‰ํ•˜๊ฑฐ๋‚˜ ์‹ค์ œ ํ•ธ๋“œํฐ์„ ์—ฐ๊ฒฐํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ๋ธŒ๋ผ์šฐ์ €์—์„œ ๋ฐ”๋กœ ์—ด์–ด๋ณผ ์ˆ˜๊ฐ€ ์—†์œผ๋‹ˆ, ์ฝ”๋“œ ์ˆ˜์ • โ†’ ํ™•์ธ โ†’ ์ˆ˜์ •์˜ ํ”ผ๋“œ๋ฐฑ ๋ฃจํ”„๊ฐ€ ๋А๋ ค์ง‘๋‹ˆ๋‹ค.
 

๐Ÿ”Ž ๊ธฐ์กด Storybook ๊ธฐ๋Šฅ์˜ ๋ถ€์žฌ

๋” ํฐ ๋ฌธ์ œ๋Š” ์›น Storybook์—์„œ ๋‹น์—ฐํžˆ ์“ธ ์ˆ˜ ์žˆ๋Š” ๊ธฐ๋Šฅ๋“ค์ด RN ๋ฒ„์ „์—์„œ๋Š” ๋น ์ ธ์žˆ๊ฑฐ๋‚˜ ์ œํ•œ์ ์ด๋ผ๋Š” ์ ์ด์—ˆ์Šต๋‹ˆ๋‹ค.

  • Controls ํŒจ๋„์—์„œ props๋ฅผ ์‹ค์‹œ๊ฐ„์œผ๋กœ ๋ฐ”๊ฟ”๋ณด๊ธฐ
  • Docs ๋ชจ๋“œ์—์„œ MDX๋กœ ์ธํ„ฐ๋ž™ํ‹ฐ๋ธŒ ๋ฌธ์„œ ๋งŒ๋“ค๊ธฐ
  • Canvas์—์„œ ์†Œ์Šค ์ฝ”๋“œ์™€ ๋ Œ๋” ๊ฒฐ๊ณผ๋ฅผ ๋‚˜๋ž€ํžˆ ๋ณด๊ธฐ

๋””์ž์ธ ์‹œ์Šคํ…œ ๋ฌธ์„œํ™”์—๋Š” ์ด๋Ÿฐ ๊ธฐ๋Šฅ๋“ค์ด ํ•ต์‹ฌ์ธ๋ฐ, RN Storybook์—์„œ๋Š” addon ์ƒํƒœ๊ณ„๊ฐ€ ์›น ๋Œ€๋น„ ๋ถ€์กฑํ–ˆ์Šต๋‹ˆ๋‹ค.
ํŒ€ ์ „์ฒด๊ฐ€ ๋ธŒ๋ผ์šฐ์ €์—์„œ ์ปดํฌ๋„ŒํŠธ๋ฅผ ๋ฐ”๋กœ ํ™•์ธํ•˜๊ณ , ๋งํฌ ํ•˜๋‚˜๋กœ ๊ณต์œ ํ•  ์ˆ˜ ์žˆ์–ด์•ผ ํ–ˆ์Šต๋‹ˆ๋‹ค.
 

๐Ÿ”Ž Storybook Web + react-native-web ์กฐํ•ฉ ์„ ํƒ

์ด๋ฏธ react-native-web์ด ํ”„๋กœ์ ํŠธ์— ์žˆ์œผ๋‹ˆ, RN ์ปดํฌ๋„ŒํŠธ๋ฅผ ์›น์—์„œ ๋ Œ๋”๋งํ•˜๋Š” ๊ฑด ๊ฐ€๋Šฅํ•œ ์ƒํƒœ์˜€์Šต๋‹ˆ๋‹ค. ์—ฌ๊ธฐ์— @storybook/addon-react-native-web์ด ๋ธŒ๋ฆฟ์ง€(Bridge) ์—ญํ• ์„ ํ•ด์ค๋‹ˆ๋‹ค. React Native์˜ View, Text, Pressable ๊ฐ™์€ ์ปดํฌ๋„ŒํŠธ๋ฅผ react-native-web์œผ๋กœ ๋ณ€ํ™˜ํ•ด์„œ, Storybook์˜ Webpack5 ํ™˜๊ฒฝ์—์„œ ๋ Œ๋”๋งํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•ด์ฃผ๋Š” addon์ž…๋‹ˆ๋‹ค.

๊ฒฐ๊ณผ์ ์œผ๋กœ ์›น Storybook์˜ ๋ชจ๋“  ๊ธฐ๋Šฅ(Controls, Docs, Canvas, addon ์ƒํƒœ๊ณ„)์„ ๊ทธ๋Œ€๋กœ ์“ฐ๋ฉด์„œ RN ์ปดํฌ๋„ŒํŠธ๋ฅผ ๋ Œ๋”๋งํ•  ์ˆ˜ ์žˆ๊ฒŒ ๋์Šต๋‹ˆ๋‹ค.
 

๐Ÿ“Œ Storybook ํ™˜๊ฒฝ ๊ตฌ์„ฑ

๐Ÿ”Ž ๋ชจ๋…ธ๋ ˆํฌ ๊ตฌ์กฐ์™€ Storybook ์œ„์น˜

ํ”„๋กœ์ ํŠธ๋Š” Yarn Workspaces๋กœ ๋ชจ๋…ธ๋ ˆํฌ๋ฅผ ๊ตฌ์„ฑํ–ˆ์Šต๋‹ˆ๋‹ค. ๋ฃจํŠธ์— ๋””์ž์ธ ์‹œ์Šคํ…œ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ, example/ ๋””๋ ‰ํ† ๋ฆฌ์— Expo ์˜ˆ์ œ ์•ฑ๊ณผ Storybook์ด ํ•จ๊ป˜ ๋“ค์–ด์žˆ๋Š” ๊ตฌ์กฐ์ž…๋‹ˆ๋‹ค.

design-system/              โ† ๋ฃจํŠธ: ๋””์ž์ธ ์‹œ์Šคํ…œ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ
โ”œโ”€โ”€ src/                    โ† ์ปดํฌ๋„ŒํŠธ, ํ›…, ํ…Œ๋งˆ
โ”œโ”€โ”€ example/                โ† Expo ์˜ˆ์ œ ์•ฑ + Storybook
โ”‚   โ”œโ”€โ”€ .storybook/         โ† Storybook ์„ค์ •
โ”‚   โ”œโ”€โ”€ src/stories/        โ† ์Šคํ† ๋ฆฌ ํŒŒ์ผ (.stories.tsx, .mdx)
โ”‚   โ””โ”€โ”€ package.json
โ””โ”€โ”€ package.json            โ† workspaces: ["example"]

๋„ค์ดํ‹ฐ๋ธŒ ๊ฐœ๋ฐœ์€ Metro ๋ฒˆ๋“ค๋Ÿฌ(ํฌํŠธ 8081)๋กœ, Storybook์€ Webpack5(ํฌํŠธ 6006)๋กœ ๊ฐ๊ฐ ๋…๋ฆฝ์ ์œผ๋กœ ์‹คํ–‰๋ฉ๋‹ˆ๋‹ค. ๋ฃจํŠธ package.json์—์„œ workspace ์Šคํฌ๋ฆฝํŠธ๋กœ ์—ฐ๊ฒฐํ•ด๋‘๋ฉด ํŽธํ•ฉ๋‹ˆ๋‹ค.

{
  "scripts": {
    "storybook": "yarn workspace example storybook",
    "build-storybook": "yarn workspace example build-storybook"
  }
}

 

๐Ÿ”Ž ํ•ต์‹ฌ ์„ค์ •: .storybook/main.ts

Storybook์˜ ์ง„์ž…์ ์ธ main.ts์ž…๋‹ˆ๋‹ค. ํ”„๋ ˆ์ž„์›Œํฌ๋Š” @storybook/react-webpack5๋ฅผ ์‚ฌ์šฉํ•˜๊ณ , ํ•ต์‹ฌ์€ addon ๊ตฌ์„ฑ์ž…๋‹ˆ๋‹ค.

import type { StorybookConfig } from "@storybook/react-webpack5";

const config: StorybookConfig = {
  stories: ["../src/**/*.mdx", "../src/**/*.stories.@(js|jsx|mjs|ts|tsx)"],
  addons: [
    "@storybook/addon-webpack5-compiler-babel",
    "@storybook/addon-essentials",
    "@storybook/addon-interactions",
    "@storybook/addon-react-native-web", // RN โ†’ ์›น ๋ธŒ๋ฆฟ์ง€
    "@storybook/addon-react-native-server",
    "storybook-addon-deep-controls", // ์ค‘์ฒฉ props ์ œ์–ด
    "@storybook/addon-docs",
  ],
  framework: "@storybook/react-webpack5",
  // ... webpack ์ปค์Šคํ„ฐ๋งˆ์ด์ง•์€ ์•„๋ž˜ ์„น์…˜์—์„œ
};

์—ฌ๊ธฐ์„œ ํ•ต์‹ฌ์€ @storybook/addon-react-native-web์ž…๋‹ˆ๋‹ค. ์ด addon์ด ์—†์œผ๋ฉด React Native์˜ View, Text ๊ฐ™์€ ์ปดํฌ๋„ŒํŠธ๋ฅผ Webpack์ด ํ•ด์„ํ•˜์ง€ ๋ชปํ•ฉ๋‹ˆ๋‹ค.

storybook-addon-deep-controls๋„ ๋””์ž์ธ ์‹œ์Šคํ…œ์—์„œ๋Š” ์œ ์šฉํ–ˆ์Šต๋‹ˆ๋‹ค. Compound Component ํŒจํ„ด ํŠน์„ฑ์ƒ ์ค‘์ฒฉ๋œ ๊ฐ์ฒด ํ˜•ํƒœ์˜ props๊ฐ€ ๋งŽ์€๋ฐ, ๊ธฐ๋ณธ Controls ํŒจ๋„๋กœ๋Š” ์ด๋ฅผ ์ œ์–ดํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. ์ด addon์ด ์ค‘์ฒฉ props๋ฅผ ํŽผ์ณ์„œ ๊ฐ๊ฐ ๊ฐœ๋ณ„ ์ปจํŠธ๋กค๋กœ ๋ณด์—ฌ์ค๋‹ˆ๋‹ค.

 

๐Ÿ”Ž ๋””์ž์ธ ์‹œ์Šคํ…œ Provider๋กœ ์Šคํ† ๋ฆฌ ๊ฐ์‹ธ๊ธฐ: preview.tsx

๋””์ž์ธ ์‹œ์Šคํ…œ์˜ ๋ชจ๋“  ์ปดํฌ๋„ŒํŠธ๋Š” ์ตœ์ƒ์œ„ Provider ์•ˆ์—์„œ ๋™์ž‘ํ•ฉ๋‹ˆ๋‹ค. ํ…Œ๋งˆ, ์•„์ด์ฝ˜ ์—์…‹, ์„ค์ •๊ฐ’์„ Context๋กœ ์ œ๊ณตํ•˜๋Š” Provider์ธ๋ฐ, Storybook ์Šคํ† ๋ฆฌ์—์„œ๋„ ์ด๊ฑธ ๊ฐ์‹ธ์ค˜์•ผ ํ•ฉ๋‹ˆ๋‹ค.

import { DSProvider, ToastProvider } from "my-design-system";
import { icons, illustrations } from "my-design-asset";
import { View } from "react-native";

const preview: Preview = {
  decorators: [
    (Story: any) => (
      <DSProvider
        assets={{
          icons: icons,
          illustrations: illustrations,
        }}
      >
        <View style={{ flexDirection: "row", justifyContent: "center" }}>
          <View style={{ width: 400 }}>
            <Story />
          </View>
        </View>
        <ToastProvider />
      </DSProvider>
    ),
  ],
};

400px ๋„ˆ๋น„์˜ ์ปจํ…Œ์ด๋„ˆ๋กœ ๊ฐ์‹ธ์„œ ๋ชจ๋ฐ”์ผ ๋ทฐ๋ฅผ ์‹œ๋ฎฌ๋ ˆ์ด์…˜ํ•ฉ๋‹ˆ๋‹ค. Provider์— ์•„์ด์ฝ˜ ์—์…‹์„ ๋„˜๊ธฐ๋Š” ๊ฒƒ๋„ ์žŠ์œผ๋ฉด ์•ˆ ๋ฉ๋‹ˆ๋‹ค. ์•ˆ ๋„˜๊ธฐ๋ฉด Icon ์ปดํฌ๋„ŒํŠธ๊ฐ€ ์ „๋ถ€ ์—๋Ÿฌ๋ฅผ ๋ฑ‰์Šต๋‹ˆ๋‹ค.
 

๐Ÿ“Œ Webpack ์ปค์Šคํ„ฐ๋งˆ์ด์ง•

addon ์„ค์น˜ํ•˜๊ณ  ์Šคํ† ๋ฆฌ ํŒŒ์ผ ๋ช‡ ๊ฐœ ๋งŒ๋“ค๋ฉด ๋๋‚  ์ค„ ์•Œ์•˜์Šต๋‹ˆ๋‹ค. ํ˜„์‹ค์€ webpackFinal ์„ค์ •์„ ๊ฝค ๋งŒ์ ธ์•ผ ํ–ˆ์Šต๋‹ˆ๋‹ค.

๐Ÿ”Ž SVG ์ฒ˜๋ฆฌ ํŒŒ์ดํ”„๋ผ์ธ

React Native์—์„œ๋Š” react-native-svg-transformer๋กœ SVG๋ฅผ ์ปดํฌ๋„ŒํŠธ๋กœ ๋ณ€ํ™˜ํ•ฉ๋‹ˆ๋‹ค. Metro ๋ฒˆ๋“ค๋Ÿฌ๊ฐ€ ์ด๊ฑธ ์ฒ˜๋ฆฌํ•˜์ฃ . ๊ทธ๋Ÿฐ๋ฐ Storybook์€ Webpack5๋ฅผ ์‚ฌ์šฉํ•˜๋‹ˆ, SVG ์ฒ˜๋ฆฌ ํŒŒ์ดํ”„๋ผ์ธ์„ ๋ณ„๋„๋กœ ๊ตฌ์„ฑํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

๋จผ์ € ๊ธฐ์กด ์ด๋ฏธ์ง€ ๋ฃฐ์—์„œ SVG๋ฅผ ์ œ์™ธํ•˜๊ณ , @svgr/webpack์œผ๋กœ SVG๋ฅผ React ์ปดํฌ๋„ŒํŠธ๋กœ ๋ณ€ํ™˜ํ•˜๋Š” ๋ฃฐ์„ ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค. ์—ฌ๊ธฐ์— string-replace-loader๋ฅผ ์ฒด์ด๋‹ํ•ด์„œ ํ•˜๋“œ์ฝ”๋”ฉ๋œ ์ƒ‰์ƒ ๊ฐ’์„ ๋™์  ํ…Œ๋งˆ์šฉ์œผ๋กœ ์น˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค.

webpackFinal: async (config) => {
  // ๊ธฐ์กด ์ด๋ฏธ์ง€ ๋ฃฐ์—์„œ SVG ์ œ์™ธ
  const imageRule = config.module?.rules?.find(
    (rule) => rule && typeof rule === "object" && rule.test instanceof RegExp && rule.test.test(".svg")
  );
  if (imageRule && typeof imageRule === "object") {
    imageRule.exclude = /\.svg$/;
  }

  // SVG โ†’ React ์ปดํฌ๋„ŒํŠธ ๋ณ€ํ™˜ + ์ƒ‰์ƒ ์น˜ํ™˜
  config.module?.rules?.push({
    test: /\.svg$/,
    use: [
      { loader: "@svgr/webpack", options: { svgo: false } },
      {
        loader: "string-replace-loader",
        options: {
          search: "#222222",
          replace: "current",
          flags: "g",
        },
      },
    ],
  });

  return config;
};

#222222๋ฅผ current๋กœ ์น˜ํ™˜ํ•˜๋Š” ์ด์œ ๋Š” ์•„์ด์ฝ˜์˜ fill ์ƒ‰์ƒ์„ ๋Ÿฐํƒ€์ž„์—์„œ ํ…Œ๋งˆ ์ปฌ๋Ÿฌ๋กœ ๋™์  ๋ณ€๊ฒฝํ•˜๊ธฐ ์œ„ํ•ด์„œ์ž…๋‹ˆ๋‹ค. SVG ์›๋ณธ์— ํ•˜๋“œ์ฝ”๋”ฉ๋œ ์ƒ‰์ƒ์ด ๋‚จ์•„์žˆ์œผ๋ฉด ํ…Œ๋งˆ๊ฐ€ ์ ์šฉ๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

 

๐Ÿ”Ž Node.js ํด๋ฆฌํ•„ ๋ฌธ์ œ

Storybook์„ ์ฒ˜์Œ ์‹คํ–‰ํ•˜๋ฉด tty์™€ os ๋ชจ๋“ˆ์„ ์ฐพ์„ ์ˆ˜ ์—†๋‹ค๋Š” ์—๋Ÿฌ๊ฐ€ ๋‚ฉ๋‹ˆ๋‹ค. ์ด ๋ชจ๋“ˆ๋“ค์€ Node.js ๋‚ด์žฅ ๋ชจ๋“ˆ์ธ๋ฐ, Webpack5๋ถ€ํ„ฐ๋Š” ๋ธŒ๋ผ์šฐ์ € ํ™˜๊ฒฝ์—์„œ Node.js ํด๋ฆฌํ•„์„ ์ž๋™์œผ๋กœ ํฌํ•จํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

config.resolve.fallback = {
  ...config.resolve.fallback,
  tty: require.resolve("tty-browserify"),
  os: require.resolve("os-browserify/browser"),
};

tty-browserify์™€ os-browserify๋ฅผ devDependencies์— ์„ค์น˜ํ•˜๊ณ  fallback์œผ๋กœ ์—ฐ๊ฒฐํ•˜๋ฉด ํ•ด๊ฒฐ๋ฉ๋‹ˆ๋‹ค.

 

๐Ÿ”Ž ๋ชจ๋…ธ๋ ˆํฌ ๋ชจ๋“ˆ ํ•ด์„ ๋ฌธ์ œ

๋””์ž์ธ ์‹œ์Šคํ…œ์ด ์ฐธ์กฐํ•˜๋Š” ์—์…‹ ํŒจํ‚ค์ง€์—์„œ ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค. ํ•ด๋‹น ํŒจํ‚ค์ง€์˜ package.json์— exports ํ•„๋“œ๊ฐ€ ์ •์˜๋˜์–ด ์žˆ๋Š”๋ฐ, Webpack์ด ์ด๋ฅผ ํ•ด์„ํ•˜๋Š” ๊ณผ์ •์—์„œ ๋ชจ๋“ˆ์„ ์ œ๋Œ€๋กœ ์ฐพ์ง€ ๋ชปํ–ˆ์Šต๋‹ˆ๋‹ค.

// ์ง์ ‘ ํŒŒ์ผ ๊ฒฝ๋กœ๋กœ alias ์šฐํšŒ
config.resolve.alias = {
  ...(config.resolve.alias || {}),
  "my-design-asset": path.resolve(__dirname, "../node_modules/my-design-asset/dist/index.js"),
};

exports ํ•„๋“œ๋ฅผ ์šฐํšŒํ•ด์„œ ๋นŒ๋“œ ์‚ฐ์ถœ๋ฌผ์˜ ์‹ค์ œ ํŒŒ์ผ ๊ฒฝ๋กœ๋กœ ์ง์ ‘ ๋งคํ•‘ํ•˜๋Š” ๋ฐฉ์‹์ž…๋‹ˆ๋‹ค. ๋ชจ๋…ธ๋ ˆํฌ ํ™˜๊ฒฝ์—์„œ ํŒจํ‚ค์ง€ ๊ฐ„ ์ฐธ์กฐ ์‹œ ์ด๋Ÿฐ ๋ฌธ์ œ๊ฐ€ ์ž์ฃผ ๋ฐœ์ƒํ•ฉ๋‹ˆ๋‹ค.

 

๐Ÿ“Œ ํ”Œ๋žซํผ๋ณ„ ์ฐจ์ด๋ฅผ ๋„˜์–ด์„œ

React Native ์ปดํฌ๋„ŒํŠธ๋ฅผ ์›น์—์„œ ๋ Œ๋”๋งํ•˜๋ฉด "๋Œ€๋ถ€๋ถ„์€" ์ž˜ ๋™์ž‘ํ•ฉ๋‹ˆ๋‹ค. ํ•˜์ง€๋งŒ ํ”Œ๋žซํผ ๊ฐ„ ๋™์ž‘์ด ๋‹ค๋ฅธ ์˜์—ญ์ด ์žˆ๊ณ , ๋””์ž์ธ ์‹œ์Šคํ…œ์—์„œ๋Š” ์ด๊ฑธ .web.tsx ํŒŒ์ผ๋กœ ๋ถ„๋ฆฌํ•ด์„œ ํ•ด๊ฒฐํ–ˆ์Šต๋‹ˆ๋‹ค.

 

๐Ÿ”Ž .web.tsx ์ „๋žต

Metro ๋ฒˆ๋“ค๋Ÿฌ์™€ Webpack ๋ชจ๋‘ ํ”Œ๋žซํผ๋ณ„ ํŒŒ์ผ ํ•ด์„์„ ์ง€์›ํ•ฉ๋‹ˆ๋‹ค. LinearGradient.tsx์™€ LinearGradient.web.tsx๊ฐ€ ์žˆ์œผ๋ฉด, ์›น ํ™˜๊ฒฝ์—์„œ๋Š” ์ž๋™์œผ๋กœ .web.tsx๋ฅผ ๊ฐ€์ ธ์˜ต๋‹ˆ๋‹ค.

๋„ค์ดํ‹ฐ๋ธŒ (LinearGradient.tsx): react-native-linear-gradient ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ์‚ฌ์šฉ

์›น (LinearGradient.web.tsx): CSS linear-gradient ์‚ฌ์šฉ

// LinearGradient.web.tsx
const LinearGradient: React.FC<LinearGradientProps> = ({ gradient, direction = "horizontal", children }) => {
  const cssGradient = useMemo(() => {
    const colors = gradients[gradient].join(", ");
    switch (direction) {
      case "horizontal":
        return `linear-gradient(to right, ${colors})`;
      case "vertical":
        return `linear-gradient(to bottom, ${colors})`;
      default:
        return `linear-gradient(to right, ${colors})`;
    }
  }, [gradient, direction]);

  return (
    <div style={{ backgroundImage: cssGradient }}>
      <View>{children}</View>
    </div>
  );
};

Modal๋„ ๋งˆ์ฐฌ๊ฐ€์ง€์ž…๋‹ˆ๋‹ค.
 

๋„ค์ดํ‹ฐ๋ธŒ์—์„œ๋Š” React Native์˜ Modal ์ปดํฌ๋„ŒํŠธ๋ฅผ ์“ฐ์ง€๋งŒ, ์›น์—์„œ๋Š” createPortal๋กœ document.body์— ๋ Œ๋”๋งํ•ฉ๋‹ˆ๋‹ค. ๋ฐ”ํ…€์‹œํŠธ(BottomSheet)์˜ ์›น ๋ฒ„์ „์—์„œ๋Š” window.innerHeight๊ฐ€ ๋ฌธ์ œ์˜€์Šต๋‹ˆ๋‹ค. ๋ ˆ์ด์•„์›ƒ ๋กœ๋“œ ์ „์— JS๊ฐ€ ์‹คํ–‰๋˜๋ฉด ๋†’์ด ๊ฐ’์ด 0์œผ๋กœ ์žกํžˆ๊ธฐ ๋•Œ๋ฌธ์—, fallback ์ฒ˜๋ฆฌ๊ฐ€ ํ•„์š”ํ–ˆ์Šต๋‹ˆ๋‹ค.

 

const getWindowHeight = () => {
  // ๋ ˆ์ด์•„์›ƒ ๋กœ๋“œ ์ „ JS๊ฐ€ ์‹คํ–‰๋˜๋ฉด window.innerHeight๊ฐ€ 0
  if (typeof window !== "undefined" && window.innerHeight > 0) {
    return window.innerHeight;
  }
  return Dimensions.get("window").height || 800;
};

 

๐Ÿ”Ž ์• ๋‹ˆ๋ฉ”์ด์…˜ ํ˜ธํ™˜์„ฑ

React Native์˜ Animated API์—์„œ useNativeDriver: true ์˜ต์…˜์€ ์• ๋‹ˆ๋ฉ”์ด์…˜์„ ๋„ค์ดํ‹ฐ๋ธŒ ์Šค๋ ˆ๋“œ์—์„œ ์‹คํ–‰ํ•ฉ๋‹ˆ๋‹ค. ์„ฑ๋Šฅ์ด ์ข‹์ฃ . ๊ทธ๋Ÿฐ๋ฐ ์›น์—์„œ๋Š” ๋„ค์ดํ‹ฐ๋ธŒ ๋“œ๋ผ์ด๋ฒ„๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค. useNativeDriver: true๋ฅผ ๊ทธ๋Œ€๋กœ ๋‘๋ฉด ํฌ๋ž˜์‹œ๊ฐ€ ๋‚ฉ๋‹ˆ๋‹ค.

// usePressAnimation.ts
const supportsNativeDriver = (type: AnimationTypes) => {
  const supportedAnimations: AnimationTypes[] = ["scale", "opacity", "rotate"];
  return supportedAnimations.includes(type) && Platform.OS !== "web";
};

// ์• ๋‹ˆ๋ฉ”์ด์…˜ ์‹คํ–‰ ์‹œ ์กฐ๊ฑด๋ถ€ ์ ์šฉ
Animated.timing(animationValue, {
  toValue,
  duration: config.duration,
  easing: config.easing,
  useNativeDriver: supportsNativeDriver(type),
}).start();

Platform.OS !== 'web' ํ•œ ์ค„๋กœ ๋ถ„๊ธฐํ•˜๋Š” ๊ฐ„๋‹จํ•œ ํ•ด๊ฒฐ์ฑ…์ž…๋‹ˆ๋‹ค. ์›น์—์„œ๋Š” JS ์Šค๋ ˆ๋“œ ์• ๋‹ˆ๋ฉ”์ด์…˜์œผ๋กœ fallback๋˜์ง€๋งŒ, Storybook ๋ฌธ์„œํ™” ์šฉ๋„๋กœ๋Š” ์ถฉ๋ถ„ํ•ฉ๋‹ˆ๋‹ค.

 

๐Ÿ”Ž Icon ์ปดํฌ๋„ŒํŠธ์˜ SVG gradient ID ์ถฉ๋Œ

์ด๊ฑด Storybook ํŠน์œ ์˜ ๋ฌธ์ œ์˜€์Šต๋‹ˆ๋‹ค. ํ•œ ํŽ˜์ด์ง€์— ๊ฐ™์€ ๊ทธ๋ž˜ํ”ฝ ์•„์ด์ฝ˜์„ ์—ฌ๋Ÿฌ ๊ฐœ ๋ Œ๋”๋งํ•˜๋ฉด SVG ๋‚ด๋ถ€์˜ linearGradient ID๊ฐ€ ์ค‘๋ณต๋˜์–ด ์ƒ‰์ƒ์ด ๊ผฌ์ด๋Š” ํ˜„์ƒ์ด ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.

 

์›์ธ์€ SVG์˜ gradient ์ •์˜๊ฐ€ <defs> ์•ˆ์—์„œ ๊ณ ์ •๋œ ID๋ฅผ ์‚ฌ์šฉํ•˜๊ธฐ ๋•Œ๋ฌธ์ž…๋‹ˆ๋‹ค. ๊ฐ™์€ SVG๊ฐ€ ๋‘ ๋ฒˆ ๋ Œ๋”๋ง๋˜๋ฉด ๊ฐ™์€ ID๊ฐ€ ๋‘ ๊ฐœ๊ฐ€ ๋˜๊ณ , ๋ธŒ๋ผ์šฐ์ €๋Š” ๋จผ์ € ์ฐพ์€ ๊ฒƒ์„ ์ฐธ์กฐํ•ฉ๋‹ˆ๋‹ค.

// gradient ID ์ถฉ๋Œ ํ•ด๊ฒฐ
const uniqueId = useId();
const suffix = uniqueId.replace(/:/g, "_");
const idMap = new Map<string, string>();

// ๋ชจ๋“  gradient ์ •์˜ ์š”์†Œ์˜ ID์— ๊ณ ์œ  suffix ์ถ”๊ฐ€
const defsElements = svg.querySelectorAll("linearGradient, radialGradient, pattern, clipPath, mask, filter");
defsElements.forEach((el: Element) => {
  const oldId = el.getAttribute("id");
  if (oldId) {
    const newId = `${oldId}_${suffix}`;
    idMap.set(oldId, newId);
    el.setAttribute("id", newId);
  }
});

// url(#oldId) ์ฐธ์กฐ๋„ ์ƒˆ ID๋กœ ์—…๋ฐ์ดํŠธ
const allElements = svg.querySelectorAll("*");
allElements.forEach((el: Element) => {
  ["fill", "stroke", "clip-path", "mask", "filter"].forEach((attr) => {
    const value = el.getAttribute(attr);
    if (value?.includes("url(#")) {
      const match = value.match(/url\(#([^)]+)\)/);
      if (match?.[1] && idMap.has(match[1])) {
        el.setAttribute(attr, `url(#${idMap.get(match[1])})`);
      }
    }
  });
});

React์˜ useId() ํ›…์œผ๋กœ ์ธ์Šคํ„ด์Šค๋งˆ๋‹ค ๊ณ ์œ ํ•œ suffix๋ฅผ ์ƒ์„ฑํ•˜๊ณ , ๋ Œ๋”๋ง ํ›„ SVG DOM์„ ์ˆœํšŒํ•˜๋ฉด์„œ ๋ชจ๋“  ID์™€ url(#) ์ฐธ์กฐ๋ฅผ ์ผ๊ด„ ๊ต์ฒดํ•ฉ๋‹ˆ๋‹ค. Storybook์ฒ˜๋Ÿผ ํ•œ ํ™”๋ฉด์— ๊ฐ™์€ ์•„์ด์ฝ˜์ด ์—ฌ๋Ÿฌ ๋ฒˆ ๋‚˜ํƒ€๋‚˜๋Š” ํ™˜๊ฒฝ์—์„œ ํ•„์š”ํ•œ ์ฒ˜๋ฆฌ์˜€์Šต๋‹ˆ๋‹ค.

 

๐Ÿ“Œ ์Šคํ† ๋ฆฌ ์ž‘์„ฑ ํŒจํ„ด

Webpack ์„ค์ •์„ ๋„˜๊ธฐ๊ณ  ๋‚˜๋ฉด, ์Šคํ† ๋ฆฌ ์ž‘์„ฑ์€ ๋น„๊ต์  ์ˆ˜์›”ํ•ฉ๋‹ˆ๋‹ค.
 

๐Ÿ”Ž Compound Component ์Šคํ† ๋ฆฌ

๋””์ž์ธ ์‹œ์Šคํ…œ์˜ ๋ชจ๋“  ์ปดํฌ๋„ŒํŠธ๊ฐ€ Compound Component ํŒจํ„ด์ด๋ผ, ์Šคํ† ๋ฆฌ์—์„œ ์กฐํ•ฉ ๋ฐฉ์‹์„ ๋ณด์—ฌ์ฃผ๋Š” ๊ฒŒ ํ•ต์‹ฌ์ž…๋‹ˆ๋‹ค.

// Button.stories.tsx
const meta: Meta<typeof Button> = {
  title: "Components/Button",
  component: Button,
  argTypes: {
    variant: {
      control: "select",
      options: ["primary", "secondary", "disabled"],
      description: "๋ฒ„ํŠผ์˜ ์Šคํƒ€์ผ ๋ณ€ํ˜•์„ ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค.",
    },
    size: {
      control: "select",
      options: ["lg", "md", "sm"],
    },
    isLoading: { control: "boolean" },
  },
};

export const WithIcon: Story = {
  render: (args) => (
    <Button {...args}>
      <View style={{ flexDirection: "row", alignItems: "center" }}>
        <Button.Icon name="add-user" />
        <Button.Label>์ถ”๊ฐ€ํ•˜๊ธฐ</Button.Label>
      </View>
    </Button>
  ),
};

Button ์•ˆ์— Button.Icon๊ณผ Button.Label์„ ์–ด๋–ป๊ฒŒ ์กฐํ•ฉํ•˜๋Š”์ง€ ์‹œ๊ฐ์ ์œผ๋กœ ๋ณด์—ฌ์ค๋‹ˆ๋‹ค. argTypes๋กœ ํ…Œ๋งˆ, ํฌ๊ธฐ, ๋กœ๋”ฉ ์ƒํƒœ๋ฅผ Controls ํŒจ๋„์—์„œ ์‹ค์‹œ๊ฐ„์œผ๋กœ ๋ฐ”๊ฟ”๋ณผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

 

๐Ÿ”Ž MDX ๋ฌธ์„œํ™”

์Šคํ† ๋ฆฌ๋งŒ์œผ๋กœ๋Š” "์ด ์ปดํฌ๋„ŒํŠธ๋ฅผ ์–ด๋–ป๊ฒŒ ์“ฐ๋Š” ๊ฑฐ์•ผ?"๋ผ๋Š” ์งˆ๋ฌธ์— ์ถฉ๋ถ„ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. @storybook/addon-docs๋ฅผ ์ถ”๊ฐ€ํ•˜๋ฉด MDX ํŒŒ์ผ๋กœ ์ธํ„ฐ๋ž™ํ‹ฐ๋ธŒ ๋ฌธ์„œ๋ฅผ ๋งŒ๋“ค ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. @storybook/blocks์—์„œ ์ œ๊ณตํ•˜๋Š” Canvas(๋ Œ๋” ๊ฒฐ๊ณผ + ์†Œ์Šค ์ฝ”๋“œ)์™€ Controls๋ฅผ ํ•œ ํŽ˜์ด์ง€์— ๋ฐฐ์น˜ํ•˜๋Š” ๋ฐฉ์‹์ž…๋‹ˆ๋‹ค.

import { Canvas, Controls } from "@storybook/blocks";
import * as ButtonStories from "./Button.stories";

# Button

๊ธฐ๋ณธ์ ์ธ ํ˜•ํƒœ์˜ ๋ฒ„ํŠผ ์ปดํฌ๋„ŒํŠธ์ž…๋‹ˆ๋‹ค.
๊ฐ ์š”์†Œ๋“ค์„ ์กฐํ•ฉํ•ด ๋‹ค์–‘ํ•œ ํ˜•ํƒœ์˜ ๋ฒ„ํŠผ์„ ๋งŒ๋“ค ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

## `<Button />` (Button.Container)

<Canvas of={ButtonStories.Default} sourceState="shown" />
<Controls of={ButtonStories.Default} />

## Snippet

VSCode์—์„œ ์•„๋ž˜ ๋ช…๋ น์–ด๋ฅผ ์ž…๋ ฅํ•˜์—ฌ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

sourceState='shown'์œผ๋กœ ์†Œ์Šค ์ฝ”๋“œ๋ฅผ ๊ธฐ๋ณธ ํŽผ์นจ ์ƒํƒœ๋กœ ๋ณด์—ฌ์ค๋‹ˆ๋‹ค. ์ปดํฌ๋„ŒํŠธ์˜ ๋ Œ๋” ๊ฒฐ๊ณผ, ์†Œ์Šค ์ฝ”๋“œ, props ์ปจํŠธ๋กค์ด ํ•œ ๋ˆˆ์— ๋“ค์–ด์˜ค๋‹ˆ ๋ณ„๋„์˜ README ์—†์ด๋„ ํŒ€์›์ด ๋ฐ”๋กœ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

์ถ”๊ฐ€๋กœ, VSCode ์Šค๋‹ˆํŽซ๋„ ๊ฐ™์€ ํŽ˜์ด์ง€์— ๋ฌธ์„œํ™”ํ•ด์„œ button-label-only ๊ฐ™์€ ์Šค๋‹ˆํŽซ ๋ช…๋ น์–ด๋ฅผ ๋ฐ”๋กœ ํ™•์ธํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ–ˆ์Šต๋‹ˆ๋‹ค.

 

๐Ÿ”Ž ์Šคํ† ๋ฆฌ ๋ฌธ์„œ ์ž‘์„ฑ ์ž๋™ํ™”

์Šคํ† ๋ฆฌ์™€ MDX ๋ฌธ์„œ์˜ ๊ตฌ์กฐ๋Š” ์ปดํฌ๋„ŒํŠธ๋งˆ๋‹ค ๊ฑฐ์˜ ๋™์ผํ•ฉ๋‹ˆ๋‹ค.
 
`meta` ์ •์˜, `argTypes` ์„ค์ •, ๋ Œ๋” ํ•จ์ˆ˜, MDX์˜ `Canvas`/`Controls` ๋ฐฐ์น˜๊นŒ์ง€ ํŒจํ„ด์ด ๋ฐ˜๋ณต๋˜์ฃ . 22๊ฐœ ์ปดํฌ๋„ŒํŠธ์— ๋Œ€ํ•ด ์ด๊ฑธ ๋งค๋ฒˆ ์ˆ˜๋™์œผ๋กœ ์ž‘์„ฑํ•˜๋Š” ๊ฑด ๋ฐ˜๋ณต ์ž‘์—…์ด์—ˆ์Šต๋‹ˆ๋‹ค. ๊ทธ๋ž˜์„œ Claude Code ์Šคํ‚ฌ(Skill)์„ ๋งŒ๋“ค์–ด ์ž๋™ํ™”ํ–ˆ์Šต๋‹ˆ๋‹ค.
 
์ปดํฌ๋„ŒํŠธ์˜ props ํƒ€์ž…๊ณผ ์„œ๋ธŒ ์ปดํฌ๋„ŒํŠธ ๊ตฌ์กฐ๋ฅผ ์ฝ์–ด์„œ `.stories.tsx`์™€ `.mdx` ํŒŒ์ผ์„ ์ž๋™ ์ƒ์„ฑํ•˜๋Š” ๋ฐฉ์‹์ž…๋‹ˆ๋‹ค. ์ปดํฌ๋„ŒํŠธ๋ฅผ ์ˆ˜์ •ํ–ˆ์„ ๋•Œ๋„ ์Šคํ‚ฌ์„ ์‹คํ–‰ํ•˜๋ฉด ๋ฌธ์„œ๊ฐ€ ๊ฐ™์ด ์—…๋ฐ์ดํŠธ๋˜๋‹ˆ, ์ฝ”๋“œ์™€ ๋ฌธ์„œ๊ฐ€ ๋”ฐ๋กœ ๋…ธ๋Š” ์ƒํ™ฉ์„ ์ค„์ผ ์ˆ˜ ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค.
 

๐Ÿ“Œ Docs ๋ชจ๋“œ๋กœ ๊ฐ€๋ณ๊ฒŒ ๋นŒ๋“œํ•˜๊ธฐ

Storybook์€ ํ”„๋ก ํŠธ ๊ฐœ๋ฐœ์ž๋งŒ ๋ณด๋Š” ๋„๊ตฌ๊ฐ€ ์•„๋‹™๋‹ˆ๋‹ค. ๋””์ž์ธํŒ€๊ณผ๋„ ๊ณต์œ ๋˜๋Š” ๋ฌธ์„œ์ด๊ธฐ๋„ ํ•˜์ฃ .
 
์•ž์„œ MDX ๋ฌธ์„œํ™” ์„น์…˜์—์„œ ๋ดค๋“ฏ์ด, ํ•˜๋‚˜์˜ MDX ํŒŒ์ผ์— ์ปดํฌ๋„ŒํŠธ์˜ ๋ Œ๋” ๊ฒฐ๊ณผ, ์†Œ์Šค ์ฝ”๋“œ, Controls, ์Šค๋‹ˆํŽซ๊นŒ์ง€ ์ „๋ถ€ ๋‹ด์•„๋’€์Šต๋‹ˆ๋‹ค. ์ปดํฌ๋„ŒํŠธ ํ•˜๋‚˜์— ํ•„์š”ํ•œ ์ •๋ณด๊ฐ€ MDX ํ•œ ํŽ˜์ด์ง€์— ๋‹ค ๋“ค์–ด์žˆ์œผ๋‹ˆ, ๊ตณ์ด ํ’€ ์ธํ„ฐ๋ž™ํ‹ฐ๋ธŒ Storybook์„ ๋นŒ๋“œํ•  ํ•„์š”๊ฐ€ ์—†์—ˆ์Šต๋‹ˆ๋‹ค.
 

storybook build --docs ๋ช…๋ น์–ด๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ์ด MDX ๋ฌธ์„œ ํŽ˜์ด์ง€๋งŒ ์ •์ ์œผ๋กœ ๋นŒ๋“œํ•ฉ๋‹ˆ๋‹ค. ์‚ฐ์ถœ๋ฌผ์ด ๊ฐ€๋ณ๊ณ , ์ •์  HTML์ด๋ผ ๋ณ„๋„ ์„œ๋ฒ„ ์—†์ด ์–ด๋””๋“  ํ˜ธ์ŠคํŒ…ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๋””์ž์ด๋„ˆ๋„ ๋ธŒ๋ผ์šฐ์ €์—์„œ ์ปดํฌ๋„ŒํŠธ๊ฐ€ ์‹ค์ œ๋กœ ์–ด๋–ป๊ฒŒ ๋ Œ๋”๋ง๋˜๋Š”์ง€, ์–ด๋–ค variant๊ฐ€ ์žˆ๋Š”์ง€ ๋ฐ”๋กœ ํ™•์ธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

 


 
์“ฐ๋‹ค ๋ณด๋‹ˆ Storybook ์„ธํŒ… ํ•˜๋‚˜์— webpack config, SVG ํŒŒ์ดํ”„๋ผ์ธ, ํด๋ฆฌํ•„, ๋ชจ๋“ˆ ํ•ด์„, ํ”Œ๋žซํผ๋ณ„ ๋ถ„๊ธฐ๊นŒ์ง€ ๊ฑด๋“œ๋ฆฐ ๊ฒŒ ๊ฝค ๋งŽ์•˜๋‹ค๋Š” ๊ฑธ ์ƒˆ์‚ผ ๋А๋‚๋‹ˆ๋‹ค. ๋Œ์ด์ผœ๋ณด๋ฉด ํ•˜๋‚˜ํ•˜๋‚˜๋Š” ํฐ ๋ฌธ์ œ๊ฐ€ ์•„๋‹ˆ์—ˆ๋Š”๋ฐ, ์ฒ˜์Œ ๋งŒ๋‚˜๋ฉด ์›์ธ์„ ์ฐพ๊ธฐ๊ฐ€ ์‰ฝ์ง€ ์•Š์€ ๊ฒƒ๋“ค์ด์—ˆ์Šต๋‹ˆ๋‹ค.
 

๊ทธ๋ž˜๋„ ํ™•์‹คํ•œ ๊ฑด, Compound Component ํŒจํ„ด์œผ๋กœ ๋งŒ๋“  ๋””์ž์ธ ์‹œ์Šคํ…œ์—์„œ Storybook์˜ ์˜ˆ์ œ์™€ ์‹œ๊ฐํ™” ์ž๋ฃŒ๊ฐ€ ๊ฐœ๋ฐœ์— ํฐ ๋„์›€์ด ๋˜์—ˆ๋‹ค๋Š” ์ ์ž…๋‹ˆ๋‹ค. ์ฝ”๋“œ๋งŒ ๋ด์„œ๋Š” Button.Label๊ณผ Button.Icon์„ ์–ด๋–ป๊ฒŒ ์กฐํ•ฉํ•˜๋Š”์ง€ ๊ฐ์ด ์•ˆ ์˜ค์ง€๋งŒ, Storybook์—์„œ ๋ Œ๋” ๊ฒฐ๊ณผ์™€ ์†Œ์Šค ์ฝ”๋“œ๋ฅผ ๋‚˜๋ž€ํžˆ ๋ณด๋ฉด ๋ฐ”๋กœ ์ดํ•ด๊ฐ€ ๋ฉ๋‹ˆ๋‹ค. React Native ํ”„๋กœ์ ํŠธ์— Storybook ๋„์ž…์„ ๊ณ ๋ฏผ ์ค‘์ด๋ผ๋ฉด ๊ผญ ์‹œ๋„ํ•ด๋ณด์‹œ๊ธธ ๊ถŒํ•ฉ๋‹ˆ๋‹ค.

๋ฐ˜์‘ํ˜•

๋Œ“๊ธ€


์˜คํ”ˆ ์ฑ„ํŒ