# 서비스별 인증 처리 리팩토링 가이드 ## 1) 목적과 원칙 여러 서비스 앱에 흩어진 인증 로직을 `@common/auth` 기반으로 통일하기 위한 표준 가이드입니다. 이 문서를 **위에서 아래로** 적용하면 `authService` + `AuthWrapper` 패턴이 갖춰집니다. 핵심 원칙: - 인증 **정책·유틸**은 `@common/auth`에 모은다. - 서비스별 차이는 `authConfig` 설정으로만 표현한다. - 앱은 API 오케스트레이션(`authService`)과 진입점(`AuthWrapper`)만 구현한다. - `login` / `silentRefresh` 성공 시 토큰을 저장한다. - 세션 연장은 confirm **확인** 시 `silentRefresh`로 수행한다. → [세션 연장 Confirm 적용 가이드](./session-extend-confirm-guide.md) 참고 구현: `apps/as/src/entities/auth/api/auth-service.ts` --- ## 2) 목표 아키텍처 ### 2-1. 레이어 책임 | 레이어 | 경로 | 역할 | | --- | --- | --- | | 환경 변수 | `shared/config/env.ts` | `CLAIM_NAME` 등 키/도메인 값 노출 | | 인증 설정 | `shared/config/auth.ts` | env → `AuthConfig` 매핑 | | 토큰 매니저 | `entities/auth/lib/token-manager.ts` | `createTokenManager(authConfig)` + re-export | | 인증 서비스 | `entities/auth/api/auth-service.ts` | login / silentRefresh / declineSessionExtend / logout | | 인증 래퍼 | `entities/auth/lib/auth-wrapper.tsx` | auth 체크, `auth:logout` 수신 | | 배럴 | `entities/auth/index.ts` | 외부 import 경로 통일 | | 레이아웃 | `BaseLayout` 등 | `authService`를 Header까지 전달 | ### 2-2. 런타임 흐름 ```mermaid flowchart TD A[login / silentRefresh 성공] --> B[setToken] C[세션 연장 confirm 확인] --> D[silentRefresh] D --> B E[세션 연장 confirm 취소] --> F[declineSessionExtend] F --> G[clearRefreshTimer] H[갱신 실패 / logout] --> I[clearAuthToken + auth:logout] ``` 세션 연장 confirm UX는 [세션 연장 가이드](./session-extend-confirm-guide.md)에서 적용한다. --- ## 3) 공통 계약 ### 3-1. AuthConfig ```ts interface AuthConfig { authUserId: string; claimName: string; cookieDomain?: string; accessToken?: string; refreshToken?: string; } ``` ### 3-2. AuthService ```ts interface AuthService { login(payload: AuthPayload): Promise; silentRefresh(): Promise; declineSessionExtend(): void; logout(): Promise; } ``` ### 3-3. TokenManager API 앱에서 사용하는 메서드: | 메서드 | 용도 | | --- | --- | | `setToken` / `getToken` | 쿠키 저장·조회 | | `clearRefreshTimer` | `declineSessionExtend`에 연결 | | `clearAuthToken` | 인증 쿠키 일괄 삭제 | --- ## 4) 적용 순서 의존 관계 순으로 진행한다. ### 4-1. 환경 변수 통일 파일: `shared/config/env.ts` ```ts export const CLAIM_NAME = process.env.NEXT_PUBLIC_CLAIM_NAME || "Authorization"; export const AUTH_USER_ID = process.env.NEXT_PUBLIC_AUTH_USER_ID || "token-id"; export const ACCESS_TOKEN = process.env.NEXT_PUBLIC_ACCESS_TOKEN || "access-token"; export const REFRESH_TOKEN = process.env.NEXT_PUBLIC_REFRESH_TOKEN || "refresh-token"; export const COOKIE_DOMAIN = process.env.NEXT_PUBLIC_COOKIE_DOMAIN; ``` ### 4-2. 인증 설정 파일: `shared/config/auth.ts` ```ts import type { AuthConfig } from "@common/auth"; import { ACCESS_TOKEN, AUTH_USER_ID, CLAIM_NAME, COOKIE_DOMAIN, REFRESH_TOKEN, } from "./env"; export const authConfig: AuthConfig = { authUserId: AUTH_USER_ID, claimName: CLAIM_NAME, cookieDomain: COOKIE_DOMAIN, accessToken: ACCESS_TOKEN, refreshToken: REFRESH_TOKEN, }; ``` ### 4-3. 토큰 매니저 파일: `entities/auth/lib/token-manager.ts` ```ts import { authConfig } from "@/shared/config/auth"; import { createTokenManager } from "@common/auth"; export const tokenManager = createTokenManager(authConfig); export const { getToken, setToken, removeToken, clearRefreshTimer, getAccessToken, getRefreshToken, getUserId, } = tokenManager; ``` ### 4-4. 인증 서비스 파일: `entities/auth/api/auth-service.ts` 참고: `apps/as/src/entities/auth/api/auth-service.ts` #### login ```ts setToken(CLAIM_NAME, accessToken); setToken(AUTH_USER_ID, tokenId); ``` #### silentRefresh ```ts async function silentRefresh() { try { // SILENT_REFRESH API → setToken(...) } catch (error) { // clearAuthToken은 내부에서 this를 쓰므로 tokenManager 객체로 호출 tokenManager.clearAuthToken(); if (typeof window !== "undefined") { window.dispatchEvent(new CustomEvent("auth:logout")); } } } ``` #### declineSessionExtend ```ts declineSessionExtend: clearRefreshTimer, ``` | 사용자 선택 | 호출 | | --- | --- | | confirm **확인** | `silentRefresh()` | | confirm **취소** | `declineSessionExtend()` | #### logout ```ts async function logout() { tokenManager.clearAuthToken(); clearDeviceId(); if (typeof window !== "undefined") { window.dispatchEvent(new CustomEvent("auth:logout")); } } ``` httpOnly refresh 쿠키 삭제가 필요하면 `fetch("/api/auth/logout")`를 먼저 호출한다 (`apps/st` 참고). #### export ```ts export const authService: AuthService = { login, silentRefresh, declineSessionExtend: clearRefreshTimer, logout, }; ``` ### 4-5. AuthWrapper 파일: `entities/auth/lib/auth-wrapper.tsx` 인증 체크와 `auth:logout` 수신을 구현한다. ```tsx useEffect(() => { // authCheck, 로그인 리다이렉트 }, []); useEffect(() => { function handleAuthLogout() { setUserInfo(null); if (!isAtLoginRedirectUrl(AUTH_LOGIN_REDIRECT_URL)) { router.replace(AUTH_LOGIN_REDIRECT_URL); } } window.addEventListener("auth:logout", handleAuthLogout); return () => window.removeEventListener("auth:logout", handleAuthLogout); }, []); ``` ### 4-6. 배럴 파일: `entities/auth/index.ts` ```ts export { authService } from "./api/auth-service"; export { AuthWrapper } from "./lib/auth-wrapper"; export { tokenManager } from "./lib/token-manager"; ``` ### 4-7. 레이아웃·Provider 연결 ```tsx import { authService } from "@/entities/auth"; {children} ``` `AppProvider`에 `AuthWrapper`를 마운트한다. --- ## 5) 적용 체크리스트 - [ ] `env.ts`에 인증 관련 환경 변수가 정의되어 있다 - [ ] `authConfig`가 `AuthConfig`로 매핑되어 있다 - [ ] `createTokenManager(authConfig)`로 tokenManager를 생성한다 - [ ] `authService`가 `AuthService` 네 메서드를 구현한다 - [ ] `entities/auth` 배럴로 외부 참조가 통일되어 있다 - [ ] `BaseLayout`에 `authService`가 주입된다 - [ ] `AppProvider`에 `AuthWrapper`가 마운트되어 있다 --- ## 6) 후속 단계 위 체크리스트 통과 후 [세션 연장 Confirm 적용 가이드](./session-extend-confirm-guide.md)를 진행한다. --- ## 7) 참고 구현 | 앱 | auth-service | | --- | --- | | as (기준) | `apps/as/src/entities/auth/api/auth-service.ts` | | cs | `apps/cs/src/entities/auth/api/auth-service.ts` | | st | `apps/st/src/entities/auth/api/auth-service.ts` | | cm | `apps/cm/src/entities/auth/api/auth-service.ts` |