트러블슈팅

다국어 적용시 라우팅 관련 트러블슈팅

2023-09-08

7

언어 변경시 쿠키와 라우팅 경로가 불일치하는 현상 해결하기

현재 미들웨어 구현 사항

middleware.ts
ts
1import { NextRequest, NextResponse } from 'next/server';
2import acceptLanguage from 'accept-language';
3import { fallbackLng, languages } from './i18n/settings';
4
5acceptLanguage.languages(languages);
6
7export const config = {
8  matcher: [
9    '/((?!api|_next/static|_next/image|images|fonts|videos|assets|favicon.ico|robots|sitemap|sw.js).*)',
10  ],
11};
12
13const cookieName = 'i18next';
14
15const getLanguageFromRequest = (req: NextRequest): string | null => {
16  const language = req.cookies.has(cookieName)
17    ? req.cookies.get(cookieName)?.value // 1
18    : req.headers.get('Accept-Language'); // 2
19
20  return acceptLanguage.get(language) || null;
21};
22
23export function middleware(req: NextRequest) {
24  const language = getLanguageFromRequest(req) || fallbackLng; // 3
25  const response = NextResponse.next();
26
27  response.cookies.set(cookieName, language); // 4
28
29  const languageInNextUrl = languages.find((loc) =>
30    req.nextUrl.pathname.startsWith(`/${loc}`),
31  );
32
33  if (languageInNextUrl && language !== languageInNextUrl) {
34    response.cookies.set(cookieName, languageInNextUrl);
35  }
36
37  // 5
38  if (
39    !languages.some((loc) => req.nextUrl.pathname.startsWith(`/${loc}`)) &&
40    !req.nextUrl.pathname.startsWith('/_next')
41  ) {
42    return NextResponse.redirect(
43      new URL(`/${language}${req.nextUrl.pathname}`, req.url),
44    );
45  }
46}
  1. '/' 로 접근시 특정 쿠키 값이 존재하면 해당 값에 해당하는 라우팅 세그먼트로 변경
  2. 쿠키가 없다면 req.header accept-language로 설정된 값이 개발자가 설정한 언어 리스트에서 존재한다면 라우팅 세그먼트로 변경
  3. 모두 해당하지 않는다면 개발자가 설정한 fallback 언어로 라우팅 세그먼트로 변경
  4. 위 세가지에 절차를 마친 language 값을 쿠키에 저장
  5. 언어가 아닌 다른 세그먼트 값으로 접근시 language 값을 첫번째 세그먼트에 세팅

위와 같은 middleware 설정시 개발자는 a 태그 또는 Link(next) 태그 그리고 router 사용시 pathname을 일관성있게 작업할 수 있게 된다.

테스트 시나리오

  1. '/' 경로로 접근 > fallback 언어 redirect 정상 작동 (예: '/' -> '/en')
  2. 루트 경로('/en') > 언어변경 정상 작동 (예: '/en' -> '/th')
  3. 루트 경로 > about 페이지 이동 정상 작동 (예: '/th' > '/th/about')
  4. about 페이지 > 언어 변경 > 변경된 언어의 루트 이동 정상 작동 (예: /{변경 언어})
  5. 루트 > about 페이지 이동 쿠키는 정상 작동, routing 오작동

i18n_language_selector_trouble_3

원인 파악

원인은 다름 아닌 NextJS 캐싱이 원인이였다.

현재 동작에서는 언어 설정 변경시 next/navigation의 router.push를 통해 라우팅을 변경해줬다.

next의 앱 라우터는 라우팅 탐색에 대해 하이브리드 접근 방식을 사용하고 있다.

  • 서버 - 라우트 세그먼트별로 자동으로 애플리케이션 코드가 분할된다.
  • 클라이언트 - Next.js는 라우트 세그먼트를 미리 가져오고 캐시한다.

이렇게 하는 이유는 사용자가 새 경로로 이동할 때 브라우저가 페이지를 다시 로드하지 않고 변경된 경로 세그먼트만 다시 렌더링하여 사용자 경험과 성능을 개선하기 위해서라고 한다. 참고

해결 방안

언어 변경시 루트 경로로 라우팅 처리를 해주고 있다.

현재 라우팅 세그먼트를 유지할 필요가 없다면, window.location.href를 변경해서 브라우저 주소 값을 변경해서 페이지를 처음부터 다시 로드해도 될 것 같다.

개발자도구 > 네트워크 탭을 통해 원인이 맞는지 확인해보자.

  • router.push를 통해 라우팅 했을 때

i18n_language_selector_trouble_4

  • window.location.href를 통해 라우팅 했을 때

i18n_language_selector_trouble_5

(변경) next-i18next>next-intl

이유 요구 사항의 변동

변경사항

  • (기존) '/' 접근시 fallback language에 맞춰 라우팅 변경
  • (변경) '/' 접근시 라우팅 변경없이 fallback language가 적용되야함

프로젝트 구조

📦root
┣ 📂app
┃ ┗ 📂[language]
┃ ┃ ┣ 📜page.tsx
┃ ┃ ┗ 📜layout.tsx

next 특성상 dynamic 라우팅으로 묶여있어 fallback language 전용으로 따로 root의 page를 만들어 적용시킬 수 있지만 이렇게 된다면 같은 내용의 2벌의 파일이 생겨나게 된다.

이런 경우 쉽게 해결해줄 수 있는 미들웨어를 제공해주는 라이브러리인 next-intl을 사용하기로 했다.(참고)

next-intl

  • 2가지 미들웨어 전략 존재
    • prefix 기반의 라우팅
    • 도메인 기반의 라우팅

prefix 기반의 라우팅

  • [language] 폴더 안에 중첩되어 있지만 기본 로케일에 대한 요청은 접두사 없이 내부적으로 작성됨
    • / > /en
    • /about > /en/about
    • /de/about > /de/about
  • accept-language 헤더 기반 locale 감지 > 쿠키에 해당 locale 저장 > 해당 locale에 맞게 redirection

도메인 기반의 라우팅

  • 요청이 가능한 도메인과 일치시키기 위해 host는 x-forwarded-host 헤더를 기반으로 감지하며 host로 대체된다.
  • 로케일은 경로에 로케일 접두어 감지 > 요청에 호스트가 domains에 구성되어 있다면 해당 도메인의 defaultLocale이 사용 > 없다면 접두사 기반 라우팅의 로케일 감지가 적용

요구사항에 맞춰 next-intl의 prefix 기반의 라우팅 전략을 활용했다.

next-intl 설정

서버 컴포넌트 설정

next.config.js
js
1const withNextIntl = require('next-intl/plugin')('./src/i18n.ts');
i18n.ts
ts
1import { getRequestConfig } from 'next-intl/server';
2
3export default getRequestConfig(async ({ locale }) => ({
4  messages: (await import(`./i18n/locales/${locale}.json`)).default,
5}));

미들웨어 설정

i18n/config.ts
ts
1export const defaultLocale = 'en';
2
3export const locales = [defaultLocale, 'pt', 'zh-hans', 'zh-hant', 'th'];
middleware.ts
ts
1import createMiddleware from 'next-intl/middleware';
2import { defaultLocale, locales } from './i18n/config';
3
4export const config = {
5  matcher: [
6    '/((?!api|_next/static|_next/image|images|fonts|videos|assets|favicon.ico|robots|sitemap|sw.js).*)',
7  ],
8};
9
10export default createMiddleware({
11  locales,
12  defaultLocale,
13  localeDetection: false,
14});

결과 화면

i18n_language_selector_trouble_6

See more "트러블슈팅"