Frontend/개발 관련 지식

useState, react state 돌아보기 =(

덕구공 2024. 11. 24. 21:40

useState

  • React로 개발을 하다보면 여러가지 훅을 사용하는데 그 중, 아마 가장 많이 사용하는 훅이 useState일 것이다.
    • useState를 이용해서 컴포넌트의 상태를 변화시키면 변화를 감지하여 컴포넌트를 리렌더링하기 위해 씀
  • 가끔.. useState 관련해서 깊게 모른다는 생각이 들곤 하는데.. 잘 몰라도 사용법만 알면 잘 작동하기 때문에 그냥 넘어간적이 한두번이 아니였던 것 같기도 하다 ;_;
  • useState, react state에 대해 여러가지 관련된 키워드들에 좀 더 자세히 알아보자 =)
    • immutability, closure, re-rendering, Fiber, reconciliation, 등등...
    • 대충 아는척 할 수는 있지만 제대로 모르는 것 같아서 글로 기록해본다!

useState 기본 지식

useState의 동작 원리

  1. 초기 상태 설정: useState는 컴포넌트가 렌더링될 때 호출되며, 초기 상태값을 설정한다
    • state: 현재 상태 값을 나타낸다.
    • setState: 상태를 업데이트하는 함수.
    • initialValue: 초기 상태값을 설정.
      const [state, setState] = useState(initialValue)
  2. React 내부에서 상태 관리: React는 내부적으로 컴포넌트의 상태와 관련된 정보를 "state queue"라는 구조로 관리한다.
    • useState는 렌더링이 발생할 때 동일한 순서로 호출되어야 하며, React는 호출 순서를 기반으로 각 상태를 연결한다.
  3. 상태 업데이트: setState 함수를 호출하면, React는 내부적으로 상태를 업데이트하고, 컴포넌트를 다시 렌더링합니다.
    • React는 상태값을 즉시 변경하지 않고 배치(batch) 처리합니다. 이는 성능 최적화를 위해 한꺼번에 여러 상태 업데이트를 처리하기 위함이다.
    • 상태 업데이트가 비동기적 특성을 가지므로, 새로운 상태값을 기반으로 기존 상태를 변경하려면 함수형 업데이트를 사용한다
      setState((prevState) => prevState + 1);
  4. 렌더링과 관계: 상태가 변경되면 React는 해당 컴포넌트를 다시 렌더링하며, 렌더링 과정에서 새로운 state 값이 반영된다.

상태 업데이트와 리렌더링 원리

React의 상태 관리와 렌더링

  • React는 컴포넌트를 "순수 함수"처럼 동작하도록 설계되었다.
    • 컴포넌트는 같은 props와 state가 주어지면 항상 동일한 UI를 반환한다.
  • 상태(state)가 변경되면 React는 컴포넌트를 다시 호출하여 새로운 UI를 계산한다.

useState가 리렌더링을 트리거하는 방식

  • useState의 setState 함수가 호출되면 React는 내부적으로 상태 변경을 감지하고, 해당 상태를 사용하는 컴포넌트를 다시 렌더링한다
  • React는 이 작업을 위해 Fiber 구조와 Scheduler API를 활용한다. 이를 통해 상태 변경이 발생한 컴포넌트를 다시 렌더링 대기열에 추가된다.

내부적으로 어떤 API가 작동할까?

  • React에서 상태 업데이트 시 리렌더링이 발생하는 이유는 React의 내부 동작 방식 때문이다.

1. Fiber 구조

  • React는 모든 컴포넌트를 Fiber 노드라는 데이터 구조로 관리한다.
  • 각 컴포넌트는 자신의 상태와 UI를 포함하는 Fiber 객체로 표현된다.
  • setState가 호출되면 React는 변경된 상태를 Fiber 트리에 기록하고, 해당 컴포넌트를 다시 렌더링할 준비를 한다.

2. Dirty Checking

  • React는 상태 변경이 발생하면 해당 컴포넌트를 "dirty" 상태로 표시한다.
  • Dirty 상태로 표시된 컴포넌트만 렌더링 대기열에 추가하여 최적화된 렌더링을 수행한다.

3. Virtual DOM Reconciliation

  • useState로 인해 상태가 업데이트되면 React는 새로운 Virtual DOM을 생성하고, 이전 Virtual DOM과 비교한다.
  • 이 과정을 "Reconciliation"이라고 하며, 변경된 부분만 실제 DOM에 반영한다.

4. 스케줄링과 렌더링

  • React는 상태 변경을 감지하면 렌더링 작업을 스케줄링한다.
  • React의 Scheduler는 최적의 렌더링 시점을 결정한다.
    • 예를 들어, 유저 인터랙션(클릭, 입력 등)이 중요한 경우 우선 순위를 높이고, 백그라운드 작업은 나중에 처리한다.

setState가 리렌더링을 트리거하는 과정

1. setState 호출

  • 사용자가 setState를 호출하면, React는 상태 변경을 Fiber 트리에 기록한다.

2. Fiber 노드 업데이트

  • React는 해당 컴포넌트를 "dirty" 상태로 표시한다.
  • "dirty" 상태는 이 컴포넌트의 상태나 props가 변경되었음을 의미한다.

3. 렌더링 대기열 추가

  • React는 dirty 상태의 컴포넌트를 렌더링 대기열에 추가한다.

4. Virtual DOM 생성, 비교, 업데이트

  • React는 컴포넌트를 다시 호출하여 새로운 Virtual DOM을 생성한다.
  • 이전 Virtual DOM과 비교하여 변경된 부분만 실제 DOM에 반영한다("Reconciliation").

5. 실제 DOM 업데이트

  • React는 변경된 부분만 효율적으로 업데이트한다

불변성(immutability)

 불변성은 데이터 구조가 변경되지 않고, 수정이 필요할 때는 기존 값을 복사한 뒤 새로운 데이터로 변경된 구조를 반환하는 원칙이다다.

  • 가변성(mutable):
let arr = [1, 2, 3]; arr.push(4); // arr는 [1, 2, 3, 4]로 변경됨
  • 불변성(immutable):
let arr = [1, 2, 3]; let newArr = [...arr, 4]; // 새로운 배열 [1, 2, 3, 4] 생성, arr는 변경되지 않음

 

 

React와 불변성의 관계!

  • React는 상태 관리와 렌더링에서 불변성을 기반으로 최적화를 수행한다.
  • 상태가 변경되었는지 확인하기 위해 얕은 비교(shallow comparison)를 사용하며, 이 과정에서 불변성의 원리가 적용된다

React의 상태 비교 방식

  • React는 이전 상태와 새로운 상태를 비교하여 상태가 변경되었는지 판단한다.
  • 얕은 비교로 이전 상태와 새로운 상태가 다른지 확인한다.
    • 새로운 상태가 동일한 참조를 가지면 변경되지 않았다고 판단
    • 새로운 상태가 다른 참조를 가지면 변경되었다고 판단

불변성을 유지하지 않을 경우의 문제

  • 상태를 직접 수정하면 참조가 동일하게 유지되기 때문에 React는 상태 변경을 감지하지 못하고 컴포넌트를 리렌더링하지 않는다!
const [state, setState] = useState({ count: 0 });

// 상태를 직접 변경 (불변성 위반)
state.count = 1;
setState(state); // React는 상태가 변경되었다고 인식하지 못함

 

불변성을 유지하는 상태 업데이트 방법

 

1. 참조형 업데이트

  • 참조형 상태를 업데이트할 때는 반드시 새로운 참조형 반환값 생성해야 한다
    • 새로운 주소를 가진 값이란 소리에용
 
const [user, setUser] = useState({ name: "duckgugong", age: 444 });

// 새로운 객체를 생성하여 업데이트
const updateAge = () => {
  setUser((prevUser) => ({
    ...prevUser, // 이전 상태를 복사
    age: prevUser.age + 1, // 변경된 부분만 수정
  }));
};

2. 원시형 업데이트

  • 원시값은 값 자체로 비교된다
    • 원시값은 본질적으로 불변성을 가진다.
      • 새로운 값을 생성하면 기존 값을 변경하지 않고 새로운 메모리 공간에 저장된다.
    • 따라서 변경될 값을 넣기만 하면 올바른 상태 업데이트를 할 수 있다

 


useState와 클로져

  • useState와 클로저(closure)는 밀접한 관계가 있다.
  • useState는 함수형 컴포넌트에서 사용되며, 상태 관리와 업데이트가 클로저의 특성을 활용하기 때문이다.

클로저란?

  • 클로저는 자신을 생성한 스코프(scope)의 변수에 접근할 수 있는 함수를 말한다.
  • 즉, 함수가 선언될 당시의 환경(lexical environment)을 기억하여, 해당 스코프의 변수에 접근하거나 이를 유지할 수 있다.
function makeCounter() {
  let count = 0; // 외부 변수

  return function () {
    count++;
    return count;
  };
}

const counter = makeCounter();
console.log(counter()); // 1
console.log(counter()); // 2
  • 여기서 count는 makeCounter 함수의 스코프 내에 있지만, 반환된 함수가 클로저로서 이를 계속 기억하고 접근한다.
    • 간단한 useState 예시로 볼 수도 있엉

 

useState와 클로저의 관계

  • React의 useState는 함수형 컴포넌트에서 상태를 업데이트하는 방법을 제공하는뎅 이 과정에서 클로저가 중요한 역할을 한다.

상태 유지와 클로저

  • useState로 생성된 상태(state)는 함수형 컴포넌트가 다시 렌더링되더라도 초기화되지 않는다
  • 이는 useState가 내부적으로 클로저를 사용해 이전 상태 값을 기억하고 관리하기 때문이다
function Counter() {
  const [count, setCount] = useState(0);

  const increment = () => {
    setCount(count + 1); // `count`는 클로저로 캡처된 값
  };

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={increment}>Increment</button>
    </div>
  );
}
  • increment 함수는 렌더링 시마다 새로 생성되지만 count 변수는 클로저로 캡처된 당시의 값을 참조!

클로저로 인한 상태 참조 문제

  • useState와 클로저의 관계 때문에, 상태가 최신값이 아닌 이전 값을 참조하는 문제가 생길 수 있습니다.
function Counter() {
  const [count, setCount] = useState(0);

  const handleClick = () => {
    setTimeout(() => {
      setCount(count + 1); // count는 렌더링 당시의 값을 기억
    }, 1000);
  };

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={handleClick}>Increment</button>
    </div>
  );
}

 

  • 위 코드는 setTimeout 안에서 클로저로 캡처된 count 값을 사용하기 때문에, 버튼 클릭 후 1초 뒤에 업데이트된 값이 아닌 렌더링 당시의 값을 기준동작한다.

해결 방법: 함수형 업데이트

  • 함수형 업데이트를 사용하면 이전 상태를 안전하게 참조할 수 있다.
function Counter() {
  const [count, setCount] = useState(0);

  const handleClick = () => {
    setTimeout(() => {
      setCount((prevCount) => prevCount + 1); // 최신 상태를 안전하게 업데이트
    }, 1000);
  };

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={handleClick}>Increment</button>
    </div>
  );
}

왜 클로저가 중요한가?

  1. React의 상태 유지 메커니즘:
    • React는 컴포넌트가 다시 렌더링되더라도 이전 상태와 함수를 유지한다. 이때 상태와 관련된 함수들이 클로저를 통해 렌더링 당시의 값을 기억한다.
  2. 상태 동기화:
    • setState와 같은 상태 업데이트 함수는 클로저로 동작하며, 이전 상태값을 안전하게 참조하거나 업데이트한다.
  3. 렌더링과 독립적 로직:
    • React의 렌더링이 반복될 때, 상태 업데이트 함수(setState)는 동일한 참조를 유지하며 클로저의 특성을 활용해 상태를 관리한다.
 

 

클로저를 이용해서 간단하게 useState 구현해보기

  • React처럼 상태를 관리하려면, 클로저와 외부 저장소를 이용해 상태값을 유지해야 한다
function createUseState() {
  let state; // 상태값을 저장할 변수

  // useState 함수
  return function useState(initialValue) {
    // 초기화는 최초 호출 시에만 수행
    if (state === undefined) {
      state = initialValue;
    }

    // 상태를 업데이트하는 함수
    function setState(newValue) {
      state = newValue; // 새로운 값으로 상태 변경
      console.log("State updated to:", state); // React에서는 여기서 re-rendering 트리거됨
    }

    return [state, setState];
  };
}

// useState 함수 생성
const useState = createUseState();

// 컴포넌트처럼 사용하는 예시
function Counter() {
  const [count, setCount] = useState(0);

  console.log("Rendered with count:", count);

  return {
    increment: () => setCount(count + 1),
    decrement: () => setCount(count - 1),
  };
}

// Counter 컴포넌트의 사용
const counter = Counter(); // 렌더링
counter.increment(); // count + 1
counter.increment(); // count + 1
counter.decrement(); // count - 1