배경
사실 다국어 언어 메뉴를 만들면서 초기구상은 nexti18n 을 사용하기로 했지만, 배움의 부담에 못이겨 하드코딩으로 진행했다. 그러다보니 유지보수는 물론,, 가독성이 완전 제로제로제로였다. 그래서 이를 잘 관리하기 위해 기존 기획했던 nexti18n 을 제대로 적용해보려고 한다.
처음부터 잘못됐던
나는 초반에 nexti18n 라이브러리를 적용하기 위해 설치를 했으나 구글링을 통해 제대로 확인해보니 Next13/14 App 기반 환경은 기존 라이브러리를 지원하지 못한다고 한다!?!
대충 app 에서는 지원하지 않는다는 의미
대신 i18next, react-i18next, i18next-resources-to-backend 라이브러리를 사용해 구성하라고 한다.
npm install i18next react-i18next i18next-resources-to-backend
https://locize.com/blog/next-app-dir-i18n/
해당 블로그를 따라하라고 하길래 보면서 열심히 따라쳤다. 하지만,, 2년전 자료를 그 때 깨닫고 하지 말았어야 했는데..
결론은 언어를 동적 링크를 생성해 주소를 호출하는 과정속에서 알 수 없는 404 에러가 계속 나와 다시 next i18n 에 대해 검색을 진행했다.
돌아도 한참 돌아갔던
조금더 검색을 해보니 next-intl 이라는 라이브러리가 존재한다는것을 알게되었다..! 아뿔싸 나의 내다버린 3시간.. 하지만 개발이란 원래 이런것..
설치과정을 잠깐 보자면
설치과정
0. 상황
├── messages (1)
│ ├── en.json
│ └── ...
├── next.config.mjs (2)
└── src
├── i18n.ts (3)
├── middleware.ts (4)
└── app
└── [locale]
├── layout.tsx (5)
└── page.tsx (6)
샘플 폴더 구조는 다음과 같다.
1. 라이브러리를 설치한다.
npm install next-intl
2. 파일 셋업하기
1) messages/en.json
{ "Index": { "title": "Hello world!" }}
- 각 언어마다 json 파일 형식으로 글에 들어갈 내용을 작성한다.
2) next.config.mjs
import createNextIntlPlugin from 'next-intl/plugin';
const withNextIntl = createNextIntlPlugin();
/** @type {import('next').NextConfig} */
const nextConfig = {};
export default withNextIntl(nextConfig);
- next-intl 을 사용하기위한 플러그인을 설정하는 코드다.
3) i18n.ts
import {notFound} from 'next/navigation';
import {getRequestConfig} from 'next-intl/server';
// Can be imported from a shared config
const locales = ['en', 'ko'];
export default getRequestConfig(async ({locale}) => {
// Validate that the incoming `locale` parameter is valid
if (!locales.includes(locale as any)) notFound();
return {
messages: (await import(`../messages/${locale}.json`)).default
};
});
- next-intl은 요청 범위의 구성 객체를 생성하여 사용자의 언어에 따라 메시지 및 기타 옵션을 제공할 수 있으며, 이를 서버 컴포넌트에서 사용할 수 있습니다.
4) middleware.ts
import createMiddleware from 'next-intl/middleware';
export default createMiddleware({
// A list of all locales that are supported
locales: ['en', 'ko'],
// Used when no locale matches
defaultLocale: 'en'
});
export const config = {
// Match only internationalized pathnames
matcher: ['/', '/(ko|en)/:path*']
};
- middleware 에서는 요청에 따른 언어가 매칭되는지 확인하고 리다이렉트하거나 리라이트 하는 역할을 한다.
5) app/[locale]/layout.tsx
import {NextIntlClientProvider} from 'next-intl';
import {getMessages} from 'next-intl/server';
export default async function LocaleLayout({
children,
params: {locale}
}: {
children: React.ReactNode;
params: {locale: string};
}) {
// Providing all messages to the client
// side is the easiest way to get started
const messages = await getMessages();
return (
<html lang={locale}>
<body>
<NextIntlClientProvider messages={messages}>
{children}
</NextIntlClientProvider>
</body>
</html>
);
}
6) app/[locale]/page.tsx
import {useTranslations} from 'next-intl';
export default function Index() {
const t = useTranslations('Index');
return <h1>{t('title')}</h1>;
}
- [locale] 이라는 파일 명을 통해 미들웨어에서 매칭된 언어인 경우 주소값을 가질 수 있다.
결과
테스트 성공!
기존 웹사이트에 적용하기
이제 next-intl 을 적용되었으니 기존 사이트에 추가해야한다. locale (언어 변수) 를 전역으로 관리해야한다는 아이디어가 생겨 상태를 관리할 수 있는 LanguageContext.tsx 를 만들었다.
// context/LanguageContext.tsx
"use client"
import { createContext, useContext, useState, ReactNode } from 'react';
import { Language } from '../types';
interface LanguageContextProps {
selectedLanguage: Language;
setSelectedLanguage: (language: Language) => void;
}
const LanguageContext = createContext<LanguageContextProps | undefined>(undefined);
interface LanguageProviderProps {
children: ReactNode;
initialLocale: Language;
}
export const LanguageProvider: React.FC<LanguageProviderProps> = ({ children, initialLocale }) => {
const [selectedLanguage, setSelectedLanguage] = useState<Language>(initialLocale);
return (
<LanguageContext.Provider value={{ selectedLanguage, setSelectedLanguage }}>
{children}
</LanguageContext.Provider>
);
};
export const useLanguage = (): LanguageContextProps => {
const context = useContext(LanguageContext);
if (context === undefined) {
throw new Error('useLanguage must be used within a LanguageProvider');
}
return context;
};
로직
1. layout.tsx 에서 locale 을 다룰 수 있는 Provider를 만들어 감싼다.
<html lang={locale}>
<body>
<NextIntlClientProvider messages={messages}>
<LanguageProvider initialLocale={locale}>
{children}
</LanguageProvider>
<div id="global-modal"></div>
<div id="howtoeat-modal"></div>
</NextIntlClientProvider>
</body>
</html>
이렇게 감싸게 되면 LanguageProvider 에 저장된 selectedLanguage 변수로 locale 을 children 에 전달하는 방법을 통해 언어 변수를 관리할 수 있다.
2. LanguageProvider selectedLanguage
export const LanguageProvider: React.FC<LanguageProviderProps> = ({ children, initialLocale }) => {
const [selectedLanguage, setSelectedLanguage] = useState<Language>(initialLocale);
return (
<LanguageContext.Provider value={{ selectedLanguage, setSelectedLanguage }}>
{children}
</LanguageContext.Provider>
);
};
여기서 저장된 selectedLanguage 는 다음 함수를 호출함으로 외부에서 사용할 수 있다.
3. useLanguage
export const useLanguage = (): LanguageContextProps => {
const context = useContext(LanguageContext);
if (context === undefined) {
throw new Error('useLanguage must be used within a LanguageProvider');
}
return context;
};
const {selectedLanguage, setSelectedLanguage} = useLanguage();
const router = useRouter();
const pathname = usePathname();
useEffect(() => {
const currentLocale = pathname.split('/')[1] as Language;
if (currentLocale && ["ko", "en", "ja", "th", "ch"].includes(currentLocale)) {
setSelectedLanguage(currentLocale);
} else {
console.error(`Invalid language in path: ${currentLocale}`);
}
}, [pathname, setSelectedLanguage]);
위와 같이 page.tsx 에서 selectedLanguage 변수에 useLanguage() 로 context 를 전달한다. 이렇게 만든다면 드롭다운을 통해 언어를 선택하고 , 선택된 언어는 Context 로 관리를 할 수 있다.
다음 할 일
이제 복잡한 건 끝났고, 언어별 json 파일을 만들어서 좀 더 보기좋은 코드로 만드는 일만 남았다. 상당히 귀찮고 복잡한 일일 것으로 예상되지만 이것 역시 내가 극복 해야할일...
또 생각이 든건데 LanguageContext가 아닌 redux 를 사용해도 뭔가 될 것 같은 기분이다. redux에 대해 제대로 배우질 않아서 적용하지 않았는데 나중에 적용해봐도 될 것같다.
'Project > [Next] 열정도쭈꾸미 외국인 메뉴판' 카테고리의 다른 글
[7] 열정도 쭈꾸미, vercel analytics 를 적용해보자 (0) | 2024.08.18 |
---|---|
[5] 열정도 쭈꾸미, Carousel 을 적용해보자 (2) | 2024.07.10 |
[4] 열정도 쭈꾸미, 최적화를 해보자 (1) | 2024.06.21 |
[3] 열정도 쭈꾸미, 맛있게 먹는 법을 만들어보자 (0) | 2024.06.14 |
[2] 열정도 쭈꾸미, 메뉴판 제작 2 (1) | 2024.06.02 |