const-tommy.dev
기록을 불러오는 중입니다
프론트엔드 아키텍처에서 API 통신은 단순히 데이터를 가져오는 행위를 넘어, 불확실한 네트워크 환경 속에서 데이터의 무결성을 보장하고 통신의 가용성을 주도적으로 통제하는 과정이다. Next.js 공식 문서 역시 서버 측 호출 시 fetch 사용을 권장하며, 프레임워크의 핵심 예제들 또한 fetch를 표준으로 삼고 있다. muroom 프로젝트는 이러한 기술적 지향점에 발맞추어 기성 라이브러리인 Axios에 의존하지 않고, 브라우저와 Node.js 표준인 Fetch API를 래핑한 customFetch 를 직접 설계하여 통신의 신뢰도를 높였다.
fetch는 환경에 구애받지 않는 최적의 선택지였다.
📸 범용 라이브러리의 복잡한 내부 구조와 대비되는, Native Fetch 기반의 직관적인
customFetch흐름도
설계 원칙을 실현하기 위해 구현된 customFetch는 단순한 래퍼를 넘어 타임아웃 제어, 환경별 주소 분기, 표준화된 에러 처리 로직을 내재화했다.
import { BE_BASE_URL } from '@/config/constants';
import { ApiRequestError, type ApiResponse } from '@/types/api';
import { HttpSuccessStatusCode } from '@/types/http';
interface CustomRequestInit extends RequestInit {
timeout?: number;
}
function isSuccessResponse<T>(
response: ApiResponse<T>,
): response is { status: HttpSuccessStatusCode; data?: T; message: string } {
return response.status >= 200 && response.status < 300;
}
export const customFetch = async <T>(
url: string,
options: CustomRequestInit = {},
): Promise<T> => {
const { timeout = 2000, ...restOptions } = options;
const baseHeaders: Record<string, string> = {
'Content-Type': 'application/json',
};
const mergedOptions: CustomRequestInit = {
...restOptions,
credentials: restOptions.credentials || 'include',
headers: {
...baseHeaders,
...restOptions.headers,
},
signal: restOptions.signal || AbortSignal.timeout(timeout),
};
const baseUrl = typeof window === 'undefined' ? BE_BASE_URL : '';
const fullUrl = `${baseUrl}/api/v1${url}`;
try {
const response = await fetch(fullUrl, mergedOptions);
if (!response.ok) {
try {
const errorData = await response.json();
throw new ApiRequestError(errorData);
} catch (e) {
if (e instanceof ApiRequestError) throw e;
throw new Error('서버 응답 파싱 실패');
}
}
const responseData: ApiResponse<T> = await response.json();
if (isSuccessResponse(responseData)) {
return (responseData.data ?? true) as T;
} else {
throw new ApiRequestError(responseData);
}
} catch (error: any) {
if (error.name === 'TimeoutError') {
console.error(`⏱️ [Timeout] ${timeout}ms 경과로 요청 중단: ${fullUrl}`);
throw new Error('요청 시간이 초과되었습니다. 잠시 후 다시 시도됩니다.');
}
if (typeof window === 'undefined') {
console.error(`❌ [Server Fetch Error] ${fullUrl}:`, error);
}
if (error instanceof ApiRequestError) {
throw error;
}
throw new Error(
error instanceof Error ? error.message : '네트워크 요청 실패',
);
}
};백엔드 서버가 간헐적으로 응답 지연(Pending) 상태에 빠질 때, 유저가 무한 로딩을 겪지 않도록 AbortSignal.timeout을 도입했다.
📸 (좌) 네트워크 탭에서 확인되는 다수의 'Canceled' 요청 상태 / (우)
customFetch내부 로직에 의해 출력된 타임아웃 중단 로그
// customFetch 내부의 타임아웃 및 세션 유지 설정
const { timeout = 2000, ...restOptions } = options;
const mergedOptions: CustomRequestInit = {
...restOptions,
credentials: restOptions.credentials || 'include',
signal: restOptions.signal || AbortSignal.
Next.js의 App Router는 요청 주체에 따라 Base URL 전략이 달라야 한다. 이를 위해 실행 환경에 따른 자동 분기 로직을 구현했다.
BE_BASE_URL)를, 클라이언트 사이드에서는 상대 경로를 사용한다.에러를 단순한 장애 상황으로 치부하지 않고, 서비스 결함을 파악하기 위한 데이터로 자산화했다.
error.tsx에서 예외 발생 시 구글 애널리틱스(GA)로 상세 로그를 전송한다. 대시보드 반영 지연과 상관없이 네트워크 페이로드 분석을 통해 데이터 송출의 무결성을 상시 검증한다.ErrorTemplate은 useResponsiveLayout과 결합하여 404/500 에러 시 모바일과 웹 환경에 각각 최적화된 복구 경로(이전으로, 홈으로)를 제공한다.
📸 커스텀 500 에러 UI 렌더링과 동시에 GA4 'collect' 신호(204 Status)가 발생하는 실제 화면
단순히 데이터를 가져오는 것을 넘어, 가져온 데이터가 어떤 형태인지 코드가 스스로 이해하게 만드는 것이 핵심이다. 성공 응답 데이터 구조인 ApiResponse<T>에 제네릭을 적용하여 비즈니스 로직에서의 타입 추론을 완벽하게 지원하도록 설계했다.
<T> 활용: 호출부에서 기대하는 데이터 타입을 직접 주입함으로써, 정의되지 않은 프로퍼티에 접근하려는 시도를 컴파일 단계에서 차단한다.
📸
responseData.입력 시 정의된 인터페이스 필드들이 즉시 자동 완성되는 인텔리센스 화면
customFetch를 직접 구현해 본 과정은 네트워크의 불확실성을 코드로 직접 다뤄보며 시스템의 예측 가능성을 높이는 유익한 경험이었다.
외부 라이브러리에 전적으로 의존하기보다 표준 API를 활용해 프로젝트 성격에 맞는 최적의 구조를 고민해 본 덕분에, 더 가볍고 관리하기 쉬운 독자적인 통신 계층을 구축할 수 있었다. 결국 안정적인 개발 환경을 바탕으로 사용자에게 더 매끄러운 경험을 제공하는 것, 이것이 이번 작업을 통해 얻은 가장 실질적인 경험이이었다.