[주문앱 9탄] API 호출하기(& useEffect)

비얌·2023년 5월 28일
0
post-thumbnail

🧹 개요

예전에 쓴 포스팅에서 잠깐 언급한 적 있는데, 기존에는 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



🛫 과정

1. fetch() 사용하기

처음에는 API를 호출하기 위해 다음 세가지를 사용했다.

  1. fetch()
  2. useEffect Hook
  3. isLoading이라는 상태

이를 사용하여 작성한 코드는 아래와 같다.

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를 호출했다.

    • 받아온 데이터가 있으면 isLoading을 false로 바꾼다. 데이터를 가져오기 전에는 isLoading이 true이므로 화면에 Loading...이 나타났다가 데이터를 받아온 후 정상적인 화면이 렌더링된다.
  • useEffect Hook

    • 데이터는 한번만 가져오면 되기 때문에, useEffect Hook 안에 fetch 함수를 넣음으로써 데이터를 처음에 한번만 가져오도록 했다.
  • isLoading이라는 상태

    • useEffect Hook으로 인해 화면에 DOM이 모두 그려진 후 데이터가 패치되게 된다. 따라서 패치 전에 그려지는 DOM에서 패치될 예정인 데이터를 사용한다면 오류가 날 것이다. 그래서 isLoading이라는 상태를 만들어 데이터가 패치되기 전과 후를 나누고 그에 따라 그려질 DOM을 구분했다.

2. async/await 사용하기

위에서 사용한 fetch()는 then().then().then()...과 같이 사용할 시 가독성이 좋지 않다는 단점 등이 있었다. 그래서 async/await를 사용하여 문제점을 개선하기로 했다.

이번에는 API를 호출하기 위해 다음 세가지를 사용했다.

  1. async/await
  2. useEffect Hook
  3. isLoading이라는 상태

이를 사용하여 작성한 코드는 아래와 같다.

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 (
      // ...패치된 데이터를 사용하는 컴포넌트들
	)
  }
}
  • fetch()를 async/await로 변경해보았다.
    • 받아온 데이터가 있으면 isLoading을 false로 바꾼다. 데이터를 가져오기 전에는 isLoading이 true이므로 화면에 Loading...이 나타났다가 데이터를 받아온 후 정상적인 화면이 렌더링된다.(fetch()를 사용했을 때와 동일한 과정이다)

3. fetchData()를 useEffect Hook 밖에서 선언하기

기존에는 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>
  )

useEffect Hook 밖으로 뺀 코드 - 오류버전

따라서 아래와 같이 변경할 수 있다. 이렇게 하면 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>
  )

useEffect Hook 밖으로 뺀 코드 - 정상 작동 버전

위의 코드에서 한 부분을 수정하면 정상 작동하는데, 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 (
      // ...패치된 데이터를 사용하는 컴포넌트들
	)
  }

💥 하나의 핸들러 안에서 동일한 setter를 불러도 될까?(& fetch)

처음에는 아래의 코드에서 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);
}

4. 불리언 대신 여러 액션을 상태로 사용하기

기존에는 아래와 같이 불리언 타입의 상태인 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라는 상태를 타입으로 구분할 수 있다고 한다. 이 포스팅에서 자세하게 설명되어있는데, 나중에 타입스크립트를 배우고 이렇게 써보고 싶다.
  • State machine과 XState
    이 개념은 위와 같은 포스팅에서 접했는데, State machine이라는 개념도 궁금하고 이를 구현한 XState라는 라이브러리도 꼭 써보고 싶다.
  • reat-query와 SWR
    처음에 fetch()를 썼을 때 어떤 분께 대신 이것들을 써보라고 추천받았다. 나중에 공부해서 사용해볼 것이다.
    공식문서에서도 이 방법 대신 React Query, useSWR, and React Router 6.4+ 등을 추천하고 있다.


🐹 회고

이번 포스팅은 너무 하고 싶어서 새벽이지만 열심히 썼는데, 알게된 것이 많아서 기록으로 남기고 싶었다.

API 호출은 예전부터 나에게 미지의 영역이었는데, 하나씩 알아가는 것 같아서 기쁘다. 분명히 이번 포스팅은 나중에 다시 찾아보러 올 것 같다!

해당 개념에 익숙하지 않아서 오개념이 있을 수 있는데, 바로 수정할 수 있도록 댓글로 알려주시면 정말 감사할 것 같습니다!

profile
🐹강화하고 싶은 기억을 기록하고 공유하자🐹

0개의 댓글