웹 시스템 개발 #React State(2)

학교 공부를 복습할 겸 적는 것이기에 내용이 부족할 수 있습니다.

 

부족한 것은 상관 없으나, 잘못된 부분이 발견된다면 지적해주시면 감사하겠습니다.

 


렌더링과 커밋 복습

  1. 트리거: 초기 렌더링(앱 시작)과 컴포넌트 상태 업데이트.
  2. 렌더링: 트리거 후, React는 화면에 무엇을 표시할지 결정하기 위해 컴포넌트를 호출합니다. "렌더링"은 React가 컴포넌트를 호출하는 과정입니다.
    • 초기 렌더링 시, React는 root 컴포넌트를 호출합니다.
    • 상태 업데이트가 발생한 후의 재렌더링에서는 해당 상태 업데이트를 트리거한 함수 컴포넌트를 호출합니다.
    • 업데이트된 컴포넌트가 다른 컴포넌트를 반환하면, React는 그 다음 컴포넌트를 렌더링합니다. 이 과정은 더 이상 중첩된 컴포넌트가 없을 때까지 계속됩니다.
  3. 커밋: React는 DOM에 변경 사항을 커밋합니다.
    • 컴포넌트를 호출(렌더링)한 후, React는 DOM을 수정합니다.
    • 초기 렌더링에서는 appendChild() DOM API를 사용하여 모든 DOM 노드를 넣습니다.
    • 재렌더링에서는 최신 렌더링 출력과 일치하도록 필요한 최소한의 연산을 적용합니다(렌더링 사이에 차이가 있을 때만 DOM 노드를 변경합니다).

 

렌더링과 스냅샷에 대한 깊은 이해

“렌더링”은 React가 컴포넌트(함수)를 호출하는 것을 의미합니다. 그 함수에서 반환하는 JSX는 시간에 따른 UI의 스냅샷과 같으며 props, 이벤트 핸들러, 로컬 변수들은 렌더링 시점의 상태를 사용하여 계산됩니다.

 

스냅샷 작동 방식에 대한 실험

import React, { 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)을 연속으로 세 번 호출하면 상태가 3이 아닌 1로 업데이트되는 이유는 React가 상태 업데이트를 일괄 처리하는 방식과 JavaScript 함수의 폐쇄 특성에 있습니다.

 

setNumber(number + 1)을 호출하면 상태 업데이트를 예약하지만 React는 업데이트를 즉시 수행하지 않습니다. 성능상의 이유로 업데이트를 일괄 처리하고 비동기식으로 작동하기 때문입니다.

 

클릭할 때마다 카운터가 '3'씩 증가하도록 하려면 'setNumber'와 함께 업데이트를 사용해야 합니다. 'useState'의 setter 함수의 기능적 업데이트 형식을 사용하면 이전 상태에 직접 액세스하고 업데이트된 값을 반환할 수 있습니다.

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

 

 

상태 변수 및 렌더

import { useState } from 'react';

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

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

 

  1. 렌더링 중 상태 불변: 단일 렌더링 주기 내에서 상태 변수의 값은 불변입니다. setNumber를 사용하여 상태 업데이트를 예약하더라도 number의 현재 값은 해당 렌더링 주기의 나머지 기간 동안 변경되지 않습니다.
  2. 비동기 업데이트: React 상태 업데이트는 비동기적이기에 상태를 업데이트하면 즉각적인 변경이 아니라 상태 변경을 예약하는 것입니다.
  3. 이벤트 핸들러의 상태 스냅샷: 구성 요소와 상호 작용할 때(예: 버튼 클릭) 모든 이벤트 핸들러는 상호 작용 당시의 현재 상태로 호출됩니다. 이벤트 핸들러(예: setTimeout)에 비동기 작업이 있는 경우 비동기 작업이 실행되는 순간의 상태가 아니라 상호 작용 순간의 상태를 캡처합니다.

그렇다면 실시간으로 카운트다운을 하고 싶다면 어떻게 해야 할까요? 이럴 때 사용할 수 있는 것이 useEffect입니다.

useEffect는 다음 시간에 알아볼 개념이므로 여기선 코드만 보고 넘어가겠습니다.

import React, { useState, useEffect } from 'react';

function Counter() {
  const [countdown, setCountdown] = useState(5);

  useEffect(() => {
    if (countdown === 0) return;

    const intervalId = setInterval(() => {
      setCountdown((currentCountdown) => currentCountdown - 1);
    }, 1000);

    return () => clearInterval(intervalId);
  }, [countdown]);

  return (
    <div>
      <h1>Countdown: {countdown}</h1>
    </div>
  );
}

 

이런식으로 countdown이 업데이트될 때 hook을 걸어줄 수 있습니다.

 

React의 대기열 상태 업데이트

  • 일괄 업데이트: 이벤트 핸들러에서 state setter를 호출하면 React는 해당 업데이트를 대기열에 추가하고 단일 배치로 적용하여 성능을 최적화합니다. 
  • 동기식 이벤트 핸들러: React는 대기열 상태 업데이트를 처리하기 전에 이벤트 핸들러의 코드 실행이 완료될 때까지 기다립니다. 즉, 해당 상태 변경에 따른 UI 업데이트는 이벤트 핸들러 코드가 완료될 때까지 발생하지 않습니다.
  • 성능 향상: 상태 업데이트를 일괄 처리하면 React가 DOM에서 수행되는 작업을 최소화하므로 성능이 향상됩니다. 모든 업데이트가 대기열에 추가될 때까지 기다리면 React는 모든 변경 사항을 적용하기 위해 단일 패스를 만들 수 있으므로 앱이 원활하게 실행됩니다.

 

새로운 React 버전

React의 최신 버전(18 이상)에서 React는 이벤트 핸들러 내의 시나리오뿐만 아니라 더 많은 시나리오를 포함하도록 일괄 처리 동작을 개선했습니다. 이러한 업데이트를 통해 React는 setTimeoutsetInterval 및 기본 이벤트 핸들러 중에도 일괄 업데이트를 수행하여 더욱 원활한 UI 업데이트와 더 나은 전체 성능을 제공할 수 있게 되었습니다.

 

기존에는 onClickonChange 등으로 제한되어 있었습니다.

function Counter() {
  const [count, setCount] = useState(0);

  function handleClick() {
    setCount(prevCount => prevCount + 1);
    setCount(prevCount => prevCount + 1);
    setCount(prevCount => prevCount + 1);
  }

  return (
    <button onClick={handleClick}>
      count : {count}
    </button>
  );
}

 

이 Counter 구성 요소에서 handleClick 함수가 실행되면 React는 세 개의 setCount 호출을 일괄 처리하고 이를 단일 재렌더링에 적용합니다. 업데이트는 동일한 기본 상태에 적용됩니다. '3'씩 증가하려면 각 'setCount'는 가장 최근에 업데이트된 상태를 기반으로 해야 합니다.

 

React의 상태 구조

둘 이상의 상태변수가 항상 함께 변하는 경우에는 하나의 상태변수로 결합하는 것이 더 효율적입니다. 

상태 변수가 서로 모순되지 않는지 확인해야 합니다. 예를 들어 'isSending' 및 'isSent' 상태가 있다 가정할 때 동시에 'true'로 잘못 설정될 수 있는 시나리오가 발생할 수 있으므로 가능한 모든 상태('입력 중'전송 중',` 전송 됨')를 나타내는 단일 상태 변수를 사용하면 좋습니다.

 

상태 올리기

  • 구성요소 간 상태 공유:
    • 여러 구성 요소에서 상태를 동기화하려면 상태를 가장 가까운 공통 상위 항목으로 이동한 다음 props를 통해 하위 항목에 전달합니다.
  • 자녀와 부모 간의 의사소통:
    • 하위 구성 요소가 상위 구성 요소와 다시 통신할 수 있도록 함수 props(콜백)을 사용하세요.
import { useState } from 'react'; 

function Accordion() {
  const [activeIndex, setActiveIndex] = useState(0);

  return (
    <>
      <h2>Almaty, Kazakhstan</h2>
      <Panel 
        title="About"
        isActive={activeIndex === 0} 
        onShow={() => setActiveIndex(0)}
      >
        Almaty is Kazakhstan's largest city. From 1929 to 1997, it was its capital city.
      </Panel>
      <Panel 
        title="Etymology" 
        isActive={activeIndex === 1} 
        onShow={() => setActiveIndex(1)}
      >
        The region surrounding Almaty is thought to be the ancestral home of the apple.
      </Panel> 
    </>
  ); 
}

function Panel({ title, children, isActive, onShow }) {
  return (
    <section className="panel"> 
      <h3>{title}</h3>
      {isActive ? 
        (<p>{children}</p>) : 
        (<button onClick={onShow}>Show</button>)
      }
    </section>
  );
}

 

<부모-자식 계층구조입니다>

Accordion (Parent) 

├── Panel (Child)
│   ├── Title: "About"
│   └── isActive, onShow

└── Panel (Child)
    ├── Title: "Etymology"
    └── isActive, onShow

 

  • 공유 상태(activeIndex)Accordion 구성 요소는 어떤 패널이 활성화되어 있는지 추적합니다. 이 상태는 각 Panel 구성요소와 공유됩니다.
  • 상태 및 핸들러를 소품으로 전달: 각 'Panel'은 'isActive' 및 'onShow' props을 받습니다. 'isActive' 소품은 패널의 콘텐츠를 표시해야 하는지 결정하는 데 사용되며, 'onShow'는 'Accordion'의 'activeIndex'를 업데이트하는 콜백입니다.
  • 선언적 UI: 이 구조를 사용하면 Accordion이 DOM을 직접 조작하지 않고도 패널 상태를 선언적으로 제어할 수 있습니다.