Hi, I’m Kang Byeong-hyeon.
API 통신부터 에러 핸들링까지(feat. Axios)
Axios란?

Axios는 Node.js와 브라우저 모두에서 사용할 수 있는 Promise 기반의 HTTP 클라이언트이다.
동일한 코드베이스를 사용해 브라우저와 Node.js에서 API 요청을 실행할 수 있는 동형(isomorphic) 클라이언트로, 서버 사이드에서는 네이티브 Node.js의 http 모듈을 사용하고, 클라이언트에서는 XMLHttpRequests를 사용한다.
Axios 설치
사용하는 패키지 매니저에 따라 설치하면된다.
아래의 설치 예시는 npm 패키지 매니저를 기반으로 설치한 명령어이다.
npm install axios
API 통신을 위한 관리
OAS란?
OAS(OpenAPI Specification)는 RESTful API의 구조와 동작 방식을 명확히 문서화하여, 개발자들이 API를 쉽게 이해할 수 있도록 돕는 도구이다.
OAS는 JSON이나 YAML 형식으로 작성되괴, API의 인증 방법, 엔드포인트, 요청 및 응답 데이터 등을 명세서에 포함한다.
OAS 문서는 다음과 같이 oas.yaml로 작성할 수 있다.
openapi: 3.0.1
info:
title: 'API'
description: 'API'
version: 1.0.0
paths:
/api/v1/ranking/products:
get:
parameters:
- in: query
name: targetType
schema:
type: string
enum: [ALL, FEMALE, MALE, TEEN]
default: ALL
required: false
description: 선물 랭킹 대상자 필터
- in: query
name: rankType
schema:
type: string
enum: [MANY_WISH, MANY_RECEIVE, MANY_WISH_RECEIVE]
default: MANY_WISH_RECEIVE
required: false
description: 선물 랭킹 정렬 필터
responses:
200:
description: 선물 랭킹 목록을 가져온다.
content:
application/json:
schema:
type: object
properties:
products:
type: array
items:
$ref: '#/components/schemas/ProductData'
required:
- products
...
.env 파일로 BASE_URL 분리
환경변수 파일(.env)을 사용해 API의 BASE_URL을 관리하면, 코드에서 직접 URL을 수정할 필요 없이 환경별로 다른 URL을 설정할 수 있다.
참고로, CRA 프로젝트에서는 다음과 같이 REACT_APP 접두사를 붙여야 한다.
REACT_APP_BASE_URL=https://test.vercel.app
API 타입 관리
oas.yaml 파일을 통해 요청과 응답의 구조를 파악하고, API 콜 함수는 apis 디렉토리에서 관리하도록 하였다.
요청과 응답의 타입들은 types 디렉토리에 미리 정의하여 코드의 유지보수성을 높일 수 있도록 하였다.
types
├── dataTypes.ts
├── requestTypes.ts
└── responseTypes.ts
요청 및 응답 타입 정의
요청과 응답의 타입을 미리 정의하여 코드의 안정성과 일관성을 유지할 수 있도록 하였다.
필자는 일단 types 디렉토리에 requestTypes.ts, responseTypes.ts로 분리하여 타입을 정의하였다.
추후에 프로젝트가 커지고, 요청하는 API콜도 많아지면 types 파일들을 관련 타입들을 apis 디렉토리에 있는 API콜 디렉토리에 가깝게 배치하여 요지보수에 용이할 수 있도록 관리할 예정이다.
// types/requestTypes.ts
export interface RankingProductsRequest {
targetType?: "ALL" | "FEMALE" | "MALE" | "TEEN";
rankType?: "MANY_WISH" | "MANY_RECEIVE" | "MANY_WISH_RECEIVE";
}
// types/responseTypes.ts
import { ProductData, ThemeData } from "./dataTypes";
export interface RankingProductsResponse {
products: ProductData[];
}
export interface ThemesResponse {
themes: ThemeData[];
}
API 콜 관리
정의된 타입들을 기반으로 apis 디렉토리에서 관련 API 호출을 관리하도록 하였다.
요청 경로 같은 경우 path.ts 파일에 상수로 관리하여, 나중에 path가 변경되었을 경우 유지보수를 쉽게 할 수 있도록 하였다.
apis
├── instance.ts
└── themes
├── index.ts
└── path.ts
Axios 인스턴스 작성
API 호출 시 매번 baseURL을 작성하는 것은 비효율적이다.
이를 해결하기 위해 Axios의 create 메소드를 사용해 미리 baseURL을 지정하고, 타임아웃 설정을 추가하여 응답이 10초 이상 지연될 경우 요청을 취소할 수 있도록 하였다.
이후, 해당 axiosInsntance를 API 콜 함수에서 사용하기만 하면 된다.
import axios from "axios";
const axiosInstance = axios.create({
baseURL: process.env.REACT_APP_BASE_URL,
timeout: 10000,
});
export default axiosInstance;
Http Status에 따른 에러메시지 처리
Axios의 인터셉터를 사용해 응답을 처리하기 전 HTTP 상태 코드에 따라 에러 메시지를 처리할 수 있다.
에러 메시지도 상수로 관리하여 나중에 에러메시지가 변경될 수 있기 때문에 유지보수를 용이하게 하기 위해 다음과 같이 관리하였다.
export const ERROR = {
DATA_FETCH: "데이터를 불러오는 중에 문제가 발생했습니다.",
NO_PRODUCTS: "보여줄 상품이 없어요!",
NOT_FOUND: "요청하신 페이지를 찾을 수 없습니다.",
SERVER_ERROR: "서버에 문제가 발생했습니다. 잠시 후 다시 시도해주세요.",
UNAUTHORIZED: "인증되지 않은 사용자입니다. 로그인 후 다시 시도해주세요.",
FORBIDDEN: "접근 권한이 없습니다.",
BAD_REQUEST: "잘못된 요청입니다. 입력한 정보를 확인해주세요.",
TIMEOUT: "요청 시간이 초과되었습니다. 잠시 후 다시 시도해주세요.",
CONFLICT: "데이터 충돌이 발생했습니다. 잠시 후 다시 시도해주세요.",
};
// apis/instance.ts
import axios, { AxiosError } from "axios";
import { ERROR } from "@utils/constants/message";
const axiosInstance = axios.create({
baseURL: process.env.REACT_APP_BASE_URL,
timeout: 10000,
});
const statusMessages: { [key: number]: string } = {
400: ERROR.DATA_FETCH,
401: ERROR.UNAUTHORIZED,
403: ERROR.FORBIDDEN,
404: ERROR.NOT_FOUND,
408: ERROR.TIMEOUT,
409: ERROR.CONFLICT,
500: ERROR.SERVER_ERROR,
};
axiosInstance.interceptors.response.use(
(res) => res,
(e: AxiosError) => {
if (e.response) {
const message = statusMessages[e.response.status];
e.message = message;
}
return Promise.reject(e);
}
);
export default axiosInstance;
API 경로 상수 관리
API의 경로(path)는 변경될 수 있는 사항이므로 상수로 관리하는 것이 유지보수에 좋다.
요청과 관련한 경로를 미리 상수로 정의하여 코드의 가독성과 유지보수성을 높인다.
const API_BASE = "/api/v1";
export const THEME_PATHS = {
THEMES: `${API_BASE}/themes`,
THEME_PRODUCT: (themeKey: string) =>
`${API_BASE}/themes/${themeKey}/products`,
};
API 호출 함수 작성
정의한 경로와 인스턴스를 사용해 API 호출 함수를 작성한다.
API 호출 함수 내부에서 try-catch 문을 사용하지 않은 이유는, 각 컴포넌트에서 일관된 방식으로 오류를 처리하기 위함이다.
// apis/themes/index.ts
import { ThemesResponse } from "@/types/responseTypes";
import axiosInstance from "../instance";
import { THEME_PATHS } from "./path";
export const getThemes = async (): Promise<ThemesResponse> => {
const res = await axiosInstance.get<ThemesResponse>(THEME_PATHS.THEMES);
return res.data;
};
export const getThemesProducts = async (
params: ThemeProductsRequest
): Promise<ThemeProductsResponse> => {
const { themeKey, pageToken, maxResults } = params;
const queryParams = { pageToken, maxResults };
const res = await axiosInstance.get<ThemeProductsResponse>(
THEME_PATHS.THEME_PRODUCT(themeKey),
{
params: queryParams,
}
);
return res.data;
};
에러 핸들링하기
Error Boundary와 Suspense를 통해 선언적으로 비동기 에러 핸들링을 할 수 있지만, 이 예제에서는 나이브하게 useFetch 훅을 구현하여 비동기 통신에 대한 데이터 상태 관리와 에러 핸들링을 직접 다뤄볼 예정이다.
useFetch 구현
API 호출 중 요청 상태와 에러 발생 시 각 컴포넌트에서 이를 다르게 처리하기 위해 useFetch라는 커스텀 훅을 작성했다.
이를 통해 비동기 요청 상태와 에러 상태를 관리할 수 있도록 하였다.
import { useState, useEffect, useCallback, useMemo } from "react";
import { AxiosError, AxiosRequestConfig } from "axios";
interface FetchState<T> {
isLoading: boolean;
isError: boolean;
data: T | null;
error: AxiosError | null;
}
export default function useFetch<T, P = undefined>(
apiCall: (params?: P) => Promise<T>,
apiParams?: P,
options?: AxiosRequestConfig
): FetchState<T> {
const [fetchState, setFetchState] = useState<FetchState<T>>({
isLoading: true,
isError: false,
error: null,
data: null,
});
const memoizedApiCall = useCallback(apiCall, [apiCall]);
const memoizedParams = useMemo(() => JSON.stringify(apiParams), [apiParams]);
useEffect(() => {
const fetchData = async () => {
setFetchState({
isLoading: true,
isError: false,
data: null,
error: null,
});
try {
const data = await memoizedApiCall(apiParams);
setFetchState({ isLoading: false, isError: false, data, error: null });
} catch (error) {
const axiosError = error as AxiosError;
console.error("Error fetching data: ", error);
setFetchState({
isLoading: false,
isError: true,
data: null,
error: axiosError,
});
}
};
fetchData();
}, [memoizedApiCall, memoizedParams]);
return fetchState;
}
이렇게 작성된 useFetch 훅을 사용하여 상태를 관리하고, 컴포넌트에서 API 데이터를 손쉽게 사용할 수 있다.
...
import useFetch from '@hooks/useFetch';
const GRID_GAP = 0;
const GRID_COLUMNS = 6;
export default function ThemeCategory() {
const { isLoading, isError, error, data } = useFetch<ThemesResponse>(getThemes);
return (
<ThemeCategoryContainer>
<CenteredContainer maxWidth="md">
<Grid gap={GRID_GAP} columns={GRID_COLUMNS}>
...
</Grid>
</CenteredContainer>
</ThemeCategoryContainer>
);
}
StatusHandler 컴포넌트를 통한 Spinner 및 에러 메시지 렌더링
useFetch로 받은 상태에 따라 컴포넌트를 렌더링해주는 StatusHandler 라는 컴포넌트로 관리하였다.
isLoading 상태가 true이면 Spinner 컴포넌트를 렌더링하고, 에러가 발생하면 ErrorMessage 컴포넌트에 에러 메시지를 전달하여 출력하도록 하였다.
추가로 데이터가 없을 경우, isEmpty 상태를 통해 에러 메시지를 출력하도록 구현하였다.
import React, { ReactNode } from "react";
import { AxiosError } from "axios";
import { ERROR } from "@utils/constants/message";
import ErrorMessage from "./ErrorMessage";
import Spinner from "./Spinner";
interface StatusHanlderProps {
isLoading: boolean;
isError: boolean;
isEmpty: boolean;
error?: AxiosError | null;
children: ReactNode;
}
export default function StatusHandler({
isLoading,
isError,
isEmpty,
error,
children,
}: StatusHanlderProps) {
if (isLoading) return <Spinner />;
if (isError && error) return <ErrorMessage message={error.message} />;
if (isEmpty) return <ErrorMessage message={ERROR.NO_PRODUCTS} />;
return <div>{children}</div>;
}
해당 StatusHandler 컴포넌트를 API 비동기 호출을 사용하는 컴포넌트 내부에 데이터 렌더링 시 로딩, 에러, 데이터 상태에 따라 적절한 UI를 제공할 수 있도록 구현할 수 있다.
export default function ThemeCategory() {
const { data, error, isError, isLoading } =
useFetch<ThemesResponse>(getThemes);
const isEmpty = !data || data?.themes.length === 0;
return (
<ThemeCategoryContainer>
<CenteredContainer maxWidth="md">
<StatusHandler
isLoading={isLoading}
isError={isError}
isEmpty={isEmpty}
error={error}
>
<Grid gap={GRID_GAP} columns={GRID_COLUMNS}>
...
</Grid>
</StatusHandler>
</CenteredContainer>
</ThemeCategoryContainer>
);
}
마치면서
이번 포스팅에서는 Axios를 활용해 API 통신을 효율적으로 관리하고, 발생할 수 있는 다양한 에러를 어떻게 처리할 수 있는지 알아보았다.
Axios의 기본 사용법을 익히고, 에러 핸들링을 중앙화하며 API 경로를 체계적으로 관리하는 방법을 다루어보았다.
또한, useFetch 훅을 구현해 비동기 API 호출에 따른 데이터 상태를 효과적으로 관리할 수 있도록 하였고, StatusHandler 컴포넌트를 통해 useFetch 훅으로 받은 상태에 따라 로딩, 에러, 데이터 상태에 맞는 렌더링을 처리할 수 있는 방법도 소개해드렸다.
나중에는 React Query와 ErrorBoundary, Suspense를 통해 더욱 간편하게 비동기 데이터를 관리하고, 효율적인 렌더링을 구현할 수 있다.
이번 포스팅에서는 Axios를 활용한 나이브한 접근 방식을 통해 기본 개념을 이해하는 데 중점을 두어보았다.