작가:
    생성:2026-09-13마지막 업데이트:2026-09-13

    next-intl VS Intlayer | React & Next.js 국제화(i18n) 벤치마크

    next-intl은 오늘날 Next.js App Router의 기본 선택지입니다. 라우팅과의 긴밀한 통합, 완전한 ICU MessageFormat 지원, 전통적인 i18n 시스템을 사용해 본 사람이라면 누구나 익숙한 개발자 경험을 제공합니다.

    반면 Intlayer는 문제를 근본적으로 재구성합니다. 중앙 집중식 사전이 없으며, 네임스페이스와 라우트를 수동으로 일치시킬 필요가 없습니다. 콘텐츠는 각 컴포넌트 바로 옆에 선언되며, 빌드 타임 컴파일러가 각 페이지에 필요한 콘텐츠만 자동으로 번들링합니다.

    이 글은 각 라이브러리로 동일한 애플리케이션을 빌드하고 브라우저가 실제로 다운로드하고 실행하는 항목을 기록하는 오픈 소스 테스트 스위트인 Benchmark Bloom의 데이터를 바탕으로 두 라이브러리를 비교합니다.

    요약 (tl;dr): next-intl은 런타임 비용만으로도 페이지당 최소 +12.6 KB gzip을 추가하며, 표준 설정(staticdynamic)에서 다른 페이지 문자열의 약 90%를 누출합니다. 이 누출을 제거하려면 카탈로그를 네임스페이스로 분할하고 페이지별로 수동 선택해야 하는 번거로운 작업이 필요합니다. 반면 Intlayer 컴파일러는 추가 설정 없이도 누출 0%, 3배 더 작은 컴포넌트 크기, 기본 앱 대비 단 +0.3 KB 증가만을 보장합니다.

    한눈에 보기

    • next-intl - Next.js 커뮤니티의 표준. 언어별 중앙 집중식 JSON 사전, 완전한 ICU MessageFormat 지원, Next.js 요청 처리 및 라우팅과의 긴밀한 통합.
    • Intlayer - 컴포넌트 중심 콘텐츠 모델. .content.ts 파일이 담당 컴포넌트 바로 옆에 위치하며, 빌드 타임 컴파일러가 컴포넌트 및 로케일별로 트리 셰이킹과 지연 로딩을 수행하고 엄격한 TypeScript 타입을 자동으로 생성합니다.
    라이브러리GitHub 스타총 커밋 수최근 커밋첫 릴리스NPM 버전NPM 다운로드 수
    aymericzip/intlayerGitHub Repo starsGitHub commit activityLast Commit2024년 4월npmnpm downloads
    amannn/next-intlGitHub Repo starsGitHub commit activityLast Commit2021년 3월npmnpm downloads
    배지는 자동으로 업데이트됩니다.

    기능별 직접 비교

    기능Intlayer (react-intlayer / next-intlayer)next-intl (next-intl / use-intl)
    컴포넌트 인근 번역 파일 배치✅ 예, 각 컴포넌트와 함께 .content.ts 위치messages/ 폴더 내 중앙 집중식 JSON 사전
    TypeScript 통합✅ 콘텐츠로부터 자동 생성되는 엄격한 타입⚠️ 메시지 경로에 대한 수동 global.d.ts 설정을 통해 지원
    누락된 번역 감지✅ TypeScript 오류 + 빌드 타임 오류/경고⚠️ 런타임에서 누락된 키를 반환하거나 설정에 따라 오류 발생
    풍부한 콘텐츠 (JSX / Markdown / 컴포넌트)✅ 직접 지원⚠️ 컴포넌트 매핑을 포함한 t.rich()를 통해 지원
    ICU MessageFormat 지원⚠️ 개발 중✅ 예, 완전한 ICU 지원
    동기식 서버 컴포넌트 지원next-intlayer/serveruseIntlayer가 자식 서버 컴포넌트에서 즉시 작동❌ 비동기 서버 부모로부터 props를 통해 번역을 전달해야 함
    트리 셰이킹 (Tree-shaking)✅ 컴포넌트 및 로케일별로 컴파일러가 자동 수행⚠️ 네임스페이스를 수동 분할하고 pick()을 사용해야 함
    지연 로딩 (Lazy loading)✅ 단 한 줄의 설정 (importMode: 'dynamic')⚠️ getRequestConfig에서 수동 동적 import 필요
    비주얼 에디터 / CMS✅ 무료 비주얼 에디터 + 선택적 CMS❌ 없음
    AI 기반 자동 번역✅ 내장 기능, 자체 API 키 사용❌ 없음
    MCP 서버 및 에이전트 스킬✅ 지원❌ 없음

    벤치마크

    측정 항목

    Benchmark Bloom 테스트 스위트는 각 라이브러리로 동일한 애플리케이션을 빌드합니다. 10개 페이지(home, about, blog, careers, contact, FAQ, pricing, products, settings, team), 10개 언어(en, fr, es, de, it, pt, zh, ja, ko, ru), 동일한 컴포넌트 및 동일한 콘텐츠로 구성됩니다. 페이지는 enfr로 측정됩니다. 각 라이브러리는 최대 4가지 로딩 전략으로 구현되었습니다.

    전략설명대상 사용자
    static모든 로케일과 모든 페이지를 시작 시 함께 번들링하여 로드빠른 프로토타입, AI 생성 코드
    dynamic활성 로케일만 로드하지만, 모든 페이지를 한 번에 가져옴대부분의 프로젝트
    scoped-static라우트별 네임스페이스 분할, 지연 로딩 없음드문 경우
    scoped-dynamic라우트별 네임스페이스 + 지연 로딩. 현재 로케일의 현재 페이지만 전송엄격한 성능 예산이 필요한 앱

    Intlayer에는 "scoped" 변형이 없습니다. 컴파일러가 콘텐츠 범위를 컴포넌트별로 자동 제한하므로 staticdynamic 행이 이미 스코프화되어 있습니다.

    각 빌드에 대해 스위트는 다음을 기록합니다:

    • 라이브러리 크기 (Lib size): i18n 라이브러리만 가져오는 빈 컴포넌트의 gzip 크기.
    • 페이지 JS (Page JS): 페이지당 다운로드된 gzip JavaScript 크기.
    • 로케일 누출률 (Locale leak %): 사용자가 보지 않는 로케일에 속한 문자열의 비율.
    • 페이지 누출률 (Page leak %): 사용자가 머물고 있지 않은 페이지에 속한 문자열의 비율.
    • 컴포넌트 평균 크기 (Component avg): 격리되어 컴파일된 각 컴포넌트의 평균 gzip 크기.
    • E2E 반응성: 새 언어 선택부터 DOM의 html[lang] 업데이트까지의 시간.
    • 하이드레이션 (Hydration): React 하이드레이션 단계의 소요 시간.
    아래 수치는 next-intl 4.14.2 및 intlayer 9.5.1을 사용한 2026-09-12 실행 결과입니다.

    Next.js (App Router) 결과

    라이브러리전략라이브러리 크기 (gz)평균 페이지 JS (gz)로케일 누출페이지 누출평균 컴포넌트 (gz)E2E 반응성하이드레이션
    기본 앱 (i18n 없음)-0.0 KB141.0 KB0.0%0.0%0.9 KB13.4 ms11.8 ms
    next-intlstatic14.7 KB153.6 KB4.2%89.8%21.8 KB16.0 ms14.7 ms
    next-intldynamic14.7 KB153.6 KB9.7%89.9%21.8 KB15.6 ms14.8 ms
    next-intlscoped-static14.7 KB153.6 KB0.0%0.0%80.1 KB17.9 ms17.4 ms
    next-intlscoped-dynamic14.7 KB153.6 KB0.0%0.0%22.9 KB17.8 ms16.8 ms
    next-intlayerstatic5.5 KB141.3 KB0.0%0.0%8.5 KB15.5 ms16.9 ms
    next-intlayerdynamic5.5 KB141.3 KB0.0%0.0%6.9 KB15.3 ms15.9 ms
    @intlayer/next-intl (호환)static8.0 KB147.5 KB0.0%0.0%8.1 KB14.5 ms12.8 ms
    @intlayer/next-intl (호환)dynamic8.0 KB148.7 KB0.0%0.0%8.1 KB11.7 ms12.8 ms

    결과 해석

    • 런타임 비용. 기본 앱은 페이지당 141.0 KB입니다. next-intl은 이를 153.6 KB(모든 페이지에서 +12.6 KB gzip)로 늘리지만, Intlayer는 141.3 KB(+0.3 KB)로 거의 차이가 없습니다.
    • 콘텐츠 누출. 가장 흔히 사용되는 설정(staticdynamic)에서 next-intl은 전체 en.json이 클라이언트 공급자에 들어가기 때문에 페이지마다 다른 페이지 문자열의 약 90%를 전송합니다. 이를 0%로 만들려면 복잡한 수동 네임스페이스 분할이 필요하지만, Intlayer는 기본적으로 0%입니다.
    • 컴포넌트 크기. useTranslations()를 호출하는 컴포넌트는 평균 21.8 KB로 컴파일되지만, useIntlayer()를 사용하는 동일한 컴포넌트는 단 6.9 KB입니다.

    TanStack Start (use-intl) 결과

    라이브러리전략라이브러리 크기 (gz)평균 페이지 JS (gz)로케일 누출페이지 누출평균 컴포넌트 (gz)E2E 반응성
    기본 앱 (i18n 없음)-0.0 KB111.0 KB0.0%0.0%0.7 KB8.1 ms
    use-intlstatic14.1 KB179.8 KB50.0%89.8%76.0 KB6.7 ms
    use-intldynamic14.1 KB119.4 KB0.0%89.8%75.9 KB7.0 ms
    use-intlscoped-static14.1 KB128.7 KB0.0%0.0%87.1 KB20.9 ms
    use-intlscoped-dynamic14.1 KB128.7 KB0.0%0.0%87.1 KB13.3 ms
    intlayerstatic5.0 KB125.8 KB50.0%0.0%8.1 KB3.2 ms
    intlayerdynamic5.0 KB118.6 KB0.0%0.0%6.3 KB3.6 ms
    @intlayer/use-intl (호환)dynamic7.3 KB129.7 KB0.0%0.0%9.3 KB8.7 ms

    결과 해석

    • 단순한 use-intl 설정은 기본 앱보다 페이지당 68.8 KB 더 많은 JS를 전송합니다.
    • dynamic 모드에서도 use-intl은 119.4 KB에 달하며 여전히 89.8%의 페이지 누출을 유지합니다.
    • 아키텍처의 차이는 컴포넌트 크기에서 명확히 드러납니다. use-intl의 76-87 KB 대비 Intlayer는 6-8 KB에 불과합니다.
    • 로케일 전환 속도는 Intlayer가 2~4배 더 빠릅니다(3 ms vs 7-21 ms).

    왜 이런 차이가 발생하는가? 중앙 집중식 카탈로그 vs 컴파일된 사전

    next-intl은 전통적인 방식을 따릅니다. 로케일당 하나의 JSON 파일이 getRequestConfig에서 로드되고, NextIntlClientProvider에 전달되며, t("namespace.key")로 읽힙니다.

    bash
    .
    ├── messages
       ├── en.json
       └── fr.json
    └── src
        ├── i18n
       ├── request.ts
       └── routing.ts
        ├── middleware.ts
        └── app
            └── [locale]
                ├── layout.tsx
                └── about
                    └── page.tsx
    

    런타임은 페이지에서 어떤 키가 사용될지 미리 알 수 없으므로 전체 카탈로그를 전송하는 것이 유일하게 안전한 기본값입니다.

    Intlayer는 이 구조를 완전히 뒤집습니다. 콘텐츠는 컴포넌트 바로 옆에 선언됩니다.

    bash
    .
    ├── intlayer.config.ts
    └── src
        ├── middleware.ts
        └── app
            └── [locale]
                ├── layout.tsx
                └── about
                    ├── page.tsx
                    └── page.content.ts
        └── components
            └── Counter
                ├── index.tsx
                └── index.content.ts
    

    빌드 시 컴파일러는 어떤 컴포넌트가 어떤 사전을 가져오는지 감지하여 활성 로케일에 필요한 사전만 번들링합니다.

    dynamic 행의 수치를 얻으려면 intlayer.config.ts에서 dictionary.importMode: 'dynamic'을 설정하세요. 번들 최적화 문서를 참조하세요.

    개발자 경험

    클라이언트 컴포넌트

    next-intl

    messages/en.json
    {
      "counter": {
        "label": "Counter",
        "increment": "Increment"
      }
    }
    
    src/components/Counter.tsx
    "use client";
    
    import { useState } from "react";
    import { useTranslations, useFormatter } from "next-intl";
    
    export const Counter = () => {
      const t = useTranslations("counter");
      const format = useFormatter();
      const [count, setCount] = useState(0);
    
      return (
        <div>
          <p>{format.number(count)}</p>
          <button aria-label={t("label")} onClick={() => setCount((c) => c + 1)}>
            {t("increment")}
          </button>
        </div>
      );
    };
    

    Intlayer

    src/components/Counter/index.content.ts
    import { t, type Dictionary } from "intlayer";
    
    const counterContent = {
      key: "counter",
      content: {
        label: t({ en: "Counter", fr: "Compteur" }),
        increment: t({ en: "Increment", fr: "Incrémenter" }),
      },
    } satisfies Dictionary;
    
    export default counterContent;
    
    src/components/Counter/index.tsx
    "use client";
    
    import { useState } from "react";
    import { useIntlayer } from "next-intlayer";
    import { useNumber } from "next-intlayer/format";
    
    export const Counter = () => {
      const { label, increment } = useIntlayer("counter");
      const number = useNumber();
      const [count, setCount] = useState(0);
    
      return (
        <div>
          <p>{number(count)}</p>
          <button aria-label={label} onClick={() => setCount((c) => c + 1)}>
            {increment}
          </button>
        </div>
      );
    };
    

    동기식 서버 컴포넌트

    디자인 시스템 요소(내비게이션 바, 바닥글, 카드 등)는 클라이언트 컴포넌트의 자식으로 렌더링되는 서버 컴포넌트인 경우가 많으므로 비동기(async)일 수 없습니다.

    next-intl

    src/components/ServerCounter.tsx
    type ServerCounterProps = {
      t: (key: string) => string;
      formattedCount: string;
    };
    
    export const ServerCounter = ({ t, formattedCount }: ServerCounterProps) => (
      <div>
        <p>{formattedCount}</p>
        <button aria-label={t("label")}>{t("increment")}</button>
      </div>
    );
    

    Intlayer

    src/components/ServerCounter.tsx
    import { useIntlayer } from "next-intlayer/server";
    import { useNumber } from "next-intlayer/server/format";
    
    export const ServerCounter = ({ count }: { count: number }) => {
      const { label, increment } = useIntlayer("counter");
      const number = useNumber();
    
      return (
        <div>
          <p>{number(count)}</p>
          <button aria-label={label}>{increment}</button>
        </div>
      );
    };
    

    메타데이터

    next-intl

    src/app/[locale]/about/page.tsx
    import type { Metadata } from "next";
    import { getTranslations } from "next-intl/server";
    import { routing } from "@/i18n/routing";
    
    const localizedPath = (locale: string, path: string) =>
      locale === routing.defaultLocale ? path : `/${locale}${path}`;
    
    export const generateMetadata = async ({
      params,
    }: {
      params: Promise<{ locale: string }>;
    }): Promise<Metadata> => {
      const { locale } = await params;
      const t = await getTranslations({ locale, namespace: "about" });
    
      const languages = Object.fromEntries(
        routing.locales.map((l) => [l, localizedPath(l, "/about")])
      );
    
      return {
        title: t("title"),
        description: t("description"),
        alternates: {
          canonical: localizedPath(locale, "/about"),
          languages: { ...languages, "x-default": "/about" },
        },
      };
    };
    

    Intlayer

    src/app/[locale]/about/page.tsx
    import { getIntlayer, getMultilingualUrls } from "intlayer";
    import type { Metadata } from "next";
    import type { LocalPromiseParams } from "next-intlayer";
    
    export const generateMetadata = async ({
      params,
    }: LocalPromiseParams): Promise<Metadata> => {
      const { locale } = await params;
      const metadata = getIntlayer("about-metadata", locale);
      const multilingualUrls = getMultilingualUrls("/about");
    
      return {
        ...metadata,
        alternates: {
          canonical: multilingualUrls[locale as keyof typeof multilingualUrls],
          languages: { ...multilingualUrls, "x-default": "/about" },
        },
      };
    };
    

    next-intl API 유지 및 Intlayer 출력 획득

    위의 벤치마크 결과를 얻기 위해 기존 컴포넌트를 모두 다시 작성할 필요는 없습니다. @intlayer/next-intl은 드롭인 어댑터입니다. useTranslations, getTranslations, useFormatter, t.rich(), ICU 복수형을 그대로 유지하면서 Intlayer 컴파일러로 컴파일된 Intlayer 사전에서 콘텐츠를 제공합니다.

    next.config.ts
    import type { NextConfig } from "next";
    import { createNextIntlPlugin } from "@intlayer/next-intl/plugin";
    
    const withIntlayer = createNextIntlPlugin();
    
    const nextConfig: NextConfig = {};
    
    export default withIntlayer(nextConfig);
    

    벤치마크에서 동일한 앱의 호환 빌드는 애플리케이션 코드를 전혀 수정하지 않고도 페이지당 153.6 KB에서 147.5 KB로, 컴포넌트당 21.8 KB에서 8.1 KB로 줄었으며, 페이지 누출은 약 90%에서 0%로 개선되었습니다. 기존 messages/{locale}.json 파일은 JSON 동기화 플러그인을 통해 계속 단일 소스로 유지할 수 있습니다.

    자세한 단계는 next-intl 마이그레이션 가이드를 참조하세요.

    언제 무엇을 선택해야 할까요?

    • next-intl 선택: Next.js의 광범위한 생태계 표준을 원하거나, ICU MessageFormat에 크게 의존하거나, 앱이 중소 규모이거나, 중앙 집중식 번역 플랫폼(Crowdin, Phrase, Lokalise 등)과 연동하는 경우.
    • Intlayer 선택: 컴포넌트 단위 콘텐츠 관리, 엄격한 TypeScript 지원, 빌드 타임 누락 키 감지, 설정 없는 자동 트리 셰이킹 및 지연 로딩, 동기식 서버 컴포넌트, 내장 편집 도구(비주얼 에디터, CMS, AI 번역, MCP 서버)를 원하는 경우. 모듈식 코드베이스나 대규모 디자인 시스템에 특히 유용합니다.
    • @intlayer/next-intl 선택: 이미 next-intl을 사용 중이며 코드 재작성 없이 번들 최적화 이점을 얻고 싶은 경우.

    관련 비교

    GitHub STARS

    GitHub 스타 수는 프로젝트의 인기, 커뮤니티의 신뢰 및 장기적인 지속 가능성을 보여주는 강력한 지표입니다.

    스타 히스토리 차트

    결론

    next-intl은 탄탄하고 잘 유지 관리되는 라이브러리이며, 벤치마크에서도 Next.js 환경에서 결코 나쁜 선택이 아님을 확인할 수 있습니다. 하지만 중앙 집중식 카탈로그 모델은 최적화의 모든 부담을 개발자에게 전가합니다. 단순한 설정에서는 다른 페이지 콘텐츠가 약 90% 누출되며, 런타임 자체만으로도 모든 페이지에서 +12.6 KB gzip의 비용이 발생합니다.

    Intlayer는 이 모든 작업을 컴파일러로 이전합니다. 컴포넌트별 사전, 로케일별 지연 로딩, 미사용 콘텐츠 정리는 규칙이 아닌 자동 빌드 결과물입니다. 동일한 앱에서 얻은 결과는 페이지당 +0.3 KB, 누출 0%, 3배 더 작은 컴포넌트, TanStack Start에서 2~4배 더 빠른 로케일 전환이었습니다.

    모든 원시 데이터, 테스트 앱 및 스크립트는 Benchmark Bloom 저장소에 공개되어 있습니다. 직접 실행해 보세요.

    자세한 내용은 'Why Intlayer?' 문서를 참조하세요.

    댓글

    아직 댓글이 없습니다. 첫 번째로 의견을 나눠보세요.

    관련 게시물

    최근 게시물