Virtual DOM(가상돔)

김동현·2021년 11월 17일
0

React

목록 보기
2/27
post-thumbnail
post-custom-banner

리액트 엘리먼트

리액트 엘리먼트는 리액트가 "UI를 표현하는 수단"입니다. 보통 JSX 문법을 사용하기 때문에 리액트 엘리먼트의 존재를 잘 모를 수 있습니다. JSX 문법은 바벨이 React.createElement 메서드를 호출하는 코드로 변환을 해줍니다.

리액트 엘리먼트를 이해하면 리액트가 내부적으로 어떻게 동작하는지 이해할 수 있습니다. 리액트는 렌더링 성능을 위해 "Virtaul DOM(가상돔)"이라는 것을 활용합니다. 브라우저에서 돔을 변경하는 것은 비교적 오래 걸리는 작업입니다. 그래서 빠른 렌더링을 위해서 돔 변경을 최소화하는 것이 좋습니다.

리액트는 리액트 엘리먼트로 구성된 가상돔을 메모리에 올려두고, 데이터가 변경된 경우 가상돔을 새로 생성하고 이를 이전 가상돔과 비교합니다. 이후 변경된 부분만 실제 돔에 반영하는 전략을 채택했습니다.

이러한 렌더링 과정을 "Reconciliation(재조정)"이라고 하며, 가상돔을 서로 비교할 때는 "Diffing Algorithm"을 사용합니다.

참고로 React v16부터는 "React Fiber"라는 새로운 알고리즘을 사용하고 있습니다.

HTML 태그 이름으로 JSX 문법 사용

리액트 엘리먼트로 가상돔을 만들고 실제 돔에 반영할 변경사항을 찾는 과정을 한 번 따라가보겠습니다.

const element = (
    // JSX 문법은 리액트 엘리먼트(일반 객체)를 반환한다
    <a key="key1" style={{ color: "blue" }} href="https://google.com">
        Click here
    </a>
);

console.log(element);

다음과 같은 리액트 엘리먼트(element 변수)를 콘솔에 출력시 아래와 같은 객체가 출력됩니다.

Object {
    type: 'a',
    key: 'key1',
    ref: null,
    props: {
        href: 'https://goggle.com',
        style: {
            color: 'blue'
        },
        children: 'Click here'
    },
    // ,,
}

먼저 "일반 객체 형식"으로 리액트 엘리먼트가 이루어져있다는 것을 알 수 있습니다.

type이라는 프로퍼티에 문자열 a가 입력되어 있습니다. 이것은 a 태그로 JSX 문법을 사용했기 때문이고, key라는 프로퍼티에 key1 문자열이 저장되어 있습니다, props 프로퍼티는 객체를 값으로 갖고 있으며 이 객체 안에 JSX 문법에 작성된 어트리뷰트인 href, style와 JSX 문법의 Content 영역에 작성된 Click here 문자열 값을 갖는 children 등이 존재합니다.

사용자 컴포넌트로 JSX 문법 사용

사용자 컴포넌트를 JSX 문법 사용하여 리액트 엘리먼트를 만드는 경우는 아래와 같습니다.

// Title Component
function Title({ title, color }) {
    return <p style={{ color }}>{title}</p>;
}

const element = <Title title="hello" color="blue" />;

console.log(element);

리액트 엘리먼트는 콘솔에 다음과 같이 출력됩니다.

Object {
    type: Title, // 컴포넌트 함수 참조가 할당, 함수가 다시 호출된다.
    props: {
        title: 'hello',
        color: 'blue'
    }
    //,,
}

type에 컴포넌트 함수가 바인딩되어 있는 것을 알 수 있습니다. type 프로퍼티에 문자열이 아닌 값이라면 문자열이 될때까지 해당 함수를 호출하게 됩니다.

리액트가 type 프로퍼티에 바인딩되어 있는 컴포넌트 함수를 실행하는데 그 결과로 리액트 엘리먼트(<p style={{ color: blue }}>hello</p>)를 얻어갈 수 있습니다.


참고로 리액트 엘리먼트는 "불변 객체"이기 때문에 변경하려고하면 에러가 발생합니다.

const element = <a href="https://goggle.com">Click here</a>";

element.type = 'b'; // 리액트 엘리먼트는 불변 객체이므로 변경 불가능, 에러 발생

Virtual DOM(가상돔)

리액트는 하나의 화면을 표현하기 위해 가상돔을 생성하는데, 가상돔은 여러 개의 리액트 엘리먼트를 트리 구조로 구성됩니다.

컴포넌트 함수의 경우 반환값에 작성된 하나의 리액트 엘리먼트를 루트 노드로 갖는 가상돔을 생성합니다. 컴포넌트 함수가 실행되어 생성된 가상돔들을 통해 리액트가 하나의 화면을 표현하게 됩니다.

// 1. JSX
<div>
    <p>안녕하세요</p>
    <div>
        <p>이름: 홍길동</p>
        <p>나이: 23</p>
    </div>
</div>

위 JSX 코드는 아래와 같은 리액트 엘리먼트 트리를 구성됩니다.

// 2. React Elements
{
    type: 'div',
    props: {
        children: [
            {
                type: 'p',
                props: {
                    childrend: '안녕하세요'
                }
            },
            {
                type: 'div',
                props: {
                    children: [
                        {
                            type: 'p',
                            props: {
                                children: '이름: 홍길동'
                            }
                        },
                        {
                            type: 'p',
                            props: {
                                childrend: '나이: 23'
                            }
                        }
                    ]
                }
            }
        ]
    }
}

프로그램 화면은 여러 가지 이벤트를 통해서 다양한 모습으로 변화를 하는데, "하나의 리액트 엘리먼트 트리"는 시간에 따라 변환하는 화면의 "한 순간"을 나타낸다고 볼 수 있습니다.

리액트는 UI를 컴포넌트 단위로 구현하며, 각 컴포넌트들은 반환값으로 작성한 하나의 리액트 엘리먼트를 루트 노드로 가상돔을 생성합니다.

루트 컴포넌트를 시작으로 각 컴포넌트가 실행되어 전체 가상돔이 만들어지고 이는 전체 화면을 구성하게 됩니다.

Render phase & Commit phase

리액트에서는 상태값이라는 것이 존재하며 상태값이 변경되면 리액트가 자동적으로 변경된 상태값으로 UI(컴포넌트)를 리렌더링해줍니다.

이때 데이터 변경(상태값 변경)에 의한 UI 리렌더링"렌더 단계""커밋 단계"를 거칩니다.

  1. 렌더 단계
    : 실제 돔에 반영할 "변경 사항을 파악"하는 단계, 가상돔을 생성하는 단계

  2. 커밋 단계
    : 파악된 변경 사항을 "실제 돔에 반영"하는 단계, 가상돔을 비교하여 차이나는 부분을 실제 돔에 반영하는 단계

1. Render phase

렌더 단계"변경 사항을 파악하기 위해서 가상돔을 생성"합니다.

컴포넌트가 실행되면 가상돔을 생성되며 이는 리액트 엘리먼트를 트리 구조로 구성되어 있습니다

리액트는 컴포넌트의 상태값이 변경될 때마다 변경된 상태값으로 컴포넌트를 재실행하여 새로운 가상돔을 생성합니다. 이때 새롭게 생성된 가상돔과 이전 가상돔을 서로 비교하여 차이나는 부분을 파악합니다.

// Todo Component
function Todo(props) {
    const [priority, setPriority] = useState('high');
    
    const handleClick = () => {
        setPriority(priority === 'high' ? 'low' : 'high');
    }
    
    return (
        <div>
            <Title title={props.title} />
            <p>{props.desc}</p>
            <p>{priority === 'high' ? '우선순위 높음' : '우선순위 낮음'}</p>
            <button onClick={handleClick}>우선순위 변경</button>
        </div>
    );
}

// Title Component
const Title = React.memo(({ title }) => {
    return <p style={{ color: 'blue' }}>{title}</p>;
});

// Render Phase
ReactDOM.createRoot(document.getElementById('root'))
    .render(<Todo title="study" desc="React" />);

Todo 컴포넌트로 만들어지는 가상돔은 아래와 같습니다.

{
    type: Todo,    // -> Todo 컴포넌트 함수
    props: {
        title: 'study',
        desc: 'React'
    },
    //,,
}

위와 같은 객체가 만들어졌을 것이고 type에는 Todo 컴포넌트 함수가 있습니다. 그리고 props 프로퍼티에는 어트리뷰터로 작성된 값들이 존재합니다.

리액트는 type 프로퍼티가 문자열이 될 때가지 해당 함수를 호출하게 됩니다. 그 결과는 아래와 같습니다.

{
    type: 'div',
    props: {
        children: [
            {
                type: Title,    // -> 컴포넌트
                props: {
                    title: 'study'
                }
            },
            {
                type: 'p',
                props: {
                    children: 'React'
                }
            },
            {
                type: 'p',
                props: {
                    children: '우선순위 높음'
                }
            },
            {
                type: 'button',
                props: {
                    onClick: function() {,,,},
                    children: '우선순위 변경'
                }
            }
        ]
    }
}

이 결과는 Todo 컴포넌트의 return 문에 작성된 구조와 똑같은 리액트 엘리먼트 트리가 만들어집니다. 하지만 아직 type 프로퍼티 값이 Title 컴포넌트가 존재하기 때문에 이 리액트 엘리먼트 트리를 실제 돔으로 만들 수 없습니다.

Title 컴포넌트를 렌더링한 결과는 아래와 같습니다.

{
    type: 'p',
    props: {
        style: { color: blue },
        children: 'study'
    }
},
// ,,

이제 모둔 리액트 엘리먼트의 type 프로퍼티 값이 문자열이므로 실제 돔으로 만들 수 있습니다. 이와 같이 실제 돔을 만들 수 있는 리액트 엘리먼트 트리를 "Virtual DOM(가상돔)"이라고 합니다.

"ReactDOM.createRoot().render();"

렌더 단계는 앞에서 살펴보았듯이 변경 사항을 파악하기 위해 가상돔을 생성하는 단계입니다.

Render 단계는 "ReactDOM.createRoot().render()"하거나, 컴포넌트 내부의 "상태값 변경함수를 호출"하는 것으로 시작될 수 있습니다.

최초 render phaseReactDOM.createRoot().render() 메서드에 의해 최초의 렌더 단계가 실행되어 최초 가상돔이 생성됩니다.
최초로 생성된 전체 가상돔은 실제 돔에 그대로 반영되에 화면에 렌더링됩니다.

// index.js, React v18
import ReactDOM from 'react-dom/client';

const rootNode = document.getElementById('root');

  // App 컴포넌트를 루트 컴포넌트로 하는 최초 가상돔 생성
  // 최초로 생성된 가상돔을 실제 돔에 반영하여 화면에 렌더링
ReactDOM.createRoot(rootNode).render(<App />);
"setState();"

상태값 변경 함수에 의해서 수행되는 렌더 단계를 한 번 따라가보겠습니다.

우선 순위 변경 버튼을 누르게 되면 handlerClick 함수가 호출되고 setPriority 상태 변경 함수가 호출됩니다. 상태 변경 함수로 컴포넌트의 상태값인 priority이 변경되어 리액트는 Todo 컴포넌트를 실행하여 새로운 가상돔을 생성합니다.

생성되는 가상돔은 반환값에 작성된 div를 루트 노드로 갖는 가상돔을 새롭게 생성합니다.

{
    type: 'div',
    props: {
        children: [
            {
                type: Title,
                props: {
                    title: 'study'
                }
            },
            {
                type: 'p',
                props: {
                    children: 'React'
                }
            },
            {
                type: 'p',
                props: {
                    children: '우선순위 낮음' // -> 값 변경
                }
            },
            {
                type: 'button',
                props: {
                    onClick: function() {,,,},
                    children: '우선순위 변경'
                }
            }
        ]
    }
}

이전 가상돔과 비교하면 '우선순위 낮음'으로 문자열만 변경되었다는 것을 알 수 있습니다. 따라서 실제 돔에서도 차이나는 부분만 반영하여 리렌더링을 수행합니다.

가상돔을 서로 비교할 때 "Diffing Algorithm"을 사용하는데 이에 대해서는 추후에 자세하게 알아보겠습니다.

profile
Frontend Dev
post-custom-banner

0개의 댓글