[짧] react 주의사항 | 같은 state 참조 | useCallback useEffect 무한루프

고광필·2022년 7월 1일
0

Front

목록 보기
18/33

state를 사용할 때는 같은 값을 참조해야 한다

이 파트의 예시 코드는 Youtube - Web Dev Simplified - Every Beginner React Developer Makes This Mistake With State 동영상을 번역했음을 알립니다.

문제 상황

import { useState } from "react";

const SelectUserNo = () => {
  const [users, setUsers] = useState([
    { id: 1, name: "전사", age: 27 },
    { id: 2, name: "마법사", age: 30 },
    { id: 3, name: "성기사", age: 33 },
    { id: 4, name: "궁수", age: 24 },
    { id: 5, name: "도적", age: 21 },
  ]);

  const [selectedUser, setSelectedUser] = useState();

  const increamentAge = (id) => {
    setUsers((currUsers) =>
      currUsers.map((user) => {
        if (user.id === id) {
          return { ...user, age: user.age + 1 };
        }
        return user;
      })
    );
  };

  const selectUser = (id) => {
    const user = users.find((user) => user.id === id);
    setSelectedUser(user);
  };

  return (
    <>
      <h3>
        SelectedUser:
        {selectedUser === undefined
          ? "None"
          : `${selectedUser.name} is ${selectedUser.age} years old`}
      </h3>
      {users.map((user) => (
        <div
          key={user.id}
          style={{
            display: "grid",
            gridTemplateColumns: "1fr auto auto",
            gap: ".25rem",
            marginBlock: ".5rem",
          }}
        >
          {user.name} is {user.age} old
          <button onClick={() => increamentAge(user.id)}>Increament</button>
          <button onClick={() => selectUser(user.id)}>Select</button>
        </div>
      ))}
    </>
  );
};

export default SelectUserNo;

user라는 상태 데이터가 있습니다
select 버튼을 누르면 그 유저 정보를 selectedUser에 저장합니다
increment 버튼을 누르면 그 유저의 나이를 1살 증가시킵니다

하지만, select를 눌러서 선택한 유저와
이후에 increment 버튼으로 나이를 증가시킨 유저는 다른 state가 됩니다
다른 state를 참조하고 있기 때문에 증가시킨 값이 실시간으로 반영되지 않습니다

해결 상황

import { useState, useMemo } from "react";

const SelectUserYes = () => {
  const [users, setUsers] = useState([
    { id: 1, name: "전사", age: 27 },
    { id: 2, name: "마법사", age: 30 },
    { id: 3, name: "성기사", age: 33 },
    { id: 4, name: "궁수", age: 24 },
    { id: 5, name: "도적", age: 21 },
  ]);

  const [selectedUserId, setSelectedUserId] = useState();
  const selectedUser = useMemo(
    () => users.find((user) => user.id === selectedUserId),
    [users, selectedUserId]
  );

  const increamentAge = (id) => {
    setUsers((currUsers) =>
      currUsers.map((user) => {
        if (user.id === id) {
          return { ...user, age: user.age + 1 };
        }
        return user;
      })
    );
  };

  const selectUser = (id) => {
    const user = users.find((user) => user.id === id);
    setSelectedUserId(user.id);
  };

  return (
    <>
      <h3>
        SelectedUser:
        {selectedUser === undefined
          ? "None"
          : `${selectedUser.name} is ${selectedUser.age} years old`}
      </h3>
      {users.map((user) => (
        <div
          key={user.id}
          style={{
            display: "grid",
            gridTemplateColumns: "1fr auto auto",
            gap: ".25rem",
            marginBlock: ".5rem",
          }}
        >
          {user.name} is {user.age} old
          <button onClick={() => increamentAge(user.id)}>Increament</button>
          <button onClick={() => selectUser(user.id)}>Select</button>
        </div>
      ))}
    </>
  );
};

export default SelectUserYes;

select를 누르면 user의 id를 기억하고,
user나 selectedUserId가 바뀔 때마다 선택한 유저의 정보를 갱신시킵니다

이렇게 되면 increament 버튼으로 유저의 나이가 늘어나도
같은 값을 참조하기 때문에 실시간으로 바뀐 나이가 반영됩니다

useCallback과 useEffect의 무한루프

페이지를 렌더링하기 위해 요청을 보내 데이터를 받아오는 일이 있습니다
해당 파트는 페이지 렌더링을 위해서 보내는 요청을 jsonplaceholder로 가정합니다

문제 상황

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

const TestLoop = () => {
  const [todoList, setTodoList] = useState([]);
  console.log("TestPage Render !");

  const getInit = useCallback(async () => {
    const response = await fetch(
      "https://jsonplaceholder.typicode.com/todos/1"
    );
    const data = await response.json();
    setTodoList(todoList.concat(data));
  }, [todoList]);

  useEffect(() => {
    getInit();
  }, [getInit]);

  return (
    <div>
      TestPage
      <div>
        <button onClick={() => console.log(todoList)}>console</button>
      </div>
    </div>
  );
};

export default TestLoop;

최초로 페이지에서 todo 목록을 요청받아오고, 기존의 todo 상태에 추가하는 코드입니다

이 코드는 프로젝트에서 실제로 발생했던 무한루프와 유사하게 만들어봤습니다

useEffect는 getInit 함수가 바뀔때마다 getInit함수를 실행합니다
getInit 함수는 useCallback으로 감싸져 있어서 todoList가 바뀔때마다 정의되고, 로직은 todoList를 추가합니다

getInit의 경우 자기가 todoList를 바꾸고, todoList가 바뀌었으니 다시 갱신되는 무한루프에 걸려있습니다

그래서 useEffect도 계속 갱신되어 콘솔에 TestPage Render!가 무수히 찍히게 됩니다

해결 상황

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

const TestOk = () => {
  const [todoList, setTodoList] = useState([]);
  console.log("TestPage Render !");

  const getInit = useCallback(async () => {
    const response = await fetch(
      "https://jsonplaceholder.typicode.com/todos/1"
    );
    const data = await response.json();
    setTodoList((prevTodoList) => prevTodoList.concat(data));
  }, []);

  useEffect(() => {
    getInit();
  }, [getInit]);

  return (
    <div>
      TestPage
      <div>
        <button onClick={() => console.log(todoList)}>console</button>
      </div>
    </div>
  );
};

export default TestOk;

setState는 갖고 있는 값을 바꾸는 로직의 callback을 인자로 받을 수 있습니다
위처럼 정의하게 되면 getInit 함수는 의존성 배열로 빈 배열을 가지고 있어서
한번만 정의됩니다

따라서 useEffect도 마운트 이후 한번만 실행됩니다

정리

  1. 같은 state를 참조해야 같은 데이터를 사용할 수 있습니다
  2. useCallback과 useEffect는 의존성 배열에 들어있는 값이 바뀔때를 감지합니다. 따라서 해당 로직이 언제 감지되는지를 파악해야 불필요한 렌더링과 무한루프를 없앨 수 있습니다

참고

Youtube - Web Dev Simplified - Every Beginner React Developer Makes This Mistake With State

profile
이해하는 개발자를 희망하는 고광필입니다.

0개의 댓글