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

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

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

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

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

      bash
      npx intlayer init --interactive
      
      --interactive ํ”Œ๋ž˜๊ทธ๋Š” ์„ ํƒ ์‚ฌํ•ญ์ž…๋‹ˆ๋‹ค. AI ์—์ด์ „ํŠธ์ธ ๊ฒฝ์šฐ intlayer-cli init๋ฅผ ์‚ฌ์šฉํ•˜์„ธ์š”.
      ์ด ๋ช…๋ น์€ ํ™˜๊ฒฝ์„ ๊ฐ์ง€ํ•˜๊ณ  ํ•„์š”ํ•œ ํŒจํ‚ค์ง€๋ฅผ ์„ค์น˜ํ•ฉ๋‹ˆ๋‹ค. ์˜ˆ๋ฅผ ๋“ค์–ด:
      bash
      npm install intlayer astro-intlayer vue vue-intlayer @astrojs/vue
      
      • intlayer ์„ค์ • ๊ด€๋ฆฌ, ๋ฒˆ์—ญ, ์ฝ˜ํ…์ธ  ์„ ์–ธ, ํŠธ๋žœ์ŠคํŒŒ์ผ ๋ฐ CLI ๋ช…๋ น์–ด๋ฅผ ์œ„ํ•œ ๊ตญ์ œํ™” ๋„๊ตฌ๋ฅผ ์ œ๊ณตํ•˜๋Š” ํ•ต์‹ฌ ํŒจํ‚ค์ง€์ž…๋‹ˆ๋‹ค.

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

      • vue ํ•ต์‹ฌ Vue ํŒจํ‚ค์ง€์ž…๋‹ˆ๋‹ค.

      • vue-intlayer Intlayer๋ฅผ Vue ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜๊ณผ ํ†ตํ•ฉํ•˜๋Š” ํŒจํ‚ค์ง€์ž…๋‹ˆ๋‹ค. Vue์˜ ๊ตญ์ œํ™”๋ฅผ ์œ„ํ•œ installIntlayer, useIntlayer ๋ฐ useLocale ์ปดํฌ์ €๋ธ”์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.

      • @astrojs/vue Vue ์ปดํฌ๋„ŒํŠธ ์•„์ผ๋žœ๋“œ๋ฅผ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•ด์ฃผ๋Š” ๊ณต์‹ 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 ํ”Œ๋Ÿฌ๊ทธ์ธ๊ณผ Vue ํ†ตํ•ฉ์„ ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค.

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

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

      src/app.content.ts
      import { t, type Dictionary } from "intlayer";
      
      const appContent = {
        key: "app",
        content: {
          title: t({
            en: "Hello World",
            fr: "Bonjour le monde",
            es: "Hola mundo",
            ko: "์•ˆ๋…•ํ•˜์„ธ์š”",
          }),
        },
      } satisfies Dictionary;
      
      export default appContent;
      
      ์ฝ˜ํ…์ธ  ์„ ์–ธ์€ contentDir(๊ธฐ๋ณธ๊ฐ’ ./src)์— ํฌํ•จ๋˜์–ด ์žˆ๊ณ  ์ฝ˜ํ…์ธ  ์„ ์–ธ ํŒŒ์ผ ํ™•์žฅ์ž(๊ธฐ๋ณธ๊ฐ’ .content.{json,ts,tsx,js,jsx,mjs,cjs,md,mdx,yaml,yml})์™€ ์ผ์น˜ํ•œ๋‹ค๋ฉด ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์–ด๋””์—์„œ๋‚˜ ์ •์˜ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
      ์ž์„ธํ•œ ๋‚ด์šฉ์€ ์ฝ˜ํ…์ธ  ์„ ์–ธ ๋ฌธ์„œ๋ฅผ ์ฐธ์กฐํ•˜์„ธ์š”.
    5. Astro์—์„œ ์ฝ˜ํ…์ธ  ์‚ฌ์šฉ

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

      src/pages/[...locale]/index.astro
      ---
      import {
        getIntlayer,
        getLocaleFromPath,
        getLocalizedUrl,
        getHTMLTextDir,
        getPrefix,
        localeMap,
        defaultLocale,
        type LocalesValues,
      } from "intlayer";
      import VueIsland from "../../components/vue/VueIsland.vue";
      
      export const getStaticPaths = () => {
        return localeMap(({ locale }) => ({
          params: { locale: getPrefix(locale).localePrefix },
        }));
      };
      
      const locale = getLocaleFromPath(Astro.url.pathname) as LocalesValues;
      const { title } = 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>{title}</title>
      
          <!-- Canonical ๋งํฌ: ๊ฒ€์ƒ‰ ์—”์ง„์— ์ด ํŽ˜์ด์ง€์˜ ์ฃผ ๋ฒ„์ „์ž„์„ ์•Œ๋ฆฝ๋‹ˆ๋‹ค -->
          <link
            rel="canonical"
            href={new URL(getLocalizedUrl(Astro.url.pathname, locale), Astro.site)}
          />
      
          <!-- Hreflang: ๋ชจ๋“  ๋กœ์ปฌ๋ผ์ด์ฆˆ๋œ ๋ฒ„์ „์— ๋Œ€ํ•ด Google์— ์•Œ๋ฆฝ๋‹ˆ๋‹ค -->
          {
            localeMap(({ locale: mapLocale }) => (
              <link
                rel="alternate"
                hreflang={mapLocale}
                href={new URL(
                  getLocalizedUrl(Astro.url.pathname, mapLocale),
                  Astro.site
                )}
              />
            ))
          }
      
          <!-- x-default: ์ผ์น˜ํ•˜๋Š” ์–ธ์–ด๊ฐ€ ์—†๋Š” ์‚ฌ์šฉ์ž๋ฅผ ์œ„ํ•œ ํด๋ฐฑ(fallback) ์˜ต์…˜ -->
          <link
            rel="alternate"
            hreflang="x-default"
            href={new URL(
              getLocalizedUrl(Astro.url.pathname, defaultLocale),
              Astro.site
            )}
          />
        </head>
        <body>
          <!-- Vue ์•„์ผ๋žœ๋“œ๋Š” ์–ธ์–ด ์ „ํ™˜๊ธฐ๋ฅผ ํฌํ•จํ•œ ๋ชจ๋“  ์ธํ„ฐ๋ž™ํ‹ฐ๋ธŒ ์ฝ˜ํ…์ธ ๋ฅผ ๋ Œ๋”๋งํ•ฉ๋‹ˆ๋‹ค -->
          <VueIsland locale={locale} client:only="vue" />
        </body>
      </html>
      

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

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

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

      src/components/vue/VueIsland.vue
      <script setup lang="ts">
      import { ref, getCurrentInstance } from "vue";
      import { useIntlayer, useLocale, installIntlayer } from "vue-intlayer";
      import { getLocalizedUrl, getLocaleName, type LocalesValues } from "intlayer";
      
      const props = defineProps<{ locale: LocalesValues }>();
      
      const app = getCurrentInstance()?.appContext.app;
      if (app) {
        installIntlayer(app, { locale: props.locale });
      }
      
      const {
        locale: currentLocale,
        availableLocales,
        setLocale,
      } = useLocale({
        onLocaleChange: (newLocale: LocalesValues) => {
          window.location.href = getLocalizedUrl(window.location.pathname, newLocale);
        },
      });
      
      const count = ref(0);
      const { title } = useIntlayer("app");
      </script>
      
      <template>
        <div>
          <h1>{{ title }}</h1>
          <!-- ์–ธ์–ด ์ „ํ™˜๊ธฐ๋Š” ์•„์ผ๋žœ๋“œ ํ…œํ”Œ๋ฆฟ ๋‚ด์— ์ธ๋ผ์ธ์œผ๋กœ ๋ Œ๋”๋ง๋ฉ๋‹ˆ๋‹ค -->
          <div class="locale-switcher">
            <span class="switcher-label">์–ธ์–ด ์ „ํ™˜:</span>
            <div class="locale-buttons">
              <button
                v-for="localeItem in availableLocales"
                :key="localeItem"
                :class="['locale-btn', { active: localeItem === currentLocale }]"
                :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>
      </template>
      
      locale ํ”„๋กญ์€ Astro ํŽ˜์ด์ง€(์„œ๋ฒ„ ๊ฐ์ง€)์—์„œ ์ „๋‹ฌ๋˜์–ด installIntlayer๋ฅผ ์ดˆ๊ธฐํ™”ํ•˜๋Š” ๋ฐ ์‚ฌ์šฉ๋˜๋ฉฐ, ์ด๋กœ ์ธํ•ด ์ปดํฌ๋„ŒํŠธ ํŠธ๋ฆฌ ๋‚ด ๋ชจ๋“  ์ปดํฌ์ €๋ธ”์˜ ๊ธฐ๋ณธ ๋กœ์ผ€์ผ์ด ๊ฒฐ์ •๋ฉ๋‹ˆ๋‹ค.
    7. ์–ธ์–ด ์ „ํ™˜๊ธฐ ์ถ”๊ฐ€

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

      src/components/vue/VueIsland.vue
      <script setup lang="ts">
      import { useLocale } from "vue-intlayer";
      import { getLocalizedUrl, getLocaleName, type LocalesValues } from "intlayer";
      
      // 6๋‹จ๊ณ„์™€ ๋™์ผํ•œ props/setup ์•ฑ ์žฌ์‚ฌ์šฉ...
      
      const {
        locale: currentLocale,
        availableLocales,
        setLocale,
      } = useLocale({
        onLocaleChange: (newLocale: LocalesValues) => {
          // ์–ธ์–ด ๋ณ€๊ฒฝ ์‹œ ๋กœ์ปฌ๋ผ์ด์ฆˆ๋œ URL๋กœ ์ด๋™
          window.location.href = getLocalizedUrl(window.location.pathname, newLocale);
        },
      });
      </script>
      
      <template>
        <div class="locale-switcher">
          <span class="switcher-label">์–ธ์–ด ์ „ํ™˜:</span>
          <div class="locale-buttons">
            <button
              v-for="localeItem in availableLocales"
              :key="localeItem"
              :class="['locale-btn', { active: localeItem === currentLocale }]"
              :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>
      </template>
      

      ์˜์†์„ฑ์— ๊ด€ํ•œ ์ฐธ๊ณ  ์‚ฌํ•ญ: 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,
      
          /**
           * Dictionary ํ‚ค ์ ‘๋‘์‚ฌ
           */
          dictionaryKeyPrefix: "",
        },
      };
      
      export default config;
      

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

      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๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์ฝ˜ํ…์ธ ๋ฅผ ์™ธ๋ถ€ํ™”ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.