[React+Typescript+Axios] Custom Hook 만들기

Ma.Kalongeeee·2024년 4월 4일

밀린 블로그를 쓰는건 매우 귀찮은 일이다

이번 사이드 프로젝트에서 가장 애를 먹었던 부분에... 대해서 작성을 해보고자 한다.

이번 사이드 프로젝트는 FE와 BE가 분리되어있고 Front에서 API를 호출하는 방식으로 진행이 되었다.

API를 호출하는데 기본적으로 axios 모듈을 사용하여 하게 되는데
아무래도 이게 typescript를 쓰는 프로젝트이다 보니, 조금 API호출하는 방식에서 typescript의 강력한 장점을 이용할 방법이 없을까 고민이 되었다.

원하는 바는 아래와 같았다.

  1. request에 넣어줘야 하는 값들의 type을 지정하고 싶다 (Non-exception Failures)
  2. response도 그에 맞게 type으로 지정하여, response type의 속성을 제안받아서 번거롭게 API를 매번 확인해야하는 일을 줄이고 싶다. (Types for Tooling)
  3. API의 request와 response를 일괄적으로 관리하고싶다.
  4. hook으로 개발하여 불필요한 코드를 줄이고 관리를 유용하게 하고싶다.

여러가지 블로그와 문서들을 참고하여
(가장 도움이 된건 이전 프로젝트와 함께한 동료님들의 조언 ^^)
아래와 같이 코드를 작성했다.

// ReactAPIType
// slugType: API URL path 중간에 들어가는 값 (object)
// ex) localhost:8080/api/{slug}
// paramsType : request parameter type (object)
// dataType: request body에 들어가는 data (object,axios request config)
// responseData: Response의 Type 
//(object | boolean | string... 이거는 뭐 응답 format에 따라서 적절히..)
//MethodType: 'GET'|'POST'|'DELETE'|'PUT' ...
export type RestAPIType<
  T extends {
    slug?: SlugType;
    params?: ParamsType;
    data?: DataType;
    response: ResponseDataType;
  }
> = {
  url: string;
  method: MethodType;
  headers?: AxiosRequestConfig<any>['headers'];
};
// use-axios
// useAxios는 두가지 인자를 받는다.
const useAxios = <S extends SlugType, 
  				  P extends ParamsType, 
                  D extends DataType, 
                  R extends ResponseDataType>(
  api: RestAPIType<{
    slug: S;
    params: P;
    data: D;
    response: R;
  }>,
  payload: PayloadType<S, P, D>) => {
  
  const { method, url, headers } = api;
  const { slug, params, data } = payload as any;

  let slugedUrl = url;
  if (slug !== undefined) {
    const keys = Object.keys(slug);
    for (const key of keys) {
      slugedUrl = url.replace(`:${key}`, slug[key]);
    }
  }  

  const response = async () => {
    const realUrl = `http://localhost:8080/api${`/${slugedUrl}`.replaceAll('//', '/')}`;
    const request = await axios({
      url: realUrl,
      method,
      headers,
      params,
      ...{data}
      })
      .then((response: AxiosResponse<ResponseType>) => {
        return response.data as R;
      })

      return request;
  }

  return response;
}
export default useAxios;

대부분의 axios를 이용한 custom hook을 만든 블로그 글에서는 useEffect에 저 response를 넣어놓고 useAxios를 선언하면 바로 사용될 수 있게끔 하던데,

그렇게 하면 버튼을 눌러서 API를 호출해야 할 때가 좀 번거로워진다.
(보통은 이런 경우가 많긴하지.)

처음에는 useAxios에 인자를 하나 더 추가해서 (boolean type)
true/false 일 경우로 나누어서 해볼까 했는데, 생각만큼 잘 안됐다.

(API 호출후 다시 상태를 바꿔줘야 쟤가 재사용이 가능해질텐데,
그러면 전역변수로 관리를 해야했을까? 이건 고민해볼 부분)

아무튼, response에 async, request부에 await을 적용하니,
useAxios가 Promise로 사용이 가능해진다.

const testAPI = useAxios(...);
// 버튼을 눌렀을때
const handleClickBtn = () => {
	testAPI().then((res) => {
    	... // 이때 위의 res는 내가 지정해준 ResponseDataType으로 반환된다.
    })
}

[질문] 그럼 useAxios에 인자는 어떻게 들어가나요?

일단 API에서 사용할 타입들을 공통적으로 관리할 폴더를 하나 만들어주자.
나는 io 라고 명명했다. (input/output...zz)

지금 BE는 내가 개발하는 것이 아니고 네이밍룰이 RESTful하지 못해서ㅋㅋ;
폴더 구분을 하기가 좀 애매한 상황이긴한데,
어쨋든, 역할에 맞는 하위 폴더를 생성해준다.

예를 들어서 user관련 API가 있다면

ㄴ io
	ㄴ user
    	ㄴ index.tsx
        ㄴ post-user-login.tsx

이런식으로 해주면 좀더 역할과 의미가 명확하게 보인다.
post 방식을 사용한 user login API라는 뜻이다.

import { RestAPIType } from "@hooks/use-axios/types";

// Get방식에서 사용할 params(query string)
export type GetUserParams = {
  name: string;
}

//Response의 Type
export type GetUserResponse = {
  name: string;
  age: number;
  likes: number;
  bookmarks: boolean;
}

//API 정보 url과 method를 적어준다.
export const GetUserAPI: RestAPIType<{
  params: GetUserParams,
  response: GetUserResponse
}> = {
  url: '/get/user',
  method: 'GET'
}

그리고 해당 파일이 있는 동일 위치에 있는 index.tsx에
namespace를 사용하여 작성해준다.
(역할을 구분하여 응집도를 높이기 위함)

namespace UserAPI {
  export const Get = GetUserAPI;
} 

export default UserAPI;

이렇게 해 준후 API를 사용하고자 하는 page로 돌아가서

const userInfo = useAxios(UserAPI.Get, {
	params: {
    	name: 'makarongeee'
    }
})


위와같이 request에 대한 정보를 입력할 수 있도록 자동으로 뜨고

내가 설정해준 속성이 자동으로 제안이 된다.

당연히 지정해준 타입과 일치하지 않으면 에러도 뜬다!

API 호출 후 응답값에 대해서도 당연히 제안이 된다.

아쉬운 점
1. request type에 params 속성만 있다면 그것만 제안하게 하고 싶은데,
hook에서 좀 더 세밀하게 잡아줄 수 있도록 수정이 필요한것 같다.
2. response를 error catch 를 해 줄 수 있도록 해야한다.
3. user의 access/refresh token 관련한 로직을 넣어야 하는데 아직 못함;ㅎ

얼레벌레 만든 Custom Hook이지만 나름 잘 작동하고 있어서 뿌듯하다 ㅎㅎ
Typescript는 참 재밌으면서도 어려운 것 같다...

profile
고양이 집사 / INTP / 프론트엔드 개발자 / 기록 용..?

0개의 댓글