Files
frontend/docs/auth-service-refactoring-guide.md
T

7.3 KiB

서비스별 인증 처리 리팩토링 가이드

1) 목적과 원칙

여러 서비스 앱에 흩어진 인증 로직을 @common/auth 기반으로 통일하기 위한 표준 가이드입니다.
이 문서를 위에서 아래로 적용하면 authService + AuthWrapper 패턴이 갖춰집니다.

핵심 원칙:

  • 인증 정책·유틸은 @common/auth에 모은다.
  • 서비스별 차이는 authConfig 설정으로만 표현한다.
  • 앱은 API 오케스트레이션(authService)과 진입점(AuthWrapper)만 구현한다.
  • login / silentRefresh 성공 시 토큰을 저장한다.
  • 세션 연장은 confirm 확인 시 silentRefresh로 수행한다.
    → 세션 연장 Confirm 적용 가이드

참고 구현: 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. 런타임 흐름

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는 세션 연장 가이드에서 적용한다.


3) 공통 계약

3-1. AuthConfig

interface AuthConfig {
  authUserId: string;
  claimName: string;
  cookieDomain?: string;
  accessToken?: string;
  refreshToken?: string;
}

3-2. AuthService

interface AuthService {
  login(payload: AuthPayload): Promise<void>;
  silentRefresh(): Promise<void>;
  declineSessionExtend(): void;
  logout(): Promise<void>;
}

3-3. TokenManager API

앱에서 사용하는 메서드:

메서드 용도
setToken / getToken 쿠키 저장·조회
clearRefreshTimer declineSessionExtend에 연결
clearAuthToken 인증 쿠키 일괄 삭제

4) 적용 순서

의존 관계 순으로 진행한다.

4-1. 환경 변수 통일

파일: shared/config/env.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

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

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

setToken(CLAIM_NAME, accessToken);
setToken(AUTH_USER_ID, tokenId);

silentRefresh

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

declineSessionExtend: clearRefreshTimer,
사용자 선택 호출
confirm 확인 silentRefresh()
confirm 취소 declineSessionExtend()

logout

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

export const authService: AuthService = {
  login,
  silentRefresh,
  declineSessionExtend: clearRefreshTimer,
  logout,
};

4-5. AuthWrapper

파일: entities/auth/lib/auth-wrapper.tsx
인증 체크와 auth:logout 수신을 구현한다.

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

export { authService } from "./api/auth-service";
export { AuthWrapper } from "./lib/auth-wrapper";
export { tokenManager } from "./lib/token-manager";

4-7. 레이아웃·Provider 연결

import { authService } from "@/entities/auth";

<BaseLayout authService={authService} /* ... */>
  {children}
</BaseLayout>

AppProvider에 AuthWrapper를 마운트한다.


5) 적용 체크리스트

  • env.ts에 인증 관련 환경 변수가 정의되어 있다
  • authConfig가 AuthConfig로 매핑되어 있다
  • createTokenManager(authConfig)로 tokenManager를 생성한다
  • authService가 AuthService 네 메서드를 구현한다
  • entities/auth 배럴로 외부 참조가 통일되어 있다
  • BaseLayout에 authService가 주입된다
  • AppProvider에 AuthWrapper가 마운트되어 있다

6) 후속 단계

위 체크리스트 통과 후 세션 연장 Confirm 적용 가이드를 진행한다.


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