Next 13 미들웨어로 인한 OG Tag 버그 잡기!

개발자 베니·2023년 12월 16일
0
post-thumbnail


안녕한가!

개요

회사 프로젝트를 Next 13으로 만들던 중, 요구 스펙중에 로컬라이제이션이 필요하다는 말을 듣고 적용하면서 발견한 버그를 공유하기 위해 작성하는 글이다. 그런데 진짜 별거 없는 것 같기도 하고 일단 12월이고,, 글을 올리는 건 좋은거니까 그냥 작성해본다.

로컬라이제이션 (Localization)?

Localization이라고도 하고 Internationalization이라고도 하는데 한국어로는 국제화라고 한다. 모든 것들이 그렇듯 단어는 꽤 멋있고 어려워보이지만 사실 별거 아니고 여러 국가의 언어를 설정해주는 기능이다. 기능을 구현하는 방법은 여러가지가 있으나 큰 회사가 주로 사용하는 방법을 쓰는게 일반적인 방법이라고 생각해서 애플의 웹페이지에서 사용하는 방법을 그대로 가져왔다.

애플은 한국어 브라우저로 사이트에 접속하면 아래의 url로 연결이 되고,
https://www.apple.com/kr

영어 브라우저로 접속하면 아래의 url로 연결이 된다.
https://www.apple.com/

다른 나라도 각 나라의 브라우저로 접속하면 url이 바뀌고 그에 따라 사이트 내용이 변하는 것을 볼 수 있다!

너무 당연한 것 아니냐고?

나는 위의 방식을 몰랐었다. 젠장 그래서 global state로 직접 구현했다. 이래서 사람은 검색을 해보고 정보를 찾아봐야 하는구나 라는걸 그때 깨달았다.

Next에서도 그냥 나와있음 ㅋ

ㅋㅋ 사실 공식 페이지에 잘 나와있다. 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 태그 버그 발견

웬일로 문제없이 코딩이 아주 잘 되나 했더니 프로젝트 마무리할 때 쯤 버그가 발견됐다. 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 태그씨.

결론

웹 페이지에 대한 요청은 내가 예상치 못한 경우가 있을 수 있으니, 주의하자.


멋있는 퇴장.

0개의 댓글