Skip to content

Commit d7aeed5

Browse files
committed
fix: hydration mismatch 및 불필요한 API 호출 제거
AuthProvider의 매 새로고침 시 사용자 조회 호출을 제거하고, useAuthHydrated 훅으로 SSR hydration 불일치 문제를 해결.
1 parent 0ea5ec2 commit d7aeed5

4 files changed

Lines changed: 27 additions & 49 deletions

File tree

src/app/settings/page.tsx

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { useEffect, useState } from "react"
44
import { useRouter } from "next/navigation"
55
import { motion } from "framer-motion"
66
import { ArrowLeft, User, Trash2, ChevronRight, Shield } from "lucide-react"
7-
import { useAuthStore } from "@/features/auth/store/auth-store"
7+
import { useAuthStore, useAuthHydrated } from "@/features/auth/store/auth-store"
88
import { DeleteAccountModal } from "@/features/user/components/delete-account-modal"
99
import { Avatar, AvatarImage, AvatarFallback } from "@/shared/components/avatar"
1010
import { Button } from "@/shared/components/button"
@@ -14,21 +14,17 @@ import { Skeleton } from "@/shared/components/skeleton"
1414
export default function SettingsPage() {
1515
const router = useRouter()
1616
const { user, isAuthenticated } = useAuthStore()
17-
const [mounted, setMounted] = useState(false)
17+
const hydrated = useAuthHydrated()
1818
const [deleteModalOpen, setDeleteModalOpen] = useState(false)
1919

2020
useEffect(() => {
21-
setMounted(true)
22-
}, [])
23-
24-
useEffect(() => {
25-
if (mounted && !isAuthenticated) {
21+
if (hydrated && !isAuthenticated) {
2622
router.push('/login')
2723
}
28-
}, [mounted, isAuthenticated, router])
24+
}, [hydrated, isAuthenticated, router])
2925

3026
// Loading state
31-
if (!mounted) {
27+
if (!hydrated) {
3228
return (
3329
<div className="min-h-screen bg-background">
3430
<div className="container max-w-2xl py-8 px-4">

src/features/auth/store/auth-store.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { useState, useEffect } from 'react';
12
import { create } from 'zustand';
23
import { persist } from 'zustand/middleware';
34
import { User } from '@/shared/types/api';
@@ -22,3 +23,18 @@ export const useAuthStore = create<AuthState>()(
2223
}
2324
)
2425
);
26+
27+
export const useAuthHydrated = () => {
28+
const [hydrated, setHydrated] = useState(false);
29+
30+
useEffect(() => {
31+
if (useAuthStore.persist?.hasHydrated?.()) {
32+
setHydrated(true);
33+
return;
34+
}
35+
const unsub = useAuthStore.persist?.onFinishHydration?.(() => setHydrated(true));
36+
return () => unsub?.();
37+
}, []);
38+
39+
return hydrated;
40+
};

src/shared/components/layout/header.tsx

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import Link from "next/link"
44
import { usePathname, useRouter } from "next/navigation"
55
import { LogOut, User, Flame, Loader2, DoorOpen, Settings } from "lucide-react"
66
import { motion } from "framer-motion"
7-
import { useAuthStore } from "@/features/auth/store/auth-store"
7+
import { useAuthStore, useAuthHydrated } from "@/features/auth/store/auth-store"
88
import { useLogout } from "@/features/auth/api/auth-service"
99
import { Button } from "@/shared/components/button"
1010
import { ThemeToggle } from "@/shared/components/theme-toggle"
@@ -26,21 +26,17 @@ import {
2626
AlertDialogTitle,
2727
} from "@/shared/components/alert-dialog"
2828
import { Avatar, AvatarFallback, AvatarImage } from "@/shared/components/avatar"
29-
import { useEffect, useState } from "react"
29+
import { useState } from "react"
3030
import { cn } from "@/shared/lib/utils"
3131

3232
export function Header() {
3333
const { user, isAuthenticated } = useAuthStore()
34+
const hydrated = useAuthHydrated()
3435
const logoutMutation = useLogout()
3536
const router = useRouter()
36-
const [mounted, setMounted] = useState(false)
3737
const [showLogoutDialog, setShowLogoutDialog] = useState(false)
3838
const pathname = usePathname()
3939

40-
useEffect(() => {
41-
setMounted(true)
42-
}, [])
43-
4440
const handleLogin = () => {
4541
window.location.href = '/login'
4642
}
@@ -94,7 +90,9 @@ export function Header() {
9490

9591
<ThemeToggle />
9692

97-
{mounted && isAuthenticated && user ? (
93+
{!hydrated ? (
94+
<div className="w-8 h-8" />
95+
) : isAuthenticated && user ? (
9896
<DropdownMenu>
9997
<DropdownMenuTrigger asChild>
10098
<button className="flex items-center gap-2.5 rounded-full py-1.5 pl-1.5 pr-3 transition-all duration-200 hover:bg-secondary/80 focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2">
Lines changed: 0 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,5 @@
11
"use client"
22

3-
import { useEffect, useRef } from "react"
4-
import { useAuthStore } from "@/features/auth/store/auth-store"
5-
import { apiClient } from "@/shared/lib/api-client"
6-
import { getUser } from "@/features/user/api/user-service"
7-
83
export function AuthProvider({ children }: { children: React.ReactNode }) {
9-
const { isAuthenticated, user, login, logout } = useAuthStore()
10-
const hasVerifiedRef = useRef(false)
11-
12-
useEffect(() => {
13-
if (hasVerifiedRef.current || !isAuthenticated || !user) {
14-
return
15-
}
16-
hasVerifiedRef.current = true
17-
18-
const verifyAuth = async () => {
19-
try {
20-
// /auth/me로 쿠키 유효성 확인 후 전체 사용자 정보 조회
21-
const me = await apiClient.get<void, { username: string }>('/auth/me')
22-
const freshUser = await getUser(me.username)
23-
login(freshUser)
24-
} catch {
25-
if (process.env.NODE_ENV === "development") {
26-
console.log("[AuthProvider] Auth verification failed, logging out")
27-
}
28-
logout()
29-
}
30-
}
31-
32-
verifyAuth()
33-
// eslint-disable-next-line react-hooks/exhaustive-deps
34-
}, [isAuthenticated])
35-
364
return <>{children}</>
375
}

0 commit comments

Comments
 (0)