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

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

    ๋ชฉ์ฐจ

    ์™œ ๋‹ค๋ฅธ ๋Œ€์•ˆ๋“ค ๋Œ€์‹  Intlayer๋ฅผ ์„ ํƒํ•ด์•ผ ํ•˜๋‚˜์š”?

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

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

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

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

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

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

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

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

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

    youtube.com
    ide.intlayer.org
    intlayer-vite-react-template.vercel.app

    GitHub์—์„œ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ํ…œํ”Œ๋ฆฟ์„ ํ™•์ธํ•˜์„ธ์š”.

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

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

      bash
      npx intlayer init --interactive
      
      --interactive ํ”Œ๋ž˜๊ทธ๋Š” ์„ ํƒ ์‚ฌํ•ญ์ž…๋‹ˆ๋‹ค. AI ์—์ด์ „ํŠธ์ธ ๊ฒฝ์šฐ intlayer-cli init์„ ์‚ฌ์šฉํ•˜์„ธ์š”.

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

      bash
      npm install intlayer react-intlayer
      npm install vite-intlayer --save-dev
      
      • intlayer ๊ตญ์ œํ™” ๋„๊ตฌ๋ฅผ ์ œ๊ณตํ•˜๋Š” ํ•ต์‹ฌ ํŒจํ‚ค์ง€๋กœ, ์„ค์ • ๊ด€๋ฆฌ, ๋ฒˆ์—ญ, ์ฝ˜ํ…์ธ  ์„ ์–ธ, ํŠธ๋žœ์ŠคํŒŒ์ผ๋ ˆ์ด์…˜, ๋ฐ CLI ๋ช…๋ น์–ด๋ฅผ ํฌํ•จํ•ฉ๋‹ˆ๋‹ค.

      • react-intlayer Intlayer๋ฅผ React ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜๊ณผ ํ†ตํ•ฉํ•˜๋Š” ํŒจํ‚ค์ง€์ž…๋‹ˆ๋‹ค. React ๊ตญ์ œํ™”๋ฅผ ์œ„ํ•œ context providers์™€ hooks์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.

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

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

      ์•„ํ‚คํ…์ฒ˜

      ์ด ์•„ํ‚คํ…์ฒ˜์—์„œ๋Š” react-intlayer๊ฐ€ main.tsx์— ๋งˆ์šดํŠธ๋œ IntlayerProvider๋ฅผ ์ œ๊ณตํ•˜์—ฌ React ํŠธ๋ฆฌ๋ฅผ ๊ฐ์Œ‰๋‹ˆ๋‹ค. ์ฝ˜ํ…์ธ  ์„ ์–ธ์€ src/์˜ ์ปดํฌ๋„ŒํŠธ์™€ ํ•จ๊ป˜ ๋ฐฐ์น˜๋˜๋ฉฐ, Vite๋Š” vite-intlayer๋ฅผ ํ†ตํ•ด ์ฝ˜ํ…์ธ  ์žฌ๋นŒ๋“œ ๋ฐ HMR์„ ์ฒ˜๋ฆฌํ•ฉ๋‹ˆ๋‹ค.

      bash
      .
      โ”œโ”€โ”€ src
      โ”‚   โ”œโ”€โ”€ app.content.tsx               # App content declaration
      โ”‚   โ”œโ”€โ”€ App.css
      โ”‚   โ”œโ”€โ”€ App.tsx                       # Main application component
      โ”‚   โ”œโ”€โ”€ components
      โ”‚   โ”‚   โ”œโ”€โ”€ LocaleSwitcher.content.ts
      โ”‚   โ”‚   โ””โ”€โ”€ LocaleSwitcher.tsx
      โ”‚   โ”œโ”€โ”€ index.css
      โ”‚   โ”œโ”€โ”€ main.tsx                      # Entry point with IntlayerProvider
      โ”‚   โ””โ”€โ”€ vite-env.d.ts
      โ”œโ”€โ”€ index.html
      โ”œโ”€โ”€ intlayer.config.ts
      โ”œโ”€โ”€ package.json
      โ”œโ”€โ”€ tsconfig.json
      โ””โ”€โ”€ vite.config.ts
      

      ์„ค์ •

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

      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. Intlayer๋ฅผ Vite ๊ตฌ์„ฑ์— ํ†ตํ•ฉ

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

      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()],
      });
      
      intlayer() Vite ํ”Œ๋Ÿฌ๊ทธ์ธ์€ Intlayer๋ฅผ Vite์™€ ํ†ตํ•ฉํ•˜๋Š” ๋ฐ ์‚ฌ์šฉ๋ฉ๋‹ˆ๋‹ค. ์ฝ˜ํ…์ธ  ์„ ์–ธ ํŒŒ์ผ์˜ ๋นŒ๋“œ๋ฅผ ๋ณด์žฅํ•˜๊ณ  ๊ฐœ๋ฐœ ๋ชจ๋“œ์—์„œ ์ด๋ฅผ ๊ฐ์‹œํ•ฉ๋‹ˆ๋‹ค. Vite ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ๋‚ด์— Intlayer ํ™˜๊ฒฝ ๋ณ€์ˆ˜๋ฅผ ์ •์˜ํ•ฉ๋‹ˆ๋‹ค. ๋˜ํ•œ ์„ฑ๋Šฅ์„ ์ตœ์ ํ™”ํ•˜๊ธฐ ์œ„ํ•œ ๋ณ„์นญ์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.
    4. ์ฝ˜ํ…์ธ  ์„ ์–ธ

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

      src/app.content.tsx
      import { t, type Dictionary } from "intlayer";
      import type { ReactNode } from "react";
      
      const appContent = {
        key: "app",
        content: {
          viteLogo: t({
            ko: "Vite ๋กœ๊ณ ",
            en: "Vite logo",
            fr: "Logo Vite",
            es: "Logo Vite",
          }),
          reactLogo: t({
            ko: "React ๋กœ๊ณ ",
            en: "React logo",
            fr: "Logo React",
            es: "Logo React",
          }),
      
          title: "Vite + React",
      
          count: t({
            ko: "์นด์šดํŠธ๋Š” ",
            en: "count is ",
            fr: "le compte est ",
            es: "el recuento es ",
          }),
      
          edit: t<ReactNode>({
            ko: (
              <>
                <code>src/App.tsx</code>๋ฅผ ์ˆ˜์ •ํ•˜๊ณ  ์ €์žฅํ•˜์—ฌ HMR์„ ํ…Œ์ŠคํŠธํ•˜์„ธ์š”
              </>
            ),
            en: (
              <>
                Edit <code>src/App.tsx</code> and save to test HMR
              </>
            ),
            fr: (
              <>
                ร‰ditez <code>src/App.tsx</code> et enregistrez pour tester HMR
              </>
            ),
            es: (
              <>
                Edita <code>src/App.tsx</code> y guarda para probar HMR
              </>
            ),
          }),
      
          readTheDocs: t({
            ko: "Vite ๋ฐ React ๋กœ๊ณ ๋ฅผ ํด๋ฆญํ•˜์—ฌ ์ž์„ธํžˆ ์•Œ์•„๋ณด์„ธ์š”",
            en: "Click on the Vite and React logos to learn more",
            fr: "Cliquez sur les logos Vite et React pour en savoir plus",
            es: "Haga clic en los logotipos de Vite y React para obtener mรกs informaciรณn",
          }),
        },
      } satisfies Dictionary;
      
      export default appContent;
      
      ์ฝ˜ํ…์ธ  ์„ ์–ธ์€ contentDir ๋””๋ ‰ํ† ๋ฆฌ(๊ธฐ๋ณธ๊ฐ’: ./src)์— ํฌํ•จ๋˜๋Š” ํ•œ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์˜ ์–ด๋А ๊ณณ์—๋“  ์ •์˜ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๊ทธ๋ฆฌ๊ณ  ์ฝ˜ํ…์ธ  ์„ ์–ธ ํŒŒ์ผ ํ™•์žฅ์ž(๊ธฐ๋ณธ๊ฐ’: .content.{json,ts,tsx,js,jsx,mjs,cjs,md,mdx,yaml,yml})์™€ ์ผ์น˜ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.
      ์ž์„ธํ•œ ๋‚ด์šฉ์€ ์ฝ˜ํ…์ธ  ์„ ์–ธ ๋ฌธ์„œ๋ฅผ ์ฐธ์กฐํ•˜์„ธ์š”.
      ์ฝ˜ํ…์ธ  ํŒŒ์ผ์— TSX ์ฝ”๋“œ๊ฐ€ ํฌํ•จ๋˜์–ด ์žˆ๋‹ค๋ฉด, ์ฝ˜ํ…์ธ  ํŒŒ์ผ์— import React from "react";๋ฅผ importํ•˜๋Š” ๊ฒƒ์„ ๊ณ ๋ คํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.
    5. ์ฝ”๋“œ์—์„œ ์ฝ˜ํ…์ธ  ํ™œ์šฉ

      ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์ „์ฒด์—์„œ ์ฝ˜ํ…์ธ  ์‚ฌ์ „์— ์ ‘๊ทผํ•˜์„ธ์š”:

      src/App.tsx
      import { useState, type FC } from "react";
      import reactLogo from "./assets/react.svg";
      import viteLogo from "/vite.svg";
      import "./App.css";
      import { IntlayerProvider, useIntlayer } from "react-intlayer";
      
      const AppContent: FC = () => {
        const [count, setCount] = useState(0);
        const content = useIntlayer("app");
      
        return (
          <>
            <div>
              <a href="https://vitejs.dev" target="_blank">
                <img src={viteLogo} className="logo" alt={content.viteLogo.value} />
              </a>
              <a href="https://react.dev" target="_blank">
                <img
                  src={reactLogo}
                  className="logo react"
                  alt={content.reactLogo.value}
                />
              </a>
            </div>
            <h1>{content.title}</h1>
            <div className="card">
              <button onClick={() => setCount((count) => count + 1)}>
                {content.count}
                {count}
              </button>
              <p>{content.edit}</p>
            </div>
            <p className="read-the-docs">{content.readTheDocs}</p>
          </>
        );
      };
      
      const App: FC = () => (
        <IntlayerProvider>
          <AppContent />
        </IntlayerProvider>
      );
      
      export default App;
      
      alt, title, href, aria-label ๋“ฑ๊ณผ ๊ฐ™์€ string ์†์„ฑ์—์„œ ์ฝ˜ํ…์ธ ๋ฅผ ์‚ฌ์šฉํ•˜๋ ค๋ฉด ํ•จ์ˆ˜์˜ ๊ฐ’์„ ๋‹ค์Œ๊ณผ ๊ฐ™์ด ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค:
      tsx
      <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 ํ›…์— ๋Œ€ํ•ด ๋” ์•Œ์•„๋ณด๋ ค๋ฉด ๋ฌธ์„œ๋ฅผ ์ฐธ์กฐํ•˜์„ธ์š”.
      ๊ธฐ์กด ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์ด ์žˆ๋Š” ๊ฒฝ์šฐ, Intlayer Compiler ๋ฐ extract ๋ช…๋ น์„ ์‚ฌ์šฉํ•˜์—ฌ ์ˆ˜์ฒœ ๊ฐœ์˜ ์ปดํฌ๋„ŒํŠธ๋ฅผ ๋ช‡ ์ดˆ ๋งŒ์— ๋ณ€ํ™˜ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
    6. ์ฝ˜ํ…์ธ  ์–ธ์–ด ๋ณ€๊ฒฝ

      ์„ ํƒ์‚ฌํ•ญ

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

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

      ์„ ํƒ์‚ฌํ•ญ

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

      -
      - https://example.com/ko/about
      - https://example.com/fr/about
      

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

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

      src/components/LocaleRouter.tsx
      import { localeMap } from "intlayer"; // 'intlayer'์˜ ์œ ํ‹ธ๋ฆฌํ‹ฐ ํ•จ์ˆ˜ ๋ฐ ํƒ€์ž…
      import type { FC, PropsWithChildren } from "react"; // ํ•จ์ˆ˜ํ˜• ์ปดํฌ๋„ŒํŠธ ๋ฐ props์— ๋Œ€ํ•œ React ํƒ€์ž…
      import { IntlayerProvider } from "react-intlayer"; // ๊ตญ์ œํ™” ์ปจํ…์ŠคํŠธ๋ฅผ ์œ„ํ•œ Provider
      import { BrowserRouter, Route, Routes } from "react-router-dom"; // ๋„ค๋น„๊ฒŒ์ด์…˜์„ ๊ด€๋ฆฌํ•˜๊ธฐ ์œ„ํ•œ Router ์ปดํฌ๋„ŒํŠธ
      
      /**
       * ๋กœ์ผ€์ผ๋ณ„ ๋ผ์šฐํŠธ๋ฅผ ์„ค์ •ํ•˜๋Š” ๋ผ์šฐํ„ฐ ์ปดํฌ๋„ŒํŠธ์ž…๋‹ˆ๋‹ค.
       * React Router๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๋„ค๋น„๊ฒŒ์ด์…˜์„ ๊ด€๋ฆฌํ•˜๊ณ  ๋กœ์ปฌ๋ผ์ด์ฆˆ๋œ ์ปดํฌ๋„ŒํŠธ๋ฅผ ๋ Œ๋”๋งํ•ฉ๋‹ˆ๋‹ค.
       */
      export const LocaleRouter: FC<PropsWithChildren> = ({ children }) => (
        <BrowserRouter>
          <Routes>
            {localeMap(({ locale, urlPrefix }) => (
              <Route
                // ๋กœ์ผ€์ผ(/en/, /fr/ ๋“ฑ)์„ ์บก์ฒ˜ํ•˜๊ณ  ์ดํ›„์˜ ๋ชจ๋“  ๊ฒฝ๋กœ์™€ ์ผ์น˜ํ•˜๋Š” ๋ผ์šฐํŠธ ํŒจํ„ด
                path={`${urlPrefix}/*`}
                key={locale}
                element={
                  <IntlayerProvider locale={locale}>{children}</IntlayerProvider>
                } // ๋กœ์ผ€์ผ ๊ด€๋ฆฌ๋ฅผ ์œ„ํ•ด children์„ ๋ž˜ํ•‘ํ•ฉ๋‹ˆ๋‹ค
              />
            ))}
          </Routes>
        </BrowserRouter>
      );
      
      ์ฐธ๊ณ : routing.mode: 'no-prefix' | 'search-params'๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๊ฒฝ์šฐ, localeMap ํ•จ์ˆ˜๋ฅผ ์‚ฌ์šฉํ•  ํ•„์š”๊ฐ€ ์—†์„ ๊ฐ€๋Šฅ์„ฑ์ด ๋†’์Šต๋‹ˆ๋‹ค.

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

      src/App.tsx
      import { LocaleRouter } from "./components/LocaleRouter";
      import type { FC } from "react";
      
      // ... Your AppContent component
      
      const App: FC = () => (
        <LocaleRouter>
          <AppContent />
        </LocaleRouter>
      );
      

      ๋˜ํ•œ 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"),
            },
          }),
        ],
      });
      
    8. ๋กœ์ผ€์ผ์ด ๋ณ€๊ฒฝ๋  ๋•Œ 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>
        );
      };
      

      ๋ฌธ์„œ ์ฐธ๊ณ :

      ์•„๋ž˜๋Š” ์ถ”๊ฐ€ ์„ค๋ช…๊ณผ ๊ฐœ์„ ๋œ ์ฝ”๋“œ ์˜ˆ์ œ๊ฐ€ ํฌํ•จ๋œ ์—…๋ฐ์ดํŠธ๋œ 9๋‹จ๊ณ„์ž…๋‹ˆ๋‹ค:

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

      ์„ ํƒ์‚ฌํ•ญ

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

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

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

      Hook ๊ตฌํ˜„ํ•˜๊ธฐ

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

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

      ํ›… ๊ตฌํ˜„ํ•˜๊ธฐ

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

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

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

      ์„ ํƒ์‚ฌํ•ญ

      ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์˜ ๋‚ด๋น„๊ฒŒ์ด์…˜์ด ํ˜„์žฌ ๋กœ์ผ€์ผ์„ ์ค€์ˆ˜ํ•˜๋„๋ก ํ•˜๋ ค๋ฉด, ์ปค์Šคํ…€ Link ์ปดํฌ๋„ŒํŠธ๋ฅผ ๋งŒ๋“ค ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ด ์ปดํฌ๋„ŒํŠธ๋Š” ๋‚ด๋ถ€ URL์— ์ž๋™์œผ๋กœ ํ˜„์žฌ ์–ธ์–ด ์ ‘๋‘์‚ฌ๋ฅผ ๋ถ™์—ฌ์ค๋‹ˆ๋‹ค. ์˜ˆ๋ฅผ ๋“ค์–ด, ํ”„๋ž‘์Šค์–ด ์‚ฌ์šฉ์ž๊ฐ€ "About" ํŽ˜์ด์ง€๋กœ ๊ฐ€๋Š” ๋งํฌ๋ฅผ ํด๋ฆญํ•˜๋ฉด /about ๋Œ€์‹  /fr/about๋กœ ๋ฆฌ๋””๋ ‰์…˜๋ฉ๋‹ˆ๋‹ค.

      ์ด ๋™์ž‘์€ ์—ฌ๋Ÿฌ ๊ฐ€์ง€ ์ด์œ ๋กœ ์œ ์šฉํ•ฉ๋‹ˆ๋‹ค:

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

      ์•„๋ž˜๋Š” TypeScript๋กœ ๊ตฌํ˜„ํ•œ ๋กœ์ปฌ๋ผ์ด์ฆˆ๋œ Link ์ปดํฌ๋„ŒํŠธ์ž…๋‹ˆ๋‹ค:

      src/components/Link.tsx
      import { getLocalizedUrl } from "intlayer";
      import {
        forwardRef,
        type DetailedHTMLProps,
        type AnchorHTMLAttributes,
      } from "react";
      import { useLocale } from "react-intlayer";
      
      export interface LinkProps extends DetailedHTMLProps<
        AnchorHTMLAttributes<HTMLAnchorElement>,
        HTMLAnchorElement
      > {}
      
      /**
       * ์ฃผ์–ด์ง„ URL์ด ์™ธ๋ถ€ ๋งํฌ์ธ์ง€ ํ™•์ธํ•˜๋Š” ์œ ํ‹ธ๋ฆฌํ‹ฐ ํ•จ์ˆ˜์ž…๋‹ˆ๋‹ค.
       * URL์ด http:// ๋˜๋Š” https://๋กœ ์‹œ์ž‘ํ•˜๋ฉด ์™ธ๋ถ€ ๋งํฌ๋กœ ๊ฐ„์ฃผํ•ฉ๋‹ˆ๋‹ค.
       */
      export const checkIsExternalLink = (href?: string): boolean =>
        /^https?:\/\//.test(href ?? "");
      
      /**
       * ํ˜„์žฌ ๋กœ์ผ€์ผ์— ๋”ฐ๋ผ href ์†์„ฑ์„ ์กฐ์ •ํ•˜๋Š” ์ปค์Šคํ…€ Link ์ปดํฌ๋„ŒํŠธ์ž…๋‹ˆ๋‹ค.
       * ๋‚ด๋ถ€ ๋งํฌ์˜ ๊ฒฝ์šฐ `getLocalizedUrl`์„ ์‚ฌ์šฉํ•˜์—ฌ URL ์•ž์— ๋กœ์ผ€์ผ์„ ๋ถ™์ž…๋‹ˆ๋‹ค (์˜ˆ: /fr/about).
       * ์ด๋ฅผ ํ†ตํ•ด ๋‚ด๋น„๊ฒŒ์ด์…˜์ด ๋™์ผํ•œ ๋กœ์ผ€์ผ ์ปจํ…์ŠคํŠธ ๋‚ด์—์„œ ์ด๋ฃจ์–ด์ง€๋„๋ก ๋ณด์žฅํ•ฉ๋‹ˆ๋‹ค.
       */
      export const Link = forwardRef<HTMLAnchorElement, LinkProps>(
        ({ href, children, ...props }, ref) => {
          const { locale } = useLocale();
          const isExternalLink = checkIsExternalLink(href);
      
          // ๋งํฌ๊ฐ€ ๋‚ด๋ถ€ ๋งํฌ์ด๊ณ  ์œ ํšจํ•œ href๊ฐ€ ์ œ๊ณต๋œ ๊ฒฝ์šฐ, ๋กœ์ผ€์ผ์ด ์ ์šฉ๋œ URL์„ ๊ฐ€์ ธ์˜ต๋‹ˆ๋‹ค.
          const hrefI18n =
            href && !isExternalLink ? getLocalizedUrl(href, locale) : href;
      
          return (
            <a href={hrefI18n} ref={ref} {...props}>
              {children}
            </a>
          );
        }
      );
      
      Link.displayName = "Link";
      

      ์ž‘๋™ ๋ฐฉ์‹

      • ์™ธ๋ถ€ ๋งํฌ ๊ฐ์ง€:
        ํ—ฌํผ ํ•จ์ˆ˜ checkIsExternalLink๋Š” ์ง€์ •๋œ URL์ด ์™ธ๋ถ€ ๋งํฌ์ธ์ง€ ์—ฌ๋ถ€๋ฅผ ํŒ๋‹จํ•ฉ๋‹ˆ๋‹ค. ์™ธ๋ถ€ ๋งํฌ๋Š” ํ˜„์ง€ํ™”ํ•  ํ•„์š”๊ฐ€ ์—†์œผ๋ฏ€๋กœ ๋ณ€๊ฒฝ๋˜์ง€ ์•Š์€ ์ƒํƒœ๋กœ ์œ ์ง€๋ฉ๋‹ˆ๋‹ค.

      • ํ˜„์žฌ ๋กœ์ผ€์ผ ๊ฐ€์ ธ์˜ค๊ธฐ:
        useLocale ํ›…์€ ํ˜„์žฌ ๋กœ์ผ€์ผ(์˜ˆ: ํ”„๋ž‘์Šค์–ด์˜ ๊ฒฝ์šฐ fr)์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.

      • URL ํ˜„์ง€ํ™”:
        ๋‚ด๋ถ€ ๋งํฌ(์ฆ‰, ์™ธ๋ถ€ ๋งํฌ๊ฐ€ ์•„๋‹Œ ๋งํฌ)์˜ ๊ฒฝ์šฐ, getLocalizedUrl์„ ์‚ฌ์šฉํ•˜์—ฌ ํ˜„์žฌ ๋กœ์ผ€์ผ๋กœ URL ์ ‘๋‘์‚ฌ๋ฅผ ์ž๋™์œผ๋กœ ๋ถ™์ž…๋‹ˆ๋‹ค. ์ฆ‰, ์‚ฌ์šฉ์ž๊ฐ€ ํ”„๋ž‘์Šค์–ด ํ™˜๊ฒฝ์ธ ๊ฒฝ์šฐ /about์„ href๋กœ ์ „๋‹ฌํ•˜๋ฉด /fr/about์œผ๋กœ ๋ณ€ํ™˜๋ฉ๋‹ˆ๋‹ค.

      • ๋งํฌ ๋ฐ˜ํ™˜:
        ์ด ์ปดํฌ๋„ŒํŠธ๋Š” ๋กœ์ปฌ๋ผ์ด์ฆˆ๋œ URL์ด ์ ์šฉ๋œ <a> ์š”์†Œ๋ฅผ ๋ฐ˜ํ™˜ํ•˜์—ฌ ๋‚ด๋น„๊ฒŒ์ด์…˜์ด ๋กœ์ผ€์ผ๊ณผ ์ผ์น˜ํ•˜๋„๋ก ๋ณด์žฅํ•ฉ๋‹ˆ๋‹ค.

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

    11. ์ปดํฌ๋„ŒํŠธ ์ฝ˜ํ…์ธ  ์ถ”์ถœ

      ์„ ํƒ์‚ฌํ•ญ

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

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

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

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

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

      bash
      npx intlayer extract
      
      Since v9, the intlayerCompiler is included in the intlayer plugin. So you don't need to add it manually.

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

      vite.config.ts
      import { defineConfig } from "vite";
      import { intlayer, intlayerCompiler } from "vite-intlayer";
      
      export default defineConfig({
        plugins: [
          intlayer(),
          intlayerCompiler(), // Adds the compiler plugin
        ],
      });
      

      ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์„ ๋นŒ๋“œํ•˜์—ฌ ์ปดํฌ๋„ŒํŠธ๋ฅผ ๋ณ€ํ™˜ํ•˜๊ณ  ์ฝ˜ํ…์ธ ๋ฅผ ์ถ”์ถœํ•ฉ๋‹ˆ๋‹ค.

      bash
      npm run build # ๋˜๋Š” npm run dev
      

    (์„ ํƒ) ์‚ฌ์ดํŠธ๋งต๊ณผ robots.txt(๋นŒ๋“œ ์‹œ ์ƒ์„ฑ)

    Intlayer๋Š” generateSitemap๊ณผ getMultilingualUrls๋กœ ํฌ๋กค๋Ÿฌ์šฉ ๋‹ค๊ตญ์–ด sitemap.xml๊ณผ robots.txt๋ฅผ ๋งŒ๋“ค์–ด public/์— ์ž๋™์œผ๋กœ ์“ธ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๋ณดํ†ต Vite ์‹คํ–‰ ์ „์— ์ž‘์€ Node ์Šคํฌ๋ฆฝํŠธ๋ฅผ ๋Œ๋ฆฝ๋‹ˆ๋‹ค(์˜ˆ: npm predev / prebuild).

    ์‚ฌ์ดํŠธ๋งต

    Intlayer ์‚ฌ์ดํŠธ๋งต ์ƒ์„ฑ๊ธฐ๋Š” ๋กœ์ผ€์ผ ์„ค์ •์„ ๋ฐ˜์˜ํ•˜๊ณ  ํฌ๋กค๋Ÿฌ์šฉ ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ๋ฅผ ํฌํ•จํ•ฉ๋‹ˆ๋‹ค.

    ์ƒ์„ฑ๋œ ์‚ฌ์ดํŠธ๋งต์€ xhtml:link(hreflang)๋ฅผ ์ง€์›ํ•ฉ๋‹ˆ๋‹ค. ๋‹จ์ˆœ URL ๋‚˜์—ด์ด ์•„๋‹ˆ๋ผ ๊ฐ ํŽ˜์ด์ง€์˜ ๋ชจ๋“  ์–ธ์–ด ๋ฒ„์ „์„ ์–‘๋ฐฉํ–ฅ์œผ๋กœ ์—ฐ๊ฒฐํ•ฉ๋‹ˆ๋‹ค(์˜ˆ: /about, /fr/about, /about?lang=fr - ๋ผ์šฐํŒ… ๋ชจ๋“œ์— ๋”ฐ๋ฆ„).

    Robots.txt

    getMultilingualUrls๋กœ Disallow๊ฐ€ ๋ฏผ๊ฐ ๊ฒฝ๋กœ์˜ ๋ชจ๋“  ํ˜„์ง€ํ™” ๋ณ€ํ˜•์— ์ ์šฉ๋˜๋„๋ก ํ•˜์„ธ์š”.

    1. ํ”„๋กœ์ ํŠธ ๋ฃจํŠธ์— generate-seo.mjs ์ถ”๊ฐ€

    generate-seo.mjs
    import fs from "fs";
    import path from "path";
    import { fileURLToPath } from "url";
    import { generateSitemap, getMultilingualUrls } from "intlayer";
    
    const __dirname = path.dirname(fileURLToPath(import.meta.url));
    
    const SITE_URL = (process.env.SITE_URL || "http://localhost:5173").replace(
      /\/$/,
      ""
    );
    
    const pathList = [
      { path: "/", changefreq: "daily", priority: 1.0 },
      { path: "/about", changefreq: "monthly", priority: 0.7 },
    ];
    
    const sitemapXml = generateSitemap(pathList, {
      siteUrl: "https://example.com",
    });
    fs.writeFileSync(path.join(__dirname, "public", "sitemap.xml"), sitemapXml);
    
    const getAllMultilingualUrls = (urls) =>
      urls.flatMap((url) => Object.values(getMultilingualUrls(url)));
    
    const disallowedPaths = getAllMultilingualUrls(["/admin", "/private"]);
    
    const robotsTxt = [
      "User-agent: *",
      "Allow: /",
      ...disallowedPaths.map((path) => `Disallow: ${path}`),
      "",
      `Sitemap: https://example.com/sitemap.xml`,
    ].join("\n");
    
    fs.writeFileSync(path.join(__dirname, "public", "robots.txt"), robotsTxt);
    
    console.log("SEO files generated successfully.");
    

    ์Šคํฌ๋ฆฝํŠธ๊ฐ€ intlayer๋ฅผ importํ•˜๋ ค๋ฉด ํŒจํ‚ค์ง€๊ฐ€ ์„ค์น˜๋˜์–ด ์žˆ์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ํ”„๋กœ๋•์…˜์—์„œ๋Š” ํ™˜๊ฒฝ ๋ณ€์ˆ˜ SITE_URL์„ ์„ค์ •ํ•˜์„ธ์š”(CI ๋“ฑ).

    Node ESM์—๋Š” generate-seo.mjs ์‚ฌ์šฉ์„ ๊ถŒ์žฅํ•ฉ๋‹ˆ๋‹ค. generate-seo.js๋ฅผ ์“ฐ๋ฉด package.json์˜ "type": "module" ๋“ฑ์œผ๋กœ ESM์„ ์ผœ์„ธ์š”.

    2. Vite ์ „์— ์Šคํฌ๋ฆฝํŠธ ์‹คํ–‰

    package.json
    {
      "scripts": {
        "dev": "vite",
        "prebuild": "node generate-seo.mjs",
        "build": "vite build",
        "preview": "vite preview"
      }
    }
    

    pnpm์ด๋‚˜ yarn์„ ์“ฐ๋ฉด ๋ช…๋ น์„ ๋งž๊ฒŒ ๋ฐ”๊พธ์„ธ์š”. CI์—์„œ ํ˜ธ์ถœํ•ด๋„ ๋ฉ๋‹ˆ๋‹ค.

    TypeScript ์„ค์ •

    Intlayer๋Š” TypeScript์˜ ์ด์ ์„ ํ™œ์šฉํ•˜๊ณ  codebase๋ฅผ ๋”์šฑ ๊ฐ•๋ ฅํ•˜๊ฒŒ ๋งŒ๋“ค๊ธฐ ์œ„ํ•ด module augmentation์„ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.

    Autocompletion

    Translation error

    TypeScript ์„ค์ •์— ์ž๋™ ์ƒ์„ฑ๋œ ํƒ€์ž…์ด ํฌํ•จ๋˜์–ด ์žˆ๋Š”์ง€ ํ™•์ธํ•˜์„ธ์š”.

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

    Git ์„ค์ •

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

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

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

    VS Code ํ™•์žฅ ํ”„๋กœ๊ทธ๋žจ

    Intlayer์™€ ํ•จ๊ป˜ํ•˜๋Š” ๊ฐœ๋ฐœ ํ™˜๊ฒฝ์„ ๊ฐœ์„ ํ•˜๊ธฐ ์œ„ํ•ด ๊ณต์‹ Intlayer VS Code ํ™•์žฅ ํ”„๋กœ๊ทธ๋žจ์„ ์„ค์น˜ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

    VS Code ๋งˆ์ผ“ํ”Œ๋ ˆ์ด์Šค์—์„œ ์„ค์น˜ํ•˜๊ธฐ

    ์ด ํ™•์žฅ ํ”„๋กœ๊ทธ๋žจ์€ ๋‹ค์Œ์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค:

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

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

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

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

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

    Vite ์ž์ฒด๋Š” i18n์— ๋Œ€ํ•ด ํŠน์ • ๋ฐฉ์‹์„ ๊ฐ•์ œํ•˜์ง€ ์•Š์œผ๋ฏ€๋กœ React ์ƒํƒœ๊ณ„์˜ ์ผ๋ฐ˜์ ์ธ ์˜ต์…˜๋“ค์„ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค:

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

    ํŠนํžˆ Vite์—์„œ Intlayer๋Š” ๋นŒ๋“œ ํ”„๋กœ์„ธ์Šค์— ์ง์ ‘ ์—ฐ๊ฒฐ๋˜๋ฏ€๋กœ ๋ฒˆ์—ญ์ด ๋Ÿฐํƒ€์ž„์— JSON์œผ๋กœ ๊ฐ€์ ธ์˜ค๋Š” ๋Œ€์‹  ์ปดํŒŒ์ผ ํƒ€์ž„์— ํ•ด์„๋˜๊ณ  ํŠธ๋ฆฌ ์‰์ดํ‚น(tree-shaking)๋ฉ๋‹ˆ๋‹ค. ์™œ 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๋งŒ ๊ฒ€ํ† ํ•˜๋ฉด ๋ฉ๋‹ˆ๋‹ค. ์ด ๊ฐ€์ด๋“œ์˜ 11๋‹จ๊ณ„๋ฅผ ํ™•์ธํ•˜์„ธ์š”.

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