Skip to main content

Command Palette

Search for a command to run...

[ 살펴보기 ] Next-Intl - Routing, Middleware

Updated
6 min readView as Markdown
[ 살펴보기 ] Next-Intl - Routing, Middleware
C

A developer living in Busan, Korea

pathname에 locale ( langauge ) 정보를 포함하는 i18n routing 형식으로 다국어 기능을 제공할 때 설정을 통해 default language를 설정하거나 혹은 default language를 사용할 때 language 정보를 pathname에서 제외하는 등 여러가지 설정을 추가할 수 있다. 해당 포스트를 통해 Next-Intl을 사용할 때 필요한 Routing, Middleware 관련 설정 방법을 살펴보자.

Redirect unprefixed pathname

Next-intl은 default로 unprefixed pathname ( pathname에 locale 정보가 포함되지 않은 ) 으로 접근하면 prefixed pathname( pathname에 locale 정보가 포함된 )로 redirect 시킨다.

src/i18n/routing.ts

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

export const routing = defineRouting({
  // ...
  localePrefix: 'always' // default value
});

unprefixed pathname이 locale 정보가 포함된 prefixed pathname으로 redirect 되려면middleware를 unprefixed pathname request도 처리할 수 있도록 matcher를 수정해주어야 한다. ( Reference - Pathnames without a locale prefix )

src/middleware.ts

import createMiddleware from "next-intl/middleware";
import { routing } from "./i18n/routing";

export default createMiddleware(routing);

export const config = {
  // /api, /_next, /_vercel로 시작하는 pathname 제외하고 모든 pathname과 match
  matcher: ["/((?!api|_next|_vercel|.*\\..*).*)"],
};

위와 같이 설정하고 http://localhost:3000/user와 같이 unprefixed uri로 접근하면 http://localhost:3000/en/user와 같이 prefixed uri로 redirect되는 것을 확인할 수 있다.

unprefixed pathname에 default locale 적용

아래의 설정을 통해 unprefixed pathname으로 페이지에 접근할 때 redirect 시키는 것이 아닌 default locale을 적용할 수도 있다.

src/i18n/routing.ts

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

export const routing = defineRouting({
  locales: ["en", "ko"],
  defaultLocale: "ko",
  localePrefix: 'as-needed'
});

위의 경우에도 마찬가지로 middleware에서 unprefixed pathname request를 처리할 수 있어야 하므로 다음과 같이 matcher를 수정한다.

src/middleware.ts

import createMiddleware from "next-intl/middleware";
import { routing } from "./i18n/routing";

export default createMiddleware(routing);

export const config = {
  // /api, /_next, /_vercel로 시작하는 pathname 제외하고 모든 pathname과 match
  matcher: ["/((?!api|_next|_vercel|.*\\..*).*)"],
};

위와 같이 설정하고 http://localhost:3000/user와 같이 unprefixed pathname를 통해 page에 접근하면 redirection 되는 것이 아닌 defaultLocale로 설정한 locale 값이 적용되는 것을 확인할 수 있다. 위의 예제에선 ko locale이 적용된다.

Custom prefixes

pathname에 포함되는 locale을 각 locale json 파일 이름이 아닌 custom prefix name을 설정하여 사용할 수도 있다.

src/i18n/routing.ts

import { defineRouting } from "next-intl/routing";
import { createNavigation } from "next-intl/navigation";

export const routing = defineRouting({
  locales: ["en", "ko"],
  defaultLocale: "en",
  localePrefix: {
    mode: "always",
    prefixes: {
      en: "/uk",
    },
  },
});


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

위의 예제에서 en locale을 /uk prefix로 mapping하고 있다. 위와 같이 설정하고 http://localhost:3000/uk/user pathname으로 page에 접근하면 prefixes property에서 mapping한 en locale이 적용된다.

만약 middleware matcher가 위의 예제처럼 모든 pathname request를 처리하도록 설정되어 있다면 추가로 수정할 부분은 없지만 만약 그렇지 않다면 custom prefix ( 위의 예제에선 /uk )를 사용하는 pathname로 middleware가 처리할 수 있도록 matcher를 수정해주어야 한다.

Next-Intl을 위해 설정한 middleware는 default로 NEXT_LOCALE이라는 이름의 cookie에 가장 최근 사용한 locale 정보를 저장한다. 그리고 추후 user가 사이트를 다시 방문할 때 unprefixed pathname을 통해 접근할 때 해당 cookie 정보를 이용하여 prefixed pathname으로 redirect 시킨다. ( 위에서 살펴본 defineRouting 설정에서 localePrefix option이 always일 때 )

NEXT_LOCALE cookie는 default로 maxAge가 1년이고 sameSite가 lax로 설정된다. 이를 변경하고자 한다면 다음과 같이 localeCookie property를 통해 변경할 수 있다.

src/i18n/routing.ts

import { defineRouting } from "next-intl/routing";
import { createNavigation } from "next-intl/navigation";

export const routing = defineRouting({
  locales: ["en", "ko"],
  defaultLocale: "ko",
  localeCookie: {
    maxAge: 60 * 60 * 24,
  },
});

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

Middleware 설정

위의 예제를 통해 살펴 보았듯이 routing을 사용해 locale 기능을 제공하고자 한다면 middleware 역시 그에 맞게 설정해주어야 한다. Routing 기반 next-intl 사용을 위한 middleware의 기본 설정은 다음과 같다. Locale은 ko.json, en.json 두 가지 언어를 제공한다고 가정한다.

src/middleware.ts

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

export default createMiddleware(routing);

export const config = {
  matcher: ['/', '/(ko|en)/:path*']
};

src/i18n/routing.ts

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

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

위의 예제는 / pathname과 /ko 또는 /en로 시작하는 pathname에 middleware를 적용하는 예제다. 위의 예제에서 살펴볼 수 있듯이 createMiddleware에 routing.ts에서 설정한 routing configuration object를 전달하면 locale 적용에 필요한 기본적인 설정과 redirect 기능을 처리해준다.

Middleware에서 현재 request의 locale을 결정할 때 사용하는 순서는 다음과 같다.

  • pathname에 포함된 locale 정보가 있다면 해당 locale을 사용한다.

  • pathname에 locale 정보가 없다면 NEXT_LOCALE cookie에 설정된 값을 사용한다.

  • NEXT_LOCALE cookie도 없다면 request header의 accept-language header에 존재하는 locale 중 사용 가능한 locale을 사용한다.

  • accept-language header도 없다면 routing 설정 중 defaultLocale property에 설정된 locale을 사용한다.

Composing middleware

createMiddleware function을 실행하면 다음과 같은 function을 return한다.

function middleware(request: NextRequest): NextResponse;

그렇기에 nex-intl createMiddleware를 실행하기 전에 request를 수정하거나 response를 수정하는 등 기타 추가 작업을 해야 할 경우 다음과 같이 middleware를 구성할 수 있다.

import createMiddleware from 'next-intl/middleware';
import {NextRequest} from 'next/server';

export default async function middleware(request: NextRequest) {
  const defaultLocale = request.headers.get('x-my-locale') || 'en';

  const handleI18nRouting = createMiddleware({
    locales: ['en', 'ko'],
    defaultLocale
  });

  const response = handleI18nRouting(request);

  return response;
}

export const config = {
  matcher: ['/', '/(ko|en)/:path*']
};

위의 예제는 request header에 x-my-locale이라는 header가 존재한다면 해당 header의 값을 defaultLocale로 사용하고 그렇지 않으면 en를 default locale로 사용하는 예제다.

물론 기존처럼 routing 설정을 별개로 구성하여 사용해도 무관하다.

src/i18n/routing.ts

import { defineRouting } from "next-intl/routing";
import { createNavigation } from "next-intl/navigation";

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

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

src/middleware.ts

import { routing } from "@/i18n/routing";
import createMiddleware from "next-intl/middleware";
import { NextRequest } from "next/server";

export default async function middleware(request: NextRequest) {
  const handleI18nRouting = createMiddleware(routing);

  const response = handleI18nRouting(request);

  return response;
}

export const config = {
  matcher: ["/((?!api|_next|_vercel|.*\\..*).*)"],
};

Navigation

Next-Intl은 현재 locale을 유지하거나 변경하여 navigation할 수 있도록 Link, userRouter와 같은 NextJS navigation API을 기반으로한 navigation API를 제공한다.

src/i18n/request.ts

import { defineRouting } from "next-intl/routing";
import { createNavigation } from "next-intl/navigation";

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

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

위의 예제와 같이 createNavigation을 실행하면 Link, redirect, userRouter와 같은 function을 return 해준다. Application navigation을 구현할 때 NextJS navigation이 아닌 위의 function을 사용해서 구성한다. function name이 NextJS navigation에서 제공하는 name과 동일하므로 import할 때 주의하자.

import { Link } from "@/i18n/routing";
import React from "react";

const Nav = () => {
  return (
    <div className="flex flex-row space-x-3">
      <Link href="/user">User</Link>
      <Link href="/order">Order</Link>
    </div>
  );
};

export default Nav;

위의 예제처럼 locale prop을 별도로 설정하지 않으면 현재 locale 정보가 유지되며 navigation이 발생하고 아래와 같이 locale prop을 함께 명시해주면 navigation이 발생할 때 해당 locale로 변경되어 이동한다.

import { Link } from "@/i18n/routing";
import React from "react";

const Nav = () => {
  return (
    <div className="flex flex-row space-x-3">
      <Link locale="ko" href="/user">
        User
      </Link>
      <Link locale="en" href="/order">
        Order
      </Link>
    </div>
  );
};

export default Nav;

Navigation link를 통한 navigation이 아닌 programatic하게 navigaton을 수행해야 할 때도 NestJS navigation에 제공하는 userRoute가 아닌 next-intl createNavigation function에서 return된 useRoute를 사용할 수 있다.

"use client";
import { useRouter } from "@/i18n/routing";
import React from "react";

const Order = () => {
  const router = useRouter();
  const handleRouter = () => {
    router.push("/user");
  };
  return (
    <div>
      <div>
        <button onClick={handleRouter}>test button</button>
      </div>
    </div>
  );
};

export default Order;

userRoute를 통해 navigation을 수행할 때 locale을 함께 변경하고 싶다면 다음과 같이 locale property도 함께 전달해준다.

"use client";
import { useRouter } from "@/i18n/routing";
import React from "react";

const Order = () => {
  const router = useRouter();
  const handleRouter = () => {
    router.push("/user", { locale: "en" });
  };
  return (
    <div>
      <div>
        <button onClick={handleRouter}>test button</button>
      </div>
    </div>
  );
};

export default Order;

언어 변경을 위한 가능 역시 다음과 같이 replace method를 통해 쉽게 구현할 수 있다.

"use client";
import { routing, usePathname, useRouter } from "@/i18n/routing";
import React from "react";

const LocaleSwitch = () => {
  const router = useRouter();
  const pathname = usePathname();
  const handleLocaleSwitch = (lang: string) => () => {
    router.replace({ pathname }, { locale: lang });
  };
  return (
    <div className="flex flex-row space-x-2">
      {routing.locales.map((lang) => (
        <button key={lang} onClick={handleLocaleSwitch(lang)}>
          {lang}
        </button>
      ))}
    </div>
  );
};

export default LocaleSwitch;

More from this blog

[ 살펴보기 ] TypeORM - Transactions, Migration

Transation Database 종류에 따라 detail한 부분은 차이점이 조금씩 있겠지만 각 sql statement는 개별적인 transaction block을 통해 실행되며 Database 설정에 따라 sql statement의 실행 결과가 자동으로 commit되어 영구히 적용되거나 commit을 직접 실행하기 전까지는 영구히 적용되지 않을 수 있다. 대부분의 경우 default로 sql statement 실행 결과가 자동으로 comm...

Feb 9, 20256 min read
[ 살펴보기 ] TypeORM - Transactions, Migration

Dev Diary

184 posts