Sooyoung Archive
admin-starter-kit 만들기 (2)
Next.js 16 + Supabase로 어드민 대시보드 만들기 (2)
– i18n 구현과 Hydration 에러 해결 로그
0. 이전 글 요약
1탄에서는 Next.js 16 App Router 환경에서의 기본적인 세팅과 Supabase 연동 시 발생했던 타입 에러들을 해결했다. 이번 글에서는 그 이후 단계인 다국어(i18n) 구현과 그 과정에서 만난 Hydration 에러를 해결하는 과정을 정리한다.
1. i18n 라이브러리 선택과 초기 설정
다국어 지원을 위해 react-i18next를 선택했다. Next.js 16 App Router 환경에서는 클라이언트/서버 컴포넌트 구분이 명확해야 하기 때문에, 초기 설정부터 이 구분을 고려해야 했다.
1-1. i18n 설정 파일 구조
src/
├── lib/
│ └── i18n.ts # 번역 데이터와 설정
├── app/
│ └── i18n-client.tsx # 클라이언트용 i18n 프로바이더
1-2. 초기 i18n 설정 코드
// src/lib/i18n.ts
import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';
const resources = {
ko: {
translation: {
// 한국어 번역
loginTitle: "관리자 로그인",
dashboardTitle: "대시보드",
// ...
}
},
en: {
translation: {
// 영어 번역
loginTitle: "Admin Login",
dashboardTitle: "Dashboard",
// ...
}
}
};
i18n
.use(initReactI18next)
.init({
resources,
lng: 'ko',
fallbackLng: 'ko',
});
2. 첫 번째 에러: 서버 컴포넌트에서의 useTranslation 훅 사용
가장 먼저 로그인 페이지에 i18n을 적용하려고 시도했다.
2-1. 문제의 코드
// src/app/(auth)/login/page.tsx
import { useTranslation } from 'react-i18next';
export default function LoginPage() {
const { t } = useTranslation(); // ❌ 에러 발생
return (
<div>
<h1>{t("loginTitle")}</h1>
{/* ... */}
</div>
);
}
Next.js가 알려준 에러:
You're importing a component that needs useTranslation, which requires React Client Component. Add "use client" directive.
2-2. 에러 원인 분석
useTranslation은 React 훅이라서 클라이언트 컴포넌트에서만 사용 가능- 하지만
src/app/(auth)/login/page.tsx는 기본적으로 서버 컴포넌트 - "서버 컴포넌트 + React 훅" 조합이 문제였다
2-3. 해결 방법
두 가지 해결책이 있었지만, 로그인 폼은 클라이언트 상태 관리가 필요하므로 클라이언트 컴포넌트로 전환했다:
// src/app/(auth)/login/page.tsx
"use client"; // ✅ 클라이언트 컴포넌트로 전환
import { useTranslation } from 'react-i18next';
export default function LoginPage() {
const { t } = useTranslation(); // ✅ 정상 작동
return (
<div>
<h1>{t("loginTitle")}</h1>
{/* ... */}
</div>
);
}
3. 두 번째 에러: 랜딩 페이지의 Hydration 에러
랜딩 페이지에도 i18n을 적용했지만, 이번에는 더 복잡한 문제가 발생했다.
3-1. Hydration 에러 증상
// src/app/(marketing)/page.tsx
"use client";
import { useTranslation } from 'react-i18next';
export default function MarketingPage() {
const { t } = useTranslation();
return (
<div>
<h1>{t("heroTitle")}</h1> // ❌ Hydration 에러
</div>
);
}
콘솔 에러:
Hydration failed because the initial UI does not match what was rendered on the server.
3-2. 에러 원인 분석
- 서버 사이드 렌더링 시점: i18n이 초기화되지 않은 상태 → 기본값(영어) 렌더링
- 클라이언트 사이드 하이드레이션 시점: i18n이 초기화된 상태 → 한국어 렌더링
- 서버와 클라이언트의 렌더링 결과가 달라 Hydration 에러 발생
3-3. 해결 방법 1: 로딩 상태 추가
가장 간단한 해결책은 로딩 상태를 추가하는 것이었다:
// src/app/(marketing)/page.tsx
"use client";
import { useState, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
export default function MarketingPage() {
const { t } = useTranslation();
const [isClient, setIsClient] = useState(false);
useEffect(() => {
setIsClient(true);
}, []);
if (!isClient) {
return <div>Loading...</div>; // ✅ 서버와 클라이언트 동기화
}
return (
<div>
<h1>{t("heroTitle")}</h1> // ✅ 정상 작동
</div>
);
}
3-4. 해결 방법 2: 동적 임포트 (더 나은 방법)
더 나은 성능을 위해 동적 임포트를 사용하는 방법도 있다:
// src/app/(marketing)/page.tsx
import dynamic from 'next/dynamic';
const MarketingPageContent = dynamic(() => import('./marketing-page-content'), {
ssr: false, // ✅ SSR 비활성화로 Hydration 문제 회피
});
export default function MarketingPage() {
return <MarketingPageContent />;
}
4. 세 번째 에러: 사이드바의 언어 전환 문제
사이드바는 서버 컴포넌트로 유지하고 싶었지만, 언어 전환 기능이 필요했다.
4-1. 문제 상황
// src/components/layout/sidebar.tsx (서버 컴포넌트)
export default function Sidebar() {
// ❌ useTranslation 사용 불가
return (
<nav>
<Link href="/dashboard">{t("dashboardNav")}</Link>
<Link href="/users">{t("users")}</Link>
</nav>
);
}
4-2. 해결 전략: 클라이언트 컴포넌트 분리
사이드바를 두 개로 분리했다:
// src/components/layout/sidebar.tsx (서버 컴포넌트 - 기본 구조)
export default function Sidebar() {
return (
<div className="sidebar-container">
<SidebarClient /> {/* ✅ 클라이언트 컴포넌트로 위임 */}
</div>
);
}
// src/components/layout/sidebar-client.tsx (클라이언트 컴포넌트 - i18n 담당)
"use client";
import { useTranslation } from 'react-i18next';
export function SidebarClient() {
const { t } = useTranslation();
return (
<nav>
<Link href="/dashboard">{t("dashboardNav")}</Link>
<Link href="/users">{t("users")}</Link>
</nav>
);
}
5. 네 번째 에러: 데이터 테이블의 undefined 상태 에러
결제 내역 페이지에서 런타임 에러가 발생했다.
5-1. 에러 증상
// src/app/dashboard/payments/columns.tsx
const statusConfig = {
completed: { label: "완료", color: "green" },
pending: { label: "대기", color: "yellow" }
};
// ❌ status가 'failed'일 때 undefined 에러
const config = statusConfig[status];
return <Badge color={config.color}>{config.label}</Badge>;
에러 메시지:
TypeError: Cannot read properties of undefined (reading 'color')
5-2. 원인 분석
- 데이터베이스에 예상치 못한 상태값('failed', 'cancelled' 등)이 존재
statusConfig객체에 해당 키가 없어undefined반환undefined.color접근 시 런타임 에러 발생
5-3. 해결 방법: 폴백(fallback) 처리
// src/app/dashboard/payments/columns.tsx
const statusConfig = {
completed: { label: "완료", color: "green" },
pending: { label: "대기", color: "yellow" }
};
// ✅ 폴백 처리 추가
const config = statusConfig[status] || {
label: "알 수 없음",
color: "gray"
};
return <Badge color={config.color}>{config.label}</Badge>;
6. 다섯 번째 에러: 페이지네이션 번역 누락
데이터 테이블 컴포넌트에 하드코딩된 한국어 텍스트가 있었다.
6-1. 문제 코드
// src/components/ui/data-table.tsx
<div className="text-sm text-muted-foreground">
페이지 {table.getState().pagination.pageIndex + 1} / {table.getPageCount()}
</div>
<Button>{t("previous")}</Button>
<Button>{t("next")}</Button>
6-2. 해결: 모든 텍스트 번역 적용
// src/components/ui/data-table.tsx
"use client"; // ✅ 클라이언트 컴포넌트로 전환
import { useTranslation } from 'react-i18next';
export function DataTable() {
const { t } = useTranslation();
return (
<div>
<div className="text-sm text-muted-foreground">
{t("page")} {table.getState().pagination.pageIndex + 1} / {table.getPageCount()}
</div>
<Button>{t("previous")}</Button>
<Button>{t("next")}</Button>
</div>
);
}
7. 마지막 문제: 랜딩페이지의 하드코딩된 숫자
랜딩페이지의 통계 수치가 언어 전환에 따라 바뀌지 않았다.
7-1. 문제 코드
// src/app/(marketing)/page.tsx
<div className="text-center">
<div className="text-2xl font-bold text-primary">10분</div> {/* ❌ 하드코딩 */}
<div className="text-sm text-muted-foreground">{t("installTime")}</div>
</div>
7-2. 해결: 번역 키 분리
// 한국어
setupTimeValue: "10분",
// 영어
setupTimeValue: "10 min",
// 컴포넌트
<div className="text-center">
<div className="text-2xl font-bold text-primary">{t("setupTimeValue")}</div>
<div className="text-sm text-muted-foreground">{t("installTime")}</div>
</div>
8. 최종 결과와 학습점
이번 작업을 통해 완성된 기능들:
8-1. 완성된 i18n 기능
- ✅ 랜딩페이지 전체 다국어 지원
- ✅ 로그인 페이지 다국어 지원
- ✅ 대시보드 사이드바 언어 전환
- ✅ 데이터 테이블 페이지네이션 다국어
- ✅ 랜딩페이지 i18n 데모 기능 (방문자 직접 체험 가능)
8-2. 주요 학습점
- 클라이언트/서버 컴포넌트 구분: React 훅은 반드시 클라이언트 컴포넌트에서 사용
- Hydration 문제: 서버/클라이언트 렌더링 불일치는 로딩 상태나 동적 임포트로 해결
- 에러 핸들링: 예상치 못한 데이터는 폴백(fallback) 처리로 안정성 확보
- 번역 관리: 모든 텍스트는 번역 키로 관리, 하드코딩 지양
9. 다음 단계 예고
이제 완벽한 다국어 지원이 구현된 어드민 대시보드가 완성되었다. 다음 글에서는:
- 배포 자동화 설정
- 성능 최적화
- 추가 기능 구현 (실시간 알림, 다크모드 등) 을 다룰 예정이다.