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

    Intlayer๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ Astro + Vanilla JS ์‚ฌ์ดํŠธ ๋ฒˆ์—ญํ•˜๊ธฐ | ๊ตญ์ œํ™” (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 + Vanilla JS์—์„œ Intlayer ์„ค์ •์„ ์œ„ํ•œ ๋‹จ๊ณ„๋ณ„ ๊ฐ€์ด๋“œ

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

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

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

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

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

      • vanilla-intlayer Intlayer๋ฅผ Vanilla JavaScript / TypeScript ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜๊ณผ ํ†ตํ•ฉํ•˜๋Š” ํŒจํ‚ค์ง€์ž…๋‹ˆ๋‹ค. ํ”„๋ ˆ์ž„์›Œํฌ ์—†์ด๋„ Astro์˜ <script> ํƒœ๊ทธ ๋‚ด ์–ด๋””์„œ๋‚˜ ์–ธ์–ด ๋ณ€๊ฒฝ์— ์‘๋‹ตํ•  ์ˆ˜ ์žˆ๋„๋ก pub/sub ์‹ฑ๊ธ€ํ†ค(IntlayerClient)๊ณผ ์ฝœ๋ฐฑ ๊ธฐ๋ฐ˜ ํ—ฌํผ(useIntlayer, useLocale ๋“ฑ)๋ฅผ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.

    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 ํ”Œ๋Ÿฌ๊ทธ์ธ์„ ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค. Vanilla JS์˜ ๊ฒฝ์šฐ ์ถ”๊ฐ€์ ์ธ UI ํ”„๋ ˆ์ž„์›Œํฌ ํ†ตํ•ฉ์€ ํ•„์š”ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

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

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

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

      Vanilla JS์˜ ๊ฒฝ์šฐ ๋ชจ๋“  ์„œ๋ฒ„ ์‚ฌ์ด๋“œ ๋ Œ๋”๋ง์€ .astro ํŒŒ์ผ ๋‚ด์—์„œ getIntlayer๋ฅผ ์ง์ ‘ ์‚ฌ์šฉํ•˜์—ฌ ์ˆ˜ํ–‰๋ฉ๋‹ˆ๋‹ค. ๊ทธ ํ›„ <script> ๋ธ”๋ก์ด ํด๋ผ์ด์–ธํŠธ ์‚ฌ์ด๋“œ์—์„œ vanilla-intlayer๋ฅผ ์ดˆ๊ธฐํ™”ํ•˜์—ฌ ์–ธ์–ด ์ „ํ™˜์„ ์ฒ˜๋ฆฌํ•ฉ๋‹ˆ๋‹ค.

      src/pages/[...locale]/index.astro
      ---
      import {
        getIntlayer,
        getLocaleFromPath,
        getLocalizedUrl,
        getPrefix,
        getLocaleName,
        localeMap,
        locales,
        defaultLocale,
        getPathWithoutLocale,
        type LocalesValues,
      } from "intlayer";
      
      export const getStaticPaths = () => {
        return localeMap(({ locale }) => ({
          params: { locale: getPrefix(locale).localePrefix },
        }));
      };
      
      const locale = getLocaleFromPath(Astro.url.pathname) as LocalesValues;
      const pathWithoutLocale = getPathWithoutLocale(Astro.url.pathname);
      const { greeting, description, switchLocale } = getIntlayer("app", 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>
          <main>
            <h1 id="greeting">{greeting}</h1>
            <p id="description">{description}</p>
      
            <div class="locale-switcher">
              <span class="switcher-label">{switchLocale}</span>
              <div class="locale-buttons">
                {
                  locales.map((localeItem) => (
                    <a
                      href={localeItem === locale ? undefined : getLocalizedUrl(pathWithoutLocale, localeItem)}
                      class={`locale-btn ${localeItem === locale ? "active" : ""}`}
                      data-locale={localeItem}
                      aria-disabled={localeItem === locale}
                    >
                      {getLocaleName(localeItem)}
                    </a>
                  ))
                }
              </div>
            </div>
          </main>
        </body>
      </html>
      
      string ์†์„ฑ(์˜ˆ: alt, title, href, aria-label ๋“ฑ)์—์„œ ์ฝ˜ํ…์ธ ๋ฅผ ์‚ฌ์šฉํ•˜๋ ค๋ฉด ํ•จ์ˆ˜์˜ ๊ฐ’์„ ๋‹ค์Œ๊ณผ ๊ฐ™์ด ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค:
      html
      <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. ์–ธ์–ด ์ „ํ™˜ ๊ธฐ๋Šฅ ์ถ”๊ฐ€

      Vanilla JS๊ฐ€ ํฌํ•จ๋œ Astro์—์„œ ์–ธ์–ด ์ „ํ™˜์€ ์„œ๋ฒ„์—์„œ ์ผ๋ฐ˜ ๋งํฌ๋กœ ๋ Œ๋”๋ง๋˜๋ฉฐ ํด๋ผ์ด์–ธํŠธ์—์„œ <script> ๋ธ”๋ก์„ ํ†ตํ•ด ํ•˜์ด๋“œ๋ ˆ์ด์…˜๋ฉ๋‹ˆ๋‹ค. ์‚ฌ์šฉ์ž๊ฐ€ ์–ธ์–ด ๋งํฌ๋ฅผ ํด๋ฆญํ•˜๋ฉด vanilla-intlayer๋Š” ๋กœ์ปฌ๋ผ์ด์ฆˆ๋œ URL๋กœ ์ด๋™ํ•˜๊ธฐ ์ „์— setLocale์„ ํ†ตํ•ด ์–ธ์–ด ์ฟ ํ‚ค๋ฅผ ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค.

      src/pages/[...locale]/index.astro
      <!-- ์œ„ 5๋‹จ๊ณ„์˜ ์„œ๋ฒ„ ์‚ฌ์ด๋“œ ๋งˆํฌ์—… ์ฐธ์กฐ -->
      
      <script>
        import { installIntlayer, useLocale } from "vanilla-intlayer";
        import { getLocaleFromPath, getLocalizedUrl, type LocalesValues } from "intlayer";
      
        // ํ˜„์žฌ URL์˜ ๋กœ์ผ€์ผ์„ ์‚ฌ์šฉํ•˜์—ฌ ํด๋ผ์ด์–ธํŠธ ์‚ฌ์ด๋“œ Intlayer ์ดˆ๊ธฐํ™”
        const locale = getLocaleFromPath(window.location.pathname);
        installIntlayer({ locale: locale as LocalesValues });
      
        const { setLocale } = useLocale({
          onLocaleChange: (newLocale: LocalesValues) => {
            window.location.href = getLocalizedUrl(window.location.pathname, newLocale);
          },
        });
      
        // ์–ธ์–ด ์ „ํ™˜ ๋งํฌ์— ํด๋ฆญ ์ด๋ฒคํŠธ ์—ฐ๊ฒฐ
        const localeLinks = document.querySelectorAll("[data-locale]");
        localeLinks.forEach((link) => {
          link.addEventListener("click", (e) => {
            const localeValue = link.getAttribute("data-locale") as LocalesValues;
            if (localeValue && localeValue !== locale) {
              e.preventDefault();
              setLocale(localeValue);
            }
          });
        });
      </script>
      

      ์˜์†์„ฑ์— ๊ด€ํ•œ ์ฐธ๊ณ  ์‚ฌํ•ญ: installIntlayer๋Š” ์„œ๋ฒ„๊ฐ€ ์ •์˜ํ•œ ๋กœ์ผ€์ผ๋กœ Intlayer ์‹ฑ๊ธ€ํ†ค์„ ์ดˆ๊ธฐํ™”ํ•ฉ๋‹ˆ๋‹ค. onLocaleChange์™€ ํ•จ๊ป˜ ์‚ฌ์šฉ๋˜๋Š” useLocale์€ ์ด๋™ ์ „์— ๋ฏธ๋“ค์›จ์–ด๋ฅผ ํ†ตํ•ด ์–ธ์–ด ์ฟ ํ‚ค๋ฅผ ์„ค์ •ํ•˜์—ฌ ํ–ฅํ›„ ๋ฐฉ๋ฌธ ์‹œ ์‚ฌ์šฉ์ž์˜ ์„ ํ˜ธ๋„๊ฐ€ ๊ธฐ์–ต๋˜๋„๋ก ๋ณด์žฅํ•ฉ๋‹ˆ๋‹ค.

      ์ ์ง„์  ํ–ฅ์ƒ(Progressive Enhancement)์— ๊ด€ํ•œ ์ฐธ๊ณ  ์‚ฌํ•ญ: ์–ธ์–ด ์ „ํ™˜ ๋งํฌ๋Š” JavaScript๊ฐ€ ์—†์–ด๋„ ํ‘œ์ค€ <a> ํƒœ๊ทธ๋กœ ์ž‘๋™ํ•ฉ๋‹ˆ๋‹ค. JS๋ฅผ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋Š” ๊ฒฝ์šฐ, setLocale ํ˜ธ์ถœ์€ ์ด๋™ ์ „์— ์ฟ ํ‚ค๋ฅผ ์—…๋ฐ์ดํŠธํ•˜์—ฌ ๋ฏธ๋“ค์›จ์–ด๊ฐ€ ์˜ฌ๋ฐ”๋ฅธ ๋ฆฌ๋””๋ ‰์…˜์„ ์‹คํ–‰ํ•˜๋„๋ก ํ•ฉ๋‹ˆ๋‹ค.

    7. 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" },
        });
      };
      
    8. ์ปดํฌ๋„ŒํŠธ์˜ ์ฝ˜ํ…์ธ  ์ถ”์ถœ

      ์„ ํƒ์‚ฌํ•ญ

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

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

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

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

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

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

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

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

    TypeScript ์„ค์ •

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

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

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

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

    tsconfig.json
    {
      // ... ๊ธฐ์กด TypeScript ์„ค์ •
      "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๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์ฝ˜ํ…์ธ ๋ฅผ ์™ธ๋ถ€ํ™”ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.