state 변수는 읽고 쓸 수 있는 일반 JavaScript 변수처럼 보일 수 있습니다. 하지만 state는 스냅샷처럼 동작합니다. state 변수를 설정해도 이미 가지고 있는 state 변수는 변경되지 않고, 대신 리렌더링이 실행됩니다.
Click 과 같은 유저 이벤트에 반응하여 사용자 인터페이스가 직접 변경된다고 생각할 수 있습니다. 하지만, React 에서는 이런 과정과는 조금 다르게 작동합니다. React 에서는 state
를 설정하면 리렌더링을 요청하게 됩니다. 즉, 유저의 이벤트에 반응하려면 state
를 업데이트해야 합니다.
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) {
// ...
}
사진이나 동영상 프레임과 달리 반환하는 UI Snapshot
은 대화형입니다. 여기에는 input, button 등에 대한 응답으로 어떤 일이 일어날지 지정하는 이벤트 핸들러와 같은 로직이 포함됩니다. 그러면 React는 이 Snapshot
과 일치하도록 화면을 업데이트하고 이벤트 핸들러를 연결합니다.
컴포넌트의 메모리로서 state
는 함수가 반환된 후 사라지는 일반 변수와 다릅니다. state
는 실제로 함수 외부에 마치 선반에 있는 것처럼 React 자체에 “존재”합니다. React가 컴포넌트를 호출하면 특정 렌더링에 대한 state의 Snapshot
을 제공합니다. 컴포넌트는 해당 렌더링의 state
값을 사용해 계산된 새로운 props 와 이벤트 핸들러가 포함된 UI의 스냅샷을 return 해줍니다.
setNumber(number + 1)
를 세 번 호출하므로 세 번 증가할 것으로 예상할 수 있습니다.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>
</>
)
}
setNumber(number + 1)
를 세 번 호출했지만, 이 렌더링에서 이벤트 핸들러의 number는 항상 0
이므로 state
를 1로 세 번 설정했습니다. 이것이 이벤트 핸들러가 완료된 후 React가 컴포넌트 안의 number 를 3이 아닌 1로 다시 렌더링하는 이유입니다.
React에서 상태를 업데이트할 때 setState
또는 useState hook
의 setter 함수
를 호출하면 현재 상태를 업데이트하는 것이 아니라, 다음 렌더링 사이클에서 적용될 새로운 상태를 예약하는 것입니다.
따라서, 여러 번 setNumber(number + 1)
을 호출하더라도 현재 렌더링 사이클에서는 number 값이 업데이트되지 않습니다. 각 호출에서 number는 여전히 0으로 유지됩니다. 그래서 이벤트 핸들러가 완료된 후 React는 컴포넌트를 다시 렌더링하고, 해당 시점에 number의 값은 이미 1로 설정되어 있습니다.
만약, 위의 코드에서 numberd의 값을 3으로 바꾸고 싶다면 아래와 같은 로직을 따를 수 있을 것 같습니다.
import { useState } from 'react';
export default function Counter() {
const [number, setNumber] = useState(0);
return (
<>
<h1>{number}</h1>
<button onClick={() => {
setNumber(prev => prev + 1);
setNumber(prev => prev + 1);
setNumber(prev => prev + 1);
}}>+3</button>
</>
)
}
prev => prev + 1
은 함수형 업데이트
를 사용하는 방법입니다. 이 방법은 이전 상태를 활용하여 새로운 상태를 설정하는 데 사용됩니다.
이전에 설명한 것처럼 setNumber(prev => prev + 1)
을 사용하면 현재 상태가 아닌 이전 상태를 기반으로 값을 업데이트합니다. 이것은 비동기적
으로 상태를 업데이트하고, 여러 번의 setNumber 호출이 있을 때 이전 상태를 올바르게 기반으로 새로운 값을 설정할 수 있습니다.
따라서 위의 코드에서 setNumber(prev => prev + 1)을 사용하여 이전 상태를 기반으로 1씩 증가시키는 것도 가능합니다. 이 방법을 사용하면 이벤트 핸들러가 실행될 때마다 현재 상태가 아닌 이전 상태를 기반으로 값을 업데이트하므로 의도대로 동작할 것입니다.
React에서 useState
를 사용하여 상태를 설정하면, 해당 렌더링에 대한 상태의 스냅샷
이 생성됩니다. 이를 통해 React는 상태의 변경을 추적하고, 다음 렌더링 사이클에서 변경된 상태를 반영합니다. 컴포넌트 외부에 마치 선반에 보관하는 것처럼 상태를 저장하고, useState를 호출하면 해당 렌더링에 대한 상태 스냅샷
을 제공합니다.
변수와 이벤트 핸들러는 다시 렌더링되어도 "살아있지" 않습니다
. 즉, 변수와 이벤트 핸들러는 각각의 렌더링마다 "재생성"
됩니다. 각 렌더링은 자체적인 이벤트 핸들러를 가지며, 모든 렌더링과 그 안에 있는 함수는 항상 해당 렌더링에 제공된 상태의 스냅샷
을 보게 됩니다. 이 때, 과거에 생성된 이벤트 핸들러는 해당 렌더링 시점의 상태 값을 갖게 됩니다.
React의 이러한 원리를 이해하면 React 컴포넌트의 동작을 더 잘 이해하고, 상태와 이벤트 처리를 효과적으로 다룰 수 있다고 생각합니다. 아직 어려운 부분들이 있지만 실전에서 코드를 많이 다뤄보며 더욱 공부하는 노력을 해볼려고 합니다.