Files
frontend/packages/common/auth/README.md
T

493 lines
12 KiB
Markdown

# @common/auth
인증 관련 공통 기능을 제공하는 독립 패키지입니다. 의존성 주입 패턴을 통해 각 앱의 환경변수를 주입받아 재사용 가능한 토큰 관리 기능을 제공합니다.
## 특징
- **의존성 주입**: 환경변수를 직접 참조하지 않고 설정 객체를 주입받음
- **재사용성**: 모든 앱에서 동일한 로직 사용 가능
- **타입 안정성**: TypeScript 인터페이스로 계약 보장
- **테스트 가능성**: Mock 설정 주입으로 단위 테스트 용이
## 패키지 구조
```
packages/common/auth/
├── package.json # 패키지 정의
├── tsconfig.json # TypeScript 설정
├── index.ts # 진입점 (Public API)
├── types.ts # 타입 정의
├── token-manager.ts # 토큰 관리 로직
├── cookie.ts # 쿠키 유틸리티
├── jwt-utils.ts # JWT 유틸리티
└── README.md # 문서
```
## 설치
각 앱의 `package.json`에 의존성을 추가합니다:
```json
{
"dependencies": {
"@common/auth": "workspace:*"
}
}
```
## 사용 방법
### 1. 설정 파일 생성
각 앱에서 자신의 환경변수로 `AuthConfig` 객체를 생성합니다.
```typescript
// 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 인스턴스 생성
설정을 주입하여 토큰 매니저 인스턴스를 생성합니다.
```typescript
// 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. 컴포넌트에서 사용
```typescript
// 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`
지정된 키의 쿠키 값을 가져옵니다.
```typescript
const token = tokenManager.getToken("access-token");
```
#### `setToken(key: string, value: string): void`
토큰을 쿠키에 저장합니다. 자동으로 도메인, secure, sameSite 설정을 적용합니다.
```typescript
tokenManager.setToken("access-token", "eyJhbGc...");
```
#### `removeToken(key: string): void`
지정된 키의 쿠키를 삭제합니다.
```typescript
tokenManager.removeToken("access-token");
```
#### `setupRefreshTimer(callback: () => Promise<void>, delayMs: number): void`
토큰 갱신 타이머를 설정합니다.
```typescript
tokenManager.setupRefreshTimer(async () => {
await refreshAccessToken();
}, 3600000); // 1시간 후
```
#### `clearAuthToken(): void`
모든 인증 토큰을 삭제하고 타이머를 정리합니다.
```typescript
tokenManager.clearAuthToken();
```
#### `getAccessToken(): string | undefined`
액세스 토큰을 가져오는 편의 메서드입니다.
```typescript
const accessToken = tokenManager.getAccessToken();
```
#### `getRefreshToken(): string | undefined`
리프레시 토큰을 가져오는 편의 메서드입니다.
```typescript
const refreshToken = tokenManager.getRefreshToken();
```
#### `getUserId(): string | undefined`
사용자 ID 토큰을 가져오는 편의 메서드입니다.
```typescript
const userId = tokenManager.getUserId();
```
### JWT 유틸리티
#### `getJwtExpiry(token: string): number | null`
JWT 토큰의 만료 시간(Unix timestamp, 초 단위)을 반환합니다.
```typescript
import { getJwtExpiry } from "@common/auth";
const expiry = getJwtExpiry(token);
if (expiry) {
console.log("Token expires at:", new Date(expiry * 1000));
}
```
#### `getRemainingMs(token: string): number`
토큰 만료 1분 전까지 남은 시간(밀리초)을 반환합니다.
```typescript
import { getRemainingMs } from "@common/auth";
const remaining = getRemainingMs(token);
if (remaining > 0) {
console.log("Token valid for:", remaining, "ms");
}
```
### 쿠키 유틸리티
#### `getCookie(name: string): string | undefined`
쿠키 값을 가져옵니다.
```typescript
import { getCookie } from "@common/auth";
const value = getCookie("my-cookie");
```
#### `setCookie(name: string, value: string, options?: CookieOptions): void`
쿠키를 설정합니다.
```typescript
import { setCookie } from "@common/auth";
setCookie("my-cookie", "value", {
secure: true,
sameSite: "lax",
path: "/",
});
```
#### `removeCookie(name: string, options?: CookieOptions): void`
쿠키를 삭제합니다.
```typescript
import { removeCookie } from "@common/auth";
removeCookie("my-cookie", { path: "/" });
```
## 타입 정의
### `AuthConfig`
```typescript
interface AuthConfig {
authUserId: string; // 사용자 ID 토큰 키
claimName: string; // 액세스 토큰 키
cookieDomain?: string; // 쿠키 도메인 (선택)
accessToken?: string; // 액세스 토큰 키 (선택)
refreshToken?: string; // 리프레시 토큰 키 (선택)
}
```
### `CookieOptions`
```typescript
interface CookieOptions {
secure?: boolean;
sameSite?: "strict" | "lax" | "none";
domain?: string;
path?: string;
expires?: number | Date;
}
```
## 마이그레이션 가이드
### 기존 앱에서 @common/auth로 마이그레이션
#### 1. 의존성 추가
```bash
# 앱 디렉토리에서
cd apps/your-app
```
`package.json`에 추가:
```json
{
"dependencies": {
"@common/auth": "workspace:*"
}
}
```
```bash
pnpm install
```
#### 2. 설정 파일 생성
```typescript
// 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 (기존 코드):**
```typescript
// 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 사용):**
```typescript
// 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 앱):**
```typescript
// Before
import { TokenManager } from "@/entities/auth/lib/token-manager";
const tokenManager = TokenManager.getInstance();
// After
import { tokenManager } from "@/entities/auth/lib/token-manager";
// 그대로 사용 가능
```
**함수형 (cs 앱):**
```typescript
// 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 앱
```typescript
// 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 앱
```typescript
// 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` 파일:
```bash
# 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
```
## 테스트 예시
```typescript
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