Files
frontend/packages/common/auth

@common/auth

인증 관련 공통 기능을 제공하는 독립 패키지입니다. 의존성 주입 패턴을 통해 각 앱의 환경변수를 주입받아 재사용 가능한 토큰 관리 기능을 제공합니다.

특징

  • 의존성 주입: 환경변수를 직접 참조하지 않고 설정 객체를 주입받음
  • 재사용성: 모든 앱에서 동일한 로직 사용 가능
  • 타입 안정성: TypeScript 인터페이스로 계약 보장
  • 테스트 가능성: Mock 설정 주입으로 단위 테스트 용이

패키지 구조

packages/common/auth/
├── package.json          # 패키지 정의
├── tsconfig.json         # TypeScript 설정
├── index.ts             # 진입점 (Public API)
├── types.ts             # 타입 정의
├── token-manager.ts     # 토큰 관리 로직
├── cookie.ts           # 쿠키 유틸리티
├── device-id.ts        # 기기 ID (쿠키)
├── jwt-utils.ts        # JWT 유틸리티
└── README.md           # 문서

설치

각 앱의 package.json에 의존성을 추가합니다:

{
  "dependencies": {
    "@common/auth": "workspace:*"
  }
}

사용 방법

1. 설정 파일 생성

각 앱에서 자신의 환경변수로 AuthConfig 객체를 생성합니다.

// apps/cs/src/shared/config/auth.ts
import type { AuthConfig } from "@common/auth/types";

export const authConfig: AuthConfig = {
  authUserId: process.env.NEXT_PUBLIC_AUTH_USER_ID || "token-id",
  claimName: process.env.NEXT_PUBLIC_CLAIM_NAME || "Authorization",
  cookieDomain: process.env.NEXT_PUBLIC_COOKIE_DOMAIN,
  accessToken: process.env.NEXT_PUBLIC_ACCESS_TOKEN || "access-token",
  refreshToken: process.env.NEXT_PUBLIC_REFRESH_TOKEN || "refresh-token",
};

2. TokenManager 인스턴스 생성

설정을 주입하여 토큰 매니저 인스턴스를 생성합니다.

// apps/cs/src/entities/auth/lib/token-manager.ts
import { createTokenManager } from "@common/auth";
import { authConfig } from "@/shared/config/auth";

export const tokenManager = createTokenManager(authConfig);

// 편의를 위한 re-export
export const {
  getToken,
  setToken,
  removeToken,
  setupRefreshTimer,
  clearAuthToken,
  getAccessToken,
  getRefreshToken,
  getUserId,
} = tokenManager;

3. 컴포넌트에서 사용

// apps/cs/src/features/auth/ui/LoginForm.tsx
import { tokenManager } from "@/entities/auth/lib/token-manager";
import { authConfig } from "@/shared/config/auth";

export function LoginForm() {
  const handleLogin = async (credentials: LoginCredentials) => {
    const response = await loginApi(credentials);
    
    // 토큰 저장
    tokenManager.setToken(authConfig.claimName, response.accessToken);
    tokenManager.setToken(authConfig.refreshToken, response.refreshToken);
    
    // 또는 편의 메서드 사용
    // tokenManager.getAccessToken();
    // tokenManager.getRefreshToken();
    // tokenManager.getUserId();
  };

  return (
    // ... JSX
  );
}

API 문서

createTokenManager(config: AuthConfig): TokenManager

토큰 매니저 인스턴스를 생성하는 팩토리 함수입니다.

Parameters:

  • config: AuthConfig - 인증 설정 객체

Returns:

  • TokenManager - 토큰 관리 메서드를 포함한 객체

TokenManager 메서드

getToken(key: string): string | undefined

지정된 키의 쿠키 값을 가져옵니다.

const token = tokenManager.getToken("access-token");

setToken(key: string, value: string): void

토큰을 쿠키에 저장합니다. 자동으로 도메인, secure, sameSite 설정을 적용합니다.

tokenManager.setToken("access-token", "eyJhbGc...");

removeToken(key: string): void

지정된 키의 쿠키를 삭제합니다.

tokenManager.removeToken("access-token");

setupRefreshTimer(callback: () => Promise<void>, delayMs: number): void

토큰 갱신 타이머를 설정합니다.

tokenManager.setupRefreshTimer(async () => {
  await refreshAccessToken();
}, 3600000); // 1시간 후

clearAuthToken(): void

모든 인증 토큰을 삭제하고 타이머를 정리합니다.

tokenManager.clearAuthToken();

getAccessToken(): string | undefined

액세스 토큰을 가져오는 편의 메서드입니다.

const accessToken = tokenManager.getAccessToken();

getRefreshToken(): string | undefined

리프레시 토큰을 가져오는 편의 메서드입니다.

const refreshToken = tokenManager.getRefreshToken();

getUserId(): string | undefined

사용자 ID 토큰을 가져오는 편의 메서드입니다.

const userId = tokenManager.getUserId();

JWT 유틸리티

getJwtExpiry(token: string): number | null

JWT 토큰의 만료 시간(Unix timestamp, 초 단위)을 반환합니다.

import { getJwtExpiry } from "@common/auth";

const expiry = getJwtExpiry(token);
if (expiry) {
  console.log("Token expires at:", new Date(expiry * 1000));
}

getRemainingMs(token: string, bufferMs?: number): number

토큰 만료 bufferMs 전까지 남은 시간(밀리초)을 반환합니다. 기본 bufferMs는 REFRESH_BUFFER_MS(1분)입니다.

import {
  getRemainingMs,
  REFRESH_BUFFER_MS,
  SESSION_EXTEND_CONFIRM_BUFFER_MS,
} from "@common/auth";

const remaining = getRemainingMs(token);
const confirmIn = getRemainingMs(token, SESSION_EXTEND_CONFIRM_BUFFER_MS);
const isValid = getRemainingMs(token, 0) > 0;

쿠키 유틸리티

getCookie(name: string): string | undefined

쿠키 값을 가져옵니다.

import { getCookie } from "@common/auth";

const value = getCookie("my-cookie");

setCookie(name: string, value: string, options?: CookieOptions): void

쿠키를 설정합니다.

import { setCookie } from "@common/auth";

setCookie("my-cookie", "value", {
  secure: true,
  sameSite: "lax",
  path: "/",
});

removeCookie(name: string, options?: CookieOptions): void

쿠키를 삭제합니다.

import { removeCookie } from "@common/auth";

removeCookie("my-cookie", { path: "/" });

타입 정의

AuthConfig

interface AuthConfig {
  authUserId: string;        // 사용자 ID 토큰 키
  claimName: string;         // 액세스 토큰 키
  cookieDomain?: string;     // 쿠키 도메인 (선택)
  accessToken?: string;      // 액세스 토큰 키 (선택)
  refreshToken?: string;     // 리프레시 토큰 키 (선택)
}

CookieOptions

interface CookieOptions {
  secure?: boolean;
  sameSite?: "strict" | "lax" | "none";
  domain?: string;
  path?: string;
  expires?: number | Date;
}

마이그레이션 가이드

기존 앱에서 @common/auth로 마이그레이션

1. 의존성 추가

# 앱 디렉토리에서
cd apps/your-app

package.json에 추가:

{
  "dependencies": {
    "@common/auth": "workspace:*"
  }
}
pnpm install

2. 설정 파일 생성

// apps/your-app/src/shared/config/auth.ts
import type { AuthConfig } from "@common/auth/types";

export const authConfig: AuthConfig = {
  authUserId: process.env.NEXT_PUBLIC_AUTH_USER_ID || "token-id",
  claimName: process.env.NEXT_PUBLIC_CLAIM_NAME || "Authorization",
  cookieDomain: process.env.NEXT_PUBLIC_COOKIE_DOMAIN,
  accessToken: process.env.NEXT_PUBLIC_ACCESS_TOKEN || "access-token",
  refreshToken: process.env.NEXT_PUBLIC_REFRESH_TOKEN || "refresh-token",
};

3. TokenManager 수정

Before (기존 코드):

// apps/your-app/src/entities/auth/lib/token-manager.ts
import { AUTH_USER_ID, CLAIM_NAME } from "@/shared/config";
import { getCookie, setCookie, removeCookie } from "@/shared/lib";

export function getToken(key: string): string | undefined {
  return getCookie(key);
}

export function clearAuthToken(): void {
  removeToken(CLAIM_NAME);
  removeToken(AUTH_USER_ID);
}

After (@common/auth 사용):

// apps/your-app/src/entities/auth/lib/token-manager.ts
import { createTokenManager } from "@common/auth";
import { authConfig } from "@/shared/config/auth";

export const tokenManager = createTokenManager(authConfig);

// 기존 코드와의 호환성을 위한 re-export
export const {
  getToken,
  setToken,
  removeToken,
  setupRefreshTimer,
  clearAuthToken,
  getAccessToken,
  getRefreshToken,
  getUserId,
} = tokenManager;

4. 사용처 업데이트

클래스 기반 (st, cm 앱):

// Before
import { TokenManager } from "@/entities/auth/lib/token-manager";
const tokenManager = TokenManager.getInstance();

// After
import { tokenManager } from "@/entities/auth/lib/token-manager";
// 그대로 사용 가능

함수형 (cs 앱):

// Before
import { getToken, setToken, clearAuthToken } from "@/entities/auth/lib/token-manager";

// After
import { getToken, setToken, clearAuthToken } from "@/entities/auth/lib/token-manager";
// 그대로 사용 가능 (re-export 덕분)

다른 앱에서 사용 예시

AS 앱

// apps/as/src/shared/config/auth.ts
import type { AuthConfig } from "@common/auth/types";

export const authConfig: AuthConfig = {
  authUserId: process.env.NEXT_PUBLIC_AS_AUTH_USER_ID || "as-token-id",
  claimName: process.env.NEXT_PUBLIC_AS_CLAIM_NAME || "X-AS-Auth",
  cookieDomain: process.env.NEXT_PUBLIC_AS_COOKIE_DOMAIN,
  accessToken: process.env.NEXT_PUBLIC_AS_ACCESS_TOKEN || "as-access-token",
  refreshToken: process.env.NEXT_PUBLIC_AS_REFRESH_TOKEN || "as-refresh-token",
};

ST 앱

// apps/st/src/shared/config/auth.ts
import type { AuthConfig } from "@common/auth/types";

export const authConfig: AuthConfig = {
  authUserId: process.env.NEXT_PUBLIC_AUTH_USER_ID || "token-id",
  claimName: process.env.NEXT_PUBLIC_CLAIM_NAME || "Authorization",
  cookieDomain: ".hasuon.or.kr", // 하드코딩된 도메인도 설정으로 이동
  accessToken: process.env.NEXT_PUBLIC_ACCESS_TOKEN || "access-token",
  refreshToken: process.env.NEXT_PUBLIC_REFRESH_TOKEN || "refresh-token",
};

환경변수 설정

각 앱의 .env.local 파일:

# CS 앱
NEXT_PUBLIC_CLAIM_NAME=Authorization
NEXT_PUBLIC_ACCESS_TOKEN=access-token
NEXT_PUBLIC_REFRESH_TOKEN=refresh-token
NEXT_PUBLIC_AUTH_USER_ID=token-id
NEXT_PUBLIC_COOKIE_DOMAIN=.example.com

# AS 앱 (다른 설정 사용 가능)
NEXT_PUBLIC_AS_CLAIM_NAME=X-AS-Auth
NEXT_PUBLIC_AS_ACCESS_TOKEN=as-access-token
NEXT_PUBLIC_AS_REFRESH_TOKEN=as-refresh-token
NEXT_PUBLIC_AS_AUTH_USER_ID=as-token-id
NEXT_PUBLIC_AS_COOKIE_DOMAIN=.as.example.com

테스트 예시

import { createTokenManager } from "@common/auth";
import type { AuthConfig } from "@common/auth/types";

describe("TokenManager", () => {
  const mockConfig: AuthConfig = {
    authUserId: "test-user-id",
    claimName: "test-claim",
    cookieDomain: ".test.com",
    accessToken: "test-access",
    refreshToken: "test-refresh",
  };

  it("should create token manager with config", () => {
    const tokenManager = createTokenManager(mockConfig);
    expect(tokenManager).toBeDefined();
    expect(tokenManager.getToken).toBeDefined();
  });

  it("should use injected config", () => {
    const tokenManager = createTokenManager(mockConfig);
    // 테스트 로직...
  });
});

주의사항

  1. 환경변수는 빌드 타임에 주입됩니다: process.env.NEXT_PUBLIC_* 값은 빌드 시점에 정적으로 치환됩니다.

  2. 쿠키 도메인 자동 감지: cookieDomain을 설정하지 않으면 자동으로 현재 호스트명을 사용합니다.

    • localhost / 127.0.0.1: 도메인 설정 안 함
    • 기타: 현재 호스트명 사용
  3. HTTPS 자동 감지: secure 옵션은 현재 프로토콜에 따라 자동 설정됩니다.

  4. 타이머 관리: setupRefreshTimer는 이전 타이머를 자동으로 정리합니다.

문제 해결

TypeScript 오류: "Cannot find module '@common/auth'"

  1. pnpm install 실행
  2. TypeScript 서버 재시작
  3. tsconfig.json의 paths 설정 확인

쿠키가 저장되지 않음

  1. 브라우저 개발자 도구에서 쿠키 확인
  2. cookieDomain 설정 확인
  3. HTTPS 환경에서 secure: true 확인

토큰이 다른 앱에서 공유되지 않음

  1. cookieDomain 설정이 공통 도메인(예: .example.com)인지 확인
  2. 각 앱의 토큰 키 이름이 다른지 확인 (충돌 방지)

라이선스

ISC