2023년 01월 05일
4

API 다루기

Frontend
KKingmo

Changmo Oh

@KKingmo

전체 글 보기

단일 인스턴스의 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,
  });
};