2장 리액트 핵심요소 깊게 살펴보기 (3)

2.4 렌더링은 어떻게 일어나는가? 2.5 컴포넌트와 함수의 무거운 연산을 기억해 두는 메모이제이션
강석우's avatar
Mar 04, 2024
2장 리액트 핵심요소 깊게 살펴보기 (3)

2.4 렌더링은 어떻게 일어나는가?

브라우저에서의 렌더링이란 쉽게 말해 HTML과 CSS 리소스를 기반으로 웹페이지에 필요한 UI를 그리는 과정을 의미한다.

2.4.1 리액트의 렌더링이란?

리액트에서의 렌더링이란 리액트 애플리케이션 트리 안에 있는 모든 컴포넌트들이 현재 자신들이 가지고 있는 props와 state의 값을 기반으로 어떻게 UI를 구성하고 이를 바탕으로 어떤 DOM 결과를 브라우저에 제공할 것인지 계산하는 일련의 과정을 의미한다.

2.4.2 리액트의 렌더링이 일어나는 이유

  1. 최초 렌더링

  2. 리렌더링 : 최초 렌더링 이후 일어나는 모든 렌더링을 의미한다.

    1. 클래스 컴포넌트의 setState가 실행되는 경우

    2. 클래스 컴포넌트의 forceUpdate가 실행되는 경우

    3. 함수 컴포넌트의 useState()의 두 번째 배열 요소인 setter가 실행되는 경우

    4. 함수 컴포넌트의 useReducer()의 두 번째 배열 요소인 dispatch가 실행되는 경우

    5. 컴포넌트의 key props가 변경되는 경우

      1. 리액트에서 배열을 사용하게 되면 key 를 입력하지 않았다고 warning이 뜨게 되는 경우가 많은데 이유는 여기에 있다.

      2. 리렌더링이 발생하면 current 트리와 workInProgress 트리 사이에서 어떠한 컴포넌트가 변경이 있었는지 구별해야 하는데, 이 두 트리 사이에서 같은 컴포넌트인지 구별하는 값이 바로 key다.
        useMemo 를 사용해 선언한 값은 배열에 key를 넣지 않은 경우 useState의 호출에도 변경되지 않는데 key 를 랜덤값으로 넣어준다면 컴포넌트 구분이 명확하게 되지 않기 때문에 key값이 변경될 때마다 리렌더링을 일으키게 된다.

    6. props가 변경되는 경우

    7. 부모 컴포넌트가 렌더링될 경우

위에 나열한 경우 이외의 경우에는 절대로 리렌더링이 일어나지 않는다. 따라서 useState로 선언하지 않은 변수는 아무리 변경된다 하여도 리렌더링을 시키지 않아 DOM에서 확인할 수가 없다.

2.4.3 리액트의 렌더링 프로세스

렌더링 결과물은 JSX 문법으로 구성돼 있고, 이것이 자바스크립트로 컴파일되면서 React.createElement()를 호출하는 구문으로 변환된다.
렌더링 프로세스가 실행되며 각 컴포넌트의 결과물을 수집한 다음, 리액트의 새로운 트리인 가상 DOM과 비교하여 실제 DOM에 반영하기 위한 변경사항들을 차례차례 수집한다.

2.4.4 렌더와 커밋

리액트의 렌더링은 렌더단계와 커밋단계 이렇게 두개로 나뉘게 된다.

렌더단계

렌더링 프로세스에서 컴포넌트를 실행해(render() 또는 return) 이 결과와 이전 가상 DOM을 비교하는 과정을 거쳐 변경이 필요한 컴포넌트를 체크하는 단계이며 주요 비교점은 type, props, key이다. 이 세가지 중 한가지라도 변경사항이 있다면 변경이 필요한 컴포넌트로 체크된다.

커밋단계

렌더 단계의 변경 사항을 실제 DOM에 적용해 사용자에게 보여주는 과정을 말한다.
리액트가 먼저 DOM을 커밋 단계에서 업데이트한다면 이렇게 만들어진 모든 DOM 노드 및 인스턴스를 가리키도록 리액트 내부의 참조를 업데이트하고 생명주기 개념이 있는 클래스 컴포넌트에서는 componentDidMount, componentDidUpdate 메서드를 호출하고, 함수 컴포넌트에서는 useLayoutEffect 훅을 호출한다.

이 두 가지의 렌더링 방식은 항상 동기식으로 작동한다. 따라서 렌더링이 오래걸릴수록 애플리케이션의 성능 저하로 이어진다.

2.4.5 일반적인 렌더링 시나리오 살펴보기

import { useState } from "react";
export default function A() {
  return (
    <div className="App">
      <h1>Hello React!</h1>
      <B />
    </div>
  );
}
function B() {
  const [counter, setCounter] = useState<number>(0);
  function handleButtonClick() {
    setCounter((previous) => previous + 1);
  }
  return (
    <>
      <label>
        <C number={counter} />
      </label>
      <button onClick={handleButtonClick}>+</button>
    </>
  );
}

function C({ number }: { number: number }) {
  return (
    <div>
      {number} <D />
    </div>
  );
}
function D() {
  return <>리액트 재밌다!</>;
}

위의 코드에 대한 시나리오를 살펴보자

  1. B 컴포넌트의 setState가 호출된다.

  2. B 컴포넌트의 리렌더링 작업이 렌더링 큐에 들어간다.

  3. 리액트는 트리 최상단에서부터 렌더링 경로를 검사한다.

  4. A 컴포넌트는 리렌더링이 필요한 컴포넌트로 표시돼 있지 않으므로 별다른 작업을 하지 않는다.

  5. 그다음 하위 컴포넌트인 B 컴포넌트는 업데이트가 필요하다고 체크돼 있으므로 B를 리렌더링한다.

  6. 5번 과정에서 B는 C를 반환했다.

  7. C는 props인 number가 업데이트됐다. 그러므로 업데이트가 필요한 컴포넌트로 체크돼 있고 업데이트한다.

  8. 7번 과정에서 C는 D를 반환했다.

  9. D도 마찬가지로 업데이트가 필요한 컴포넌트로 체크되지 않았다. 그러나 C가 렌더링됐으므로 그 자식인 D도 렌더링됐다.

여기에서 D에 memo를 추가해준다면 props가 변경되지 않았기 때문에 리렌더링이 일어나지 않는다.

2.5 컴포넌트와 함수의 무거운 연산을 기억해 두는 메모이제이션

꼭 필요한 곳에만 메모이제이션 vs 모조리 메모이제이션

메모이제이션은 비용이 들어가기때문에 silver bullet이 절대로 아니다.
하지만 이 책의 저자분께서는 만일 공부를 deep 하게 해서 낭비하는 메모리 없이 메모이제이션을 사용하겠다 하면 "꼭 필요한 곳에만 메모이제이션"을 하고 그게 아니라면 "모조리 메모이제이션" 해버리는 것도 방법이라고 말했다.

Share article

석우의 개발블로그