TIL 99 - State as a snapshot, Batching, State와 Closure

김영현·2024년 6월 3일
0

TIL

목록 보기
110/129

서론

FiberV-DOM에 대해서 자세히 알아보려했으나 먼저 리액트 공식문서를 기반으로 기초를 조금 더 다져놓은뒤 심도깊은 학습을 한다면 이해가 더 잘될 것 같다. 그리하여 공식문서 내용 중 궁금하고 잘 몰랐던 부분을 간추려 적었습니다!


State as a snapshot

상태는 순간적인 사진이다.

rendering은 리액트가 컴포넌트를 호출(함수 실행)한다는 의미다. 해당 컴포넌트(함수)에서 반환하는 JSX는 결국 시간상UI의 스냅샷이다.
=> 즉 Prop, 이벤트 핸들러, 로컬 변수렌더 당시의 state를 기준으로 계산된다.

이게 무슨말인지 예제로 파악해보자.

rendering takes a snapshot in time

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는 몇일까? 3?
아쉽게도 정답은 1이다.

이전편에서 setState()re-rendringtriggering한다고 하였다.
이때 render phase에서는 이전 렌더링현재 렌더링을 비교하여 달라진 부분을 업데이트 한다고 했다.

즉, 각 setNumber를 기준으로 이전 렌더링이란 상태가 0인 순간을 뜻한다. (바로바로 업데이트되지 않고 대기열로 들어간다고 하였다.)
버튼의 이벤트 핸들러가 React에게 지시하는 작업을 말로 표현해보자면 아래 사진과 같다.

결국 상태스냅샷이기에 직관적으로 동작하지 않지만, 동작원리를 알아둔다면 상태를 적절히 관리할 수 있을 것이다.

State over time

두번째 문제를 맞춰보자!

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>
    </>
  )
}

위 코드의 결과는 몇일까? 5? 0? 이전 목차를 잘 숙지하였다면, 정답이 0이란걸 쉽게 알 수 있다.

개발자가 원하는 방향이었던 숫자5가 뜨게하려면 어떻게 해야할까?

...
      <button onClick={() => {
        setNumber(number + 5);
        setTimeout(() => {
          alert(number);
        }, 3000);
      }}>+5</button>

이렇게 setTimeout을 이용하여 render - commit phase가 끝난 뒤 작동하게 하면 되는걸까?
아쉽게도 똑같이 초기상태인 0을 반환하는 모습을 볼 수 있다.

이는 staterendering도중에 절대 변경되지 않는다는 점을 의미한다. 이미 초기상태인 0으로 스냅샷이 찍혀있기에...


Batching

setStatere-render을 유발한다. 이때 re-rendering이 대기열에 들어간다. 그러나 대기열에 들어가기 전에 여러 작업을 수행하고 싶을 수도 있다.

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>
    </>
  )
}

이전 snapshot파트에서 봤던 예제다. 개발자는 3번씩 더하는 버튼을 만들고싶었다. 그러나 상태는 이미 snapshot으로 찍혀있기에, 01을 더한 행위를 세번 할 뿐이다.

이를 해결하려면 어찌 해야할까?

일괄처리하기(batching)

잠시 짚고 넘어가야할 부분이 있다. Reactstate를 업데이트 하기 전에 모든 이벤트 핸들러 코드가 실행될 때 까지 기다린다.
즉, re-rendering은 모든 setState()가 실행된 이후에 시작한다는 뜻이다.

이렇게 작업하는 이유는 간단하다. 컴포넌트는 하나가 아니다. re-rendring도 한 번이 아니다. 따라서 여러 컴포넌트에서 사용하고있는 state를 한 번에 업데이트하여 re-rendering의 촉발을 최소화하는 것이다.
이를 Batching(일괄처리)라 부른다.

Updating the same state multiple times before the next render

3번씩 더하는 버튼문제를 한번 해결해보자.

//스냅샷을 이용하기에 0+1을 세번 한 결과가 된다. 즉, 1을 반환한다.
const snapshotUpdate = () => {
  setNumber(number + 1);
  setNumber(number + 1);
  setNumber(number + 1);
}

//updater function을 이용하여 setState에 전달한다.
const updaterFunction = () => {
  setNumber((prev) => prev+1);
  setNumber((prev) => prev+1);
  setNumber((prev) => prev+1);
}

두 방식의 차이점은 무엇일까? 값? 함수?
일단 두번째 방식이 큐에 들어간 뒤 어떻게 작동하는지 보자.
먼저 Batching때문에 마지막 setNumber가 실행될때까지 기다린다. 이후 한 번의 렌더링으로 처리된다.

위 함수들이 큐(대기열)에 들어가있다. 이 로직들은 계단식이다. 즉, 첫 번째 함수의 리턴 값을 두 번째 함수가 사용하고, 두 번째 함수의 리턴값을 세 번째 함수가 가져다 사용한다. 따라서 항상 최신 상태값을 기반으로 계산을 실행하게 된다

만약 setNumber(number + 1)을 3번 사용한다면 아래와 같은 과정을 겪게된다.

queued updatenreturns
replace With (1)0 (unused)1
replace With (1)0 (unused)1
replace With (1)0 (unused)1

=> 직접 값을 대입하여 상태를 업데이트하게되면 내부적으로는 updater function을 이용하지만 인수(바로이전 상태)를 사용하지 않게된다


번외) 클로저와 useState

useState는 컴포넌트가 렌더링(호출)되는 동안에도 값을 저장하고있다. 컴포넌트 내 단순히 선언한 지역 변수와 다른 기능을 갖고있다.
함수가 사라져도 남아있는 값이라...어디서 들어본적 있지 않은가? 바로 클로저의 역할이다.

const $app = document.querySelector(".app");

const React = (function () {
  let state;

  const useState = (initialState) => {
    if (state === undefined) state = initialState;
    const setState = (param) => {
      if (typeof param === "function") {
        state = param(state);
      } else {
        state = param;
      }
      render();
    };

    return [state, setState];
  };
  return { useState };
})();

const render = () => {
  $app.innerHTML = `
  <div>
    ${Counter()}
  </div>`;
};

const Counter = () => {
  const [number, setNumber] = React.useState(0);

  window.addCount = () => setNumber((prev) => prev + 1);
  return `<span>${number}</span> <button>숫자 증가</button>`;
};

render();

위 코드를 실행한 뒤, 버튼을 클릭해보면 다음과 같은 결과를 얻을 수 있다.

자세히보면 setState()함수 내부에서 render()를 계속 호출하는데, 어떻게 상태(number)가 보존되는걸까?

  1. React라는 상수에 IIFE(즉시실행 함수)가 실행된 결과를 할당한다. 이는 객체다.
  2. React는 이제 객체이며 useState()를 프로퍼티로 갖고있다.
    2-1. useState()state, setState()를 배열 형식으로 반환하는데, 이중 stateuseState()의 외부 변수이다. 이를 Lexcial Environment(정확히는 Lexical Environment내부 outer environment)가 기억하기에 클로저다.
    2-2. setState()는 항상 render()를 호출한다.
  3. render()Counter()을 호출한 결과를 할당한다.
  4. Counter()React객체의 함수인 useState()를 호출한 뒤 리턴값을 꺼내온다. 이때 꺼내온 number클로저다. 이를 활용하여 이벤트 핸들러와 상태값을 적절히 섞어 만든 html문자열을 반환한다. 이후 3번에서 할당된다.
  5. done!

결국 useState클로저를 활용한 로직이다.


요약

  1. state는 일종의 snapshot이다. 이전 렌더링버전의 상태는 렌더링 도중변경되지 않는다.
  2. state의 변경은 re-rendering을 촉발하기에 모든 setState()(이벤트 핸들러 등)의 처리가 끝나고 한 번에 업데이트 되게 만들었다. 일종의 대기열(큐)에 저장해둔 뒤 처리하는데, 이를 Batching이라고 한다.
  3. setState()값을 직접 넣으면 2번의 batching이 끝나야 비로소 상태가 업데이트된다. 하지만 update function을 전달하면, 즉시 업데이트된 값을 활용할 수 있다.
  4. useState()클로저다.

다음시간에는 드디어 V-DOMFiber에 대해서 알아보겠습니다...😮


출처

https://react.dev/learn/state-as-a-snapshot
https://react.dev/learn/queueing-a-series-of-state-updates
출처는 리액트 공식문서입니다

profile
모르는 것을 모른다고 하기

0개의 댓글