외부 리소스와의 상호 작용 중
외부 API 호출할 때는 useEffect
훅을 사용한다.
이는 컴포넌트가 렌더링 된 후에 외부 데이터를 가져와 컴포넌트의 상태를 업데이트 되도록 해준다.
다만, 여러 컴포넌트에서 동일한 데이터 패치 로직
을 사용할 경우 코드 중복이 발생할 수 있다. 이러한 중복을 줄이고 코드의 재사용성과 유지보수성을 높이기 위해 공통로직
을 커스텀 훅
으로 분리해서 사용할 수 있다.
커스텀 훅은 React의 기본 훅을 활용하여 사용자가 만든 재사용 가능한 함수다.
기존 프로젝트에서 반복되는 로직을 따로 함수로 만들어서 사용하던 방식에 React의 기본 훅이 추가되었다고 생각할 수 있다.
상태 자체
를 공유하는 것이 아닌 상태 로직
을 공유한다.결과
에만 의존하면 된다.즉, 데이터를 가져오는 방식은 커스텀 훅에서 고민하고 결과 값을 반환해준다.
컴포넌트에서는 가져온데이터를 어떻게 보여줄까
만 고민하면 된다.
이는 컴포넌트를 보다 더 선언적으로 만들어 준다.
useEffect
를 사용해 외부 데이터를 받아오고 있다.// Home
export const Home = () => {
const [loading, setLoading] = useState<boolean>(true);
const [characters, setCharacters] = useState<CharacterTypes[]>([]);
useEffect(() => {
let ignore = false;
const getCharacters = async () => {
const data = await fetchCharacters(50);
if (!ignore) {
setCharacters(data.results);
setLoading(false);
}
};
getCharacters();
return () => {
ignore = true;
};
}, []);
return (
// --중략
);
};
// Detail
export const Detail = () => {
const { id } = useParams();
const navigate = useNavigate();
const [loading, setLoading] = useState(true);
const [characterDetail, setCharacterDetail] =
useState<CharacterDetailTypes>();
useEffect(() => {
let ignore = false;
const getCharacterDetail = async () => {
const data = await fetchCharacterDetail(id);
if (!ignore) {
setCharacterDetail(data.results[0]);
setLoading(false);
}
};
getCharacterDetail();
return () => {
ignore = true;
};
}, [id]);
return (
<>
// -- 중략
</>
);
};
API 호출 함수
와 해당 함수에 전달될 인수
를 받아야 한다.에러 처리
를 고려해야 한다.type Status = 'initial' | 'pending' | 'fulfilled' | 'rejected';
interface UseFetch<T> {
data?: T;
status: Status;
error?: Error;
}
Status
는 요청의 상태를 나타내며, 초기 상태(initial
), 로딩 중(pending
), 요청 성공(fulfilled
), 요청 실패(rejected
) 중 하나의 값을 갖는다.UseFetch<T>
는 useFetch
훅이 반환하는 객체의 타입이다. 여기서 T
는 fetch 함수의 반환 타입을 나타낸다.fetchFunction
: 데이터를 가져오는 함수(API 호출 함수
)다. 이 함수는 Promise
를 반환해야 한다....args
: fetchFunction
에 전달될 인수들이다.UseFetch<T>
타입의 객체를 반환한다.const [state, setState] = useState<UseFetch<T>>({
status: 'initial',
data: undefined,
error: undefined
});
status
가 initial
이며, data
와 error
는 정의되지 않은 상태다.useEffect(() => {
let ignore = false;
ignore
변수는 컴포넌트가 언마운트된 후에 상태를 변경하려는 시도를 방지한다.const fetchData = async () => {
setState({ ...state, status: 'pending' });
try {
const data = await fetchFunction(...args);
if (!ignore) {
setState({ status: 'fulfilled', data });
}
} catch (error) {
if (!ignore) {
setState({ status: 'rejected', error });
}
}
};
fetchData
를 선언한다.pending
으로 설정하여 로딩 중임을 나타낸다.fetchFunction
을 호출하여 데이터를 가져온다.fulfilled
로 설정하고 데이터를 저장한다.rejected
로 설정하고 오류를 저장한다.fetchData();
return () => {
ignore = true;
};
useEffect
내에서 fetchData
함수를 호출하여 데이터 로드를 시작한다.ignore
를 true
로 설정하여 비동기 작업이 일어나도 상태가 변경되는 것을 방지한다.return state;
data
, status
, error
가 포함된다.import { useState, useEffect } from 'react';
type Status = 'initial' | 'pending' | 'fulfilled' | 'rejected';
interface UseFetch<T> {
data?: T;
status: Status;
error?: Error;
}
export const useFetch = <T>(
// API 호출 함수 그리고 함께 전달될 인수.
fetchFunction: (...args: any[]) => Promise<T>,
...args: any[]
): UseFetch<T> => {
// useFetch에서 관리하는 상태 -> 최종 반환값이 된다.
const [state, setState] = useState<UseFetch<T>>({
status: 'initial',
data: undefined,
error: undefined,
});
// useEffect를 통해 데이터를 가져온다.
useEffect(() => {
// 컴포넌트가 언마운트 되면 비동기 작업으로 인한 상태 변경 방지.
let ignore = false;
const fetchData = async () => {
// 비동기 함수 fetchData()를 실행하면 "로딩중"이 된다.
setState({ ...state, status: 'pending' });
// 요청 API(fetchFunction)를 실행한다.
try {
// 요청 API 인수에 함께 전달받았던 "...args"를 준다.
const data = await fetchFunction(...args);
if (!ignore) {
// 성공하면 상태를 'fulfilled로 변경하고 데이터를 저장한다.
setState({ status: 'fulfilled', data });
}
} catch (error) {
if (!ignore) {
// 실패하면 상태를 'rejected'로 변경하고 오류를 저장한다.
setState({ status: 'rejected', error: error as Error });
}
}
};
fetchData();
return () => {
ignore = true;
};
}, [fetchFunction]);
return state;
};
// Home
export const Home = () => {
const { data: characters, status } = useFetch(fetchCharacters, 50);
const charactersList = characters?.results;
return (
// -- 중략
);
};
// Details
export const Detail = () => {
const { id } = useParams();
const navigate = useNavigate();
const { data: characterDetail, status } = useFetch(fetchCharacterDetail, id);
const detailsInfo = characterDetail?.results[0];
const handleBackPage = () => {
navigate(-1);
};
return (
<>
// -- 중략
</>
);
};
데이터를 받아오는 과정, 과정 중의 상태, 에러 여부는 컴포넌트가 고려하지 않아도 된다.
컴포넌트는 받아온 결과 값과 상태만을 고려해서 화면을 보여주면 된다.