web/react

[React/함수형 컴포넌트] props가 undefined인 경우 default value로 인한 useEffect 무한 루프 (state update와 rendering)

fien 2021. 9. 16. 19:56

Warning: Maximum update depth exceeded. This can happen when a component calls setState inside useEffect, but useEffect either doesn't have a dependency array, or one of the dependencies changes on every render.

일단 제목과 같은 문제가 발생하게 된 경위부터 이야기해보겠습니다.

Parent component에서 비동기적으로 서버에서 데이터를 조회하고 child component에 props로 전달합니다. child component 최초 렌더링 시에 에러를 방지하기 위해 props에 default value를 할당합니다. 그리고 props가 바뀔 때 child의 state를 업데이트하는 useEffect가 있습니다.

이 때 서버에서 데이터 로드에 실패해 props가 계속 undefined 상태로 남아있게 되는 경우 해당 props를 감시하는 useEffect가 무한루프에 빠지게 됩니다. 

 

예제 코드로 살펴 보겠습니다. 

import React, { useEffect, useState } from 'react';
import Child from './Child';
import axios from 'axios';

const Parent = () => {
  const [singer, setSinger] = useState({});
  useEffect(() => {
    // 비동기 서버 요청
    axios.get().then((data) => {
      // data를 이용해 아래처럼 setState 한다고 가정
      setSinger({
        name: 'taeyeon',
        songList: [
          { title: 'Weekend', isTitle: true },
          { title: 'To the moon', isTitle: false },
          { title: 'What Do I Call You', isTitle: true },
        ],
      });
    }).catch((error) => console.log(error)); 
    // error가 발생하면 songList는 계속 undefined로 무한루프에 빠지게 됨
  }, []);

  return <Child name={singer.name} songList={singer.songList} />;
};

export default Parent;

parent는 비동기 요청을 보내 가수의 곡 정보를 받아오고 child에 props로 전달합니다. 

import React, { useEffect, useState } from 'react';

// 최초 렌더링 시 오류를 막기 위해 default parameter를 빈 배열로 설정
const Child = ({ name, songList = [] }) => {
  const [titleList, setTitleList] = useState([]);

  useEffect(() => {
    // songList를 빈배열로 초기화 해줘야 오류가 발생하지 않음
    const list = songList.filter((song) => song.isTitle);
    setTitleList(list); // child의 state를 update -> child re-rendering
  }, [songList]);

  return (
    <>
      <p>{name}</p>
      {titleList.map((song) => (
        <p>{song.title}</p>
      ))}
    </>
  );
};

export default Child;

child에서는 해당가수의 타이틀 곡만 뽑아서 렌더링하는 예제입니다.

 

parent보다 child가 먼저 렌더링 되기 때문에 최초 렌더링시에 name과 songList는 undefined입니다. 

useEffect 코드를 보면 songList가 배열이기 때문에 map 함수를 호출하는데 초기에는 undefined라서 map 함수를 호출할 때 오류가 발생합니다. 이를 방지하기 위해 default parameter를 통해 songList의 default value를 [] 빈 배열로 설정했습니다.

map 함수 이후에는 setTitleList로 child의 state를 업데이트 합니다.

state가 업데이트되었기 때문에 리렌더링을 위해 child function component를 재실행합니다. parent에서 전달해주는 props인 songList가 여전히 undefined라면 default parameter에 설정한 [] 빈 배열로 다시 세팅해줍니다.

이 때, 이전의 빈 배열과 새로운 빈 배열이 동일하지 않기 때문에 child에서 songList를 감시하던 useEffect가 다시 동작합니다. 서버에서 데이터가 정상적으로 돌아와 songList가 undefined에서 벗어나게 되면 default value를 사용하지 않기 때문에 반복을 멈추지만 오류가 발생해 undefined인 상태로 남게되면 무한루프에 빠지게 됩니다. 그 뿐 아니라, 서버에서 응답이 늦는다면 그 만큼 useEffect를 반복하면서 불 필요한 렌더링이 발생합니다.

 

여러 방법을 통해 위 문제를 방지할 수 있습니다.

1. prop에 default value를 설정하지 않고 useEffect에 조건문을 거는 방식

const Child = ({ name, songList }) => {
  useEffect(() => {
    if (songList) {
      const list = songList.filter((song) => song.isTitle);
      setTitleList(list);
    }
  }, [songList]); 
 };

2. 초기 값을 컴포넌트 밖에 선언하기

const defaultValue = []
const Child = ({ name, songList= defaultValue }) => {
	...
}

3. parent에서 undefined 방지하기

const Parent = () => {
  const [singer, setSinger] = useState({});
  ...

  return <Child name={singer.name} songList={singer.songList || []} />;
};

export default Parent;

이 밖에 다른 방법도 있을 것 같은데 어떤게 가장 베스트인지는 모르겠습니다.

상황에 따라 다를 것 같기도 하고요.. 아시면 공유해주세요.

 

 

여기서 조금 헷갈리는게

child의 prop이나 state가 배열이나 객체가 아닌 값 타입(primitive type)이면 위의 문제가 발생하지 않습니다. 

 

https://ko.reactjs.org/docs/hooks-reference.html#bailing-out-of-a-state-update

 

Hooks API Reference – React

A JavaScript library for building user interfaces

ko.reactjs.org

공식문서에 따르면 동일값으로 업데이트하면 렌더링이나 useEffect 실행을 회피한다고 합니다. 여기에 힌트가 있습니다. (값 비교는 Object.is를 이용한다고 합니다.)

child의 props이나 state가 값 타입이면 두번째 렌더링 때 동일한 값을 비교하게 되서 반복을 멈추게 됩니다....!

로그 열심히 찍어보다가 알게된 사실...

물론 매번 다른 값으로 업데이트하면 계속 반복되겠죠.

 

정말 react hook는 알면 알수록 흥미진진하고 궁금증이 생겨나는 것 같아요. 

열심히 코드만 짜다가 싸다가(ㅠㅠ) 간만에 공부를 하니 재밌네요..

그리고 react도 정말 마냥 쓸게 아니라 동작원리에 대해 더 공부해야겠어요.. 

그럼 이만 ~~