13 KiB
세션 연장 Confirm 적용 가이드
1) 문서 목적
JWT 만료 5분 전에 "로그인을 연장하시겠습니까?" confirm 창을 띄우고, 사용자 선택에 따라 세션을 연장하거나 자동 갱신을 중단하는 기능을 서비스 앱에 적용하기 위한 가이드입니다.
이 문서는 서비스별 인증 처리 리팩토링 가이드의 후속 단계로 읽습니다.
@common/auth 기반 authService + token-manager + AuthWrapper 패턴이 먼저 갖춰져 있어야 합니다.
2) 선행 조건
아래 항목이 완료된 서비스만 이 가이드를 적용합니다.
| 항목 | 확인 |
|---|---|
createTokenManager(authConfig) 사용 |
3-3. 토큰 매니저 공통화 |
authService.login / silentRefresh에서 getRemainingMs + setupRefreshTimer |
3-4. 로그인 성공 처리 표준화 |
authService.declineSessionExtend 구현 |
confirm 취소 시 refresh 타이머 해제 |
AuthWrapper에서 새로고침 시 refresh 타이머 복구 |
cs / as / st 참고 |
ConfirmProvider + ConfirmDialog 마운트 |
AppProvider |
ConfirmProvider 하위에 BaseLayout → Header 렌더링 |
useConfirm 사용 조건 |
BaseLayout에 authService prop 전달 |
Header까지 전달됨 |
적용 완료 앱: cs, as, st
미적용: wr, sn, fo 등 legacy LoginApi 패턴 앱
3) 전체 흐름
로그인 이후 JWT exp를 기준으로 두 개의 독립 타이머가 동작합니다.
sequenceDiagram
participant Login as authService.login
participant RefreshTimer as refreshTimer
participant Header as Header
participant User as 사용자
participant API as authService.silentRefresh
Login->>RefreshTimer: setupRefreshTimer(exp - 1분)
Header->>Header: confirm 예약(exp - 5분)
Note over Header: 만료 5분 전
Header->>User: 로그인을 연장하시겠습니까?
alt 확인
User->>API: silentRefresh()
API->>RefreshTimer: 새 토큰 기준 타이머 재설정
Header->>Header: confirm 타이머 재설정
else 취소
Header->>API: declineSessionExtend()
API->>RefreshTimer: clearRefreshTimer()
Note over RefreshTimer: 취소시 만료까지 자동 갱신 없음
end
| 시점 | 동작 | 담당 |
|---|---|---|
| 로그인 / silentRefresh 성공 | 만료 1분 전 자동 silentRefresh 예약 |
authService → setupRefreshTimer |
| 만료 5분 전 | confirm 표시 | Header → useSessionExtendConfirm |
| confirm 확인 | 즉시 silentRefresh + 타이머 재설정 |
Header → authService.silentRefresh |
| confirm 취소 | 자동 silentRefresh 타이머 해제 |
Header → authService.declineSessionExtend |
3-1. 시간 계산 (getRemainingMs)
[packages/common/auth/jwt-utils.ts](../packages/common/auth/jwt-utils.ts)
export const REFRESH_BUFFER_MS = 60_000; // silent refresh: 만료 1분 전
export const SESSION_EXTEND_CONFIRM_BUFFER_MS = 5 * 60_000; // confirm: 만료 5분 전
getRemainingMs(token); // → refresh 타이머 delay
getRemainingMs(token, SESSION_EXTEND_CONFIRM_BUFFER_MS); // → confirm 타이머 delay
getRemainingMs(token, 0) > 0; // → 토큰 미만료 여부
bufferMs 인자 하나로 용도를 구분합니다. 기존 getRemainingMs(token) 호출부는 변경하지 않아도 됩니다.
4) 공통 모듈 변경 사항
4-1. @common/auth — AuthService / clearRefreshTimer
[packages/common/auth/types.ts](../packages/common/auth/types.ts)
export interface AuthService {
login(payload: AuthPayload): Promise<void>;
silentRefresh(): Promise<void>;
declineSessionExtend(): void; // confirm 취소 시 refresh 타이머 해제
logout(): Promise<void>;
}
[packages/common/auth/token-manager.ts](../packages/common/auth/token-manager.ts)
setupRefreshTimer: 만료 1분 전silentRefresh예약 (기존)clearRefreshTimer: 예약된 refresh 타이머 해제 (신규)clearAuthToken: 내부에서clearRefreshTimer호출
refreshTimer는 createTokenManager() 클로저 안의 앱당 싱글톤 변수입니다.
authService.declineSessionExtend()가 내부에서 clearRefreshTimer()를 호출하므로, Header는 token-manager를 직접 import하지 않습니다.
4-2. @common/components — Header + useSessionExtendConfirm
[packages/common/components/src/ui/header/index.tsx](../packages/common/components/src/ui/header/index.tsx)에서 훅을 실행합니다.
useSessionExtendConfirm({
enabled:
mounted &&
isLoggedIn &&
!viewOnly &&
!!authService &&
!isAtLoginRedirectUrl(getAuthLoginRedirectUrl()),
getAccessToken: () => getCookie(appConfig.CLAIM_NAME ?? ""),
silentRefresh: authService.silentRefresh,
clearRefreshTimer: authService.declineSessionExtend,
});
| enabled 조건 | 이유 |
|---|---|
mounted |
hydration 후 타이머 시작 |
isLoggedIn |
비로그인 제외 |
!viewOnly |
VBaseLayout 등 조회 전용 제외 |
!!authService |
legacy 앱 자동 비활성 |
!isAtLoginRedirectUrl(...) |
로그인 페이지 제외 |
세션 만료 카운트다운 UI와 confirm 로직이 Header에 응집됩니다.
5) 서비스별 적용 단계
5-1. auth-service에 declineSessionExtend 추가
[apps/cs/src/entities/auth/api/auth-service.ts](../apps/cs/src/entities/auth/api/auth-service.ts) 참고:
import { clearRefreshTimer } from "../lib/token-manager";
function declineSessionExtend(): void {
clearRefreshTimer();
}
export const authService: AuthService = {
login,
silentRefresh,
declineSessionExtend,
logout,
};
5-2. BaseLayout에 authService 전달
[apps/cs/app/(main)/(with-base-layout)/layout.tsx](../apps/cs/app/(main)/(with-base-layout)/layout.tsx) 참고:
<BaseLayout
queryClient={queryClient}
authService={authService}
currentPath={pathname ?? ""}
handleMenuClick={handleMenuClick}
>
BaseLayout은 authService를 Header에 그대로 전달합니다. AuthWrapper에서 훅을 연결하지 않습니다.
5-3. AuthWrapper — refresh 타이머 복구만 유지
[apps/cs/src/entities/auth/lib/auth-wrapper.tsx](../apps/cs/src/entities/auth/lib/auth-wrapper.tsx):
useSessionExtendConfirm사용하지 않음- 페이지 새로고침 시
setupRefreshTimer(authService.silentRefresh, remainingMs)복구 유지 - 인증 체크,
auth:logout수신 유지
5-4. AppProvider 구조 확인
[apps/cs/src/app/providers/app-provider.tsx](../apps/cs/src/app/providers/app-provider.tsx) 참고:
<ConfirmProvider>
<AuthWrapper>
{children}
<ConfirmDialog />
</AuthWrapper>
</ConfirmProvider>
Header는 ConfirmProvider 하위에서 렌더링되므로 useConfirm()이 동작합니다.
6) 책임 분리 요약
| 레이어 | 파일 | 역할 |
|---|---|---|
| JWT 시간 계산 | @common/auth jwt-utils.ts |
getRemainingMs, 버퍼 상수 |
| refresh 타이머 | @common/auth token-manager.ts |
setupRefreshTimer, clearRefreshTimer |
| API 오케스트레이션 | entities/auth/api/auth-service.ts |
login, silentRefresh, declineSessionExtend, logout |
| confirm UX | @common/components Header |
카운트다운 UI + 5분 전 confirm |
| 앱 진입점 | entities/auth/lib/auth-wrapper.tsx |
타이머 복구, auth 체크 |
| 레이아웃 | app/.../layout.tsx |
authService → BaseLayout → Header |
| UI 쉘 | app/providers/app-provider.tsx |
ConfirmProvider, ConfirmDialog |
7) 엣지 케이스
| 상황 | 동작 |
|---|---|
| 토큰 유효 시간 < 5분 | confirmMs === 0이면 페이지 진입 즉시 confirm |
| confirm 취소 | declineSessionExtend() + 동일 JWT에 재표시 안 함 |
| confirm 확인 | silentRefresh() → 새 토큰 기준 confirm/refresh 타이머 재설정 |
silentRefresh 실패 |
authService가 auth:logout 발행 → AuthWrapper 리다이렉트 |
| 로그아웃 | clearAuthToken이 refresh 타이머 정리, 훅 cleanup |
viewOnly Header |
confirm 비활성 |
authService 미전달 |
confirm 비활성 (wr/fo 등) |
| 페이지 새로고침 | AuthWrapper가 refresh 타이머 복구, Header가 confirm 타이머 복구 |
8) 적용 체크리스트
리팩토링 가이드 체크리스트에 더해 아래를 확인합니다.
auth-service에declineSessionExtend구현token-manager에서clearRefreshTimerre-exportBaseLayout에authServiceprop 전달 (→ Header까지)ConfirmProvider>AuthWrapper>BaseLayout>Header계층 확인AuthWrapper에useSessionExtendConfirm없음 확인- 만료 5분 전 confirm 노출 확인
- 확인 클릭 시
SILENT_REFRESHAPI 호출 확인 - 취소 클릭 시 만료 1분 전 자동 refresh 미발생 확인
9) 검증 방법
- 로그인 후 JWT
exp가 5분 이내가 되도록 대기하거나 테스트 토큰 사용 - confirm 문구
"로그인을 연장하시겠습니까?"표시 확인 - 확인 → Network 탭에서
SILENT_REFRESHPUT 호출, 쿠키 갱신, confirm 재예약 - 취소 → 만료 1분 전까지
SILENT_REFRESH자동 호출 없음 - 취소 후 세션 만료 → 401 / 로그인 리다이렉트
- 새로고침 후 refresh·confirm 타이머 복구
10) 참고 구현
| 앱 | Header (공통) | auth-service | BaseLayout layout |
|---|---|---|---|
| cs | [packages/common/components/src/ui/header/index.tsx](../packages/common/components/src/ui/header/index.tsx) |
[apps/cs/src/entities/auth/api/auth-service.ts](../apps/cs/src/entities/auth/api/auth-service.ts) |
[apps/cs/app/(main)/(with-base-layout)/layout.tsx](../apps/cs/app/(main)/(with-base-layout)/layout.tsx) |
| as | (동일) | [apps/as/src/entities/auth/api/auth-service.ts](../apps/as/src/entities/auth/api/auth-service.ts) |
[apps/as/src/app/layouts/base-layout.tsx](../apps/as/src/app/layouts/base-layout.tsx) |
| st | (동일) | [apps/st/src/entities/auth/api/auth-service.ts](../apps/st/src/entities/auth/api/auth-service.ts) |
[apps/st/app/(main)/(with-base-layout)/layout.tsx](../apps/st/app/(main)/(with-base-layout)/layout.tsx) |
11) 관련 문서
- 서비스별 인증 처리 리팩토링 가이드 — 선행 리팩토링
[packages/common/auth/README.md](../packages/common/auth/README.md)—getRemainingMs, token-manager API