단일 인스턴스의 API 클라이언트 사용
애플리케이션이 RESTful 또는 GraphQL API와 상호 작용할 때, 미리 구성된 단일 인스턴스의 API 클라이언트를 사용하는 것이 유익하다. 예를 들어, 네이티브 fetch API 또는 axios, graphql-request, apollo-client와 같은 라이브러리를 사용하여 단일 API 클라이언트 인스턴스를 만들고 애플리케이션 전체에서 재사용할 수 있다.
API Client 예시 코드(axios)
import Axios, { InternalAxiosRequestConfig } from 'axios';
import { useNotifications } from '@/components/ui/notifications';
import { env } from '@/config/env';
function authRequestInterceptor(config: InternalAxiosRequestConfig) {
if (config.headers) {
config.headers.Accept = 'application/json';
}
config.withCredentials = true;
return config;
}
export const api = Axios.create({
baseURL: env.API_URL,
});
api.interceptors.request.use(authRequestInterceptor);
api.interceptors.response.use(
(response) => {
return response.data;
},
(error) => {
const message = error.response?.data?.message || error.message;
useNotifications.getState().addNotification({
type: 'error',
title: 'Error',
message,
});
if (error.response?.status === 401) {
const searchParams = new URLSearchParams();
const redirectTo = searchParams.get('redirectTo');
window.location.href = `/auth/login?redirectTo=${redirectTo}`;
}
return Promise.reject(error);
},
);
요청 선언 정의 및 내보내기
API 요청을 즉석에서 선언하는 대신, 별도로 정의하고 내보내는 것이 좋다. 구조화된 방식으로 API 요청을 선언하면 코드베이스를 깨끗하고 조직적으로 유지할 수 있다.
모든 API 요청 선언은 다음을 포함해야 한다.
- 요청 및 응답 데이터에 대한 타입과 검증 스키마
- API 클라이언트 인스턴스를 사용하여 엔드포인트를 호출하는 fetcher 함수
- fetcher 함수를 사용하는 훅. 이는 react-query, swr, apollo-client, urql 등과 같은 라이브러리를 기반으로 데이터 페칭 및 캐싱 로직을 관리한다.
이 접근 방식은 애플리케이션에서 사용 가능한 정의된 엔드포인트를 추적하기 쉽게 만든다. 또한, 응답을 타입화하고 애플리케이션 전체에서 이를 추론하면 애플리케이션의 타입 안전성이 향상된다.
예시 코드(react-query)
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { z } from 'zod';
import { api } from '@/lib/api-client';
import { MutationConfig } from '@/lib/react-query';
import { Discussion } from '@/types/api';
import { getDiscussionsQueryOptions } from './get-discussions';
export const createDiscussionInputSchema = z.object({
title: z.string().min(1, 'Required'),
body: z.string().min(1, 'Required'),
});
export type CreateDiscussionInput = z.infer<typeof createDiscussionInputSchema>;
export const createDiscussion = ({
data,
}: {
data: CreateDiscussionInput;
}): Promise<Discussion> => {
return api.post(`/discussions`, data);
};
type UseCreateDiscussionOptions = {
mutationConfig?: MutationConfig<typeof createDiscussion>;
};
export const useCreateDiscussion = ({
mutationConfig,
}: UseCreateDiscussionOptions = {}) => {
const queryClient = useQueryClient();
const { onSuccess, ...restConfig } = mutationConfig || {};
return useMutation({
onSuccess: (...args) => {
queryClient.invalidateQueries({
queryKey: getDiscussionsQueryOptions().queryKey,
});
onSuccess?.(...args);
},
...restConfig,
mutationFn: createDiscussion,
});
};