컴포넌트 내부에 선언한 변수는 컴포넌트가 다시 호출될 때마다 초기화된다. 그러나 컴포넌트 리렌더링이 일어난다고 해서 state가 초기화되진 않는다. 컴포넌트 안에 state를 선언했는데 어떻게 그게 가능할까?
state는 함수의 실행 및 종료에 관계없이 리액트 자체에 존재하기 때문이다.
let stateStorage = {}; // 컴포넌트별 상태 저장소
function useState(initialValue) {
const componentId = getCurrentComponentId(); // 현재 컴포넌트의 ID 가져오기
if (!(componentId in stateStorage)) {
stateStorage[componentId] = initialValue; // 최초 렌더링 시 초기화
}
return [
stateStorage[componentId], // 저장된 상태 반환
(newValue) => {
stateStorage[componentId] = newValue; // 상태 업데이트
reRenderComponent(componentId); // 해당 컴포넌트만 리렌더링
},
];
}
GPT가 리액트에서 상태를 관리하는 방식을 간략히 보여줬다. 한 번도 선언된 적 없는 state는 useState의 인자로 들어온 값으로 초기화하고, 그 외에는 리액트 자체적으로 저장된 상태를 반환한다.
즉, 컴포넌트 호출 시 리액트는 호출 당시의 state를 스냅샷으로서 제공한다. 그리고 그 state가 props, 이벤트 핸들러, 로컬 변수 등 컴포넌트 내부의 모든 계산에 사용된다. 이게 스냅샷으로서의 state의 개괄적인 개념이다.
지난 포스팅의 예제를 다시 가져와 봤다. number의 현재 값이 0이라면, 버튼 클릭 시 동작은 다음과 같다.
<button onClick={() => {
setNumber(number + 1); // setNumber(1)
setNumber(number + 2); // setNumber(2)
setNumber(number + 3); // setNumber(3)
}}>+3</button>
상태 변화 계산
이때 현재의 스냅샷은 number <- 0이므로 수행은 주석과 같다.
따라서 최종적으로 number <- 3으로의 변화가 계산된다.
렌더링 트리거
number 업데이트가 렌더링을 트리거한다.
컴포넌트 렌더링
이제 새 스냅샷에는 number <- 3이 찍혀 있고, 모든 number와 number를 사용하는 props, 이벤트 핸들러, 로컬 변수는 number <- 3을 이용해 계산된다.
DOM에 커밋
다음 예제에서 alert에 표시되는 값은 뭘까? 정답을 맞힌다면 스냅샷의 개념을 잘 이해했다고 봐도 되겠다!
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>
</>
)
}
이제부터는 state 업데이트 큐의 내용이다. 자연스럽게 연결되는 내용이라 한꺼번에 정리했다.
한때 나는 업데이터 함수를 남발해 왔다. 업데이터 함수란 setNumber(prev => prev + 3)에서 prev => prev + 3을 말한다. state의 동작을 정확히 이해하지 못했던 때에는 state가 바로 업데이트되지 않을 우려가 있을 때 무적의 prev를 꺼내는 줄 알았다...💦💦 하지만 state는 스냅샷으로서 동작하며, 렌더링 시 컴포넌트 전체에서 동일한 값을 가진다는 것을 이제는 안다.
아래 예제는 업데이터 함수를 사용했다. 왠지 결과는 n <- n + 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>
</>
)
}
setState는 사실 바로 값을 업데이트하지 않는다. 대신에 state 업데이트 큐에 인자로 들어온 값을 저장한다. 지난 포스팅에서 언급한 배치 업데이트의 핵심이 여기 있다고도 볼 수 있겠다. 그러니까 위 코드를 실행한 결과 큐에는 첫 번째 표와 같이 값이 저장된다.
그러면 다음 useState가 호출될 때, 즉, 다음 렌더링 중에 큐를 순회하며 state를 업데이트한다. 큐를 순회하면서 반환값을 prev에 넘겨주기 때문에 아래 표에서와 같이 연쇄적인 업데이트가 가능해진다.
| prev => prev + 1 |
| prev => prev + 1 |
| prev => prev + 1 |
| prev | 다음 업데이트 | return |
| 0 | prev => prev + 1 | 0 + 1 |
| 1 | prev => prev + 1 | 1 + 1 |
| 2 | prev => prev + 1 | 2 + 1 |
이 방식으로 처음 예제도 다시 보자.
버튼을 클릭하면, setNumber는 사실상 주석과 같이 동작하기 때문에 큐에는 첫 번째 표와 같이 값이 저장될 것이다. prev 값은 사용하지 않고, number를 계속해서 덮어씌우면서 큐를 순회하여 최종적으로는 number에 3이 저장되게 된다.
<button onClick={() => {
setNumber(number + 1); // setNumber(1)
setNumber(number + 2); // setNumber(2)
setNumber(number + 3); // setNumber(3)
}}>+3</button>
| number를 1로 바꾸기 |
| number를 2로 바꾸기 |
| number를 3로 바꾸기 |
| prev | 다음 업데이트 | return |
| 0 (사용 안 함) | number를 1로 바꾸기 | 1 |
| 1 (사용 안 함) | number를 2로 바꾸기 | 2 |
| 2 (사용 안 함) | number를 3으로 바꾸기 | 3 |
업데이터 함수의 매개변수 이름은 그냥 앞자를 따서 n을 사용해도 되고, 명시적으로 prevNumber, prev, ... 등으로도 사용한다.
이제 리액트의 핵심 기능인 state 전반에 대한 이해가 가능해진 것 같다! 다음 포스팅에서는 State 구조 선택하기를 정리하면서 어떻게 state를 잘 관리할지 살펴보려고 한다.