Files
sam-react-prod/claudedocs/api/[IMPL-2025-11-11] api-route-type-safety.md
byeongcheolryu 65a8510c0b fix: 품목기준관리 실시간 동기화 수정
- BOM 항목 추가/수정/삭제 시 섹션탭 즉시 반영
- 섹션 복제 시 UI 즉시 업데이트 (null vs undefined 이슈 해결)
- 항목 수정 기능 추가 (useTemplateManagement)
- 실시간 동기화 문서 추가

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-27 22:19:50 +09:00

7.5 KiB

API Route 타입 안전성 가이드

📋 개요

Next.js API Route에서 백엔드 API 응답 데이터를 프론트엔드로 전달할 때, TypeScript 타입 정의를 통해 데이터 누락을 방지하는 방법


🎯 문제 사례

발생한 이슈

로그인 API를 테스트할 때, API 테스트 도구에서는 roles 데이터가 정상적으로 나오지만, 프론트엔드에서는 빈 배열로 나오는 현상 발생

원인 분석

// ❌ 타입 정의 없이 데이터 전달 (문제 코드)
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. 백엔드 응답 타입 정의

/**
 * 백엔드 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. 프론트엔드 응답 타입 정의

/**
 * 프론트엔드로 전달할 응답 타입 (토큰 제외)
 */
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. 타입 적용

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. 컴파일 타임 에러 감지

// ❌ 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() 시 타입 지정
  • 프론트 응답 객체에 타입 지정
  • 모든 필수 필드 포함 확인

타입 정의 원칙

// ✅ 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 (문제 코드)

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 (개선 코드)

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. 타입과 실제 데이터 불일치

// ⚠️ 백엔드 API 스펙 변경 시
interface BackendResponse {
  // 타입 정의는 그대로인데
  user_name: string;
}

// 실제 응답은 변경됨
{
  "username": "홍길동" // 필드명 변경됨
}

대응 방안:

  • 백엔드 API 스펙 변경 시 타입 정의도 함께 업데이트
  • API 응답 검증 로직 추가 (런타임 체크)
  • 백엔드 팀과 스펙 변경 사전 공유

2. Optional vs Required

// 명확한 옵셔널 표시
interface Response {
  required_field: string;      // 필수
  optional_field?: string;     // 선택
  nullable_field: string | null; // null 가능
}

3. any 타입 남용 금지

// ❌ Bad
interface Response {
  data: any; // 타입 안전성 상실
}

// ✅ Good
interface Response {
  data: {
    id: number;
    name: string;
  };
}

📚 관련 문서


📌 핵심 요약

  1. API Route는 백엔드와 프론트 사이의 중간 레이어

    • 데이터 변환/필터링 역할 수행
    • 타입 정의로 누락 방지
  2. 타입 정의의 3가지 핵심 가치

    • 컴파일 타임 에러 감지
    • 개발 생산성 향상 (자동완성)
    • 리팩토링 안정성 보장
  3. 실무 적용 원칙

    • 백엔드 응답 타입 → 프론트 응답 타입 순서로 정의
    • 모든 API Route에 타입 적용
    • 백엔드 스펙 변경 시 타입도 함께 업데이트

작성일: 2025-11-11 작성자: Claude Code 마지막 수정: 2025-11-11


관련 파일

프론트엔드

  • src/app/api/auth/login/route.ts - 로그인 API Route
  • src/types/auth.ts - 인증 타입 정의
  • src/lib/api/auth/types.ts - API 인증 타입

참조 문서

  • claudedocs/auth/[IMPL-2025-11-07] authentication-implementation-guide.md
  • claudedocs/auth/[IMPL-2025-11-10] token-management-guide.md