์ž‘๊ฐ€:
    ์ƒ์„ฑ:2026-03-20๋งˆ์ง€๋ง‰ ์—…๋ฐ์ดํŠธ:2026-05-31

    Storybook๊ณผ ํ•จ๊ป˜ํ•˜๋Š” Intlayer

    ๋ชฉ์ฐจ

    ๋Œ€์•ˆ๋ณด๋‹ค Intlayer๋ฅผ ์„ ํƒํ•ด์•ผ ํ•˜๋Š” ์ด์œ ๋Š” ๋ฌด์—‡์ž…๋‹ˆ๊นŒ?

    'storybook-react-i18next' ๋˜๋Š” 'i18next'์™€ ๊ฐ™์€ ์ฃผ์š” ์†”๋ฃจ์…˜๊ณผ ๋น„๊ตํ•  ๋•Œ Intlayer๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์€ ํ†ตํ•ฉ ์ตœ์ ํ™”๊ฐ€ ์ œ๊ณต๋˜๋Š” ์†”๋ฃจ์…˜์ž…๋‹ˆ๋‹ค.

    Intlayer๋Š” ๋‹ค๊ตญ์–ด ์Šคํ† ๋ฆฌ ๋ฐ์ฝ”๋ ˆ์ดํ„ฐ, ๋กœ์ผ€์ผ ์ „ํ™˜ ๋ฐ ๋””์ž์ธ ์‹œ์Šคํ…œ ์ „๋ฐ˜์— ๊ฑธ์ณ ๊ตญ์ œํ™”(i18n)๋ฅผ ํ™•์žฅํ•˜๋Š” ๋ฐ ํ•„์š”ํ•œ ๋ชจ๋“  ๊ธฐ๋Šฅ์„ ์ œ๊ณตํ•˜์—ฌ Storybook๊ณผ ์™„๋ฒฝํ•˜๊ฒŒ ์ž‘๋™ํ•˜๋„๋ก ์ตœ์ ํ™”๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

    ๋Œ€์šฉ๋Ÿ‰ JSON ํŒŒ์ผ์„ ํŽ˜์ด์ง€์— ๋กœ๋“œํ•˜๋Š” ๋Œ€์‹  ํ•„์š”ํ•œ ์ฝ˜ํ…์ธ ๋งŒ ๋กœ๋“œํ•˜์„ธ์š”. Intlayer๋Š” ๋ฒˆ๋“ค ๋ฐ ํŽ˜์ด์ง€ ํฌ๊ธฐ๋ฅผ ์ตœ๋Œ€ 50% ์ค„์ด๋Š” ๋ฐ ๋„์›€์ด ๋ฉ๋‹ˆ๋‹ค.

    ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์ฝ˜ํ…์ธ ์˜ ๋ฒ”์œ„๋ฅผ ์ง€์ •ํ•˜๋ฉด ๋Œ€๊ทœ๋ชจ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์˜ ์œ ์ง€ ๊ด€๋ฆฌ๊ฐ€ ์šฉ์ดํ•ด์ง‘๋‹ˆ๋‹ค. ์ „์ฒด ์ฝ˜ํ…์ธ  ์ฝ”๋“œ๋ฒ ์ด์Šค๋ฅผ ๊ฒ€ํ† ํ•ด์•ผ ํ•˜๋Š” ์ •์‹ ์  ๋ถ€๋‹ด ์—†์ด ๋‹จ์ผ ๊ธฐ๋Šฅ ํด๋”๋ฅผ ๋ณต์ œํ•˜๊ฑฐ๋‚˜ ์‚ญ์ œํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๋˜ํ•œ Intlayer๋Š” ์™„์ „ํžˆ ์œ ํ˜•๋˜์–ด ์ฝ˜ํ…์ธ ์˜ ์ •ํ™•์„ฑ์„ ๋ณด์žฅํ•ฉ๋‹ˆ๋‹ค.

    ์ฝ˜ํ…์ธ ๋ฅผ ๊ฐ™์€ ์œ„์น˜์— ๋ฐฐ์น˜ํ•˜๋ฉด LLM(๋Œ€ํ˜• ์–ธ์–ด ๋ชจ๋ธ)์— ํ•„์š”ํ•œ ์ปจํ…์ŠคํŠธ๊ฐ€ ์ค„์–ด๋“ญ๋‹ˆ๋‹ค. Intlayer์—๋Š” ๋ˆ„๋ฝ๋œ ๋ฒˆ์—ญ์„ ํ…Œ์ŠคํŠธํ•˜๊ธฐ ์œ„ํ•œ CLI, LSP, MCP ๋ฐ agent skills, AI ์—์ด์ „ํŠธ๋ฅผ ์œ„ํ•œ ๊ฐœ๋ฐœ์ž ๊ฒฝํ—˜(DX)์„ ๋”์šฑ ์›ํ™œํ•˜๊ฒŒ ๋งŒ๋“ญ๋‹ˆ๋‹ค.

    AI ๊ณต๊ธ‰์ž์˜ ๋น„์šฉ์œผ๋กœ ์„ ํƒํ•œ LLM์„ ์‚ฌ์šฉํ•˜์—ฌ CI/CD ํŒŒ์ดํ”„๋ผ์ธ์„ ๋ฒˆ์—ญํ•˜๋ ค๋ฉด ์ž๋™ํ™”๋ฅผ ์‚ฌ์šฉํ•˜์„ธ์š”. Intlayer๋Š” ๋˜ํ•œ ์ฝ˜ํ…์ธ  ์ถ”์ถœ์„ ์ž๋™ํ™”ํ•˜๋Š” ์ปดํŒŒ์ผ๋Ÿฌ์™€ ๋ฐฑ๊ทธ๋ผ์šด๋“œ์—์„œ ๋ฒˆ์—ญ์„ ๋•๋Š” ์›น ํ”Œ๋žซํผ์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.

    ๋Œ€๊ทœ๋ชจ JSON ํŒŒ์ผ์„ ๊ตฌ์„ฑ ์š”์†Œ์— ์—ฐ๊ฒฐํ•˜๋ฉด ์„ฑ๋Šฅ ๋ฐ ๋ฐ˜์‘์„ฑ ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. Intlayer๋Š” ๋นŒ๋“œ ์‹œ ์ฝ˜ํ…์ธ  ๋กœ๋”ฉ์„ ์ตœ์ ํ™”ํ•ฉ๋‹ˆ๋‹ค.

    Intlayer๋Š” ๋‹จ์ˆœํ•œ i18n ์†”๋ฃจ์…˜ ๊ทธ ์ด์ƒ์œผ๋กœ ๊ด€๋ฆฌ์— ๋„์›€์ด ๋˜๋Š” ์ž์ฒด ํ˜ธ์ŠคํŒ… ๋น„์ฃผ์–ผ ํŽธ์ง‘๊ธฐ ๋ฐ ์ „์ฒด CMS๋ฅผ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. ๋‹ค๊ตญ์–ด ์ฝ˜ํ…์ธ ๋ฅผ ์‹ค์‹œ๊ฐ„์œผ๋กœ ์ œ๊ณตํ•˜์—ฌ ๋ฒˆ์—ญ๊ฐ€, ์นดํ”ผ๋ผ์ดํ„ฐ, ๊ธฐํƒ€ ํŒ€์›๊ณผ ์›ํ™œํ•˜๊ฒŒ ํ˜‘์—…ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ฝ˜ํ…์ธ ๋Š” ๋กœ์ปฌ ๋ฐ/๋˜๋Š” ์›๊ฒฉ์œผ๋กœ ์ €์žฅ๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

    ์™œ Storybook์—์„œ Intlayer๋ฅผ ์‚ฌ์šฉํ•ด์•ผ ํ•˜๋‚˜์š”?

    Storybook์€ UI ์ปดํฌ๋„ŒํŠธ๋ฅผ ๋…๋ฆฝ์ ์œผ๋กœ ๊ฐœ๋ฐœํ•˜๊ณ  ๋ฌธ์„œํ™”ํ•˜๊ธฐ ์œ„ํ•œ ์—…๊ณ„ ํ‘œ์ค€ ๋„๊ตฌ์ž…๋‹ˆ๋‹ค. Intlayer์™€ ๊ฒฐํ•ฉํ•˜๋ฉด ๋‹ค์Œ๊ณผ ๊ฐ™์€ ์ด์ ์ด ์žˆ์Šต๋‹ˆ๋‹ค:

    • ๋ชจ๋“  ๋กœ์ผ€์ผ ๋ฏธ๋ฆฌ๋ณด๊ธฐ: ๋„๊ตฌ ๋ชจ์Œ ์Šค์œ„์ฒ˜๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ Storybook ์บ”๋ฒ„์Šค ๋‚ด๋ถ€์—์„œ ์ง์ ‘ ํ™•์ธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
    • ๋ˆ„๋ฝ๋œ ๋ฒˆ์—ญ ๊ฐ์ง€: ํ”„๋กœ๋•์…˜์— ๋„๋‹ฌํ•˜๊ธฐ ์ „์— ๋ฒˆ์—ญ ๋ˆ„๋ฝ์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
    • ๋‹ค๊ตญ์–ด ์ปดํฌ๋„ŒํŠธ ๋ฌธ์„œํ™”: ํ•˜๋“œ์ฝ”๋”ฉ๋œ ๋ฌธ์ž์—ด ๋Œ€์‹  ์‹ค์ œ ํ˜•์‹์ด ์•ˆ์ „ํ•œ(type-safe) ์ฝ˜ํ…์ธ ๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๋ฌธ์„œํ™”ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

    ๋‹จ๊ณ„๋ณ„ ์„ค์ •

    1. ์ข…์†์„ฑ ์„ค์น˜

      bash
      npm install intlayer react-intlayer
      npm install vite-intlayer --save-dev
      
      ํŒจํ‚ค์ง€์—ญํ• 
      intlayer์ฝ”์–ด - ๊ตฌ์„ฑ, ์ฝ˜ํ…์ธ  ์ปดํŒŒ์ผ, CLI
      react-intlayerReact ๋ฐ”์ธ๋”ฉ - IntlayerProvider, useIntlayer ํ›…
      vite-intlayerVite ํ”Œ๋Ÿฌ๊ทธ์ธ - ์ฝ˜ํ…์ธ  ์„ ์–ธ ํŒŒ์ผ์„ ๊ฐ์‹œํ•˜๊ณ  ์ปดํŒŒ์ผ
    2. Intlayer ๊ตฌ์„ฑ ์ƒ์„ฑ

      ํ”„๋กœ์ ํŠธ ๋ฃจํŠธ(๋˜๋Š” ๋””์ž์ธ ์‹œ์Šคํ…œ ํŒจํ‚ค์ง€ ๋‚ด๋ถ€)์— intlayer.config.ts๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค:

      intlayer.config.ts
      import { Locales, type IntlayerConfig } from "intlayer";
      
      const config: IntlayerConfig = {
        internationalization: {
          locales: [
            Locales.ENGLISH,
            Locales.FRENCH,
            Locales.SPANISH,
            // ํ•„์š”ํ•œ ๋กœ์ผ€์ผ์„ ์ถ”๊ฐ€ํ•˜์„ธ์š”
          ],
          defaultLocale: Locales.ENGLISH,
        },
        content: {
          contentDir: ["./src"], // *.content.ts ํŒŒ์ผ์ด ์žˆ๋Š” ์œ„์น˜
        },
      };
      
      export default config;
      
      ์ „์ฒด ์˜ต์…˜ ๋ชฉ๋ก์€ ๊ตฌ์„ฑ ์ฐธ์กฐ๋ฅผ ํ™•์ธํ•˜์„ธ์š”.
    3. Storybook์— Vite ํ”Œ๋Ÿฌ๊ทธ์ธ ์ถ”๊ฐ€

      Storybook์˜ viteFinal ํ›…์„ ์‚ฌ์šฉํ•˜๋ฉด ๋‚ด๋ถ€ Vite ๊ตฌ์„ฑ์„ ํ™•์žฅํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์—ฌ๊ธฐ์— intlayer() ํ”Œ๋Ÿฌ๊ทธ์ธ์„ ๊ฐ€์ ธ์™€์„œ ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค:

      .storybook/main.ts
      import type { StorybookConfig } from "@storybook/react-vite";
      import { defineConfig, mergeConfig } from "vite";
      import { intlayer } from "vite-intlayer";
      
      const config: StorybookConfig = {
        stories: ["../src/**/*.stories.@(js|jsx|ts|tsx)"],
        addons: [
          "@storybook/addon-essentials",
          // โ€ฆ๊ธฐํƒ€ ์• ๋“œ์˜จ
        ],
        framework: {
          name: "@storybook/react-vite",
          options: {},
        },
      
        async viteFinal(baseConfig, { configType }) {
          const env = {
            command: configType === "DEVELOPMENT" ? "serve" : "build",
            mode: configType === "DEVELOPMENT" ? "development" : "production",
          } as const;
      
          const viteConfig = defineConfig(() => ({
            plugins: [
              intlayer({
                proxy: {
                  ignore: (req) => req.url?.startsWith("/api"),
                },
              }),
            ],
          }));
      
          return mergeConfig(baseConfig, viteConfig(env));
        },
      };
      
      export default config;
      

      intlayer() ํ”Œ๋Ÿฌ๊ทธ์ธ์€ *.content.ts ํŒŒ์ผ์„ ๊ฐ์‹œํ•˜๊ณ  Storybook ๊ฐœ๋ฐœ ์ค‘์— ๋ณ€๊ฒฝ ์‚ฌํ•ญ์ด ์ƒ๊ธธ ๋•Œ๋งˆ๋‹ค ์ž๋™์œผ๋กœ ์‚ฌ์ „์„ ๋‹ค์‹œ ๋นŒ๋“œํ•ฉ๋‹ˆ๋‹ค.

    4. `IntlayerProvider` ๋ฐ์ฝ”๋ ˆ์ดํ„ฐ ๋ฐ ๋กœ์ผ€์ผ ๋„๊ตฌ ๋ชจ์Œ ์ถ”๊ฐ€

      Storybook์˜ preview ํŒŒ์ผ์€ ๋ชจ๋“  ์Šคํ† ๋ฆฌ๋ฅผ IntlayerProvider๋กœ ๊ฐ์‹ธ๊ณ  ๋„๊ตฌ ๋ชจ์Œ์— ๋กœ์ผ€์ผ ์Šค์œ„์ฒ˜๋ฅผ ๋…ธ์ถœํ•˜๊ธฐ์— ์ ํ•ฉํ•œ ์žฅ์†Œ์ž…๋‹ˆ๋‹ค:

      .storybook/preview.tsx
      import type { Preview, StoryContext } from "@storybook/react";
      import { IntlayerProvider } from "react-intlayer";
      
      const preview: Preview = {
        // ๋ชจ๋“  ์Šคํ† ๋ฆฌ๋ฅผ IntlayerProvider ๋‚ด๋ถ€์— ๋ž˜ํ•‘
        decorators: [
          (Story, context: StoryContext) => {
            const locale = context.globals.locale ?? "en";
            return (
              <IntlayerProvider locale={locale}>
                <Story />
              </IntlayerProvider>
            );
          },
        ],
      
        // Storybook ๋„๊ตฌ ๋ชจ์Œ์— ๋กœ์ผ€์ผ ์Šค์œ„์ฒ˜ ๋…ธ์ถœ
        globalTypes: {
          locale: {
            description: "ํ™œ์„ฑ ๋กœ์ผ€์ผ",
            defaultValue: "en",
            toolbar: {
              title: "๋กœ์ผ€์ผ",
              icon: "globe",
              items: [
                { value: "en", title: "English" },
                { value: "fr", title: "Franรงais" },
                { value: "es", title: "Espaรฑol" },
              ],
              dynamicTitle: true,
            },
          },
        },
      
        parameters: {
          controls: {
            matchers: {
              color: /(background|color)$/i,
              date: /Date$/i,
            },
          },
        },
      };
      
      export default preview;
      
      locale ๊ฐ’์€ intlayer.config.ts์— ์„ ์–ธ๋œ ๋กœ์ผ€์ผ๊ณผ ์ผ์น˜ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.
    1. ์ข…์†์„ฑ ์„ค์น˜

      bash
      npm install intlayer react-intlayer
      npm install @intlayer/webpack --save-dev
      
    2. Intlayer ๊ตฌ์„ฑ ์ƒ์„ฑ

      ํ”„๋กœ์ ํŠธ ๋ฃจํŠธ์— intlayer.config.ts๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค:

      intlayer.config.ts
      import { Locales, type IntlayerConfig } from "intlayer";
      
      const config: IntlayerConfig = {
        internationalization: {
          locales: [Locales.ENGLISH, Locales.FRENCH, Locales.SPANISH],
          defaultLocale: Locales.ENGLISH,
        },
        content: {
          contentDir: ["./src"],
        },
      };
      
      export default config;
      
    3. Storybook์˜ Webpack ๊ตฌ์„ฑ

      Webpack ๊ธฐ๋ฐ˜ Storybook ์„ค์ •(์˜ˆ: @storybook/react-webpack5)์˜ ๊ฒฝ์šฐ, webpackFinal์„ ํ†ตํ•ด webpack ๊ตฌ์„ฑ์„ ํ™•์žฅํ•˜์—ฌ Intlayer ๋ณ„์นญ(alias) ๋ฐ ๋กœ๋”๋ฅผ ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค:

      .storybook/main.ts
      import type { StorybookConfig } from "@storybook/react-webpack5";
      import { IntlayerPlugin } from "@intlayer/webpack";
      
      const config: StorybookConfig = {
        stories: ["../src/**/*.stories.@(js|jsx|ts|tsx)"],
        addons: ["@storybook/addon-essentials"],
        framework: {
          name: "@storybook/react-webpack5",
          options: {},
        },
      
        webpackFinal: async (baseConfig) => {
          baseConfig.plugins = [...(baseConfig.plugins ?? []), new IntlayerPlugin()];
          return baseConfig;
        },
      };
      
      export default config;
      
    4. `IntlayerProvider` ๋ฐ์ฝ”๋ ˆ์ดํ„ฐ ๋ฐ ๋กœ์ผ€์ผ ๋„๊ตฌ ๋ชจ์Œ ์ถ”๊ฐ€

      Vite ์„ค์ •๊ณผ ๋™์ผํ•˜๊ฒŒ .storybook/preview.tsx์— ๋ฐ์ฝ”๋ ˆ์ดํ„ฐ์™€ ์ „์—ญ ๋กœ์ผ€์ผ ์œ ํ˜•์„ ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค:

      .storybook/preview.tsx
      import type { Preview, StoryContext } from "@storybook/react";
      import { IntlayerProvider } from "react-intlayer";
      
      const preview: Preview = {
        decorators: [
          (Story, context: StoryContext) => {
            const locale = context.globals.locale ?? "en";
            return (
              <IntlayerProvider locale={locale}>
                <Story />
              </IntlayerProvider>
            );
          },
        ],
      
        globalTypes: {
          locale: {
            description: "ํ™œ์„ฑ ๋กœ์ผ€์ผ",
            defaultValue: "en",
            toolbar: {
              title: "๋กœ์ผ€์ผ",
              icon: "globe",
              items: [
                { value: "en", title: "English" },
                { value: "fr", title: "Franรงais" },
                { value: "es", title: "Espaรฑol" },
              ],
              dynamicTitle: true,
            },
          },
        },
      };
      
      export default preview;
      

    ์ฝ˜ํ…์ธ  ์„ ์–ธ

    ๊ฐ ์ปดํฌ๋„ŒํŠธ ์˜†์— *.content.ts ํŒŒ์ผ์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. Intlayer๋Š” ์ปดํŒŒ์ผ ์ค‘์— ์ด๋ฅผ ์ž๋™์œผ๋กœ ๊ฐ์ง€ํ•ฉ๋‹ˆ๋‹ค.

    src/components/CopyButton/CopyButton.content.ts
    import { type Dictionary, t } from "intlayer";
    
    const copyButtonContent = {
      key: "copy-button",
      content: {
        label: t({
          en: "Copy content",
          fr: "Copier le contenu",
          es: "Copiar contenido",
        }),
      },
    } satisfies Dictionary;
    
    export default copyButtonContent;
    
    ๋” ๋งŽ์€ ์ฝ˜ํ…์ธ  ์„ ์–ธ ํ˜•์‹๊ณผ ๊ธฐ๋Šฅ์€ ์ฝ˜ํ…์ธ  ์„ ์–ธ ๋ฌธ์„œ๋ฅผ ์ฐธ์กฐํ•˜์„ธ์š”.

    ์ปดํฌ๋„ŒํŠธ์—์„œ useIntlayer ์‚ฌ์šฉํ•˜๊ธฐ

    src/components/CopyButton/index.tsx
    "use client";
    
    import { type FC } from "react";
    import { useIntlayer } from "react-intlayer";
    
    type CopyButtonProps = {
      content: string;
    };
    
    export const CopyButton: FC<CopyButtonProps> = ({ content }) => {
      const { label } = useIntlayer("copy-button");
    
      return (
        <button
          onClick={() => navigator.clipboard.writeText(content)}
          aria-label={label.value}
          title={label.value}
        >
          ๋ณต์‚ฌ
        </button>
      );
    };
    

    useIntlayer๋Š” ๊ฐ€์žฅ ๊ฐ€๊นŒ์šด IntlayerProvider์—์„œ ์ œ๊ณตํ•˜๋Š” ํ˜„์žฌ ๋กœ์ผ€์ผ์— ๋Œ€ํ•œ ์ปดํŒŒ์ผ๋œ ์‚ฌ์ „์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. Storybook ๋„๊ตฌ ๋ชจ์Œ์—์„œ ๋กœ์ผ€์ผ์„ ์ „ํ™˜ํ•˜๋ฉด ์—…๋ฐ์ดํŠธ๋œ ๋ฒˆ์—ญ์œผ๋กœ ์Šคํ† ๋ฆฌ๊ฐ€ ์ž๋™์œผ๋กœ ๋‹ค์‹œ ๋ Œ๋”๋ง๋ฉ๋‹ˆ๋‹ค.

    ๊ตญ์ œํ™”๋œ ์ปดํฌ๋„ŒํŠธ๋ฅผ ์œ„ํ•œ ์Šคํ† ๋ฆฌ ์ž‘์„ฑ

    IntlayerProvider ๋ฐ์ฝ”๋ ˆ์ดํ„ฐ๊ฐ€ ์„ค์ •๋˜๋ฉด ์Šคํ† ๋ฆฌ๋Š” ์ด์ „๊ณผ ๋™์ผํ•˜๊ฒŒ ์ž‘๋™ํ•ฉ๋‹ˆ๋‹ค. ๋กœ์ผ€์ผ ๋„๊ตฌ ๋ชจ์Œ์€ ์ „์ฒด ์บ”๋ฒ„์Šค์˜ ํ™œ์„ฑ ๋กœ์ผ€์ผ์„ ์ œ์–ดํ•ฉ๋‹ˆ๋‹ค:

    src/components/CopyButton/CopyButton.stories.tsx
    import type { Meta, StoryObj } from "@storybook/react";
    import { CopyButton } from ".";
    
    const meta: Meta<typeof CopyButton> = {
      title: "Components/CopyButton",
      component: CopyButton,
      tags: ["autodocs"],
      argTypes: {
        content: { control: "text" },
      },
    };
    
    export default meta;
    type Story = StoryObj<typeof CopyButton>;
    
    /** ๊ธฐ๋ณธ ์Šคํ† ๋ฆฌ - ๋ฒˆ์—ญ์„ ๋ฏธ๋ฆฌ ๋ณด๋ ค๋ฉด ๋„๊ตฌ ๋ชจ์Œ์—์„œ ๋กœ์ผ€์ผ์„ ์ „ํ™˜ํ•˜์„ธ์š”. */
    export const Default: Story = {
      args: {
        content: "npm install intlayer react-intlayer",
      },
    };
    
    /** ์ผ๋ฐ˜์ ์ธ ์‹ค์ œ ์‚ฌ์šฉ ์‚ฌ๋ก€์ธ ์ฝ”๋“œ ๋ธ”๋ก ๋‚ด๋ถ€์— ๋ฒ„ํŠผ์„ ๋ Œ๋”๋งํ•ฉ๋‹ˆ๋‹ค. */
    export const InsideCodeBlock: Story = {
      render: (args) => (
        <div style={{ position: "relative", display: "inline-block" }}>
          <pre style={{ background: "#1e1e1e", color: "#fff", padding: "1rem" }}>
            <code>{args.content}</code>
          </pre>
          <CopyButton
            content={args.content}
            style={{ position: "absolute", top: 8, right: 8 }}
          />
        </div>
      ),
      args: {
        content: "npx intlayer init",
      },
    };
    
    ๊ฐ ์Šคํ† ๋ฆฌ๋Š” ๋„๊ตฌ ๋ชจ์Œ์—์„œ locale ์ „์—ญ ๋ณ€์ˆ˜๋ฅผ ์ƒ์†ํ•˜๋ฏ€๋กœ ์Šคํ† ๋ฆฌ ์ฝ”๋“œ๋ฅผ ๋ณ€๊ฒฝํ•˜์ง€ ์•Š๊ณ ๋„ ๋ชจ๋“  ๋กœ์ผ€์ผ์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

    ์Šคํ† ๋ฆฌ์—์„œ ๋ฒˆ์—ญ ํ…Œ์ŠคํŠธํ•˜๊ธฐ

    Storybook์˜ play ๊ธฐ๋Šฅ์„ ์‚ฌ์šฉํ•˜์—ฌ ํŠน์ • ๋กœ์ผ€์ผ์— ๋Œ€ํ•ด ์˜ฌ๋ฐ”๋ฅธ ๋ฒˆ์—ญ ํ…์ŠคํŠธ๊ฐ€ ๋ Œ๋”๋ง๋˜์—ˆ๋Š”์ง€ ํ™•์ธํ•˜์„ธ์š”:

    src/components/CopyButton/CopyButton.stories.tsx
    import type { Meta, StoryObj } from "@storybook/react";
    import { expect, within } from "@storybook/test";
    import { CopyButton } from ".";
    
    const meta: Meta<typeof CopyButton> = {
      title: "Components/CopyButton",
      component: CopyButton,
      tags: ["autodocs"],
    };
    
    export default meta;
    type Story = StoryObj<typeof CopyButton>;
    
    export const AccessibleLabel: Story = {
      args: { content: "Hello World" },
      play: async ({ canvasElement }) => {
        const canvas = within(canvasElement);
        const button = canvas.getByRole("button");
    
        // ๋ฒ„ํŠผ์— ๋น„์–ด ์žˆ์ง€ ์•Š์€ ์ ‘๊ทผ ๊ฐ€๋Šฅํ•œ ์ด๋ฆ„์ด ์žˆ๋Š”์ง€ ํ™•์ธ
        await expect(button).toHaveAccessibleName();
        // ๋ฒ„ํŠผ์ด ๋น„ํ™œ์„ฑํ™”๋˜์ง€ ์•Š์•˜๋Š”์ง€ ํ™•์ธ
        await expect(button).not.toBeDisabled();
        // ํ‚ค๋ณด๋“œ ์ ‘๊ทผ์„ฑ ํ™•์ธ
        await expect(button).toHaveAttribute("tabindex", "0");
      },
    };
    

    ์ถ”๊ฐ€ ๋ฆฌ์†Œ์Šค