TypeScript에서 Axios사용 시, 타입을 지정하는 방법

조민호·2023년 10월 27일
0

기본 개념

axios를 TypeScript와 함께 사용할 때는 제네릭을 사용하여 응답의 타입을 지정할 수 있다.

이를 통해 TypeScript의 타입 검사 기능을 활용하여 API 응답의 구조를 보장받을 수 있는 것이다.

예를 들어, API에서 User 타입의 데이터를 반환한다고 가정하고

interface User {
  id: number;
  name: string;
  email: string;
}

axios.get 요청을 할 때 제네릭을 사용하여 응답의 타입을

User로 지정할 수 있다

import axios from 'axios';

async function fetchUser(userId: number): Promise<User> {
  const response = await axios.get<User>(`https://api.example.com/users/${userId}`);
  return response.data;
}

axios.get는 응답의 데이터가 User 인터페이스와 일치하는지 확인 한다.

만약 서버에서 예상치 못한 형태의 데이터를 반환하면, TypeScript에서는 컴파일 타임 또는 개발 시에 에러를 뱉게 된다.








실 사용 예시


대부분의 경우 api요청 메소드를 곧바로 사용하지는 않고 커스텀 훅으로 묶어서 사용하는 경우가 많다.

나같은 경우 보통 아래의 방식으로 진행한다.

  • api요청 함수를 생성

  • 기능별로 분류한 객체의 메소드 형태로 묶음

  • 커스텀 훅에서 해당 메소드를 호출

이렇게 하나의 api를 호출하기 위한 깊이가 깊어지는 패턴의 경우

제네릭을 사용하고, 최상단에서 타입을 던져주면 커스텀 훅을

보다 범용적으로 사용할 수 있다.


실제 예시는 아래와 같다

api호출은 useQuery를 통해 진행한다


최상단 (api호출하는 곳)

// 최상단에서 타입을 선언후 제네릭으로 넘겨줌
useGetProblemDetail<GetProblemDetailType>(Number(problemId)); 
  • useGetProblemDetail은 리액트쿼리의 useQuery를 사용하는 커스텀 훅이다

  • useGetProblemDetail 내부의 useQuery에서 api 메소드를 호출하고 반환되는 프로미스 타입인 GetProblemDetailType타입을 제네릭으로 선언 해준다



useGetProblemDetail.ts

// 호출된 메소드에서 타입을 받아와야 하므로
// queryFn에서 사용할 제네릭 타입을 여기서 선언
export const useGetProblemDetail = <T, K = any>(
  problemId: number | null,
  options?: K,
): UseQueryResult<T> => {
  return useQuery<T>({
    queryKey: ['problemDetail', { problemId }],

    queryFn: async (): Promise<T> => {
      // getProblemDetail함수에도 GetProblemDetailType타입 전달
      const res = await problemApi.getProblemDetail<T>(problemId!);

      // GetProblemDetailType타입의 값을 반환해야 하니까
      // .data 를 리턴해야 함
      return res.data;
    },
    staleTime: Infinity,
    retry: 0,
    enabled: problemId !== null,
    ...options,
  });
};
  1. useQuery에 사용되는 options타입은 제네릭 K타입이 되는데

    useGetProblemDetail호출할때 딱히 선언을 하지 않았으므로

    any타입이 된다

  2. T에는 상위에서 제네릭으로 넘겨받은 GetProblemDetailType 타입이 적용된다

    • 제네릭으로 선언한 T타입은 get요청을 하는

      getProblemDetail 메소드의 제네릭으로 넘겨줌으로써

      GetProblemDetailType 타입을 또 한번 넘겨주는 것이다

    • queryFn의 반환 타입 역시 Promise타입이 된다



getProblemDetail.ts

// instance.ts

import axios, { AxiosError } from 'axios';

export const API = axios.create({});
import axios, { AxiosResponse } from 'axios';
import { API } from './instance';

**//  useGetProblemDetail 메소드에서 넘겨받은** 
**//  GetProblemDetailType타입이 T로 들어감**
export const problemApi = {
  getProblemDetail: async <T>(id: number): Promise<AxiosResponse<T>> => {
    return await API.get(`/api/problem/detail/${id}`);
  },
  ...
};

기능별 api메소드를 모아둔 객체 problemApi에 있는

getProblemDetail 메소드가 있다.

  • 실제 API를 호출하는 곳에서 useGetProblemDetail 메소드에서 넘겨받은 GetProblemDetailType타입이 제네릭 T에 들어간다
  • get메소드에 이 제네릭을 사용해서 GetProblemDetailType타입을 .get 메소드의 리턴 타입으로 사용한다
  • 여기서 axios 반환 객체를 바로 리턴하고 있으므로 AxiosResponse에 제네릭을 감싸서 반환한다.

결국은 axios.get 요청을 할 때 제네릭을 사용하여 API.get<T> 형태로

응답의 타입을 지정하는 것인데, 이렇게 되는 원인은

API.get에서 TAxiosResponse 객체 내의 data 필드의

타입을 나타내기 때문이다

interface AxiosResponse<T = any> {
  data: T;
  status: number;
  statusText: string;
  headers: any;
  config: AxiosRequestConfig;
  request?: any;
}



전체 과정을 간단히 작성하면 아래와 같다
useGetProblemDetail**<GetProblemDetailType>**(Number(problemId)); 

>> 

const useGetProblemDetail = <**T**, K = any>(problemId: number | null, options?: K) => {
  return useQuery**<T>**({ ... });
};

>> 

const problemApi = {
  getProblemDetail: async (id: number): **Promise<AxiosResponse<T>>** => {
    return await API.get(`/api/problem/detail/${id}`);
               **// 타입 전달 최종 목적지**
  },
};
  1. api를 호출하는 부분에 , API호출 후 반환 받는 타입을 넣어주고
  2. 이 타입을 해당 메소드를 호출하는 최상위에서 (왜냐하면 최상위에서 메소드를 호출하고 데이터를 반환받기 때문) 호출할 때 타입을 지정해주고
  3. 여기서부터 제네릭으로 타고타고 전달해줘서 API요청하는 부분까지 타입을 전달해주는 것이다
profile
웰시코기발바닥

0개의 댓글