์ž‘๊ฐ€:
    ์ƒ์„ฑ:2024-08-11๋งˆ์ง€๋ง‰ ์—…๋ฐ์ดํŠธ:2026-08-30

    Intlayer๋กœ Create React App ๋ฒˆ์—ญํ•˜๊ธฐ | ๊ตญ์ œํ™”(i18n)

    ide.intlayer.org

    Application Template ์ฐธ์กฐ.

    Intlayer๋ž€ ๋ฌด์—‡์ธ๊ฐ€์š”?

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

    Intlayer๋Š” ํ˜„๋Œ€ ์›น ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์—์„œ ๋‹ค๊ตญ์–ด ์ง€์›์„ ๊ฐ„์†Œํ™”ํ•˜๊ธฐ ์œ„ํ•ด ์„ค๊ณ„๋œ ํ˜์‹ ์ ์ด๊ณ  ์˜คํ”ˆ ์†Œ์Šค์ธ ๊ตญ์ œํ™”(i18n) ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์ž…๋‹ˆ๋‹ค.

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

    Intlayer๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด:

    • ์ปดํฌ๋„ŒํŠธ ์ˆ˜์ค€์—์„œ ์„ ์–ธ์  ์‚ฌ์ „์„ ์‚ฌ์šฉํ•˜์—ฌ ๋ฒˆ์—ญ์„ ์‰ฝ๊ฒŒ ๊ด€๋ฆฌํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
    • ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ, ๋ผ์šฐํŠธ ๋ฐ ์ฝ˜ํ…์ธ ๋ฅผ ๋™์ ์œผ๋กœ ๋กœ์ปฌ๋ผ์ด์ฆˆํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
    • ์ž๋™ ์ƒ์„ฑ๋œ ํƒ€์ž…์œผ๋กœ TypeScript ์ง€์›์„ ๋ณด์žฅํ•˜์—ฌ ์ž๋™ ์™„์„ฑ ๋ฐ ์˜ค๋ฅ˜ ๊ฐ์ง€๋ฅผ ๊ฐœ์„ ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
    • ๋™์  ๋กœ์ผ€์ผ ๊ฐ์ง€ ๋ฐ ์ „ํ™˜๊ณผ ๊ฐ™์€ ๊ณ ๊ธ‰ ๊ธฐ๋Šฅ์„ ํ™œ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

    ์ฝ˜ํ…์ธ ๋ฅผ ํ•จ๊ป˜ ๋ฐฐ์น˜ํ•˜๋ฉด Large Language Models (LLMs)์— ํ•„์š”ํ•œ ์ปจํ…์ŠคํŠธ๋ฅผ ์ค„์ผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. Intlayer๋Š” ๋˜ํ•œ ๋ˆ„๋ฝ๋œ ๋ฒˆ์—ญ์„ ํ…Œ์ŠคํŠธํ•˜๊ธฐ ์œ„ํ•œ CLI, LSP, MCP, ๋ฐ agent skills ๋“ฑ์˜ ๋„๊ตฌ ๋ชจ์Œ์„ ์ œ๊ณตํ•˜์—ฌ AI agents๋ฅผ ์œ„ํ•œ ๊ฐœ๋ฐœ์ž ๊ฒฝํ—˜(DX)์„ ๋”์šฑ ๋งค๋„๋Ÿฝ๊ฒŒ ํ•ฉ๋‹ˆ๋‹ค.

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

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

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


    React ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์—์„œ Intlayer ์„ค์ • ๋‹จ๊ณ„๋ณ„ ๊ฐ€์ด๋“œ

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

      npm์„ ์‚ฌ์šฉํ•˜์—ฌ ํ•„์š”ํ•œ ํŒจํ‚ค์ง€๋ฅผ ์„ค์น˜ํ•ฉ๋‹ˆ๋‹ค:

      bash
      npx intlayer init --interactive
      
      --interactive ํ”Œ๋ž˜๊ทธ๋Š” ์„ ํƒ ์‚ฌํ•ญ์ž…๋‹ˆ๋‹ค. AI ์—์ด์ „ํŠธ์ธ ๊ฒฝ์šฐ intlayer-cli init๋ฅผ ์‚ฌ์šฉํ•˜์„ธ์š”.
      ์ด ๋ช…๋ น์€ ํ™˜๊ฒฝ์„ ๊ฐ์ง€ํ•˜๊ณ  ํ•„์š”ํ•œ ํŒจํ‚ค์ง€๋ฅผ ์„ค์น˜ํ•ฉ๋‹ˆ๋‹ค. ์˜ˆ๋ฅผ ๋“ค์–ด:
      bash
      npm install intlayer react-intlayer react-scripts-intlayer
      
      • intlayer

        ๊ตฌ์„ฑ ๊ด€๋ฆฌ, ๋ฒˆ์—ญ, ์ฝ˜ํ…์ธ  ์„ ์–ธ, ํŠธ๋žœ์ŠคํŒŒ์ผ๋ง ๋ฐ CLI ๋ช…๋ น์„ ์œ„ํ•œ ๊ตญ์ œํ™” ๋„๊ตฌ๋ฅผ ์ œ๊ณตํ•˜๋Š” ํ•ต์‹ฌ ํŒจํ‚ค์ง€์ž…๋‹ˆ๋‹ค.

      • react-intlayer

        Intlayer๋ฅผ React ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜๊ณผ ํ†ตํ•ฉํ•˜๋Š” ํŒจํ‚ค์ง€์ž…๋‹ˆ๋‹ค. React ๊ตญ์ œํ™”๋ฅผ ์œ„ํ•œ ์ปจํ…์ŠคํŠธ ์ œ๊ณต์ž์™€ ํ›…์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.

      • react-scripts-intlayer

      Create React App ๊ธฐ๋ฐ˜ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜๊ณผ Intlayer๋ฅผ ํ†ตํ•ฉํ•˜๊ธฐ ์œ„ํ•œ react-scripts-intlayer ๋ช…๋ น ๋ฐ ํ”Œ๋Ÿฌ๊ทธ์ธ์„ ํฌํ•จํ•ฉ๋‹ˆ๋‹ค. ์ด ํ”Œ๋Ÿฌ๊ทธ์ธ๋“ค์€ craco๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ํ•˜๋ฉฐ, Webpack ๋ฒˆ๋“ค๋Ÿฌ์— ๋Œ€ํ•œ ์ถ”๊ฐ€ ๊ตฌ์„ฑ์„ ํฌํ•จํ•ฉ๋‹ˆ๋‹ค.

    2. ํ”„๋กœ์ ํŠธ ๊ตฌ์„ฑ

      ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์˜ ์–ธ์–ด๋ฅผ ๊ตฌ์„ฑํ•˜๊ธฐ ์œ„ํ•œ ์„ค์ • ํŒŒ์ผ์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค:

      intlayer.config.ts
      import { Locales, type IntlayerConfig } from "intlayer";
      
      const config: IntlayerConfig = {
        internationalization: {
          locales: [
            Locales.ENGLISH,
            Locales.FRENCH,
            Locales.SPANISH,
            // ๋‹ค๋ฅธ ๋กœ์ผ€์ผ ์ถ”๊ฐ€
          ],
          defaultLocale: Locales.ENGLISH,
        },
      };
      
      export default config;
      
      ์ด ๊ตฌ์„ฑ ํŒŒ์ผ์„ ํ†ตํ•ด ๋กœ์ปฌ๋ผ์ด์ฆˆ๋œ URL, ๋ฏธ๋“ค์›จ์–ด ๋ฆฌ๋””๋ ‰์…˜, ์ฟ ํ‚ค ์ด๋ฆ„, ์ฝ˜ํ…์ธ  ์„ ์–ธ์˜ ์œ„์น˜ ๋ฐ ํ™•์žฅ์ž, ์ฝ˜์†”์—์„œ Intlayer ๋กœ๊ทธ ๋น„ํ™œ์„ฑํ™” ๋“ฑ์„ ์„ค์ •ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ ๋งค๊ฐœ๋ณ€์ˆ˜์˜ ์ „์ฒด ๋ชฉ๋ก์€ ๊ตฌ์„ฑ ๋ฌธ์„œ๋ฅผ ์ฐธ์กฐํ•˜์„ธ์š”.
    3. CRA ๊ตฌ์„ฑ์— Intlayer ํ†ตํ•ฉ

      ์Šคํฌ๋ฆฝํŠธ๋ฅผ react-intlayer๋กœ ๋ณ€๊ฒฝํ•ฉ๋‹ˆ๋‹ค:

      package.json
        "scripts": {
          "build": "react-scripts-intlayer build",
          "start": "react-scripts-intlayer start",
          "transpile": "intlayer build"
        },
      
      react-scripts-intlayer ์Šคํฌ๋ฆฝํŠธ๋Š” CRACO๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ํ•ฉ๋‹ˆ๋‹ค. Intlayer craco ํ”Œ๋Ÿฌ๊ทธ์ธ์„ ๊ธฐ๋ฐ˜์œผ๋กœ ์ž์ฒด ์„ค์ •์„ ๊ตฌํ˜„ํ•  ์ˆ˜๋„ ์žˆ์Šต๋‹ˆ๋‹ค. ์˜ˆ์ œ ๋ณด๊ธฐ.
    4. ์ฝ˜ํ…์ธ  ์„ ์–ธ

      ๋ฒˆ์—ญ์„ ์ €์žฅํ•˜๊ธฐ ์œ„ํ•ด ์ฝ˜ํ…์ธ  ์„ ์–ธ์„ ์ƒ์„ฑํ•˜๊ณ  ๊ด€๋ฆฌํ•ฉ๋‹ˆ๋‹ค:

      src/app.content.tsx
      import { t, type Dictionary } from "intlayer";
      import React, { type ReactNode } from "react";
      
      const appContent = {
        key: "app",
        content: {
          getStarted: t<ReactNode>({
            en: (
              <>
                Edit <code>src/App.tsx</code> and save to reload
              </>
            ),
            fr: (
              <>
                ร‰ditez <code>src/App.tsx</code> et enregistrez pour recharger
              </>
            ),
            es: (
              <>
                Edita <code>src/App.tsx</code> y guarda para recargar
              </>
            ),
          }),
          reactLink: {
            href: "https://reactjs.org",
            content: t({
              ko: "React ๋ฐฐ์šฐ๊ธฐ",
              en: "Learn React",
              fr: "Apprendre React",
              es: "Aprender React",
            }),
          },
        },
      } satisfies Dictionary;
      
      export default appContent;
      
      ์ฝ˜ํ…์ธ  ์„ ์–ธ์€ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์–ด๋””์—์„œ๋‚˜ ์ •์˜ํ•  ์ˆ˜ ์žˆ์œผ๋ฉฐ, contentDir ๋””๋ ‰ํ† ๋ฆฌ(๊ธฐ๋ณธ๊ฐ’: ./src)์— ํฌํ•จ๋˜๊ณ  ์ฝ˜ํ…์ธ  ์„ ์–ธ ํŒŒ์ผ ํ™•์žฅ์ž(๊ธฐ๋ณธ๊ฐ’: .content.{json,ts,tsx,js,jsx,mjs,cjs,md,mdx,yaml,yml})์™€ ์ผ์น˜ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.
      ์ž์„ธํ•œ ๋‚ด์šฉ์€ ์ฝ˜ํ…์ธ  ์„ ์–ธ ๋ฌธ์„œ๋ฅผ ์ฐธ์กฐํ•˜์„ธ์š”.
      ์ฝ˜ํ…์ธ  ํŒŒ์ผ์— TSX ์ฝ”๋“œ๊ฐ€ ํฌํ•จ๋œ ๊ฒฝ์šฐ, ์ฝ˜ํ…์ธ  ํŒŒ์ผ์— import React from "react";๋ฅผ ๊ฐ€์ ธ์˜ค๋Š” ๊ฒƒ์„ ๊ณ ๋ คํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.
    5. ์ฝ”๋“œ์—์„œ Intlayer ์‚ฌ์šฉ

      ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์ „๋ฐ˜์—์„œ ์ฝ˜ํ…์ธ  ์‚ฌ์ „์„ ์•ก์„ธ์Šคํ•ฉ๋‹ˆ๋‹ค:

      src/App.tsx
      import logo from "./logo.svg";
      import "./App.css";
      import type { FC } from "react";
      import { IntlayerProvider, useIntlayer } from "react-intlayer";
      
      const AppContent: FC = () => {
        const content = useIntlayer("app");
      
        return (
          <div className="App">
            <img src={logo} className="App-logo" alt="logo" />
      
            {content.getStarted}
            <a
              className="App-link"
              href={content.reactLink.href.value}
              target="_blank"
              rel="noopener noreferrer"
            >
              {content.reactLink.content}
            </a>
          </div>
        );
      };
      
      const App: FC = () => (
        <IntlayerProvider>
          <AppContent />
        </IntlayerProvider>
      );
      
      export default App;
      
      ์ฐธ๊ณ : alt, title, href, aria-label ๋“ฑ๊ณผ ๊ฐ™์€ string ์†์„ฑ์—์„œ ์ฝ˜ํ…์ธ ๋ฅผ ์‚ฌ์šฉํ•˜๋ ค๋ฉด, ํ•จ์ˆ˜์˜ ๊ฐ’์„ ํ˜ธ์ถœํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ์˜ˆ๋ฅผ ๋“ค์–ด:
      html
      <img src="{content.image.src.value}" alt="{content.image.value}" />
      <img src="{content.image.src.toString()}" alt="{content.image.toString()}" />
      <img src="{String(content.image.src)}" alt="{String(content.image)}" />
      

      useIntlayer ํ›…์— ๋Œ€ํ•ด ๋” ์•Œ์•„๋ณด๋ ค๋ฉด ๋ฌธ์„œ๋ฅผ ์ฐธ์กฐํ•˜์„ธ์š”.

  1. ์ฝ˜ํ…์ธ  ์–ธ์–ด ๋ณ€๊ฒฝ

    ์„ ํƒ์‚ฌํ•ญ

    ์ฝ˜ํ…์ธ ์˜ ์–ธ์–ด๋ฅผ ๋ณ€๊ฒฝํ•˜๋ ค๋ฉด useLocale ํ›…์—์„œ ์ œ๊ณตํ•˜๋Š” setLocale ํ•จ์ˆ˜๋ฅผ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ด ํ•จ์ˆ˜๋Š” ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์˜ ๋กœ์ผ€์ผ์„ ์„ค์ •ํ•˜๊ณ  ์ฝ˜ํ…์ธ ๋ฅผ ์—…๋ฐ์ดํŠธํ•ฉ๋‹ˆ๋‹ค.

    src/components/LocaleSwitcher.tsx
    import { Locales } from "intlayer";
    import { useLocale } from "react-intlayer";
    
    const LocaleSwitcher = () => {
      const { setLocale } = useLocale();
    
      return (
        <button onClick={() => setLocale(Locales.English)}>
          ์–ธ์–ด๋ฅผ ์˜์–ด๋กœ ๋ณ€๊ฒฝ
        </button>
      );
    };
    
    useLocale ํ›…์— ๋Œ€ํ•ด ๋” ์•Œ์•„๋ณด๋ ค๋ฉด ๋ฌธ์„œ๋ฅผ ์ฐธ์กฐํ•˜์„ธ์š”.
  2. ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์— ๋กœ์ปฌํ™”๋œ ๋ผ์šฐํŒ… ์ถ”๊ฐ€

    ์„ ํƒ์‚ฌํ•ญ

    ์ด ๋‹จ๊ณ„์˜ ๋ชฉ์ ์€ ๊ฐ ์–ธ์–ด์— ๋Œ€ํ•ด ๊ณ ์œ ํ•œ ๊ฒฝ๋กœ๋ฅผ ๋งŒ๋“œ๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค. ์ด๋Š” SEO ๋ฐ SEO ์นœํ™”์ ์ธ URL์— ์œ ์šฉํ•ฉ๋‹ˆ๋‹ค. ์˜ˆ์‹œ:

    plaintext
    - https://example.com/about
    - https://example.com/es/about
    - https://example.com/fr/about
    
    ๊ธฐ๋ณธ์ ์œผ๋กœ, ๊ธฐ๋ณธ ๋กœ์ผ€์ผ์— ๋Œ€ํ•ด์„œ๋Š” ๊ฒฝ๋กœ๊ฐ€ ์ ‘๋‘์–ด๋กœ ์„ค์ •๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ๊ธฐ๋ณธ ๋กœ์ผ€์ผ์— ์ ‘๋‘์–ด๋ฅผ ์„ค์ •ํ•˜๋ ค๋ฉด, ๊ตฌ์„ฑ์—์„œ middleware.prefixDefault ์˜ต์…˜์„ true๋กœ ์„ค์ •ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ž์„ธํ•œ ๋‚ด์šฉ์€ ๊ตฌ์„ฑ ๋ฌธ์„œ๋ฅผ ์ฐธ์กฐํ•˜์„ธ์š”.

    ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์— ๋กœ์ปฌํ™”๋œ ๋ผ์šฐํŒ…์„ ์ถ”๊ฐ€ํ•˜๋ ค๋ฉด, ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์˜ ๊ฒฝ๋กœ๋ฅผ ๊ฐ์‹ธ๊ณ  ๋กœ์ผ€์ผ ๊ธฐ๋ฐ˜ ๋ผ์šฐํŒ…์„ ์ฒ˜๋ฆฌํ•˜๋Š” LocaleRouter ์ปดํฌ๋„ŒํŠธ๋ฅผ ์ƒ์„ฑํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๋‹ค์Œ์€ React Router๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ์˜ˆ์ œ์ž…๋‹ˆ๋‹ค:

    src/components/LocaleRouter.tsx
    // ํ•„์š”ํ•œ ์ข…์†์„ฑ๊ณผ ํ•จ์ˆ˜ ๊ฐ€์ ธ์˜ค๊ธฐ
    import { type Locales, configuration, getPathWithoutLocale } from "intlayer"; // 'intlayer'์—์„œ ์œ ํ‹ธ๋ฆฌํ‹ฐ ํ•จ์ˆ˜ ๋ฐ ํƒ€์ž… ๊ฐ€์ ธ์˜ค๊ธฐ
    import type { FC, PropsWithChildren } from "react"; // React์˜ ํ•จ์ˆ˜ํ˜• ์ปดํฌ๋„ŒํŠธ ๋ฐ props ํƒ€์ž…
    import { IntlayerProvider } from "react-intlayer"; // ๊ตญ์ œํ™” ์ปจํ…์ŠคํŠธ๋ฅผ ์œ„ํ•œ ์ œ๊ณต์ž
    import {
      BrowserRouter,
      Routes,
      Route,
      Navigate,
      useLocation,
    } from "react-router-dom"; // ํƒ์ƒ‰ ๊ด€๋ฆฌ๋ฅผ ์œ„ํ•œ ๋ผ์šฐํ„ฐ ์ปดํฌ๋„ŒํŠธ
    
    // Intlayer์—์„œ ๊ตฌ์„ฑ ํ•ด์ฒด
    const { internationalization, middleware } = configuration;
    const { locales, defaultLocale } = internationalization;
    
    /**
     * ๋กœ์ปฌํ™”๋ฅผ ์ฒ˜๋ฆฌํ•˜๊ณ  ์ ์ ˆํ•œ ๋กœ์ผ€์ผ ์ปจํ…์ŠคํŠธ๋กœ ์ž์‹์„ ๊ฐ์‹ธ๋Š” ์ปดํฌ๋„ŒํŠธ.
     * URL ๊ธฐ๋ฐ˜ ๋กœ์ผ€์ผ ๊ฐ์ง€ ๋ฐ ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ๋ฅผ ๊ด€๋ฆฌํ•ฉ๋‹ˆ๋‹ค.
     */
    const AppLocalized: FC<PropsWithChildren<{ locale: Locales }>> = ({
      children,
      locale,
    }) => {
      const { pathname, search } = useLocation(); // ํ˜„์žฌ URL ๊ฒฝ๋กœ ๊ฐ€์ ธ์˜ค๊ธฐ
    
      // ํ˜„์žฌ ๋กœ์ผ€์ผ ๊ฒฐ์ •, ์ œ๊ณต๋˜์ง€ ์•Š์€ ๊ฒฝ์šฐ ๊ธฐ๋ณธ ๋กœ์ผ€์ผ๋กœ ๋Œ€์ฒด
      const currentLocale = locale ?? defaultLocale;
    
      // ๊ฒฝ๋กœ์—์„œ ๋กœ์ผ€์ผ ์ ‘๋‘์‚ฌ๋ฅผ ์ œ๊ฑฐํ•˜์—ฌ ๊ธฐ๋ณธ ๊ฒฝ๋กœ ์ƒ์„ฑ
      const pathWithoutLocale = getPathWithoutLocale(
        pathname // ํ˜„์žฌ URL ๊ฒฝ๋กœ
      );
    
      /**
       * middleware.prefixDefault๊ฐ€ true์ธ ๊ฒฝ์šฐ, ๊ธฐ๋ณธ ๋กœ์ผ€์ผ์€ ํ•ญ์ƒ ์ ‘๋‘์–ด๋กœ ์„ค์ •๋˜์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.
       */
      if (middleware.prefixDefault) {
        // ๋กœ์ผ€์ผ ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ
        if (!locale || !locales.includes(locale)) {
          // ๊ธฐ๋ณธ ๋กœ์ผ€์ผ๊ณผ ์—…๋ฐ์ดํŠธ๋œ ๊ฒฝ๋กœ๋กœ ๋ฆฌ๋””๋ ‰์…˜
          return (
            <Navigate
              to={`/${defaultLocale}/${pathWithoutLocale}${search}`}
              replace // ํ˜„์žฌ ํžˆ์Šคํ† ๋ฆฌ ํ•ญ๋ชฉ์„ ์ƒˆ ํ•ญ๋ชฉ์œผ๋กœ ๋Œ€์ฒด
            />
          );
        }
    
        // IntlayerProvider๋กœ ์ž์‹์„ ๊ฐ์‹ธ๊ณ  ํ˜„์žฌ ๋กœ์ผ€์ผ ์„ค์ •
        return (
          <IntlayerProvider locale={currentLocale}>{children}</IntlayerProvider>
        );
      } else {
        /**
         * middleware.prefixDefault๊ฐ€ false์ธ ๊ฒฝ์šฐ, ๊ธฐ๋ณธ ๋กœ์ผ€์ผ์€ ์ ‘๋‘์–ด๋กœ ์„ค์ •๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.
         * ํ˜„์žฌ ๋กœ์ผ€์ผ์ด ์œ ํšจํ•˜๊ณ  ๊ธฐ๋ณธ ๋กœ์ผ€์ผ์ด ์•„๋‹Œ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค.
         */
        if (
          currentLocale.toString() !== defaultLocale.toString() &&
          !locales
            .filter(
              (locale) => locale.toString() !== defaultLocale.toString() // ๊ธฐ๋ณธ ๋กœ์ผ€์ผ ์ œ์™ธ
            )
            .includes(currentLocale) // ํ˜„์žฌ ๋กœ์ผ€์ผ์ด ์œ ํšจํ•œ ๋กœ์ผ€์ผ ๋ชฉ๋ก์— ์žˆ๋Š”์ง€ ํ™•์ธ
        ) {
          // ๋กœ์ผ€์ผ ์ ‘๋‘์‚ฌ๊ฐ€ ์—†๋Š” ๊ฒฝ๋กœ๋กœ ๋ฆฌ๋””๋ ‰์…˜
          return <Navigate to={`${pathWithoutLocale}${search}`} replace />;
        }
    
        // IntlayerProvider๋กœ ์ž์‹์„ ๊ฐ์‹ธ๊ณ  ํ˜„์žฌ ๋กœ์ผ€์ผ ์„ค์ •
        return (
          <IntlayerProvider locale={currentLocale}>{children}</IntlayerProvider>
        );
      }
    };
    
    /**
     * ๋กœ์ผ€์ผ๋ณ„ ๊ฒฝ๋กœ๋ฅผ ์„ค์ •ํ•˜๋Š” ๋ผ์šฐํ„ฐ ์ปดํฌ๋„ŒํŠธ์ž…๋‹ˆ๋‹ค.
     * React Router๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ํƒ์ƒ‰์„ ๊ด€๋ฆฌํ•˜๊ณ  ๋กœ์ปฌํ™”๋œ ์ปดํฌ๋„ŒํŠธ๋ฅผ ๋ Œ๋”๋งํ•ฉ๋‹ˆ๋‹ค.
     */
    export const LocaleRouter: FC<PropsWithChildren> = ({ children }) => (
      <BrowserRouter>
        <Routes>
          {locales
            .filter(
              (locale) => middleware.prefixDefault || locale !== defaultLocale
            )
            .map((locale) => (
              <Route
                // ๋กœ์ผ€์ผ์„ ์บก์ฒ˜ํ•˜๋Š” ๊ฒฝ๋กœ ํŒจํ„ด(e.g., /en/, /fr/) ๋ฐ ๋ชจ๋“  ํ›„์† ๊ฒฝ๋กœ์™€ ์ผ์น˜
                path={`/${locale}/*`}
                key={locale}
                element={<AppLocalized locale={locale}>{children}</AppLocalized>} // ๋กœ์ผ€์ผ ๊ด€๋ฆฌ๋กœ ์ž์‹์„ ๊ฐ์Œˆ
              />
            ))}
    
          {
            // ๊ธฐ๋ณธ ๋กœ์ผ€์ผ ์ ‘๋‘์–ด๊ฐ€ ๋น„ํ™œ์„ฑํ™”๋œ ๊ฒฝ์šฐ, ๋ฃจํŠธ ๊ฒฝ๋กœ์—์„œ ์ž์‹์„ ์ง์ ‘ ๋ Œ๋”๋ง
            !middleware.prefixDefault && (
              <Route
                path="*"
                element={
                  <AppLocalized locale={defaultLocale}>{children}</AppLocalized>
                } // ๋กœ์ผ€์ผ ๊ด€๋ฆฌ๋กœ ์ž์‹์„ ๊ฐ์Œˆ
              />
            )
          }
        </Routes>
      </BrowserRouter>
    );
    

    ๊ทธ๋Ÿฐ ๋‹ค์Œ, ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์—์„œ LocaleRouter ์ปดํฌ๋„ŒํŠธ๋ฅผ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค:

    src/App.tsx
    import { LocaleRouter } from "./components/LocaleRouter";
    import type { FC } from "react";
    
    // ... AppContent ์ปดํฌ๋„ŒํŠธ
    
    const App: FC = () => (
      <LocaleRouter>
        <AppContent />
      </LocaleRouter>
    );
    
  3. ๋กœ์ผ€์ผ ๋ณ€๊ฒฝ ์‹œ URL ๋ณ€๊ฒฝ

    ์„ ํƒ์‚ฌํ•ญ

    ๋กœ์ผ€์ผ์ด ๋ณ€๊ฒฝ๋  ๋•Œ URL์„ ๋ณ€๊ฒฝํ•˜๋ ค๋ฉด useLocale ํ›…์—์„œ ์ œ๊ณตํ•˜๋Š” onLocaleChange ์†์„ฑ์„ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๋™์‹œ์—, react-router-dom์˜ useLocation ๋ฐ useNavigate ํ›…์„ ์‚ฌ์šฉํ•˜์—ฌ URL ๊ฒฝ๋กœ๋ฅผ ์—…๋ฐ์ดํŠธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

    src/components/LocaleSwitcher.tsx
    import { useLocation, useNavigate } from "react-router-dom";
    import {
      Locales,
      getHTMLTextDir,
      getLocaleName,
      getLocalizedUrl,
    } from "intlayer";
    import { useLocale } from "react-intlayer";
    import { type FC } from "react";
    
    const LocaleSwitcher: FC = () => {
      const { pathname, search } = useLocation(); // ํ˜„์žฌ URL ๊ฒฝ๋กœ๋ฅผ ๊ฐ€์ ธ์˜ต๋‹ˆ๋‹ค. ์˜ˆ: /fr/about?foo=bar
      const navigate = useNavigate();
    
      const { locale, availableLocales, setLocale } = useLocale({
        onLocaleChange: (locale) => {
          // ์—…๋ฐ์ดํŠธ๋œ ๋กœ์ผ€์ผ๋กœ URL์„ ๊ตฌ์„ฑํ•ฉ๋‹ˆ๋‹ค.
          // ์˜ˆ: /es/about?foo=bar
          const pathWithLocale = getLocalizedUrl(`${pathname}${search}`, locale);
    
          // URL ๊ฒฝ๋กœ๋ฅผ ์—…๋ฐ์ดํŠธํ•ฉ๋‹ˆ๋‹ค.
          navigate(pathWithLocale);
        },
      });
    
      return (
        <div>
          <button popoverTarget="localePopover">{getLocaleName(locale)}</button>
          <div id="localePopover" popover="auto">
            {availableLocales.map((localeItem) => (
              <a
                href={getLocalizedUrl(location.pathname, localeItem)}
                hrefLang={localeItem}
                aria-current={locale === localeItem ? "page" : undefined}
                onClick={(e) => {
                  e.preventDefault();
                  setLocale(localeItem);
                }}
                key={localeItem}
              >
                <span>
                  {/* ๋กœ์ผ€์ผ - ์˜ˆ: FR */}
                  {localeItem}
                </span>
                <span>
                  {/* ํ•ด๋‹น ๋กœ์ผ€์ผ์˜ ์–ธ์–ด - ์˜ˆ: Franรงais */}
                  {getLocaleName(localeItem, locale)}
                </span>
                <span dir={getHTMLTextDir(localeItem)} lang={localeItem}>
                  {/* ํ˜„์žฌ ๋กœ์ผ€์ผ์˜ ์–ธ์–ด - ์˜ˆ: Francรฉs (ํ˜„์žฌ ๋กœ์ผ€์ผ์ด Locales.SPANISH๋กœ ์„ค์ •๋œ ๊ฒฝ์šฐ) */}
                  {getLocaleName(localeItem)}
                </span>
                <span dir="ltr" lang={Locales.ENGLISH}>
                  {/* ์˜์–ด๋กœ ๋œ ์–ธ์–ด - ์˜ˆ: French */}
                  {getLocaleName(localeItem, Locales.ENGLISH)}
                </span>
              </a>
            ))}
          </div>
        </div>
      );
    };
    

    ๋ฌธ์„œ ์ฐธ์กฐ:

  4. HTML ์–ธ์–ด ๋ฐ ๋ฐฉํ–ฅ ์†์„ฑ ์ „ํ™˜

    ์„ ํƒ์‚ฌํ•ญ

    ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์ด ์—ฌ๋Ÿฌ ์–ธ์–ด๋ฅผ ์ง€์›ํ•  ๋•Œ, <html> ํƒœ๊ทธ์˜ lang ๋ฐ dir ์†์„ฑ์„ ํ˜„์žฌ ๋กœ์ผ€์ผ์— ๋งž๊ฒŒ ์—…๋ฐ์ดํŠธํ•˜๋Š” ๊ฒƒ์ด ์ค‘์š”ํ•ฉ๋‹ˆ๋‹ค. ์ด๋ฅผ ํ†ตํ•ด ๋‹ค์Œ์„ ๋ณด์žฅํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค:

    • ์ ‘๊ทผ์„ฑ: ์Šคํฌ๋ฆฐ ๋ฆฌ๋” ๋ฐ ๋ณด์กฐ ๊ธฐ์ˆ ์€ ์˜ฌ๋ฐ”๋ฅธ lang ์†์„ฑ์„ ์‚ฌ์šฉํ•˜์—ฌ ์ฝ˜ํ…์ธ ๋ฅผ ์ •ํ™•ํžˆ ๋ฐœ์Œํ•˜๊ณ  ํ•ด์„ํ•ฉ๋‹ˆ๋‹ค.
    • ํ…์ŠคํŠธ ๋ Œ๋”๋ง: dir(๋ฐฉํ–ฅ) ์†์„ฑ์€ ํ…์ŠคํŠธ๊ฐ€ ์˜ฌ๋ฐ”๋ฅธ ์ˆœ์„œ๋กœ ๋ Œ๋”๋ง๋˜๋„๋ก ๋ณด์žฅํ•ฉ๋‹ˆ๋‹ค(์˜ˆ: ์˜์–ด๋Š” ์™ผ์ชฝ์—์„œ ์˜ค๋ฅธ์ชฝ, ์•„๋ž์–ด๋‚˜ ํžˆ๋ธŒ๋ฆฌ์–ด๋Š” ์˜ค๋ฅธ์ชฝ์—์„œ ์™ผ์ชฝ). ์ด๋Š” ๊ฐ€๋…์„ฑ์— ํ•„์ˆ˜์ ์ž…๋‹ˆ๋‹ค.
    • SEO: ๊ฒ€์ƒ‰ ์—”์ง„์€ lang ์†์„ฑ์„ ์‚ฌ์šฉํ•˜์—ฌ ํŽ˜์ด์ง€์˜ ์–ธ์–ด๋ฅผ ํŒ๋‹จํ•˜๊ณ , ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ์—์„œ ์ ์ ˆํ•œ ํ˜„์ง€ํ™”๋œ ์ฝ˜ํ…์ธ ๋ฅผ ์ œ๊ณตํ•˜๋Š” ๋ฐ ๋„์›€์„ ์ค๋‹ˆ๋‹ค.

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

  5. Hook ๊ตฌํ˜„

    HTML ์†์„ฑ์„ ๊ด€๋ฆฌํ•˜๊ธฐ ์œ„ํ•œ ์ปค์Šคํ…€ ํ›…์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. ์ด ํ›…์€ ๋กœ์ผ€์ผ ๋ณ€๊ฒฝ์„ ๊ฐ์ง€ํ•˜๊ณ  ์†์„ฑ์„ ์ ์ ˆํžˆ ์—…๋ฐ์ดํŠธํ•ฉ๋‹ˆ๋‹ค:

    src/hooks/useI18nHTMLAttributes.tsx
    import { useEffect } from "react";
    import { useLocale } from "react-intlayer";
    import { getHTMLTextDir } from "intlayer";
    
    /**
     * ํ˜„์žฌ ๋กœ์ผ€์ผ์— ๋”ฐ๋ผ HTML <html> ์š”์†Œ์˜ `lang` ๋ฐ `dir` ์†์„ฑ์„ ์—…๋ฐ์ดํŠธํ•ฉ๋‹ˆ๋‹ค.
     * - `lang`: ๋ธŒ๋ผ์šฐ์ €์™€ ๊ฒ€์ƒ‰ ์—”์ง„์— ํŽ˜์ด์ง€์˜ ์–ธ์–ด๋ฅผ ์•Œ๋ฆฝ๋‹ˆ๋‹ค.
     * - `dir`: ์˜ฌ๋ฐ”๋ฅธ ์ฝ๊ธฐ ๋ฐฉํ–ฅ์„ ๋ณด์žฅํ•ฉ๋‹ˆ๋‹ค (์˜ˆ: ์˜์–ด์˜ ๊ฒฝ์šฐ 'ltr', ์•„๋ž์–ด์˜ ๊ฒฝ์šฐ 'rtl').
     *
     * ์ด ๋™์  ์—…๋ฐ์ดํŠธ๋Š” ์˜ฌ๋ฐ”๋ฅธ ํ…์ŠคํŠธ ๋ Œ๋”๋ง, ์ ‘๊ทผ์„ฑ ๋ฐ SEO๋ฅผ ์œ„ํ•ด ํ•„์ˆ˜์ ์ž…๋‹ˆ๋‹ค.
     */
    export const useI18nHTMLAttributes = () => {
      const { locale } = useLocale();
    
      useEffect(() => {
        // ํ˜„์žฌ ๋กœ์ผ€์ผ๋กœ ์–ธ์–ด ์†์„ฑ์„ ์—…๋ฐ์ดํŠธํ•ฉ๋‹ˆ๋‹ค.
        document.documentElement.lang = locale;
    
        // ํ˜„์žฌ ๋กœ์ผ€์ผ์— ๋”ฐ๋ผ ํ…์ŠคํŠธ ๋ฐฉํ–ฅ์„ ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค.
        document.documentElement.dir = getHTMLTextDir(locale);
      }, [locale]);
    };
    

    ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์—์„œ Hook ์‚ฌ์šฉํ•˜๊ธฐ

    ๋กœ์ผ€์ผ์ด ๋ณ€๊ฒฝ๋  ๋•Œ๋งˆ๋‹ค HTML ์†์„ฑ์ด ์—…๋ฐ์ดํŠธ๋˜๋„๋ก ํ›…์„ ๋ฉ”์ธ ์ปดํฌ๋„ŒํŠธ์— ํ†ตํ•ฉํ•˜์„ธ์š”:

    src/App.tsx
    import type { FC } from "react";
    import { IntlayerProvider, useIntlayer } from "react-intlayer";
    import { useI18nHTMLAttributes } from "./hooks/useI18nHTMLAttributes";
    import "./App.css";
    
    const AppContent: FC = () => {
      // ๋กœ์ผ€์ผ์— ๋”ฐ๋ผ <html> ํƒœ๊ทธ์˜ lang ๋ฐ dir ์†์„ฑ์„ ์—…๋ฐ์ดํŠธํ•˜๋Š” ํ›…์„ ์ ์šฉํ•ฉ๋‹ˆ๋‹ค.
      useI18nHTMLAttributes();
    
      // ... ๋‚˜๋จธ์ง€ ์ปดํฌ๋„ŒํŠธ
    };
    
    const App: FC = () => (
      <IntlayerProvider>
        <AppContent />
      </IntlayerProvider>
    );
    
    export default App;
    

    ์ด๋Ÿฌํ•œ ๋ณ€๊ฒฝ ์‚ฌํ•ญ์„ ์ ์šฉํ•˜๋ฉด ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์€ ๋‹ค์Œ์„ ๋ณด์žฅํ•ฉ๋‹ˆ๋‹ค:

    • ์–ธ์–ด(lang) ์†์„ฑ์ด ํ˜„์žฌ ๋กœ์ผ€์ผ์„ ์ •ํ™•ํžˆ ๋ฐ˜์˜ํ•˜์—ฌ SEO ๋ฐ ๋ธŒ๋ผ์šฐ์ € ๋™์ž‘์— ์ค‘์š”ํ•ฉ๋‹ˆ๋‹ค.
    • ๋กœ์ผ€์ผ์— ๋”ฐ๋ผ ํ…์ŠคํŠธ ๋ฐฉํ–ฅ(dir)์„ ์กฐ์ •ํ•˜์—ฌ ๋‹ค๋ฅธ ์ฝ๊ธฐ ์ˆœ์„œ๋ฅผ ๊ฐ€์ง„ ์–ธ์–ด์˜ ๊ฐ€๋…์„ฑ๊ณผ ์‚ฌ์šฉ์„ฑ์„ ํ–ฅ์ƒ์‹œํ‚ต๋‹ˆ๋‹ค.
    • ์ ‘๊ทผ์„ฑ์„ ๊ฐœ์„ ํ•˜์—ฌ ๋ณด์กฐ ๊ธฐ์ˆ ์ด ์ด๋Ÿฌํ•œ ์†์„ฑ์— ์˜์กดํ•ด ์ตœ์ ์˜ ๊ธฐ๋Šฅ์„ ์ˆ˜ํ–‰ํ•  ์ˆ˜ ์žˆ๋„๋ก ํ•ฉ๋‹ˆ๋‹ค.

    TypeScript ๊ตฌ์„ฑ

    Intlayer๋Š” TypeScript์˜ ๋ชจ๋“ˆ ํ™•์žฅ์„ ์‚ฌ์šฉํ•˜์—ฌ ์ฝ”๋“œ๋ฒ ์ด์Šค๋ฅผ ๋” ๊ฐ•๋ ฅํ•˜๊ฒŒ ๋งŒ๋“ญ๋‹ˆ๋‹ค.

    Autocompletion

    Translation error

    TypeScript ๊ตฌ์„ฑ์— ์ž๋™ ์ƒ์„ฑ๋œ ํƒ€์ž…์„ ํฌํ•จํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

    tsconfig.json
    {
      // ... ๊ธฐ์กด TypeScript ๊ตฌ์„ฑ
      "include": [
        // ... ๊ธฐ์กด TypeScript ๊ตฌ์„ฑ
        ".intlayer/**/*.ts", // ์ž๋™ ์ƒ์„ฑ๋œ ํƒ€์ž… ํฌํ•จ
      ],
    }
    

    Git ๊ตฌ์„ฑ

    Intlayer์—์„œ ์ƒ์„ฑ๋œ ํŒŒ์ผ์„ ๋ฌด์‹œํ•˜๋Š” ๊ฒƒ์ด ์ข‹์Šต๋‹ˆ๋‹ค. ์ด๋ฅผ ํ†ตํ•ด ํ•ด๋‹น ํŒŒ์ผ์„ Git ์ €์žฅ์†Œ์— ์ปค๋ฐ‹ํ•˜๋Š” ๊ฒƒ์„ ๋ฐฉ์ง€ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

    ์ด๋ฅผ ์œ„ํ•ด .gitignore ํŒŒ์ผ์— ๋‹ค์Œ ์ง€์นจ์„ ์ถ”๊ฐ€ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค:

    .gitignore
    # Intlayer์—์„œ ์ƒ์„ฑ๋œ ํŒŒ์ผ ๋ฌด์‹œ
    .intlayer
    

    VS Code ํ™•์žฅ

    Intlayer๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด์„œ ๊ฐœ๋ฐœ ๊ฒฝํ—˜์„ ๊ฐœ์„ ํ•˜๊ธฐ ์œ„ํ•ด ๊ณต์‹ Intlayer VS Code Extension์„ ์„ค์น˜ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

    Intlayer์™€ ํ•จ๊ป˜ ๊ฐœ๋ฐœ ๊ฒฝํ—˜์„ ํ–ฅ์ƒ์‹œํ‚ค๋ ค๋ฉด ๊ณต์‹ Intlayer VS Code ํ™•์žฅ์„ ์„ค์น˜ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. VS Code ๋งˆ์ผ“ํ”Œ๋ ˆ์ด์Šค์—์„œ ์„ค์น˜ํ•˜๊ธฐ

    ์ด ํ™•์žฅ ๊ธฐ๋Šฅ์€ ๋‹ค์Œ์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค:

    • ๋ฒˆ์—ญ ํ‚ค์— ๋Œ€ํ•œ ์ž๋™ ์™„์„ฑ.
    • ๋ˆ„๋ฝ๋œ ๋ฒˆ์—ญ์— ๋Œ€ํ•œ ์‹ค์‹œ๊ฐ„ ์˜ค๋ฅ˜ ๊ฐ์ง€.
    • ๋ฒˆ์—ญ๋œ ์ฝ˜ํ…์ธ ์˜ ์ธ๋ผ์ธ ๋ฏธ๋ฆฌ๋ณด๊ธฐ.
    • ๋ฒˆ์—ญ์„ ์‰ฝ๊ฒŒ ์ƒ์„ฑํ•˜๊ณ  ์—…๋ฐ์ดํŠธํ•  ์ˆ˜ ์žˆ๋Š” ๋น ๋ฅธ ์ž‘์—….

    ํ™•์žฅ ๊ธฐ๋Šฅ ์‚ฌ์šฉ ๋ฐฉ๋ฒ•์— ๋Œ€ํ•œ ์ž์„ธํ•œ ๋‚ด์šฉ์€ Intlayer VS Code ํ™•์žฅ ๊ธฐ๋Šฅ ๋ฌธ์„œ๋ฅผ ์ฐธ์กฐํ•˜์„ธ์š”.

    ๋” ๋‚˜์•„๊ฐ€๊ธฐ

    ๋” ๋‚˜์•„๊ฐ€๋ ค๋ฉด ๋น„์ฃผ์–ผ ์—๋””ํ„ฐ๋ฅผ ๊ตฌํ˜„ํ•˜๊ฑฐ๋‚˜ CMS๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์ฝ˜ํ…์ธ ๋ฅผ ์™ธ๋ถ€ํ™”ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

    ์ž์ฃผ ๋ฌป๋Š” ์งˆ๋ฌธ

    • react-i18next / i18next: ๊ฐ€์žฅ ๋„๋ฆฌ ์‚ฌ์šฉ๋˜๋ฉฐ, ๋Ÿฐํƒ€์ž„์— JSON ๋„ค์ž„์ŠคํŽ˜์ด์Šค๋ฅผ ๋กœ๋“œํ•ฉ๋‹ˆ๋‹ค.
    • react-intl ๋ฐ Lingui: ICU ๋ฉ”์‹œ์ง€ ํ˜•์‹ ๊ธฐ๋ฐ˜์œผ๋กœ, ์ถ”์ถœ ๋ฐฉ์‹์„ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.
    • Intlayer: ๊ฐ€์žฅ ์ง„๋ณด๋œ ์†”๋ฃจ์…˜์ž…๋‹ˆ๋‹ค. ์ฝ”๋“œ๋ฒ ์ด์Šค ์–ด๋””์—๋‚˜ ์ฝ˜ํ…์ธ ๋ฅผ ์„ ์–ธํ•  ์ˆ˜ ์žˆ์œผ๋ฉฐ(๊ฐ ์ปดํฌ๋„ŒํŠธ ์˜† ๋˜๋Š” ์ค‘์•™ ์ง‘์ค‘์‹), react-scripts-intlayer๋ฅผ ํ†ตํ•ด ๋นŒ๋“œ ํƒ€์ž„์— ์ปดํŒŒ์ผ๋˜๊ณ , ์™„์ „ํ•œ ํƒ€์ž… ์•ˆ์ „์„ฑ์„ ์ œ๊ณตํ•˜๋ฉฐ AI ๋ฒˆ์—ญ, ๋น„์ฃผ์–ผ ์—๋””ํ„ฐ ๋ฐ CMS๋ฅผ ์ง€์›ํ•ฉ๋‹ˆ๋‹ค.

    Create React App์€ ์ž์ฒด webpack ๊ตฌ์„ฑ์„ ๋ž˜ํ•‘ํ•˜๋ฏ€๋กœ, ์ง์ ‘ ํ”Œ๋Ÿฌ๊ทธ์ธ์„ ๋“ฑ๋กํ•˜๋Š” ๋Œ€์‹  react-scripts์˜ ๋“œ๋กญ์ธ ๋Œ€์ฒดํ’ˆ์ธ react-scripts-intlayer๋ฅผ ํ†ตํ•ด ํ†ตํ•ฉ๋ฉ๋‹ˆ๋‹ค. ์™œ Intlayer์ธ๊ฐ€์™€ ๋ฒค์น˜๋งˆํฌ๋ฅผ ์ฐธ์กฐํ•˜์„ธ์š”.

    ๋„ค์ž„์ŠคํŽ˜์ด์Šค ๊ธฐ๋ฐ˜ ์„ค์ •๋ณด๋‹ค ํ›จ์”ฌ ์ ์Šต๋‹ˆ๋‹ค. ํŽ˜์ด์ง€๋Š” ๋ Œ๋”๋งํ•˜์ง€ ์•Š๋Š” ์–ธ์–ด์˜ ์นดํƒˆ๋กœ๊ทธ๋ฅผ ์ ˆ๋Œ€ ๋‹ค์šด๋กœ๋“œํ•˜์ง€ ์•Š๊ธฐ ๋•Œ๋ฌธ์ž…๋‹ˆ๋‹ค. ๋นŒ๋“œ ํƒ€์ž„ ์ปดํŒŒ์ผ๋Ÿฌ๋Š” useIntlayer ํ˜ธ์ถœ์„ ์ปดํฌ๋„ŒํŠธ๊ฐ€ ์‚ฌ์šฉํ•˜๋Š” ์ •ํ™•ํ•œ ์‚ฌ์ „ ํ•ญ๋ชฉ์œผ๋กœ ๋Œ€์ฒดํ•˜๋ฏ€๋กœ ์‚ฌ์šฉ๋˜์ง€ ์•Š๋Š” ํ‚ค์™€ ์–ธ์–ด๋Š” ์ œ๊ฑฐ๋˜๊ณ , ๋™์  ์‚ฌ์ „์„ ํ†ตํ•ด ๋กœ์ผ€์ผ๋ณ„๋กœ ๋ถ„ํ• ๋ฉ๋‹ˆ๋‹ค. ์ผ๋ฐ˜์ ์ธ ๋Œ€์•ˆ๋“ค๊ณผ ๋น„๊ตํ–ˆ์„ ๋•Œ Intlayer๋Š” ๋ฒˆ๋“ค ๋ฐ ํŽ˜์ด์ง€ ํฌ๊ธฐ๋ฅผ ์ตœ๋Œ€ 50%๊นŒ์ง€ ์ค„์—ฌ์ค๋‹ˆ๋‹ค. ๋ฒˆ๋“ค ์ตœ์ ํ™”์™€ ๋ฒค์น˜๋งˆํฌ๋ฅผ ์ฐธ์กฐํ•˜์„ธ์š”.

    ๋„ค, ๋‘ ๊ฐ€์ง€ ๋ฐฉ๋ฒ•์ด ์žˆ์Šต๋‹ˆ๋‹ค. react-i18next ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ๊ฐ€์ด๋“œ ๋˜๋Š” i18next ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ๊ฐ€์ด๋“œ๋ฅผ ๋”ฐ๋ผ ์ ์ง„์ ์œผ๋กœ ์ด์ „ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๋˜๋Š” ํ˜„์žฌ API๋ฅผ ์™„์ „ํžˆ ์œ ์ง€ํ•  ์ˆ˜๋„ ์žˆ์Šต๋‹ˆ๋‹ค. ํ˜ธํ™˜ ์–ด๋Œ‘ํ„ฐ(compat adapters)๋Š” react-i18next, react-intl ๋ฐ i18next์™€ ์™„์ „ํžˆ ๋™์ผํ•œ API๋ฅผ ๋…ธ์ถœํ•˜๋ฉด์„œ Intlayer ์‚ฌ์ „์—์„œ ๋ฐ์ดํ„ฐ๋ฅผ ์ œ๊ณตํ•˜๋ฏ€๋กœ, import ๊ตฌ๋ฌธ๋งŒ ๋ณ€๊ฒฝํ•˜๊ณ  ์ปดํฌ๋„ŒํŠธ ์ฝ”๋“œ๋Š” ๊ทธ๋Œ€๋กœ ์œ ์ง€ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

    ๋„ค. sync JSON ํ”Œ๋Ÿฌ๊ทธ์ธ์€ /messages/{locale}/{namespace}.json ํŒŒ์ผ์„ ๋‹จ์ผ ์ง„์‹ค ๊ณต๊ธ‰์›(source of truth)์œผ๋กœ ์œ ์ง€ํ•˜๋ฉด์„œ ์–‘๋ฐฉํ–ฅ์œผ๋กœ Intlayer ์‚ฌ์ „์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. sync PO ํ”Œ๋Ÿฌ๊ทธ์ธ์€ gettext ์นดํƒˆ๋กœ๊ทธ์— ๋Œ€ํ•ด ๋™์ผํ•œ ์ž‘์—…์„ ์ˆ˜ํ–‰ํ•˜๋ฉฐ, ๋กœ์ผ€์ผ๋ณ„ ํŒŒ์ผ์„ ํ†ตํ•ด ๋กœ์ผ€์ผ์„ ํ•œ ํŒŒ์ผ์— ๋ชจ์œผ๋Š” ๋Œ€์‹  ์–ธ์–ด๋ณ„๋กœ ์ฝ˜ํ…์ธ ๋ฅผ ๋ถ„ํ• ํ•  ์ˆ˜๋„ ์žˆ์Šต๋‹ˆ๋‹ค.

    ์•„๋‹™๋‹ˆ๋‹ค. npx intlayer extract๋ฅผ ์‹คํ–‰ํ•˜๋ฉด Intlayer๊ฐ€ ์ปดํฌ๋„ŒํŠธ๋ฅผ ์ฝ๊ณ  ์‚ฌ์šฉ์ž ๋Œ€๋ฉด ๋ฌธ์ž์—ด์„ ์ถ”์ถœํ•˜์—ฌ ๊ฐ ์ปดํฌ๋„ŒํŠธ ์˜†์— .content ํŒŒ์ผ์„ ์ƒ์„ฑํ•˜๋ฏ€๋กœ ์นดํƒˆ๋กœ๊ทธ์— ์ผ์ผ์ด ๋ณต์‚ฌํ•  ํ•„์š” ์—†์ด diff๋งŒ ๊ฒ€ํ† ํ•˜๋ฉด ๋ฉ๋‹ˆ๋‹ค.

    ์™„์ „ ์ž๋™ํ™”๋œ ํŒŒ์ดํ”„๋ผ์ธ์„ ์œ„ํ•ด Intlayer ์ปดํŒŒ์ผ๋Ÿฌ๋Š” ๋นŒ๋“œ ํƒ€์ž„์— JSX, TSX, Vue ๋ฐ Svelte ์†Œ์Šค์—์„œ ๋™์ผํ•œ ์ž‘์—…์„ ์ˆ˜ํ–‰ํ•˜์—ฌ ๋ณ€๊ฒฝ๋  ๋•Œ๋งˆ๋‹ค ์‚ฌ์ „์„ ์ƒ์„ฑํ•˜๊ณ  HMR์„ ํ†ตํ•ด ๋™๊ธฐํ™”ํ•˜๋ฏ€๋กœ ์ˆ˜๋™์œผ๋กœ ํ‚ค๋ฅผ ๊ด€๋ฆฌํ•  ํ•„์š”๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.

    ์ปดํŒŒ์ผ๋Ÿฌ๋ฅผ ์ผœ๊ธฐ ์ „์— ์•Œ์•„๋‘์–ด์•ผ ํ•  ๋‘ ๊ฐ€์ง€ ์ œํ•œ ์‚ฌํ•ญ์ด ์žˆ์Šต๋‹ˆ๋‹ค. ์ •์  ๋ถ„์„์œผ๋กœ ์ž‘๋™ํ•˜๋ฏ€๋กœ API ์˜ค๋ฅ˜ ์ฝ”๋“œ๋‚˜ CMS ํ•„๋“œ์™€ ๊ฐ™์ด ๋Ÿฐํƒ€์ž„์—๋งŒ ์กด์žฌํ•˜๋Š” ๋ฌธ์ž์—ด์€ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. ๋˜ํ•œ ํฐ ์ฝ”๋“œ๋ฒ ์ด์Šค์—์„œ ๋ช‡ ๊ฐ€์ง€ ์–ด๋…ธํ…Œ์ด์…˜์ด ํ•„์š”ํ•œ className="active" ๋˜๋Š” ์ƒํƒœ ์ฝ”๋“œ์™€ ๊ฐ™์€ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ๋กœ์ง๊ณผ ์‚ฌ์šฉ์ž ๋Œ€๋ฉด ํ…์ŠคํŠธ๋ฅผ ๊ตฌ๋ถ„ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. extract ๋ช…๋ น์€ ์‚ฌ์šฉ์ž๊ฐ€ ์ง์ ‘ ์ œ์–ดํ•  ์ˆ˜ ์žˆ๋„๋ก ํ•˜์—ฌ ๋‘ ๊ฐ€์ง€ ๋ฌธ์ œ๋ฅผ ๋ชจ๋‘ ๋ฐฉ์ง€ํ•ฉ๋‹ˆ๋‹ค.

    5๊ฐ€์ง€ ๋„๊ตฌ๊ฐ€ ๋ชจ๋‘ ์„ ํƒ ์‚ฌํ•ญ์œผ๋กœ ์ œ๊ณต๋ฉ๋‹ˆ๋‹ค:

    • VS Code ํ™•์žฅ ํ”„๋กœ๊ทธ๋žจ: useIntlayer ํ‚ค์—์„œ ์ด๋ฅผ ์„ ์–ธํ•œ ์ฝ˜ํ…์ธ  ํŒŒ์ผ๋กœ ๋ฐ”๋กœ ์ด๋™ํ•˜๊ณ , ์ปดํฌ๋„ŒํŠธ์—์„œ ์ฝ˜ํ…์ธ ๋ฅผ ์ถ”์ถœํ•˜๋ฉฐ, ๋ช…๋ น ํŒ”๋ ˆํŠธ๋‚˜ ์ „์šฉ Intlayer ํƒญ์—์„œ build, fill, test, push, pull์„ ์‹คํ–‰ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
    • LSP ์„œ๋ฒ„: LSP๋ฅผ ์ง€์›ํ•˜๋Š” ๋ชจ๋“  ์—๋””ํ„ฐ์—์„œ ์ •์˜๋กœ ์ด๋™, ๋ชจ๋“  ์ฐธ์กฐ ์ฐพ๊ธฐ, ๋ฒˆ์—ญ ๊ฐ’ ๋งˆ์šฐ์Šค ์˜ค๋ฒ„ ๋ฏธ๋ฆฌ๋ณด๊ธฐ, ํ‚ค ๋ฐ ํ•„๋“œ ์ž๋™ ์™„์„ฑ, ์„ ์–ธ๋˜์ง€ ์•Š์€ ํ‚ค์— ๋Œ€ํ•œ ๊ฒฝ๊ณ  ๋“ฑ ๋™์ผํ•œ ๊ธฐ๋Šฅ์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. ๋˜ํ•œ i18next, react-i18next, next-intl, use-intl ํ˜ธ์ถœ๋„ ํ•ด์„ํ•˜๋ฏ€๋กœ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์‹œ ์œ ์šฉํ•ฉ๋‹ˆ๋‹ค.
    • MCP ์„œ๋ฒ„: Cursor, VS Code, Claude Desktop, Claude Code, ChatGPT์— Intlayer ๋ฌธ์„œ์™€ CLI๋ฅผ ๋…ธ์ถœํ•˜์—ฌ AI ์–ด์‹œ์Šคํ„ดํŠธ๊ฐ€ ์ตœ์‹  ๋ฌธ์„œ๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ์ •ํ™•ํžˆ ๋‹ต๋ณ€ํ•˜๊ณ  intlayer fill ๋“ฑ์˜ ๋ช…๋ น์„ ์ง์ ‘ ์‹คํ–‰ํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•ฉ๋‹ˆ๋‹ค.
    • Agent Skills: intlayer-config, intlayer-cli, intlayer-content ๋ฐ ๊ฐ ํ”„๋ ˆ์ž„์›Œํฌ ์ „์šฉ ์Šคํ‚ฌ์„ ํ†ตํ•ด AI ์—์ด์ „ํŠธ์—๊ฒŒ ๋ผ์šฐํŒ… ์„ค์ •๊ณผ ์ฝ˜ํ…์ธ  ๋…ธ๋“œ ํƒ€์ž…์„ ํ•™์Šต์‹œํ‚ต๋‹ˆ๋‹ค.
    • ESLint ํ”Œ๋Ÿฌ๊ทธ์ธ: no-raw-text ๊ทœ์น™์œผ๋กœ ํ•˜๋“œ์ฝ”๋”ฉ๋œ ๋ฌธ์ž์—ด์„ ํ‘œ์‹œํ•˜๊ณ , ์ •์  ์‚ฌ์ „ ํ‚ค ๋ฐ ์‚ฌ์šฉ๋˜์ง€ ์•Š๋Š” ์ฝ˜ํ…์ธ ์— ๋Œ€ํ•œ ์ถ”๊ฐ€ ๊ทœ์น™์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.