Skip to Content
PatternsAPI Integration

API Integration

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

개요

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

사용 사례:

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

사용하지 말아야 할 때:

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

구현 패턴

1. Axios 인스턴스 (iCignal Demo)

Axios를 사용한 타입 안전 API 클라이언트 구현입니다.

// lib/axios.ts import axios, { AxiosError, AxiosRequestConfig, AxiosResponse, InternalAxiosRequestConfig, } from "axios" export interface ApiResponse<T = unknown> { data: T message?: string | null success?: boolean } // Axios 인스턴스 생성 export const axiosInstance = axios.create({ baseURL: process.env.NEXT_PUBLIC_API_SERVER_URL, timeout: 30 * 1000, // 30초 headers: { "Content-Type": "application/json", }, }) // Request Interceptor axiosInstance.interceptors.request.use( async (config: InternalAxiosRequestConfig) => { // 인증 토큰 추가 const token = localStorage.getItem("auth_token") if (token) { config.headers.Authorization = `Bearer ${token}` } // 요청 로깅 (개발 환경) if (process.env.NODE_ENV === "development") { console.log("API Request:", config.method?.toUpperCase(), config.url) } return config }, (error) => { return Promise.reject(error) }, ) // Response Interceptor axiosInstance.interceptors.response.use( async (response: AxiosResponse<ApiResponse>) => { // 응답 로깅 (개발 환경) if (process.env.NODE_ENV === "development") { console.log("API Response:", response.status, response.config.url) } return response }, async (error: AxiosError<ApiResponse>) => { // 에러 응답 처리 if (error.response) { const { status, data } = error.response // HTTP 상태 코드별 처리 switch (status) { case 401: // 인증 실패 - 로그인 페이지로 리다이렉트 console.error("인증 실패: 로그인이 필요합니다") localStorage.removeItem("auth_token") window.location.href = "/login" break case 403: // 권한 없음 console.error("접근 권한이 없습니다") break case 404: // 리소스 없음 console.error("요청한 리소스를 찾을 수 없습니다") break case 500: case 502: case 503: // 서버 에러 console.error("서버 오류가 발생했습니다") break default: console.error("API 오류:", data?.message || "알 수 없는 오류") } } else if (error.request) { // 요청은 전송되었으나 응답을 받지 못함 (네트워크 에러) console.error("네트워크 오류: 서버에 연결할 수 없습니다") } else { // 요청 설정 중 에러 발생 console.error("요청 설정 오류:", error.message) } return Promise.reject(error) }, ) // API 헬퍼 함수 export const api = { get: axiosInstance.get, post: axiosInstance.post, put: axiosInstance.put, patch: axiosInstance.patch, delete: axiosInstance.delete, $get: async <T,>( url: string, params?: object, config?: AxiosRequestConfig, ) => { return axiosInstance .get<T>(url, { params, ...config }) .then((res) => res.data) }, $post: async <T,>( url: string, data?: object, config?: AxiosRequestConfig, ) => { return axiosInstance.post<T>(url, data, config).then((res) => res.data) }, $put: async <T,>(url: string, data?: object, config?: AxiosRequestConfig) => { return axiosInstance.put<T>(url, data, config).then((res) => res.data) }, $patch: async <T,>( url: string, data?: object, config?: AxiosRequestConfig, ) => { return axiosInstance.patch<T>(url, data, config).then((res) => res.data) }, $delete: async <T,>(url: string, config?: AxiosRequestConfig) => { return axiosInstance.delete<T>(url, config).then((res) => res.data) }, }

주요 기능:

  • Request Interceptor: 인증 토큰 자동 추가, 요청 로깅
  • Response Interceptor: 상태 코드별 에러 처리
  • 401 에러: 자동 로그아웃 및 로그인 페이지 리다이렉트
  • 네트워크 에러: 연결 실패 감지 및 처리
  • 헬퍼 함수: $get, $post 등으로 간편한 API 호출

사용 예제

// features/users/api/get-users.ts import { api } from "@/lib/axios" import { GetUsersParamsSchema, GetUsersResponseSchema } from "../schemas" import type { GetUsersParams, GetUsersResponse } from "../schemas" export async function getUsers(params?: GetUsersParams) { // 요청 파라미터 검증 const validatedParams = GetUsersParamsSchema.parse(params) // API 호출 const res = await api.$get<GetUsersResponse>("/users", validatedParams) // 응답 데이터 검증 return GetUsersResponseSchema.parse(res) }

2. Fetch API 클라이언트

재사용 가능한 Fetch 기반 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