From a6f723e7c9f9141f30c8a5890923f9848b7193fb Mon Sep 17 00:00:00 2001 From: Sage Date: Wed, 7 Jan 2026 13:39:15 +0900 Subject: [PATCH 1/2] =?UTF-8?q?refactor(auth):=20=ED=97=A4=EB=8D=94=20?= =?UTF-8?q?=EC=9D=B8=EC=A6=9D=20=EB=A1=9C=EC=A7=81=EC=9D=84=20=ED=81=B4?= =?UTF-8?q?=EB=9D=BC=EC=9D=B4=EC=96=B8=ED=8A=B8=20=EC=9C=A0=EC=A0=80=20?= =?UTF-8?q?=EC=83=81=ED=83=9C=EB=A1=9C=20=EC=A0=84=ED=99=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 헤더의 인증/프로필 로직을 React Query 기반 클라이언트 상태로 이동 - 사용자 기본 정보를 조회하는 useUserBasic 훅 추가 - 로그인 상태 분기를 담당하는 HeaderAuthSlot 컴포넌트 추가 - HeaderActions가 서버에서 프로필을 조회하지 않고 avatar props를 받도록 수정 - 로그아웃 시 사용자 관련 query cache를 정리하도록 처리 --- src/entities/user/api/user-api.server.ts | 6 ++-- src/entities/user/index.ts | 7 ++++- src/features/user/api/use-user-basic.ts | 25 +++++++++++++++ src/widgets/header/header.tsx | 24 ++------------- src/widgets/header/ui/header-actions.tsx | 18 ++++++----- src/widgets/header/ui/header-auth-slot.tsx | 36 ++++++++++++++++++++++ src/widgets/header/ui/header-user-menu.tsx | 3 ++ 7 files changed, 84 insertions(+), 35 deletions(-) create mode 100644 src/features/user/api/use-user-basic.ts create mode 100644 src/widgets/header/ui/header-auth-slot.tsx diff --git a/src/entities/user/api/user-api.server.ts b/src/entities/user/api/user-api.server.ts index 4d9077bd..0b82ab9a 100644 --- a/src/entities/user/api/user-api.server.ts +++ b/src/entities/user/api/user-api.server.ts @@ -1,5 +1,3 @@ -import { cache } from "react"; - import { cookies } from "next/headers"; import { fetch as apiFetch } from "@/shared/api/server"; @@ -14,7 +12,7 @@ interface UserApiResponse { isOwner: boolean; } -export const getUserProfileServer = cache(async (targetUserId: number) => { +export const getUserProfileServer = async (targetUserId: number) => { const cookieStore = await cookies(); const accessToken = cookieStore.get("accessToken")?.value; @@ -45,4 +43,4 @@ export const getUserProfileServer = cache(async (targetUserId: number) => { } catch { return null; } -}); +}; diff --git a/src/entities/user/index.ts b/src/entities/user/index.ts index a8324991..6bbdaa32 100644 --- a/src/entities/user/index.ts +++ b/src/entities/user/index.ts @@ -3,4 +3,9 @@ export type { UserProfileType } from "./model/types"; export type { UserBasicInfoResponseType } from "./model/types"; export { DASHBOARD_TABS } from "./model/dashboard-tabs.config"; export type { TabIdType, TabConfig } from "./model/types"; -export { getUserProfile, updateUserProfileImage, updateUserName } from "./api/user-api"; +export { + getUserProfile, + getUserBasic, + updateUserProfileImage, + updateUserName, +} from "./api/user-api"; diff --git a/src/features/user/api/use-user-basic.ts b/src/features/user/api/use-user-basic.ts new file mode 100644 index 00000000..763ea8e9 --- /dev/null +++ b/src/features/user/api/use-user-basic.ts @@ -0,0 +1,25 @@ +"use client"; + +import { useQuery } from "@tanstack/react-query"; + +import { getUserBasic } from "@/entities/user"; + +const userBasicKey = ["user", "basic"] as const; + +async function fetchUserBasic() { + try { + return await getUserBasic(); + } catch { + return null; + } +} + +export function useUserBasic() { + return useQuery({ + queryKey: userBasicKey, + queryFn: fetchUserBasic, + retry: false, + staleTime: 1000 * 60 * 5, + refetchOnWindowFocus: false, + }); +} diff --git a/src/widgets/header/header.tsx b/src/widgets/header/header.tsx index 2adc1b9f..ab240554 100644 --- a/src/widgets/header/header.tsx +++ b/src/widgets/header/header.tsx @@ -1,23 +1,12 @@ -import { cookies } from "next/headers"; import Image from "next/image"; import Link from "next/link"; -import { LogIn } from "lucide-react"; - import Logo from "@/shared/assets/icons/windfall.svg"; -import { ROUTES } from "@/shared/config/routes"; import { Container } from "@/shared/ui"; -import Button from "@/shared/ui/button/button"; -import HeaderActions from "@/widgets/header/ui/header-actions"; +import HeaderAuthSlot from "@/widgets/header/ui/header-auth-slot"; import HeaderSearch from "@/widgets/header/ui/header-search"; export async function Header() { - const cookieStore = await cookies(); - const userId = cookieStore.get("userId")?.value; - - const numericUserId = Number(userId); - const hasValidUserId = Number.isInteger(numericUserId) && numericUserId > 0; - return (
@@ -30,16 +19,7 @@ export async function Header() { - {hasValidUserId ? ( - - ) : ( - - )} +
); diff --git a/src/widgets/header/ui/header-actions.tsx b/src/widgets/header/ui/header-actions.tsx index 5b470cf0..bf759254 100644 --- a/src/widgets/header/ui/header-actions.tsx +++ b/src/widgets/header/ui/header-actions.tsx @@ -1,18 +1,20 @@ +"use client"; + import Link from "next/link"; import { Bell, Plus } from "lucide-react"; -import { getUserProfileServer } from "@/entities/user/api/user-api.server"; import { ROUTES } from "@/shared/config/routes"; import { Button } from "@/shared/ui"; import HeaderUserMenu from "@/widgets/header/ui/header-user-menu"; -export default async function HeaderActions({ userId }: { userId: number }) { - const profile = await getUserProfileServer(userId); - - const avatarUrl = profile?.avatarUrl; - const avatarAlt = profile?.name ?? "프로필"; - +export default function HeaderActions({ + avatarUrl, + avatarAlt, +}: { + avatarUrl?: string; + avatarAlt: string; +}) { return (
- +
); } diff --git a/src/widgets/header/ui/header-auth-slot.tsx b/src/widgets/header/ui/header-auth-slot.tsx new file mode 100644 index 00000000..61d7bdaa --- /dev/null +++ b/src/widgets/header/ui/header-auth-slot.tsx @@ -0,0 +1,36 @@ +"use client"; + +import Link from "next/link"; + +import { LogIn } from "lucide-react"; + +import { useUserBasic } from "@/features/user/api/use-user-basic"; +import { ROUTES } from "@/shared/config/routes"; +import Button from "@/shared/ui/button/button"; +import HeaderActions from "@/widgets/header/ui/header-actions"; + +export default function HeaderAuthSlot() { + const { data, isPending } = useUserBasic(); + + if (isPending) { + return
; + } + + if (!data) { + return ( + + ); + } + + return ( + + ); +} diff --git a/src/widgets/header/ui/header-user-menu.tsx b/src/widgets/header/ui/header-user-menu.tsx index 9c1c79c0..6a1c4a4a 100644 --- a/src/widgets/header/ui/header-user-menu.tsx +++ b/src/widgets/header/ui/header-user-menu.tsx @@ -3,6 +3,7 @@ import Link from "next/link"; import { useRouter } from "next/navigation"; +import { useQueryClient } from "@tanstack/react-query"; import { LogOut, Moon, Sun, User } from "lucide-react"; import { useTheme } from "next-themes"; @@ -26,6 +27,7 @@ interface HeaderUserMenuProps { export default function HeaderUserMenu({ avatarUrl, avatarAlt }: HeaderUserMenuProps) { const router = useRouter(); + const queryClient = useQueryClient(); const { resolvedTheme, setTheme } = useTheme(); const isDarkMode = resolvedTheme === "dark"; const themeLabel = isDarkMode ? "라이트 모드" : "다크 모드"; @@ -40,6 +42,7 @@ export default function HeaderUserMenu({ avatarUrl, avatarAlt }: HeaderUserMenuP } catch { showToast.error("로그아웃에 실패했습니다. 잠시 후 다시 시도해주세요."); } finally { + queryClient.setQueryData(["user", "basic"], null); router.refresh(); router.push(ROUTES.login); } From a03ba4e9c67d0b490e73ef6db458c1a48aa3ae5f Mon Sep 17 00:00:00 2001 From: Sage Date: Wed, 7 Jan 2026 14:02:32 +0900 Subject: [PATCH 2/2] =?UTF-8?q?refactor(user):=20=EC=9C=A0=EC=A0=80=20?= =?UTF-8?q?=EC=BF=BC=EB=A6=AC=20=ED=82=A4=20=EC=A0=95=EB=A6=AC=20=EB=B0=8F?= =?UTF-8?q?=20=ED=97=A4=EB=8D=94=20=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=20?= =?UTF-8?q?=EA=B5=AC=EC=A1=B0=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 유저 관련 queryKey를 userKeys로 중앙화 - HeaderSearch를 Suspense로 감싸도록 헤더 구성 변경 - Header 컴포넌트에서 불필요한 async 제거 - 로그아웃 시 유저 기본 쿼리를 null로 설정하는 방식 대신 유저 관련 query 전체 제거 --- src/features/user/api/use-my-profile.ts | 6 ++++-- src/features/user/api/use-user-basic.ts | 5 ++--- src/widgets/header/header.tsx | 8 ++++++-- src/widgets/header/ui/header-user-menu.tsx | 3 ++- 4 files changed, 14 insertions(+), 8 deletions(-) diff --git a/src/features/user/api/use-my-profile.ts b/src/features/user/api/use-my-profile.ts index 701aac4f..d6906437 100644 --- a/src/features/user/api/use-my-profile.ts +++ b/src/features/user/api/use-my-profile.ts @@ -1,11 +1,13 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { getUserProfile, updateUserProfileImage, updateUserName } from "@/entities/user"; -import type { UserProfileType } from "@/entities/user"; // 타입 위치에 맞게 수정 +import type { UserProfileType } from "@/entities/user"; import { showToast } from "@/shared/lib/utils/toast/show-toast"; export const userKeys = { - profile: (userId: number) => ["user", "profile", userId] as const, + all: ["user"] as const, + basic: () => [...userKeys.all, "basic"] as const, + profile: (userId: number) => [...userKeys.all, "profile", userId] as const, }; export function useUserProfile(userId: number, initialData?: UserProfileType) { diff --git a/src/features/user/api/use-user-basic.ts b/src/features/user/api/use-user-basic.ts index 763ea8e9..d00a6508 100644 --- a/src/features/user/api/use-user-basic.ts +++ b/src/features/user/api/use-user-basic.ts @@ -3,8 +3,7 @@ import { useQuery } from "@tanstack/react-query"; import { getUserBasic } from "@/entities/user"; - -const userBasicKey = ["user", "basic"] as const; +import { userKeys } from "@/features/user/api/use-my-profile"; async function fetchUserBasic() { try { @@ -16,7 +15,7 @@ async function fetchUserBasic() { export function useUserBasic() { return useQuery({ - queryKey: userBasicKey, + queryKey: userKeys.basic(), queryFn: fetchUserBasic, retry: false, staleTime: 1000 * 60 * 5, diff --git a/src/widgets/header/header.tsx b/src/widgets/header/header.tsx index ab240554..d6bda7fc 100644 --- a/src/widgets/header/header.tsx +++ b/src/widgets/header/header.tsx @@ -1,3 +1,5 @@ +import { Suspense } from "react"; + import Image from "next/image"; import Link from "next/link"; @@ -6,7 +8,7 @@ import { Container } from "@/shared/ui"; import HeaderAuthSlot from "@/widgets/header/ui/header-auth-slot"; import HeaderSearch from "@/widgets/header/ui/header-search"; -export async function Header() { +export function Header() { return (
@@ -17,7 +19,9 @@ export async function Header() { - + }> + + diff --git a/src/widgets/header/ui/header-user-menu.tsx b/src/widgets/header/ui/header-user-menu.tsx index 6a1c4a4a..f4712887 100644 --- a/src/widgets/header/ui/header-user-menu.tsx +++ b/src/widgets/header/ui/header-user-menu.tsx @@ -7,6 +7,7 @@ import { useQueryClient } from "@tanstack/react-query"; import { LogOut, Moon, Sun, User } from "lucide-react"; import { useTheme } from "next-themes"; +import { userKeys } from "@/features/user/api/use-my-profile"; import { ROUTES } from "@/shared/config/routes"; import { showToast } from "@/shared/lib/utils/toast/show-toast"; import { @@ -42,7 +43,7 @@ export default function HeaderUserMenu({ avatarUrl, avatarAlt }: HeaderUserMenuP } catch { showToast.error("로그아웃에 실패했습니다. 잠시 후 다시 시도해주세요."); } finally { - queryClient.setQueryData(["user", "basic"], null); + queryClient.removeQueries({ queryKey: userKeys.all, exact: false }); router.refresh(); router.push(ROUTES.login); }