setState를 사용할 때 무한 렌더링이 되는 이유

GY·2021년 11월 13일
6

리액트

목록 보기
11/54
post-thumbnail

setState를 사용하기만 하면 무한루프가 발생해 계속해서 state값이 업데이트 되고 리렌더링되는 문제가 있었다.

LifeCycle에 대해 제대로 이해하지 못했기 때문인 것 같아 다시 정리해보려고 한다.

setState가 실행되면 안되는 lifecycle단계

props가 변경되었을 때 리액트가 동작하는 과정을 먼저 알아보자.

  1. componentWillRiceiveProps는 상위 컴포넌트로부터 props를 받을 때 실행된다. props를 받은 뒤 state를 업데이트하는 경우에 사용하면 좋다.

    이 단계에서는 setState를 사용해도 된다.

  2. shouldComponentUpdate는 props나 state가 변경되었을 때 다시 렌더링을 할지 말지 결정하는 API로, 불리언 값을 리턴한다.

  3. shouldComponentUpdate리턴값이 true일 경우 다시 렌더링한다.

  4. componentWilUpdate는 컴포넌트가 업데이트 되기 전에 실행되는 API이다.

    이 때!!! setState를 변경하면 안된다. state가 변경되면 리렌더링을 해야하므로 무한 루프가 발생한다.

    이 단계에서 의도치 않게 setState를 호출해 무한 리렌더링을 발생시킨 것 같다.

예시

  • 컴포넌트가 최초 렌더링될 때는 loading의 초기 값이 true이기 때문에 Loading... 이라는 메세지가 화면에 나타난다.
  • 이후 componentDidMount() 함수가 호출되면 API가 호출되어 그 응답 결과가 users 속성에 할당되고 loading이 false로 업데이트 된다.
  • 따라서 화면에 있던 Loading... 메세지는 사라지고, 대신 사용자 이름들이 나타난다.
import React, { Component } from "react";

class UserListClass extends Component {
  state = {
    loading: true,
    users: [],
  };

  componentDidMount() {
    fetch("https://jsonplaceholder.typicode.com/users")
      .then((response) => response.json())
      .then((users) => this.setState({ users, loading: false }));
  }

  render() {
    const { loading, users } = this.state;
    if (loading) return <div>Loading...</div>;
    return (
      <ul>
        {users.map((user) => (
          <li key={user.id}>{user.name}</li>
        ))}
      </ul>
    );
  }
}

react hook으로는 어떻게 할까?

useEffect 는 react의 클래스 기반 컴포넌트의 생명주기 중 componentDidMount, componentDidUpdate, componentWillUnmount가 합쳐진 것으로 볼 수 있다.

  • useEffect는 react에게 컴포넌트가 렌더링을 한 후에 어떤 다른 일을 수행해야 하는지를 알려준다.
    DOM 업데이트가 완료되고 나면 이 일들을 수행한다.

  • 기본적으로 첫번째 렌더링과 이후의 모든 업데이트에서 useEffect가 실행된다.

  • 처음 loading값이 true이기 때문에 loading...이 화면에 렌더링된다.

  • 렌더링 이후 useEffect가 호출된다. users데이터를 가져오고 setUsers(users)로 받아온 users의 값을 users 라는 useState 변수에 넣어준다. loading은 false로 바꿔 주었으므로, setState를 마친 뒤 리렌더링될때 loading은 사라지고 user만 보여진다.

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

function UserListFunction() {
  const [loading, setLoading] = useState(true);
  const [users, setUsers] = useState([]);

  useEffect(() => {
    fetch("https://jsonplaceholder.typicode.com/users")
      .then((response) => response.json())
      .then((users) => {
        setUsers(users);
        setLoading(false);
      });
  });

  if (loading) return <div>Loading...</div>;
  return (
    <ul>
      {users.map((user) => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  );
}

approach1

function MainPage({loggedIn}) {
  const [eventsState, setEventsState] = useState([]);
  
  useEffect(() => {
    fetch('assets/event.json')
    .then(response => response.json())
    .then(data => data.events)
    .then(events => setEventsState(events))
  })
  
  return (

      <ul className={styles.event_container}>
        {eventsState.map((event) => (
          <li className={styles.event_list}>
            <img className={styles.event_img}
              key={event.details}
              src={event.thumbnail}
              alt="event_img"
            />
          </li>
        ))}
      </ul>
  )

이렇게 했을 때, 이전에 다른 방법을 사용했을 때와는 달리 무사히 이미지가 렌더링 된다 (휴...)
데이터를 fetch로 받아온 다음에 렌더링을 다시 하는 것은 성공했다.

단, 계속해서 리렌더링 되는 문제는 여전히 남아있었다.

approach2

useEffect의 두번째 매개변수를 활용하면 된다 (이걸 잊고 넘겨버렸다 ㅎㅎ)

두 번째 매개변수를 넣어주면, 해당 값이 바뀔때만 effect를 재실행한다. [] 빈 배열만 넣어주면 어떤 값이 바뀌어도 efft가 재실행되지 않으므로 useEffect를 한 번만 실행시켜줄 수 있다.

따라서 setState로 값이 바뀜에 따라 다시 useEffect가 호출되고, setState가 호출되고, 값이 바뀌어서 다시 useEffect가 호출되는 무한루프의 늪에서 벗어날 수 있다.

  useEffect(() => {
    fetch('assets/event.json')
    .then(response => response.json())
    .then(data => data.events)
    .then(events => setEventsState(events))
  },[])

이렇게 두번째 매개변수로 빈 배열을 전달해주면, 한번만 렌더링된다!!

해결!!

Reference

profile
Why?에서 시작해 How를 찾는 과정을 좋아합니다. 그 고민과 성장의 과정을 꾸준히 기록하고자 합니다.

0개의 댓글