[React] TypeScript로 재사용 가능한 useAxios 훅 만들기

@eunjios·2023년 10월 31일
0
post-thumbnail

프로젝트에서 로직 분리한 내용을 빠르게 정리한 것 (실제 코드와는 조금 다름)

Why?

커스텀 훅이 꼭 필요한가?

  • 그렇지 않다.
  • 커스텀 훅이 없어도 동일한 기능을 구현할 수 있기 때문에 필수는 아니다.

그럼 왜 커스텀 훅을 만드는가?

  • 커스텀 훅을 사용하면 반복되는 코드를 줄일 수 있다. 즉, 재사용이 가능하다.
  • 비슷한 기능을 개발할 때 커스텀 훅을 사용하면 훨씬 편하다.

How to

훅 설계하기

커스텀 훅을 만들 때는 공통으로 사용되는 것과 경우에 따라 달라질 수 있는 것이 무엇인지 고려해야 한다.

REST API 통신을 생각해보자. 클라이언트 측은 서버에 다양한 request를 보낼 수 있다. 예를 들어, 상품 전체 데이터를 fetch해 온다거나, 사용자가 입력한 form을 서버로 보낸다거나, 특정 아이템을 삭제하는 요청을 보낼 수도 있다.


공통으로 필요한 정보

  • 서버로부터 가져온 데이터
  • 에러 발생 여부
  • 로딩 중인지 (UI 측면에서 필요)

경우에 따라 달라지는 정보

  • 엔드포인트
  • HTTP 메서드 (GET, POST, ...)
  • 서버로 보낼 데이터
  • 그 외 axios request config

커스텀 훅을 사용하면 해당 훅이 실행되는 컴포넌트 각각에 개별적인 상태를 가지게 되고 이를 독립적으로 관리할 수 있게 된다. 따라서 모든 HTTP 요청에 사용되는 정보는 커스텀 훅 useAxios 내부에 state로 저장하자. 그리고 HTTP 요청에 따라 달라지는 정보는 파라미터로 받아 제네릭하게 사용할 수 있게 하자.


로직 분리하기

간단히 데이터를 fetch 해오는 예시를 들어 로직을 분리하지 않는 경우와 분리하는 경우를 비교해보자.

(1) 컴포넌트에 로직을 두는 경우

const Products: React.FC = () => {
  const [products, setProducts] = useState([]);
  const [isLoading, setIsLoading] = useState(true);
  const [error, setError] = useState<AxiosError>();
  
  useEffect(() => {
    const fetchData = async () => {
      setIsLoading(true);
      setError(undefined);
      try {
        const response = await axios.get(API_URL);
        setProducts(response?.data.products);
      } catch (error) {
        setError(error as AxiosError);
      }
      setIsLoading(false);
    };
    fetchData();
  }, []);
  
  return (
    <>
      {isLoading && <div>Loading...</div>}
      {error && <div>Error...</div>}
      {!isLoading && !error && products && (
        <ProductList products={products} />
      )}
    </>
  );
};

useEffect를 사용하여 Products 라는 컴포넌트가 마운트 될 때 데이터를 fetch 해올 수 있다.


(2) 컴포넌트와 로직을 분리하는 경우

위 코드에서의 받아온 데이터, 로딩 상태, 에러 상태커스텀 훅에서 관리하도록 하고, 별도의 파일로 요청마다 API_URL과 메서드, 데이터를 정의하여 로직을 분리해보자.

위 코드에서 state로 관리한 데이터는 products 였다. 하지만 이는 엔드포인트마다 달라질 수 있으니, 모든 HTTP 요청에서 공통의 response 를 state로 관리하자.


hooks/useAxios.ts 🤔❓

const useAxios = () => {
  const [response, setResponse] = useState<AxiosResponse>();
  const [error, setError] = useState<AxiosError>();
  const [isLoading, setIsLoading] = useState(false);
  
  const fetchData = async () => {
    setResponse(undefined);
    setError(undefined);
    setIsLoading(true);
    try {
      const response = await axios.get(API_URL);
      setResponse(response);
    } catch (error) {
      setError(error as AxiosError);
    }
    setIsLoading(false);
  };
  
  const sendRequest = async () => {
    fetchData();
  };

  return { response, error, isLoading, sendRequest };
};

위 코드에서 response error isLoading 을 state로 관리하고, sendRequest 를 통해 API 통신을 할 때마다 각 state를 업데이트 할 수 있게 되었다. 이렇게 커스텀 훅을 만들었다!

끝 🤔❓

const response = await axios.get(API_URL);

당연하게도 위처럼 코드를 작성하게 되면 재사용이 불가능하다. axios.get(API_URL) 로 API 통신을 한정해버리기 때문이다. 따라서 우리는 requestFn 과 axios 통신에 필요한 파라미터를 받아 제네릭하게 커스텀 훅을 구성해야 한다.


hooks/useAxios.ts

import { useState } from 'react';
import { AxiosError, AxiosResponse } from 'axios';

type RequestFn<T> = (params: T) => Promise<AxiosResponse>;

const useAxios = <T>(requestFn: RequestFn<T>, params: T) => {
  const [response, setResponse] = useState<AxiosResponse>();
  const [error, setError] = useState<AxiosError>();
  const [isLoading, setIsLoading] = useState(false);

  const fetchData = async () => {
    setResponse(undefined);
    setError(undefined);
    setIsLoading(true);
    try {
      const response = await requestFn(params);
      setResponse(response);
    } catch (error) {
      setError(error as AxiosError);
    }
    setIsLoading(false);
  };

  const sendRequest = async () => {
    fetchData();
  };

  return { response, error, isLoading, sendRequest };
};

export default useAxios;

requestFnparams 를 받아 제네릭한 커스텀 훅을 완성했다.


그러면 이제 axios instance를 만들고, requestFn 을 정의해보자.

// instance 정의
const instance = axios.create({
  baseURL: process.env.API_BASE_URL,
  headers: {
    Authorization: `Bearer ${process.env.TOKEN}`,
  },
});
  • baseURL : API 주소의 Base URL을 설정 (ex. https://api.example.com/)
  • headers : 해당 API에 인증이 필요한 경우 헤더에 Authorization 지정

// HTTP 요청 정의
const api = {
  getProducts: () => instance.get(''),
  getKeywordProducts: (query: string) => instance.get('', { params: { keyword: query } }),
  getProduct: (id: string) => instance.get(id),
  // 그 외 다양한 HTTP 요청들 정의 
}
  • gerProducts: query 를 params (keyword) 로 지정하여 products 데이터를 가져옴
  • getProduct: id를 받아 해당 API 주소의 데이터를 가져옴

api/axios.ts

import axios from 'axios';

const instance = axios.create({
  baseURL: process.env.API_BASE_URL,
  headers: {
    Authorization: `Bearer ${process.env.TOKEN}`,
  },
});

const api = {
  getProducts: () => instance.get(''),
  getKeywordProducts: (query: string) => instance.get('', { params: { keyword: query } }),
  getProduct: (id: string) => {
    if (!id) {
      throw new Error('invalid id');
    }
    return instance.get(id);
  },
};

export default api;

api 를 export 하여 외부 컴포넌트에서 api.getProduct('12345') 와 같이 사용할 수 있다.


컴포넌트에서 사용하기

components/Products.tsx

import useAxios from '../hooks/useAxios';
import api from '../api/axios';
import ProductList from './ProductList';

const Products: React.FC = () => {
  const {
    response,
    isLoading,
    error,
    sendRequest: fetchProducts,
  } = useAxios(api.getProducts);
  
  useEffect(() => {
    fetchData();
  }, []);
  
  return (
    <>
      {isLoading && <div>Loading...</div>}
      {error && <div>Error...</div>}
      {!isLoading && !error && response?.data.products && (
        <ProductList products={response?.data.products} />
      )}
    </>
  );
};

export default Products;

기존 컴포넌트 내부에 로직을 위치시킬 때보다 훨씬 간단하게 코드를 작성할 수 있다.


References

profile
growth

0개의 댓글