React 에서 렌더링이란.

leetrueleetrue
7 min read

👩🏻‍🌾 React 파먹기 - Rendering

최근들어 개념적인 부분, 원리적인 부분들을 공부하다보니 계속 ‘왜?’라는 생각을 하게 되었다. 지금까지 호기심이 많이 없었던게 문제였기도 하겠지만 말이다. 그래서 늦었지만 내가 주로 사용하는 React 를 조금 더 자세히 이해해야겠다는 욕심이 들어서 공부를 시작했다.

한 번에 너무 많은 것을 하면 금방 질릴 것을 알기에, 조금씩 깊게 파보려고 한다.

🥲 나는 렌더링의 개념을 설명할 줄 아는가?

누군가가 나에게 “리액트에서 렌더링이 뭔가요?”라고 질문한다면 나는 자신 있게 설명할 수 있을까? ← 라는 생각을 문득 고향 가는 기차에서 했었는데, 고개를 푹 숙였다. 리액트에서 말하는 렌더링은 단순히 Painting 을 의미하는 것은 아니기 때문일 것이기에.

그래서 이번에는 렌더링에 대해 제대로 이해를 해보기로 했다. 리액트에서 다루는 렌더링은 그 개념이 방대한데, 우선은 전반적인 기초 개념과 순서적인 부분만 다루면서 친해져보자.


🤔 rendering 이란?

리액트에서 이야기하는 렌더링이란 컴포넌트가 현재의 propsstate 의 상태에 기초해서 UI 를 어떻게 구성하면 되는지 컴포넌트에게 요청하는 작업을 의미한다.

아래의 예시를 보면, Components 라는 함수형 컴포넌트가 선언이 되어있고, propsstate 의 값을 이용해 View 를 구성하고 있다. 아래의 컴포넌트를 그려내기 위해서는 컴포넌트에게 “난 화면을 그릴테니, 네가 가지고 있는 props 와 state 를 알려줘!” 라는 단계가 필요하다는 의미이다.

import { useState } from "react";

const Component = (props) => {
    const [age, setAge] = useState<number>(20);

    return <div>{`Hello, my nams is ${props.name} and i'm ${age} years old.`}</div>
};

export default Component;

♻️ React 에서의 렌더링

기본적으로 리액트는 root DOM(<div id=”root”></div>) 에서부터 시작해서 업데이트가 필요한 플래그가 지정된 모든 컴포넌트를 찾는다.

React 프로젝트를 생성하면 index.tsx 파일 또는 main.tsx 파일에 아래와 같이 생성된 코드를 확인할 수 있을 것이다. 아래 코드를 보면 "root" 라는 id 를 가진 HTMLElementReactDOM.createRoot() 에 전달한 다음, 해당하는 Element 를 .render() 메서드에 전달한다.

ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
  <React.StrictMode>
    <App />
  </React.StrictMode>
);

리액트가 모든 컴포넌트 가지들을 훑으며 업데이트가 필요한 플래그가 지정된 컴포넌트를 찾으면, 클래스 컴포넌트는 classComponentInstance.render() 를 호출하고 함수형 컴포넌트라면 FunctionComponent() 를 호출해 렌더링된( 👉🏻 propsstate에 기초해 UI를 구성하는 방법에 대해 질문해서 받아온!) 결과를 저장하며 렌더링을 진행한다.

컴포넌트의 렌더링 결과는 일반적으로 JSX 문법으로 구성되어 있으며, 이것은 javaScript가 컴파일이 되고 배포 준비가 되는 순간 React.createElement() 를 호출해 변환된다. createElement 는 UI 구조를 설명하는 일반적인 javaScript 객체인 React Element 를 반환한다.


🧀 Element : DOM Element, Component Element

Element 는 무엇일까? Element 는 화면에 렌더링 할 DOM 노드들의 정보를 React 에 알려주기 위한 하나의 수단이다. Element 는 DOM node 또는 컴포넌트를 표현하는 JavaScript 의 일반 불변 객체이며, typeprops 를 가진다.

type and props

React Element 의 type 은 문자열 혹은 함수형/클래스형 컴포넌트이며, props 는 하나의 객체이다.

Let’s create Element

React Element 는 React.createElement() 함수 또는 JSX의 태그 문법으로 작성된다.

  • createElement() 를 이용해서 생성하기
React.createElement(
    'div',
    { className: 'pokemon' },
    'Pikachu'
);

위와 같은 방법은 사실 사용이 쉽지않고 직관적이지 못한 단점이 있긴 하다. (그렇다고 해서 몰라도 되는건 아니지만!) 그래서 보통은 이 방법 보다는, 아래의 JSX 문법을 많이 사용한다.

  • JSX 문법을 이용해서 생성하기
<div className='pokemon'>Pikachu</div>

위 두 가지 방법을 이용해서 React Element 를 생성하면, 아래와 같은 객체가 형성된다. 이러한 element 들이 모여 트리를 만들면 이를 element tree 라 부르며, 이는 곧 메모리상에만 존재하게 되는 virtual DOM 이 된다.

{
    type: 'div',
    props: {
        className: 'pokemon',
        children: 'Pikachu'
    }
}

간단하게 정리하면 element 는 컴포넌트를 JSON 으로 표현한 것이다.

<div class="leetrue">
  <b>leetrue</b>
</div>
{
  "type": "div",
  "props": {
    "className": "leetrue",
    "children": {
      "type": "b",
      "children": "leetrue"
    }
  }
}

만약 type 이 string 인 경우라면 type 은 해당 컴포넌트가 어떤 HTML 태그인지를 표현한다. 그리고 이 경우, props 는 해당 HTML 태그의 속성을 명시한다.

DOM Element

element 의 type 이 태그 이름에 해당하는 문자열인 경우(소문자로 시작)를 말한다.

해당 태그를 가진 DOM 노드를 표현하며, props 정보를 통해서 해당 노드의 attribute 들을 표현한다. React 가 실제로 화면에 렌더링 하는 대상에 해당한다.

Component Element

element 의 type 이 클래스형/함수형 컴포넌트인 경우를 의미한다.

사용자가 직접 정의한 컴포넌트를 표현하며, 입력으로 props 를 받으면 렌더링할 element tree 를 반환한다. 이 element tree 는 어떠한 element tree 를 반환하는지 묻는 것을 반복한다. 클래스형 컴포넌트의 경우 당연히 컴포넌트 인스턴스의 생성이 선행될 것이다.

// 🤔 React: Form이 뭔데..? 
{
  type: SignUpForm
  props: {
    isSubmitted: false,
    buttonText: 'OK!'
  }
}
// 🤔 React: Form에 있는 이 Button은 또 뭐야?
{
  type: Button,
  props: {
    children: 'OK!',
    color: 'blue'
  }
}
// 😜 React: Form에 있는 Button을 보니 Dom Node 였구나! ㅇㅋㅇㅋ 그만!
  type: 'button',
  props: {
    className: 'button button-blue',
    children: {
      type: 'b',
      props: {
        children: 'OK!'
      }
    }
  }
}
  • 클래스형 컴포넌트

    지역 상태를 가질 수 있고, 해당 컴포넌트 인스턴스에 대응하는 DOM 노드가 생성, 수정, 삭제될 때의 동작을 제어할 수 있다.(생명 주기)

  • 함수형 컴포넌트

    render() 함수만 가지는 클래스형 컴포넌트와 동일하며, 지역 상태를 가질 수 없지만 구현이 단순하다.

🥔 Component Instance

클래스로 선언된 컴포넌트들만 인스턴스를 가지며, 이것을 컴포넌트 인스턴스라고 부른다. 컴포넌트 클래스 내부에서 this 키워드를 통해 참조하는 대상에 해당한다. 지역 상태를 저장하고 생명 주기 이벤트들에 대한 반응을 제어할 때 매우 유용하다. 함수형 컴포넌트는 인스턴스를 가지지 않는다.

🐟 인스턴스가 뭔데?

비슷한 성질을 가진 여러 개의 객체를 생성하기 위해 사용되는, 생성자 함수(constructor)를 하나의 붕어빵 틀이라고 생각한다면 이렇게 찍어낸 붕어빵들을 인스턴스라고 한다.

function FishBread(anggo, price) {
  this.anggo = anggo;
  this.price = price;
  this.desciption = function () {
    console.log(`이 붕어빵 앙꼬는 ${this.anggo}이고 ${price}원입니다!`);
  };
}

const creamFishBread = new FishBread("슈크림", 2000);
console.log(creamFishBread); // FishBread { anggo: '슈크림', price: 2000, is: f }

creamFishBread.desciption(); // 이 붕어빵 앙꼬는 슈크림이고 2000원입니다!

🐟 그럼 React 에서 인스턴스는 뭔데

함수형 컴포넌트와 클래스형 컴포넌트부터 다시 들어가보자. 두 가지 타입의 컴포넌트는 모두 props 객체 인자를 받고 React element 를 반환하는 컴포넌트이지만 유형이 다르다.

class ClassComponent extends React.Component {
    render() {
        return <h1>Hello, {this.props.name}<h1>
    }
}

위 코드의 경우는 클래스형 컴포넌트로 React Component Class 또는 React Component type 이라고 한다. 각각의 컴포넌트는 props 라는 매개변수를 받고 render 함수를 통해 표시할 뷰 계층 구조를 반환한다.

function FuntionalComponent(props) {
  return <h1>Hello, {props.name}</h1>;
}

위 코드 같은 경우는 인스턴스가 아니다. 이 함수는 팩토리 형태이며, 실제 DOM 에 렌더링 되는 컴포넌트의 인스턴스들을 생성한다.


다시 렌더링으로 돌아와서,

어쨌든, element 를 다루며 하고 싶었던 이야기는, 어떤식으로 DOM 노드를 반환하는지에 대해 이야기하고 싶었다. 내가 리액트를 이해하는 데에 큰 도움이 되었기 때문에!

다시 돌아와서- 렌더링이 일어나면 프로젝트의 전체 컴포넌트에서 렌더링 결과물을 수집하고, 리액트는 새로운 Object tree 와 비교하며 실제 DOM 을 의도한 출력처럼 보이게 적용해야하는 모든 변경사항을 수집한다. 이렇게 DOM 의 변경사항을 비교하고 계산하는 과정을 리액트에서는 reconciliation (재조정)이라고 부른다. 계산이 끝나면 리액트는 모든 변경 사항을 하나의 동기 시퀀스로 DOM 에 적용하게 된다.


🌍 Virtual DOM

React 의 대표적 특징 중 하나인 Virtual DOM 은 실제 DOM 구조와 비슷한 React 객체의 트리를 의미한다.

브라우저에서는 유저와의 다양한 인터랙션을 통해 DOM 구조의 빈번한 변화가 일어나는데, 이 때 마다 DOM 수정으로 인한 Render Tree 생성, Reflow, Repaint 과정이 일어난다면 브라우저의 성능은 ㅈㅓ.. 심해 속으로 쳐박힐 것잉ㄹ.ㄹ..다.

React 에서는 이 경우 Virtual DOM 을 실제 DOM 에 필요한 부분만 적절히 반영해 불필요한 수정이 일어나지 않도록 돕는다. 이 Virtual DOM 의 가장 큰 장점은 개발자가 직접 DOM 을 조작하지 않아도 된다는 점이고 이 과정이 모두 자동화가 된다는 점이다.

또한 DOM 수정에 대한 것은 batch 로 한 번에 수행이 되므로 불필요한 리렌더링을 최소화 할 수 있다.


♻️ Reconciliation

Reconciliation(이하 재조정) 은 다룰 내용이 많기 때문에, 이번에는 조금 간단하게만 정리하려한다. 👉🏻 재조정은 간단히 생각하면 기존의 Virtual DOM 과 새로운 Virtual DOM 을 비교하는 과정이다.

하지만 모든 트리를 순회하며 탐색하고 비교한다면 최첨단의 알고리즘을 이용해도 O(n^3) 의 시간복잡도를 가진다고 한다. 물론 리액트는 그런식으로 비교하지 않겠지만..

리액트에서는 브라우저 렌더링 시 기존 컴포넌트와 어떤 점이 변경되었는지 비교하기 위해 diffing 알고리즘을 사용해 컴포넌트를 갱신한다. 리액트는 아래의 2가지 가정을 기반으로 O(n) 의 시간 복잡도를 가지는 휴리스틱 알고리즘을 구현했다.

  1. 서로 다른 타입의 두 엘리먼트는 서로 다른 트리를 생성해낸다.

    • 같은 타입인 경우는 변경된 속성들에 대해서만 갱신한다.
  2. 개발자가 key prop 을 통해 컴포넌트 인스턴스를 식별해 여러 렌더링 사이에서 어떤 자식 엘리먼트가 변경되지 말아야할 것인지 표시해줄 수 있다.

위의 재조정 단계를 거쳐 이전 elements 와 새롭게 생성된 elements 를 비교해서 elements 가 변경되었다면 렌더링을 수행한다.


🖼️ Re-rendering

리액트에서는 초기 렌더링이 한 번 진행되고, 이후에는 특정 조건 발생 시 리렌더링을 진행한다. 그렇다면 리렌더링을 진행하는 조건은 무엇이 있을까?

리렌더링을 유발하는 조건

  • state 변경 시

  • 부모 요소로부터 전달 받은 props 변경 시

  • 중앙 상태값 (Context value 또는 redux store ) 변경 시

  • 부모 컴포넌트의 리렌더링이 일어날 시

리렌더링 과정 간단 정리

  1. 위 조건 충족 시 리렌더링이 큐에 들어감

  2. 구현부의 실행 → props 취득, hooks 실행, 내부 변수 및 함수 재생성

  3. return 실행, 렌더링 시작

  4. Render Phase(렌더 단계) : 새로운 Virtual DOM 생성 후 이전 Virtual DOM 과 비교해 달라진 부분 확인

  5. Commit Phase(커밋 단계) : 달라진 부분만 실제 DOM 에 반영

  6. useLayoutEffect : 브라우저가 화면에 Paint 하기 전 useLayoutEffect 에 등록해둔 effect 가 동기적으로 실행되며 이 과정에서 state, redux store 등의 변경이 있다면 리렌더링이 일어남

  7. Paint : 브라우저가 실제 DOM 을 화면에 그림 ( → didUpdate 완료 )

  8. useEffect : update 되어 화면에 그려진 직후, useEffect 에 등록해둔 effect 가 비동기적으로 실행됨


Render Phase and Commit Phase

  • 🧮 Render Phase : 컴포넌트를 렌더링하고 변경사항을 계산하는 모든 작업

  • ♻️ Commit Phase : DOM 에 변경사항을 적용하는 과정


👩🏻‍🌾 마무리

이렇게 리액트 한 스푼 떠먹었다. 이제는 렌더링이 무엇인가요? 라고 물으면 “어느 정도는” 대답할 수 있을 것 같다. 100% 자신있게 대답할 수 있기 위해서는 계속 뜯어봐야겠지만! 그래도 어느정도 렌더링 된다 ← 라는 이야기를 들었을 때 어떤 것들이 일어나는지에 대한 그림이 그려지는 것 같다.

위 내용을 공부하는 과정에서 Reconciliation 과 Fiber 에 대해서도 좀 자세히 다뤄봐야겠다는 생각이 들었다. 리스트에 올려두는걸로-! 구럼 오늘도 수고했다 잍을우.


참고

0
Subscribe to my newsletter

Read articles from leetrue directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

leetrue
leetrue

직면하는 모든 문제에 유치한 것은 없으며, 의미 없는 삽질 또한 없다고 믿습니다.