ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [React] React Hook을 이용한 data fetching
    Frontend 2019. 8. 18. 23:00

     

     

    과거에 리액트 함수형 컴포넌트(Functional Component)클래스 컴포넌트(Class Component)와 달리 state나 life cycle method들을 사용할 수 없어 Stateless Component라고 불렸으며 단순히 props를 받아서 렌더링해주는 역할로만 사용되었다.

     

    그러나 React 16.8Hook이란 새로운 기능이 추가되면서 함수형 컴포넌트에서도 state나 다른 여러 기능들을 사용할 수 있게 되었다. Hook은 props, state, context, refs, lifecycle과 같은 여러 React 개념에 좀 더 직관적인 API를 제공한다는 장점이 있어 요즘은 클래스 컴포넌트보다 함수형 컴포넌트를 사용하는 추세로 변하였다.

     

     

    그래서 오늘은 간단한 예제 코드를 만들어 Hook을 이용하여 data fetching을 어떻게 하는지 정리해보려고 한다.

     

     

    import React, { useState, useEffect } from 'react';
    import './App.css';
    
    const App = () => {
    
    }
    
    export default App;
    

     

    먼저 함수형 컴포넌트에서 statelifecycle을 사용하기 위해서 필요한 useStateuseEffect를 불러온다.

     

     

    const App = () => {
      const [articles, setArticles] = useState([]);
    
      const getArticlesPromise = () => {
        return new Promise ((resolve, reject) => {
          setTimeout(() => {
            resolve([
              {id: 1, title: 'article title sample1'},
              {id: 2, title: 'article title sample2'},
              {id: 3, title: 'article title sample3'},
              {id: 4, title: 'article title sample4'},
              {id: 5, title: 'article title sample5'}
            ]);
          }, 1000);
        });
      };
    
      console.log('=== render ===');
      console.log('articles', articles);
      return (
        <div className="container">
          <ul>
            {articles.map(article => (
              <li key={article.id}>{article.title}</li>
            ))}
          </ul>
        </div>
      );
    }

     

    const [articles, setArticles] = useState([]); 이 부분은

    기존에 constructor() 안에서 아래와 같이 작성한 것과 동일하다.

     

    this.state = {
      articles: []
    };

     

    배열의 왼쪽은 state이름, 오른쪽은 해당 state의 값을 변경할 수 있는 메소드 이름을 적는다.

    useState()의 인자로는 state 초기값을 넣는다.

     

    getArticlesPromise() 메소드는 1초 후에 데이터 객체를 resolve하는 프로미스 함수이다.

    이제 useEffect를 사용하여 데이터를 fetch하고 ul 태그에 출력하는 과정이 필요하다.

     

     

    const App = () => {
      const [articles, setArticles] = useState([]);
    
      const getArticlesPromise = () => {
        return new Promise ((resolve, reject) => {
          setTimeout(() => {
            resolve([
              {id: 1, title: 'article title sample1'},
              {id: 2, title: 'article title sample2'},
              {id: 3, title: 'article title sample3'},
              {id: 4, title: 'article title sample4'},
              {id: 5, title: 'article title sample5'}
            ]);
          }, 1000);
        });
      };
    
      useEffect(() => {
        console.log('=== useEffect ===');
    
        const fetchArticles = async () => {
          const articleData = await getArticlesPromise();
          setArticles(articleData);
        }
    
        fetchArticles();
      }, []);
    
      console.log('=== render ===');
      console.log('articles', articles);
      return (
        <div className="container">
          <ul>
            {articles.map(article => (
              <li key={article.id}>{article.title}</li>
            ))}
          </ul>
        </div>
      );
    }

     

    useEffect는 lifecycle의 componentDidMountcomponentDidUpdate를 합친 듯한 개념으로

    컴포넌트가 리턴하는 jsx를 render한 이후에 호출된다.

     

     

    useEffect(() => {
      console.log('=== useEffect ===');
    
      const fetchArticles = async () => {
        const articleData = await getArticlesPromise();
        setArticles(articleData);
      }
    
      fetchArticles();
    }, []);

     

    component가 unmount되기 전, 혹은 업데이트 되기 직전에 어떤 작업을 수행하고 싶다면

    useEffect 함수에서 cleanup 함수를 리턴해야한다.

     

    useEffect에서 어떤 이벤트 리스너를 사용하거나 setInterval 등의 타이머 메소드를 사용한 후 clean up하고 싶을 때 특히 자주 쓰인다.

     

    위에서 우리는 async / await을 사용하고 있는데, 여기서 주의할 점은 async 함수는 promise 객체를 리턴하기 때문에

    useEffect 함수 자체를 async 함수로 사용할 수는 없다는 것이다. 왜냐하면 useEffect 함수는 위에서 말했듯이 반드시 함수만을 리턴해야하기 때문이다.

     

    useEffect(async () => {
      console.log('=== useEffect ===');
    
      const articleData = await getArticlesPromise();
      setArticles(articleData);
    }, []);

     

    즉 위 코드처럼 사용하면 에러가 발생한다.

     

    useEffect 함수는 최초 렌더링 직후에 실행되고, 모든 업데이트 때 마다 계속 실행된다.

    만약에 화면에 가장 처음 렌더링 될 때만 실행하고 싶으면 함수의 두 번째 파라미터에 빈 배열을 넣어주면 된다.

    만약 특정 값이 변경될 때만 호출하고 싶다면 useEffect 함수 두 번째 인자에 검사하고 싶은 값을 넣은 배열을 넣어주면 된다.

     

     

     

    ERROR HANDLING

    const App = () => {
      const [articles, setArticles] = useState([]);
      const [isError, setIsError] = useState(false);
    
      const getArticlesPromise = () => {
        return new Promise ((resolve, reject) => {
          setTimeout(() => {
            resolve([
              {id: 1, title: 'article title sample1'},
              {id: 2, title: 'article title sample2'},
              {id: 3, title: 'article title sample3'},
              {id: 4, title: 'article title sample4'},
              {id: 5, title: 'article title sample5'}
            ]);
          }, 1000);
        });
      };
    
      useEffect(() => {
        console.log('=== useEffect ===');
    
        const fetchArticles = async () => {
          setIsError(false);
    
          try {
            const articleData = await getArticlesPromise();
    
            setArticles(articleData);
          } catch (error) {
            setIsError(true);
          }
        }
    
        fetchArticles();
      }, []);
    
      console.log('=== render ===');
      console.log('articles', articles);
      console.log('isError', isError);
      return (
        <div className="container">
          {isError ? <div>Something went wrong!</div> : (
          	<ul>
              {articles.map(article => (
                <li key={article.id}>{article.title}</li>
              ))}
            </ul>
          )}
        </div>
      );
    }

     

    async / await을 사용하였으므로 try ... catch ...를 이용하여 에러 핸들링을 할 수 있다.

    isError라는 state를 추가해주고 데이터 불러오기 전에는 값을 false로 리셋해주고,

    데이터 불러온 후 에러가 발생했을 때 catch 구문에서 true로 바꿔준다.

     

    만약에 isError가 true라면 렌더링할 때, 에러를 표시해주는 div만 리턴할 것이다.

     

     

     

    LOADING INDICATOR

    const App = () => {
      const [articles, setArticles] = useState([]);
      const [isError, setIsError] = useState(false);
      const [isLoading, setIsLoading] = useState(false);
    
      const getArticlesPromise = () => {
        return new Promise ((resolve, reject) => {
          setTimeout(() => {
            resolve([
              {id: 1, title: 'article title sample1'},
              {id: 2, title: 'article title sample2'},
              {id: 3, title: 'article title sample3'},
              {id: 4, title: 'article title sample4'},
              {id: 5, title: 'article title sample5'}
            ]);
          }, 1000);
        });
      };
    
      useEffect(() => {
        console.log('=== useEffect ===');
    
        const fetchArticles = async () => {
          setIsError(false);
          setIsLoading(true);
    
          try {
            const articleData = await getArticlesPromise();
    
            setArticles(articleData);
          } catch (error) {
            setIsError(true);
          }
    
          setIsLoading(false);
        }
    
        fetchArticles();
      }, []);
    
      console.log('=== render ===');
      console.log('articles', articles);
      console.log('isError', isError);
      console.log('isLoading', isLoading);
      return (
        <div className="container">
          {isError && <div>Something went wrong!</div>}
    
          {isLoading ? (
            <div className="loading">
              Loading...
            </div>
          ) : (
            <ul>
              {articles.map(article => (
                <li key={article.id}>{article.title}</li>
              ))}
            </ul>
          )}
        </div>
      );
    }

     

    이제 로딩 인디케이터도 달아보았다.

     

    isLoading이라는 state를 만들고 데이터 fetch 전 값을 true로 바꾼다.

    데이터를 다 불러온 후, 맨 마지막에 false로 다시 값을 변경한다.

     

     

     

    CUSTOM HOOK

    우리는 article data를 fetch하여 에러가 발생하면 에러 페이지를 리턴하고,

    data를 불러오는 도중에는 loading 페이지를 리턴하고,

    data를 다 불러온 이후에는 data를 화면에 출력하는 부분만을 따로 모아서

    Custom Hook을 만들어 사용할 수 있다.

     

    const useArticleApi = () => {
      const [articles, setArticles] = useState([]);
      const [isError, setIsError] = useState(false);
      const [isLoading, setIsLoading] = useState(false);
    
      const getArticlesPromise = () => {
        return new Promise ((resolve, reject) => {
          setTimeout(() => {
            resolve([
              {id: 1, title: 'article title sample1'},
              {id: 2, title: 'article title sample2'},
              {id: 3, title: 'article title sample3'},
              {id: 4, title: 'article title sample4'},
              {id: 5, title: 'article title sample5'}
            ]);
          }, 1000);
        });
      };
    
      useEffect(() => {
        const process = async () => {
          setIsError(false);
          setIsLoading(true);
    
          try {
            const resolvedData = await getArticlesPromise();
            setArticles(resolvedData);
          } catch (error) {
            setIsError(true);
          }
    
          setIsLoading(false);
        }
    
        process();
      }, []);
    
      return [articles, isError, isLoading];
    }

     

    이 커스텀 훅을 실행하면, article data를 fetch하고 반환된 결과와

    에러 발생여부, loading 여부 등을 배열로 리턴해준다.

     

    const process = async () => {
      setIsError(false);
      setIsLoading(true);
    
      try {
        const resolvedData = await getArticlesPromise();
        setArticles(resolvedData);
      } catch (error) {
        setIsError(true);
      }
    
      setIsLoading(false);
    }
    
    useEffect(() => {
      process();
    }, []);

     

    참고로 useArticleApi()의 useEffect() 메소드 안에서 실행하는 process()를

    useEffect 밖에서 선언하면 다음과 같은 경고창이 뜬다.

     

     

    이런 경고창이 뜨는 이유는 React Hook의 useEffect가 process라는 함수에 의존적인데도 불구하고

    process 함수가 useEffect 밖에 선언되어 있기 때문이다.

    그러므로 아까 원래의 코드처럼 process 함수를 useEffect 내부에 선언하면 경고창이 사라진다.

     

     

    const Articles = () => {
      const [articles, isError, isLoading] = useArticleApi();
    
      if (isError) {
        return <div>Something went wrong!</div>;
      }
      
      if (isLoading) {
        return (
          <div className="loading">
            Loading...
          </div>
        );
      }
      
      if (!articles) return null;
    
      return (
        <ul>
          {articles.map(article => (
            <li key={article.id}>{article.title}</li>
          ))}
        </ul>
      );
    }

     

    그럼 우리는 이렇게 간단하게 커스텀 훅을 실행하여 반환된 결과를 가지고

    결과에 맞는 JSX를 리턴해주는 함수를 만들 수 있다.

     

     

    const App = () => {
      return (
        <div className="container">
          <Articles />
        </div>
      );
    }

     

    이제 코드를 실행해보면 우리가 원하던대로 결과가 잘 출력된다.

     

     

     

     

     


    참고자료:

    https://ko.reactjs.org/docs/hooks-effect.html

    https://www.robinwieruch.de/react-hooks-fetch-data/

    https://velog.io/@velopert/react-hooks

    https://stackoverflow.com/questions/56527984/react-hook-useeffect-has-a-missing-dependency

    반응형

    COMMENT