이 파트의 예시 코드는 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 버튼으로 유저의 나이가 늘어나도
같은 값을 참조하기 때문에 실시간으로 바뀐 나이가 반영됩니다
페이지를 렌더링하기 위해 요청을 보내 데이터를 받아오는 일이 있습니다
해당 파트는 페이지 렌더링을 위해서 보내는 요청을 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도 마운트 이후 한번만 실행됩니다
정리
참고
Youtube - Web Dev Simplified - Every Beginner React Developer Makes This Mistake With State