
React를 이용해 프로젝트를 개발하다보면, 여러 데이터들을 많이 다루게 되고 useState 같은 상태 관리 Hook들을 많이 사용하고는 한다.
이 과정에서, '상태 관리'란 무엇인지,
그리고 대표적인 상태 관리 Hook인 useState의 동작 원리는 무엇이며 어떻게 활용하면 좋을지에 대한 글을 작성해보고자 한다.
🧾 참고 문서 :
https://ko.react.dev/learn/state-a-components-memory
https://ko.react.dev/reference/react/useState
https://ko.react.dev/learn/state-as-a-snapshot
https://ko.react.dev/learn/queueing-a-series-of-state-updates
state란state 변수 사용state를 다른 컴포넌트에 전달하려면, props를 사용해야 함useState, useReducer Hook을 사용하여 상태 관리useContext Hook을 사용하여 상태 관리Redux, Recoil 같은 상태 관리 라이브러리를 이용하기도 함state 변수를 추가할 수 있는 React Hookconst [state, setState] = useState(initialState);
setState(변경할 값);
<div>{state}</div>
initialState: state의 초기 설정값. 어떤 값이든 지정 가능
function TodoList() {
const [todos, setTodos] = useState(createInitialTodos());
// ...current state: state. 첫 번째 렌더링 중에는 initialState가 들어감.set function: setState 함수. state를 다른 값으로 업데이트하고 리렌더링 발생시킴.useState는 Hook이므로 컴포넌트의 최상위 레벨이나 직접 만든 Hook에서만 호출 가능.setSomething(nextState) 과 같이 사용setName('Taylor');
setAge(a => a + 1);
nextState: state가 될 값. 모든 데이터 타입이 허용 가능하나,
반환값 없음.
set function은 다음 렌더링을 위해서 state를 업데이트함.
set function을 호출한 후 바로 state를 읽으면 호출 이전 값을 얻게 됨.새로 전달한 값이 현재 state와 동일할 경우 React는 최적화를 위해 컴포넌트 리렌더링을 하지 않음
React는 state 업데이트를 일괄적으로(batch) 처리함.
set function이 호출된 후에 화면이 업데이트됨. (하나의 이벤트 중에 여러 번 렌더링 되는 것 방지 위함)아래 예제는 카운터(숫자)
- 버튼 한 번 누른 결과

이 외에 TextField(문자열), Checkbox(불리언), Form(2개의 변수)도 가능
import { useState } from 'react'; // useState 가져오기
export default function Counter() {
const [count, setCount] = useState(0); // 상태 선언하기
function handleClick() {
setCount(count + 1); /// 상태 변경하기
}
return (
<button onClick={handleClick}>
You pressed me {count} times // UI에 상태 반영
</button>
);
}
+3 버튼을 누르면 Your age: 45로 변함import { useState } from 'react';
export default function Counter() {
const [age, setAge] = useState(42);
function increment() {
setAge(a => a + 1);
}
return (
<>
<h1>Your age: {age}</h1>
<button onClick={() => {
increment();
increment();
increment();
}}>+3</button>
<button onClick={() => {
increment();
}}>+1</button>
</>
);
}

// 🚩 state 안에 있는 객체를 다음과 같이 변경하지 마세요.
form.firstName = 'Taylor';... 사용)// ✅ 새로운 객체로 state를 교체합니다.
setForm({
...form,
firstName: 'Taylor'
});
다만, 아래 예제와 같이
Form의 상태가 중첩된 객체로 이루어져 있을 경우,
import { useState } from 'react';
export default function Form() {
const [person, setPerson] = useState({
name: 'Niki de Saint Phalle',
artwork: {
title: 'Blue Nana',
city: 'Hamburg',
image: 'https://i.imgur.com/Sd1AgUOm.jpg',
}
});
return <></>;
}

바꾸고 싶은 값이 객체 안의 깊숙한 곳에 있으면,
ex) person state
person
└── artwork
└── title ← 이걸 바꾸고 싶다!
아래와 같이 ❗ 그 값을 포함하고 있는 모든 객체 ❗ 들을 차례로 복사해야 함.
function handleTitleChange(e) {
setPerson({
...person,
artwork: {
...person.artwork,
title: e.target.value
}
});
}
자바스크립트는 객체를 복사할 때 얕은 복사를 하기 때문.
즉, 최상위 한 단계까지만 복사하고,
그 내부에 있는 객체들은 원본과 똑같은 참조를 가지기 때문에
React는 객체가 변경되었다고 감지하지 않음.
const original = {
name: 'Kim',
artwork: {
title: 'Untitled'
}
};
const copy = { ...original };
위 코드에서 copy는 original과는 다른 객체지만, 그 안의 artwork는 같은 객체를 참조함.
따라서
copy.artwork.title = 'New Title';
console.log(original.artwork.title); // 'New Title'
즉, copy.artwork.title을 바꾸면 원래 original.artwork.title도 함께 바뀜
=> 객체 내부의 artwork 까지도 복사해서 사용해야 함!
Immer 라이브러리 사용useReducer Hook 사용function handleClick() {
console.log(count); // 0
setCount(count + 1); // 1로 리렌더링 요청합니다.
console.log(count); // 아직 0입니다!
setTimeout(() => {
console.log(count); // 여기도 0이고요!
}, 5000);
}

=> 따라서 set function을 호출했더라도 아직 리렌더링 발생하기 전이라 업데이트된 값이 아닌, 이전 값이 출력됨
set function에 변경값을 전달하기 전에, 변경값을 먼저 console.log에 찍어보기Object.is()로 비교한 뒤 다음 state가 이전 state와 같으면 업데이트를 무시하기 때문obj.x = 10; // 🚩 잘못된 방법: 기존 객체를 변경
setObj(obj); // 🚩 아무것도 하지 않습니다. // ✅ 올바른 방법: 새로운 객체 생성
setObj({
...obj, // 이전 state를 복사해서
x: 10 // 원하는 부분만 수정 후 set function에 전달
});// 🚩 잘못된 방법: 렌더링 동안 핸들러 요청
return <button onClick={handleClick()}>Click me</button>// ✅ 올바른 방법: 이벤트 핸들러로 전달
return <button onClick={handleClick}>Click me</button>// ✅ 올바른 방법: **인라인 함수**로 전달
return <button onClick={(e) => handleClick(e)}>Click me</button>예시
function handleClick() {
setAge(age + 1); // setAge(42 + 1)
setAge(age + 1); // setAge(42 + 1)
setAge(age + 1); // setAge(42 + 1)
}
🤔 Why?
한 번의 렌더링에서 발생한 모든 상태 변경을 큐(queue)에 저장한 후
일괄적으로 처리하기 때문.

식당에서 웨이터가 손님의 주문을 모두 받은 후 주방에 전달하는 것처럼,
React는 state 업데이트를 하기 전에 이벤트 핸들러의 모든 코드가 실행될 때까지 기다림
😇 해결 방안)
업데이터 함수를 사용하여
대기 중인 이전 state를 바탕으로 다음 state를 계산하기
function handleClick() {
setAge(a => a + 1); // setAge(42 => 43)
setAge(a => a + 1); // setAge(43 => 44)
setAge(a => a + 1); // setAge(44 => 45)
}
React는 업데이터 함수를 큐에 넣으면,
첫 번째 a => a + 1은 대기 중인 state로 42를 받고 다음 state로 43을 반환함.
두 번째 a => a + 1은 대기 중인 state로 43을 받고 다음 state로 44를 반환함.
세 번째 a => a + 1은 대기 중인 state로 44를 받고 다음 state로 45를 반환함.
| 큐(queue) 목록 | a | 반환값 |
|---|---|---|
| a => a + 1 | 42 | 42 + 1 = 43 |
| a => a + 1 | 43 | 43 + 1 = 44 |
| a => a + 1 | 44 | 44 + 1 = 45 |