-
Notifications
You must be signed in to change notification settings - Fork 4
chore(web): 로그인 불안정 원인 분석용 인증 디버그 계측 추가 #471
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,51 @@ | ||
| # Login Instability Investigation Guide | ||
|
|
||
| ## Scope | ||
| - Symptoms: sudden redirect to `/login`, intermittent 401, or authenticated user UI showing as unauthenticated. | ||
| - Target routes: `/my`, `/mentor`, `/community/*`. | ||
|
|
||
| ## Enable Auth Debug Logs | ||
| 1. Open browser devtools console. | ||
| 2. Run `localStorage.setItem("authDebug", "1")`. | ||
| 3. Hard refresh. | ||
| 4. Reproduce once. | ||
| 5. Export logs with: | ||
| - `copy(JSON.stringify(window.__AUTH_DEBUG_LOGS__ ?? [], null, 2))` | ||
|
|
||
| Optional (server-side middleware logs): | ||
| - Set `NEXT_PUBLIC_AUTH_DEBUG=true` and run the app. | ||
| - Middleware prints `[AUTH_DEBUG][middleware.*]` logs in server output. | ||
|
|
||
| ## Reproduction Matrix | ||
| 1. Logged-in user hard refreshes `/my`. | ||
| 2. Logged-in user hard refreshes `/mentor`. | ||
| 3. Logged-in user navigates to `/community` and `/community/[boardCode]/create`. | ||
| 4. User with expired access token but valid refresh token opens protected route. | ||
| 5. User keeps app idle until access token expires, then triggers API call. | ||
|
|
||
| ## What To Check | ||
| - `request.start` with `hasAccessToken=true` and `tokenExpired=true`. | ||
| - Immediate `response.401` after sending expired access token. | ||
| - Whether `reissue.start` appears before redirect. | ||
| - `auth.redirectToLogin` reason/message and preceding request URL. | ||
| - Middleware redirect events where `hasRefreshToken=false`. | ||
|
|
||
| ## Interpretation Rules | ||
| - `tokenExpired=true` + no `reissue.start` + `response.401`: | ||
| likely missing proactive refresh for expired access tokens. | ||
| - Frequent `reissue.failed` while middleware has refresh cookie: | ||
| likely backend `/auth/reissue` or cookie attribute/domain issue. | ||
| - Multiple `reissue.start` from same user action: | ||
| possible reissue concurrency/race condition. | ||
| - Middleware redirects before app boot: | ||
| refresh token cookie missing/expired at edge layer. | ||
|
|
||
| ## Deliverables | ||
| - Reproduction steps used. | ||
| - Exported `window.__AUTH_DEBUG_LOGS__` JSON. | ||
| - Network HAR covering `/auth/reissue` and first failing 401. | ||
| - Suspected root cause category: | ||
| - token expiry handling | ||
| - refresh failure/cookie issue | ||
| - race condition | ||
| - route guard mismatch |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,47 @@ | ||
| type AuthDebugDetails = Record<string, unknown>; | ||
|
|
||
| type AuthDebugEvent = { | ||
| at: string; | ||
| event: string; | ||
| details: AuthDebugDetails; | ||
| }; | ||
|
|
||
| declare global { | ||
| interface Window { | ||
| __AUTH_DEBUG_LOGS__?: AuthDebugEvent[]; | ||
| } | ||
| } | ||
|
|
||
| const AUTH_DEBUG_STORAGE_KEY = "authDebug"; | ||
|
|
||
| const isAuthDebugEnabled = (): boolean => { | ||
| const envEnabled = process.env.NEXT_PUBLIC_AUTH_DEBUG === "true"; | ||
|
|
||
| if (typeof window === "undefined") { | ||
| return envEnabled; | ||
| } | ||
|
|
||
| try { | ||
| const storageEnabled = window.localStorage.getItem(AUTH_DEBUG_STORAGE_KEY) === "1"; | ||
| return envEnabled || storageEnabled; | ||
| } catch { | ||
| return envEnabled; | ||
| } | ||
| }; | ||
|
|
||
| export const authDebugLog = (event: string, details: AuthDebugDetails = {}): void => { | ||
| if (!isAuthDebugEnabled()) return; | ||
|
|
||
| const payload: AuthDebugEvent = { | ||
| at: new Date().toISOString(), | ||
| event, | ||
| details, | ||
| }; | ||
|
|
||
| if (typeof window !== "undefined") { | ||
| window.__AUTH_DEBUG_LOGS__ = window.__AUTH_DEBUG_LOGS__ ?? []; | ||
| window.__AUTH_DEBUG_LOGS__.push(payload); | ||
|
Comment on lines
+41
to
+43
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🌐 Web query:
💡 Result: They’re reset. A navigation triggered by One important exception: if the user later returns via Back/Forward Cache (bfcache), the browser may restore the prior page from memory and preserve its JS state, including globals—but that’s a page restoration, not a normal full reload. (developer.mozilla.org) Citations:
현재 debug buffer가 이번 PR이 정확히 해결하려는 "갑작스러운
이렇게 하면 🤖 Prompt for AI Agents |
||
| } | ||
|
|
||
| console.info("[AUTH_DEBUG]", payload); | ||
| }; | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🌐 Web query:
In Zustand persist middleware, does mutatingstatedirectly insideonRehydrateStoragenotify subscribers, or shouldsetState/ store actions be used instead?💡 Result:
Directly mutating the
stateobject you receive inonRehydrateStoragewill not notify Zustand subscribers. Zustand only notifies subscribers when the store’ssetStateruns (it updates the internalstatereference and then calls alllisteners.forEach(...)).[1]So inside
onRehydrateStorage, you should update via store actions (which callset) or callstore.setState(...)—not by mutatingstatein-place.[2][3]Sources:
[1] Zustand core
setStateimplementation calls listeners only fromsetState(src/vanilla.ts) (github.com)[2] Persist middleware describes
onRehydrateStorageas a hook for custom logic around rehydration (zustand.docs.pmnd.rs)[3] Example showing using an action inside
onRehydrateStorageworks (GitHub issue#830) (github.com)Citations:
Zustand 하이드레이션 완료 상태를 구독자에게 올바르게 알려주세요.
onRehydrateStorage콜백 내에서state객체를 직접 변경하면 Zustand의 구독자 알림이 작동하지 않습니다. 현재 코드처럼state.userRole과state.isInitialized를 직접 대입하면, 이 값들을 구독하는 컴포넌트가 하이드레이션 완료를 감지하지 못해 UI가 업데이트되지 않을 수 있습니다.하이드레이션 로직을 다음 중 하나로 개선해주세요:
setUserRole,setInitialized)를onRehydrateStorage내에서 호출setState경로 사용:store.setState()를 통해 상태 업데이트이렇게 하면 Zustand가 올바르게 모든 구독자에게 변경을 알릴 수 있고, 로그인 안정성 조사 시 상태 전이를 명확하게 추적할 수 있습니다.
🤖 Prompt for AI Agents