[React] Warning: Can't perform a React state update on an unmounted component 에러 AbortController, useEffect의 cleanup function 으로 해결하기

이동현·2021년 6월 30일
0

React

목록 보기
5/16
post-thumbnail
//Home.js
const Home = () => {
  const { data: blogs, isPending, error } = useFetch(
    "http://localhost:8000/blogs"
  );
  return (
    <div className="home">
      {error && <div>{error}</div>}
      {isPending && <div>Loading...</div>}
      {blogs && <BlogList blogs={blogs} title="All Blogs" />}
    </div>
  );
};
       
//App.js
function App() {
  return (
    <Router>
      <div className="App">
        <NavBar />
        <div className="content">
          <Switch>
            <Route exact path="/"><Home /></Route>
            <Route path="/create"><Create /</Route>
          </Switch>
        </div>
      </div>
    </Router>
  );
}

위 코드는 Home 컴포넌트와 최상위 컴포넌트인 App 컴포넌트이다. 리액트 라우터를 이용해서 Home 컴포넌트로 또는 Create 컴포넌트로 갈 수 있다.

그런데 Home 컴포넌트를 보면 custom hook 인 useFetch 를 사용하는데 그 custom hook 내부에 useEffect 훅을 사용하고 그 안에 fetch() 를 이용해서 데이터를 가져오는 부분이 있다. 그런데 데이터를 가져오는데 분명히 네트워크 통신을 해야 하기 때문에 어느 정도 시간이 걸리는 상황을 알고 있어야 한다.

그래서 Home컴포넌트로 이동을 하면 데이터를 가져오기 전까지 어느 정도 시간이 필요한데 그 찰나의 순간에 라우터를 통해서 Create 컴포넌트를 보여주는 화면으로 이동을 하게 되면 제목과 같은 Warning: Can't perform a React state update on an unmounted component 에러를 만나게 된다.

이 에러가 나는 이유는 Home 컴포넌트에서 fetch를 통해서 데이터를 가져와서 state를 업데이트 해주는데 state 업데이트를 완료하기 전에 다른 컴포넌트가 렌더링 돼버리니까 마운트되어있지 않은 컴포넌트(Home)의 state를 업데이트할 수 없다고 말하는 것이다.

그렇다면 이 문제를 해결하기 위해서는 fetch를 통해서 데이터를 가져오고 있는 도중에 다른 컴포넌트를 마운트해서 렌더링하게 되는 상황에는 fetch를 중단해서 Home 컴포넌트의 state(위 상황에서는 data:blogs, isPending, error)를 업데이트하지 말고 중단해야 할 것이다.

그 때 우리가 사용할 것이 AbortControlleruseEffect의 cleanup function이다.

cleanup function, AbortController

useEffect 함수 내부에서 함수 구현부 마지막에 return 을 해주면서 함수를 작성해주면 그것이 바로 cleanup function이다. 보통 메모리릭을 막기 위해서 등의 목적으로 활용된다.

위 같은 상황에서 Home 컴포넌트 안에서 구현돼있던 useEffect 함수에 cleanup function으로 console.log('cleanup') 을 추가한다면 Create 컴포넌트로 이동할 때 Home 컴포넌트가 언마운트되면서 콘솔창에 cleanup 이 출력이 된다. 그렇다면 언마운트될 때 fetch 하는 것을 취소하면 우리가 직면한 이 에러를 해결할 수 있을 것이다.

주의! cleanup function을 작성할 때 return console.log('hi'); 라고 작성하면 클린업 함수로 작동하지 않고 바로 console.log함수를 실행하게 된다. 여기서는 콜백함수로 작성을 해줘야 컴포넌트가 언마운트될 때 의도한대로 기능을 한다. 그래서 return () => console.log('hi'); 라고 해줘야 cleanup function이 된다.

AbortController는 자바스크립트에서 비동기 fetch 작업을 할 때 그 작업을 취소할 수 있도록 해주는 인터페이스이다. 간단한 사용방법은 아래 코드 스니펫을 참고하면 된다.

const abortController = new AbortController(); // 1

fetch( 'http://example.com', {
  signal: abortController.signal // 2
} ).catch( err => { // 4
  console.log( err.message );
} );

abortController.abort(); // 3

위의 코드를 보면, 우선 AbortController DOM 인터페이스의 새로운 인스턴스를 만든 후 (1), 인스턴스의 signal 프로퍼티를 fetch 의 signal 옵션에 할당하는 것을 볼 수 있다. (2)

패칭을 중단하기 위해서는 단순히 abortController.abort() 를 호출하기만 하면 된다. (3) abort 를 호출하게 되면 fetch 의 Promise 는 자동으로 reject 되게 되고 제어는 catch() 블럭으로 진입하게 된다. (4)

이제 다시 돌아와서 아래의 useFetch 파일은 커스텀 훅을 구현한 것이고 그 안에 useEffect 함수를 사용하는 것을 알 수 있다. 첫 번 째로 const abortCont = new AbortController(); 처럼 AbortController 를 생성하고 그 이후에 fetch 함수에 두 번째 매개변수로 signal을 다음과 같이 넣어준다 fetch(url, { signal: abortCont.signal }). 그리고 나서 useEffect의 마지막 부분에 return 문에 함수를 추가함으로써 cleanup function을 추가하는데 여기에 return () => abortCont.abort(); 를 추가한다.

이렇게 Home 컴포넌트에서 fetch를 하고 있는 도중에 언마운트되고 다른 컴포넌트가 마운트되면서 렌더링이 되는 상황이 오면 cleanup function이 실행되면서 시그널이 발생되고 그 시그널을 받은 fetch함수는 에러가 발생하면서 catch 구문으로 넘어갈 것이다. 그런데 여기서 catch 구문에서도 state를 바꾸는 일을 하면 지금까지 한 노력이 다 헛걸음이 되므로 그 안에서도 state를 절대 update 하지 않도록 해야 한다.

//useFetch.js
const useFetch = url => {
  const [data, setData] = useState(null);
  const [isPending, setIsPending] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    //abort controller 생성
    const abortCont = new AbortController();

    //여기서는 async await 을 쓸 수 없다.
    setTimeout(() => {
      fetch(url, { signal: abortCont.signal }) //두번째 매개변수로 signal을 넣어준다.
        .then(res => {
          if (!res.ok) {
            throw Error("could not fetch the data for that resource");
          }
          return res.json();
        })
        .then(_data => {
          setData(_data);
          setIsPending(false);
          setError(null);
        })
        .catch(err => {
        //시그널이 발생했을 때는 state를 업데이트하지 않게 함
          if (err.name === "AbortError") {
            console.log("fetch error");
          } else {
            setIsPending(false);
            setError(err.message);
          }
        });
    }, 1000);

    return () => abortCont.abort();	//cleanup function
  }, [url]); //url이 바뀔때마다 다시 실행시키도록

  return { data, isPending, error };
};

이렇게 코드를 구현한다면 언마운트된 컴포넌트의 state를 update 하려는 동작을 막을 수 있고 오늘 제목과 같은 에러를 해결할 수 있다.

출처:
profile
Dom Hardy : 멋쟁이 개발자 되기 인생 프로젝트 진행중

0개의 댓글