322 lines
7.1 KiB
Markdown
322 lines
7.1 KiB
Markdown
|
|
# API Route 타입 안전성 가이드
|
||
|
|
|
||
|
|
## 📋 개요
|
||
|
|
|
||
|
|
Next.js API Route에서 백엔드 API 응답 데이터를 프론트엔드로 전달할 때, TypeScript 타입 정의를 통해 데이터 누락을 방지하는 방법
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 🎯 문제 사례
|
||
|
|
|
||
|
|
### 발생한 이슈
|
||
|
|
로그인 API를 테스트할 때, API 테스트 도구에서는 `roles` 데이터가 정상적으로 나오지만, 프론트엔드에서는 빈 배열로 나오는 현상 발생
|
||
|
|
|
||
|
|
### 원인 분석
|
||
|
|
```typescript
|
||
|
|
// ❌ 타입 정의 없이 데이터 전달 (문제 코드)
|
||
|
|
const responseData = {
|
||
|
|
message: data.message,
|
||
|
|
user: data.user,
|
||
|
|
tenant: data.tenant,
|
||
|
|
menus: data.menus,
|
||
|
|
// roles: data.roles, ← 누락됨!
|
||
|
|
token_type: data.token_type,
|
||
|
|
expires_in: data.expires_in,
|
||
|
|
expires_at: data.expires_at,
|
||
|
|
};
|
||
|
|
```
|
||
|
|
|
||
|
|
**문제점:**
|
||
|
|
- 백엔드에서 `roles` 데이터를 반환했지만
|
||
|
|
- Next.js API Route에서 프론트로 전달할 때 `roles` 필드를 포함하지 않음
|
||
|
|
- 타입 정의가 없어서 컴파일 타임에 감지 불가
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## ✅ 해결 방법
|
||
|
|
|
||
|
|
### 1. 백엔드 응답 타입 정의
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
/**
|
||
|
|
* 백엔드 API 로그인 응답 타입
|
||
|
|
*/
|
||
|
|
interface BackendLoginResponse {
|
||
|
|
message: string;
|
||
|
|
access_token: string;
|
||
|
|
refresh_token: string;
|
||
|
|
token_type: string;
|
||
|
|
expires_in: number;
|
||
|
|
expires_at: string;
|
||
|
|
user: {
|
||
|
|
id: number;
|
||
|
|
user_id: string;
|
||
|
|
name: string;
|
||
|
|
email: string;
|
||
|
|
phone: string;
|
||
|
|
};
|
||
|
|
tenant: {
|
||
|
|
id: number;
|
||
|
|
company_name: string;
|
||
|
|
business_num: string;
|
||
|
|
tenant_st_code: string;
|
||
|
|
other_tenants: any[];
|
||
|
|
};
|
||
|
|
menus: Array<{
|
||
|
|
id: number;
|
||
|
|
parent_id: number | null;
|
||
|
|
name: string;
|
||
|
|
url: string;
|
||
|
|
icon: string;
|
||
|
|
sort_order: number;
|
||
|
|
is_external: number;
|
||
|
|
external_url: string | null;
|
||
|
|
}>;
|
||
|
|
roles: Array<{
|
||
|
|
id: number;
|
||
|
|
name: string;
|
||
|
|
description: string;
|
||
|
|
}>;
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### 2. 프론트엔드 응답 타입 정의
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
/**
|
||
|
|
* 프론트엔드로 전달할 응답 타입 (토큰 제외)
|
||
|
|
*/
|
||
|
|
interface FrontendLoginResponse {
|
||
|
|
message: string;
|
||
|
|
user: BackendLoginResponse['user'];
|
||
|
|
tenant: BackendLoginResponse['tenant'];
|
||
|
|
menus: BackendLoginResponse['menus'];
|
||
|
|
roles: BackendLoginResponse['roles']; // ✅ 명시적으로 포함
|
||
|
|
token_type: string;
|
||
|
|
expires_in: number;
|
||
|
|
expires_at: string;
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### 3. 타입 적용
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
export async function POST(request: NextRequest) {
|
||
|
|
try {
|
||
|
|
// ... 백엔드 API 호출
|
||
|
|
|
||
|
|
// ✅ 타입 지정
|
||
|
|
const data: BackendLoginResponse = await backendResponse.json();
|
||
|
|
|
||
|
|
// ✅ 타입 지정 + 모든 필드 포함
|
||
|
|
const responseData: FrontendLoginResponse = {
|
||
|
|
message: data.message,
|
||
|
|
user: data.user,
|
||
|
|
tenant: data.tenant,
|
||
|
|
menus: data.menus,
|
||
|
|
roles: data.roles, // ✅ 누락 방지
|
||
|
|
token_type: data.token_type,
|
||
|
|
expires_in: data.expires_in,
|
||
|
|
expires_at: data.expires_at,
|
||
|
|
};
|
||
|
|
|
||
|
|
return NextResponse.json(responseData, { status: 200 });
|
||
|
|
} catch (error) {
|
||
|
|
// ... 에러 처리
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 🎁 타입 정의의 장점
|
||
|
|
|
||
|
|
### 1. 컴파일 타임 에러 감지
|
||
|
|
```typescript
|
||
|
|
// ❌ roles 누락 시 TypeScript 에러 발생
|
||
|
|
const responseData: FrontendLoginResponse = {
|
||
|
|
message: data.message,
|
||
|
|
user: data.user,
|
||
|
|
// ... roles 필드 빠짐
|
||
|
|
// ⚠️ Type Error: Property 'roles' is missing in type
|
||
|
|
};
|
||
|
|
```
|
||
|
|
|
||
|
|
### 2. 자동 완성 지원
|
||
|
|
- IDE에서 필드명 자동 완성
|
||
|
|
- 오타 방지
|
||
|
|
- 개발 생산성 향상
|
||
|
|
|
||
|
|
### 3. API 문서 역할
|
||
|
|
- 백엔드 API 스펙이 코드에 명시됨
|
||
|
|
- 별도 문서 없이도 데이터 구조 파악 가능
|
||
|
|
- 팀원 간 커뮤니케이션 비용 절감
|
||
|
|
|
||
|
|
### 4. 리팩토링 안정성
|
||
|
|
- 백엔드 API 변경 시 즉시 감지
|
||
|
|
- 영향 범위 파악 용이
|
||
|
|
- 안전한 코드 수정
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 📝 적용 체크리스트
|
||
|
|
|
||
|
|
### API Route 작성 시 필수 사항
|
||
|
|
|
||
|
|
- [ ] 백엔드 응답 타입 인터페이스 정의
|
||
|
|
- [ ] 프론트엔드 응답 타입 인터페이스 정의
|
||
|
|
- [ ] `await response.json()` 시 타입 지정
|
||
|
|
- [ ] 프론트 응답 객체에 타입 지정
|
||
|
|
- [ ] 모든 필수 필드 포함 확인
|
||
|
|
|
||
|
|
### 타입 정의 원칙
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
// ✅ Good: 명시적 타입 지정
|
||
|
|
const data: BackendResponse = await response.json();
|
||
|
|
const result: FrontendResponse = {
|
||
|
|
// ... 모든 필드 포함
|
||
|
|
};
|
||
|
|
|
||
|
|
// ❌ Bad: 타입 없이 작성
|
||
|
|
const data = await response.json();
|
||
|
|
const result = {
|
||
|
|
// ... 필드 누락 가능성
|
||
|
|
};
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 🔍 실제 적용 예시
|
||
|
|
|
||
|
|
### 파일 위치
|
||
|
|
```
|
||
|
|
src/app/api/auth/login/route.ts
|
||
|
|
```
|
||
|
|
|
||
|
|
### Before (문제 코드)
|
||
|
|
```typescript
|
||
|
|
export async function POST(request: NextRequest) {
|
||
|
|
// ...
|
||
|
|
const data = await backendResponse.json(); // 타입 없음
|
||
|
|
|
||
|
|
const responseData = {
|
||
|
|
message: data.message,
|
||
|
|
user: data.user,
|
||
|
|
menus: data.menus,
|
||
|
|
// roles 누락!
|
||
|
|
};
|
||
|
|
|
||
|
|
return NextResponse.json(responseData);
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### After (개선 코드)
|
||
|
|
```typescript
|
||
|
|
interface BackendLoginResponse {
|
||
|
|
// ... 전체 타입 정의
|
||
|
|
roles: Array<{ id: number; name: string; description: string }>;
|
||
|
|
}
|
||
|
|
|
||
|
|
interface FrontendLoginResponse {
|
||
|
|
// ... 전체 타입 정의
|
||
|
|
roles: BackendLoginResponse['roles'];
|
||
|
|
}
|
||
|
|
|
||
|
|
export async function POST(request: NextRequest) {
|
||
|
|
// ...
|
||
|
|
const data: BackendLoginResponse = await backendResponse.json();
|
||
|
|
|
||
|
|
const responseData: FrontendLoginResponse = {
|
||
|
|
message: data.message,
|
||
|
|
user: data.user,
|
||
|
|
menus: data.menus,
|
||
|
|
roles: data.roles, // ✅ 명시적 포함
|
||
|
|
// ... 기타 필드
|
||
|
|
};
|
||
|
|
|
||
|
|
return NextResponse.json(responseData);
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 🚨 주의사항
|
||
|
|
|
||
|
|
### 1. 타입과 실제 데이터 불일치
|
||
|
|
```typescript
|
||
|
|
// ⚠️ 백엔드 API 스펙 변경 시
|
||
|
|
interface BackendResponse {
|
||
|
|
// 타입 정의는 그대로인데
|
||
|
|
user_name: string;
|
||
|
|
}
|
||
|
|
|
||
|
|
// 실제 응답은 변경됨
|
||
|
|
{
|
||
|
|
"username": "홍길동" // 필드명 변경됨
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
**대응 방안:**
|
||
|
|
- 백엔드 API 스펙 변경 시 타입 정의도 함께 업데이트
|
||
|
|
- API 응답 검증 로직 추가 (런타임 체크)
|
||
|
|
- 백엔드 팀과 스펙 변경 사전 공유
|
||
|
|
|
||
|
|
### 2. Optional vs Required
|
||
|
|
```typescript
|
||
|
|
// 명확한 옵셔널 표시
|
||
|
|
interface Response {
|
||
|
|
required_field: string; // 필수
|
||
|
|
optional_field?: string; // 선택
|
||
|
|
nullable_field: string | null; // null 가능
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### 3. any 타입 남용 금지
|
||
|
|
```typescript
|
||
|
|
// ❌ Bad
|
||
|
|
interface Response {
|
||
|
|
data: any; // 타입 안전성 상실
|
||
|
|
}
|
||
|
|
|
||
|
|
// ✅ Good
|
||
|
|
interface Response {
|
||
|
|
data: {
|
||
|
|
id: number;
|
||
|
|
name: string;
|
||
|
|
};
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 📚 관련 문서
|
||
|
|
|
||
|
|
- [Authentication Implementation Guide](./[IMPL-2025-11-07]%20authentication-implementation-guide.md)
|
||
|
|
- [Token Management Guide](./[IMPL-2025-11-10]%20token-management-guide.md)
|
||
|
|
- [API Requirements](./[REF]%20api-requirements.md)
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 📌 핵심 요약
|
||
|
|
|
||
|
|
1. **API Route는 백엔드와 프론트 사이의 중간 레이어**
|
||
|
|
- 데이터 변환/필터링 역할 수행
|
||
|
|
- 타입 정의로 누락 방지
|
||
|
|
|
||
|
|
2. **타입 정의의 3가지 핵심 가치**
|
||
|
|
- 컴파일 타임 에러 감지
|
||
|
|
- 개발 생산성 향상 (자동완성)
|
||
|
|
- 리팩토링 안정성 보장
|
||
|
|
|
||
|
|
3. **실무 적용 원칙**
|
||
|
|
- 백엔드 응답 타입 → 프론트 응답 타입 순서로 정의
|
||
|
|
- 모든 API Route에 타입 적용
|
||
|
|
- 백엔드 스펙 변경 시 타입도 함께 업데이트
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
**작성일:** 2025-11-11
|
||
|
|
**작성자:** Claude Code
|
||
|
|
**마지막 수정:** 2025-11-11
|