[React] 공식문서 톺아보기 2

Part 2. State 업데이트의 작동 원리: 큐와 일괄 처리

React에서 state를 변경하는 setState 함수는 개발자의 직관과 다르게 동작할 때가 있다. 코드가 실행되는 즉시 값이 변하지 않고, 여러 번 호출해도 마지막 결과만 반영되는 것처럼 보이는 현상이 대표적이다. 이는 React가 성능 최적화를 위해 업데이트를 처리하는 방식인 배칭과 큐 시스템 때문이다.

1. 배칭과 업데이트 큐

질문: setNumber(number + 1)을 세 번 호출했는데 왜 3이 더해지지 않는가?

export default function Counter() {
  const [number, setNumber] = useState(0);

  return (
    <button onClick={() => {
      setNumber(number + 1);
      setNumber(number + 1);
      setNumber(number + 1);
    }}>+3</button>
  );
}

위 코드를 실행하면 버튼을 클릭할 때 숫자가 3씩 증가할 것이라 예상하지만, 실제로는 1만 증가한다. setState를 세 번이나 호출했는데 React가 이를 무시한 것인지 의문이 든다.

답변: 렌더링 시점의 스냅샷과 일괄 처리

이 현상은 두 가지 원인이 복합적으로 작용한 결과다. 첫째, 해당 컴포넌트가 렌더링될 때의 number 값은 0으로 고정되어 있다. 따라서 세 번의 setNumber 호출은 모두 setNumber(0 + 1)과 동일하게 동작한다.

둘째, React는 state 업데이트를 즉시 처리하지 않는다. 이벤트 핸들러 내의 모든 코드가 실행될 때까지 기다렸다가, 변경 요청들을 큐라는 대기열에 담아둔다. 코드가 모두 실행된 후 큐에 담긴 요청을 한 번에 처리하여 화면을 갱신한다. 이를 배칭이라고 한다.

위 코드의 경우 큐에는 [1로 변경, 1로 변경, 1로 변경]이라는 주문이 쌓인다. React가 이를 순서대로 처리하면 최종 결과는 1이 된다. 이는 불필요한 리렌더링을 방지하고 성능을 높이기 위한 React의 핵심 동작 방식이다.

추가 의문: 이전 값을 참조하여 연속으로 계산하려면 어떻게 해야 하는가?

단순한 값 교체가 아니라, 앞선 업데이트가 완료된 후의 값을 바탕으로 다음 계산을 이어가고 싶다면 값을 대체하는 방식 대신 함수를 전달하는 방식을 사용해야 한다.

2. 값 교체와 업데이터 함수

질문: 업데이터 함수를 사용하면 왜 결과가 달라지는가?

setNumber(n => n + 1);
setNumber(n => n + 1);
setNumber(n => n + 1);

이렇게 작성하면 버튼 클릭 시 숫자가 정상적으로 3씩 증가한다. 단순히 값을 넣는 것과 함수를 넣는 것에 어떤 차이가 있어 결과가 달라지는지 이해가 필요하다.

답변: 큐에 함수를 등록하여 이전 상태를 이어받는다

setState에 값을 전달하면 React는 큐에 해당 값으로 교체하라는 명령을 저장한다. 반면 함수를 전달하면 React는 큐에 그 함수 자체를 저장한다. 그리고 큐를 처리할 때, 앞선 업데이트가 완료된 최신 state 값을 함수의 인자로 전달한다. 이를 업데이터 함수라고 한다.

React는 큐를 순회하며 다음과 같이 처리한다.

  1. n => n + 1 함수 실행. (현재 state 0을 받아 1 반환)
  2. n => n + 1 함수 실행. (방금 계산된 1을 받아 2 반환)
  3. n => n + 1 함수 실행. (방금 계산된 2를 받아 3 반환)

결과적으로 이전 계산 결과를 다음 계산의 입력으로 사용하게 되어 연속적인 업데이트가 가능해진다.

추가 의문: 값과 함수가 섞여 있으면 어떻게 처리되는가?

만약 큐에 [5로 교체, n => n + 1, 42로 교체] 순서로 명령이 들어있다면, React는 순서대로 처리한다. 5로 바꾼 뒤 1을 더해 6이 되지만, 마지막에 42로 교체하라는 명령이 있으므로 최종 state는 42가 된다.

3. 큐를 처리하는 자바스크립트의 논리

질문: React 내부에서는 이 큐를 어떻게 처리하는가?

이 과정을 시뮬레이션하는 getFinalState 함수를 작성할 때, 배열을 순회하는 방식에 대한 의문이 생긴다. 보통 반복문이라 하면 인덱스를 사용하는 for (int i) 방식을 떠올리지만, 여기서는 for…of 문법이 사용된다.

export function getFinalState(baseState, queue) {
  let finalState = baseState;

  for (let update of queue) { // 인덱스가 아닌 값을 직접 꺼낸다
    if (typeof update === 'function') {
      finalState = update(finalState);
    } else {
      finalState = update;
    }
  }

  return finalState;
}

답변: 인덱스보다 값 자체가 중요하다

큐에 담긴 내용은 단순한 숫자가 아니라 숫자일 수도 있고 함수일 수도 있다. React 입장에서는 이 주문이 몇 번째인지(인덱스)보다, 주문 내용이 무엇인지(값)가 중요하다.

for…of 문법은 배열의 인덱스를 무시하고 배열 안에 담긴 요소 자체를 하나씩 꺼내준다. update 변수에는 0, 1 같은 인덱스가 아니라 5, n => n+1 같은 실제 주문 내용이 담긴다. 이를 통해 React는 각 주문의 타입을 확인하고, 함수라면 실행하고 값이라면 덮어쓰는 로직을 간결하게 수행한다. 이는 React가 복잡한 상태 변화를 효율적으로 계산해내는 기본 원리다.


© 2022. All rights reserved.

Powered by Hydejack v9.2.1