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
✅ 권장 사항
-
에러 처리
- 상태 코드별 처리
- 명확한 에러 메시지
- 재시도 로직 구현
- 타임아웃 설정
-
보안
- 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