React - React의 렌더링 방식 : State

반응형

State가 변경될 때 React는 어떻게 렌더링할까?

React는 가상 DOM (Virtual DOM)을 기반으로 효율적인 렌더링을 수행하는 프레임워크이다.

※가상 (Virtual) DOM ?
리액트가 효율적으로 UI를 업데이트하기 위해 사용하는 가상의 DOM 구조이다.
기존 웹 브라우저는 일반적인 HTML DOM에서 직접 수정하면 속도가 느려지고
DOM 조작이 많아질 수록 성능이 크게 저하되는데
여기서 가상 DOM의 역할이 중요시하게 된다.
리액트는 가상 DOM을 메모리에 저장하고 실제로 DOM을 직접 조작하지 않으며
여기서 놀라운점은 상태(state)변경이 발생하면 가상 DOM에서 먼저 변경된 부분을 적용한 후,
실제 DOM과 비교하여 최소한의 변경만 적용한다.
이로써, 브라우저의 성능을 최적화하면서도 빠른 UI 업데이트가 가능하다.

어느정도 가상 DOM을 이해했다면, state가 변경될 때 리액트가 렌더링하는 방식을 알아보자


1. React의 기본 렌더링 원리

React 컴포넌트는 propsstate에 따라 UI를 결정하는 것은 지난 글들을 봤다면 알고 있을 것이다.

즉 props 또는 state가 변경되면 컴포넌트가 다시 렌더링된다.

import { useState } from "react"

function Counter() {
  const [count, setCount] = useState(0)
  return (
    <div>
      <h2>카운트 : {count} </h2>
      <button onClick={() => setCount(count + 1)}>증가</button>
    </div>
  )
}

//
export default Counter

setCount(count + 1)이 실행되면 리액트는 컴포넌트를 다시 렌더링 하지만

단순히 DOM을 다시 그리는 것이 아니라 최적화된 방식으로 렌더링한다.

다음 과정을 살펴보면

 

▼ React의 렌더링 과정 (State가 변경될 때의 흐름)

1) State가 변경됨 (setState 호출)

  • setState가 실행되면 리액트는 해당 컴포넌트의 새로운 state 값을 계산한다.
  • state 값이 변경되었을 때만 다음 단계로 진행.

2) 변경된 state로 새로운 가상 DOM 생성

  • 리액트는 기존 가상 DOM을 복사한 후 새로운 state값을 반영한
    새로운 가상 DOM을 생성한다.

3) 기존 가상 DOM과 새로운 가상 DOM을 비교 (Diffing 알고리즘)

  • 리액트는 변경된 부분만 찾아서 업데이트하는 디핑 알고리즘 (Diffing)을 사용한다.
  • 가상 DOM을 비교하여 어떤 요소를 변경해야하는지 확인한다.

4) 실제 DOM을 업데이트 (Reconciliation - 조정 과정)

  • 변경된 부분만 실제 DOM에 적용한다.
  • 전체 DOM을 다시 그리는 것이 아닌 최소한의 변경만 반영하여 성능을 최적화.
※ 디핑 (Diffing) 알고리즘?
디핑 (Diffing)은 비교를 뜻하는 단어로, 변경된 부분을 찾아내는 알고리즘을 의미한다.
새로운 가상 DOM기존 DOM을 비교하여 변경된 부분을 찾는 과정이다.
이 과정을 통해 최소한의 DOM 업데이트가 이루어진다.

2. React 렌더링 과정 예제

다음 코드는 state 변경으로 인한 렌더링의 흐름을 예시로 나타냈다.

import { useState } from "react";

function Example() {
  console.log('컴포넌트 랜더링'); // 렌더링될 떄마다 실행됨

  const [count, setCount] = useState(0);
  return (
    <div>
      <h2>카운트: {count}</h2>
      <button onClick={() => setCount(count + 1)}>증가</button>
    </div>
  )
}
export default Example

etc-image-0etc-image-1

처음 실행될 때 "컴포넌트 렌더링: {count:0}",

버튼을 클릭하여 setCount(count +1) 실행하면 "컴포넌트 렌더링: {count:1}"

또 버튼을 클릭하면 "컴포넌트 렌더링: {count:2}"

즉, state가 변경될 떄마다 컴포넌트가 다시 렌더링된다.


3. 불필요한 렌더링 방지

부모 컴포넌트가 렌더링 될 때 자식 컴포넌트도 함께 렌더링되는 경우가 있다.

원래 리액트의 기본 동작 방식이지만 불필요한 리렌더링이 발생하면

성능 저하를 유발할 수 있다.

다음 코드의 문제를 살펴보자.

 

▼ 부모가 리렌더링될 때 자식도 함께 리렌더링 되는 경우

import { useState } from "react"

function Parent() {
  const [count, setCount] = useState(0);
  console.log('부모 컴포넌트 렌더링');

  return (
    <div>
      <h2>부모 컴포넌트</h2>
      <button onClick={() => setCount(count + 1)}>부모 카운트 증가</button>
      <Child />
    </div>
  )
}

function Child() {
  console.log('자식 컴포넌트 렌더링');

  return <h3>자식 컴포넌트</h3>
}

export default Parent

초기 렌더링은 다음과 같다.

etc-image-2

부모의 setCount 버튼을 클릭하면

etc-image-3

count 값이 변경되었으므로 부모는 다시 렌더링 되는 것은 맞는데

자식은 변경된 내용이 없는데도 부모와 같이 다시 렌더링 되는 것을 확인할 수 있다.

즉 부모의 count가 변경되면 자식도 함께 렌더링 되지만

자식의 내용은 전혀 바뀌지 않으며 불필요한 렌더링이 발생하고 있음을 알 수 있다.

 

▼ 불필요한 렌더링 해결 방법 : React.memo()

React.memo()를 사용하면 props가 변경되지 않는 한

자식 컴포넌트를 다시 렌더링하지 않는다.

import { memo } from "react";
import { useState } from "react"

function Parent() {
  const [count, setCount] = useState(0);
  console.log('부모 컴포넌트 렌더링');

  return (
    <div>
      <h2>부모 컴포넌트</h2>
      <button onClick={() => setCount(count + 1)}>부모 카운트 증가</button>
      <MemoizedChild />
    </div>
  )
}

// React.memo() 적용 (불필요한 리렌더링 방지)
function Child() {
  console.log('자식 컴포넌트 렌더링');

  return <h3>자식 컴포넌트</h3>
}

const MemoizedChild = memo(Child)
export default Parent

etc-image-4

위와 같이 자식은 처음을 제외한 이후 렌더링되지 않는 것을 학인할 수 있다.

 

▼ React.memo로 해결되지 않는 경우 - useCallback

만약에 부모에서 자식으로 함수 props로 전달하는 경우

React.memo() 만으로는 최적화되지 않는다.

다음은 문제가 발생한 코드이다.

import { memo } from "react";
import { useState } from "react"

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

  // handleClick 함수가 부모가 리렌더링 될 때마다 새로 생성됨
  const handleClick = () => {
    console.log('버튼 클릭됨');

  }
  console.log('부모 컴포넌트 렌더링');

  return (
    <div>
      <h2>부모 컴포넌트 카운트 {count}</h2>
      <button onClick={() => setCount(count + 1)}>부모 카운트 증가</button>
      <Child handleClick={handleClick} />
    </div>
  )
}

const Child = memo(({ handleClick }) => {
  console.log("자식 컴포넌트 렌더링");
  return <button onClick={handleClick}>자식 버튼</button>;
});

export default Parent;

etc-image-5etc-image-6

React.memo를 사용했음에도 handleClick 함수가

부모가 리렌더링될 때마다 새로 생성되므로 Child도 계속 리렌더링되는 현상이 생긴다.

이럴떄 useCallback을 사용하여 부모가 리렌더링되더라도

같은 함수 객체를 유지할 수 있다.

다음과 같이 useCallback을 사용하여 함수가 새로 생성되지 않도록 최적화한다.

import { useCallback } from "react";
import { memo } from "react";
import { useState } from "react"

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

  // useCallback 사용하여 함수가 새로 생성되지 않도록 최적화
  const handleClick = useCallback(() => {
    console.log('버튼 클릭됨');

  }, []);
  console.log('부모 컴포넌트 렌더링');

  return (
    <div>
      <h2>부모 컴포넌트 카운트 {count}</h2>
      <button onClick={() => setCount(count + 1)}>부모 카운트 증가</button>
      <Child handleClick={handleClick} />
    </div>
  )
}

const Child = memo(({ handleClick }) => {
  console.log("자식 컴포넌트 렌더링");
  return <button onClick={handleClick}>자식 버튼</button>;
});

export default Parent;

etc-image-7

이렇게하여 부모가 setCount로 리렌더링되더라도 Child는 다시 렌더링되지 않으며

useCallback 덕분에 handleClick 함수가 매번 새로 생성되지않는다.

 

즉 부모가 리렌더링될 때 자식이 불필요하게 렌더링되지 않도록 하려면

React.memo와 useCallback을 함께 사용해야한다.

 

반응형

'React' 카테고리의 다른 글

React - useRef  (0) 2025.03.12
React- React에서 인라인 스타일 사용하기  (0) 2025.02.28
React - State  (0) 2025.02.26
React - children props  (0) 2025.02.21
React - props  (0) 2025.02.20