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

    Intlayer๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ Vite ๋ฐ Lit ์›น์‚ฌ์ดํŠธ ๋ฒˆ์—ญํ•˜๊ธฐ | ๊ตญ์ œํ™” (i18n)

    ide.intlayer.org
    intlayer-vite-lit-template.vercel.app

    ๋ชฉ์ฐจ

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

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

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

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

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

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

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

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

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

    GitHub์—์„œ Application Template์„ ์ฐธ๊ณ ํ•˜์„ธ์š”.

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

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

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

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

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

      • lit-intlayer Intlayer๋ฅผ Lit ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜๊ณผ ํ†ตํ•ฉํ•˜๋Š” ํŒจํ‚ค์ง€์ž…๋‹ˆ๋‹ค. ReactiveController ๊ธฐ๋ฐ˜ ํ›…(useIntlayer, useLocale ๋“ฑ)์„ ์ œ๊ณตํ•˜์—ฌ ๋กœ์ผ€์ผ์ด ๋ณ€๊ฒฝ๋  ๋•Œ LitElement๊ฐ€ ์ž๋™์œผ๋กœ ๋‹ค์‹œ ๋ Œ๋”๋ง๋˜๋„๋ก ํ•ฉ๋‹ˆ๋‹ค.

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

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

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

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

      Vite ๊ตฌ์„ฑ์— intlayer ํ”Œ๋Ÿฌ๊ทธ์ธ์„ ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค.

      vite.config.ts
      import { defineConfig } from "vite";
      import { intlayer } from "vite-intlayer";
      
      // https://vitejs.dev/config/
      export default defineConfig({
        plugins: [
          intlayer({
            proxy: {
              ignore: (req) => req.url?.startsWith("/api"),
            },
          }),
        ],
      });
      
      intlayer() Vite ํ”Œ๋Ÿฌ๊ทธ์ธ์€ Intlayer๋ฅผ Vite์™€ ํ†ตํ•ฉํ•˜๋Š” ๋ฐ ์‚ฌ์šฉ๋ฉ๋‹ˆ๋‹ค. ์ฝ˜ํ…์ธ  ์„ ์–ธ ํŒŒ์ผ์˜ ๋นŒ๋“œ๋ฅผ ๋ณด์žฅํ•˜๊ณ  ๊ฐœ๋ฐœ ๋ชจ๋“œ์—์„œ ์ด๋ฅผ ๋ชจ๋‹ˆํ„ฐ๋งํ•ฉ๋‹ˆ๋‹ค. Vite ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ๋‚ด์— Intlayer ํ™˜๊ฒฝ ๋ณ€์ˆ˜๋ฅผ ์ •์˜ํ•ฉ๋‹ˆ๋‹ค. ๋˜ํ•œ ์„ฑ๋Šฅ ์ตœ์ ํ™”๋ฅผ ์œ„ํ•œ ๋ณ„์นญ์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.
    4. ์—”ํŠธ๋ฆฌ ํฌ์ธํŠธ์—์„œ Intlayer ๋ถ€ํŠธ์ŠคํŠธ๋žฉ

      ์ฒซ ๋ฒˆ์งธ ์—˜๋ฆฌ๋จผํŠธ๊ฐ€ ์—ฐ๊ฒฐ๋  ๋•Œ ๊ธ€๋กœ๋ฒŒ ๋กœ์ผ€์ผ ์‹ฑ๊ธ€ํ†ค์ด ์ค€๋น„๋˜๋„๋ก ์ปค์Šคํ…€ ์—˜๋ฆฌ๋จผํŠธ๊ฐ€ ๋“ฑ๋ก๋˜๊ธฐ ์ „์— installIntlayer()๋ฅผ ํ˜ธ์ถœํ•˜์„ธ์š”.

      src/main.ts
      import { installIntlayer } from "lit-intlayer";
      
      // LitElement๊ฐ€ DOM์— ์—ฐ๊ฒฐ๋˜๊ธฐ ์ „์— ํ˜ธ์ถœํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.
      installIntlayer();
      
      // ์ปค์Šคํ…€ ์—˜๋ฆฌ๋จผํŠธ๋ฅผ ์ž„ํฌํŠธํ•˜๊ณ  ๋“ฑ๋กํ•ฉ๋‹ˆ๋‹ค.
      import "./my-element.js";
      

      md() ์ฝ˜ํ…์ธ  ์„ ์–ธ(Markdown)๋„ ์‚ฌ์šฉํ•˜๋Š” ๊ฒฝ์šฐ ๋งˆํฌ๋‹ค์šด ๋ Œ๋”๋Ÿฌ๋„ ์„ค์น˜ํ•˜์„ธ์š”:

      src/main.ts
      import { installIntlayer, installIntlayerMarkdown } from "lit-intlayer";
      
      installIntlayer();
      installIntlayerMarkdown();
      
      import "./my-element.js";
      
    5. ์ฝ˜ํ…์ธ  ์„ ์–ธ

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

      src/app.content.ts
      import { t, type Dictionary } from "intlayer";
      
      const appContent = {
        key: "app",
        content: {
          title: "Vite + Lit",
      
          viteLogo: t({
            en: "Vite logo",
            fr: "Logo Vite",
            es: "Logo Vite",
          }),
          litLogo: t({
            en: "Lit logo",
            fr: "Logo Lit",
            es: "Logo Lit",
          }),
      
          count: t({
            en: "count is {{count}}",
            fr: "le compte est {{count}}",
            es: "el recuento es {{count}}",
          }),
      
          readTheDocs: t({
            en: "Click on the Vite and Lit logos to learn more",
            fr: "Cliquez sur les logos Vite et Lit pour en savoir plus",
            es: "Haga clic en los logotipos de Vite y Lit 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})์™€ ์ผ์น˜ํ•˜๋Š” ๊ฒฝ์šฐ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์–ด๋””์—์„œ๋‚˜ ์ •์˜ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

      ์ž์„ธํ•œ ๋‚ด์šฉ์€ ์ฝ˜ํ…์ธ  ์„ ์–ธ ๋ฌธ์„œ๋ฅผ ์ฐธ์กฐํ•˜์„ธ์š”.

    6. LitElement์—์„œ Intlayer ํ™œ์šฉ

      LitElement ๋‚ด์—์„œ useIntlayer๋ฅผ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค. ํ™œ์„ฑ ๋กœ์ผ€์ผ์ด ๋ณ€๊ฒฝ๋  ๋•Œ๋งˆ๋‹ค ์ž๋™์œผ๋กœ ๋‹ค์‹œ ๋ Œ๋”๋ง์„ ํŠธ๋ฆฌ๊ฑฐํ•˜๋Š” ReactiveController ํ”„๋ก์‹œ๋ฅผ ๋ฐ˜ํ™˜ํ•˜๋ฉฐ ๋ณ„๋„์˜ ์„ค์ •์ด ํ•„์š”ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

      src/my-element.ts
      import { LitElement, html } from "lit";
      import { customElement, property } from "lit/decorators.js";
      import { useIntlayer } from "lit-intlayer";
      
      @customElement("my-element")
      export class MyElement extends LitElement {
        @property({ type: Number })
        count = 0;
      
        // useIntlayer๋Š” ์ž์‹ ์„ ReactiveController๋กœ ๋“ฑ๋กํ•ฉ๋‹ˆ๋‹ค.
        // ๋กœ์ผ€์ผ์ด ๋ณ€๊ฒฝ๋˜๋ฉด ์—˜๋ฆฌ๋จผํŠธ๊ฐ€ ์ž๋™์œผ๋กœ ๋‹ค์‹œ ๋ Œ๋”๋ง๋ฉ๋‹ˆ๋‹ค.
        private content = useIntlayer(this, "app");
      
        override render() {
          const { content } = this;
      
          return html`
            <h1>${content.title}</h1>
      
            <img src="/vite.svg" alt=${content.viteLogo.value} />
            <img src="/lit.svg" alt=${content.litLogo.value} />
      
            <button @click=${() => this.count++}>
              ${content.count({ count: this.count })}
            </button>
      
            <p>${content.readTheDocs}</p>
          `;
        }
      }
      

      ๋„ค์ดํ‹ฐ๋ธŒ HTML ์†์„ฑ(์˜ˆ: alt, aria-label, title)์—์„œ ๋ฒˆ์—ญ๋œ ๋ฌธ์ž์—ด์ด ํ•„์š”ํ•œ ๊ฒฝ์šฐ ๋ฆฌํ”„ ๋…ธ๋“œ์—์„œ .value๋ฅผ ํ˜ธ์ถœํ•˜์„ธ์š”:

      typescript
      html`<img alt=${content.viteLogo.value} />`;
      html`<img alt=${content.viteLogo.toString()} />`;
      html`<img alt=${String(content.viteLogo)} />`;
      
    7. ์ฝ˜ํ…์ธ  ์–ธ์–ด ๋ณ€๊ฒฝ

      ์„ ํƒ์‚ฌํ•ญ

      ์ฝ˜ํ…์ธ  ์–ธ์–ด๋ฅผ ๋ณ€๊ฒฝํ•˜๋ ค๋ฉด useLocale ์ปจํŠธ๋กค๋Ÿฌ์—์„œ ๋…ธ์ถœํ•˜๋Š” setLocale ๋ฉ”์„œ๋“œ๋ฅผ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.

      src/locale-switcher.ts
      import { LitElement, html } from "lit";
      import { customElement } from "lit/decorators.js";
      import { getLocaleName } from "intlayer";
      import { useLocale } from "lit-intlayer";
      
      @customElement("locale-switcher")
      export class LocaleSwitcher extends LitElement {
        private locale = useLocale(this);
      
        private _onChange(e: Event) {
          const select = e.target as HTMLSelectElement;
          this.locale.setLocale(select.value as any);
        }
      
        override render() {
          return html`
            <select @change=${this._onChange}>
              ${this.locale.availableLocales.map(
                (loc) => html`
                  <option value=${loc} ?selected=${loc === this.locale.locale}>
                    ${getLocaleName(loc)}
                  </option>
                `
              )}
            </select>
          `;
        }
      }
      
    8. ๋งˆํฌ๋‹ค์šด ๋ฐ HTML ์ฝ˜ํ…์ธ  ๋ Œ๋”๋ง

      ์„ ํƒ์‚ฌํ•ญ

      Intlayer๋Š” md() ๋ฐ html() ์ฝ˜ํ…์ธ  ์„ ์–ธ์„ ์ง€์›ํ•ฉ๋‹ˆ๋‹ค. Lit์—์„œ ์ปดํŒŒ์ผ๋œ ์ถœ๋ ฅ์€ unsafeHTML ๋””๋ ‰ํ‹ฐ๋ธŒ๋ฅผ ํ†ตํ•ด ์›์‹œ HTML๋กœ ์‚ฝ์ž…๋ฉ๋‹ˆ๋‹ค.

      ์—˜๋ฆฌ๋จผํŠธ์—์„œ ์ปดํŒŒ์ผ๋œ HTML์„ ๋ Œ๋”๋งํ•ฉ๋‹ˆ๋‹ค:

      src/my-element.ts
      import { LitElement, html } from "lit";
      import { customElement } from "lit/decorators.js";
      import { unsafeHTML } from "lit/directives/unsafe-html.js";
      import { useIntlayer } from "lit-intlayer";
      import { compileMarkdown } from "lit-intlayer/markdown";
      
      @customElement("my-element")
      export class MyElement extends LitElement {
        private content = useIntlayer(this, "app");
      
        override render() {
          return html`
            <div class="edit-note">
              ${unsafeHTML(compileMarkdown(String(this.content.editNote)))}
            </div>
          `;
        }
      }
      
      TIP
      String(content.editNote)๋Š” ์›์‹œ ๋งˆํฌ๋‹ค์šด ๋ฌธ์ž์—ด์„ ๋ฐ˜ํ™˜ํ•˜๋Š” IntlayerNode์˜ toString()์„ ํ˜ธ์ถœํ•ฉ๋‹ˆ๋‹ค. ์ด๋ฅผ compileMarkdown์— ์ „๋‹ฌํ•˜์—ฌ HTML ๋ฌธ์ž์—ด์„ ๊ฐ€์ ธ์˜จ ๋‹ค์Œ Lit์˜ unsafeHTML ๋””๋ ‰ํ‹ฐ๋ธŒ๋กœ ๋ Œ๋”๋งํ•ฉ๋‹ˆ๋‹ค.
    9. ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์— ๋กœ์ปฌ๋ผ์ด์ฆˆ๋œ ๋ผ์šฐํŒ… ์ถ”๊ฐ€

      ์„ ํƒ์‚ฌํ•ญ

      ๊ฐ ์–ธ์–ด์— ๋Œ€ํ•ด ๊ณ ์œ ํ•œ ๋ผ์šฐํŠธ๋ฅผ ๋งŒ๋“ค๋ ค๋ฉด(SEO์— ์œ ์šฉ) Intlayer์˜ localeMap / localeFlatMap ํ—ฌํผ์™€ ์„œ๋ฒ„ ์ธก ๋กœ์ผ€์ผ ๊ฐ์ง€๋ฅผ ์œ„ํ•œ intlayerProxy Vite ํ”Œ๋Ÿฌ๊ทธ์ธ๊ณผ ํ•จ๊ป˜ ํด๋ผ์ด์–ธํŠธ ์ธก ๋ผ์šฐํ„ฐ๋ฅผ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

      ๋จผ์ € Vite ๊ตฌ์„ฑ์— intlayerProxy๋ฅผ ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค:

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

      ์„ ํƒ์‚ฌํ•ญ

      ๋กœ์ผ€์ผ์ด ๋ณ€๊ฒฝ๋  ๋•Œ ๋ธŒ๋ผ์šฐ์ € URL์„ ์—…๋ฐ์ดํŠธํ•˜๋ ค๋ฉด ๋กœ์ผ€์ผ ์Šค์œ„์ฒ˜์™€ ํ•จ๊ป˜ useRewriteURL์„ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค:

      src/locale-switcher.ts
      import { LitElement, html } from "lit";
      import { customElement } from "lit/decorators.js";
      import { getLocaleName, getLocalizedUrl } from "intlayer";
      import { useLocale, useRewriteURL } from "lit-intlayer";
      
      @customElement("locale-switcher")
      export class LocaleSwitcher extends LitElement {
        private locale = useLocale(this);
      
        // ๋กœ์ผ€์ผ์ด ๋ณ€๊ฒฝ๋  ๋•Œ ํ˜„์žฌ URL์„ ์ž๋™์œผ๋กœ ๋‹ค์‹œ ์ž‘์„ฑํ•ฉ๋‹ˆ๋‹ค.
        private _rewriteURL = useRewriteURL(this);
      
        private _onChange(e: Event) {
          const select = e.target as HTMLSelectElement;
          this.locale.setLocale(select.value as any);
        }
      
        override render() {
          return html`
            <select @change=${this._onChange}>
              ${this.locale.availableLocales.map(
                (loc) => html`
                  <option value=${loc} ?selected=${loc === this.locale.locale}>
                    ${getLocaleName(loc)}
                  </option>
                `
              )}
            </select>
          `;
        }
      }
      
    11. HTML ์–ธ์–ด ๋ฐ ๋ฐฉํ–ฅ ์†์„ฑ ์ „ํ™˜

      ์„ ํƒ์‚ฌํ•ญ

      ์ ‘๊ทผ์„ฑ ๋ฐ SEO๋ฅผ ์œ„ํ•ด ํ˜„์žฌ ๋กœ์ผ€์ผ์— ๋งž๊ฒŒ <html> ํƒœ๊ทธ์˜ lang ๋ฐ dir ์†์„ฑ์„ ์—…๋ฐ์ดํŠธํ•ฉ๋‹ˆ๋‹ค.

      src/my-element.ts
      import { LitElement, html } from "lit";
      import { customElement } from "lit/decorators.js";
      import { getHTMLTextDir } from "intlayer";
      import { useLocale } from "lit-intlayer";
      
      @customElement("my-element")
      export class MyElement extends LitElement {
        private locale = useLocale(this, {
          onLocaleChange: (loc) => {
            document.documentElement.lang = loc;
            document.documentElement.dir = getHTMLTextDir(loc);
          },
        });
      
        override render() {
          return html`<!-- ์—ฌ๋Ÿฌ๋ถ„์˜ ์ฝ˜ํ…์ธ  -->`;
        }
      }
      
    12. ์ปดํฌ๋„ŒํŠธ์˜ ์ฝ˜ํ…์ธ  ์ถ”์ถœ

      ์„ ํƒ์‚ฌํ•ญ

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

      ์ด ํ”„๋กœ์„ธ์Šค๋ฅผ ์‰ฝ๊ฒŒ ํ•˜๊ธฐ ์œ„ํ•ด 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;
      

    (์„ ํƒ) ์‚ฌ์ดํŠธ๋งต๊ณผ 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: SITE_URL });
    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: ${SITE_URL}/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 ๊ตฌ์„ฑ

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

    tsconfig.json
    {
      "compilerOptions": {
        // ...
        "experimentalDecorators": true,
        "useDefineForClassFields": false,
      },
      "include": ["src", ".intlayer/**/*.ts"],
    }
    
    experimentalDecorators ๋ฐ useDefineForClassFields: false๋Š” Lit์—์„œ ๋ฐ์ฝ”๋ ˆ์ดํ„ฐ ์ง€์›์„ ์œ„ํ•ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.

    Git ๊ตฌ์„ฑ

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

    ์ด๋ ‡๊ฒŒ ํ•˜๋ ค๋ฉด .gitignore ํŒŒ์ผ์— ๋‹ค์Œ ์ง€์นจ์„ ์ถ”๊ฐ€ํ•˜๋ฉด ๋ฉ๋‹ˆ๋‹ค:

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

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

    Intlayer ๊ฐœ๋ฐœ ํ™˜๊ฒฝ์„ ๊ฐœ์„ ํ•˜๊ธฐ ์œ„ํ•ด ๊ณต์‹ Intlayer VS Code Extension์„ ์„ค์น˜ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

    VS Code Marketplace์—์„œ ์„ค์น˜

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

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

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

    ๋” ์•Œ์•„๋ณด๊ธฐ

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

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

    Vite๋Š” i18n์— ๋Œ€ํ•ด ํŠน์ • ๋ฐฉ์‹์„ ๊ฐ•์ œํ•˜์ง€ ์•Š์œผ๋ฏ€๋กœ Lit ์ƒํƒœ๊ณ„์—์„œ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์„ ํƒํ•ฉ๋‹ˆ๋‹ค:

    • @lit/localize: XLIFF ์ถ”์ถœ๊ณผ ๋กœ์ผ€์ผ๋‹น ํ•˜๋‚˜์˜ ์ปดํŒŒ์ผ๋œ ๋ฒˆ๋“ค์„ ๊ธฐ๋ฐ˜์œผ๋กœ ํ•˜๋Š” ๊ณต์‹ ์˜ต์…˜์ด๋ฏ€๋กœ, ๋Ÿฐํƒ€์ž„์— ์–ธ์–ด๋ฅผ ์ „ํ™˜ํ•˜๋ ค๋ฉด ๋Ÿฐํƒ€์ž„ ๋ชจ๋“œ๊ฐ€ ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.
    • ์ˆ˜๊ธฐ ์ž‘์„ฑ ์‚ฌ์ „(Hand-written dictionary): ๊ฐ ์š”์†Œ๋กœ import๋˜๋Š” ๋ฐฉ์‹์ด์ง€๋งŒ ์˜์กด์„ฑ์ด ์—†๋Š” ๋Œ€์‹  ํƒ€์ž… ์•ˆ์ „์„ฑ๊ณผ ๋„๊ตฌ๊ฐ€ ๋ถ€์กฑํ•ฉ๋‹ˆ๋‹ค.
    • Intlayer: ๊ฐ€์žฅ ์ง„๋ณด๋œ ์†”๋ฃจ์…˜์ž…๋‹ˆ๋‹ค. ์ฝ”๋“œ๋ฒ ์ด์Šค ์–ด๋””์—๋‚˜ ์ฝ˜ํ…์ธ ๋ฅผ ์„ ์–ธํ•  ์ˆ˜ ์žˆ์œผ๋ฉฐ(๊ฐ ์ปดํฌ๋„ŒํŠธ ์˜† ๋˜๋Š” ์ค‘์•™ ์ง‘์ค‘์‹), ๋นŒ๋“œ ํƒ€์ž„์— Vite ํ”Œ๋Ÿฌ๊ทธ์ธ์— ์˜ํ•ด ์ปดํŒŒ์ผ๋˜๊ณ , ์™„์ „ํ•œ ํƒ€์ž… ์•ˆ์ „์„ฑ์„ ์ œ๊ณตํ•˜๋ฉฐ AI ๋ฒˆ์—ญ, ๋น„์ฃผ์–ผ ์—๋””ํ„ฐ ๋ฐ CMS๋ฅผ ํฌํ•จํ•ฉ๋‹ˆ๋‹ค.

    Vite ํŠนํ™”๋œ ์žฅ์ ์€ ๋ฒˆ์—ญ์ด ๋Ÿฐํƒ€์ž„์— JSON์œผ๋กœ ๊ฐ€์ ธ์˜ค๋Š” ๋Œ€์‹  ์ปดํŒŒ์ผ ํƒ€์ž„์— ํ™•์ธ๋˜๊ณ  ํŠธ๋ฆฌ ์‰์ดํ‚น(tree-shaking)๋˜๋ฏ€๋กœ ํŽ˜์ด์ง€๊ฐ€ ๋ Œ๋”๋งํ•˜๋Š” ํ•ญ๋ชฉ๋งŒ ์ •ํ™•ํžˆ ์ „๋‹ฌ๋œ๋‹ค๋Š” ์ ์ž…๋‹ˆ๋‹ค. ์™œ Intlayer์ธ๊ฐ€์™€ ๋ฒค์น˜๋งˆํฌ๋ฅผ ์ฐธ์กฐํ•˜์„ธ์š”.

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

    ๋Œ€๋ถ€๋ถ„ ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค. ์ฝ˜ํ…์ธ ๋ฅผ ์ด์ „ํ•˜๋ ค๋ฉด ํ˜ธํ™˜ ์–ด๋Œ‘ํ„ฐ(compat adapters) ๊ฐœ์š”๋ฅผ ๋”ฐ๋ฅด์„ธ์š”. ์ ์ง„์ ์œผ๋กœ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ํ•  ์ˆ˜๋„ ์žˆ์Šต๋‹ˆ๋‹ค: sync JSON ํ”Œ๋Ÿฌ๊ทธ์ธ์€ ๊ธฐ์กด JSON ์นดํƒˆ๋กœ๊ทธ๋ฅผ ๋‹จ์ผ ์ง„์‹ค ๊ณต๊ธ‰์›(source of truth)์œผ๋กœ ์œ ์ง€ํ•˜๋ฉด์„œ Intlayer ์‚ฌ์ „์„ ์ƒ์„ฑํ•˜๋ฏ€๋กœ ์ปดํฌ๋„ŒํŠธ๋ฅผ ํ•˜๋‚˜์”ฉ ์ด์ „ํ•˜๋Š” ๋™์•ˆ ๋‘ ๊ณ„์ธต์˜ ๋™๊ธฐํ™”๊ฐ€ ์œ ์ง€๋ฉ๋‹ˆ๋‹ค.

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