fix: 계정 간 본인인증 상태 오염 및 Hydration mismatch 콘솔 경고#386
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
📝 WalkthroughWalkthrough서버 사이드에서 accessToken을 읽어 초기화 토큰을 MainProvider에 주입하고, 인증 수화(hydration)/준비 상태 훅을 도입하여 useAuth → useAuthReady로 마이그레이션했으며, 휴대폰 인증 상태를 서버 중심으로 동기화하고 토스트를 포탈로 렌더링하도록 UI와 레이아웃을 변경했습니다. Changes
Sequence Diagram(s)sequenceDiagram
participant Layout as Layout (server)
participant Server as ServerRuntime
participant Cookie as getServerCookie
participant Provider as MainProvider
participant AuthHydration as AuthHydrationProvider
Layout->>Server: 실행(SSR) (async layout)
Server->>Cookie: getServerCookie('accessToken')
Cookie-->>Server: accessToken | undefined
Server->>Provider: render MainProvider(initialAccessToken)
Provider->>AuthHydration: wrap with initialAccessToken
AuthHydration-->>Provider: provide token via context
sequenceDiagram
participant Component as Client Component
participant AuthHook as useAuthReady
participant AuthHydration as AuthHydrationProvider (context)
participant Store as PhoneVerificationStore / UserProfileAPI
Component->>AuthHook: call useAuthReady()
AuthHook->>AuthHydration: read initialAccessToken & isHydrated
AuthHook->>Store: obtain memberId / decoded token
AuthHook-->>Component: { isAuthReady, memberId, ... }
sequenceDiagram
participant Modal as PhoneVerificationModal
participant StatusHook as usePhoneVerificationStatus
participant Server as UserProfile API
participant Store as Zustand phone-verification store
Modal->>StatusHook: usePhoneVerificationStatus(memberId)
StatusHook->>Server: fetch userProfile (if memberId)
Server-->>StatusHook: verification data
StatusHook->>Store: sync server data into store
StatusHook-->>Modal: return { isVerified, isLoading, isError, setVerified }
sequenceDiagram
participant ToastComp as Toast Component
participant DOM as document.body (portal)
participant CSS as global.css animations
ToastComp->>ToastComp: isVisible true -> isRendered true
ToastComp->>DOM: createPortal(toastElement)
DOM->>CSS: apply .toast-enter animation (300ms)
ToastComp->>ToastComp: onClose -> add .toast-exit
DOM->>CSS: apply .toast-exit animation (200ms)
ToastComp->>DOM: remove portal element
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Suggested labels
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
🧪 Generate unit tests (beta)
Comment |
- Fixes: 계정 간 본인인증 상태 오염 - Improves: Toast z-index 및 애니메이션 - Improves: Voting 공유 신뢰성 fix:typecheck fix
7faf75f to
7021d6c
Compare
There was a problem hiding this comment.
Actionable comments posted: 4
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
src/entities/user/model/use-user-profile-query.ts (1)
10-16:⚠️ Potential issue | 🟠 MajorstaleTime을 프로젝트 가이드라인인 60초로 변경하세요
Line 15의
staleTime: 0은 프로젝트 가이드라인("Use TanStack Query for server state with a default staleTime of 60 seconds")을 따르지 않습니다. 이 설정으로 인해 쿼리는 마운트, 윈도우 포커스, 네트워크 재연결 시 불필요한 재요청이 발생합니다.staleTime: 60000(60초)으로 변경해 주세요.src/components/voting/voting-detail-view.tsx (1)
324-332:⚠️ Potential issue | 🟡 Minor프로덕션 코드에서 디버깅용 console.log를 제거하세요.
디버깅용 로그가 프로덕션 코드에 남아있습니다. 브라우저 콘솔에 내부 상태가 노출되며, 불필요한 성능 오버헤드가 발생합니다.
🧹 제거 제안
- // 디버깅용 로그 추가 - console.log('Voting Data:', { - myVote: voting.myVote, - hasVoted, - endsAt: voting.endsAt, - rawIsActive: voting.isActive, - isActiveByEndsAt, - isActive, - }); - const showVoteOptions = isAuthReady && !hasVoted && isActive;
🤖 Fix all issues with AI agents
In `@src/app/`(landing)/layout.tsx:
- Around line 65-67: The Clarity initialization inside the async server-only
layout never runs because typeof window !== 'undefined' is always false on the
server; move the Clarity.init(CLARITY_PROJECT_ID) call into a client-side
context: create a client component (or a useEffect within a client component)
that checks CLARITY_PROJECT_ID and calls Clarity.init, then import/render that
client component from your layout; update references to Clarity.init and
CLARITY_PROJECT_ID in the new client component so initialization happens only on
the client.
In `@src/components/ui/toast.tsx`:
- Around line 27-47: Summary: useEffect reads isRendered but doesn't include it
in the dependency array, breaking exhaustive-deps. Fix: update the dependency
array of the useEffect that manages isVisible/isRendered to include isRendered
(so it becomes [isVisible, isRendered, duration, onClose]); you can omit
exitDuration if it is a true constant (200). Keep the same timer setup and
cleanup logic in the effect; no other logic changes are needed. Ensure you
reference the existing symbols useEffect, isVisible, isRendered, duration,
exitDuration, and onClose when making the change.
In `@src/features/phone-verification/model/store.ts`:
- Around line 40-50: The partialize callback in phone-verification-storage
returns an object whose properties are inferred as any; fix this by declaring an
explicit return type for partialize (or casting the returned object) to the
store slice type containing isVerified, phoneNumber, verifiedAt, and memberId so
TypeScript can check types; update the partialize signature in
src/features/phone-verification/model/store.ts (the partialize function) to
return e.g. Partial<PhoneVerificationState> (or the specific typed shape)
ensuring each property matches the declared types of isVerified, phoneNumber,
verifiedAt, and memberId.
In `@src/features/study/group/ui/group-study-form-modal.tsx`:
- Around line 101-103: The refineStudyDetail helper returns undefined while
isGroupStudyLoading is true which causes GroupStudyForm to receive
defaultValues={undefined} and never update after load; fix by guarding render or
ensuring form reset on prop change: either render GroupStudyForm only when
isGroupStudyLoading is false and refineStudyDetail(value) yields the DTO, or in
GroupStudyForm add a useEffect that watches defaultValues and calls
reset(defaultValues) when they change; reference refineStudyDetail,
isGroupStudyLoading, GroupStudyForm, defaultValues and reset in your change.
🧹 Nitpick comments (9)
src/features/study/participation/ui/reservation-list.tsx (2)
99-106: 본인인증 상태 체크 로직이 적절합니다.로딩 중 조기 반환과 에러 시 알림 표시가 적절합니다. 다만, 사용자 경험 향상을 위해
isVerificationLoading상태일 때 버튼에 시각적 피드백(예: disabled 상태 또는 스피너)을 추가하는 것을 고려해 보세요.
46-49: memberId 타입 체크 일관성 검토Line 47에서
memberId !== null체크를 사용하고 있지만,useAuthReady는 hydration 전에undefined를 반환합니다.!!memberId로 간소화하면null과undefined모두를 처리할 수 있습니다.♻️ 제안된 수정
const firstMemberId = useMemo( - () => (autoMatching && memberId !== null ? memberId : null), + () => (autoMatching && memberId ? memberId : null), [autoMatching, memberId], );src/features/study/participation/ui/start-study-modal.tsx (1)
142-162: 중복된 알림 메시지 상수화 권장
handleOpenChange(Line 115)와useEffect(Line 145)에서 동일한 에러 메시지가 사용됩니다. 메시지를 상수로 추출하면 유지보수성이 향상됩니다.♻️ 제안된 수정
+const VERIFICATION_ERROR_MESSAGE = '인증 상태를 확인할 수 없습니다. 잠시 후 다시 시도해주세요.'; + export default function StartStudyModal({ // ... }) { // handleOpenChange 내부 if (isOpen && isError) { - alert('인증 상태를 확인할 수 없습니다. 잠시 후 다시 시도해주세요.'); + alert(VERIFICATION_ERROR_MESSAGE); return; } // useEffect 내부 if (isError) { - alert('인증 상태를 확인할 수 없습니다. 잠시 후 다시 시도해주세요.'); + alert(VERIFICATION_ERROR_MESSAGE); // ... }src/app/(service)/(my)/layout.tsx (1)
2-2: GlobalToast 컴포넌트를 루트 레이아웃으로 이동하는 것을 권장합니다.Portal 구현은 올바릅니다(
Toast컴포넌트가createPortal로document.body에 렌더링). 그러나 검증 결과GlobalToast가 4개 레이아웃/페이지에서 중복되고 있습니다:
src/app/(service)/(my)/layout.tsxsrc/app/(service)/group-study/layout.tsxsrc/app/(service)/home/page.tsxsrc/app/(service)/premium-study/layout.tsx각 위치에서 동일한
useToastStore상태를 구독하는 독립적인 컴포넌트 인스턴스가 4개 생성되므로 불필요한 렌더링 오버헤드가 발생합니다. 단일 루트 레이아웃에서 한 번만 렌더링하는 것이 더 효율적입니다.src/widgets/my-page/sidebar.tsx (1)
13-16:memberId ?? 0폴백 사용 시 주의 필요
useUserProfileQuery는enabled: !!memberId조건이 있어memberId가 0이면 쿼리가 실행되지 않습니다. 하지만isAuthReady를 활용하여 인증 상태를 더 명확하게 체크하면 의도가 더 분명해집니다.- const { memberId } = useAuthReady(); + const { memberId, isAuthReady } = useAuthReady(); const { mutateAsync: logout } = useLogoutMutation(); - const { data: profile } = useUserProfileQuery(memberId ?? 0); + const { data: profile } = useUserProfileQuery(memberId ?? 0); + // 또는 조건부 렌더링에 isAuthReady 활용 고려현재 구현도 동작하지만, 다른 컴포넌트들(
notification-api.ts등)에서isAuthReady로 쿼리를 게이팅하는 패턴과 일관성을 맞추는 것을 권장합니다.src/components/section/group-study-info-section.tsx (1)
29-31:memberId이중 소스 사용에 대한 참고
authMemberId(useAuthReady)와memberId(useUserStore)가 동시에 사용되고 있습니다. 현재 각각의 용도가 다르지만(GTM vs isLeader 체크), 향후 리팩토링 시useAuthReady의memberId로 통일하는 것을 고려해볼 수 있습니다.src/components/home/tab-navigation.tsx (1)
64-72: 중복된memberId확인 로직을 단순화할 수 있습니다.
useAuthReady()에서 반환하는memberId는 이미 hydration 상태를 고려하므로,getCookie('memberId')폴백은 불필요해 보입니다.isAuthReady가false일 때 최종적으로canViewHistory도false가 되므로 현재 구현은 안전하지만, 코드 단순화를 고려해볼 수 있습니다.♻️ 단순화 제안
useEffect(() => { if (!isHydrated) { setCanViewHistory(false); return; } - const hasMemberId = !!memberId || !!getCookie('memberId'); - setCanViewHistory(isAuthReady && hasMemberId); + setCanViewHistory(isAuthReady && !!memberId); }, [isAuthReady, isHydrated, memberId]);src/features/study/group/ui/group-study-member-item.tsx (1)
34-42:isMe계산에isAuthReady가드 추가를 고려해보세요.
isLeader는isAuthReady로 가드되어 있지만, Line 41의isMe는 그렇지 않습니다.myId가undefined일 때member.id === undefined가false가 되므로 실제로 안전하지만, 아래GreetingBox의isMe계산(Line 222)과 일관성을 위해 동일한 패턴 적용을 고려해볼 수 있습니다.♻️ 일관성 개선 제안
- const isMe = member.id === myId; + const isMe = isAuthReady && member.id === myId; const isLeader = isAuthReady && leaderId === myId;src/features/study/group/ui/group-study-form-modal.tsx (1)
70-86: 인증 상태 로딩 중 무반응 UX를 보완하는 게 좋습니다.
현재 로딩 중에는 클릭해도 피드백이 없어 혼란 가능성이 있습니다. 트리거 비활성화나 토스트 안내를 고려해주세요.
| if (typeof window !== 'undefined' && CLARITY_PROJECT_ID) { | ||
| Clarity.init(CLARITY_PROJECT_ID); | ||
| } |
There was a problem hiding this comment.
서버 컴포넌트에서 Clarity 초기화 코드가 실행되지 않습니다.
async function 레이아웃은 서버에서만 실행되므로 typeof window !== 'undefined' 조건이 항상 false가 되어 Clarity.init()이 절대 호출되지 않습니다. Clarity 초기화는 클라이언트 컴포넌트나 별도의 useEffect 훅으로 이동해야 합니다.
🤖 Prompt for AI Agents
In `@src/app/`(landing)/layout.tsx around lines 65 - 67, The Clarity
initialization inside the async server-only layout never runs because typeof
window !== 'undefined' is always false on the server; move the
Clarity.init(CLARITY_PROJECT_ID) call into a client-side context: create a
client component (or a useEffect within a client component) that checks
CLARITY_PROJECT_ID and calls Clarity.init, then import/render that client
component from your layout; update references to Clarity.init and
CLARITY_PROJECT_ID in the new client component so initialization happens only on
the client.
| useEffect(() => { | ||
| if (isVisible) { | ||
| setIsRendered(true); | ||
| setIsExiting(false); | ||
| const timer = setTimeout(() => { | ||
| onClose(); | ||
| }, duration); | ||
|
|
||
| return () => clearTimeout(timer); | ||
| } | ||
|
|
||
| if (isRendered) { | ||
| setIsExiting(true); | ||
| const timer = setTimeout(() => { | ||
| setIsRendered(false); | ||
| setIsExiting(false); | ||
| }, exitDuration); | ||
|
|
||
| return () => clearTimeout(timer); | ||
| } | ||
| }, [isVisible, duration, onClose]); |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
cat -n src/components/ui/toast.tsxRepository: code-zero-to-one/study-platform-client
Length of output: 2620
🏁 Script executed:
fd -e .eslintrc -e .eslintrc.js -e .eslintrc.json -e .eslintrc.ymlRepository: code-zero-to-one/study-platform-client
Length of output: 64
🏁 Script executed:
rg "react-hooks/exhaustive-deps" --type json --type javascript --type typescriptRepository: code-zero-to-one/study-platform-client
Length of output: 116
🏁 Script executed:
find . -maxdepth 3 -name ".eslint*" -o -name "eslint*" 2>/dev/null | head -20Repository: code-zero-to-one/study-platform-client
Length of output: 958
🏁 Script executed:
find . -maxdepth 3 -type f \( -name "*.config.js" -o -name "*.config.ts" \) | grep -i eslintRepository: code-zero-to-one/study-platform-client
Length of output: 159
🏁 Script executed:
rg "exhaustive-deps" -t ts -t tsx -t js -A 2 -B 2Repository: code-zero-to-one/study-platform-client
Length of output: 109
🏁 Script executed:
cat -n .eslintrc.cjsRepository: code-zero-to-one/study-platform-client
Length of output: 2976
🏁 Script executed:
rg "exhaustive-deps" -A 2 -B 2Repository: code-zero-to-one/study-platform-client
Length of output: 1504
useEffect 의존성 검토 필요
isRendered 상태는 useEffect 내에서 읽히지만(라인 38) 의존성 배열에 포함되지 않아 react-hooks/exhaustive-deps 규칙에 위배됩니다. 다만 exitDuration은 상수(200)로 의존성에 추가할 필요가 없습니다.
🛠️ 권장 수정
- }, [isVisible, duration, onClose]);
+ }, [isVisible, duration, onClose, isRendered]);🤖 Prompt for AI Agents
In `@src/components/ui/toast.tsx` around lines 27 - 47, Summary: useEffect reads
isRendered but doesn't include it in the dependency array, breaking
exhaustive-deps. Fix: update the dependency array of the useEffect that manages
isVisible/isRendered to include isRendered (so it becomes [isVisible,
isRendered, duration, onClose]); you can omit exitDuration if it is a true
constant (200). Keep the same timer setup and cleanup logic in the effect; no
other logic changes are needed. Ensure you reference the existing symbols
useEffect, isVisible, isRendered, duration, exitDuration, and onClose when
making the change.
There was a problem hiding this comment.
Actionable comments posted: 6
🤖 Fix all issues with AI agents
In `@src/app/`(service)/(my)/my-study/page.tsx:
- Around line 18-24: memberId from useAuthReady() can be undefined during
hydration but useMemberStudyListQuery expects a number
(MemberStudyListRequest.memberId); update the call to pass a non-undefined
number (same pattern used in my-participating-studies-section.tsx) by coercing
memberId to 0 when undefined and keep the existing enabled guard (e.g., pass
memberId ?? 0 into useMemberStudyListQuery while leaving enabled: memberId > 0)
so TypeScript types align and runtime behavior is unchanged.
In `@src/features/phone-verification/model/store.ts`:
- Around line 4-13: PhoneVerificationState currently defines verifiedAt as
string | null which conflicts with use-phone-verification-state.ts where
verifiedAt is Date | null; pick one canonical type and apply it consistently:
either change PhoneVerificationState.verifiedAt to Date | null (and adjust
setVerified to accept/assign a Date) or change the hook to use string | null and
convert Date↔ISO at setVerified/get points; update all related symbols
(PhoneVerificationState, setVerified, reset, and the hook
usePhoneVerificationState) so the stored type and any conversions are explicit
and consistent to prevent runtime mismatches.
In `@src/features/phone-verification/model/use-phone-verification-status.ts`:
- Around line 186-193: The setVerified callback currently calls
store.setVerified(phone, memberId ?? undefined) which can overwrite another
user's cached verification when memberId is null/undefined; change setVerified
(the useCallback defined around setVerified) to first check that memberId is a
defined value and perform a no-op if it's null/undefined, otherwise call
store.setVerified(phone, memberId), so the store is only updated when a concrete
memberId exists.
In `@src/features/study/one-to-one/archive/model/use-archive-actions.ts`:
- Line 69: The returned object in use-archive-actions.ts maps isAuthReady to the
misleading key isAuthenticated; update the return to expose the actual value
name (e.g., return isAuthReady: isAuthReady instead of isAuthenticated:
isAuthReady) and then update all consumers (notably archive-tab-client.tsx) to
read props.isAuthReady; ensure references to useAuthReady(), isAuthReady and
isAuthenticated are reconciled so the boolean meaning remains clear.
In `@src/features/study/one-to-one/schedule/ui/today-study-card.tsx`:
- Around line 34-42: The query runs before auth/hydration completes; get the
auth-ready flag from useAuthReady (e.g. const { memberId: authMemberId,
isAuthReady } = useAuthReady()) and gate useDailyStudyDetailQuery with it:
compute queryEnabled = tutorialMode ? !!queryStudyDate /*or true per existing
logic*/ : (isAuthReady && !!studyDate) and call
useDailyStudyDetailQuery(queryStudyDate, { enabled: queryEnabled }). Update
references around useAuthReady, tutorialMode, studyDate and
useDailyStudyDetailQuery so the query only fires after isAuthReady when not in
tutorialMode.
In `@src/providers/index.tsx`:
- Around line 16-23: The effect currently triggers fetchAndSetUser only when
memberId is falsy; change the guard to compare identities so it runs whenever
the store's memberId differs from authMemberId. In the useEffect that uses
useAuthReady and useUserStore (symbols: useEffect, useAuthReady, useUserStore,
fetchAndSetUser, authMemberId, memberId), replace the condition `!memberId` with
`memberId !== authMemberId` so you fetch and set the user when the authenticated
member changed (covering edge cases where logout didn’t fully clear the store).
🧹 Nitpick comments (6)
src/entities/user/model/use-user-profile-query.ts (1)
15-15: staleTime: 0 설정 검토 필요
staleTime: 0으로 변경하면 데이터가 즉시 stale로 간주되어, 컴포넌트 마운트나 윈도우 포커스 시마다 리페칭이 발생합니다. 권장 기본값인 60초와 달리 서버 부하가 증가할 수 있습니다.의도적인 변경이라면 무시해도 되지만, 적절한 값(예:
staleTime: 1000 * 60)을 고려해 보세요.Based on learnings: "Use TanStack Query for server state with a default staleTime of 60 seconds"
src/hooks/common/auth-hydration-context.tsx (1)
25-27: 개발 환경에서의 디버깅을 위한 선택적 개선Provider 외부에서
useAuthHydration이 호출되면 빈 객체가 반환됩니다. 개발 중 실수 방지를 위해 경고를 추가하는 것을 고려해 보세요.♻️ 선택적 개선 제안
+const AuthHydrationContext = createContext<AuthHydrationContextValue | null>(null); + export function useAuthHydration() { - return useContext(AuthHydrationContext); + const context = useContext(AuthHydrationContext); + if (context === null) { + if (process.env.NODE_ENV === 'development') { + console.warn('useAuthHydration must be used within AuthHydrationProvider'); + } + return {}; + } + return context; }src/features/study/group/ui/group-study-member-item.tsx (1)
41-42:isMe계산에서isAuthReady가드 누락Line 42의
isLeader는isAuthReady &&가드를 사용하지만, Line 41의isMe는 가드 없이member.id === myId로 비교합니다.myId가 hydration 전undefined일 때 비교는 항상false가 되어 버그는 발생하지 않지만, 동일 파일 내GreetingBox컴포넌트(Line 222)와 패턴이 일관되지 않습니다.♻️ 일관성을 위한 선택적 수정
const isMe = member.id === myId; - const isLeader = isAuthReady && leaderId === myId; + const isLeader = isAuthReady && isMe && leaderId === myId;또는
isMe에도 가드를 추가:- const isMe = member.id === myId; + const isMe = isAuthReady && member.id === myId; const isLeader = isAuthReady && leaderId === myId;src/features/study/one-to-one/archive/model/use-archive-actions.ts (1)
8-9: import 문 뒤 빈 줄 누락Line 8과 Line 9 사이에 빈 줄이 없어 import와 interface 선언이 붙어 있습니다. 가독성을 위해 빈 줄 추가를 권장합니다.
♻️ 포맷 수정
import { useAuthReady } from '@/hooks/common/use-auth'; + interface ArchiveViewTarget {src/features/phone-verification/model/store.ts (1)
19-22: 불필요한 타입 캐스트를 제거하세요.
null as string | null,null as number | null같은 타입 캐스트는 불필요합니다. TypeScript에서null은 이미 nullable 타입에 할당 가능합니다.♻️ 타입 캐스트 제거 제안
(set) => ({ isVerified: false, - phoneNumber: null as string | null, - verifiedAt: null as string | null, - memberId: null as number | null, + phoneNumber: null, + verifiedAt: null, + memberId: null, hasHydrated: false,reset: () => set({ isVerified: false, - phoneNumber: null as string | null, - verifiedAt: null as string | null, - memberId: null as number | null, + phoneNumber: null, + verifiedAt: null, + memberId: null, }),Also applies to: 33-35
src/features/phone-verification/model/use-phone-auth-mutation.ts (1)
45-46:||대신 nullish coalescing 사용을 고려하세요.
currentMemberId || undefined는currentMemberId가0인 경우에도undefined로 변환됩니다. member ID가 0이 될 가능성은 낮지만, 더 안전한 처리를 위해 nullish coalescing(??)을 사용하는 것이 좋습니다.♻️ Nullish coalescing 사용 제안
// 인증 상태 저장 (memberId 포함) - setVerified(variables.phoneNumber, currentMemberId || undefined); + setVerified(variables.phoneNumber, currentMemberId ?? undefined);
| const { memberId } = useAuthReady(); | ||
|
|
||
| const { data, isLoading } = useMemberStudyListQuery({ | ||
| memberId: authData?.memberId, | ||
| memberId, | ||
| studyType: 'GROUP_STUDY', | ||
| studyStatus: 'BOTH', | ||
| }); |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# Find the MemberStudyListRequest type definition
fd --type f -e ts -e tsx | xargs rg "interface MemberStudyListRequest|type MemberStudyListRequest" -A 5Repository: code-zero-to-one/study-platform-client
Length of output: 634
🏁 Script executed:
# Check the useAuthReady hook return type
fd --type f -e ts -e tsx -path "*hooks*use-auth*" | head -5Repository: code-zero-to-one/study-platform-client
Length of output: 250
🏁 Script executed:
# Find useMemberStudyListQuery implementation
fd --type f -e ts -e tsx | xargs rg "useMemberStudyListQuery|MemberStudyListQuery" -A 10 -B 2 | head -80Repository: code-zero-to-one/study-platform-client
Length of output: 7220
🏁 Script executed:
# Check the actual code in the target file
cat -n src/app/\(service\)/\(my\)/my-study/page.tsxRepository: code-zero-to-one/study-platform-client
Length of output: 3505
🏁 Script executed:
# Check my-participating-studies-section.tsx to see how it handles memberId
sed -n '40,50p' src/components/section/my-participating-studies-section.tsxRepository: code-zero-to-one/study-platform-client
Length of output: 523
🏁 Script executed:
# Check useAuthReady hook return type
cat -n src/hooks/common/use-auth.tsRepository: code-zero-to-one/study-platform-client
Length of output: 4646
🏁 Script executed:
# Check useMemberStudyListQuery implementation to see how it handles the parameters
cat -n src/features/study/group/model/use-member-study-list-query.tsRepository: code-zero-to-one/study-platform-client
Length of output: 1256
memberId의 타입 불일치: undefined | number를 number로 전달
useAuthReady()에서 반환되는 memberId는 hydration 이전에 undefined일 수 있습니다. MemberStudyListRequest는 memberId: number로 정의되어 있어 타입 호환성 문제가 발생합니다. useMemberStudyListQuery의 enabled: memberId > 0 조건이 런타임 안전성을 보장하지만, 타입 검사 단계에서 오류가 발생할 수 있습니다.
my-participating-studies-section.tsx에서 사용되는 패턴(memberId ?? 0)을 적용하여 타입 안전성을 확보하세요.
🤖 Prompt for AI Agents
In `@src/app/`(service)/(my)/my-study/page.tsx around lines 18 - 24, memberId from
useAuthReady() can be undefined during hydration but useMemberStudyListQuery
expects a number (MemberStudyListRequest.memberId); update the call to pass a
non-undefined number (same pattern used in my-participating-studies-section.tsx)
by coercing memberId to 0 when undefined and keep the existing enabled guard
(e.g., pass memberId ?? 0 into useMemberStudyListQuery while leaving enabled:
memberId > 0) so TypeScript types align and runtime behavior is unchanged.
| interface PhoneVerificationState { | ||
| isVerified: boolean; | ||
| phoneNumber: string | null; | ||
| verifiedAt: Date | null; | ||
| setVerified: (phoneNumber: string) => void; | ||
| verifiedAt: string | null; | ||
| memberId: number | null; | ||
| hasHydrated: boolean; | ||
| setVerified: (phoneNumber: string, memberId?: number) => void; | ||
| reset: () => void; | ||
| setHasHydrated: (hasHydrated: boolean) => void; | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Check all verifiedAt usages and type definitions in phone-verification feature
echo "=== Searching for verifiedAt in phone-verification directory ==="
rg -n "verifiedAt" src/features/phone-verification/
echo -e "\n=== Checking store.ts file ==="
cat -n src/features/phone-verification/model/store.ts | head -50
echo -e "\n=== Checking use-phone-verification-state.ts file ==="
fd "use-phone-verification-state" src/features/phone-verification/ -x cat -n {}Repository: code-zero-to-one/study-platform-client
Length of output: 4130
store.ts와 use-phone-verification-state.ts 간 verifiedAt 타입 불일치를 수정하세요.
store.ts에서 verifiedAt은 string | null로 정의되어 ISO 문자열로 저장되지만, use-phone-verification-state.ts에서는 verifiedAt: Date | null로 정의되어 Date 객체로 저장됩니다. 이 타입 불일치는 두 상태 관리자가 상호작용할 때 런타임 오류를 유발할 수 있습니다.
두 파일의 verifiedAt 타입을 통일하거나, 각각의 용도에 맞게 명확히 분리하고 타입 변환 로직을 추가해야 합니다.
🤖 Prompt for AI Agents
In `@src/features/phone-verification/model/store.ts` around lines 4 - 13,
PhoneVerificationState currently defines verifiedAt as string | null which
conflicts with use-phone-verification-state.ts where verifiedAt is Date | null;
pick one canonical type and apply it consistently: either change
PhoneVerificationState.verifiedAt to Date | null (and adjust setVerified to
accept/assign a Date) or change the hook to use string | null and convert
Date↔ISO at setVerified/get points; update all related symbols
(PhoneVerificationState, setVerified, reset, and the hook
usePhoneVerificationState) so the stored type and any conversions are explicit
and consistent to prevent runtime mismatches.
| // 인증 완료 시 호출 — memberId를 자동 주입 | ||
| const setVerified = useCallback( | ||
| (phone: string) => { | ||
| store.setVerified(phone, memberId ?? undefined); | ||
| }, | ||
| // eslint-disable-next-line react-hooks/exhaustive-deps | ||
| [memberId], | ||
| ); |
There was a problem hiding this comment.
memberId 미확정 상태의 setVerified는 오염 가능성
memberId가 null/undefined일 때 store.setVerified가 기존 memberId를 유지한 채 인증 정보를 덮을 수 있어 계정 간 캐시 오염 여지가 있습니다. memberId가 없으면 no-op 처리하는 방어 로직을 추가해 주세요.
🛡️ 수정 제안
const setVerified = useCallback(
(phone: string) => {
- store.setVerified(phone, memberId ?? undefined);
+ if (memberId == null) return;
+ store.setVerified(phone, memberId);
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[memberId],
);🤖 Prompt for AI Agents
In `@src/features/phone-verification/model/use-phone-verification-status.ts`
around lines 186 - 193, The setVerified callback currently calls
store.setVerified(phone, memberId ?? undefined) which can overwrite another
user's cached verification when memberId is null/undefined; change setVerified
(the useCallback defined around setVerified) to first check that memberId is a
defined value and perform a no-op if it's null/undefined, otherwise call
store.setVerified(phone, memberId), so the store is only updated when a concrete
memberId exists.
| updateArchive: handleUpdateArchive, | ||
| openAndRecordView, | ||
| isAuthenticated, | ||
| isAuthenticated: isAuthReady, |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
cat -n src/features/study/one-to-one/archive/model/use-archive-actions.tsRepository: code-zero-to-one/study-platform-client
Length of output: 3049
🏁 Script executed:
rg 'useArchiveActions' --type=ts --type=tsx -B 2 -A 8Repository: code-zero-to-one/study-platform-client
Length of output: 109
🏁 Script executed:
rg 'useArchiveActions' -t ts -A 10Repository: code-zero-to-one/study-platform-client
Length of output: 4343
🏁 Script executed:
rg 'from.*use-archive-actions' -t tsRepository: code-zero-to-one/study-platform-client
Length of output: 356
🏁 Script executed:
cat src/features/study/one-to-one/archive/ui/archive-tab-client.tsx | head -100Repository: code-zero-to-one/study-platform-client
Length of output: 3326
🏁 Script executed:
rg 'useArchiveActions' src/features/study/one-to-one/archive/ui/archive-tab-client.tsx -A 20 -B 2Repository: code-zero-to-one/study-platform-client
Length of output: 1771
🏁 Script executed:
rg 'useAuthReady' --type ts -B 2 -A 10 | head -60Repository: code-zero-to-one/study-platform-client
Length of output: 5293
🏁 Script executed:
fd 'use-auth' --type tsRepository: code-zero-to-one/study-platform-client
Length of output: 251
🏁 Script executed:
find src -name '*use-auth*' -type fRepository: code-zero-to-one/study-platform-client
Length of output: 191
🏁 Script executed:
cat src/hooks/common/use-auth.tsRepository: code-zero-to-one/study-platform-client
Length of output: 3715
isAuthenticated 필드 네이밍 수정 필요
반환 객체에서 isAuthenticated: isAuthReady로 매핑하고 있습니다. 이는 의미론적 불일치입니다.
useAuthReady()의 isAuthReady는 isHydrated && isAuthenticated이므로, "인증 여부만 확인"하는 isAuthenticated와 다릅니다. 필드명 isAuthenticated는 단순한 인증 상태를 의미하지만, 실제 값은 hydration 완료 여부까지 포함합니다.
소비자(archive-tab-client.tsx)가 이 필드를 사용할 때 명확한 의미를 알 수 없습니다. 필드명을 isAuthReady로 변경하거나, 값을 실제 isAuthenticated로 수정하세요.
🤖 Prompt for AI Agents
In `@src/features/study/one-to-one/archive/model/use-archive-actions.ts` at line
69, The returned object in use-archive-actions.ts maps isAuthReady to the
misleading key isAuthenticated; update the return to expose the actual value
name (e.g., return isAuthReady: isAuthReady instead of isAuthenticated:
isAuthReady) and then update all consumers (notably archive-tab-client.tsx) to
read props.isAuthReady; ensure references to useAuthReady(), isAuthReady and
isAuthenticated are reconciled so the boolean meaning remains clear.
| const { memberId: authMemberId } = useAuthReady(); | ||
| const memberId = tutorialMode | ||
| ? forcedRole === 'INTERVIEWER' | ||
| ? TUTORIAL_DAILY_STUDY_MOCK.interviewerId | ||
| : TUTORIAL_DAILY_STUDY_MOCK.intervieweeId | ||
| : (authData?.memberId ?? null); | ||
| : (authMemberId ?? null); | ||
|
|
||
| const queryStudyDate = tutorialMode ? '' : studyDate; | ||
| const { data: todayStudyData } = useDailyStudyDetailQuery(queryStudyDate); |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# Find useDailyStudyDetailQuery definition and usage
fd -t f '\.(ts|tsx)$' src | xargs rg -l 'useDailyStudyDetailQuery' | head -20Repository: code-zero-to-one/study-platform-client
Length of output: 202
🏁 Script executed:
# Find useAuthReady definition and usage
fd -t f '\.(ts|tsx)$' src | xargs rg -l 'useAuthReady' | head -20Repository: code-zero-to-one/study-platform-client
Length of output: 1081
🏁 Script executed:
# Get the full content of the target file to understand context
cat -n "src/features/study/one-to-one/schedule/ui/today-study-card.tsx" | head -60Repository: code-zero-to-one/study-platform-client
Length of output: 2729
🏁 Script executed:
# Find and examine useAuthReady implementation
rg -nA 15 'export.*useAuthReady|function useAuthReady' src/hooks/common/use-auth.tsRepository: code-zero-to-one/study-platform-client
Length of output: 466
🏁 Script executed:
# Find and examine useDailyStudyDetailQuery implementation
rg -nA 20 'export.*useDailyStudyDetailQuery|function useDailyStudyDetailQuery' src/features/study/interview/model/use-interview-query.tsRepository: code-zero-to-one/study-platform-client
Length of output: 732
Hydration 완료 후 쿼리를 실행해야 합니다.
라인 41-42에서 useDailyStudyDetailQuery가 isAuthReady와 무관하게 실행됩니다. 쿼리의 enabled: !!params 옵션은 tutorialMode와 studyDate 값에만 의존하며, hydration 전(isHydrated=false)에도 params가 유효하면 쿼리가 실행될 수 있습니다. 이 경우 인증 토큰 없이 API 호출이 발생하여 401 오류와 재시도가 발생할 수 있으므로, isAuthReady로 쿼리 실행을 게이트해야 합니다.
🔧 제안 변경
- const { memberId: authMemberId } = useAuthReady();
+ const { memberId: authMemberId, isAuthReady } = useAuthReady();
- const queryStudyDate = tutorialMode ? '' : studyDate;
+ const queryStudyDate = tutorialMode || !isAuthReady ? '' : studyDate;🤖 Prompt for AI Agents
In `@src/features/study/one-to-one/schedule/ui/today-study-card.tsx` around lines
34 - 42, The query runs before auth/hydration completes; get the auth-ready flag
from useAuthReady (e.g. const { memberId: authMemberId, isAuthReady } =
useAuthReady()) and gate useDailyStudyDetailQuery with it: compute queryEnabled
= tutorialMode ? !!queryStudyDate /*or true per existing logic*/ : (isAuthReady
&& !!studyDate) and call useDailyStudyDetailQuery(queryStudyDate, { enabled:
queryEnabled }). Update references around useAuthReady, tutorialMode, studyDate
and useDailyStudyDetailQuery so the query only fires after isAuthReady when not
in tutorialMode.
| const { memberId: authMemberId, isAuthReady } = useAuthReady(); | ||
| const { memberId, fetchAndSetUser } = useUserStore(); | ||
|
|
||
| useEffect(() => { | ||
| if (isAuthenticated && authData?.memberId && !memberId) { | ||
| fetchAndSetUser(authData.memberId).catch(console.error); | ||
| if (isAuthReady && authMemberId && !memberId) { | ||
| fetchAndSetUser(authMemberId).catch(console.error); | ||
| } | ||
| }, [isAuthenticated, authData?.memberId, memberId, fetchAndSetUser]); | ||
| }, [isAuthReady, authMemberId, memberId, fetchAndSetUser]); |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# First, let's explore the repository structure
find . -type f -name "*.tsx" -o -name "*.ts" | grep -E "(provider|store)" | head -20Repository: code-zero-to-one/study-platform-client
Length of output: 1461
🏁 Script executed:
# Look at the full context of src/providers/index.tsx
cat -n src/providers/index.tsxRepository: code-zero-to-one/study-platform-client
Length of output: 1705
🏁 Script executed:
# Search for useUserStore implementation
fd useUserStore | head -10Repository: code-zero-to-one/study-platform-client
Length of output: 108
🏁 Script executed:
# Search for UserStore or useUserStore definition
rg -n "useUserStore\s*=" --type=ts --type=tsx -A 5Repository: code-zero-to-one/study-platform-client
Length of output: 109
🏁 Script executed:
# Look for logout or reset handlers
rg -n "logout\|\.reset\|onLogout" --type=ts --type=tsx -B 2 -A 3Repository: code-zero-to-one/study-platform-client
Length of output: 109
🏁 Script executed:
# Read useUserStore implementation
cat -n src/stores/useUserStore.tsRepository: code-zero-to-one/study-platform-client
Length of output: 1718
🏁 Script executed:
# Search for logout or reset with correct file types
rg -n "logout" --type ts -B 2 -A 3Repository: code-zero-to-one/study-platform-client
Length of output: 6841
🏁 Script executed:
# Search for reset method
rg -n "\.reset\(" --type ts -B 2 -A 3Repository: code-zero-to-one/study-platform-client
Length of output: 1329
🏁 Script executed:
# Search for where memberId is cleared
rg -n "memberId\s*=" --type ts -B 2 -A 2Repository: code-zero-to-one/study-platform-client
Length of output: 50394
🏁 Script executed:
# Look for useAuthReady implementation to understand auth flow
fd "auth" src | grep -E "\.(ts|tsx)$"Repository: code-zero-to-one/study-platform-client
Length of output: 706
🏁 Script executed:
# Read the full use-auth-mutation.ts file
cat -n src/features/auth/model/use-auth-mutation.tsRepository: code-zero-to-one/study-platform-client
Length of output: 2469
🏁 Script executed:
# Also check if useUserStore.reset() is called anywhere
rg -n "useUserStore.*reset\|\.reset\(\)" --type tsRepository: code-zero-to-one/study-platform-client
Length of output: 64
🏁 Script executed:
# Check useAuthReady to understand authMemberId behavior
cat -n src/hooks/common/use-auth.tsRepository: code-zero-to-one/study-platform-client
Length of output: 4646
로그아웃 시 스토어 리셋이 제대로 구현되어 있지만, 계정 전환 시 더욱 견고한 비교로 개선하세요.
useLogoutMutation에서 resetUserStore()가 올바르게 호출되므로 정상적인 로그아웃 흐름에서는 문제가 없습니다. 다만 로그아웃이 완전히 실행되지 않는 엣지 케이스를 방어하기 위해, 현재 !memberId 조건을 memberId !== authMemberId 비교로 개선하면 더욱 견고합니다. 이는 코드베이스의 다른 스토어(예: usePhoneVerificationStore)에서 이미 사용 중인 패턴입니다.
🔧 개선 제안
- if (isAuthReady && authMemberId && !memberId) {
+ if (isAuthReady && authMemberId && memberId !== authMemberId) {
fetchAndSetUser(authMemberId).catch(console.error);
}🤖 Prompt for AI Agents
In `@src/providers/index.tsx` around lines 16 - 23, The effect currently triggers
fetchAndSetUser only when memberId is falsy; change the guard to compare
identities so it runs whenever the store's memberId differs from authMemberId.
In the useEffect that uses useAuthReady and useUserStore (symbols: useEffect,
useAuthReady, useUserStore, fetchAndSetUser, authMemberId, memberId), replace
the condition `!memberId` with `memberId !== authMemberId` so you fetch and set
the user when the authenticated member changed (covering edge cases where logout
didn’t fully clear the store).
fix: SSR/CSR Hydration 불일치 및 UX 개선
🎯 Summary
서버 사이드 렌더링(SSR)과 클라이언트 사이드 렌더링(CSR) 간의 hydration 불일치 문제를 해결하고, 토스트 및 공유 기능의 사용자 경험을 개선했습니다.
📋 주요 변경사항
1. Auth Hydration 아키텍처 개선 ⚡️
문제:
useAuth()가 클라이언트에서 쿠키를 읽기 전까지undefined를 반환하여 SSR/CSR 간 불일치 발생Hydration mismatch로 인한 콘솔 경고 및 잠재적 렌더링 이슈
인증 상태 확인 시점이 명확하지 않아 race condition 발생 가능
해결:
AuthHydrationProvider 도입: 서버에서 읽은
initialAccessToken을 클라이언트로 전달하여 초기 렌더링 일관성 보장useAuth() 개선:
isHydrated상태 추가로 hydration 완료 여부 추적useState와useEffect로 클라이언트 쿠키 읽기를 안전하게 처리useAuthReady() 훅 추가:
isAuthReady = isHydrated && isAuthenticated로 인증 체크 시점 명확화컴포넌트에서 안전하게 인증 상태 확인 가능
영향 범위:
30개 이상의 컴포넌트를
useAuthReady()로 업데이트Auth-gated 쿼리 (
notification-api,group-study-member-api) 수정모든 레이아웃 (
(admin),(landing),(service)) 업데이트업데이트된 컴포넌트: (전체 목록)
페이지:
group-study-list-page,mentoring-list-page,premium-study-list-page,my-study/page위젯:
todo-list,my-page/sidebar기능:
start-study-modal,reservation-list,study-matching-toggle,phone-verification-modal그룹 스터디:
apply-group-study-modal,group-study-form-modal,group-study-member-item기타:
voting-detail-view,tab-navigation,admin-sidebar등파일:
추가:
src/hooks/common/auth-hydration-context.tsx수정:
src/hooks/common/use-auth.ts(+useAuthReady()훅)수정:
src/providers/index.tsx(AuthHydrationProvider 적용)문서:
docs/HYDRATION_AUTH_CHECKLIST.md2. Phone Verification Store 개선 🔐
문제:
계정 간 본인인증 상태 오염 가능 (A 계정 인증 → B 계정 로그인 시 인증된 것처럼 보임)
verifiedAt이 Date 객체로 localStorage에 직렬화 불가능Hydration 완료 여부 추적 불가
해결:
memberId필드 추가: 인증 상태가 특정 계정에만 유효하도록 보장verifiedAt을 Date → ISO string으로 변경하여 직렬화 문제 해결hasHydrated상태 추가 및onRehydrateStorage콜백 구현partialize로 필요한 필드만 persist 대상으로 지정파일:
src/features/phone-verification/model/store.ts3. Toast 컴포넌트 UX 개선 🎨
문제:
z-index 충돌로 다른 요소에 가려질 수 있음
퇴장 애니메이션 없이 갑자기 사라짐
렌더링 타이밍 이슈
해결:
React Portal 사용:
document.body에 직접 렌더링하여 z-index 문제 완전 해결부드러운 애니메이션 추가:
진입:
toast-enter(300ms fade-in + slide-down)퇴장:
toast-exit(200ms fade-out + slide-up)상태 관리 개선:
isRendered,isExiting분리로 애니메이션 완료까지 DOM 유지파일:
수정:
src/components/ui/toast.tsx수정:
src/app/global.css(keyframes 애니메이션 정의)4. Voting 공유 기능 개선 📤
문제:
Web Share API 실패 시 fallback 로직 미흡
Clipboard API 실패 시 사용자 피드백 부족
에러 처리가 복잡하고 불명확
해결:
navigator.clipboard.writeText()(최신 브라우저)document.execCommand('copy')(레거시 지원)window.prompt()(수동 복사)각 단계별 성공/실패 처리 명확화
Toast 메시지 동적 처리 (
openShareToast)파일:
src/components/voting/voting-detail-view.tsx5. 유틸리티 및 문서 추가 📚
새 유틸리티:
src/utils/safe-server-prefetch.ts: 서버 사이드 prefetch 에러를 안전하게 처리새 문서:
docs/HYDRATION_AUTH_CHECKLIST.md: Auth hydration 업데이트 추적 체크리스트docs/PRODUCTION_DEPLOYMENT_VERIFICATION.md: 프로덕션 배포 검증 가이드 (작성 중)🧪 테스트 시나리오
Auth Hydration
비로그인 상태에서 페이지 접근 시 hydration 경고 없음
로그인 상태에서 페이지 새로고침 시 인증 상태 즉시 반영
서버 렌더링과 클라이언트 렌더링 결과 일치 확인
토큰 만료 시 자동 갱신 후 상태 정상 동작
Phone Verification
A 계정 인증 → 로그아웃 → B 계정 로그인 시 미인증 상태로 표시
본인인증 완료 후 새로고침해도 인증 상태 유지
memberId불일치 시 자동으로 미인증 처리Toast
Toast가 모든 요소보다 위에 표시됨 (z-index 문제 없음)
진입/퇴장 애니메이션이 부드럽게 작동
여러 Toast가 연속으로 표시될 때 정상 동작
Voting 공유
최신 브라우저에서 Clipboard API로 복사 성공
Clipboard API 미지원 브라우저에서 fallback 동작
복사 성공 시 Toast 표시
수동 복사 프롬프트에서 취소 시 Toast 미표시
📊 영향 분석
변경된 파일 통계
51개 파일 수정 (+1,768줄, -360줄)
3개 파일 추가 (문서 2개, 유틸리티 1개)
영향받는 도메인
인증 (Auth) ⚡️ 높음
본인인증 (Phone Verification) ⚡️ 높음
UI 컴포넌트 (Toast) 🔧 중간
커뮤니티 (Voting 공유) 🔧 중간
🔄 Migration Guide
기존
useAuth()사용하던 컴포넌트Before:
After:
Phone Verification 체크
Before:
After:
🚀 배포 전 확인사항
yarn typecheck통과yarn lint통과yarn build성공Hydration 경고 없음 확인
개발/스테이징 환경 테스트 완료
📝 Breaking Changes
없음 - 기존 API 호환성 유지됨
🔗 관련 이슈
Fixes: Hydration mismatch 콘솔 경고
Fixes: 계정 간 본인인증 상태 오염
Improves: Toast z-index 및 애니메이션
Improves: Voting 공유 신뢰성
🤖 Co-Authored-By: Claude Sonnet 4.5 noreply@anthropic.com
Summary by CodeRabbit
릴리스 노트
새로운 기능
버그 수정
UI/UX 개선