
상태 변수는 읽고 쓸 수 있는 일반 JavaScript 변수처럼 보일 수 있다. 하지만 상태는 스냅샷처럼 동작한다. 이를 설정하면 이미 가지고 있는 상태 변경되지 않고 대신 다시 렌더링이 트리거 된다.

클릭과 같은 사용자 이벤트에 대한 응답으로 사용자 인터페이스가 직접 변경되는 것으로 생각할 수 있다. 리액트에서는 이 모델과 약간 다르게 동작한다. 이전 페이지에서 상태 설정이 리액트에서 리렌더링을 요청하는 것을 보았다. 이는 인터페이스가 이벤트에 반응하려면 상태를 업데이트해야 함을 의미한다.
import { useState } from 'react';
export default function Form() {
const [isSent, setIsSent] = useState(false);
const [message, setMessage] = useState('Hi!');
if (isSent) {
return <h1>Your message is on its way!</h1>
}
return (
<form onSubmit={(e) => {
e.preventDefault();
setIsSent(true);
sendMessage(message);
}}>
<textarea
placeholder="Message"
value={message}
onChange={e => setMessage(e.target.value)}
/>
<button type="submit">Send</button>
</form>
);
}
function sendMessage(message) {
// ...
}
버튼을 클릭하면 다음과 같은 일이 발생한다.
onSubmit 이벤트 핸들러가 실행된다.setIsSent(true) 는 isSent 를 true 로 설정하고 새 렌더링을 대기열에 추가한다.isSent 값에 따라 컴포넌트를 다시 렌더링한다.상태와 렌더링의 관계를 자세 살펴보겠다.
“Rendering” 은 리액트가 함수인 컴포넌트를 호출한다는 것을 의미한다. 해당 함수에서 반환하는 JSX는 시간에 따른 UI의 스냅샷과 같다. props, 이벤트 핸들러 그리고 지역 변수는 모두 렌더링 당시의 상태를 사용하여 계산되었다.
사진이나 동영상 프레임과 달리 반환되는 UI “스냅샷”은 대화형이다. 여기에는 입력에 대한 응답으로 발생하는 작업을 지정하는 이벤트 핸들러와 같은 논리가 포함된다. 리액트는 이 스냅샷과 일치하도록 화면을 업데이트하고 이벤트 핸들러를 연결한다. 결과적으로 버튼을 누르면 JSX에서 클릭 핸들러가 트리거된다.
리액트가 컴포넌트를 리렌더링할 때

컴포넌트의 메모리로서 상태는 함수가 반환된 후 사라지는 일반 변수와는 다르다. 상태는 실제로 마치 선반 위에 있는 것처럼 리액트 자체에서 함수 외부에 “살아 있다.” 리액트가 컴포넌트를 호출하면 특정 렌더링 상태에 대한 스냅샷을 제공한다. 컴포넌트는 JSX의 새로운 props 및 이벤트 핸들러 세트와 함께 UI의 스냅샷을 반환하며, 모두 해당 렌더링의 상태 값을 사용하여 계산된다.

이것이 어떻게 작동하는지 보여주는 작은 실험이 있다. 이 예제에서는 “+3” 버튼을 클릭하면 setNumber(number + 1) 이 세 번 호출되므로 카운터가 세 번 증가할 것으로 예상할 수 있다.
“+3” 버튼을 클릭하면 어떤 일이 일어나는지 확인해라.
import { useState } from 'react';
export default function Counter() {
const [number, setNumber] = useState(0);
return (
<>
<h1>{number}</h1>
<button onClick={() => {
setNumber(number + 1);
setNumber(number + 1);
setNumber(number + 1);
}}>+3</button>
</>
)
}
number 는 클릭당 한 번만 증가한다.
설정된 상태는 다음 렌더링에 대해서만 변경된다. 첫 번째 렌더링 동안 number 은 0 이다. 이것이 해당 렌더링의 onClick 핸들러에서 setNumber(number + 1) 가 호출된 후에도 number 값이 여전히 0 인 이유이다.
<button onClick={() => {
setNumber(number + 1);
setNumber(number + 1);
setNumber(number + 1);
}}>+3</button>
이 버튼의 클릭 핸들러가 리액트에게 지시하는 작업은 다음과 같다.
setNumber(number + 1): number 는 0 이므로 setNumber(0 + 1).number 를 1로 변경할 준비를 한다.setNumber(number + 1): number 는 0 이므로 setNumber(0 + 1).number 를 1로 변경할 준비를 한다.setNumber(number + 1): number 는 0 이므로 setNumber(0 + 1).number 를 1로 변경할 준비를 한다.setNumber(number + 1) 을 세 번 호출했지만 이 렌더링에서는 이벤트 핸들러 number가 항상 0 이므로 상태를 세 번 1 로 설정했다. 이것이 바로 이벤트 핸들러가 완료된 뒤 리액트가 3 이 아닌 1 과 같은 number 로 컴포넌트를 다시 렌더링하는 이유이다.
또한 상태 변수를 코드의 해당 값으로 대체하여 이를 시각화할 수도 있다. 이 렌더링의 number 상태 변수는 0 이므로 해당 이벤트 핸들러는 다음과 같다.
<button onClick={() => {
setNumber(0 + 1);
setNumber(0 + 1);
setNumber(0 + 1);
}}>+3</button>
다음 렌더링의 경우 number 는 1 이므로 렌더링의 클릭 핸들러는 다음과 같다.
<button onClick={() => {
setNumber(1 + 1);
setNumber(1 + 1);
setNumber(1 + 1);
}}>+3</button>
이것이 바로 버튼을 다시 클릭하면 카운터가 2로 설정되고 다음 클릭 시 3으로 설정되는 이유이다.
이 버튼을 클릭하면 어떤 알림이 표시되는지 추측해보아라.
import { useState } from 'react';
export default function Counter() {
const [number, setNumber] = useState(0);
return (
<>
<h1>{number}</h1>
<button onClick={() => {
setNumber(number + 5);
alert(number);
}}>+5</button>
</>
)
}
이전의 대체 방법을 사용하면 경고에 “0”이 표시되는 것으로 추측할 수 있다.
setNumber(0 + 5);
alert(0);
하지만 경고에 타이머를 설정하여 컴포넌트가 리렌더링된 후에만 실행된다면 어떻게 될까? “0”일까 “5”일까
import { useState } from 'react';
export default function Counter() {
const [number, setNumber] = useState(0);
return (
<>
<h1>{number}</h1>
<button onClick={() => {
setNumber(number + 5);
setTimeout(() => {
alert(number);
}, 3000);
}}>+5</button>
</>
)
}
대체 방법을 사용하면 경고에 전달된 상태의 “스냅샷”을 볼 수 있다.
리액트에 저장된 상태는 알림이 실행될 때 변경되었을 수 있지만 사용자가 상호 작용할 때 상태의 스냅샷을 사용하여 예약되었다.
이벤트 핸들러의 코드가 비동기적이더라도 상태 변수의 값은 렌더링 내에서 절대 변경되지 않는다. 해당 렌더링의 onClick 내에서 setNumber(number + 5) 가 호출된 후에도 number 의 값은 계속 0 이다. 리액트가 컴포넌트를 호출하여 UI의 “스냅샷을 찍을” 때 그 값은 “고정”되었다.
다음은 이벤트 핸들러의 타이밍 실수를 줄이는 방법에 대한 예이다. 다음은 5초 지연하여 메시지를 보내는 양식이다. 다음 시나리오를 상상해보아라.
경고에 어떤 내용이 표시될 것으로 예상하나? “You said Hello to Alice” 아니면 “You said Hello to Bob”? 알고 있는 내용을 바탕으로 추측한 후 시도해봐라.
import { useState } from 'react';
export default function Form() {
const [to, setTo] = useState('Alice');
const [message, setMessage] = useState('Hello');
function handleSubmit(e) {
e.preventDefault();
setTimeout(() => {
alert(`You said ${message} to ${to}`);
}, 5000);
}
return (
<form onSubmit={handleSubmit}>
<label>
To:{' '}
<select
value={to}
onChange={e => setTo(e.target.value)}>
<option value="Alice">Alice</option>
<option value="Bob">Bob</option>
</select>
</label>
<textarea
placeholder="Message"
value={message}
onChange={e => setMessage(e.target.value)}
/>
<button type="submit">Send</button>
</form>
);
}
리액트는 한 렌더의 이벤트 핸들러 내에서 상태를 “고정”으로 유지한다. 코드가 실행되는 동안 상태가 변경되었는지 걱정할 필요가 없다.
하지만 다시 렌더링하기 전에 최신 상태를 읽고 싶다면 어떻게 해야할까? state updater function를 참조해라.
useState 를 호출하면 리액트는 해당 렌더링 상태의 스냅샷을 제공한다.