예전에 쓴 포스팅에서 잠깐 언급한 적 있는데, 기존에는 Node로 백엔드를 만들어 그곳에서 토핑 데이터를 가져왔었다. 하지만 현재 백엔드를 공부하고 있는 것이 아니므로 Firebase를 사용하여 API를 연동하기로 했다.
이번 포스팅에서는 useEffect Hook과 async/await을 사용하여 API를 호출하는 방법을 살펴볼 것이다.
하지만 리액트에서 useEffect Hook을 사용하여 이렇게 수동으로 데이터를 가져오는 것보다 React Query, useSWR, and React Router 6.4+ 등을 쓰는 것이 추천된다고 한다.
https://react.dev/reference/react/useEffect#fetching-data-with-effects
처음에는 API를 호출하기 위해 다음 세가지를 사용했다.
이를 사용하여 작성한 코드는 아래와 같다.
function App() {
const [backendData, setBackendData] = useState([]);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
fetch('이곳에 Firebase에서 만든 API가 들어감').then(
response => {
return response.json()
}
).then(
data => {
setIsLoading(false);
return setBackendData(data);
})
}, [])
if (isLoading) {
return <div>Loading...</div>
} else {
return (
// ...패치된 데이터를 사용하는 컴포넌트들
)
}
}
fetch()를 사용하여 API를 호출했다.
Loading...
이 나타났다가 데이터를 받아온 후 정상적인 화면이 렌더링된다.useEffect Hook
isLoading이라는 상태
위에서 사용한 fetch()는 then().then().then()...과 같이 사용할 시 가독성이 좋지 않다는 단점 등이 있었다. 그래서 async/await를 사용하여 문제점을 개선하기로 했다.
이번에는 API를 호출하기 위해 다음 세가지를 사용했다.
이를 사용하여 작성한 코드는 아래와 같다.
function App() {
const [backendData, setBackendData] = useState([]);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
const fetchData = async () => {
const response = await fetch('이곳에 Firebase에서 만든 API가 들어감');
const data = await response.json();
setIsLoading(false);
setBackendData(data);
}
fetchData();
}, [])
if (isLoading) {
return <div>Loading...</div>
} else {
return (
// ...패치된 데이터를 사용하는 컴포넌트들
)
}
}
Loading...
이 나타났다가 데이터를 받아온 후 정상적인 화면이 렌더링된다.(fetch()를 사용했을 때와 동일한 과정이다)기존에는 useEffect Hook 안에 fetchData를 선언했는데, 당장의 문제는 아니지만 나중에 문제가 생길 여지가 있다.
예시를 들어보면 아래와 같다.
만약 fetchData를 여러번 호출해야 한다고 해보자(버튼을 누르면 fetchData 함수가 호출되는 방식) 이때 fetchData가 한번 호출되면 마지막에 setIsLoading(false)
을 했으므로 isLoading이 false가 된다. 따라서 버튼을 한번 누르면 isLoading이 true가 아니라 false가 되게 되어 fetchData를 처음 호출했을 때와 그 다음에 호출했을 때 동일한 기능이 아니게 된다.
function App() {
const [backendData, setBackendData] = useState([]);
const [isLoading, setIsLoading] = useState(true);
const fetchData = async () => {
const response = await fetch('이곳에 Firebase에서 만든 API가 들어감');
const data = await response.json();
setIsLoading(false);
setBackendData(data);
}
useEffect(() => {
fetchData();
}, [])
return (
<button onClick={fetchData}>reload</button>
)
따라서 아래와 같이 변경할 수 있다. 이렇게 하면 isLoading과 fetchData가 한 세트가 되어 여러번 호출해도 동일한 기능을 수행한다.
하지만 이를 실제 코드에 적용했을 때 오류가 발생했는데, 가장 처음에 화면이 렌더링될 때 isLoading이 false이므로 데이터를 받아온 줄 알고 받아온 데이터를 사용하는 DOM을 그리려다가 오류가 나는 것이다.
function App() {
const [backendData, setBackendData] = useState([]);
const [isLoading, setIsLoading] = useState(false);
const fetchData = async () => {
setIsLoading(true);
const response = await fetch('이곳에 Firebase에서 만든 API가 들어감');
const data = await response.json();
setIsLoading(false);
setBackendData(data);
}
useEffect(() => {
fetchData();
}, [])
return (
<button onClick={fetchData}>reload</button>
)
위의 코드에서 한 부분을 수정하면 정상 작동하는데, else if (backendData.length > 0)
조건을 추가해주면 된다.
이렇게 하면 맨 처음에 가져온 backendData의 길이가 0이므로 로딩 화면으로 돌아가고, 그 다음 데이터를 받아온 다음부터는 정상적인 화면을 렌더링한다.
function App() {
const [backendData, setBackendData] = useState([]);
const [isLoading, setIsLoading] = useState(false);
const fetchData = async () => {
setIsLoading(true);
const response = await fetch('이곳에 Firebase에서 만든 API가 들어감');
const data = await response.json();
setIsLoading(false);
setBackendData(data);
}
useEffect(() => {
fetchData();
}, [])
if (isLoading) {
return <div>Loading...</div>
} else if (backendData.length > 0) {
return (
// ...패치된 데이터를 사용하는 컴포넌트들
)
}
처음에는 아래의 코드에서 setIsLoading
부분이 잘못된 줄 알았다. 왜냐하면 이전에 쓴 포스팅에서 언급했듯이 이러한 상황에서 오류가 발생했기 때문이다. 하지만, 아래의 상황에서는 문제가 되지 않는다.
const fetchData = async () => {
setIsLoading(true);
const response = await fetch('이곳에 Firebase에서 만든 API가 들어감');
const data = await response.json();
setIsLoading(false);
setBackendData(data);
}
이유는 아래와 같다.
setState는 queueing되어 실행되는데, async 함수인 fetch 실행 직전까지 불린 setState가 queueing되어 반영되고, await fetch 가 완료되고 난 이후의 setState는 이전과는 별개로 queueing되어 반영된다.
따라서 아래와 같은 상황에서 setIsLoading(true)
=> async/await
=> setIsLoading(false)
순서로 실행된다.
만약 setIsLoading(false)
다음에 setIsLoading('test9')
가 있다면 isLoading은 'false'가 아니라 'test9'가 된다.
const fetchData = async () => {
setIsLoading('test1')
setIsLoading('test2')
setIsLoading('test3')
setIsLoading('test4')
setIsLoading(true);
const response = await fetch('이곳에 Firebase에서 만든 API가 들어감');
const data = await response.json();
setIsLoading('test5')
setIsLoading('test6')
setIsLoading('test7')
setIsLoading('test8')
setIsLoading(false);
// setIsLoading('test9') -> 주석처리를 지우면 이게 실행됨
setBackendData(data);
}
기존에는 아래와 같이 불리언 타입의 상태인 isLoading을 사용했었다.
const [isLoading, setIsLoading] = useState(false);
이번에는 위의 isLoading 대신 init, loading, loaded를 액션으로 가지는 status라는 상태를 사용해보았다.
지금은 init과 loading이 같아서 이전과 동일한 결과를 보여주지만, 만약 init이 '아무것도 받아오지 못했습니다'이고 loading이 '데이터를 받아왔지만 로딩중입니다'라면 상황이 달라질 것이다. 기존에는 이 둘을 구분할 수 없었다.
function App() {
const [backendData, setBackendData] = useState([]);
const [status, setStatus] = useState('init');
const fetchData = async () => {
setStatus('loading');
const response = await fetch('이곳에 Firebase에서 만든 API가 들어감');
const data = await response.json();
setStatus('loaded');
setBackendData(data);
setCartIsShown(false);
}
if (status === 'loading' || status === 'init') {
return <div>Loading...</div>
} else if (status === 'loaded') {
return (
return (
// ...패치된 데이터를 사용하는 컴포넌트들
)
}
status
라는 상태를 타입으로 구분할 수 있다고 한다. 이 포스팅에서 자세하게 설명되어있는데, 나중에 타입스크립트를 배우고 이렇게 써보고 싶다.이번 포스팅은 너무 하고 싶어서 새벽이지만 열심히 썼는데, 알게된 것이 많아서 기록으로 남기고 싶었다.
API 호출은 예전부터 나에게 미지의 영역이었는데, 하나씩 알아가는 것 같아서 기쁘다. 분명히 이번 포스팅은 나중에 다시 찾아보러 올 것 같다!
해당 개념에 익숙하지 않아서 오개념이 있을 수 있는데, 바로 수정할 수 있도록 댓글로 알려주시면 정말 감사할 것 같습니다!