state — 화면이 바뀌는 진짜 이유
Article
1강에서 UI = f(state)라고 했다. 이제 그 state가 무엇인지 볼 차례다.
버튼을 누르면 숫자가 올라가는 카운터를 만들어본다. 배운 것만으로 써보자.
그냥 변수로 해본다
function Counter() {
let count = 0;
function handleClick() {
count++;
console.log(count);
}
return (
<div>
<p>{count}</p>
<button onClick={handleClick}>+1</button>
</div>
);
}
버튼을 세 번 눌러본다.
- 1
- 2
- 3
콘솔에는 값이 올라간다. 그런데 화면은 계속 0이다.
왜 화면이 안 바뀌나
두 가지 문제가 겹쳤다.
- 01
React 는 다시 그릴 이유를 모른다
count++은 그냥 자바스크립트 변수를 바꿨을 뿐이다. React 에게 아무도 알려주지 않았다. - 02
설령 다시 그려도 값이 초기화된다
컴포넌트는 함수다. 다시 실행되면
let count = 0이 또 실행된다.let count = 0; // 매번 0 으로 시작
첫 번째가 알림의 문제, 두 번째가 기억의 문제다.
컴포넌트 바깥에 두면?
기억 문제를 피하려고 함수 밖으로 빼본다.
let count = 0;
function Counter() {
function handleClick() {
count++;
}
return (
<div>
<p>{count}</p>
<button onClick={handleClick}>+1</button>
</div>
);
}
값은 남는다. 하지만 여전히 화면은 안 바뀐다. React가 모르기 때문이다.
게다가 더 나쁜 문제가 생긴다.
function App() {
return (
<>
<Counter />
<Counter />
</>
);
}
카운터 두 개가 같은 count를 공유한다. 한쪽을 누르면 다른 쪽도 올라가야 하는데, 그마저도 화면에 반영되지 않는다.
함수 안의 변수
let count = 0
- 다시 실행되면 초기화된다
- 값을 기억하지 못한다
- React 가 알아채지 못한다
함수 밖의 변수
모듈 최상단
- 값은 남는다
- 모든 인스턴스가 공유한다
- React 가 알아채지 못한다
필요한 건 각 컴포넌트마다 따로 기억되면서, 바뀌면 React가 알아채는 값이다. 그게 state다.
useState
import { useState } from "react";
function Counter() {
const [count, setCount] = useState(0);
function handleClick() {
setCount(count + 1);
}
return (
<div>
<p>{count}</p>
<button onClick={handleClick}>+1</button>
</div>
);
}
이제 화면이 바뀐다.
useState는 값을 두 개 담은 배열을 돌려주고, 우리는 자바스크립트 편 18강의 배열 구조분해로 꺼낸다.
| 이름 | 무엇인가 | |
|---|---|---|
| [0] | count | 지금의 값 (읽기 전용) |
| [1] | setCount | 값을 바꾸고 React 에게 알리는 함수 |
| 인자 | useState(0) | 첫 렌더링에서만 쓰이는 초기값 |
이름은 관례다. [값, set값] 형태로 짓는다.
const [name, setName] = useState("");
const [isOpen, setIsOpen] = useState(false);
const [items, setItems] = useState([]);
setState 가 하는 두 가지 일
setCount(1)은 값을 바꾸는 게 전부가 아니다.
- 값을 저장한다React 가 따로 보관
- 리렌더링 예약"이 컴포넌트 다시 그려"
- 함수 재실행새 count 로 화면을 만든다
두 번째가 핵심이다. 값만 바꾸는 게 아니라 React에게 "다시 그려달라"고 알린다. 그냥 변수에는 없던 기능이다.
그리고 값은 React가 컴포넌트 바깥 어딘가에 보관한다. 그래서 함수가 다시 실행돼도 사라지지 않는다.
- React 내부
- Counter (첫 번째)
- 다시 실행돼도 3 을 돌려준다
- Counter (두 번째)
- 서로 간섭하지 않는다
- Counter (첫 번째)
컴포넌트를 두 개 쓰면 state도 두 개다. 자바스크립트 편 14강의 클로저에서 makeCounter()를 두 번 부르면 count가 각자 생기던 것과 같은 이야기다.
state 는 그 렌더링의 스냅샷이다
가장 헷갈리는 부분이다. 이 코드는 몇이 될까.
function Counter() {
const [count, setCount] = useState(0);
function handleClick() {
setCount(count + 1);
setCount(count + 1);
setCount(count + 1);
}
return <button onClick={handleClick}>{count}</button>;
}
세 번 더했으니 3일 것 같다. 1이다.
- 01
이 렌더링에서 count 는 0 이다
count는 변수다. 함수가 실행되는 동안 바뀌지 않는다. - 02
세 줄 모두 setCount(0 + 1) 이다
count가 계속0이므로 전부setCount(1)이다.setCount(0 + 1); // 세 번 다 같다 - 03
React 는 마지막 값을 쓴다
리렌더링 후
count는1이다.
setCount를 불러도 지금 실행 중인 함수의 count는 안 바뀐다. 다음 렌더링에서야 새 값이 들어온다.
function handleClick() {
console.log(count);
setCount(count + 1);
console.log(count);
}
- 0
- 0
연달아 더하려면 함수를 넘긴다.
setCount(c => c + 1);
setCount(c => c + 1);
setCount(c => c + 1);
이러면 3이 된다. React가 이전 값을 인자로 넣어주기 때문이다. 다음 강의에서 자세히 본다.
무엇이 state 여야 하나
전부 state로 만들면 안 된다. 판단 기준이 있다.
state 로
시간에 따라 변하고, 변하면 화면이 바뀐다
- 입력창에 친 글자
- 모달이 열렸는지
- 서버에서 받아온 목록
- 지금 선택된 탭
state 가 아님
다른 값으로 계산되거나, 안 변한다
- props 로 받은 값
- 다른 state 로 계산되는 값
- 화면과 무관한 값
- 상수
계산할 수 있는 값을 state로 두면 반드시 어긋난다.
function Cart({ items }) {
const [total, setTotal] = useState(0);
// items 가 바뀔 때마다 setTotal 을 부르는 걸 잊으면 끝
}
그냥 계산한다.
function Cart({ items }) {
const total = items.reduce((acc, item) => acc + item.price, 0);
return <p>{total}원</p>;
}
items가 바뀌면 컴포넌트가 다시 실행되고, total도 자동으로 다시 계산된다. 1강에서 말한 "데이터와 화면이 어긋나지 않는다"가 이것이다.
정리하면
- 그냥 변수는 다시 실행되면 초기화되고, 모듈 변수는 모든 인스턴스가 공유한다. 둘 다 React가 알아채지 못한다.
useState는 값을 기억하고, 바뀌면 리렌더링을 예약한다.- state는 컴포넌트마다 따로 보관된다.
count는 그 렌더링의 스냅샷이다.setCount를 불러도 지금 함수 안의 값은 안 바뀐다.- 연달아 갱신하려면 함수를 넘긴다.
setCount(c => c + 1) - 계산할 수 있는 값은 state로 두지 않는다. state가 적을수록 어긋나지 않는다.
다음 강의에서는 useState를 제대로 다룬다. 갱신 함수, 배칭, 객체와 배열을 담을 때의 규칙까지.