안녕한가!
회사 프로젝트를 Next 13으로 만들던 중, 요구 스펙중에 로컬라이제이션이 필요하다는 말을 듣고 적용하면서 발견한 버그를 공유하기 위해 작성하는 글이다. 그런데 진짜 별거 없는 것 같기도 하고 일단 12월이고,, 글을 올리는 건 좋은거니까 그냥 작성해본다.
Localization이라고도 하고 Internationalization이라고도 하는데 한국어로는 국제화라고 한다. 모든 것들이 그렇듯 단어는 꽤 멋있고 어려워보이지만 사실 별거 아니고 여러 국가의 언어를 설정해주는 기능이다. 기능을 구현하는 방법은 여러가지가 있으나 큰 회사가 주로 사용하는 방법을 쓰는게 일반적인 방법이라고 생각해서 애플의 웹페이지에서 사용하는 방법을 그대로 가져왔다.
애플은 한국어 브라우저로 사이트에 접속하면 아래의 url로 연결이 되고,
https://www.apple.com/kr
영어 브라우저로 접속하면 아래의 url로 연결이 된다.
https://www.apple.com/
다른 나라도 각 나라의 브라우저로 접속하면 url이 바뀌고 그에 따라 사이트 내용이 변하는 것을 볼 수 있다!
너무 당연한 것 아니냐고?
나는 위의 방식을 몰랐었다. 젠장 그래서 global state로 직접 구현했다. 이래서 사람은 검색을 해보고 정보를 찾아봐야 하는구나 라는걸 그때 깨달았다.
ㅋㅋ 사실 공식 페이지에 잘 나와있다. Next는 참 문서화가 잘 되어있는 것 같다.
Nextjs Internationalization
위의 사이트에 내용을 참고해서 코드를 작성하면 아래와 같이 된다.
/* /i18n.config.ts */
export const i18n = {
defaultLocale: "ko",
locales: ["ko", "en"],
} as const;
export type Locale = (typeof i18n)["locales"][number];
/* /src/middleware.ts */
import Negotiator from 'negotiator'
import { NextRequest, NextResponse } from 'next/server'
import { match } from '@formatjs/intl-localematcher'
import { i18n } from '../i18n.config'
function getLocale(request: NextRequest): string | undefined {
const negotiatorHeaders: Record<string, string> = {}
request.headers.forEach((value, key) => {
negotiatorHeaders[key] = value
})
// @ts-ignore locales are readonly
const locales: string[] = i18n.locales
const languages = new Negotiator({ headers: negotiatorHeaders }).languages()
const locale = match(languages, locales, i18n.defaultLocale)
return locale
}
export function middleware(request: NextRequest) {
const pathname = request.nextUrl.pathname
const search = request.nextUrl.search
if (pathname === '/' || pathname === '/ko' || pathname === '/en') {
const locale = getLocale(request)
return NextResponse.redirect(new URL(`/${locale}/main${search}`, request.url))
}
const pathnameIsMissingLocale = i18n.locales.every((locale) => !pathname.startsWith(`/${locale}/`) && pathname !== `/${locale}`)
// Redirect if there is no locale
if (pathnameIsMissingLocale) {
const locale = getLocale(request)
return NextResponse.redirect(new URL(`/${locale}${pathname.startsWith('/') ? '' : '/'}${pathname}`, request.url))
}
}
export const config = {
// Matcher ignoring `/_next/` and `/api/`
matcher: '/((?!api|static|locales|.*\\..*|_next).*)'
}
root 경로에 i18n.config.ts를 만들고 처리할 언어값을 설정한다.
getLocale은 내 브라우저가 한국어인지 영어인지 또 다른 국가의 언어인지를 알려주는 함수고, 미들웨어는 그 locale 데이터를 기반으로 내 url에 어떤 언어값을 붙여서 나갈지를 결정해준다. 위치는 src 폴더에 바로 올리면 된다.
웬일로 문제없이 코딩이 아주 잘 되나 했더니 프로젝트 마무리할 때 쯤 버그가 발견됐다. OG 태그 내용을 작성하고 잘 되는가 확인하려고 Opengraph 디버거를 통해서 확인을 해봤는데 모든 페이지가 잘 나오는데, 이상하게 루트 도메인을 테스트하면 500 에러가 나오는 것이다.
당황함.
암튼 버그는 잡아야하니까 얼른 분석을 시작해봤다. 우선 Opengragh에서 내 사이트에 요청을 할 때는 서버에서 요청을 먼저 할테니까, 나도 한번 내 사이트에 curl로 요청을 해봤다.
{
"props": { "pageProps": { "statusCode": 500 } },
"page": "/_error",
"query": {},
"buildId": "development",
"isFallback": false,
"err": {
"name": "RangeError",
"source": "edge-server",
"message": "Incorrect locale information provided",
"stack": "RangeError: Incorrect locale information provided\n...
},
"gip": true,
"scriptLoader": []
}
쓰윽 훑어보니 이러한 에러가 나왔다.
에러는 RangeError 500 에러이고, 내용은 제공된 locale의 정보가 이상하다는 것이다.
curl localhost:4000을 요청했을 때, header에 locale 데이터를 명시해주지 않아서 그런가 해서
curl -H "Accept-Language: en-US,en;q=0.5" localhost:4000
를 해줬더니, 문제 없이 잘 되었다.
그렇다는것은!!
내 코드에는 언어값이 명시되어있지 않은 요청에 대한 처리가 되어있지 않다는 뜻.
따라서 즉시 코드 수정을 한다.
import Negotiator from 'negotiator'
import { NextRequest, NextResponse } from 'next/server'
import { match } from '@formatjs/intl-localematcher'
import { i18n } from '../i18n.config'
function getLocale(request: NextRequest): string | undefined {
const negotiatorHeaders: Record<string, string> = {}
request.headers.forEach((value, key) => {
if (key === 'accept-language' && value === '*') {
value = 'ko-KR,ko;q=0.9'
}
negotiatorHeaders[key] = value
})
// @ts-ignore locales are readonly
const locales: string[] = i18n.locales
const languages = new Negotiator({ headers: negotiatorHeaders }).languages()
const locale = match(languages, locales, i18n.defaultLocale)
return locale
}
export function middleware(request: NextRequest) {
const pathname = request.nextUrl.pathname
const search = request.nextUrl.search
if (pathname === '/' || pathname === '/ko' || pathname === '/en') {
const locale = getLocale(request)
return NextResponse.redirect(new URL(`/${locale}/main${search}`, request.url))
}
const pathnameIsMissingLocale = i18n.locales.every((locale) => !pathname.startsWith(`/${locale}/`) && pathname !== `/${locale}`)
// Redirect if there is no locale
if (pathnameIsMissingLocale) {
const locale = getLocale(request)
return NextResponse.redirect(new URL(`/${locale}${pathname.startsWith('/') ? '' : '/'}${pathname}`, request.url))
}
}
export const config = {
// Matcher ignoring `/_next/` and `/api/`
matcher: '/((?!api|static|locales|.*\\..*|_next).*)'
}
request의 헤더를 체크하는 부분에서 만약 accept-language의 값이 *로 되어있다면, 한국어로 고정해서 헤더를 설정해주었다.
이제는 잘 넘어와주는 OG 태그씨.
웹 페이지에 대한 요청은 내가 예상치 못한 경우가 있을 수 있으니, 주의하자.
멋있는 퇴장.