안녕하세요 웹 프론트엔드 개발자 설탕시럽입니다.
프로젝트 내 axios를 활용한 중복 코드 제거를 위해 Axios Custom Hook을 만든 경험을 공유하고자 합니다.
axios에서 중복되는 설정과 로직을 React의 Hook을 통해 묶어서 관리하고, 앞으로 재사용할 때에도 편의성을 주기 위해 이 과정을 거쳤습니다.
이번 글은 Writing a custom Axios hook in TypeScript for API calls in your React application 를 많이 참고해서, 참고한 김에 번역을 했습니다. 내부 코드의 일부만 프로젝트와 저에게 맞게 수정했고 텍스트는 모두 번역/의역 한 내용임을 미리 밝힙니다.
개요
Axios는 HTTP client에서 브라우저와 Node.js에서 API 요청을 위해 많이 사용됩니다. ( npm에서 매주 17M 이상 다운로드 되고 있다 ). 이번 React 프로젝트에서 API호출을 위해 Axios의 cutom hook을 TypeScript로 어떻게 작성하는지 알아보려고 합니다. Custom hook은 재사용률이 높고 컴포넌트가 더 가벼워지기에, Axios를 프로젝트에 사용한다면 Custom hook으로 정의하는 건 확실히 효과가 있을 것입니다..
Hook의 구조
기본적으로 hook은 아래와 같은 구조를 따릅니다.
- state
- API와 상호작용 하기 위한 함수
- return 값
첫 번째로, custom hook에서는 3개의 상태, response, error 그리고 loading을 갖게 될 것입니다.
const [response, setResponse] = useState<AxiosResponse>();
const [error, setError] = useState<AxiosError>();
const [loading, setLoading] = useState(true);
이제, API와 상호작용할 함수를 작성해 봅시다. 함수는 AxiosConfig 객체를 파라미터로 받아야 하며, HTTP 요청에 따른 response 또는 error를 성공 또는 실패에 따라 반환해야 합니다. ( AxiosConfig의 타입은 axios의 RawAxiosRequestConfig로 지정해야 합니다.! )
const fetchData = async (params: RawAxiosRequestConfig) => {
await axios.request(params)
.then(response => {
setResponse(response);
})
.catch(error => {
setError(error);
})
.finally(() => {
setLoading(false);
});
};
Component에서 hook을 사용할 때 함수가 필요이상으로 호출되지 않고 로드될 때 한번 호출되기를 원합니다. 그래서 useEffect를 사용합니다.
useEffect(() => {
fetchData(axiosParams);
},[]);
마지막으로 hook은 AxiosConfig 객체를 매개변수로 받아들이고 response, error, loading 3가지 상태를 반환합니다. 완성된 hook은 다음과 같습니다.
import { useState, useEffect } from 'react';
import axios, { AxiosError, RawAxiosRequestConfig, AxiosResponse } from 'axios';
axios.defaults.baseURL = '';
const useAxios = (axiosParams: RawAxiosRequestConfig) => {
const [response, setResponse] = useState<AxiosResponse>();
const [error, setError] = useState<AxiosError>();
const [loading, setLoading] = useState(true);
const fetchData = async (params: AxiosRequestConfig) => {
await axios.request(params)
.then(response => {
setResponse(response);
})
.catch(error => {
setError(error);
})
.finally(() => {
setLoading(false);
});
};
useEffect(() => {
fetchData(axiosParams);
},[]);
return { response, error, loading };
}
custom hook을 component 내부에 사용하기
이제 hook은 React Component 내부에서 사용될 준비가 되었습니다. 이제 직접 사용해 봅시다! 컴포넌트 내에서 hook을 사용해서 한 개의 post를 받는 예제 코드입니다.
function App() {
const { response, loading, error, sendData } = useAxios({
method: "GET",
url: `/posts/1`,
headers: {
accept: '*/*'
}
});
return (
<div className="App">
<h1 className="page-title">Posts</h1>
{loading && (
<p>Loading...</p>
)}
{error && (
<p>{error.message}</p>
)}
{!loading && !error && (
<article className="post">
<h3 className="post-title">{response?.data.title}</h3>
<p className="post-body">
{response?.data.body}
</p>
</article>
)}
</div>
);
}
이제 custom hook은 계획 내에서 완벽하게 동작합니다. 이제 hook을 어떻게 더 개선하고 범용성 있는 코드가 될 수 있을지 살펴봅시다.
hook 개선하기
이제 useAxios hook은 component가 로딩되는 동안 API를 요청할 수 있게 잘 동작합니다. 그러나 몇몇 버튼이나 POST/PATCH와 같은 클릭을 통해서 요청할 때에는 어떻게 대처해야 할까요? 이러한 상황에서도 hook이 잘 동작할 수 있게끔 개선할 필요가 있습니다.
우선 일부 데이터를 가져오거나, GET 요청을 날리는 상황을 가정할 때, 항상 component가 로딩될 때 가져오기를 원합니다. 현 코드는 그 상황에 잘 맞게 동작합니다만, 사용 사례에 맞게 코드를 수정할 수 있습니다. request 매개변수의 method를 확인해서 hook으로 전달하기 위해, 두 가지를 수정해야 합니다.
- loading 상태, The loading state
const [loading, setLoading] = useState(axiosParams.method === "GET" || axiosParams.method === "get");
- useEffect (함수를 호출할 때, where we’re calling the function)
useEffect(() => {
if(axiosParams.method === "GET" || axiosParams.method === "get"){
fetchData(axiosParams);
}
},[]);
이제 다른 method에도 수동으로 axios 동작을 하게 추가해야 합니다. Component에서 수동으로 액세스 할 수 있도록 useAxios의 return 문에 해당 함수를 추가해야 합니다.
const sendData = () => {
fetchData(axiosParams);
}
return { response, error, loading, sendData };
/* replace the previous return statement with this */
개선된 hook이 준비가 되었습니다. component 내부에서 어떻게 동작하는지 아래 코드를 통해 살펴봅시다. 이번 예제에서는 더 동적으로 만들고, 매 클릭마다 "다음 게시글"이 표시되도록 했습니다.
function App() {
const [postId, setPostId] = useState(1);
const { response, loading, error, sendData } = useAxios({
method: "get",
url: `/posts/${postId}`,
headers: {
accept: '*/*'
}
});
const getNextPost = () => {
setPostId(postId + 1);
sendData();
}
return (
<div className="App">
<h1 className="page-title">Posts</h1>
{loading && (
<p>Loading...</p>
)}
{error && (
<p>{error.message}</p>
)}
{!loading && !error && (
<article className="post">
<h3 className="post-title">{response?.data.title}</h3>
<p className="post-body">
{response?.data.body}
</p>
</article>
)}
<button onClick={() => getNextPost()}>
Next Article Please!
</button>
</div>
);
}
이제 매 ‘Next Article Please’ 버튼을 누를 때마다 새로운 게시글을 가져오는 걸 볼 수 있고, 그 말은 버튼을 클릭할 때마다 API 요청을 동적으로 보낸다는 것을 의미합니다!
이걸 더 커스텀할 수도 있고 본인의 프로젝트에 더 맞게끔 만들 수도 있습니다! hook을 작성하기 위해서 처음 참고한 자료는 Link 입니다.
Custom Hook Code
코드만 필요한 분들을 위해 완성된 custom hook 코드를 아래에 남깁니다
import { useState, useEffect } from 'react';
import axios, { AxiosError, RawAxiosRequestConfig, AxiosResponse } from 'axios';
axios.defaults.baseURL = '';
const useAxios = (axiosParams: RawAxiosRequestConfig) => {
const [response, setResponse] = useState<AxiosResponse>();
const [error, setError] = useState<AxiosError>();
const [loading, setLoading] = useState(true);
const fetchData = async (params: AxiosRequestConfig) => {
await axios.request(params)
.then(response => {
setResponse(response);
})
.catch(error => {
setError(error);
})
.finally(() => {
setLoading(false);
});
};
const sendData = () => {
fetchData(axiosParams);
}
useEffect(() => {
fetchData(axiosParams);
},[]);
return { response, error, loading };
}
export default useAxios;
hook을 사용하는 Component 예제 코드는 바로 위 주제 있습니다.
마무리
원문 블로그의 코드는 에러가 발생하는 부분이 있어서 수정을 거친 제 코드를 작성했고, 제 프로젝트에서는 sendData에서 data를 받아 data를 전송하는 방식으로 처리했습니다.
이해를 위해 의역을 많이 가미했으므로 코드 또는 글에 문제가 있거나 의견이 있다면 댓글을 통해 언제든 남겨주시면 감사하겠습니다. 마지막으로 해당 글의 원문 링크를 다시 한번 남기면서 포스팅 마치도록 하겠습니다.
Reference (참고자료, 원문)
https://blog.sreejit.dev/custom-axios-hook-useaxios-in-typescript-react
'React' 카테고리의 다른 글
useState 딥다이브 (0) | 2023.06.04 |
---|---|
JSX (0) | 2023.05.17 |
React의 특징과 장점, 적용 예제 까지! (0) | 2023.05.16 |
react-hook-form과 yup 라이브러리 적용 하기 (0) | 2023.02.12 |
react에서 websocket 통신하기 (0) | 2023.01.29 |