next-type-fetch 라이브러리: Next.js와 Zod를 활용한 타입 안전한 API 클라이언트
Next.js App Router 환경에서 타입 안전한 API 요청을 위한 fetch 기반 라이브러리 개발 과정과 활용 사례

Next.js의 App Router와 함께 사용할 수 있는 타입 안전한 HTTP 클라이언트 라이브러리를 개발했습니다. 이 글에서는 라이브러리 개발 배경, 주요 기능, 구현 과정, 그리고 실제 Next.js 애플리케이션에서의 활용 사례를 공유합니다.
작업 개요
작업 배경
Next.js App Router를 사용하는 프로젝트에서 API 요청을 처리할 때 다음과 같은 문제점을 발견했습니다:
- 기본
fetch
API는 편의 기능이 부족함 - 타입 안전성을 위한 별도의 검증 코드가 필요함
- Next.js의 캐싱과 통합하기 위한 설정이 반복적임
- axios와 같은 기존 라이브러리는 Next.js 환경에 최적화되지 않음
이러한 문제를 해결하기 위해 Next.js에 최적화된 타입 안전한 HTTP 클라이언트 라이브러리를 개발하게 되었습니다.
목적
- TypeScript와 Zod를 활용한 완전한 타입 안전성 제공
- axios와 유사한 직관적인 API 인터페이스 제공
- Next.js의 캐싱 메커니즘과 완벽하게 통합
- 요청과 응답 인터셉터를 통한 확장성 제공
사용 기술/도구
- TypeScript: 타입 안전성 보장
- Zod: 런타임 데이터 검증
- Vitest: 유닛 테스트
- Next.js App Router: 통합 테스트
구현 과정
수행한 작업
1. 인터페이스 설계
먼저 라이브러리의 사용성을 고려하여 다음과 같은 인터페이스를 설계했습니다:
// 라이브러리 인스턴스 생성
const api = createFetch({
baseURL: 'https://api.example.com',
headers: { ... },
timeout: 5000
});
// HTTP 메서드 사용
const result = await api.get('/users', { schema: userSchema });
// 결과 처리
if (result.error) {
// 오류 처리
} else {
// 타입 안전한 데이터 사용
console.log(result.data);
}
2. 코어 기능 구현
다음으로 라이브러리의 핵심 기능을 구현했습니다:
createFetch
: 기본 설정을 받아 API 클라이언트 인스턴스 생성- HTTP 메서드 래퍼 (
get
,post
,put
,delete
,patch
) - 요청 처리 및 응답 파싱 로직
3. 인터셉터 메커니즘 구현
요청과 응답을 가로채서 수정할 수 있는 인터셉터 시스템을 구현했습니다:
InterceptorManager
클래스: 인터셉터 등록, 제거, 실행 기능- 요청 인터셉터: 요청 전송 전 설정 수정 가능
- 응답 인터셉터: 응답 처리 전 데이터 변환 가능
- 오류 인터셉터: 오류 발생 시 처리 로직
4. 유틸리티 함수 개발
라이브러리 내부에서 사용할 다양한 유틸리티 함수를 개발했습니다:
- URL 조합 및 쿼리 파라미터 처리
- 설정 병합
- 타임아웃 처리
- 데이터 직렬화
5. 테스트 작성
라이브러리의 안정성을 보장하기 위해 다양한 테스트를 작성했습니다:
- 기본 인스턴스 생성 테스트
- HTTP 메서드별 요청 실행 테스트
- Zod 스키마 검증 테스트
- 인터셉터 동작 테스트
- 오류 처리 테스트
주요 포인트
타입 안전성 구현
TypeScript와 Zod를 조합하여 완벽한 타입 안전성을 구현했습니다:
- TypeScript 인터페이스를 통한 정적 타입 검사
- Zod 스키마를 통한 런타임 데이터 검증
- 제네릭을 활용한 타입 추론
// Zod 스키마 정의
const userSchema = z.object({
id: z.number(),
name: z.string(),
email: z.string().email()
});
// 타입 안전한 API 요청
const result = await api.get('/users/1', { schema: userSchema });
인터셉터 시스템 설계
확장성을 고려한 인터셉터 시스템을 설계했습니다:
// 요청 인터셉터
api.interceptors.request.use((config) => {
return {
...config,
headers: {
...config.headers,
Authorization: `Bearer ${getToken()}`
}
};
});
// 응답 인터셉터
api.interceptors.response.use(
(response) => {
// 응답 데이터 변환
return response.data;
},
(error) => {
// 오류 처리
return Promise.reject(error);
}
);
Next.js 통합
Next.js App Router와의 통합을 위한 기능을 구현했습니다:
// Next.js 캐싱 옵션 지원
const result = await api.get('/users', {
cache: 'force-cache',
next: {
tags: ['users'],
revalidate: 60
}
});
해결한 문제
- 타입 불일치 문제: Zod 스키마를 통해 백엔드 API 응답과 프론트엔드 타입 간의 불일치 해결
- 캐싱 설정 복잡성: Next.js 캐싱 옵션을 API 클라이언트에 통합하여 사용 편의성 향상
- 인증 토큰 관리: 인터셉터를 통한 자동 토큰 추가 및 갱신
- 오류 처리 일관성: 표준화된 오류 처리 패턴 제공
주요 코드
라이브러리 인스턴스 생성 함수
export function createFetch(defaultConfig: FetchConfig = {}): NextTypeFetch {
const interceptors = createInterceptors();
async function request<T = any>(config: RequestConfig): Promise<ZodResponse<T>> {
try {
// 스키마 추출
const schema = config.schema as z.ZodType<T> | undefined;
delete config.schema;
// 요청 인터셉터 실행
const requestConfig = await interceptors.request.run(config);
// URL 조합 및 쿼리 파라미터 추가
const url = combineURLs(requestConfig.baseURL, requestConfig.url);
const fullUrl = appendQueryParams(url, requestConfig.params);
// 타임아웃 설정
const timeoutResult = createTimeoutPromise(requestConfig.timeout);
// fetch 요청 실행
const response = await (timeoutResult
? Promise.race([fetch(fullUrl, requestInit), timeoutResult.promise])
: fetch(fullUrl, requestInit));
// 응답 처리 및 스키마 검증
// ...
return { data, error: null, status, headers };
} catch (error) {
// 오류 처리
// ...
return { data: null, error: { message, raw: processedError } };
}
}
// HTTP 메서드별 래퍼 함수들
// ...
return instance;
}
인터셉터 매니저 클래스
export class InterceptorManager<T> {
private handlers: Array<{ id: number; handler: T } | null> = [];
private idCounter = 0;
use(handler: T): number {
const id = this.idCounter++;
this.handlers.push({ id, handler });
return id;
}
eject(id: number): void {
const index = this.handlers.findIndex((h) => h !== null && h.id === id);
if (index !== -1) {
this.handlers[index] = null;
}
}
async forEach<V>(value: V): Promise<V> {
let result = value;
for (const handler of this.handlers) {
if (handler !== null) {
result = (await handler.handler(result as any)) as unknown as V;
}
}
return result;
}
}
결과
구현 결과
next-type-fetch 라이브러리로 구현한 기능은 다음과 같습니다:
- 타입 안전한 API 요청: TypeScript와 Zod를 활용한 타입 지원
- 편리한 API: axios와 유사한 직관적인 인터페이스 제공
- 인터셉터 지원: 요청 및 응답 가로채기와 수정 기능
- Next.js 통합: App Router의 캐싱 메커니즘과 통합
- 확장성: 다양한 사용 사례에 적용 가능한 유연한 설계
실제 프로젝트 적용 사례
Next.js 프로젝트에 next-type-fetch 라이브러리를 적용한 결과, 다음과 같은 이점을 얻을 수 있었습니다:
API 클라이언트 설정
// lib/api.ts
import { createFetch } from 'next-type-fetch';
import { z } from 'zod';
// API 클라이언트 생성
export const api = createFetch({
baseURL: API_URL,
headers: {
'Content-Type': 'application/json',
},
});
// 사용자 스키마 정의
export const userSchema = z.object({
id: z.number(),
name: z.string(),
email: z.string().email(),
});
// 사용자 응답 스키마
export const usersResponseSchema = z.object({
users: z.array(userSchema),
lastUpdated: z.string(),
timestamp: z.string(),
});
// 유틸리티 함수
export async function getUsers(options?: {
cache?: RequestCache;
revalidate?: number;
}) {
return api.get<UsersResponse>('/users', {
schema: usersResponseSchema,
cache: options?.cache,
next: {
tags: ['users'],
...(options?.revalidate ? { revalidate: options.revalidate } : {}),
},
});
}
서버 컴포넌트에서 사용
// app/users/page.tsx
import { getUsers } from '@/lib/api';
export const revalidate = 30; // 30초마다 재검증
export default async function UsersPage() {
// 캐시 태그를 이용해 데이터 가져오기
const result = await getUsers({
revalidate: 30,
});
if (result.error) {
return <div>오류가 발생했습니다: {result.error.message}</div>;
}
const { users, lastUpdated, timestamp } = result.data;
return (
<div>
<h1>사용자 목록</h1>
{/* 사용자 데이터 렌더링 */}
</div>
);
}
캐싱 제어 옵션
// 캐싱 없이 항상 새로운 데이터를 요청
const result = await getUsers({
cache: 'no-store',
});
// 캐시된 데이터 사용 (ISR 방식)
const result = await getUsers({
cache: 'force-cache',
revalidate: 60,
});
수동 재검증 구현
// Server Action
export async function revalidateUserData() {
try {
// 백엔드 API 호출
await backendApi.put('/users/revalidate');
// Next.js 캐시 태그 무효화
revalidateTag('users');
return { success: true };
} catch (error) {
return { success: false, error };
}
}
배운 점
이 라이브러리를 개발하면서 얻은 주요 인사이트:
- 타입 안전성의 중요성: TypeScript와 Zod의 조합이 런타임 안전성을 크게 향상시킴
- 인터셉터 패턴의 유용성: 요청/응답 처리를 모듈화하고 분리할 수 있어 코드 관리가 용이해짐
- Next.js 캐싱 메커니즘: App Router의 다양한 캐싱 전략을 효과적으로 활용하는 방법
- 테스트 주도 개발: 유닛 테스트를 통해 라이브러리의 안정성 보장
- API 설계의 중요성: 직관적인 API 설계가 라이브러리 사용성에 큰 영향을 미침
참고 자료
링크
문서
향후 계획
이 글은 next-type-fetch 라이브러리 개발 시리즈의 첫 번째 글입니다. 앞으로의 개발 계획에서 가장 먼저 추가할 두 가지 핵심 기능을 소개합니다.
1. 자동 재시도 기능 구현
네트워크 오류나 일시적인 서버 장애가 발생했을 때 자동으로 요청을 재시도하는 기능을 구현할 예정입니다. 이는 불안정한 네트워크 환경에서 애플리케이션의 복원력을 크게 향상시킬 수 있습니다.
// 재시도 옵션 설정
const result = await api.get('/users', {
retry: 3, // 최대 3번 재시도
retryDelay: 1000, // 재시도 간 1초 대기
retryCondition: (error) => error.status >= 500, // 서버 오류일 때만 재시도
});
구현 계획:
- 지수 백오프 알고리즘(Exponential Backoff)을 적용한 재시도 메커니즘
- 사용자 정의 재시도 조건 함수 지원
- 재시도 횟수, 간격, 최대 대기 시간 등의 설정 옵션
- 재시도 진행 상황 모니터링을 위한 이벤트 콜백
2. 동시 요청 처리 및 취소 기능
여러 API 요청을 효율적으로 관리하고 필요한 경우 취소할 수 있는 기능을 추가할 예정입니다. 이를 통해 복잡한 데이터 요구사항을 가진 애플리케이션에서 성능과 사용자 경험을 개선할 수 있습니다.
// 여러 요청 동시 실행
const [usersResult, postsResult, commentsResult] = await api.all([
api.get('/users', { schema: usersSchema }),
api.get('/posts', { schema: postsSchema }),
api.get('/comments', { schema: commentsSchema }),
]);
// 첫 번째 성공하는 요청 결과 사용
const result = await api.race([
api.get('/mirror1/users'),
api.get('/mirror2/users'),
api.get('/mirror3/users'),
]);
// 요청 취소 예제
const controller = new AbortController();
const request = api.get('/long-operation', {
signal: controller.signal
});
// 사용자 작업이나 타임아웃에 따라 요청 취소
setTimeout(() => {
controller.abort('Operation timed out');
}, 5000);
구현 계획:
Promise.all
과Promise.race
를 활용한 병렬 요청 처리- AbortController를 활용한 요청 취소 메커니즘
- 타임아웃 자동 취소 옵션
- 부분 실패 시 대응 전략 (일부 요청만 성공한 경우의 처리)
- 모든 요청에 대한 진행 상황 추적
시리즈 글 계획
이 라이브러리 개발 과정은 다음과 같은 시리즈로 계속 공유할 예정입니다:
-
Part 1: 기본 구조와 타입 안전성 (현재 글)
- 라이브러리 설계 원칙과 기본 구현
- TypeScript와 Zod를 활용한 타입 안전성
-
Part 2: 자동 재시도 메커니즘 구현
- 네트워크 복원력을 위한 재시도 로직 설계
- 지수 백오프 알고리즘 구현
- 사용자 정의 재시도 조건
-
Part 3: 동시 요청 처리 및 취소
- 병렬 요청 처리 구현
- AbortController를 활용한 요청 취소
- 병렬 요청의 오류 처리 전략
각 글에서는 구현 과정뿐만 아니라 실제 프로젝트에서의 적용 사례와 성능 측정 결과도 함께 공유할 예정입니다.
결론
next-type-fetch 라이브러리 개발은 계속 진행 중인 프로젝트입니다. 이 글에서 공유한 현재까지의 구현 내용을 기반으로 더 많은 기능과 개선점을 추가해 나갈 예정입니다. 긴 글 읽어주셔서 감사합니다!