THE DEVLOG

scribbly.

Next JS 치트 시트

2026.02.08 02:44:14

next-intl 사용법을 정리한 문서

특히 next-intl은 rewrite를 통해 locale을 자동으로 감춰주는 등의 설정이 편리하게 되어 있다.

따라서 loacle을 통해 generateStaticParams 만 잘 지정하면 정적인 웹 사이트를 만들 수 있다.

추가로 "use cache" 호환성 이슈를 우회하는 방법을 정리하였다.


0) 디렉토리 구조

messages/
  ko.json
  en.json

src/
  i18n/
    routing.ts
    navigation.ts
    request.ts
  proxy.ts
  app/
    [locale]/
      layout.tsx
      page.tsx

1) 설치

npm i next-intl

2) messages 준비

가장 단순하게는 로컬에 messages/{locale}.json을 둔다.

// messages/ko.json
{
  "HomePage": {
    "title": "안녕하세요"
  }
}

3) next.config.ts — 플러그인 연결

next-intl/plugin을 붙여서 i18n/request.ts를 연결한다.

// next.config.ts
import type {NextConfig} from 'next';
import createNextIntlPlugin from 'next-intl/plugin';

const nextConfig: NextConfig = {};

const withNextIntl = createNextIntlPlugin();
export default withNextIntl(nextConfig);

4) i18n 라우팅 세팅

4-1) routing.ts

지원하는 locale과 default locale을 정해 둔다.

// src/i18n/routing.ts
import {defineRouting} from 'next-intl/routing';

export const routing = defineRouting({
  locales: ['ko', 'en'],
  defaultLocale: 'ko'
});

4-2) proxy.ts (미들웨어)

페이지 요청에 대해 locale prefix(/en/...)를 처리한다.

// src/proxy.ts
import createMiddleware from 'next-intl/middleware';
import {routing} from './i18n/routing';

export default createMiddleware(routing);

export const config = {
  matcher: '/((?!api|trpc|_next|_vercel|.*\\..*).*)'
};

참고: Next.js 16부터 파일명이 proxy.ts로 바뀌었고, 그 전에는 middleware.ts를 쓴다.

4-3) navigation.ts

Next.js 네비게이션 API를 locale-aware하게 감싼다.

// src/i18n/navigation.ts
import {createNavigation} from 'next-intl/navigation';
import {routing} from './routing';

export const {Link, redirect, usePathname, useRouter, getPathname} =
  createNavigation(routing);

5) request.ts — 요청 단위 locale/messages 결정

서버에서 “이번 요청의 locale + messages”를 결정하는 파일.

// src/i18n/request.ts
import {getRequestConfig} from 'next-intl/server';
import {hasLocale} from 'next-intl';
import {routing} from './routing';

export default getRequestConfig(async ({requestLocale}) => {
  const requested = await requestLocale;

  const locale = hasLocale(routing.locales, requested)
    ? requested
    : routing.defaultLocale;

  return {
    locale,
    messages: (await import(`../../messages/${locale}.json`)).default
  };
});

6) app/[locale]/layout.tsx — Provider + 정적 렌더링 옵션

  1. generateStaticParams 을 이용해 locale을 특정할 수 있다. 이를 통해 페이지를 정적으로 생성할 수 있도록 한다.
  2. NextIntlClientProvider로 감싸면 Client Component에서 next-intl 훅을 사용할 수 있다.
// src/app/[locale]/layout.tsx
import {NextIntlClientProvider, hasLocale} from 'next-intl';
import {getMessages, setRequestLocale} from 'next-intl/server';
import {notFound} from 'next/navigation';
import {routing} from '@/i18n/routing';

export function generateStaticParams() {
  return routing.locales.map((locale) => ({locale}));
}

export default async function LocaleLayout({
  children,
  params
}: {
  children: React.ReactNode;
  params: Promise<{locale: string}>;
}) {
  const {locale} = await params;

  if (!hasLocale(routing.locales, locale)) notFound();

  setRequestLocale(locale);

  const messages = await getMessages();

  return (
    <html lang={locale}>
      <body>
        <NextIntlClientProvider messages={messages}>
          {children}
        </NextIntlClientProvider>
      </body>
    </html>
  );
}

7) 번역 사용

7-1) Client Component

'use client';
import {useTranslations} from 'next-intl';

export default function Title() {
  const t = useTranslations('HomePage');
  return <h1>{t('title')}</h1>;
}

7-2) Server Component (async)

import {getTranslations} from 'next-intl/server';

export default async function Page() {
  const t = await getTranslations('HomePage');
  return <h1>{t('title')}</h1>;
}

8) locale prefix 옵션

import {defineRouting} from 'next-intl/routing';

export const routing = defineRouting({
  locales: ['ko', 'en'],
  defaultLocale: 'ko',

  localePrefix: 'as-needed'
});
  • "always" : 프리픽스가 항상 표시된다.

  • "as-needed" : defaultLocale인 경우 프리픽스가 감춰진다.

  • "never" : locale을 항상 rewrite 하여 감춘다. (SEO 최적화 유의)

9) use cache 우회 방법

Implementing Next.js 16 'use cache' with next-intl Internationalization - aurorascharff 블로그
 

Next.js 16 cacheComponents 방식에서 getTranslations('IndexPage')를 호출하면 <Suspense> 경계로 분리하라는 에러가 발생된다. getTranslation 함수는 locale을 확인하기 위해 사용자 요청에 접근한다.

이를 우회하는 방법은 locale을 인자에 명시적으로 넘겨주는 것이다. 파라미터 객체에 locale을 명시적으로 넘기면 fallback으로 넘어가지 않고 인자에 기재된 locale을 사용한다.

  const t = await getTranslations({
    locale,
    namespace: 'IndexPage'
  });

 

9-1) 예시 코드

import {Locale} from 'next-intl';
import {setRequestLocale} from 'next-intl/server';
import {Suspense} from 'react';
import {getTranslations} from 'next-intl/server';
import PageLayout from '@/components/PageLayout';
import NavigationLink from '@/components/NavigationLink';


// 레이아웃에서 staticParams를 만들어 정적으로 렌더링하도록 한다.
export function generateStaticParams() {
  return routing.locales.map((locale) => ({locale}));
}

export default async function IndexPage({params}: PageProps<'/[locale]'>) {
  const resolvedParams = await params;
  const locale = resolvedParams.locale as Locale;

  setRequestLocale(locale);

  const t = await getTranslations({
    locale: locale,
    namespace: 'IndexPage'
  });

  return (
    <PageLayout title={t('title')}>
      <Suspense fallback={<ComponentSkeleton />}>
        <DynamicComponent />
      </Suspense>
      {/* 정적 컴포넌트에는 locale을 props로 넘겨 요청에 접근하지 않도록 한다 */}
      <CachedComponent locale={locale} />
      <p className="max-w-[590px]">{t('description')}</p>
    </PageLayout>
  );
}

async function DynamicComponent() {
  await new Promise((resolve) => setTimeout(resolve, 1000)); // async가 실행되는 예시
  const t = await getTranslations('IndexPage');

  return (
    <div className="mb-8 rounded-lg bg-gray-800 p-6">
      <h2 className="mb-4 text-2xl font-bold text-white">
        {t('dynamicComponent.title')}
      </h2>
      <p>{t('dynamicComponent.content')}</p>
    </div>
  );
}

async function CachedComponent({locale}: {locale: Locale}) {
  'use cache';

  await new Promise((resolve) => setTimeout(resolve, 1000)); 

  // props로 받은 locale을 translations에 인자로 넘긴다.
  const t = await getTranslations({
    locale,
    namespace: 'IndexPage'
  });

  return (
    <div className="mb-8 rounded-lg bg-gray-800 p-6">
      <NavigationLink href="/pathnames">{t('link')}</NavigationLink>
      <h2 className="mb-4 text-2xl font-bold text-white">
        {t('cachedComponent.title')}
      </h2>
      <p>{t('cachedComponent.content')}</p>
    </div>
  );
}

function ComponentSkeleton() {
  return (
    <div className="mb-8 rounded-lg bg-gray-800 p-6 animate-pulse">
      <div className="mb-4 h-6 w-1/3 rounded bg-gray-700"></div>
      <div className="mb-2 h-4 w-full rounded bg-gray-700"></div>
      <div className="h-4 w-2/3 rounded bg-gray-700"></div>
    </div>
  );
}

 


출처