프로젝트에서 로직 분리한 내용을 빠르게 정리한 것 (실제 코드와는 조금 다름)
커스텀 훅이 꼭 필요한가?
그럼 왜 커스텀 훅을 만드는가?
커스텀 훅을 만들 때는 공통으로 사용되는 것과 경우에 따라 달라질 수 있는 것이 무엇인지 고려해야 한다.
REST API 통신을 생각해보자. 클라이언트 측은 서버에 다양한 request를 보낼 수 있다. 예를 들어, 상품 전체 데이터를 fetch해 온다거나, 사용자가 입력한 form을 서버로 보낸다거나, 특정 아이템을 삭제하는 요청을 보낼 수도 있다.
공통으로 필요한 정보
경우에 따라 달라지는 정보
커스텀 훅을 사용하면 해당 훅이 실행되는 컴포넌트 각각에 개별적인 상태를 가지게 되고 이를 독립적으로 관리할 수 있게 된다. 따라서 모든 HTTP 요청에 사용되는 정보는 커스텀 훅 useAxios
내부에 state로 저장하자. 그리고 HTTP 요청에 따라 달라지는 정보는 파라미터로 받아 제네릭하게 사용할 수 있게 하자.
간단히 데이터를 fetch 해오는 예시를 들어 로직을 분리하지 않는 경우와 분리하는 경우를 비교해보자.
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 해올 수 있다.
위 코드에서의 받아온 데이터, 로딩 상태, 에러 상태를 커스텀 훅에서 관리하도록 하고, 별도의 파일로 요청마다 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;
requestFn
과 params
를 받아 제네릭한 커스텀 훅을 완성했다.
그러면 이제 axios instance를 만들고, requestFn
을 정의해보자.
// instance 정의
const instance = axios.create({
baseURL: process.env.API_BASE_URL,
headers: {
Authorization: `Bearer ${process.env.TOKEN}`,
},
});
// HTTP 요청 정의
const api = {
getProducts: () => instance.get(''),
getKeywordProducts: (query: string) => instance.get('', { params: { keyword: query } }),
getProduct: (id: string) => instance.get(id),
// 그 외 다양한 HTTP 요청들 정의
}
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;
기존 컴포넌트 내부에 로직을 위치시킬 때보다 훨씬 간단하게 코드를 작성할 수 있다.