수영장(Sooyoung Archive)
홈개발일지이번캠App소개
전체보기
프로젝트
블로그 만들기그림톡캠핑 인스타
개발

© 2026 수영장 (Sooyoung Archive). All rights reserved.

개인정보처리방침연락처
개발일지 목록
개발

admin-starter-kit 만들기 (2)

2026년 2월 4일약 17분 소요45회 조회
목차
– i18n 구현과 Hydration 에러 해결 로그0. 이전 글 요약1. i18n 라이브러리 선택과 초기 설정2. 첫 번째 에러: 서버 컴포넌트에서의 useTranslation 훅 사용3. 두 번째 에러: 랜딩 페이지의 Hydration 에러4. 세 번째 에러: 사이드바의 언어 전환 문제5. 네 번째 에러: 데이터 테이블의 undefined 상태 에러6. 다섯 번째 에러: 페이지네이션 번역 누락7. 마지막 문제: 랜딩페이지의 하드코딩된 숫자8. 최종 결과와 학습점9. 다음 단계 예고

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 설정 파일 구조

code
src/
├── lib/
│   └── i18n.ts           # 번역 데이터와 설정
├── app/
│   └── i18n-client.tsx   # 클라이언트용 i18n 프로바이더

1-2. 초기 i18n 설정 코드

typescript
// 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. 문제의 코드

typescript
// 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가 알려준 에러:

code
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. 해결 방법

두 가지 해결책이 있었지만, 로그인 폼은 클라이언트 상태 관리가 필요하므로 클라이언트 컴포넌트로 전환했다:

typescript
// 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 에러 증상

typescript
// 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>
  );
}

콘솔 에러:

code
Hydration failed because the initial UI does not match what was rendered on the server.

3-2. 에러 원인 분석

  • 서버 사이드 렌더링 시점: i18n이 초기화되지 않은 상태 → 기본값(영어) 렌더링
  • 클라이언트 사이드 하이드레이션 시점: i18n이 초기화된 상태 → 한국어 렌더링
  • 서버와 클라이언트의 렌더링 결과가 달라 Hydration 에러 발생

3-3. 해결 방법 1: 로딩 상태 추가

가장 간단한 해결책은 로딩 상태를 추가하는 것이었다:

typescript
// 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: 동적 임포트 (더 나은 방법)

더 나은 성능을 위해 동적 임포트를 사용하는 방법도 있다:

typescript
// 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. 문제 상황

typescript
// 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. 해결 전략: 클라이언트 컴포넌트 분리

사이드바를 두 개로 분리했다:

typescript
// 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. 에러 증상

typescript
// 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>;

에러 메시지:

code
TypeError: Cannot read properties of undefined (reading 'color')

5-2. 원인 분석

  • 데이터베이스에 예상치 못한 상태값('failed', 'cancelled' 등)이 존재
  • statusConfig 객체에 해당 키가 없어 undefined 반환
  • undefined.color 접근 시 런타임 에러 발생

5-3. 해결 방법: 폴백(fallback) 처리

typescript
// 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. 문제 코드

typescript
// 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. 해결: 모든 텍스트 번역 적용

typescript
// 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. 문제 코드

typescript
// 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. 해결: 번역 키 분리

typescript
// 한국어
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. 주요 학습점

  1. 클라이언트/서버 컴포넌트 구분: React 훅은 반드시 클라이언트 컴포넌트에서 사용
  2. Hydration 문제: 서버/클라이언트 렌더링 불일치는 로딩 상태나 동적 임포트로 해결
  3. 에러 핸들링: 예상치 못한 데이터는 폴백(fallback) 처리로 안정성 확보
  4. 번역 관리: 모든 텍스트는 번역 키로 관리, 하드코딩 지양

9. 다음 단계 예고

이제 완벽한 다국어 지원이 구현된 어드민 대시보드가 완성되었다. 다음 글에서는:

  • 배포 자동화 설정
  • 성능 최적화
  • 추가 기능 구현 (실시간 알림, 다크모드 등) 을 다룰 예정이다.

클릭할 때마다 작가에게 따뜻한 응원이 전달됩니다 ❤️

이 글이 유익하셨나요?
동료 개발자들과 경험과 노하우를 공유해 보세요.
이전 글개발
admin-starter-kit 만들기 (1)
개발다음 글
삼문판결 만들기 1편: '좋은 말' 대신 '판결문'을 선택한 이유

함께 읽으면 좋은 다른 개발일지

'개발' 및 추천 아카이브 글 모음

Next.js App Router에서 구글 서치 콘솔 '적절한 표준 태그가 포함된 대체 페이지' 해결기
개발
Next.js App Router에서 구글 서치 콘솔 '적절한 표준 태그가 포함된 대체 페이지' 해결기
Next.js App Router의 메타데이터 상속 구조로 인해 모든 블로그 포스트의 canonical URL이 홈으로 고정되어 검색 색인에서 누락되던 문제를 self-referencing canonical과 한글 슬러그 인코딩으로 해결한 실전 트러블슈팅 기록.
2026. 9. 7.

댓글

목차
– i18n 구현과 Hydration 에러 해결 로그0. 이전 글 요약1. i18n 라이브러리 선택과 초기 설정2. 첫 번째 에러: 서버 컴포넌트에서의 useTranslation 훅 사용3. 두 번째 에러: 랜딩 페이지의 Hydration 에러4. 세 번째 에러: 사이드바의 언어 전환 문제5. 네 번째 에러: 데이터 테이블의 undefined 상태 에러6. 다섯 번째 에러: 페이지네이션 번역 누락7. 마지막 문제: 랜딩페이지의 하드코딩된 숫자8. 최종 결과와 학습점9. 다음 단계 예고
0회
🏊‍♂️
개발
Next.js 16과 Tailwind CSS에서 다크/라이트 테마 깜빡임(FOUC) 없이 구현하기
Next.js 16 App Router 환경에서 localStorage 기반 다크 모드 전환 시 발생하는 첫 화면 깜빡임(FOUC) 문제를 인라인 스크립트와 CSS 변수로 깔끔하게 해결한 과정.
2026. 9. 4.2회
🏊‍♂️
개발
React 마크다운 렌더러에 스마트 목차(TOC)와 스크롤 스파이(Scroll-Spy) 적용기
긴 기술 블로그 아티클을 읽기 편하게 만들기 위해 마크다운 헤딩(#, ##)을 자동 추출하여 동적 목차(TOC)를 생성하고 IntersectionObserver로 실시간 스크롤 스파이를 구현한 경험.
2026. 9. 4.2회