[ 살펴보기 ] Next-Intl - Routing, Middleware
![[ 살펴보기 ] Next-Intl - Routing, Middleware](https://cdn.hashnode.com/res/hashnode/image/upload/v1734503676119/6d2a0994-59dc-41df-a725-8e3a01b645f6.jpeg)
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를 수정해주어야 한다.
Locale cookie
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_LOCALEcookie에 설정된 값을 사용한다.NEXT_LOCALEcookie도 없다면 request header의accept-languageheader에 존재하는 locale 중 사용 가능한 locale을 사용한다.accept-languageheader도 없다면 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;
![[ 살펴보기 ] RDB - Relationships](https://cdn.hashnode.com/res/hashnode/image/upload/v1739711556668/48dc9e84-a621-42aa-9c9f-5fc5c436f0ec.jpeg)
![[ 살펴보기 ] MySQL - Data types](https://cdn.hashnode.com/res/hashnode/image/upload/v1739593589113/530f8704-4d27-42c9-a451-bb5c63150b99.jpeg)
![[ 살펴보기 ] TypeORM - Transactions, Migration](https://cdn.hashnode.com/res/hashnode/image/upload/v1739106042581/980b8133-61d4-406a-a026-65be9c28eace.jpeg)
![[ 살펴보기 ] TypeORM - Relations](https://cdn.hashnode.com/res/hashnode/image/upload/v1738666874402/b688bd0b-b6bb-4f43-87d8-c1b46b59f1b7.jpeg)
![[ 살펴보기 ] TypeORM - Basics](https://cdn.hashnode.com/res/hashnode/image/upload/v1738666803591/bef5df17-7dc7-4123-ae55-004d5042df39.jpeg)