React, Next, Redux/🚀 React with Hooks

React Hooks : useEffect에서 데이터 fetching하기

DarrenKwonDev 2020. 4. 16. 21:30
 

How to fetch data with React Hooks? - RWieruch

A tutorial on how to fetch data in React with Hooks from third-party APIs. You will use state and effect hooks for the data request from a real API ...

www.robinwieruch.de

위 게시물을 근거하여 작성되었으며 기본적인 useEffect에 대해 알고 있다는 전제 하에 작성되었습니다.

 

 

🧷 useEffect를 componentDidMount처럼 사용하기

 

즉, 처음 마운트 된 후 1번만 useEffect가 실행하게 하고 싶다는 의미이다. 이 경우 deps에 빈 배열을 주면 된다. 해당 배열의 의미는, useEffect가 감시할 변수 대상을 말한다. 감시할 변수가 없으므로 변수의 변화라는 Effect에 관여하지 않는다.

useEffect(() => {...}, [])

 

🧷 Race condition(경쟁 조건, 동시 실행)없이 데이터 fetching 하는 방법

 

일반적으로 다음과 같이 axios를 통해 데이터를 가져오고, 그것을 state로 저장하여 활용합니다. Hook이 아닌 클래스 컴포넌트에서는 componentDidMount에 async를 걸어서 처리할 것입니다. Hook에서는 그와 비슷한 속성을 가진 useEffect Hook을 이용합니다.

 

여기서 중요한 건 useEffect의 두번째 인자로 빈 배열을 주지 않으면 구성 요소가 업데이트 될 때마다 다시 데이터를 fetching 할 것이란 사실입니다. state를 설정하는 useEffect의 경우 state를 설정하는 것 또한 변화에 해당하므로 이 경우 useEffect는 무한루프에 빠지게 됩니다.

 

따라서, 일반적인 경우 useEffect에 두번 째 인자로 빈 객체를 할당합니다. 이 경우 감시할 변수가 없으므로 변수 변동이 일어날지라도 발생하지 않고 componentDidMount처럼 마운트 후 최초 한 번만 실행됩니다.

import React, { useState, useEffect } from "react";
import axios from "axios";

export default () => {
  const [data, setData] = useState({ hits: [] });
  useEffect(async () => {
    const result = await axios.get(
      "https://hn.algolia.com/api/v1/search?query=redux"
    );
    setData(result.data);
  }, []);
  // useEffect의 두번째 인자로 빈 배열[]을 주었습니다.
  return (
    <ul>
      {data.hits.map((item) => (
        <li key={item.objectID}>
          <a href={item.url}>{item.title}</a>
        </li>
      ))}
    </ul>
  );
};

 

그러나 이 방식도 완전하지는 않습니다. 아마 명령창에서는 Effect callbacks are synchronous to prevent race conditions. Put the async function inside(effect 콜백 함수는 race condition을 방지하기 위해 동기적이어야 합니다. async를 콜백 함수 안쪽에 넣으십시오.) 라며 async의 위치를 함수 안으로 수정하기를 바랄 것입니다. 

 

🎈 race condition (경쟁조건)
 
다중 프로세스 환경에서 두 개 이상의 프로세스가 동시에 수행 될 때 발생되는 비정상적인 상태를 의미합니다. 동시 수행은 실행 결과의 일관성을 보장하기 어렵습니다. 여러 개의 비동기 함수를 동시에 실행한다면 무슨 정보가 먼저 들어올 지 매 실행마다 달라지기 때문입니다. 이를 방지하고자 async의 위치를 안 쪽으로 넣으라고 한 것입니다. 

 

이를 반영하여 수정하면 다음과 같습니다. 단순히 fetchData라는 비동기적 함수를 하나 만든 것 뿐입니다.

 

import React, { useState, useEffect } from "react";
import axios from "axios";

export default () => {
  const [data, setData] = useState({ hits: [] });
  useEffect(() => {
    // fetchData란 비동기함수 생성
    const fetchData = async () => {
      const result = await axios(
        "https://hn.algolia.com/api/v1/search?query=redux"
      );
      setData(result.data);
    };
    // 실행함으로써 데이타를 fetching합니다.
    fetchData();
  }, []);
  return (
    <ul>
      {data.hits.map((item) => (
        <li key={item.objectID}>
          <a href={item.url}>{item.title}</a>
        </li>
      ))}
    </ul>
  );
};

 

 

🧷 입력값으로 데이터 fetching 요청하기

 

만약 input 태그에 입력한 값으로 query를 구성하고 싶다면 어떻게 해야 할까? query값의 변화에 대해서 useEffect가 반응하도록 하면 된다. 두번째 인자로 준 빈 배열 내부에 query state를 추가한다.

 

import React, { Fragment, useState, useEffect } from "react";
import axios from "axios";

export default () => {
  const [data, setData] = useState({ hits: [] });
  const [query, setQuery] = useState("redux");

  useEffect(() => {
    const fetchData = async () => {
      const result = await axios(
        `https://hn.algolia.com/api/v1/search?query=${query}`
      );
      setData(result.data);
    };
    fetchData();
    // 객체에 query값이 있으므로 query값의 변화에 따라 useEffect가 재실행된다.
  }, [query]);
  

  return (
    <Fragment>
      <input
        type="text"
        value={query}
        onChange={(event) => setQuery(event.target.value)}
      />
      <ul>
        {data.hits.map((item) => (
          <li key={item.objectID}>
            <a href={item.url}>{item.title}</a>
          </li>
        ))}
      </ul>
    </Fragment>
  );
};

 

그러나 이 방식은 input에 무언가를 입력할 때마다 데이터를 fetching하기 때문에 정보가 많을수록 신속성이 떨어진다. 때문에 input 태그에 검색어를 입력한 후 제출한 후에만 fetching을 하도록 바꿔보자.

 

import React, { Fragment, useState, useEffect } from "react";
import axios from "axios";

export default () => {
  const [data, setData] = useState({ hits: [] });
  const [query, setQuery] = useState("redux");
  const [search, setSearch] = useState("");
  useEffect(() => {
    const fetchData = async () => {
      const result = await axios(
        `http://hn.algolia.com/api/v1/search?query=${search}`
      );
      setData(result.data);
    };
    fetchData();
    // 검색 시에만 data fetching을 요구해야 하므로 객체에는 search을 넣음
  }, [search]);
  return (
    <Fragment>
      <input
        type="text"
        value={query}
        onChange={(event) => setQuery(event.target.value)}
      />
      // 버튼을 눌렀을 때 search에 query값을 담기게 함
      <button type="button" onClick={() => setSearch(query)}>
        Search
      </button>
      <ul>
        {data.hits.map((item) => (
          <li key={item.objectID}>
            <a href={item.url}>{item.title}</a>
          </li>
        ))}
      </ul>
    </Fragment>
  );
};

 

 

🧷 Loader 사용하기

 

데이터를 받아오는 도는 도중에는 Loader를 렌더링하다가 데이터를 다 받아오면 Loader를 끄는 방식으로 클래스 컴포넌트를 작성한 바가 있을 것입니다. Hook에도 마찬가지로 데이터를 불러오는 도중에는 로딩 중이라 표시하는 것이 좋습니다. 이건 필수가리보다 습관이며 가급적 사용하길 권장합니다.

 

import React, { Fragment, useState, useEffect } from "react";
import axios from "axios";

export default () => {
  const [data, setData] = useState({ hits: [] });
  const [query, setQuery] = useState("redux");
  const [search, setSearch] = useState("");
  // loading이란 state를 하나 만들었습니다. 
  const [loading, setLoading] = useState(false);

  useEffect(() => {
    const fetchData = async () => {
      // 데이터를 받아오기 전이므로 로딩 중입니다.
      setLoading(true);
      const result = await axios(
        `http://hn.algolia.com/api/v1/search?query=${search}`
      );
      setData(result.data);
      // 데이터를 받아온 후이므로 로딩이 끝났습니다.
      setLoading(false);
    };
    fetchData();
  }, [search]);
  return (
    <Fragment>
      <input
        type="text"
        value={query}
        onChange={(event) => setQuery(event.target.value)}
      />
      <button type="button" onClick={() => setSearch(query)}>
        Search
      </button>
      // 로딩 중이면 Loading...을 출력하고 로딩이 끝나면 정보를 출력합니다.
      {loading ? (
        <div>Loading...</div>
      ) : (
        <ul>
          {data.hits.map((item) => (
            <li key={item.objectID}>
              <a href={item.url}>{item.title}</a>
            </li>
          ))}
        </ul>
      )}
    </Fragment>
  );
};

 

🧷 에러 핸들러

 

데이터 fetching에 실패한 경우 오류를 표시해야 합니다. 이 역시 필수는 아니나 유지보수에 있어서 에러 핸들링은 필수입니다.

 

import React, { Fragment, useState, useEffect } from "react";
import axios from "axios";

export default () => {
  const [data, setData] = useState({ hits: [] });
  const [query, setQuery] = useState("redux");
  const [search, setSearch] = useState("");
  const [loading, setLoading] = useState(false);
  // error를 설정합니다. ""는 boolean상 false입니다.
  const [error, setError] = useState("");

  useEffect(() => {
    const fetchData = async () => {
      // error를 ""(false)로 설정합시다. 
      setError("");
      setLoading(true);
      try {
        const result = await axios(
          `http://hn.algolia.com/api/v1/search?query=${search}`
        );
        setData(result.data);
        setLoading(false);
      } catch (error) 
        // 에러가 나면 error에 error 메세지를 담습니다.
        // 빈 문자열이 아니므로 true입니다.
        setError(error);
      }
    };
    fetchData();
  }, [search]);
  return (
    <Fragment>
      <input
        type="text"
        value={query}
        onChange={(event) => setQuery(event.target.value)}
      />
      <button type="button" onClick={() => setSearch(query)}>
        Search
      </button>
      
      // error가 true라면 에러 메세지를 출력합니다.
      {error && <div>에러가 발생했습니다 : {error}</div>}
      
      {loading ? (
        <div>Loading...</div>
      ) : (
        <ul>
          {data.hits.map((item) => (
            <li key={item.objectID}>
              <a href={item.url}>{item.title}</a>
            </li>
          ))}
        </ul>
      )}
    </Fragment>
  );
};