안녕하세요~! 이번 글에서는 Next.js로 마이그레이션을 결정하게 된 계기와 그 방법에 대해 소개해드리려고 합니다.
해당 프로젝트는 회사에 대한 소개 위주의 글로벌 홈페이지로, 처음에는 Vue 3의 CSR(Client-Side Rendering) 방식을 사용해 빠르게 구축되었습니다. 당시 제한된 개발 기간으로 인해 Vue 3라는 프레임워크를 사용하게 되었습나더. Vue3 프레임워크는 빠른 개발을 가능하게 해줬고, 복잡한 기능이 없는 단순한 소개 사이트로는 충분하다고 생각했습니다.
그러나 사이트 운영이 지속되면서 CSR 방식의 몇 가지 한계가 눈에 띄기 시작했습니다:
첫째, 초기 로딩 속도 문제입니다. CSR 특성상 모든 JavaScript 파일이 클라이언트 측에서 실행되다 보니, 첫 화면이 로딩되는 데 시간이 걸렸습니다. 이로 인해 사용자가 첫 화면을 보기까지 기다려야 하는 시간이 길어졌고, 이탈률이 높아지는 문제를 겪게 되었습니다.
둘째, SEO 최적화의 어려움입니다. CSR 방식에서는 초기 HTML이 거의 비어있고, 모든 콘텐츠가 클라이언트 측에서 렌더링되기 때문에 검색 엔진이 페이지를 제대로 인덱싱하지 못했습니다. 그 결과, 검색 엔진 결과에서 우리 사이트의 노출도가 낮아지는 문제가 발생했습니다.
이런 문제들을 해결하기 위해 Next.js 14로의 마이그레이션을 결정했습니다. Next.js는 Server-Side Rendering(SSR)과 Static Site Generation(SSG)을 지원하여 초기 로딩 속도를 크게 개선할 수 있었고, SEO 친화적인 구조로 사이트가 검색 엔진에 더 잘 노출될 수 있도록 해줬습니다. 또한, Next.js 14의 최신 기능인 App Router와 i18next 통합을 통해 더욱 빠르고 유연한 다국어 지원이 가능해졌습니다.
이 글에서는 Next.js로의 전환 과정을 설명드리겠습니다.
Next.js 14로의 전환에서 중요한 부분 중 하나는 다국어 지원이었습니다. 기존 Vue 3 프로젝트에서는 i18n이라는 다른 국제화 라이브러리를 사용했지만, Next.js로 전환하면서 i18next와 react-i18next를 사용하기로 했습니다. i18next는 Next.js의 App Router와 잘 맞물려 작동하며, 서버 사이드와 클라이언트 사이드 모두에서 안정적으로 다국어 처리를 지원해주기 때문에 선택하게 되었습니다.
$ pnpm install react-i18next i18next --save
project-root/
│
├── app/
│ ├── [locale]/
│ │ ├── page.tsx
│ │ └── layout.tsx
│ │
│ └── i18n/
│ ├── locales/
│ │ ├── en / common.json
│ │ └── ko / common.json
│ └── index.ts
│
└── package.json
app/[locale]/page.tsx
이 파일은 각 언어별로 페이지를 구성하는 파일입니다. [locale]은 URL 경로에서 사용되는 언어 코드로, 예를 들어 en이나 ko가 될 수 있습니다. 이 구조 덕분에 URL 경로에 따라 자동으로 해당 언어 페이지를 렌더링할 수 있습니다.
app/i18n/locales/
언어별 번역 파일은 이곳에 위치합니다. 번역 파일은 각각의 언어와 관련된 모든 페이지와 레이아웃에서 사용되며, 다국어 지원의 핵심을 담당합니다.
왜 이런 구조를 선택했을까요?
component 폴더의 구조는 Atomic 디자인패턴을 도입했으나 이 글의 목적과는 맞지 않아 설명은 생략하겠습니다.
웹사이트를 다국어로 제공할 때 사용자 경험을 최적화하기 위해 가장 중요한 요소 중 하나는 사용자의 브라우저 언어를 감지하여 올바른 언어 페이지로 리디렉션하는 것입니다. Next.js 14와 App Router를 사용하여 기본 URL로 접근할 때 사용자의 브라우저 언어를 감지하고, 올바른 언어 페이지로 자동으로 리디렉션하는 방법에 대해서 설명하겠습니다.
저희 팀이 원하는 동작 방식은 다음과 같았습니다.
1. 기본 언어 설정: 기본 언어는 en(영어)로 설정합니다. 모든 페이지는 언어 코드가 포함된 URL로 접근하게 됩니다.
예를 들어:
// app/i18n/i18nConfig.ts
// 지원하는 언어 코드 타입 정의
export type Locale = "en" | "fr" | "th"; // 지원하는 언어 코드 목록
// 설정 타입 정의
interface I18nConfig {
locales: Locale[]; // 지원하는 언어 목록
defaultLocale: Locale; // 기본 언어 설정
}
// 설정 객체
const i18nConfig: I18nConfig = {
locales: ["en", "fr", "th"], // Locale 타입만 허용됨
defaultLocale: "en",
};
export default i18nConfig;
이제 이 i18nConfig를 미들웨어에서 사용하여 동적 라우팅을 처리하도록 설정을 수정합니다. i18nConfig를 import하여 지원 언어와 기본 언어를 간편하게 사용할 수 있게 됩니다.
// middleware.ts
import { NextRequest, NextResponse } from "next/server";
import i18nConfig, { Locale } from "./app/i18n/i18nConfig"; // i18nConfig 파일 import
export function middleware(request: NextRequest) {
// 현재 URL 경로
const { pathname } = request.nextUrl;
// 이미 언어가 경로에 포함된 경우
if (i18nConfig.locales.some((locale) => pathname.startsWith(`/${locale}`))) {
return NextResponse.next();
}
// 쿠키에서 선호하는 언어를 가져옴
const preferredLocale = request.cookies.get("preferredLocale")?.value;
// 브라우저 언어를 감지 (쿠키에 값이 없을 때만 사용)
const browserLanguage = request.headers
.get("accept-language")
?.split(",")[0]
.slice(0, 2);
// 타입 가드를 통해 Locale 타입으로 변환
const isLocale = (lang: string | undefined): lang is Locale =>
lang !== undefined && i18nConfig.locales.includes(lang as Locale);
// 쿠키 언어 > 브라우저 언어 > 기본 언어 순으로 결정
const locale: Locale = isLocale(preferredLocale)
? preferredLocale
: isLocale(browserLanguage)
? (browserLanguage as Locale)
: i18nConfig.defaultLocale;
// 언어에 맞는 경로로 리디렉션
return NextResponse.redirect(new URL(`/${locale}${pathname}`, request.url));
}
export const config = {
matcher: ["/((?!api|_next|static|.*\\..*|favicon.ico).*)"],
};
middleware에서 원하는 동작은 다음과 같았습니다.
1. 언어가 URL에 포함된 경우: 사용자가 이미 /en, /fr 등의 경로로 접근했을 때는 별도의 리디렉션 없이 해당 언어 페이지를 그대로 보여줍니다.
2. 언어가 URL에 포함되지 않은 경우: localhost:3000과 같이 언어 코드가 없는 URL로 접근했을 때:
이번 포스팅에서는 Next.js에서 미들웨어를 활용해 사용자의 언어를 감지하고 적절한 페이지로 리디렉션하는 방법을 다뤄봤습니다. 이를 통해 더 나은 사용자 경험을 제공하고, SEO 성능을 최적화할 수 있는 방법을 확인할 수 있었습니다.
다음 포스팅에서는 클라이언트 컴포넌트와 서버 컴포넌트에서 i18n을 적용하는 방법에 대해 설명하겠습니다. 특히, Next.js의 최신 기능을 활용하여 효율적으로 다국어를 처리하는 방법과 Context API를 이용해 글로벌 상태를 관리하는 방식에 대해 깊이 있게 다룰 예정입니다.