본문 바로가기

리액트 심화 스터디

[Week 1] snap state, Context, Virtual Dom, Reconciliation

1. snap state

setState 함수를 이용하여 상태를 업데이트하면 현재의 상태를 즉시 변경시키지 않고 리렌더링을 일으킨다.

즉, 현재 시점의 값을 변경시키는 것이 아니라 다음 리렌더링에서 반영해야 하는 상태를 전달하는 것이다.

예시를 들어 설명하면 쉽다.

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만 증가한다.

이는 버튼이 클릭된 시점의 number 값이 0이기 때문이며, setNumber(number + 1)를 여러 번 호출해도

리렌더링은 모든 setNumber() 호출이 완료된 후에 발생하므로 결과적으로는 1만 증가하게 된다.

 

이처럼 리액트는 동일한 이벤트 핸들러 내에서 발생하는 state 업데이트를 묶어 한 번의 리렌더링만 발생시키는데,

이를 Batching(일괄 처리)이라고 한다.

 

이러한 일괄 처리를 해소하는 방법은 다음과 같다.

import { useState } from 'react';

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

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

 

리액트는 이벤트 핸들러의 다른 코드가 모두 실행되고 그 후에 함수가 처리되도록 큐에 추가한다.

그리고 다음 렌더링 시점에 큐에 쌓인 업데이트 함수를 하나씩 순회하며 최종적으로 업데이트된 state 값을 제공한다.

따라서, setNumber(n => n + 1) 함수는 현재 큐에 있는 상태를 기준으로 다음 상태를 계산하므로

동일한 state 변수에 대해 여러 번의 업데이트를 호출해도 다음 렌더링 전까지 상태가 올바르게 반영된다.

따라서, 위 코드에서 number 값은 기대한 대로 3 만큼 증가한다.

 

2. Context

 

상위 컴포넌트의 데이터를 props를 통해 하위 컴포넌트에 넘겨주다보면 불편한 점이 생긴다.

바로 중첩된 여러 계층의 컴포넌트에 props를 전달해야 할 경우,

해당 props를 사용하지 않는 컴포넌트들까지 불필요하게 데이터를 전달 받는다는 것이다.

이러한 문제를 Prop drilling이라고 한다.

 

이 문제를 해결해주는 것이 바로 Context이다.

리액트의 Context를 사용하면 props를 사용하지 않고 필요한 데이터를 전역적으로 공유할 수 있다.

이는 리액트에 내장된 전역 상태 관리 도구이기 때문에 별도로 설치할 필요 없다.

 

📌 Context 사용하기

1. createContext 메서드를 사용해 Context 생성하기

import { createContext } from 'react';

export const LevelContext = createContext(1);

 

2.  useContext 메서드를 사용해 Context 사용하기

import { useContext } from 'react';
import { LevelContext } from './LevelContext.js';

export default function Heading({ children }) {
  const level = useContext(LevelContext);
  // ...
}

 

3. context provider로 Context 제공하기

import { LevelContext } from './LevelContext.js';

export default function Section({ level, children }) {
  return (
    <section className="section">
      <LevelContext.Provider value={level}>
        {children}
      </LevelContext.Provider>
    </section>
  );
}

 

🚨 Context 사용 시 주의사항

 

여러 컴포넌트에 내려주며 데이터를 전달할 필요 없이 전역적으로 공유할 수 있기 때문에 굉장히 간편해보이지만 주의사항이 있다.

바로, Provider의 부모 컴포넌트가 렌더링될 때마다 하위 컴포넌트들이 불필요하게 다시 렌더링된다는 문제이다.

만약, 리액트 context provider에 객체를 내려주고 있고 그 객체의 프로퍼티가 업데이트된다면,

그 context를 사용하고 있는 모든 컴포넌트에서 리렌더링이 일어난다.

따라서, 테마 데이터처럼 업데이트가 자주 되지 않는 경우에 Context를 사용하는 것이 좋다.

 

3. Virtual DOM

📌 DOM (Document Object Model)

웹 페이지를 이루는 태그들을 트리 형태로 표현한 객체 모델

 

📌 DOM에 변화가 생기면?

웹 브라우저는 DOM에 변화가 생기면 그 변화를 반영하기 위해 페이지를 리렌더링 한다. 이때, 실제 DOM을 조작한다면, 렌더링을 할 때마다 Render Tree를 생성하고, 레이아웃을 구성하고, CSS를 다시 연산해야 하기 때문에 비용이 많이 들고 성능 저하를 일으킬 수 있다.

따라서, 리액트는 DOM 조작을 최소화하여 작업을 처리하고 리렌더링하기 위해 가상 DOM을 만들어 비교하는 방식을 적용했다.

 

📌 Virtual DOM

가상 돔(Virtual DOM)이란, 브라우저에 실제로 보여지는 DOM이 아닌 메모리에 가상으로 존재하는 DOM을 말한다.

리액트는 이를 활용하여 실제 DOM을 조작하는 방식이 아닌, 실제 DOM을 모방한 가상의 DOM을 구성해 원래의 DOM과 비교하여 변화한 부분만 리렌더링 하는 방식으로 작동한다.

 

📌 Virtual DOM의 원리

  1. 업데이트한 전체 UI를 Virtual DOM에 리렌더링
  2. Diffing Algorithm을 활용해 실제 DOM과 생성된 Virtual DOM을 비교
  3. 업데이트한 부분만 실제 DOM에 반영

 

4. Reconciliation

기존의 가상 DOM과 변화가 생긴 가상 DOM을 비교하고 변화를 반영하는 과정

 

📌 비교 알고리즘 (Diffing Algorithm)

두 개의 트리를 비교할 때, 리액트는 두 엘리먼트의 루트 엘리먼트부터 비교한다.

  • 엘리먼트의 타입이 다른 경우 : 이전 트리를 버리고 새로운 트리를 구축
<div>
	<Counter />
</div>

<span>
	<Counter />
</span>
  • 엘리먼트의 타입이 같은 경우 : 두 엘리먼트의 속성을 확인하여, 동일한 내역은 유지하고 변경된 속성만 갱신
<div className="before" title="stuff" />

<div className="after" title="stuff" />
  • DOM 노드의 처리가 끝나면, 해당 노드의 자식들을 재귀적으로 처리한다.

 

📌 자식에 대한 재귀적 처리

DOM 노드의 자식들을 재귀적으로 처리할 때, 기본적으로 동시에 두 리스트를 순회하고 차이점이 있으면 변경된 부분을 갱신한다.

 

 

https://ko.react.dev/learn/state-as-a-snapshot

https://ko.react.dev/learn/passing-data-deeply-with-context

https://ko.legacy.reactjs.org/docs/reconciliation.html