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

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

    ์ด ๊ฐ€์ด๋“œ๋Š” React Router v7 ํ”„๋กœ์ ํŠธ์—์„œ ๋กœ์ผ€์ผ ์ธ์‹ ๋ผ์šฐํŒ…, TypeScript ์ง€์› ๋ฐ ์ตœ์‹  ๊ฐœ๋ฐœ ๋ฐฉ์‹์„ ํ™œ์šฉํ•˜์—ฌ Intlayer๋ฅผ ํ†ตํ•ฉํ•ด ์›ํ™œํ•œ ๊ตญ์ œํ™”(i18n)๋ฅผ ๊ตฌํ˜„ํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ๋ณด์—ฌ์ค๋‹ˆ๋‹ค.

    ์ด ๊ฐ€์ด๋“œ๋Š” ํ”„๋ก ํŠธ์—”๋“œ ๋ผ์šฐํŒ…์— ์ค‘์ ์„ ๋‘ก๋‹ˆ๋‹ค. fs-routes ๋ผ์šฐํŒ…์˜ ๊ฒฝ์šฐ Intlayer with React Router v7 File-System Routes ๊ฐ€์ด๋“œ๋ฅผ ์ฐธ์กฐํ•˜์„ธ์š”.

    ๋ชฉ์ฐจ

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

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

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

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

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

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

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

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

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

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

    www.youtube.com
    ide.intlayer.org
    ide.intlayer.org
    intlayer-react-router-v7.vercel.app

    See the Config-Based Routing Template or File-System Routes Template on GitHub.

    1. ์˜์กด์„ฑ ์„ค์น˜

      ์„ ํ˜ธํ•˜๋Š” ํŒจํ‚ค์ง€ ๊ด€๋ฆฌ์ž๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ํ•„์š”ํ•œ ํŒจํ‚ค์ง€๋ฅผ ์„ค์น˜ํ•˜์„ธ์š”:

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

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

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

      • vite-intlayer
        Intlayer๋ฅผ Vite ๋ฒˆ๋“ค๋Ÿฌ์™€ ํ†ตํ•ฉํ•˜๊ธฐ ์œ„ํ•œ Vite ํ”Œ๋Ÿฌ๊ทธ์ธ๊ณผ, ์‚ฌ์šฉ์ž์˜ ์„ ํ˜ธ ๋กœ์ผ€์ผ ๊ฐ์ง€, ์ฟ ํ‚ค ๊ด€๋ฆฌ, URL ๋ฆฌ๋””๋ ‰์…˜ ์ฒ˜๋ฆฌ๋ฅผ ์œ„ํ•œ ๋ฏธ๋“ค์›จ์–ด๋ฅผ ํฌํ•จํ•ฉ๋‹ˆ๋‹ค.

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

      ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์˜ ์–ธ์–ด๋ฅผ ๊ตฌ์„ฑํ•˜๊ธฐ ์œ„ํ•œ ์„ค์ • ํŒŒ์ผ์„ ๋งŒ๋“œ์„ธ์š”:

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

      ๊ตฌ์„ฑ์— intlayer ํ”Œ๋Ÿฌ๊ทธ์ธ์„ ์ถ”๊ฐ€ํ•˜์„ธ์š”:

      vite.config.ts
      import { reactRouter } from "@react-router/dev/vite";
      import { defineConfig } from "vite";
      import { intlayer } from "vite-intlayer";
      
      export default defineConfig({
        plugins: [reactRouter(), intlayer()],
      });
      
      intlayer() Vite ํ”Œ๋Ÿฌ๊ทธ์ธ์€ Intlayer๋ฅผ Vite์™€ ํ†ตํ•ฉํ•˜๋Š” ๋ฐ ์‚ฌ์šฉ๋ฉ๋‹ˆ๋‹ค. ์ด ํ”Œ๋Ÿฌ๊ทธ์ธ์€ ์ฝ˜ํ…์ธ  ์„ ์–ธ ํŒŒ์ผ์˜ ๋นŒ๋“œ๋ฅผ ๋ณด์žฅํ•˜๊ณ  ๊ฐœ๋ฐœ ๋ชจ๋“œ์—์„œ ์ด๋ฅผ ๋ชจ๋‹ˆํ„ฐ๋งํ•ฉ๋‹ˆ๋‹ค. ๋˜ํ•œ Vite ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ๋‚ด์—์„œ Intlayer ํ™˜๊ฒฝ ๋ณ€์ˆ˜๋ฅผ ์ •์˜ํ•˜๋ฉฐ, ์„ฑ๋Šฅ ์ตœ์ ํ™”๋ฅผ ์œ„ํ•œ ๋ณ„์นญ(alias)๋„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.
    4. React Router v7 ๋ผ์šฐํŠธ ๊ตฌ์„ฑ

      ๋กœ์ผ€์ผ ์ธ์‹ ๋ผ์šฐํŠธ๋กœ ๋ผ์šฐํŒ… ๊ตฌ์„ฑ์„ ์„ค์ •ํ•˜์„ธ์š”:

      app/routes.ts
      import { layout, route, type RouteConfig } from "@react-router/dev/routes";
      
      export default [
        route("/:locale?", "routes/page.tsx"), // ์ง€์—ญํ™”๋œ ํ™ˆ ํŽ˜์ด์ง€
        route("/:locale?/about", "routes/about/page.tsx"), // ์ง€์—ญํ™”๋œ ์†Œ๊ฐœ ํŽ˜์ด์ง€
      ] satisfies RouteConfig;
      

      Set up your routing configuration to use file-system based routes with flatRoutes:

      app/routes.ts
      import type { RouteConfig } from "@react-router/dev/routes";
      import { flatRoutes } from "@react-router/fs-routes";
      import { configuration } from "intlayer";
      
      const routes: RouteConfig = flatRoutes({
        // Ignore content declaration files from being treated as routes
        ignoredRouteFiles: configuration.content.fileExtensions.map(
          (fileExtension) => `**/*${fileExtension}`
        ),
      });
      
      export default routes;
      
      The flatRoutes function from @react-router/fs-routes enables file-system based routing, where the file structure in the routes/ directory determines your application's routes. The ignoredRouteFiles option ensures that Intlayer content declaration files (.content.ts, etc.) are not treated as route files.
    5. ๋ ˆ์ด์•„์›ƒ ์ปดํฌ๋„ŒํŠธ ์ƒ์„ฑ

      ๋ฃจํŠธ ๋ ˆ์ด์•„์›ƒ๊ณผ ๋กœ์ผ€์ผ๋ณ„ ๋ ˆ์ด์•„์›ƒ์„ ์„ค์ •ํ•˜์„ธ์š”:

      ๋ฃจํŠธ ๋ ˆ์ด์•„์›ƒ

      app/root.tsx
      import { getLocaleFromPath } from "intlayer";
      import { IntlayerProvider } from "react-intlayer";
      import {
        data,
        Meta,
        Scripts,
        ScrollRestoration,
        useLoaderData,
      } from "react-router";
      import type { Route } from "./+types/root";
      
      // ... Unchanged App, links and ErrorBoundary code
      
      export async function loader({ request }: Route.LoaderArgs) {
        const locale = getLocaleFromPath(request.url);
      
        if (!locale) {
          throw data("Language not supported", { status: 404 });
        }
      
        return { locale };
      }
      
      export function Layout({
        children,
      }: { children: React.ReactNode } & Route.ComponentProps) {
        const data = useLoaderData<typeof loader>();
        const { locale } = data ?? {};
      
        return (
          <html lang={locale}>
            <head>
              <meta charSet="utf-8" />
              <meta content="width=device-width, initial-scale=1" name="viewport" />
              <Meta />
              <Links />
            </head>
            <body>
              <IntlayerProvider locale={locale}>{children}</IntlayerProvider>
              <ScrollRestoration />
              <Scripts />
            </body>
          </html>
        );
      }
      
    6. ์ฝ˜ํ…์ธ  ์„ ์–ธํ•˜๊ธฐ

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

      app/routes/[lang]/page.content.ts
      import { t, type Dictionary } from "intlayer";
      
      const pageContent = {
        key: "page",
        content: {
          title: t({
            en: "Welcome to React Router v7 + Intlayer",
            es: "Bienvenido a React Router v7 + Intlayer",
            fr: "Bienvenue sur React Router v7 + Intlayer",
          }),
          description: t({
            en: "Build multilingual applications with ease using React Router v7 and Intlayer.",
            es: "Cree aplicaciones multilingรผes fรกcilmente usando React Router v7 y Intlayer.",
            fr: "Crรฉez des applications multilingues facilement avec React Router v7 et Intlayer.",
          }),
          aboutLink: t({
            en: "Learn About Us",
            es: "Aprender Sobre Nosotros",
            fr: "En savoir plus sur nous",
          }),
          homeLink: t({
            en: "Home",
            es: "Inicio",
            fr: "Accueil",
          }),
        },
      } satisfies Dictionary;
      
      export default pageContent;
      
      ์ฝ˜ํ…์ธ  ์„ ์–ธ์€ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ๋‚ด ์–ด๋””์„œ๋“  ์ •์˜ํ•  ์ˆ˜ ์žˆ์œผ๋ฉฐ, contentDir ๋””๋ ‰ํ† ๋ฆฌ(๊ธฐ๋ณธ๊ฐ’: ./app)์— ํฌํ•จ๋˜๋ฉด ์ž๋™์œผ๋กœ ์ธ์‹๋ฉ๋‹ˆ๋‹ค. ๋˜ํ•œ ์ฝ˜ํ…์ธ  ์„ ์–ธ ํŒŒ์ผ ํ™•์žฅ์ž(๊ธฐ๋ณธ๊ฐ’: .content.{json,ts,tsx,js,jsx,mjs,cjs,md,mdx,yaml,yml})์™€ ์ผ์น˜ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.
      ํŒŒ์ผ ์‹œ์Šคํ…œ ๊ธฐ๋ฐ˜ ๋ผ์šฐํŒ…์„ ์‚ฌ์šฉํ•˜๋Š” ๊ฒฝ์šฐ app/routes/($locale)._index.content.ts์— ๋ฐฐ์น˜ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
      ์ž์„ธํ•œ ๋‚ด์šฉ์€ ์ฝ˜ํ…์ธ  ์„ ์–ธ ๋ฌธ์„œ๋ฅผ ์ฐธ์กฐํ•˜์„ธ์š”.
    7. ๋กœ์ผ€์ผ ์ธ์‹ ์ปดํฌ๋„ŒํŠธ ์ƒ์„ฑ

      ๋กœ์ผ€์ผ ์ธ์‹ ๋‚ด๋น„๊ฒŒ์ด์…˜์„ ์œ„ํ•œ LocalizedLink ์ปดํฌ๋„ŒํŠธ๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค:

      app/components/localized-link.tsx
      import type { FC } from "react";
      
      import { getLocalizedUrl, type LocalesValues } from "intlayer";
      import { useLocale } from "react-intlayer";
      import { Link, type LinkProps, type To } from "react-router";
      
      const isExternalLink = (to: string) => /^(https?:)?\/\//.test(to);
      
      // ์™ธ๋ถ€ ๋งํฌ์ธ์ง€ ํ™•์ธํ•˜๋Š” ํ•จ์ˆ˜
      export const locacalizeTo = (to: To, locale: LocalesValues): To => {
        if (typeof to === "string") {
          if (isExternalLink(to)) {
            return to; // ์™ธ๋ถ€ ๋งํฌ๋ฉด ๊ทธ๋Œ€๋กœ ๋ฐ˜ํ™˜
          }
      
          return getLocalizedUrl(to, locale); // ๋‚ด๋ถ€ ๋งํฌ๋ฉด ๋กœ์ผ€์ผ์— ๋งž๊ฒŒ ๋ณ€ํ™˜
        }
      
        if (isExternalLink(to.pathname ?? "")) {
          return to; // ์™ธ๋ถ€ ๋งํฌ๋ฉด ๊ทธ๋Œ€๋กœ ๋ฐ˜ํ™˜
        }
      
        return {
          ...to,
          pathname: getLocalizedUrl(to.pathname ?? "", locale), // ๋‚ด๋ถ€ ๊ฒฝ๋กœ๋ฅผ ๋กœ์ผ€์ผ์— ๋งž๊ฒŒ ๋ณ€ํ™˜
        };
      };
      
      export const LocalizedLink: FC<LinkProps> = (props) => {
        const { locale } = useLocale();
      
        return <Link {...props} to={locacalizeTo(props.to, locale)} />;
      };
      

      ๋กœ์ปฌ๋ผ์ด์ฆˆ๋œ ๊ฒฝ๋กœ๋กœ ์ด๋™ํ•˜๋ ค๋Š” ๊ฒฝ์šฐ, useLocalizedNavigate ํ›…์„ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค:

      app/hooks/useLocalizedNavigate.ts
      import { useLocale } from "react-intlayer";
      import { type NavigateOptions, type To, useNavigate } from "react-router";
      
      import { locacalizeTo } from "~/components/localized-link";
      
      export const useLocalizedNavigate = () => {
        const navigate = useNavigate();
        const { locale } = useLocale();
      
        const localizedNavigate = (to: To, options?: NavigateOptions) => {
          const localedTo = locacalizeTo(to, locale);
      
          navigate(localedTo, options);
        };
      
        return localizedNavigate;
      };
      
    8. ํŽ˜์ด์ง€์—์„œ Intlayer ํ™œ์šฉํ•˜๊ธฐ

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

      ํ˜„์ง€ํ™”๋œ ํ™ˆ ํŽ˜์ด์ง€

      app/routes/page.tsx
      import { getIntlayer, validatePrefix } from "intlayer";
      import { useIntlayer } from "react-intlayer";
      import { data } from "react-router";
      
      import { LocaleSwitcher } from "~/components/locale-switcher";
      
      import { Navbar } from "~/components/navbar";
      import type { Route } from "./+types/page";
      
      export const loader = ({ params }: Route.LoaderArgs) => {
        const { locale } = params;
      
        const { isValid } = validatePrefix(locale);
      
        if (!isValid) {
          throw data("Locale not supported", { status: 404 });
        }
      };
      
      export const meta: Route.MetaFunction = ({ params }) => {
        const content = getIntlayer("page", params.locale);
      
        return [
          { title: content.title },
          { content: content.description, name: "description" },
        ];
      };
      
      export default function Page() {
        const { title, description, aboutLink } = useIntlayer("page");
      
        return (
          <div>
            <h1>{title}</h1>
            <p>{description}</p>
            <nav>
              <LocalizedLink to="/about">{aboutLink}</LocalizedLink>
            </nav>
          </div>
        );
      }
      
      app/routes/($locale)._index.tsx
      import { getIntlayer, validatePrefix } from "intlayer";
      import { useIntlayer } from "react-intlayer";
      import { data } from "react-router";
      
      import { LocaleSwitcher } from "~/components/locale-switcher";
      
      import { Navbar } from "~/components/navbar";
      import type { Route } from "./+types/($locale)._index";
      
      export const loader = ({ params }: Route.LoaderArgs) => {
        const { locale } = params;
      
        const { isValid } = validatePrefix(locale);
      
        if (!isValid) {
          throw data("Locale not supported", { status: 404 });
        }
      };
      
      export const meta: Route.MetaFunction = ({ params }) => {
        const content = getIntlayer("page", params.locale);
      
        return [
          { title: content.title },
          { content: content.description, name: "description" },
        ];
      };
      
      export default function Page() {
        const { title, description, aboutLink } = useIntlayer("page");
      
        return (
          <div>
            <h1>{title}</h1>
            <p>{description}</p>
            <nav>
              <LocalizedLink to="/about">{aboutLink}</LocalizedLink>
            </nav>
          </div>
        );
      }
      
      useIntlayer ํ›…์— ๋Œ€ํ•ด ๋” ์•Œ์•„๋ณด๋ ค๋ฉด ๋ฌธ์„œ๋ฅผ ์ฐธ์กฐํ•˜์„ธ์š”.
      ๊ธฐ์กด ์•ฑ์ด ์žˆ๋‹ค๋ฉด, Intlayer Compiler์™€ extract ๋ช…๋ น์–ด๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์ˆ˜์ฒœ ๊ฐœ์˜ ์ปดํฌ๋„ŒํŠธ๋ฅผ ๋ช‡ ์ดˆ ์•ˆ์— ๋ณ€ํ™˜ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
    9. ๋กœ์ผ€์ผ ์Šค์œ„์ฒ˜ ์ปดํฌ๋„ŒํŠธ ๋งŒ๋“ค๊ธฐ

      ์‚ฌ์šฉ์ž๊ฐ€ ์–ธ์–ด๋ฅผ ๋ณ€๊ฒฝํ•  ์ˆ˜ ์žˆ๋„๋ก ์ปดํฌ๋„ŒํŠธ๋ฅผ ๋งŒ๋“œ์„ธ์š”:

      app/components/locale-switcher.tsx
      import type { FC } from "react";
      
      import {
        getHTMLTextDir,
        getLocaleName,
        getLocalizedUrl,
        getPathWithoutLocale,
        defaultLocale,
      } from "intlayer";
      import { useIntlayer, useLocale } from "react-intlayer";
      import { Link, useLocation } from "react-router";
      
      export const LocaleSwitcher: FC = () => {
        const { localeSwitcherLabel } = useIntlayer("locale-switcher");
        const { pathname } = useLocation();
      
        const { availableLocales, locale, setLocale } = useLocale();
      
        const pathWithoutLocale = getPathWithoutLocale(pathname);
      
        return (
          <ol>
            {availableLocales.map((localeItem) => (
              <li key={localeItem}>
                <Link
                  aria-current={localeItem === locale ? "page" : undefined}
                  aria-label={`${localeSwitcherLabel.value} ${getLocaleName(localeItem)}`}
                  onClick={() => setLocale(localeItem)}
                  reloadDocument // Reload the page to apply the new locale
                  to={getLocalizedUrl(pathWithoutLocale, localeItem)}
                >
                  <span>
                    {/* ๋กœ์ผ€์ผ - ์˜ˆ: FR */}
                    {localeItem}
                  </span>
                  <span>
                    {/* ํ•ด๋‹น ๋กœ์ผ€์ผ์˜ ์–ธ์–ด - ์˜ˆ: Franรงais */}
                    {getLocaleName(localeItem, locale)}
                  </span>
                  <span dir={getHTMLTextDir(localeItem)} lang={localeItem}>
                    {/* ํ˜„์žฌ ๋กœ์ผ€์ผ์—์„œ์˜ ์–ธ์–ด - ์˜ˆ: ํ˜„์žฌ ๋กœ์ผ€์ผ์ด Locales.SPANISH์ผ ๋•Œ Francรฉs */}
                    {getLocaleName(localeItem)}
                  </span>
                  <span dir="ltr" lang={defaultLocale}>
                    {/* ์˜์–ด๋กœ ๋œ ์–ธ์–ด ์ด๋ฆ„ - ์˜ˆ: French */}
                    {getLocaleName(localeItem, defaultLocale)}
                  </span>
                </Link>
              </li>
            ))}
          </ol>
        );
      };
      
      app/components/locale-switcher.tsx
      import type { FC } from "react";
      
      import {
        getHTMLTextDir,
        getLocaleName,
        getLocalizedUrl,
        getPathWithoutLocale,
        defaultLocale,
      } from "intlayer";
      import { useIntlayer, useLocale } from "react-intlayer";
      import { Link, useLocation } from "react-router";
      
      export const LocaleSwitcher: FC = () => {
        const { localeSwitcherLabel } = useIntlayer("locale-switcher");
        const { pathname } = useLocation();
      
        const { availableLocales, locale, setLocale } = useLocale();
      
        const pathWithoutLocale = getPathWithoutLocale(pathname);
      
        return (
          <ol>
            {availableLocales.map((localeItem) => (
              <li key={localeItem}>
                <Link
                  aria-current={localeItem === locale ? "page" : undefined}
                  aria-label={`${localeSwitcherLabel.value} ${getLocaleName(localeItem)}`}
                  onClick={() => setLocale(localeItem)}
                  reloadDocument // Reload the page to apply the new locale
                  to={getLocalizedUrl(pathWithoutLocale, localeItem)}
                >
                  <span>
                    {/* Locale - e.g. FR */}
                    {localeItem}
                  </span>
                  <span>
                    {/* Language in its own Locale - e.g. Franรงais */}
                    {getLocaleName(localeItem, locale)}
                  </span>
                  <span dir={getHTMLTextDir(localeItem)} lang={localeItem}>
                    {/* Language in current Locale - e.g. Francรฉs with current locale set to Locales.SPANISH */}
                    {getLocaleName(localeItem)}
                  </span>
                  <span dir="ltr" lang={defaultLocale}>
                    {/* Language in English - e.g. French */}
                    {getLocaleName(localeItem, defaultLocale)}
                  </span>
                </Link>
              </li>
            ))}
          </ol>
        );
      };
      
      useLocale ํ›…์— ๋Œ€ํ•ด ๋” ์•Œ์•„๋ณด๋ ค๋ฉด ๋ฌธ์„œ๋ฅผ ์ฐธ์กฐํ•˜์„ธ์š”.
    10. HTML ์†์„ฑ ๊ด€๋ฆฌ ์ถ”๊ฐ€ (์„ ํƒ ์‚ฌํ•ญ)

      ์„ ํƒ์‚ฌํ•ญ

      HTML์˜ lang ๋ฐ dir ์†์„ฑ์„ ๊ด€๋ฆฌํ•˜๋Š” ํ›…์„ ๋งŒ๋“ญ๋‹ˆ๋‹ค:

      app/hooks/useI18nHTMLAttributes.tsx
      import { getHTMLTextDir } from "intlayer";
      import { useEffect } from "react";
      import { useLocale } from "react-intlayer";
      
      export const useI18nHTMLAttributes = () => {
        const { locale } = useLocale();
      
        useEffect(() => {
          document.documentElement.lang = locale;
          document.documentElement.dir = getHTMLTextDir(locale);
        }, [locale]);
      };
      

      ๊ทธ๋Ÿฐ ๋‹ค์Œ ๋ฃจํŠธ ์ปดํฌ๋„ŒํŠธ์—์„œ ์‚ฌ์šฉํ•˜์„ธ์š”:

      app/routes/layout.tsx
      import { Outlet } from "react-router";
      import { IntlayerProvider } from "react-intlayer";
      
      import { useI18nHTMLAttributes } from "app/hooks/useI18nHTMLAttributes"; // ํ›…์„ ๊ฐ€์ ธ์˜ต๋‹ˆ๋‹ค
      
      export default function RootLayout() {
        useI18nHTMLAttributes(); // ํ›…์„ ํ˜ธ์ถœํ•ฉ๋‹ˆ๋‹ค
      
        return (
          <IntlayerProvider>
            <Outlet />
          </IntlayerProvider>
        );
      }
      
    11. ๋ฏธ๋“ค์›จ์–ด ์ถ”๊ฐ€

      intlayerProxy๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์— ์„œ๋ฒ„ ์ธก ๋ผ์šฐํŒ…์„ ์ถ”๊ฐ€ํ•  ์ˆ˜๋„ ์žˆ์Šต๋‹ˆ๋‹ค. ์ด ํ”Œ๋Ÿฌ๊ทธ์ธ์€ URL์„ ๊ธฐ๋ฐ˜์œผ๋กœ ํ˜„์žฌ ๋กœ์ผ€์ผ์„ ์ž๋™์œผ๋กœ ๊ฐ์ง€ํ•˜๊ณ  ์ ์ ˆํ•œ ๋กœ์ผ€์ผ ์ฟ ํ‚ค๋ฅผ ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค. ๋กœ์ผ€์ผ์ด ์ง€์ •๋˜์ง€ ์•Š์œผ๋ฉด ํ”Œ๋Ÿฌ๊ทธ์ธ์ด ์‚ฌ์šฉ์ž์˜ ๋ธŒ๋ผ์šฐ์ € ์–ธ์–ด ๊ธฐ๋ณธ ์„ค์ •์„ ๊ธฐ๋ฐ˜์œผ๋กœ ๊ฐ€์žฅ ์ ์ ˆํ•œ ๋กœ์ผ€์ผ์„ ๊ฒฐ์ •ํ•ฉ๋‹ˆ๋‹ค. ๋กœ์ผ€์ผ์ด ๊ฐ์ง€๋˜์ง€ ์•Š์œผ๋ฉด ๊ธฐ๋ณธ ๋กœ์ผ€์ผ๋กœ ๋ฆฌ๋‹ค์ด๋ ‰ํŠธ๋ฉ๋‹ˆ๋‹ค.

      intlayerProxy๋ฅผ ํ”„๋กœ๋•์…˜์—์„œ ์‚ฌ์šฉํ•˜๋ ค๋ฉด vite-intlayer ํŒจํ‚ค์ง€๋ฅผ devDependencies์—์„œ dependencies๋กœ ๋ณ€๊ฒฝํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.
      Intlayer v9๋ถ€ํ„ฐ intlayerProxy()๋Š” intlayer() ํ”Œ๋Ÿฌ๊ทธ์ธ์— ์ง์ ‘ ๋ฒˆ๋“ค๋กœ ํฌํ•จ๋˜์–ด ์žˆ์œผ๋ฉฐ routing.enableProxy ์˜ต์…˜(true๊ฐ€ ๊ธฐ๋ณธ๊ฐ’)์„ ํ†ตํ•ด ๊ธฐ๋ณธ์ ์œผ๋กœ ํ™œ์„ฑํ™”๋ฉ๋‹ˆ๋‹ค. ์•„๋ž˜์™€ ๊ฐ™์ด ๋”ฐ๋กœ ๋“ฑ๋กํ•˜๋Š” ๊ฒƒ์€ ์ด์ œ ์„ ํƒ ์‚ฌํ•ญ์ž…๋‹ˆ๋‹ค. ์ด๋Š” ํ•˜์œ„ ํ˜ธํ™˜์„ฑ์„ ์œ ์ง€ํ•˜๊ณ  ํ”Œ๋Ÿฌ๊ทธ์ธ ์ˆœ์„œ๋ฅผ ์ œ์–ดํ•ด์•ผ ํ•˜๋Š” ์„ค์ •์„ ์œ„ํ•ด ์œ ์ง€๋ฉ๋‹ˆ๋‹ค. routing.enableProxy: false๋กœ ์„ค์ •ํ•˜์—ฌ ๊ฑฐ์ ˆํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. v9 ๋ฆด๋ฆฌ์Šค ๋…ธํŠธ๋ฅผ ์ฐธ์กฐํ•˜์„ธ์š”.
      vite.config.ts
      import { defineConfig } from "vite";
      import react from "@vitejs/plugin-react-swc";
      import { intlayer } from "vite-intlayer";
      
      // https://vitejs.dev/config/
      export default defineConfig({
        plugins: [
          react(),
          intlayer({
            proxy: {
              ignore: (req) => req.url?.startsWith("/api"),
            },
          }),
        ],
      });
      
    12. ์ปดํฌ๋„ŒํŠธ์—์„œ ์ฝ˜ํ…์ธ  ์ถ”์ถœ

      ์„ ํƒ์‚ฌํ•ญ

      ๊ธฐ์กด codebase๊ฐ€ ์žˆ๋Š” ๊ฒฝ์šฐ, ์ˆ˜์ฒœ ๊ฐœ์˜ ํŒŒ์ผ์„ ๋ณ€ํ™˜ํ•˜๋Š” ๊ฒƒ์€ ์‹œ๊ฐ„์ด ๋งŽ์ด ๊ฑธ๋ฆด ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

      ์ด ๊ณผ์ •์„ ์‰ฝ๊ฒŒ ํ•˜๊ธฐ ์œ„ํ•ด Intlayer๋Š” ์ปดํฌ๋„ŒํŠธ๋ฅผ ๋ณ€ํ™˜ํ•˜๊ณ  ์ฝ˜ํ…์ธ ๋ฅผ ์ถ”์ถœํ•˜๋Š” compiler / extractor๋ฅผ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.

      ์ด๋ฅผ ์„ค์ •ํ•˜๋ ค๋ฉด intlayer.config.ts ํŒŒ์ผ์— compiler ์„น์…˜์„ ์ถ”๊ฐ€ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค:

      intlayer.config.ts
      import { type IntlayerConfig } from "intlayer";
      
      const config: IntlayerConfig = {
        // ... ๋‚˜๋จธ์ง€ ์„ค์ •
        compiler: {
          /**
           * ์ปดํŒŒ์ผ๋Ÿฌ ํ™œ์„ฑํ™” ์—ฌ๋ถ€๋ฅผ ๋‚˜ํƒ€๋ƒ…๋‹ˆ๋‹ค.
           */
          enabled: true,
      
          /**
           * ์ถœ๋ ฅ ํŒŒ์ผ ๊ฒฝ๋กœ๋ฅผ ์ •์˜ํ•ฉ๋‹ˆ๋‹ค.
           */
          output: ({ fileName, extension }) => `./${fileName}${extension}`,
      
          /**
           * ๋ณ€ํ™˜ ํ›„ ์ปดํฌ๋„ŒํŠธ๋ฅผ ์ €์žฅํ• ์ง€ ์—ฌ๋ถ€๋ฅผ ๋‚˜ํƒ€๋ƒ…๋‹ˆ๋‹ค.
           *
           * - `true`์ธ ๊ฒฝ์šฐ, ์ปดํŒŒ์ผ๋Ÿฌ๋Š” ๋””์Šคํฌ์˜ ์ปดํฌ๋„ŒํŠธ ํŒŒ์ผ์„ ์žฌ์ž‘์„ฑํ•ฉ๋‹ˆ๋‹ค. ๋”ฐ๋ผ์„œ ๋ณ€ํ™˜์€ ์˜๊ตฌ์ ์ด๋ฉฐ, ์ปดํŒŒ์ผ๋Ÿฌ๋Š” ๋‹ค์Œ ํ”„๋กœ์„ธ์Šค์—์„œ ๋ณ€ํ™˜์„ ๊ฑด๋„ˆ๋œ๋‹ˆ๋‹ค. ์ด๋ ‡๊ฒŒ ํ•˜๋ฉด ์ปดํŒŒ์ผ๋Ÿฌ๋Š” ์•ฑ์„ ๋ณ€ํ™˜ํ•œ ๋‹ค์Œ ์ œ๊ฑฐํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
           *
           * - `false`์ธ ๊ฒฝ์šฐ, ์ปดํŒŒ์ผ๋Ÿฌ๋Š” ๋นŒ๋“œ ์ถœ๋ ฅ์—๋งŒ `useIntlayer()` ํ•จ์ˆ˜ ํ˜ธ์ถœ์„ ์ฃผ์ž…ํ•˜๊ณ  ๊ธฐ๋ณธ codebase๋ฅผ ์œ ์ง€ํ•ฉ๋‹ˆ๋‹ค. ๋ณ€ํ™˜์€ ๋ฉ”๋ชจ๋ฆฌ์—์„œ๋งŒ ์ˆ˜ํ–‰๋ฉ๋‹ˆ๋‹ค.
           */
          saveComponents: false,
      
          /**
           * ๋”•์…”๋„ˆ๋ฆฌ ํ‚ค ์ ‘๋‘์‚ฌ
           */
          dictionaryKeyPrefix: "",
        },
      };
      
      export default config;
      

      extractor๋ฅผ ์‹คํ–‰ํ•˜์—ฌ ์ปดํฌ๋„ŒํŠธ๋ฅผ ๋ณ€ํ™˜ํ•˜๊ณ  ์ฝ˜ํ…์ธ ๋ฅผ ์ถ”์ถœํ•ฉ๋‹ˆ๋‹ค

      bash
      npx intlayer extract
      
      v9๋ถ€ํ„ฐ intlayerCompiler๋Š” intlayer ํ”Œ๋Ÿฌ๊ทธ์ธ์— ํฌํ•จ๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค. ๋”ฐ๋ผ์„œ ์ˆ˜๋™์œผ๋กœ ์ถ”๊ฐ€ํ•  ํ•„์š”๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.

      vite.config.ts๋ฅผ ์—…๋ฐ์ดํŠธํ•˜์—ฌ intlayerCompiler ํ”Œ๋Ÿฌ๊ทธ์ธ์„ ํฌํ•จํ•˜์„ธ์š”:

      vite.config.ts
      import { defineConfig } from "vite";
      import { intlayer, intlayerCompiler } from "vite-intlayer";
      
      export default defineConfig({
        plugins: [
          intlayer(),
          intlayerCompiler(), // compiler plugin์„ ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค
        ],
      });
      
      bash
      npm run build # ๋˜๋Š” npm run dev
      

    Configure TypeScript

    Intlayer uses module augmentation to get benefits of TypeScript and make your codebase stronger.

    Ensure your TypeScript configuration includes the autogenerated types:

    tsconfig.json
    {
      // ... your existing configurations
      include: [
        // ... your existing includes
        ".intlayer/**/*.ts", // Include the auto-generated types
      ],
    }
    

    Git Configuration

    It is recommended to ignore the files generated by Intlayer. This allows you to avoid committing them to your Git repository.

    To do this, you can add the following instructions to your .gitignore file:

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

    VS Code Extension

    To improve your development experience with Intlayer, you can install the official Intlayer VS Code Extension.

    Install from the VS Code Marketplace

    This extension provides:

    • Autocompletion for translation keys.
    • Real-time error detection for missing translations.
    • Inline previews of translated content.
    • Quick actions to easily create and update translations.

    For more details on how to use the extension, refer to the Intlayer VS Code Extension documentation.

    Go Further

    To go further, you can implement the visual editor or externalize your content using the CMS.

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

    ์ด ํฌ๊ด„์ ์ธ ๊ฐ€์ด๋“œ๋Š” Intlayer๋ฅผ React Router v7๊ณผ ํ†ตํ•ฉํ•˜์—ฌ ๋กœ์ผ€์ผ ์ธ์‹ ๋ผ์šฐํŒ…๊ณผ TypeScript ์ง€์›์ด ํฌํ•จ๋œ ์™„์ „ํ•œ ๊ตญ์ œํ™” ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์„ ๊ตฌ์ถ•ํ•˜๋Š” ๋ฐ ํ•„์š”ํ•œ ๋ชจ๋“  ๊ฒƒ์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.

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

    React Router v7์€ ์ž์ฒด ๋ฉ”์‹œ์ง€ ๋ ˆ์ด์–ด๋ฅผ ์ œ๊ณตํ•˜์ง€ ์•Š์œผ๋ฏ€๋กœ i18n ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์™€ ์กฐํ•ฉํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค:

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

    ์™œ 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๋งŒ ๊ฒ€ํ† ํ•˜๋ฉด ๋ฉ๋‹ˆ๋‹ค. ์ด ๊ฐ€์ด๋“œ์˜ 12๋‹จ๊ณ„๋ฅผ ํ™•์ธํ•˜์„ธ์š”.

    ์™„์ „ ์ž๋™ํ™”๋œ ํŒŒ์ดํ”„๋ผ์ธ์„ ์œ„ํ•ด 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 ๊ทœ์น™์œผ๋กœ ํ•˜๋“œ์ฝ”๋”ฉ๋œ ๋ฌธ์ž์—ด์„ ํ‘œ์‹œํ•˜๊ณ , ์ •์  ์‚ฌ์ „ ํ‚ค ๋ฐ ์‚ฌ์šฉ๋˜์ง€ ์•Š๋Š” ์ฝ˜ํ…์ธ ์— ๋Œ€ํ•œ ์ถ”๊ฐ€ ๊ทœ์น™์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.