TIL - 20260729

juni·2026년 7월 29일

TIL

목록 보기
417/470

0729 프론트엔드 실무 심화 (11/N): API 연동 구조, 에러 처리와 프론트-백엔드 계약 관리


✅ 1. 프론트엔드 API 연동이란 무엇인가?

  • 프론트엔드 API 연동은 React 화면에서 백엔드 서버와 통신해 데이터를 조회, 생성, 수정, 삭제하는 작업입니다.
  • 상품 목록 조회, 상담 신청, 관리자 상담 검색, 상태 변경, 엑셀 다운로드 요청, 로그인 모두 API 연동입니다.
  • 프론트엔드는 사용자가 보는 화면이고, 백엔드는 데이터와 비즈니스 규칙을 관리합니다.
사용자 행동
  ↓
프론트엔드 이벤트
  ↓
API 요청
  ↓
백엔드 처리
  ↓
API 응답
  ↓
프론트엔드 상태 갱신
  ↓
화면 변경

➕ 1-1. API 연동이 중요한 이유

  • 화면은 결국 API 응답을 기반으로 동작합니다.
  • API 구조가 흔들리면 프론트 화면도 바로 깨질 수 있습니다.
  • 에러 처리가 부족하면 사용자는 무엇이 문제인지 알 수 없습니다.
  • 관리자 페이지에서는 API 응답과 상태 변경이 운영 데이터에 직접 영향을 줍니다.
  • 프론트와 백엔드 계약이 명확해야 AI에게 작업을 맡겨도 안전합니다.
나쁜 API 연동:
화면마다 fetch 코드 중복
에러 처리 제각각
API 응답 타입 없음
401/403/409/500 구분 없음
상태 변경 후 목록 갱신 누락

결과:
버그 추적 어려움, 운영 중 장애 가능성 증가

✅ 2. API 연동 구조의 기본 원칙

  • API 호출 코드는 컴포넌트 안에 직접 흩어두기보다 별도 모듈로 분리하는 것이 좋습니다.
  • 화면 컴포넌트는 “어떤 데이터를 보여줄지”에 집중하고, API 모듈은 “어떻게 서버와 통신할지”에 집중해야 합니다.

➕ 2-1. 나쁜 구조

function ProductListPage() {
  const [products, setProducts] = useState([]);

  useEffect(() => {
    fetch('https://api.example.com/products')
      .then((res) => res.json())
      .then((data) => setProducts(data.items));
  }, []);

  return <ProductList products={products} />;
}

문제점:

API 주소가 컴포넌트에 직접 있음
에러 처리 없음
로딩 처리 없음
응답 타입 없음
재사용 어려움
테스트 어려움

➕ 2-2. 좋은 구조

apiClient:
공통 baseURL, 인증, 에러 처리

domainApi:
상품, 상담, 주문 등 도메인별 API 함수

query hook:
TanStack Query useQuery/useMutation 연결

component:
화면 표시와 사용자 이벤트 처리

✅ 3. 추천 폴더 구조

src/
  lib/
    apiClient.ts
    queryClient.ts
  features/
    products/
      api/
        productApi.ts
      hooks/
        useProducts.ts
      types/
        productTypes.ts
    consults/
      api/
        consultApi.ts
      hooks/
        useCreateConsult.ts
      types/
        consultTypes.ts
    admin-consults/
      api/
        adminConsultApi.ts
      hooks/
        useAdminConsults.ts
        useUpdateConsultStatus.ts
      types/
        adminConsultTypes.ts

➕ 3-1. 역할

위치역할
lib/apiClient.ts공통 HTTP client
features/*/api도메인별 API 함수
features/*/hooksuseQuery/useMutation custom hook
features/*/types요청/응답 타입
componentsUI 표시
  • API 호출 코드와 UI 컴포넌트를 분리하면 유지보수가 쉬워집니다.
  • 나중에 API 주소나 인증 방식이 바뀌어도 수정 범위가 줄어듭니다.

✅ 4. API Client 설계

  • API Client는 모든 HTTP 요청의 공통 입구입니다.
  • baseURL, credentials, headers, 인증 만료 처리, 공통 에러 변환을 담당합니다.

➕ 4-1. axios 예시

import axios from 'axios';
import { env } from '@/config/env';

export const apiClient = axios.create({
  baseURL: env.apiBaseUrl,
  withCredentials: true,
  timeout: 10000,
});

➕ 4-2. 요청 인터셉터

apiClient.interceptors.request.use((config) => {
  config.headers['X-Client'] = 'web';

  return config;
});

➕ 4-3. 응답 인터셉터

apiClient.interceptors.response.use(
  (response) => response,
  (error) => {
    return Promise.reject(normalizeApiError(error));
  },
);
  • API Client에서 공통 에러를 정리하면 화면마다 에러 처리 코드가 줄어듭니다.
  • 단, 인터셉터가 너무 많은 일을 하면 디버깅이 어려워지므로 역할을 명확히 해야 합니다.

✅ 5. fetch 기반 API Client

  • axios를 쓰지 않고 fetch를 직접 감싸서 사용할 수도 있습니다.
  • 작은 프로젝트에서는 fetch wrapper도 충분합니다.
import { env } from '@/config/env';

type RequestOptions = RequestInit & {
  params?: Record<string, string | number | undefined>;
};

export async function request<T>(
  path: string,
  options: RequestOptions = {},
): Promise<T> {
  const url = new URL(`${env.apiBaseUrl}${path}`);

  Object.entries(options.params ?? {}).forEach(([key, value]) => {
    if (value !== undefined && value !== '') {
      url.searchParams.set(key, String(value));
    }
  });

  const response = await fetch(url.toString(), {
    ...options,
    credentials: 'include',
    headers: {
      'Content-Type': 'application/json',
      ...options.headers,
    },
  });

  const data = await response.json().catch(() => null);

  if (!response.ok) {
    throw normalizeApiError({
      statusCode: response.status,
      data,
    });
  }

  return data as T;
}

➕ 5-1. fetch wrapper 장점

의존성 적음
공통 처리 가능
응답 타입 지정 가능
params 처리 공통화 가능

➕ 5-2. 주의점

response.ok 직접 확인 필요
JSON이 아닌 응답 처리 필요
timeout 기본 지원 없음
interceptor 구조 직접 구현 필요
  • axios든 fetch든 중요한 것은 API 호출 기준을 한 곳으로 모으는 것입니다.

✅ 6. 도메인별 API 함수

  • 실제 화면에서는 API Client를 직접 호출하기보다 도메인별 API 함수를 만들어 사용합니다.
  • 상품은 productApi, 상담은 consultApi, 관리자 상담은 adminConsultApi로 나누면 좋습니다.

➕ 6-1. 상품 API 예시

import { apiClient } from '@/lib/apiClient';

export const productApi = {
  getProducts: async (params: GetProductsParams) => {
    const { data } = await apiClient.get<ProductListResponse>('/products', {
      params,
    });

    return data;
  },

  getProductDetail: async (productId: number) => {
    const { data } = await apiClient.get<ProductDetailResponse>(
      `/products/${productId}`,
    );

    return data;
  },
};

➕ 6-2. 상담 API 예시

export const consultApi = {
  createConsult: async (payload: CreateConsultPayload) => {
    const { data } = await apiClient.post<CreateConsultResponse>(
      '/consults',
      payload,
    );

    return data;
  },
};

➕ 6-3. 관리자 상담 API 예시

export const adminConsultApi = {
  getConsults: async (params: GetAdminConsultsParams) => {
    const { data } = await apiClient.get<AdminConsultListResponse>(
      '/admin/consults',
      { params },
    );

    return data;
  },

  updateStatus: async (consultId: number, payload: UpdateConsultStatusPayload) => {
    const { data } = await apiClient.patch<UpdateConsultStatusResponse>(
      `/admin/consults/${consultId}/status`,
      payload,
    );

    return data;
  },
};
  • 도메인별 API 함수는 화면 코드에서 API 경로를 숨겨줍니다.
  • API 경로가 바뀌면 API 모듈만 수정하면 됩니다.

✅ 7. 요청 타입과 응답 타입

  • TypeScript를 쓴다면 API 요청/응답 타입을 명확히 정의해야 합니다.
  • 타입이 없으면 백엔드 응답 구조가 바뀌었을 때 프론트에서 늦게 깨질 수 있습니다.

➕ 7-1. 상담 목록 타입 예시

export type ConsultStatus =
  | 'PENDING'
  | 'CALLING'
  | 'DONE'
  | 'CANCELLED';

export type GetAdminConsultsParams = {
  page: number;
  limit: number;
  keyword?: string;
  status?: ConsultStatus;
  source?: string;
  startDate?: string;
  endDate?: string;
  sort?: string;
  order?: 'asc' | 'desc';
};

export type AdminConsultListItem = {
  id: number;
  customerName: string;
  phone: string;
  productName: string;
  status: ConsultStatus;
  source: string;
  createdAt: string;
  updatedAt: string;
};

export type AdminConsultListResponse = {
  items: AdminConsultListItem[];
  meta: {
    page: number;
    limit: number;
    total: number;
    totalPages: number;
  };
};

➕ 7-2. 타입 기준

Request Params:
GET query string

Request Payload:
POST/PATCH body

Response:
API 응답 전체 구조

Item:
목록의 개별 row

Enum:
백엔드 enum과 맞추기
  • 백엔드 DTO와 프론트 타입이 너무 다르면 유지보수가 어렵습니다.
  • 가능하면 OpenAPI/Swagger 기반으로 타입 생성까지 고려할 수 있습니다.

✅ 8. API 에러 응답 구조

  • 프론트에서 에러 처리를 잘하려면 백엔드 에러 응답 구조가 일정해야 합니다.
  • 화면마다 다른 에러 형태를 받으면 공통 처리가 어렵습니다.

➕ 8-1. 권장 에러 응답 예시

{
  "statusCode": 409,
  "code": "CONSULT_DUPLICATED",
  "message": "이미 신청된 전화번호입니다.",
  "details": {
    "field": "phone"
  }
}

➕ 8-2. 필요한 값

필드의미
statusCodeHTTP 상태 코드
code프론트가 분기할 수 있는 에러 코드
message사용자 또는 관리자에게 보여줄 기본 메시지
details필드 에러, 추가 정보
requestId로그 추적용 ID

➕ 8-3. 좋은 에러 코드 예시

VALIDATION_ERROR
UNAUTHORIZED
FORBIDDEN
CONSULT_NOT_FOUND
CONSULT_DUPLICATED
INVALID_STATUS_TRANSITION
EXCEL_EXPORT_LIMIT_EXCEEDED
INTERNAL_SERVER_ERROR
  • 프론트는 message 문자열보다 code를 기준으로 분기하는 것이 안전합니다.
  • 메시지는 바뀔 수 있지만 code는 정책으로 유지하는 것이 좋습니다.

✅ 9. 프론트 에러 타입 정규화

  • axios 에러, fetch 에러, 네트워크 에러, 서버 에러는 형태가 다릅니다.
  • 화면에서는 공통 ApiError 형태로 다루는 것이 좋습니다.

➕ 9-1. ApiError 타입

export type ApiError = {
  statusCode?: number;
  code?: string;
  message: string;
  details?: unknown;
  requestId?: string;
};

➕ 9-2. normalizeApiError 예시

export function normalizeApiError(error: unknown): ApiError {
  if (isAxiosError(error)) {
    const data = error.response?.data;

    return {
      statusCode: data?.statusCode ?? error.response?.status,
      code: data?.code,
      message: data?.message ?? '일시적인 문제가 발생했습니다.',
      details: data?.details,
      requestId: data?.requestId,
    };
  }

  return {
    message: '네트워크 상태를 확인해 주세요.',
  };
}
  • 에러를 정규화하면 화면마다 axios 내부 구조를 몰라도 됩니다.
  • 사용자에게 보여줄 메시지도 공통화할 수 있습니다.

✅ 10. HTTP 상태 코드별 프론트 처리

상태 코드의미프론트 처리
400입력값 오류필드 에러 또는 상단 alert
401로그인 필요/만료로그인 페이지 이동
403권한 없음Forbidden 안내
404데이터 없음Not Found 또는 안내
409중복/충돌중복 안내, 상태 충돌 안내
422처리 불가비즈니스 검증 실패 안내
429요청 과다잠시 후 재시도 안내
500서버 오류재시도 안내
503일시 장애잠시 후 재시도 안내

➕ 10-1. 에러 메시지 매핑

export function getUserFriendlyErrorMessage(error: ApiError) {
  switch (error.code) {
    case 'CONSULT_DUPLICATED':
      return '이미 신청된 전화번호입니다.';
    case 'INVALID_STATUS_TRANSITION':
      return '현재 상태에서는 해당 변경을 할 수 없습니다.';
    case 'FORBIDDEN':
      return '이 작업을 수행할 권한이 없습니다.';
    default:
      return '일시적인 문제가 발생했습니다. 잠시 후 다시 시도해 주세요.';
  }
}
  • 고객 화면과 관리자 화면은 메시지 톤을 다르게 가져갈 수 있습니다.
  • 고객에게는 기술 용어를 줄이고, 관리자에게는 조치 가능한 정보를 조금 더 줘야 합니다.

✅ 11. 401 처리

  • 401은 인증이 없거나 로그인 시간이 만료된 상태입니다.
  • 관리자 페이지에서는 공통 처리해야 합니다.

➕ 11-1. 처리 흐름

API 요청
  ↓
401 응답
  ↓
인증 정보 제거
  ↓
me query cache 제거
  ↓
로그인 페이지 이동
  ↓
로그인 만료 메시지 표시

➕ 11-2. 메시지

로그인 시간이 만료되었습니다.
다시 로그인해 주세요.

➕ 11-3. 주의점

401을 서버 장애로 보여주지 않기
무한 refresh retry 방지
민감한 캐시 제거
현재 경로를 redirect 파라미터로 보존할지 검토
  • 401은 화면마다 처리하지 말고 API Client 또는 공통 Auth Handler에서 처리하는 것이 좋습니다.
  • 고객 화면과 관리자 화면의 로그인 정책이 다르면 분리해야 합니다.

✅ 12. 403 처리

  • 403은 로그인은 되어 있지만 권한이 없는 상태입니다.
  • 401과 403을 혼동하면 안 됩니다.

➕ 12-1. 처리 기준

페이지 접근 403:
ForbiddenPage 표시

버튼 액션 403:
토스트 또는 모달로 권한 없음 안내

엑셀 다운로드 403:
다운로드 권한 없음 안내

관리자 API 403:
권한 정책 변경 가능성 고려

➕ 12-2. 메시지

이 작업을 수행할 권한이 없습니다.
필요한 경우 최고 관리자에게 권한을 요청해 주세요.
  • 프론트에서 버튼을 숨겨도 백엔드가 403을 줄 수 있습니다.
  • 권한이 변경되었거나, 화면 캐시가 오래되었거나, 직접 URL 접근한 상황일 수 있습니다.

✅ 13. 409 처리

  • 409는 중복 요청이나 상태 충돌에서 자주 사용됩니다.
  • 상담 신청 중복, 상태 변경 충돌, 이미 처리된 요청 등이 대표적입니다.

➕ 13-1. 상담 신청 중복

if (error.code === 'CONSULT_DUPLICATED') {
  form.setError('phone', {
    message: '이미 신청된 전화번호입니다.',
  });

  return;
}

➕ 13-2. 상태 변경 충돌

이미 다른 관리자가 처리한 상담입니다.
목록을 새로고침한 뒤 다시 확인해 주세요.

➕ 13-3. 처리 기준

폼 필드와 관련:
field error

상태 충돌:
목록/상세 refetch 안내

중복 요청:
중복 안내 + 재시도 제한

이미 처리됨:
최신 데이터 다시 조회
  • 409는 사용자 실수일 수도 있고, 동시성 문제일 수도 있습니다.
  • 관리자 화면에서는 최신 데이터 refetch가 중요합니다.

✅ 14. API 응답 마스킹

  • 개인정보는 가능하면 백엔드 응답 단계에서 권한에 맞게 마스킹해야 합니다.
  • 프론트에서만 마스킹하면 네트워크 탭에서 원본이 보일 수 있습니다.

➕ 14-1. 위험한 방식

{
  "customerName": "홍길동",
  "phone": "01012345678"
}

프론트에서만:

<span>{maskPhone(consult.phone)}</span>

문제:

화면에는 마스킹되어 보여도
Network 탭에는 원본 전화번호가 보임

➕ 14-2. 안전한 방향

{
  "customerName": "홍길동",
  "phone": "010****5678"
}
  • 권한이 있는 관리자에게만 원본을 내려주고, 권한 없는 사용자는 마스킹된 값을 받아야 합니다.
  • 엑셀 다운로드도 권한별 마스킹 기준을 따로 둬야 합니다.

✅ 15. API 응답과 화면 모델 분리

  • API 응답 타입을 화면에서 그대로 쓰는 경우도 있지만, 화면에 맞게 가공한 ViewModel을 만들면 편할 때가 있습니다.

➕ 15-1. API 응답

type AdminConsultListItem = {
  id: number;
  customerName: string;
  phone: string;
  productName: string;
  status: 'PENDING' | 'CALLING' | 'DONE' | 'CANCELLED';
  source: string;
  createdAt: string;
};

➕ 15-2. 화면 모델

type AdminConsultRow = {
  id: number;
  customerLabel: string;
  phoneLabel: string;
  productLabel: string;
  statusLabel: string;
  sourceLabel: string;
  createdAtLabel: string;
};

➕ 15-3. 변환 함수

export function toAdminConsultRow(
  item: AdminConsultListItem,
): AdminConsultRow {
  return {
    id: item.id,
    customerLabel: item.customerName,
    phoneLabel: item.phone,
    productLabel: item.productName,
    statusLabel: CONSULT_STATUS_LABEL[item.status],
    sourceLabel: SOURCE_LABEL[item.source] ?? item.source,
    createdAtLabel: formatDateTime(item.createdAt),
  };
}
  • 화면 가공 로직이 컴포넌트 안에 흩어지는 것을 막을 수 있습니다.
  • 단, 너무 작은 화면까지 과하게 ViewModel을 만들 필요는 없습니다.

✅ 16. API 계약이란 무엇인가?

  • API 계약(API Contract)은 프론트엔드와 백엔드가 서로 약속한 요청/응답 구조입니다.
  • 어떤 endpoint가 있고, 어떤 query param을 받고, 어떤 응답을 주고, 어떤 에러 코드가 나오는지 정한 것입니다.
Endpoint
Method
Request Params
Request Body
Response Body
Error Response
Auth/Permission
Pagination
Sort/Filter

➕ 16-1. 계약이 중요한 이유

  • 프론트와 백엔드가 동시에 작업할 수 있습니다.
  • API 응답 변경으로 화면이 깨지는 일을 줄입니다.
  • QA 기준이 명확해집니다.
  • Swagger/Postman/문서화에 연결됩니다.
  • AI에게 코드 생성을 맡길 때 기준 자료가 됩니다.

✅ 17. API 계약 문서 예시

## 상담 목록 조회

### Endpoint
GET /api/admin/consults

### Query Params
| 이름 | 타입 | 필수 | 설명 |
|---|---|---|---|
| page | number | O | 페이지 번호 |
| limit | number | O | 페이지 크기 |
| keyword | string | X | 고객명/전화번호/상품명 검색 |
| status | string | X | 상담 상태 |
| source | string | X | 유입경로 |
| startDate | string | X | 시작일 |
| endDate | string | X | 종료일 |

### Response
```json
{
  "items": [
    {
      "id": 1,
      "customerName": "홍길동",
      "phone": "010****5678",
      "productName": "iPhone 17 Pro",
      "status": "PENDING",
      "source": "NAVER_AD",
      "createdAt": "2026-07-29T10:00:00.000Z"
    }
  ],
  "meta": {
    "page": 1,
    "limit": 20,
    "total": 100,
    "totalPages": 5
  }
}

Error

code설명
UNAUTHORIZED로그인 필요
FORBIDDEN권한 없음
INTERNAL_SERVER_ERROR서버 오류

*   이런 문서가 있으면 프론트/백엔드 작업 기준이 명확해집니다.
*   Swagger가 있어도 운영 정책, 권한, 마스킹 기준은 별도 문서로 보강하는 것이 좋습니다.

---

### ✅ 18. Swagger/OpenAPI 활용

*   백엔드에서 Swagger/OpenAPI를 제공하면 프론트엔드 작업이 쉬워집니다.
*   endpoint, DTO, 응답 구조, 에러 응답을 확인할 수 있습니다.

#### ➕ 18-1. 활용 방법

```txt id="swagger-frontend-use"
API endpoint 확인
Request/Response 타입 확인
에러 코드 확인
Postman 대신 테스트 호출
OpenAPI 기반 타입 생성
백엔드와 계약 변경 확인

➕ 18-2. 주의할 점

Swagger가 실제 운영 정책을 모두 설명하지 않을 수 있음
권한/마스킹/상태 전이 규칙은 별도 문서 필요
Swagger 최신화 여부 확인
example 값이 실제 응답과 다를 수 있음
  • Swagger는 API 문서의 기본이지만, 실무 운영 규칙까지 자동으로 설명해주지는 않습니다.
  • 상태 변경 가능 조건, 권한별 응답 차이, 엑셀 다운로드 기준은 별도 문서가 필요합니다.

✅ 19. OpenAPI 타입 생성

  • 프로젝트가 커지면 OpenAPI 스펙에서 TypeScript 타입을 자동 생성할 수 있습니다.
  • 프론트와 백엔드 타입 불일치를 줄이는 데 도움이 됩니다.

➕ 19-1. 장점

API 응답 타입 수동 작성 감소
백엔드 DTO 변경 감지 쉬움
타입 불일치 감소
프론트 개발 속도 향상

➕ 19-2. 주의점

Swagger 스펙이 정확해야 함
생성 타입이 너무 복잡할 수 있음
프론트 화면용 ViewModel은 별도 필요할 수 있음
에러 응답 타입까지 잘 정의해야 함
  • 초기에는 수동 타입으로 시작해도 됩니다.
  • API가 많아지고 DTO 변경이 잦아지면 타입 생성 도입을 검토하면 좋습니다.

✅ 20. TanStack Query Hook 분리

  • API 함수를 직접 컴포넌트에서 useQuery로 감싸도 되지만, 반복되는 화면은 custom hook으로 분리하면 좋습니다.

➕ 20-1. 상담 목록 hook

export function useAdminConsults(params: GetAdminConsultsParams) {
  return useQuery({
    queryKey: ['admin', 'consults', params],
    queryFn: () => adminConsultApi.getConsults(params),
    placeholderData: (previousData) => previousData,
  });
}

➕ 20-2. 상태 변경 hook

export function useUpdateConsultStatus() {
  const queryClient = useQueryClient();

  return useMutation({
    mutationFn: ({
      consultId,
      payload,
    }: {
      consultId: number;
      payload: UpdateConsultStatusPayload;
    }) => adminConsultApi.updateStatus(consultId, payload),

    onSuccess: () => {
      queryClient.invalidateQueries({
        queryKey: ['admin', 'consults'],
      });
    },
  });
}

➕ 20-3. 장점

컴포넌트 코드 간결
queryKey 기준 통일
invalidate 기준 통일
테스트와 리팩토링 쉬움
API 변경 대응 쉬움
  • queryKey를 hook 안에 모으면 화면마다 다른 key를 쓰는 실수를 줄일 수 있습니다.
  • 다만 모든 작은 API까지 hook으로 감싸는 것은 과할 수 있습니다.

✅ 21. queryKey 관리

  • queryKey는 서버 상태 캐시의 핵심입니다.
  • 문자열을 화면마다 직접 쓰면 오타와 불일치가 생길 수 있습니다.

➕ 21-1. queryKeys 객체 예시

export const queryKeys = {
  auth: {
    me: ['auth', 'me'] as const,
  },
  adminConsults: {
    all: ['admin', 'consults'] as const,
    list: (params: GetAdminConsultsParams) =>
      ['admin', 'consults', params] as const,
    detail: (consultId: number) =>
      ['admin', 'consults', consultId] as const,
  },
};

➕ 21-2. 사용 예시

useQuery({
  queryKey: queryKeys.adminConsults.list(params),
  queryFn: () => adminConsultApi.getConsults(params),
});

➕ 21-3. invalidate 예시

queryClient.invalidateQueries({
  queryKey: queryKeys.adminConsults.all,
});
  • queryKey를 공통화하면 invalidate 기준이 명확해집니다.
  • 관리자 테이블처럼 필터 조건이 많은 화면에서 특히 좋습니다.

✅ 22. Mutation 성공 후 갱신 기준

  • 데이터를 수정한 뒤에는 어떤 query를 갱신할지 정해야 합니다.
  • 너무 좁게 갱신하면 화면이 안 바뀌고, 너무 넓게 갱신하면 API 요청이 많아집니다.

➕ 22-1. 상담 상태 변경

상태 변경 성공 후:
상담 목록 invalidate
상담 상세 invalidate
상태 이력 invalidate
대시보드 통계 invalidate 필요 여부 검토

➕ 22-2. 상품 수정

상품 수정 성공 후:
상품 목록 invalidate
상품 상세 invalidate
고객 상품 상세 invalidate
카테고리별 상품 목록 invalidate 필요 여부 검토

➕ 22-3. 엑셀 Export 요청

ExportJob 생성 성공 후:
ExportJob 목록 invalidate
상담 목록은 보통 갱신 불필요
  • mutation 후 갱신 기준은 도메인별로 정리해두는 것이 좋습니다.
  • 무조건 모든 query를 invalidate하면 성능과 UX가 나빠질 수 있습니다.

✅ 23. 낙관적 업데이트와 보수적 업데이트

  • Mutation 후 화면을 갱신하는 방식은 크게 두 가지입니다.
방식설명적합한 경우
낙관적 업데이트서버 성공 전 화면 먼저 변경좋아요, 간단 토글
보수적 업데이트서버 성공 후 화면 변경주문/상담/권한/결제

➕ 23-1. 관리자 기준

상담 상태 변경:
보수적 업데이트 권장

주문 상태 변경:
보수적 업데이트 권장

상품 노출 토글:
상황에 따라 가능하지만 실패 롤백 필요

권한 변경:
보수적 업데이트 권장
  • 운영 데이터에 영향이 큰 작업은 서버 성공 후 반영하는 것이 안전합니다.
  • 실패 시 되돌리는 로직이 없다면 낙관적 업데이트를 쓰지 않는 것이 좋습니다.

✅ 24. API 연동에서 자주 하는 실수

➕ 24-1. 화면마다 API 경로 직접 작성

문제:
컴포넌트마다 /api/admin/consults 직접 작성

결과:
경로 변경 시 수정 범위 증가
오타 발생
테스트 어려움

➕ 24-2. 에러를 문자열로만 처리

문제:
message === "이미 신청됨" 으로 분기

결과:
백엔드 메시지 문구 변경 시 프론트 분기 깨짐
  • 에러 분기는 code 기준으로 하는 것이 좋습니다.

➕ 24-3. API 응답 타입 없음

문제:
data: any

결과:
응답 필드 변경을 컴파일 단계에서 못 잡음
런타임에서 undefined 오류 발생

➕ 24-4. mutation 후 invalidate 누락

문제:
상태 변경 성공했는데 목록 query 갱신 안 함

결과:
화면에는 이전 상태가 계속 보임
운영자 혼란

➕ 24-5. 개인정보를 프론트에서만 가림

문제:
백엔드는 원본 전화번호 내려주고 프론트에서만 마스킹

결과:
Network 탭에서 개인정보 확인 가능

✅ 25. 실무 체크리스트

➕ 25-1. API Client 체크리스트

  1. API Base URL이 환경변수로 관리되는가?
  2. API Client가 한 곳에 정의되어 있는가?
  3. 공통 인증/credentials 설정이 있는가?
  4. 응답 에러가 공통 ApiError로 정규화되는가?
  5. 401/403 처리가 구분되어 있는가?
  6. timeout 기준이 있는가?
  7. 운영 로그에 민감정보를 남기지 않는가?
  8. API 주소가 컴포넌트에 직접 흩어져 있지 않은가?

➕ 25-2. 타입 체크리스트

  1. 요청 params 타입이 정의되어 있는가?
  2. 요청 payload 타입이 정의되어 있는가?
  3. 응답 타입이 정의되어 있는가?
  4. enum 값이 백엔드와 일치하는가?
  5. 페이지네이션 meta 타입이 공통화되어 있는가?
  6. 에러 응답 타입이 정의되어 있는가?
  7. any 사용이 과하지 않은가?
  8. Swagger/OpenAPI와 실제 타입이 맞는가?

➕ 25-3. TanStack Query 체크리스트

  1. queryKey에 모든 검색 조건이 포함되어 있는가?
  2. queryKey가 공통 객체로 관리되는가?
  3. mutation 후 필요한 query를 invalidate하는가?
  4. 너무 넓은 invalidate로 불필요한 요청을 만들지 않는가?
  5. 상세 데이터는 필요할 때만 조회하는가?
  6. enabled 조건이 필요한 query에 적용되어 있는가?
  7. staleTime이 데이터 성격에 맞는가?
  8. 에러/로딩/빈 상태가 화면에 반영되는가?

➕ 25-4. API 계약 체크리스트

  1. endpoint/method가 문서화되어 있는가?
  2. query param과 request body가 문서화되어 있는가?
  3. response 구조가 문서화되어 있는가?
  4. error code가 문서화되어 있는가?
  5. 권한 조건이 문서화되어 있는가?
  6. 개인정보 마스킹 기준이 문서화되어 있는가?
  7. pagination/sort/filter 기준이 문서화되어 있는가?
  8. API 변경 시 프론트 영향 범위를 확인하는가?

✅ 26. AI를 활용해 API 연동을 점검할 때 질문법

  • AI에게 API 연동 구조를 점검시킬 때는 API client, 도메인별 API 함수, queryKey, 에러 응답 구조, 권한 처리, 개인정보 마스킹 기준을 함께 알려줘야 합니다.

➕ 26-1. 좋은 질문 예시

React + TypeScript + TanStack Query 기반 프론트엔드에서 API 연동 구조를 점검하고 싶어.

상황:
1. 고객 화면에는 상품 목록/상세, 상담 신청 API가 있음
2. 관리자 화면에는 상담 목록 검색, 상담 상세, 상태 변경, 엑셀 Export 요청 API가 있음
3. API Base URL은 VITE_API_BASE_URL로 관리함
4. axios 기반 apiClient를 사용함
5. 에러 응답은 statusCode, code, message, details, requestId 구조임
6. 상담 신청 중복은 CONSULT_DUPLICATED code와 409로 내려옴
7. 관리자 권한 없음은 FORBIDDEN code와 403으로 내려옴
8. 상담 목록은 page, limit, keyword, status, source, startDate, endDate를 query param으로 받음
9. 개인정보는 권한별로 백엔드에서 마스킹된 값을 내려줘야 함

요청:
- apiClient 구조
- 도메인별 API 함수 분리
- 요청/응답 타입 설계
- ApiError 정규화
- 401/403/409/500 처리 기준
- TanStack Query queryKey 설계
- mutation 후 invalidate 기준
- API 계약 문서 템플릿
- 개인정보 마스킹 주의사항
- 배포 전 API 연동 QA 체크리스트
를 실무 기준으로 정리해줘.

➕ 26-2. AI 답변 검증 기준

  1. API 경로를 컴포넌트에 직접 흩어두라고 하지 않는가?
  2. 에러를 message 문자열보다 code 기준으로 처리하라고 하는가?
  3. 401, 403, 409, 500 처리를 구분하는가?
  4. queryKey에 모든 검색 조건을 포함하는가?
  5. mutation 후 invalidate 기준을 설명하는가?
  6. 개인정보 마스킹을 백엔드 응답 기준으로 설명하는가?
  7. API 계약 문서화를 강조하는가?
  8. 현재 프로젝트 규모에 맞는 현실적인 구조인가?

📌 요약

  • 프론트엔드 API 연동은 React 화면이 백엔드 서버와 통신해 데이터를 조회, 생성, 수정, 삭제하는 작업입니다.
  • API 호출 코드는 컴포넌트 안에 직접 흩어두지 말고, apiClient와 도메인별 API 모듈로 분리하는 것이 좋습니다.
  • apiClient는 baseURL, credentials, timeout, 공통 에러 정규화, 401/403 처리 같은 공통 통신 기준을 담당합니다.
  • 상품, 상담, 관리자 상담, 주문처럼 도메인별 API 함수를 만들면 경로 변경과 응답 타입 변경에 대응하기 쉽습니다.
  • 요청 params, request body, response, error response 타입을 명확히 정의하면 API 변경으로 인한 런타임 오류를 줄일 수 있습니다.
  • 프론트 에러 처리는 서버 응답의 message 문자열보다 code를 기준으로 분기하는 것이 안전합니다.
  • 401은 로그인 만료, 403은 권한 없음, 409는 중복/상태 충돌, 500은 서버 오류로 구분해 UI를 다르게 처리해야 합니다.
  • 개인정보는 프론트에서만 마스킹하지 말고, 백엔드가 권한에 맞게 마스킹된 응답을 내려주는 것이 안전합니다.
  • TanStack Query의 queryKey는 API 결과를 바꾸는 모든 조건을 포함해야 하며, mutation 성공 후에는 필요한 query만 정확히 invalidate해야 합니다.
  • API 계약 문서는 endpoint, request, response, error code, 권한, 마스킹, pagination/sort/filter 기준까지 포함해야 프론트와 백엔드 작업이 안정적으로 맞춰집니다.

0개의 댓글