리액트에서 커스텀 훅은 우리가 원하는 대로 로직을 정의해 훅을 만들고, 그 훅을 컴포넌트에서 재사용할 수 있는 기능이다. 예를 들어, 데이터를 fetch하는 기능을 가진 커스텀 훅을 만들어 보고, 이를 사용하는 방법을 살펴보자.
먼저, useFetch.js라는 파일을 만들어서 커스텀 훅을 작성해보자.
import { useState, useEffect } from "react";
const useFetch = (url) => {
const [data, setData] = useState(null)
useEffect(() => {
fetch(url)
.then(res => res.json())
.then(data => setData(data))
},[url])
return [data]
}
export default useFetch
이 훅은 url을 매개변수로 받아서, 해당 URL로 데이터를 fetch해온 다음, 그 데이터를 data라는 상태에 저장하는 로직이다. useEffect 훅을 사용해서 url이 변경될 때마다 데이터를 새로 가져오도록 하고 있다. 마지막으로, data를 배열 형태로 반환한다.
여기서 굳이 [data]형식으로 가져와야 하나 검색해봤는데 이렇게 data하나일때는 그냥 return data 라고 써도 된다. 근데 대부분 이렇게 api를 가져올때는 아래의 예시와 같이 로딩중인지, 에러가 있는지도 같이 가져오는게 일반적이다. 아래의 예시를 보자.
import { useState, useEffect } from "react";
const useFetch = (url) => {
const [data, setData] = useState(null);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
fetch(url)
.then(res => res.json())
.then(data => {
setData(data);
setIsLoading(false);
})
.catch(err => {
setError(err);
setIsLoading(false);
});
}, [url]);
return [data, isLoading, error];
}
이런식으로 로딩중인지, 에러가 있는지의 정보를 같이 보내주려면 여러개이므로 []안에 써야 한다.
이제 App.js에서 이 커스텀 훅을 사용하는 방법을 보자.
import useFetch from "./useFetch"
function App(){
const [data] = useFetch("https://jsonplaceholder.typicode.com/todos")
return (
<>
{data && data.map(item => {
return (
<p key={item.id}>{item.title}</p>
)
})}
</>
)
}
export default App
useFetch 훅을 임포트한 뒤, const [data]를 통해 반환된 데이터를 구조 분해 할당(destructuring)으로 받아온다. useFetch 함수에 원하는 url을 넣으면, 해당 URL로부터 데이터를 가져와 data에 저장하게 된다. 이후 이 data를 화면에 렌더링하기 위해 map을 사용해 각각의 항목을 출력해주면 된다.
처음에는 useFetch.js에서 App.js로 관련 로직을 보내는 걸로만 생각했는데 그게 아니라 App.js에서 useFetch 훅을 호출하면서 필요한 정보(여기서는 url)를 전달하는 거였다. 그렇게 필요한 정보를 전달하면 useFetch에서 그 정보와 함께 쓰여진 로직으로 요청을 처리한 뒤 다시 App.js에 반환하는 것이다.
만약 loading 과 error 정보까지 함께 받아왔다면, 우리가 이 정보를 받은 이유는 사용자에게 데이터를 로드 중임을 알리고, 데이터 요청이 실패했을 때 에러 메시지를 제공하기 위함이므로 아래와 같이 코드를 작성하면 된다.
function App() {
const [data, isLoading, error] = useFetch("https://jsonplaceholder.typicode.com/todos");
if (isLoading) return <p>Loading...</p>;
// 로딩 중이면 로딩 메시지를 바로 반환
if (error) return <p>Error: {error.message}</p>;
// 에러가 있으면 에러 메시지를 바로 반환
// 로딩이 끝났고, 에러도 없다면 데이터 렌더링
return (
<>
{data.map(item => (
<p key={item.id}>{item.title}</p>
))}
</>
);
}
return() 안에 같이 안써준 이유는 같이 써주게 되도 코드는 작동하지만 로딩 중이거나 에러가 있을 때도 data.map코드가 평가되기 때문에 비효율적이고 코드도 너무 많아서 복잡해보이기 때문이다.