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

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

    ide.intlayer.org
    intlayer-astro-template.vercel.app

    ๋ชฉ์ฐจ

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

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

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

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

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

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

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

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

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

    Astro + Lit์—์„œ Intlayer ์„ค์ •์„ ์œ„ํ•œ ๋‹จ๊ณ„๋ณ„ ๊ฐ€์ด๋“œ

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

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

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

      bash
      npm install intlayer astro-intlayer lit lit-intlayer @astrojs/lit
      
      npx intlayer init
      
      alt, title, href, aria-label ๋“ฑ๊ณผ ๊ฐ™์€ ๋ฌธ์ž์—ด ์†์„ฑ์—์„œ ์ฝ˜ํ…์ธ ๋ฅผ ์‚ฌ์šฉํ•˜๋ ค๋ฉด ๋‹ค์Œ๊ณผ ๊ฐ™์ด ํ•จ์ˆ˜์˜ ๊ฐ’์„ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
      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)} />
      
      bash
      npm install intlayer astro-intlayer lit lit-intlayer @astrojs/lit
      
      • intlayer ์„ค์ • ๊ด€๋ฆฌ, ๋ฒˆ์—ญ, ์ฝ˜ํ…์ธ  ์„ ์–ธ, ํŠธ๋žœ์ŠคํŒŒ์ผ ๋ฐ CLI ๋ช…๋ น์–ด๋ฅผ ์œ„ํ•œ ๊ตญ์ œํ™” ๋„๊ตฌ๋ฅผ ์ œ๊ณตํ•˜๋Š” ํ•ต์‹ฌ ํŒจํ‚ค์ง€์ž…๋‹ˆ๋‹ค.

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

      • lit ๋น ๋ฅด๊ณ  ๊ฐ€๋ฒผ์šด ์›น ์ปดํฌ๋„ŒํŠธ๋ฅผ ๋นŒ๋“œํ•˜๊ธฐ ์œ„ํ•œ ํ•ต์‹ฌ Lit ํŒจํ‚ค์ง€์ž…๋‹ˆ๋‹ค.

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

      • @astrojs/lit Astro ํŽ˜์ด์ง€์—์„œ Lit ์ปค์Šคํ…€ ์—˜๋ฆฌ๋จผํŠธ๋ฅผ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•ด์ฃผ๋Š” ๊ณต์‹ Astro ํ†ตํ•ฉ ๋„๊ตฌ์ž…๋‹ˆ๋‹ค.

    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. Astro ์„ค์ •์— Intlayer ํ†ตํ•ฉ

      Astro ์„ค์ •์— intlayer ํ”Œ๋Ÿฌ๊ทธ์ธ๊ณผ Lit ํ†ตํ•ฉ์„ ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค.

      astro.config.ts
      // @ts-check
      
      import { intlayer } from "astro-intlayer";
      import lit from "@astrojs/lit";
      import { defineConfig } from "astro/config";
      
      // https://astro.build/config
      export default defineConfig({
        integrations: [intlayer(), lit()],
      });
      
      intlayer() ํ†ตํ•ฉ ํ”Œ๋Ÿฌ๊ทธ์ธ์€ Intlayer๋ฅผ Astro์™€ ํ†ตํ•ฉํ•˜๋Š” ๋ฐ ์‚ฌ์šฉ๋ฉ๋‹ˆ๋‹ค. ์ฝ˜ํ…์ธ  ์„ ์–ธ ํŒŒ์ผ์˜ ๋นŒ๋“œ๋ฅผ ๋ณด์žฅํ•˜๊ณ  ๊ฐœ๋ฐœ ๋ชจ๋“œ์—์„œ ์ด๋ฅผ ๊ฐ์‹œํ•ฉ๋‹ˆ๋‹ค. Astro ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ๋‚ด์—์„œ Intlayer ํ™˜๊ฒฝ ๋ณ€์ˆ˜๋ฅผ ์ •์˜ํ•˜๋ฉฐ, ์„ฑ๋Šฅ ์ตœ์ ํ™”๋ฅผ ์œ„ํ•œ ์—์ผ๋ฆฌ์–ด์Šค(alias)๋ฅผ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.
      lit() ํ†ตํ•ฉ์€ Astro ํŽ˜์ด์ง€ ๋‚ด์—์„œ Lit ์ปค์Šคํ…€ ์—˜๋ฆฌ๋จผํŠธ๋ฅผ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•ฉ๋‹ˆ๋‹ค.
    4. ์ฝ˜ํ…์ธ  ์„ ์–ธ

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

      src/components/lit/app.content.ts
      import { t, type Dictionary } from "intlayer";
      
      const litDemoContent = {
        key: "lit-demo",
        content: {
          greeting: t({
            en: "Hello World",
            fr: "Bonjour le monde",
            es: "Hola mundo",
            ko: "์•ˆ๋…•ํ•˜์„ธ์š”",
          }),
          description: t({
            en: "Welcome to my multilingual Astro + Lit site.",
            fr: "Bienvenue sur mon site Astro + Lit multilingue.",
            es: "Bienvenido a mi sitio Astro + Lit multilingรผe.",
            ko: "๋‹ค๊ตญ์–ด Astro + Lit ์‚ฌ์ดํŠธ์— ์˜ค์‹  ๊ฒƒ์„ ํ™˜์˜ํ•ฉ๋‹ˆ๋‹ค.",
          }),
        },
      } satisfies Dictionary;
      
      export default litDemoContent;
      
      ์ฝ˜ํ…์ธ  ์„ ์–ธ์€ contentDir(๊ธฐ๋ณธ๊ฐ’ ./src)์— ํฌํ•จ๋˜์–ด ์žˆ๊ณ  ์ฝ˜ํ…์ธ  ์„ ์–ธ ํŒŒ์ผ ํ™•์žฅ์ž(๊ธฐ๋ณธ๊ฐ’ .content.{json,ts,tsx,js,jsx,mjs,cjs,md,mdx,yaml,yml})์™€ ์ผ์น˜ํ•œ๋‹ค๋ฉด ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์–ด๋””์—์„œ๋‚˜ ์ •์˜ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
      ์ž์„ธํ•œ ๋‚ด์šฉ์€ ์ฝ˜ํ…์ธ  ์„ ์–ธ ๋ฌธ์„œ๋ฅผ ์ฐธ์กฐํ•˜์„ธ์š”.
    5. Astro์—์„œ ์ฝ˜ํ…์ธ  ์‚ฌ์šฉ

      intlayer์—์„œ ๋‚ด๋ณด๋‚ธ ํ•ต์‹ฌ ํ—ฌํผ๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ .astro ํŒŒ์ผ์—์„œ ์ง์ ‘ ์‚ฌ์ „์„ ์†Œ๋น„ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๋˜ํ•œ ๊ฐ ํŽ˜์ด์ง€์— hreflang ๋ฐ canonical ๋งํฌ์™€ ๊ฐ™์€ SEO ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ๋ฅผ ์ถ”๊ฐ€ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. Lit ์ปค์Šคํ…€ ์—˜๋ฆฌ๋จผํŠธ๋Š” ํด๋ผ์ด์–ธํŠธ ์‚ฌ์ด๋“œ <script>๋ฅผ ํ†ตํ•ด ์ž„ํฌํŠธ๋˜์–ด body์— ๋ฐฐ์น˜๋ฉ๋‹ˆ๋‹ค.

      src/pages/[...locale]/index.astro
      ---
      import {
        getIntlayer,
        getLocaleFromPath,
        getLocalizedUrl,
        getHTMLTextDir,
        getPrefix,
        localeMap,
        defaultLocale,
        type LocalesValues,
      } from "intlayer";
      
      export const getStaticPaths = () => {
        return localeMap(({ locale }) => ({
          params: { locale: getPrefix(locale).localePrefix },
        }));
      };
      
      const locale = getLocaleFromPath(Astro.url.pathname) as LocalesValues;
      const { greeting } = getIntlayer("lit-demo", locale);
      ---
      
      <!doctype html>
      <html lang={locale} dir={getHTMLTextDir(locale)}>
        <head>
          <meta charset="utf-8" />
          <meta name="viewport" content="width=device-width" />
          <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
          <title>{greeting}</title>
      
          <!-- Canonical ๋งํฌ -->
          <link
            rel="canonical"
            href={new URL(getLocalizedUrl(Astro.url.pathname, locale), Astro.site)}
          />
      
          <!-- Hreflang ๋งํฌ -->
          {
            localeMap(({ locale: mapLocale }) => (
              <link
                rel="alternate"
                hreflang={mapLocale}
                href={new URL(
                  getLocalizedUrl(Astro.url.pathname, mapLocale),
                  Astro.site
                )}
              />
            ))
          }
      
          <link
            rel="alternate"
            hreflang="x-default"
            href={new URL(
              getLocalizedUrl(Astro.url.pathname, defaultLocale),
              Astro.site
            )}
          />
        </head>
        <body>
          <!-- Lit ์ปค์Šคํ…€ ์—˜๋ฆฌ๋จผํŠธ - ์„œ๋ฒ„์—์„œ ๊ฐ์ง€๋œ ๋กœ์ผ€์ผ์„ ํ”„๋กœํผํ‹ฐ๋กœ ๋ฐ›์Šต๋‹ˆ๋‹ค -->
          <lit-demo locale={locale}></lit-demo>
        </body>
      </html>
      
      <script>
        import "../../components/lit/LitDemo";
      </script>
      
      string ์†์„ฑ(์˜ˆ: alt, title, href, aria-label ๋“ฑ)์—์„œ ์ฝ˜ํ…์ธ ๋ฅผ ์‚ฌ์šฉํ•˜๋ ค๋ฉด ํ•จ์ˆ˜์˜ ๊ฐ’์„ ๋‹ค์Œ๊ณผ ๊ฐ™์ด ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค:
      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)} />
      

      ๋ผ์šฐํŒ… ์„ค์ •์— ๊ด€ํ•œ ์ฐธ๊ณ  ์‚ฌํ•ญ: ์‚ฌ์šฉํ•˜๋Š” ๋””๋ ‰ํ† ๋ฆฌ ๊ตฌ์กฐ๋Š” intlayer.config.ts์˜ middleware.routing ์„ค์ •์— ๋”ฐ๋ผ ๋‹ฌ๋ผ์ง‘๋‹ˆ๋‹ค:

      • prefix-no-default (๊ธฐ๋ณธ๊ฐ’): ๋ฃจํŠธ(ํ”„๋ ˆํ”ฝ์Šค ์—†์Œ)์— ๊ธฐ๋ณธ ์–ธ์–ด๋ฅผ ์œ ์ง€ํ•˜๊ณ  ๋‚˜๋จธ์ง€์— ํ”„๋ ˆํ”ฝ์Šค๋ฅผ ๋ถ™์ž…๋‹ˆ๋‹ค. ๋ชจ๋“  ์ผ€์ด์Šค๋ฅผ ์ฒ˜๋ฆฌํ•˜๋ ค๋ฉด [...locale]์„ ์‚ฌ์šฉํ•˜์„ธ์š”.
      • prefix-all: ๋ชจ๋“  URL์— ์–ธ์–ด ํ”„๋ ˆํ”ฝ์Šค๊ฐ€ ๋ถ™์Šต๋‹ˆ๋‹ค. ๋ฃจํŠธ๋ฅผ ๋ถ„๋ฆฌํ•ด์„œ ์ฒ˜๋ฆฌํ•  ํ•„์š”๊ฐ€ ์—†๋‹ค๋ฉด ํ‘œ์ค€ [locale]์„ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
      • search-param ๋˜๋Š” no-prefix: ๋กœ์ผ€์ผ ํด๋”๊ฐ€ ํ•„์š” ์—†์Šต๋‹ˆ๋‹ค. ๋กœ์ผ€์ผ์€ ๊ฒ€์ƒ‰ ํŒŒ๋ผ๋ฏธํ„ฐ๋‚˜ ์ฟ ํ‚ค๋ฅผ ํ†ตํ•ด ์ฒ˜๋ฆฌ๋ฉ๋‹ˆ๋‹ค.
    6. Lit ์ปค์Šคํ…€ ์—˜๋ฆฌ๋จผํŠธ ์ƒ์„ฑ

      Lit ์ปค์Šคํ…€ ์—˜๋ฆฌ๋จผํŠธ๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. ํด๋ผ์ด์–ธํŠธ์—์„œ Intlayer ์‹ฑ๊ธ€ํ†ค์„ ์ดˆ๊ธฐํ™”ํ•˜๊ธฐ ์œ„ํ•ด ์„œ๋ฒ„์—์„œ ๊ฐ์ง€๋œ locale ํ”„๋กœํผํ‹ฐ๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ connectedCallback์—์„œ installIntlayer๋ฅผ ํ˜ธ์ถœํ•ฉ๋‹ˆ๋‹ค.

      src/components/lit/LitDemo.ts
      import { LitElement, html } from "lit";
      import { installIntlayer, useIntlayer, useLocale } from "lit-intlayer";
      import { getLocalizedUrl, getLocaleName, type LocalesValues } from "intlayer";
      
      class LitDemo extends LitElement {
        static properties = {
          locale: { type: String },
        };
      
        locale: LocalesValues = "en" as LocalesValues;
      
        private _content = useIntlayer(this, "lit-demo");
        private _localeCtrl = useLocale(this, {
          onLocaleChange: (newLocale: LocalesValues) => {
            window.location.href = getLocalizedUrl(
              window.location.pathname,
              newLocale
            );
          },
        });
      
        override connectedCallback() {
          super.connectedCallback();
          // ์„œ๋ฒ„์—์„œ ๊ฐ์ง€๋œ ๋กœ์ผ€์ผ๋กœ ์ดˆ๊ธฐํ™”
          installIntlayer({ locale: this.locale as any });
        }
      
        override render() {
          const { greeting, description } = this._content;
          const {
            locale: currentLocale,
            availableLocales,
            setLocale,
          } = this._localeCtrl;
      
          return html`
            <div>
              <h1>${greeting}</h1>
              <p>${description}</p>
              <!-- ์–ธ์–ด ์ „ํ™˜๊ธฐ๋Š” LitElement ๋‚ด์— ๋ Œ๋”๋ง๋ฉ๋‹ˆ๋‹ค -->
              <div class="locale-switcher">
                <span class="switcher-label">์–ธ์–ด ์ „ํ™˜:</span>
                <div class="locale-buttons">
                  ${availableLocales.map(
                    (localeItem) => html`
                      <button
                        class="locale-btn ${
                          localeItem === currentLocale ? "active" : ""
                        }"
                        ?disabled=${localeItem === currentLocale}
                        @click=${() => setLocale(localeItem)}
                      >
                        <span class="ls-own-name">${getLocaleName(localeItem)}</span>
                        <span class="ls-current-name"
                          >${getLocaleName(localeItem, currentLocale)}</span
                        >
                        <span class="ls-code">${localeItem.toUpperCase()}</span>
                      </button>
                    `
                  )}
                </div>
              </div>
            </div>
          `;
        }
      }
      
      customElements.define("lit-demo", LitDemo);
      
      locale ํ”„๋กญ์€ Astro ํŽ˜์ด์ง€(์„œ๋ฒ„ ๊ฐ์ง€)์—์„œ ์ „๋‹ฌ๋˜์–ด connectedCallback์—์„œ installIntlayer๋ฅผ ์ดˆ๊ธฐํ™”ํ•˜๋Š” ๋ฐ ์‚ฌ์šฉ๋˜๋ฉฐ, ์ด๋กœ ์ธํ•ด ์—˜๋ฆฌ๋จผํŠธ ๋‚ด ๋ชจ๋“  ReactiveController ํ›…์˜ ์ดˆ๊ธฐ ๋กœ์ผ€์ผ์ด ๊ฒฐ์ •๋ฉ๋‹ˆ๋‹ค.
      useIntlayer๋Š” ReactiveController๋กœ ๋“ฑ๋ก๋ฉ๋‹ˆ๋‹ค. ์–ธ์–ด๊ฐ€ ๋ณ€๊ฒฝ๋˜๋ฉด ์—˜๋ฆฌ๋จผํŠธ๊ฐ€ ์ž๋™์œผ๋กœ ๋‹ค์‹œ ๋ Œ๋”๋ง๋˜๋ฏ€๋กœ ์ถ”๊ฐ€ ์„ค์ •์ด ํ•„์š” ์—†์Šต๋‹ˆ๋‹ค.
    7. ์–ธ์–ด ์ „ํ™˜๊ธฐ ์ถ”๊ฐ€

      ์–ธ์–ด ์ „ํ™˜ ๊ธฐ๋Šฅ์€ Lit ์ปค์Šคํ…€ ์—˜๋ฆฌ๋จผํŠธ์˜ render() ๋ฉ”์„œ๋“œ(6๋‹จ๊ณ„ ์ฐธ์กฐ)์— ์ง์ ‘ ํ†ตํ•ฉ๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค. lit-intlayer์˜ useLocale์„ ์‚ฌ์šฉํ•˜๋ฉฐ, ์‚ฌ์šฉ์ž๊ฐ€ ์ƒˆ ์–ธ์–ด๋ฅผ ์„ ํƒํ•  ๋•Œ ๋กœ์ปฌ๋ผ์ด์ฆˆ๋œ URL๋กœ ์ด๋™ํ•ฉ๋‹ˆ๋‹ค:

      src/components/lit/LitDemo.ts
      // LitElement ํด๋ž˜์Šค ๋‚ด์—์„œ useLocale ์„ค์ •(6๋‹จ๊ณ„ ํ•ญ๋ชฉ) ์ดํ›„:
      
      private _localeCtrl = useLocale(this, {
        onLocaleChange: (newLocale: LocalesValues) => {
          // ์–ธ์–ด ๋ณ€๊ฒฝ ์‹œ ๋กœ์ปฌ๋ผ์ด์ฆˆ๋œ URL๋กœ ์ด๋™
          window.location.href = getLocalizedUrl(window.location.pathname, newLocale);
        },
      });
      
      override render() {
        const { locale: currentLocale, availableLocales, setLocale } = this._localeCtrl;
      
        return html`
          <div class="locale-switcher">
            <span class="switcher-label">์–ธ์–ด ์ „ํ™˜:</span>
            <div class="locale-buttons">
              ${availableLocales.map(
                (localeItem) => html`
                  <button
                    class="locale-btn ${localeItem === currentLocale ? "active" : ""}"
                    ?disabled=${localeItem === currentLocale}
                    @click=${() => setLocale(localeItem)}
                  >
                    <span class="ls-own-name">${getLocaleName(localeItem)}</span>
                    <span class="ls-current-name">${getLocaleName(localeItem, currentLocale)}</span>
                    <span class="ls-code">${localeItem.toUpperCase()}</span>
                  </button>
                `
              )}
            </div>
          </div>
        `;
      }
      

      Lit ๋ฐ˜์‘์„ฑ์— ๊ด€ํ•œ ์ฐธ๊ณ  ์‚ฌํ•ญ: useLocale์€ ReactiveController๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. setLocale์ด ํ˜ธ์ถœ๋˜๋ฉด ์ปจํŠธ๋กค๋Ÿฌ๊ฐ€ ์ž๋™์œผ๋กœ ๋‹ค์‹œ ๋ Œ๋”๋ง์„ ์˜ˆ์•ฝํ•˜๋ฏ€๋กœ ๋ณ„๋„์˜ DOM ์กฐ์ž‘ ์—†์ด๋„ ํ™œ์„ฑ ๋ฒ„ํŠผ ์ƒํƒœ๊ฐ€ ์—…๋ฐ์ดํŠธ๋ฉ๋‹ˆ๋‹ค.

      ์˜์†์„ฑ์— ๊ด€ํ•œ ์ฐธ๊ณ  ์‚ฌํ•ญ: window.location.href๋ฅผ ํ†ตํ•œ ๋ฆฌ๋””๋ ‰์…˜์„ ์œ„ํ•ด onLocaleChange๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ์ƒˆ ์–ธ์–ด URL์ด ํ™•์‹คํžˆ ๋ฐฉ๋ฌธ๋˜๋„๋ก ๋ณด์žฅํ•ฉ๋‹ˆ๋‹ค. ์ด๋ฅผ ํ†ตํ•ด Intlayer ๋ฏธ๋“ค์›จ์–ด๊ฐ€ ์–ธ์–ด ์ฟ ํ‚ค๋ฅผ ์„ค์ •ํ•˜๊ณ  ํ–ฅํ›„ ๋ฐฉ๋ฌธ ์‹œ ์‚ฌ์šฉ์ž์˜ ์„ ํ˜ธ๋„๋ฅผ ๊ธฐ์–ตํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

    8. Sitemap ๋ฐ Robots.txt

      Intlayer๋Š” ๋™์ ์œผ๋กœ ๋กœ์ปฌ๋ผ์ด์ฆˆ๋œ ์‚ฌ์ดํŠธ๋งต๊ณผ robots.txt ํŒŒ์ผ์„ ์ƒ์„ฑํ•˜๊ธฐ ์œ„ํ•œ ์œ ํ‹ธ๋ฆฌํ‹ฐ๋ฅผ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.

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

      Intlayer๋Š” ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์˜ ์‚ฌ์ดํŠธ๋งต์„ ์‰ฝ๊ฒŒ ๋งŒ๋“ค ์ˆ˜ ์žˆ๋Š” ๋‚ด์žฅ ์‚ฌ์ดํŠธ๋งต ์ƒ์„ฑ๊ธฐ๋ฅผ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. ๋กœ์ปฌ๋ผ์ด์ฆˆ๋œ ๊ฒฝ๋กœ๋ฅผ ์ฒ˜๋ฆฌํ•˜๊ณ  ๊ฒ€์ƒ‰ ์—”์ง„์— ํ•„์š”ํ•œ ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ๋ฅผ ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค.

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

      ๋ชจ๋“  ๋กœ์ปฌ๋ผ์ด์ฆˆ๋œ ๊ฒฝ๋กœ๋ฅผ ํฌํ•จํ•˜๋Š” ์‚ฌ์ดํŠธ๋งต์„ ์ƒ์„ฑํ•˜๊ธฐ ์œ„ํ•ด src/pages/sitemap.xml.ts๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค.

      src/pages/sitemap.xml.ts
      import type { APIRoute } from "astro";
      import { generateSitemap, type SitemapUrlEntry } from "intlayer";
      
      const pathList: SitemapUrlEntry[] = [
        { path: "/", changefreq: "daily", priority: 1.0 },
        { path: "/about", changefreq: "monthly", priority: 0.7 },
      ];
      
      const SITE_URL = import.meta.env.SITE ?? "http://localhost:4321";
      
      export const GET: APIRoute = async ({ site }) => {
        const xmlOutput = generateSitemap(pathList, { siteUrl: SITE_URL });
      
        return new Response(xmlOutput, {
          headers: { "Content-Type": "application/xml" },
        });
      };
      

      Robots.txt

      ๊ฒ€์ƒ‰ ์—”์ง„ ํฌ๋กค๋ง์„ ์ œ์–ดํ•˜๊ธฐ ์œ„ํ•ด src/pages/robots.txt.ts๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค.

      src/pages/robots.txt.ts
      import type { APIRoute } from "astro";
      import { getMultilingualUrls } from "intlayer";
      
      const getAllMultilingualUrls = (urls: string[]) =>
        urls.flatMap((url) => Object.values(getMultilingualUrls(url)) as string[]);
      
      const disallowedPaths = getAllMultilingualUrls(["/admin", "/private"]);
      
      export const GET: APIRoute = ({ site }) => {
        const robotsTxt = [
          "User-agent: *",
          "Allow: /",
          ...disallowedPaths.map((path) => `Disallow: ${path}`),
          "",
          `Sitemap: ${new URL("/sitemap.xml", site).href}`,
        ].join("\n");
      
        return new Response(robotsTxt, {
          headers: { "Content-Type": "text/plain" },
        });
      };
      
    9. ์ปดํฌ๋„ŒํŠธ์—์„œ ์ฝ˜ํ…์ธ  ์ถ”์ถœ

      ์„ ํƒ์‚ฌํ•ญ

      ๊ธฐ์กด 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 plugin์— ํฌํ•จ๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค. ๋”ฐ๋ผ์„œ ์ˆ˜๋™์œผ๋กœ ์ถ”๊ฐ€ํ•  ํ•„์š”๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.

      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
      

    TypeScript ์„ค์ •

    Intlayer๋Š” ๋ชจ๋“ˆ ์ฆ๊ฐ•(module augmentation)์„ ์‚ฌ์šฉํ•˜์—ฌ TypeScript์˜ ์ด์ ์„ ํ™œ์šฉํ•จ์œผ๋กœ์จ ์ฝ”๋“œ๋ฒ ์ด์Šค๋ฅผ ๋” ๊ฒฌ๊ณ ํ•˜๊ฒŒ ๋งŒ๋“ญ๋‹ˆ๋‹ค. ๋ฐ์ฝ”๋ ˆ์ดํ„ฐ ๊ตฌ๋ฌธ์„ ์‚ฌ์šฉํ•˜๋Š” ๊ฒฝ์šฐ Lit์—์„œ experimentalDecorators๋ฅผ ํ™œ์„ฑํ™”ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

    ์ž๋™ ์™„์„ฑ

    ๋ฒˆ์—ญ ์˜ค๋ฅ˜

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

    tsconfig.json
    {
      compilerOptions: {
        // ...
        experimentalDecorators: true,
        useDefineForClassFields: false, // ๋ฐ์ฝ”๋ ˆ์ดํ„ฐ ์ง€์›์„ ์œ„ํ•ด Lit์—์„œ ํ•„์ˆ˜
      },
      include: [
        // ... ๊ธฐ์กด TypeScript ์„ค์ •
        ".intlayer/**/*.ts", // ์ž๋™ ์ƒ์„ฑ๋œ ํƒ€์ž… ํฌํ•จ
      ],
    }
    

    Git ์„ค์ •

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

    ๋ฌด์‹œํ•˜๋ ค๋ฉด .gitignore ํŒŒ์ผ์— ๋‹ค์Œ ์ง€์นจ์„ ์ถ”๊ฐ€ํ•˜์„ธ์š”:

    bash
    # Intlayer๊ฐ€ ์ƒ์„ฑํ•œ ํŒŒ์ผ ๋ฌด์‹œ
    .intlayer
    

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

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

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

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

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

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

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

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

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

    Astro์˜ ๋‚ด์žฅ i18n ์˜ต์…˜์€ ๋กœ์ผ€์ผ ์ ‘๋‘์‚ฌ์™€ ๋ฆฌ๋””๋ ‰์…˜๋งŒ ์ฒ˜๋ฆฌํ•  ๋ฟ ์ฝ˜ํ…์ธ  ์ž์ฒด๋Š” ๊ด€๋ฆฌํ•˜์ง€ ์•Š์œผ๋ฏ€๋กœ ๋ฉ”์‹œ์ง€ ๋ ˆ์ด์–ด๊ฐ€ ํ•„์š”ํ•˜๋ฉฐ, ์•„์ผ๋žœ๋“œ ๊ตฌ์กฐ๋กœ ์ธํ•ด ๋‘ ๋ฒˆ์งธ ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ•ฉ๋‹ˆ๋‹ค: ์•„์ผ๋žœ๋“œ๋Š” Astro๊ฐ€ ์•„๋‹Œ Lit์œผ๋กœ ์‹คํ–‰๋œ๋‹ค๋Š” ์ ์ž…๋‹ˆ๋‹ค.

    • Astro i18n๊ณผ ์ง์ ‘ ์ž‘์„ฑํ•œ ์‚ฌ์ „, ๊ทธ๋ฆฌ๊ณ  ์•„์ผ๋žœ๋“œ ๋‚ด๋ถ€์˜ @lit/localize: ๋™๊ธฐํ™”ํ•ด์•ผ ํ•  ๋‘ ๊ฐœ์˜ ์ฝ˜ํ…์ธ  ์†Œ์Šค๊ฐ€ ์ƒ๊ธฐ๊ณ , ๋‘˜ ์‚ฌ์ด์— ๊ณต์œ ๋œ ํƒ€์ž… ์•ˆ์ „์„ฑ์ด ์—†์Šต๋‹ˆ๋‹ค.
    • Intlayer: ๋‘˜ ๋ชจ๋‘๋ฅผ ์œ„ํ•œ ๋‹จ์ผ ์ฝ˜ํ…์ธ  ๋ ˆ์ด์–ด์ž…๋‹ˆ๋‹ค. astro-intlayer๋Š” .astro ํŽ˜์ด์ง€๋ฅผ ๋‹ค๋ฃจ๊ณ  lit-intlayer๋Š” Lit ์•„์ผ๋žœ๋“œ๋ฅผ ๋‹ค๋ฃจ๋ฉฐ, ๋‘˜ ๋‹ค ๋™์ผํ•œ ์„ ์–ธ์„ ์ฝ์Šต๋‹ˆ๋‹ค.

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

    ๋„ค์ž„์ŠคํŽ˜์ด์Šค ๊ธฐ๋ฐ˜ ์„ค์ •๋ณด๋‹ค ํ›จ์”ฌ ์ ์Šต๋‹ˆ๋‹ค. ํŽ˜์ด์ง€๋Š” ๋ Œ๋”๋งํ•˜์ง€ ์•Š๋Š” ์–ธ์–ด์˜ ์นดํƒˆ๋กœ๊ทธ๋ฅผ ์ ˆ๋Œ€ ๋‹ค์šด๋กœ๋“œํ•˜์ง€ ์•Š๊ธฐ ๋•Œ๋ฌธ์ž…๋‹ˆ๋‹ค. Astro ํŽ˜์ด์ง€๋Š” ๋นŒ๋“œ ํƒ€์ž„์— ๋ Œ๋”๋ง๋˜๋ฏ€๋กœ ๋ฒˆ์—ญ๋œ HTML๋งŒ ์ „์†กํ•˜๊ณ  ์‚ฌ์ „์€ ์ „ํ˜€ ์ „์†กํ•˜์ง€ ์•Š์œผ๋ฉฐ, ์˜ค์ง ์•„์ผ๋žœ๋“œ๋งŒ ์‚ฌ์ „์„ ์ „๋‹ฌ๋ฐ›์Šต๋‹ˆ๋‹ค. ๋นŒ๋“œ ํƒ€์ž„ ์ปดํŒŒ์ผ๋Ÿฌ๋Š” ์ฝ˜ํ…์ธ  ํ˜ธ์ถœ์„ ์ปดํฌ๋„ŒํŠธ๊ฐ€ ์‚ฌ์šฉํ•˜๋Š” ์ •ํ™•ํ•œ ํ•ญ๋ชฉ์œผ๋กœ ํ™•์ธํ•˜๊ณ , ๋™์  ์‚ฌ์ „์„ ํ†ตํ•ด ๋กœ์ผ€์ผ๋ณ„๋กœ ๋ถ„ํ• ํ•ฉ๋‹ˆ๋‹ค. ์ผ๋ฐ˜์ ์ธ ๋Œ€์•ˆ๋“ค๊ณผ ๋น„๊ตํ–ˆ์„ ๋•Œ 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๋งŒ ๊ฒ€ํ† ํ•˜๋ฉด ๋ฉ๋‹ˆ๋‹ค. ์ด ๊ฐ€์ด๋“œ์˜ 15๋‹จ๊ณ„๋ฅผ ํ™•์ธํ•˜์„ธ์š”.

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