TIL

2024.02.28 TIL #useEffect와 렌더링 순서

inz1234 2024. 2. 29. 11:55
 
 function App() {
  const [todolist, setTodolist] = useState([]);
  const [videoList, setVideoList] = useState([1, 2, 3, 4, 5, 6, 7, 8]);

  const [displays, setDisplays] = useState(videoList.slice(0, 2));
  const [fourList, setFourList] = useState([]);
  const [twoList, setTwoList] = useState([]);
  const [totalWinner, setTotalWinner] = useState(null);

  useEffect(() => {
    setDisplays(videoList.slice(0, 2));
  }, [videoList]);

  const click = (game) => {
    // 8강 - 4강 리스트의 아이템 갯수가 4개가 되기 전
    if (fourList.length < 4 && videoList.length !== 0) {
      const newFourlist = [...fourList, game];
      setFourList(newFourlist);
      const filterdList = videoList.filter(
        (v) => v !== displays[0] && v !== displays[1]
      );
      if (filterdList.length !== 0) {
        setVideoList(filterdList);
      } else {
        setVideoList(newFourlist);
      }
      return;
    }

    // 4강 - 2강 리스트의 아이템 갯수가 2개가 되기 전
    if (fourList.length <= 4 && twoList.length < 2) {
      const newTwoList = [...twoList, game];
      setTwoList(newTwoList);
      const secondFilteredList = videoList.filter(
        (v) => v !== displays[0] && v !== displays[1]
      );
      if (secondFilteredList.length !== 0) {
        setVideoList(secondFilteredList);
      } else {
        setVideoList(newTwoList);
      }
      return;
    }

    // 2강
    if (twoList.length === 2) {
      setDisplays([]);
      setTotalWinner(game);
    }
  };
  return (
    <>
      <>
        {displays.map((game, idx) => (
          <button key={idx} onClick={() => click(game)}>
            {game}
          </button>
        ))}
        {totalWinner ? <div>최종 우승자는 {totalWinner}님 입니다</div> : null}
      </>
    </>
  );
}

export default App;

 

- 8강 월드컵을 하는 로직이다.

useEffect에 따른 렌더링 순서를 잘 모르는 스스로를 발견하여 정리하고자 한다.

 

원래 내가 생각했던 순서

1. button을 눌러서 onClick이 발생하면 click 함수가 실행된다.

2. 8강이니까 가장 처음 if문으로 들어갈 것이고, 거기서 setFourList라는 상태변경을 한다.

3. 상태 변경을 했으니 (상태변경이 된 직후 re-렌더링이 일어나는 게 아니라) 일단 함수가 끝날 때까지 기다린 후 위에서 부터 다시 렌더링이 된다.

4. useEffect는 무조건 렌더링이 다 완료된 다음에 그 안의 로직이 실행된다고 했으니,

    위에서부터 일단 useEffect를 지나치고  렌더링 된다.

5. 엇 그러면 useEffect 안의 setDisplays가 실행되기 전에 렌더링이 먼저 되니까 displays는 아직 반영되기 전의 값들로 보여지겠네?

6. 땡. 틀렸다. 실행해보니 useEffect 내의 setDisplays도 반영된 뒤에 화면에 업데이트 된 fourList와 displays가 보여졌다.

 

어떤 것을 잘못알고 있던 걸까?

4번을 잘못 알고 있었다.

useEffect 내의 의존성 배열에 setFourList로 바꾼 fourList 가 의존성 배열에 있으면,

setFourList가 있는 함수가 끝나고 다시 위에서부터 렌더링 될 때

useEffect를 지나치지 않는다. 

useEffect내의 로직이 실행되고, 거기서 setDisplays라는 상태변경이 또 있다면, 

displays 상태를 변경했으니 또 다시 위에서부터 렌더링 된다. 

 

순서를 정리해보자면

1. setFourList 를 포함한 함수 종료

2. 위에서부터 re-렌더링

3. 변경된 fourList를 의존성 배열 안에 가진 useEffect 로직 실행

4. useEffect 내의 setDisplays

5. state가 변경되었으니 다시 re-렌더링 

6. fourList와 displays 모두 변경된 값으로 화면에 보여짐

 

그래서 만약 useEffect의 의존성 배열에 displays도 포함되었다면,

useEffect내에서 setDisplays를 만나서 displays 상태를 업데이트 하고

상태를 변경했으니 다시 위에서부터 렌더링이 되면서 useEffect 의존성 배열에 displays가 있으니 

다시 useEffect 내의 로직이 실행되면서 setDiaplays 를 실행하고 또.... 무한루프가 발생한다.

하지만 위의 경우에는 의존성배열에 displays가 없으니 setDisplays가 실행된 후에는 다시 위에서부터 렌더링할 때 

그 때는 useEffect를 지나친 것이다.

 


즉, useEffect는 의존성 배열에 뭐가 있든 그저 마지막에 실행되는 줄 알았다.

그게 아니라, 컴포넌트 하위에서 useEffect의 의존성배열 안에 있는 상태(fourList)를 변경했다면,

위에서부터 다시 렌더링 될 때 useEffect를 만나서 안의 로직이 실행되고 그 밑으로 렌더링 되는 것이다.