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
✅ 권장 사항
-
에러 처리
- 상태 코드별 처리
- 명확한 에러 메시지
- 재시도 로직 구현
- 타임아웃 설정
-
보안
- HTTPS 사용
- 토큰 안전하게 저장
- CORS 올바르게 설정
- API 키 환경변수 관리
-
성능
- 요청 디바운싱/쓰로틀링
- 응답 캐싱
- 압축 활성화
- 페이지네이션
-
타입 안전성
- TypeScript 타입 정의
- Zod로 런타임 검증
- API 스키마 공유
-
개발 경험
- API 문서화
- Mock 데이터 제공
- 개발자 도구 통합
⚠️ 피해야 할 것
-
보안 취약점
- 평문 비밀번호 전송
- 클라이언트에 API 키 노출
- CORS 와일드카드 허용
-
성능 문제
- 불필요한 중복 요청
- 과도한 데이터 전송
- 재시도 무한 루프
-
유지보수 문제
- 하드코딩된 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 예제는 곧 제공될 예정입니다.
로컬에서 실행하기
-
프로젝트 생성
npx @vortex/cli init my-api-project --template next-app cd my-api-project -
의존성 설치
pnpm add zod @tanstack/react-query -
환경변수 설정
echo "NEXT_PUBLIC_API_URL=http://localhost:3001" > .env.local -
코드 복사 및 실행
pnpm dev
관련 패턴
- Data Fetching - 데이터 가져오기
- Error Handling - API 에러 처리
- Authentication - API 인증
- Security Patterns - API 보안
Last updated on