fetch 에 들어갈 여러 설정값들을 매번 정의하는 것은 상당히 번거로운 일이다. fetch를 미리 함수로 정의해두어 좀 더 편리하게 사용할 수 있도록 커스텀훅으로 만들어보려고 한다.
커스텀훅은 중복이 되는 로직을 재사용이 가능하도록, 마치 useState, useEffect와 같이 편하게 사용할 수 있게 자신만의 훅을 만드는 것을 말한다.
커스텀훅을 만드는 방법은 간단하다.
useSate
, useOnlineStatus
)여기서 든 의문은, 도대체 일반 util 함수를 작성하는 것과 무슨 차이가 있을까? 이다. 이 차이는 내부에서 다른 훅을 부를 수 있느냐, 없느냐의 차이에 있다. 즉 위에서 커스텀훅을 만드는 규칙을 지켜야 다른 훅을 호출할 수 있는 것이다. 내부에서 훅을 쓰지 않는 경우라면 커스텀훅으로 작성하는 것을 지양해야 한다. React에서 강제되는 사항은 아니지만, 위 원칙을 지켜서 코드를 작성하도록 하자.
// 🔴 Avoid: A Hook that doesn't use Hooks
function useSorted(items) {
return items.slice().sort();
}
// ✅ Good: A regular function that doesn't use Hooks
function getSorted(items) {
return items.slice().sort();
}
// ✅ Good: A Hook that uses other Hooks
function useAuth() {
return useContext(Auth);
}
대표적으로 사용할 수 있는 케이스는 input이다. 다음과 같이 useFormInput을 만들어 사용할 수 있다. 아래와 같이 커스텀훅을 작성해 View에 들어가는 로직을 최소화할 수 있다.
import { useState } from 'react';
export function useFormInput(initialValue) {
const [value, setValue] = useState(initialValue);
function handleChange(e) {
setValue(e.target.value);
}
const inputProps = {
value: value,
onChange: handleChange
};
return inputProps;
}
...
function Form() {
const firstNameProps = useFormInput('Mary');
const lastNameProps = useFormInput('Poppins');
// ...
여기에서 주의할 것은 커스텀 훅은 로직은 공유할 수 있지만 상태 그 자체는 공유할 수 없다. 같은 훅을 호출하더라도 각각의 훅들은 완전히 독립적인 상태를 갖게 된다.
그렇다면 오늘의 목표인 fetch 를 위한 커스텀훅 작성해보자. 잠깐, 그전에 fetch가 커스텀훅이 되어야 하는 이유는 무엇일까? 위에서 살펴보았듯 내부에서 훅을 사용하지 않다면 굳이 커스텀훅이 될 필요는 없다. 생각을 해보면, 버튼을 클릭하면 post 요청이 된다던지 하는 경우에는 useEffect와 같은 훅이 포함될 필요가 없다.
그래서 나는 우선 customFetch 이라는 함수를 작성한 뒤, useEffect를 포함한 get 요청이 일어나는 경우에만 사용하는 useData를 작성하기로 했다.
export const customFetch = async <B>({
path,
queries,
method,
body,
auth = false,
options,
}: customFetchProps<B>) => {
let url = HOST + path;
if (queries) {
const copiedQueries = removeEmptyKeyValues(queries);
const queryString = copiedQueries
? '?' + new URLSearchParams(Object.entries(copiedQueries)).toString()
: '';
url += queryString;
}
const init = {
method,
body: JSON.stringify(body),
};
const headers = {};
if (typeof body === 'object') {
headers['Content-Type'] = 'application/json';
}
if (options) {
Object.assign(headers, options);
}
if (auth) {
const accessToken = getAccessToken();
if (!accessToken) {
return; // TODO: 로그아웃
}
headers['Authorization'] = localStorage.getItem(ACCESS_TOKEN);
}
if (headers) {
Object.assign(init, { headers });
}
try {
const res = await fetch(url, init);
if (res.ok) {
const resJSON = await res.json();
if (resJSON.data === undefined) {
return { error: new Error('응답에 데이터가 존재하지 않습니다.') };
}
const data = resJSON.data;
return data;
}
} catch (error) {
return { error: new Error(`Error: ${error}`) };
}
};
// 위 customFetch를 가져와 useData를 만들기
export const useData = <B>(fetchProps: FetchProps<B>) => {
const [data, setData] = useState(null);
useEffect(() => {
async () => {
try {
const data = await customFetch(fetchProps);
if (data.error) {
console.error(data.error);
return;
}
setData(data);
} catch (error) {
if (error instanceof Error) {
console.error(error.message);
}
}
};
}, []);
return data;
};