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

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

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

    ํด๋ผ์ด์–ธํŠธ ์‚ฌ์ด๋“œ ๋ผ์šฐํŒ…์˜ ๊ฒฝ์šฐ Intlayer์™€ React Router v7 ๊ฐ€์ด๋“œ๋ฅผ ์ฐธ์กฐํ•˜์„ธ์š”.

    ๋ชฉ์ฐจ

    ๋Œ€์•ˆ๋ณด๋‹ค 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 ์„ค์ • ๋‹จ๊ณ„๋ณ„ ๊ฐ€์ด๋“œ

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

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

      bash
      npx intlayer init --interactive
      

      </Tab> </Tabs>

      --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 ๋ฆฌ๋””๋ ‰์…˜ ์ฒ˜๋ฆฌ๋ฅผ ์œ„ํ•œ ๋ฏธ๋“ค์›จ์–ด๋ฅผ ํฌํ•จํ•ฉ๋‹ˆ๋‹ค.

      • @react-router/fs-routes React Router v7์„ ์œ„ํ•œ ํŒŒ์ผ ์‹œ์Šคํ…œ ๊ธฐ๋ฐ˜ ๋ผ์šฐํŒ…์„ ํ™œ์„ฑํ™”ํ•˜๋Š” ํŒจํ‚ค์ง€์ž…๋‹ˆ๋‹ค.

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

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

      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 ํ™˜๊ฒฝ ๋ณ€์ˆ˜๋ฅผ ์ •์˜ํ•ฉ๋‹ˆ๋‹ค. ๋˜ํ•œ ์„ฑ๋Šฅ์„ ์ตœ์ ํ™”ํ•˜๊ธฐ ์œ„ํ•ด ๋ณ„์นญ์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.

    4. React Router v7 ํŒŒ์ผ ์‹œ์Šคํ…œ ๋ผ์šฐํŠธ ๊ตฌ์„ฑ

      ํŒŒ์ผ ์‹œ์Šคํ…œ ๊ธฐ๋ฐ˜ ๋ผ์šฐํŠธ๋ฅผ 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({
        // ์ฝ˜ํ…์ธ  ์„ ์–ธ ํŒŒ์ผ์ด ๊ฒฝ๋กœ๋กœ ์ฒ˜๋ฆฌ๋˜์ง€ ์•Š๋„๋ก ๋ฌด์‹œ
        ignoredRouteFiles: configuration.content.fileExtensions.map(
          (fileExtension) => `**/*${fileExtension}`
        ),
      });
      
      export default routes;
      
      @react-router/fs-routes์˜ flatRoutes ํ•จ์ˆ˜๋Š” ํŒŒ์ผ ์‹œ์Šคํ…œ ๊ธฐ๋ฐ˜ ๋ผ์šฐํŒ…์„ ํ™œ์„ฑํ™”ํ•˜๋ฉฐ, routes/ ๋””๋ ‰ํ† ๋ฆฌ์˜ ํŒŒ์ผ ๊ตฌ์กฐ๊ฐ€ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์˜ ๊ฒฝ๋กœ๋ฅผ ๊ฒฐ์ •ํ•ฉ๋‹ˆ๋‹ค. ignoredRouteFiles ์˜ต์…˜์€ Intlayer ์ฝ˜ํ…์ธ  ์„ ์–ธ ํŒŒ์ผ(.content.ts ๋“ฑ)์ด ๋ผ์šฐํŠธ ํŒŒ์ผ๋กœ ์ฒ˜๋ฆฌ๋˜์ง€ ์•Š๋„๋ก ํ•ฉ๋‹ˆ๋‹ค.
    5. ๋ ˆ์ด์•„์›ƒ ์ปดํฌ๋„ŒํŠธ ์ƒ์„ฑ

      ํŒŒ์ผ ์‹œ์Šคํ…œ ๋ผ์šฐํŒ…์„ ์‚ฌ์šฉํ•  ๋•Œ๋Š” ํ‰ํƒ„ํ•œ ๋ช…๋ช… ๊ทœ์น™์„ ์‚ฌ์šฉํ•˜๋ฉฐ, ์—ฌ๊ธฐ์„œ ์ (.)์€ ๊ฒฝ๋กœ ์„ธ๊ทธ๋จผํŠธ๋ฅผ ๋‚˜ํƒ€๋‚ด๊ณ  ๊ด„ํ˜ธ ()๋Š” ์„ ํƒ์  ์„ธ๊ทธ๋จผํŠธ๋ฅผ ๋‚˜ํƒ€๋ƒ…๋‹ˆ๋‹ค.

      app/routes/ ๋””๋ ‰ํ† ๋ฆฌ์— ๋‹ค์Œ ํŒŒ์ผ๋“ค์„ ์ƒ์„ฑํ•˜์„ธ์š”:

      ํŒŒ์ผ ๊ตฌ์กฐ

      bash
      app/
      โ”œโ”€โ”€ root.tsx                         # ๋กœ์ผ€์ผ ๊ฒฝ๋กœ๋ฅผ ์œ„ํ•œ ๋ ˆ์ด์•„์›ƒ ๋ž˜ํผ
      โ””โ”€โ”€routes/
          โ”œโ”€โ”€ ($locale)._index.tsx         # ํ™ˆ ํŽ˜์ด์ง€ (/, /es, ๋“ฑ)
          โ”œโ”€โ”€ ($locale)._index.content.ts  # ํ™ˆ ํŽ˜์ด์ง€ ์ฝ˜ํ…์ธ 
          โ”œโ”€โ”€ ($locale).about.tsx          # About ํŽ˜์ด์ง€ (/about, /es/about, ๋“ฑ)
          โ””โ”€โ”€ ($locale).about.content.ts   # About ํŽ˜์ด์ง€ ์ฝ˜ํ…์ธ 
      

      ๋ช…๋ช… ๊ทœ์น™:

      • ($locale) - ๋กœ์ผ€์ผ ํŒŒ๋ผ๋ฏธํ„ฐ๋ฅผ ์œ„ํ•œ ์„ ํƒ์  ๋™์  ์„ธ๊ทธ๋จผํŠธ
      • _layout - ์ž์‹ ๊ฒฝ๋กœ๋ฅผ ๋ž˜ํ•‘ํ•˜๋Š” ๋ ˆ์ด์•„์›ƒ ๊ฒฝ๋กœ
      • _index - ์ธ๋ฑ์Šค ๊ฒฝ๋กœ (๋ถ€๋ชจ ๊ฒฝ๋กœ์—์„œ ๋ Œ๋”๋ง)
      • . (์ ) - ๊ฒฝ๋กœ ์„ธ๊ทธ๋จผํŠธ ๊ตฌ๋ถ„ (์˜ˆ: ($locale).about โ†’ /:locale?/about)

      Layout ์ปดํฌ๋„ŒํŠธ

      app/root.tsx
      import { getLocaleFromPath } from "intlayer";
      import { IntlayerProvider } from "react-intlayer";
      import {
        isRouteErrorResponse,
        Meta,
        Outlet,
        Scripts,
        ScrollRestoration,
        useLoaderData,
      } from "react-router";
      
      import type { Route } from "./+types/root";
      
      import "./app.css";
      
      // ... ๋ณ€๊ฒฝ๋˜์ง€ ์•Š์€ App, links ๋ฐ ErrorBoundary ์ฝ”๋“œ
      
      export async function loader({ request }: Route.LoaderArgs) {
        const locale = getLocaleFromPath(request.url);
      
        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>
        );
      }
      

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

      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)} />;
      };
      

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

      app/routes/[lang]/page.tsx
      import { useIntlayer } from "react-intlayer";
      import { LocalizedLink } from "~/components/localized-link";
      
      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>
        );
      }
      
    6. ์ฝ˜ํ…์ธ  ์„ ์–ธ

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

      app/routes/($locale)._index.content.ts
      import { t, type Dictionary } from "intlayer";
      
      const pageContent = {
        key: "page",
        content: {
          title: t({
            ko: "React Router v7 + Intlayer์— ์˜ค์‹  ๊ฒƒ์„ ํ™˜์˜ํ•ฉ๋‹ˆ๋‹ค",
            en: "Welcome to React Router v7 + Intlayer",
            es: "Bienvenido a React Router v7 + Intlayer",
            fr: "Bienvenue sur React Router v7 + Intlayer",
          }),
          description: t({
            ko: "React Router v7๊ณผ Intlayer๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๋‹ค๊ตญ์–ด ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์„ ์‰ฝ๊ฒŒ ๊ตฌ์ถ•ํ•˜์„ธ์š”.",
            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({
            ko: "์ €ํฌ์— ๋Œ€ํ•ด ์•Œ์•„๋ณด๊ธฐ",
            en: "Learn About Us",
            es: "Aprender Sobre Nosotros",
            fr: "En savoir plus sur nous",
          }),
        },
      } satisfies Dictionary;
      
      export default pageContent;
      
      app/routes/($locale).about.content.ts
      import { t, type Dictionary } from "intlayer";
      
      const aboutContent = {
        key: "about",
        content: {
          title: t({
            ko: "์šฐ๋ฆฌ์— ๋Œ€ํ•ด",
            en: "About Us",
            es: "Sobre Nosotros",
            fr: "ร€ propos de nous",
          }),
          content: t({
            ko: "์ด๊ฒƒ์€ ์ •๋ณด ํŽ˜์ด์ง€์˜ ๋‚ด์šฉ์ž…๋‹ˆ๋‹ค.",
            en: "This is the about page content.",
            es: "Este es el contenido de la pรกgina de informaciรณn.",
            fr: "Ceci est le contenu de la page ร  propos.",
          }),
          homeLink: t({
            ko: "ํ™ˆ",
            en: "Home",
            es: "Inicio",
            fr: "Accueil",
          }),
        },
      } satisfies Dictionary;
      
      export default aboutContent;
      
      ์ฝ˜ํ…์ธ  ์„ ์–ธ์€ contentDir ๋””๋ ‰ํ† ๋ฆฌ์— ํฌํ•จ๋˜๋Š” ํ•œ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์˜ ์–ด๋А ๊ณณ์—๋‚˜ ์ •์˜ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค(๊ธฐ๋ณธ๊ฐ’: ./app). ๊ทธ๋ฆฌ๊ณ  ์ฝ˜ํ…์ธ  ์„ ์–ธ ํŒŒ์ผ ํ™•์žฅ์ž์™€ ์ผ์น˜ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค(๊ธฐ๋ณธ๊ฐ’: .content.{json,ts,tsx,js,jsx,mjs,cjs,md,mdx,yaml,yml}).
      useIntlayer ํ›…์— ๋Œ€ํ•ด ๋” ์•Œ์•„๋ณด๋ ค๋ฉด ๋ฌธ์„œ๋ฅผ ์ฐธ์กฐํ•˜์„ธ์š”.
      ๊ธฐ์กด ์•ฑ์ด ์žˆ๋Š” ๊ฒฝ์šฐ Intlayer Compiler ๋ฐ extract command๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์ˆ˜์ฒœ ๊ฐœ์˜ ์ปดํฌ๋„ŒํŠธ๋ฅผ ํ•œ ๋ฒˆ์— ๋ณ€ํ™˜ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
    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 hook์„ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค:

      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. ๋กœ์ผ€์ผ ์Šค์œ„์ฒ˜ ์ปดํฌ๋„ŒํŠธ ๋งŒ๋“ค๊ธฐ

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

      app/components/locale-switcher.tsx
      import type { FC } from "react";
      
      import {
        getHTMLTextDir,
        getLocaleName,
        getLocalizedUrl,
        getPathWithoutLocale,
        Locales,
      } from "intlayer";
      import { setLocaleInStorage, 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 } = 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)}
                  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={Locales.ENGLISH}>
                    {/* ์˜์–ด๋กœ ๋œ ์–ธ์–ด ์ด๋ฆ„ - ์˜ˆ: French */}
                    {getLocaleName(localeItem, Locales.ENGLISH)}
                  </span>
                </Link>
              </li>
            ))}
          </ol>
        );
      };
      
      useLocale ํ›…์— ๋Œ€ํ•ด ๋” ์•Œ์•„๋ณด๋ ค๋ฉด ๋ฌธ์„œ๋ฅผ ์ฐธ์กฐํ•˜์„ธ์š”.
    9. 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]);
      };
      

      ์ด hook์€ ์ด๋ฏธ Step 5์—์„œ ๋ณด์—ฌ์ง„ ๋ ˆ์ด์•„์›ƒ ์ปดํฌ๋„ŒํŠธ(($locale)._layout.tsx)์—์„œ ์‚ฌ์šฉ๋˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค.

    10. ๋ฏธ๋“ค์›จ์–ด ์ถ”๊ฐ€

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

      intlayerProxy๋ฅผ ํ”„๋กœ๋•์…˜์—์„œ ์‚ฌ์šฉํ•˜๋ ค๋ฉด vite-intlayer ํŒจํ‚ค์ง€๋ฅผ devDependencies์—์„œ dependencies๋กœ ๋ณ€๊ฒฝํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

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

      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. ์ปดํฌ๋„ŒํŠธ์—์„œ ์ฝ˜ํ…์ธ  ์ถ”์ถœ

      ์„ ํƒ์‚ฌํ•ญ

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

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

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

      intlayer.config.ts
      import { type IntlayerConfig } from "intlayer";
      
      const config: IntlayerConfig = {
        // ... Rest of your config
        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(), // ์ปดํŒŒ์ผ๋Ÿฌ ํ”Œ๋Ÿฌ๊ทธ์ธ ์ถ”๊ฐ€
        ],
      });
      
      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 ์ง€์›์ด ํฌํ•จ๋œ ์™„์ „ํ•œ ๊ตญ์ œํ™” ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์„ ๊ตฌ์ถ•ํ•˜๋Š” ๋ฐ ํ•„์š”ํ•œ ๋ชจ๋“  ๊ฒƒ์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.