Skip to Content
PatternsAPI Integration

API Integration

API 통합 및 클라이언트 구현 패턴입니다.

개요

API Integration 패턴은 REST API, GraphQL 등 백엔드 서비스와 안전하고 효율적으로 통신하며, 타입 안전성을 보장하고, 에러를 우아하게 처리하는 검증된 구현 방법입니다. API 클라이언트 설계, 인터셉터, 재시도 전략, 타입 안전 API 등 프로덕션 환경에서 필요한 모든 API 통합 시나리오를 다룹니다.

사용 사례:

  • REST API 클라이언트
  • GraphQL 통합
  • 인증 토큰 관리
  • 에러 처리 및 재시도
  • API 모킹 및 테스트

사용하지 말아야 할 때:

  • 정적 데이터만 사용
  • 서버리스 함수 직접 호출
  • 로컬 스토리지만 사용

기본 패턴

1. API 클라이언트 구축

재사용 가능한 API 클라이언트 패턴입니다.

// lib/api-client.ts interface RequestConfig extends RequestInit { params?: Record<string, string>; } class ApiClient { private baseURL: string; private defaultHeaders: HeadersInit; constructor(baseURL: string) { this.baseURL = baseURL; this.defaultHeaders = { "Content-Type": "application/json", }; } private buildURL(endpoint: string, params?: Record<string, string>): string { const url = new URL(endpoint, this.baseURL); if (params) { Object.entries(params).forEach(([key, value]) => { url.searchParams.append(key, value); }); } return url.toString(); } private async request<T>( endpoint: string, config?: RequestConfig ): Promise<T> { const { params, ...fetchConfig } = config || {}; const url = this.buildURL(endpoint, params); const response = await fetch(url, { ...fetchConfig, headers: { ...this.defaultHeaders, ...fetchConfig.headers, }, }); if (!response.ok) { const error = await response.json().catch(() => ({})); throw new Error( error.message || `HTTP error! status: ${response.status}` ); } return response.json(); } async get<T>(endpoint: string, config?: RequestConfig): Promise<T> { return this.request<T>(endpoint, { ...config, method: "GET" }); } async post<T>( endpoint: string, data?: unknown, config?: RequestConfig ): Promise<T> { return this.request<T>(endpoint, { ...config, method: "POST", body: JSON.stringify(data), }); } async put<T>( endpoint: string, data?: unknown, config?: RequestConfig ): Promise<T> { return this.request<T>(endpoint, { ...config, method: "PUT", body: JSON.stringify(data), }); } async delete<T>(endpoint: string, config?: RequestConfig): Promise<T> { return this.request<T>(endpoint, { ...config, method: "DELETE" }); } setAuthToken(token: string) { this.defaultHeaders = { ...this.defaultHeaders, Authorization: `Bearer ${token}`, }; } } // API 클라이언트 인스턴스 생성 export const apiClient = new ApiClient(process.env.NEXT_PUBLIC_API_URL || ""); // 사용 예시 interface User { id: number; name: string; email: string; } export async function fetchUsers(): Promise<User[]> { return apiClient.get<User[]>("/users"); } export async function createUser(data: Omit<User, "id">): Promise<User> { return apiClient.post<User>("/users", data); }

2. 인터셉터 패턴

요청/응답 인터셉터를 활용한 공통 처리 패턴입니다.

// lib/api-interceptors.ts class ApiClientWithInterceptors { private baseURL: string; private requestInterceptors: Array<(config: RequestInit) => RequestInit> = []; private responseInterceptors: Array<(response: Response) => Response> = []; constructor(baseURL: string) { this.baseURL = baseURL; } // 요청 인터셉터 추가 addRequestInterceptor(interceptor: (config: RequestInit) => RequestInit) { this.requestInterceptors.push(interceptor); } // 응답 인터셉터 추가 addResponseInterceptor(interceptor: (response: Response) => Response) { this.responseInterceptors.push(interceptor); } async request<T>(endpoint: string, config: RequestInit = {}): Promise<T> { // 요청 인터셉터 실행 let finalConfig = config; for (const interceptor of this.requestInterceptors) { finalConfig = interceptor(finalConfig); } let response = await fetch(`${this.baseURL}${endpoint}`, finalConfig); // 응답 인터셉터 실행 for (const interceptor of this.responseInterceptors) { response = interceptor(response); } if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } return response.json(); } } // 인터셉터 설정 const api = new ApiClientWithInterceptors( process.env.NEXT_PUBLIC_API_URL || "" ); // 인증 토큰 자동 추가 api.addRequestInterceptor((config) => { const token = localStorage.getItem("authToken"); if (token) { config.headers = { ...config.headers, Authorization: `Bearer ${token}`, }; } return config; }); // 로깅 인터셉터 api.addRequestInterceptor((config) => { console.log("Request:", config); return config; }); api.addResponseInterceptor((response) => { console.log("Response:", response); return response; }); // 401 에러 시 자동 로그아웃 api.addResponseInterceptor((response) => { if (response.status === 401) { localStorage.removeItem("authToken"); window.location.href = "/login"; } return response; }); export { api };

고급 패턴

3. 타입 안전 API

TypeScript로 완전한 타입 안전성을 보장하는 패턴입니다.

// types/api.ts export interface ApiResponse<T> { data: T; message?: string; status: number; } export interface PaginatedResponse<T> { items: T[]; total: number; page: number; pageSize: number; hasMore: boolean; } export interface ApiError { message: string; code: string; details?: unknown; } // lib/typed-api.ts import { z } from "zod"; // Zod 스키마로 런타임 타입 검증 const UserSchema = z.object({ id: z.number(), name: z.string(), email: z.string().email(), createdAt: z.string().datetime(), }); type User = z.infer<typeof UserSchema>; class TypedApiClient { async fetchUsers(): Promise<User[]> { const response = await fetch("/api/users"); const data = await response.json(); // 런타임 타입 검증 return z.array(UserSchema).parse(data); } async fetchUser(id: number): Promise<User> { const response = await fetch(`/api/users/${id}`); const data = await response.json(); return UserSchema.parse(data); } async createUser(input: Omit<User, "id" | "createdAt">): Promise<User> { const response = await fetch("/api/users", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(input), }); const data = await response.json(); return UserSchema.parse(data); } } export const typedApi = new TypedApiClient(); // 사용 예시 export default function TypeSafeExample() { const [users, setUsers] = useState<User[]>([]); useEffect(() => { typedApi.fetchUsers().then(setUsers); }, []); return ( <div> {users.map((user) => ( <div key={user.id}> {user.name} - {user.email} </div> ))} </div> ); }

4. 재시도 전략

네트워크 오류 시 자동 재시도 패턴입니다.

// lib/retry-api.ts interface RetryConfig { maxRetries?: number; retryDelay?: number; backoffMultiplier?: number; retryableStatuses?: number[]; } class RetryableApiClient { private defaultConfig: Required<RetryConfig> = { maxRetries: 3, retryDelay: 1000, backoffMultiplier: 2, retryableStatuses: [408, 429, 500, 502, 503, 504], }; async fetchWithRetry<T>( url: string, options?: RequestInit, retryConfig?: RetryConfig ): Promise<T> { const config = { ...this.defaultConfig, ...retryConfig }; let lastError: Error | null = null; let delay = config.retryDelay; for (let attempt = 0; attempt <= config.maxRetries; attempt++) { try { const response = await fetch(url, options); // 재시도 가능한 상태 코드 확인 if ( !response.ok && config.retryableStatuses.includes(response.status) && attempt < config.maxRetries ) { console.log(`Retry attempt ${attempt + 1}/${config.maxRetries}`); await this.sleep(delay); delay *= config.backoffMultiplier; continue; } if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } return response.json(); } catch (error) { lastError = error as Error; // 네트워크 에러인 경우에만 재시도 if ( error instanceof TypeError && error.message === "Failed to fetch" && attempt < config.maxRetries ) { console.log( `Network error, retry attempt ${attempt + 1}/${config.maxRetries}` ); await this.sleep(delay); delay *= config.backoffMultiplier; continue; } throw error; } } throw lastError || new Error("Request failed after all retries"); } private sleep(ms: number): Promise<void> { return new Promise((resolve) => setTimeout(resolve, ms)); } } export const retryApi = new RetryableApiClient(); // 사용 예시 export async function fetchWithRetry() { try { const data = await retryApi.fetchWithRetry<User[]>( "/api/users", { method: "GET" }, { maxRetries: 5, retryDelay: 2000 } ); return data; } catch (error) { console.error("Failed after all retries:", error); throw error; } }

5. GraphQL 통합

GraphQL API 통합 패턴입니다.

"use client"; import { ApolloClient, InMemoryCache, gql, useQuery, useMutation, } from "@apollo/client"; // Apollo Client 설정 const client = new ApolloClient({ uri: process.env.NEXT_PUBLIC_GRAPHQL_URL, cache: new InMemoryCache(), headers: { authorization: `Bearer ${ typeof window !== "undefined" ? localStorage.getItem("token") : "" }`, }, }); // GraphQL 쿼리 const GET_USERS = gql` query GetUsers($limit: Int, $offset: Int) { users(limit: $limit, offset: $offset) { id name email posts { id title } } } `; // GraphQL 뮤테이션 const CREATE_USER = gql` mutation CreateUser($input: CreateUserInput!) { createUser(input: $input) { id name email } } `; // 사용 예시 export default function GraphQLExample() { const { data, loading, error } = useQuery(GET_USERS, { variables: { limit: 10, offset: 0 }, }); const [createUser, { loading: creating }] = useMutation(CREATE_USER, { refetchQueries: [{ query: GET_USERS }], }); const handleCreateUser = async () => { try { await createUser({ variables: { input: { name: "New User", email: "user@example.com", }, }, }); } catch (err) { console.error("Failed to create user:", err); } }; if (loading) return <div>로딩 중...</div>; if (error) return <div>에러: {error.message}</div>; return ( <div className="p-4"> <button onClick={handleCreateUser} disabled={creating} className="mb-4 px-4 py-2 bg-blue-600 text-white rounded" > {creating ? "생성 중..." : "사용자 추가"} </button> <div className="space-y-4"> {data?.users.map((user: any) => ( <div key={user.id} className="p-4 border rounded"> <h3 className="font-bold">{user.name}</h3> <p className="text-sm text-gray-600">{user.email}</p> <div className="mt-2"> <h4 className="text-sm font-semibold">게시물:</h4> {user.posts.map((post: any) => ( <p key={post.id} className="text-sm"> {post.title} </p> ))} </div> </div> ))} </div> </div> ); }

6. API 모킹

테스트 및 개발을 위한 API 모킹 패턴입니다.

// lib/mock-api.ts import { rest } from "msw"; import { setupWorker } from "msw/browser"; const users = [ { id: 1, name: "User 1", email: "user1@example.com" }, { id: 2, name: "User 2", email: "user2@example.com" }, ]; // Mock 핸들러 정의 const handlers = [ rest.get("/api/users", (req, res, ctx) => { return res(ctx.status(200), ctx.json(users)); }), rest.get("/api/users/:id", (req, res, ctx) => { const { id } = req.params; const user = users.find((u) => u.id === Number(id)); if (!user) { return res(ctx.status(404), ctx.json({ message: "User not found" })); } return res(ctx.status(200), ctx.json(user)); }), rest.post("/api/users", async (req, res, ctx) => { const body = await req.json(); const newUser = { id: users.length + 1, ...body, }; users.push(newUser); return res(ctx.status(201), ctx.json(newUser)); }), rest.delete("/api/users/:id", (req, res, ctx) => { const { id } = req.params; const index = users.findIndex((u) => u.id === Number(id)); if (index === -1) { return res(ctx.status(404), ctx.json({ message: "User not found" })); } users.splice(index, 1); return res(ctx.status(204)); }), ]; // Mock Service Worker 설정 export const worker = setupWorker(...handlers); // 개발 환경에서 MSW 시작 if (process.env.NODE_ENV === "development") { worker.start(); }

Best Practices

✅ 권장 사항

  1. 에러 처리

    • 상태 코드별 처리
    • 명확한 에러 메시지
    • 재시도 로직 구현
    • 타임아웃 설정
  2. 보안

    • HTTPS 사용
    • 토큰 안전하게 저장
    • CORS 올바르게 설정
    • API 키 환경변수 관리
  3. 성능

    • 요청 디바운싱/쓰로틀링
    • 응답 캐싱
    • 압축 활성화
    • 페이지네이션
  4. 타입 안전성

    • TypeScript 타입 정의
    • Zod로 런타임 검증
    • API 스키마 공유
  5. 개발 경험

    • API 문서화
    • Mock 데이터 제공
    • 개발자 도구 통합

⚠️ 피해야 할 것

  1. 보안 취약점

    • 평문 비밀번호 전송
    • 클라이언트에 API 키 노출
    • CORS 와일드카드 허용
  2. 성능 문제

    • 불필요한 중복 요청
    • 과도한 데이터 전송
    • 재시도 무한 루프
  3. 유지보수 문제

    • 하드코딩된 URL
    • 타입 검증 없음
    • 에러 무시

성능 최적화

Request Batching

// 여러 요청을 하나로 묶기 class BatchedApiClient { private queue: Array<{ url: string; resolve: Function; reject: Function }> = []; private timeoutId: NodeJS.Timeout | null = null; async get(url: string): Promise<any> { return new Promise((resolve, reject) => { this.queue.push({ url, resolve, reject }); if (!this.timeoutId) { this.timeoutId = setTimeout(() => this.flush(), 50); } }); } private async flush() { const requests = [...this.queue]; this.queue = []; this.timeoutId = null; const urls = requests.map((r) => r.url); const response = await fetch("/api/batch", { method: "POST", body: JSON.stringify({ urls }), }); const results = await response.json(); requests.forEach((req, index) => { req.resolve(results[index]); }); } }

Foundation 예제

범용 API 클라이언트

Foundation 컴포넌트로 구현한 API 호출입니다.

import { apiClient } from "@/lib/api-client"; export async function fetchData() { const data = await apiClient.get("/api/data"); return data; }

전체 예제 보기 →


iCignal 예제

Analytics API 통합

iCignal Blue 브랜드를 적용한 Analytics API 클라이언트입니다.

import "@vortex/ui-icignal/theme"; import { apiClient } from "@/lib/api-client"; export async function fetchAnalytics() { return apiClient.get("/api/analytics", { params: { timeRange: "week" }, }); }

전체 예제 보기 →


Cals 예제

예약 API 통합

Cals Pink 브랜드를 적용한 예약 API 클라이언트입니다.

import "@vortex/ui-cals/theme"; import { apiClient } from "@/lib/api-client"; export async function createBooking(data) { return apiClient.post("/api/bookings", data); }

전체 예제 보기 →


CodeSandbox

CodeSandbox 예제는 곧 제공될 예정입니다.

로컬에서 실행하기

  1. 프로젝트 생성

    npx @vortex/cli init my-api-project --template next-app cd my-api-project
  2. 의존성 설치

    pnpm add zod @tanstack/react-query
  3. 환경변수 설정

    echo "NEXT_PUBLIC_API_URL=http://localhost:3001" > .env.local
  4. 코드 복사 및 실행

    pnpm dev

관련 패턴

Last updated on