useState 제대로 쓰기
Article
8강에서 setCount(count + 1)을 세 번 불렀더니 1이 됐다. 이번 강의는 그 지점에서 시작한다.
갱신 함수 — 이전 값을 받아온다
setState에는 값 대신 함수를 넘길 수 있다.
setCount(count + 1);
setCount(c => c + 1);
아래 형태는 React가 가장 최신 값을 인자로 넣어준다. 그래서 연달아 불러도 쌓인다.
function handleClick() {
setCount(c => c + 1);
setCount(c => c + 1);
setCount(c => c + 1);
}
- 3
- 01
함수 세 개를 큐에 넣는다
당장 실행하지 않는다. "이따 순서대로 적용해라" 하고 쌓아둔다.
- 02
첫 함수에 현재 값을 넣는다
0이 들어가1이 나온다.c => c + 1 // 0 → 1 - 03
그 결과를 다음 함수에 넣는다
1이 들어가2가 나오고, 또3이 된다. - 04
최종값으로 리렌더링
count는3이 된다.
setCount(값)
새 값이 이전 값과 무관할 때
setCount(0)— 초기화setName(input)— 입력값 대입- 읽기 쉽다
setCount(함수)
이전 값으로 새 값을 만들 때
setCount(c => c + 1)setItems(prev => [...prev, item])- 항상 최신 값을 보장한다
배칭 — 한 번에 모아서 그린다
setState를 세 번 부르면 화면을 세 번 그릴까. 아니다.
function handleClick() {
setCount(c => c + 1);
setName("지원");
setIsOpen(true);
}
React는 이벤트 핸들러가 끝날 때까지 기다렸다가 한 번만 다시 그린다. 이걸 **배칭(batching)**이라 부른다.
덕분에 화면이 중간 상태로 깜빡이지 않는다. 그리고 8강에서 count가 안 바뀌던 이유이기도 하다. 핸들러가 끝나기 전까지는 아직 다시 그려지지 않았다.
객체를 state 에 담을 때
여기서부터 실수가 많이 나온다.
const [user, setUser] = useState({ name: "지원", age: 27 });
function handleClick() {
user.age = 28;
setUser(user);
}
화면이 안 바뀐다. 왜일까.
자바스크립트 편 17강에서 객체 비교는 내용이 아니라 주소를 본다고 했다. user.age를 바꿔도 user가 가리키는 주소는 그대로다. React는 Object.is로 이전 값과 비교하는데, 같은 주소니까 "안 바뀌었네" 하고 그냥 넘어간다.
주소가 같으니 React 는 변화를 알아채지 못한다
- 이전 user
- 0x1a2b
- { name: '지원', age: 28 }
- 새 user
- 0x1a2b
- 같은 객체 — 바뀐 게 없다고 판단
새 객체를 만들어야 한다. 자바스크립트 편 18강의 스프레드를 쓴다.
function handleClick() {
setUser({ ...user, age: 28 });
}
{ ...user }가 새 객체를 만들고, age만 덮어쓴다. 주소가 다르니 React가 알아챈다.
배열을 state 에 담을 때
같은 규칙이다. 자바스크립트 편 15강에서 배운 원본을 바꾸는 메서드와 안 바꾸는 메서드의 구분이 그대로 쓰인다.
| 하려는 일 | 쓰면 안 되는 것 | 써야 하는 것 |
|---|---|---|
| 추가 | items.push(x) | [...items, x] |
| 앞에 추가 | items.unshift(x) | [x, ...items] |
| 삭제 | items.splice(i, 1) | items.filter(…) |
| 수정 | items[i] = x | items.map(…) |
| 정렬 | items.sort() | [...items].sort() |
const [items, setItems] = useState([]);
function addItem(item) {
setItems(prev => [...prev, item]);
}
function removeItem(id) {
setItems(prev => prev.filter(item => item.id !== id));
}
function toggleItem(id) {
setItems(prev =>
prev.map(item =>
item.id === id ? { ...item, done: !item.done } : item
)
);
}
map 안에서도 { ...item, done: ... }으로 새 객체를 만든다. 바꿔야 할 것만 새로 만들고 나머지는 그대로 둔다.
초기값은 첫 렌더링에만 쓰인다
function Counter({ initial }) {
const [count, setCount] = useState(initial);
...
}
initial이 나중에 바뀌어도 count는 안 바뀐다. useState의 인자는 첫 렌더링에서만 읽힌다. 이후 렌더링에서는 무시된다.
초기값 계산이 무겁다면 함수를 넘긴다.
const [items, setItems] = useState(loadFromStorage());
const [items, setItems] = useState(() => loadFromStorage());
위쪽은 렌더링할 때마다 loadFromStorage()를 실행한다. 결과는 버려지는데 실행은 된다. 아래쪽은 첫 렌더링에서 딱 한 번만 부른다.
state 를 몇 개로 나눌까
하나의 객체로 묶을지, 여러 개로 나눌지 고민된다.
const [name, setName] = useState("");
const [email, setEmail] = useState("");
const [form, setForm] = useState({ name: "", email: "" });
따로따로
서로 무관한 값
- 갱신이 간단하다
- 무엇이 바뀌는지 명확
- 대부분 이쪽이 낫다
객체로 묶기
항상 함께 바뀌는 값
- 폼 필드가 많을 때
- 스프레드를 매번 써야 한다
- 필드를 빠뜨리면 값이 사라진다
기본은 나누는 쪽이다. 함께 바뀌는 게 확실할 때만 묶는다.
묶었다면 갱신할 때 스프레드를 잊지 않는다.
setForm({ email: value });
setForm(prev => ({ ...prev, email: value }));
위쪽은 name이 통째로 사라진다. 아래쪽 화살표 함수에서 객체를 괄호로 감싼 것도 눈여겨본다. 3강에서 본 그 규칙이다.
정리하면
- 이전 값으로 새 값을 만든다면 갱신 함수를 넘긴다.
setCount(c => c + 1) - React는 핸들러가 끝날 때까지 모아뒀다가 한 번만 다시 그린다. 이게 배칭이다.
- state를 직접 고치면 React가 알아채지 못한다. 객체 비교는 주소로 하기 때문이다.
- 항상 새 객체·새 배열을 만든다.
push,splice,sort를 쓰지 않는다. - 스프레드는 얕은 복사다. state 구조를 얕게 유지한다.
useState의 인자는 첫 렌더링에서만 읽힌다. 무거운 계산은 함수로 넘긴다.- state는 기본적으로 나눈다. 함께 바뀌는 것만 묶는다.
다음 강의에서는 사용자의 클릭과 입력을 받는 법, 이벤트 처리를 본다.