175 Vue.js의 렌더링에 관해 - 가상 DOM

source: categories/study/vue-experiance/vue-experiance_9-99_76.md

175 Vue.js의 렌더링에 관해 - 가상 DOM

Vue.js는 React와 마찬가지로 가상 DOM(virtual DOM) 기반의 렌더링 시스템으로 구현되어있다.

React에 의해 대중화된 이 가상 DOM 기반의 렌더링에는 중요한 특징이 하나 있는데, 바로 View 갱신을 처리할 때는 매번 전체 화면을 처음부터 다시 렌더링한다는 사실이다.
데이터 혹은 여러 요인에 따라 View가 변경되어야 하는 경우, 변경될 부분을 구분해서 그 부분만 다시 렌더링 하도록 로직을 구성하는 것이 아니라 매번 전체 화면을 처음부터 다시 생성하듯이 렌더링을 한다는 것이다.
만약 실제 브라우저의 DOM을 이런식으로 매번 전체를 다시 그리도록 하는 것은 성능이 너무 떨어지겠지만,
Vue.js나 React는 가상 DOM이라는 View의 추상화 층을 두고, 로직에서는 이 가상 DOM을 매번 처음부터 다시 그리도록 한다.
이 가상 DOM은 단순히 메모리 요소이므로 이를 생성하는 것은 매우 빠르기 때문이다.

이렇게 로직에서 가상 DOM을 만들어내면 프레임워크는 이 가상 DOM을 브라우저에 반영하는 과정에서 변경사항만을 효율적으로 구분해서 최소한의 실제 브라우저 DOM 변경만이 이뤄지도록 하게된다.

[출처] https://www.slideshare.net/gyeongseokseo/virtual-dom

이런 방식의 장점은 무엇보다 어플리케이션 렌더링 로직이 간단해진다는 것이다.
개발자가 작성하는 어플리케이션 로직에서 직접 변경 사항만을 추려서 그 부분만 갱신하도록 최적화해야 하는 것이 아니라,
로직에서는 그냥 매번 처음부터 다시 그린다고 생각하듯이 처리하고,
실제 DOM 반영시의 효율성은 프레임워크가 변경사항만을 효과적으로 처리함으로써 달성해준다. (변경 사항을 구분해서 처리하는 로직보다는 그냥 처음 부터 다시 그리도록 수행하는 로직이 간단해 질 수 밖에 없다.)

이렇기 대문에 어플리케이션의 렌더링 로직은 간단하게 만들 수 있으면서도 성능은 떨어지지 않게된다.
사실 가상 DOM을 쓰는 가장 큰 이유는 성능 보다도 이 렌더링 로직을 단순하게 만들 수 있다는 것이 더 크다.

참조 동영상

가상 DOM을 기반으로하는 React와 Vue.js는 컴포넌트 단위로 어플리케이션을 구성하는데, 각각의 컴포넌트는 render 함수를 가지고 있으며 이 render 함수에서 그 컴포넌트의 View를 나타내는 가상 DOM의 Node를 생성하게된다.
그러면 프레임워크에서 생성된 이 가상 DOM Node Tree를 기반으로 실제 브라우저 DOM Tree를 생성 및 업데이트한다.

그런데 React와 Vue.js와의 차이점이라고 한다면,
React는 원칙적으로 이 render 함수를 직접 구현하도록(비록 JSX라는 도메인 언어가 HTML을 좀더 쉽게 render 함수에 사용할 수 있게 해주기는 하지만) 되어 있지만
Vue.js는 Angular 처럼 HTML 형태의 template를 작성하면 Vue.js의 컴파일러가 이를 render 함수로 생성해 준다는 것이다.

이는 Vue.js 내부 실제 구현은 React의 그것과 같은 방식이지만,
코드와 HTML을 필연적으로 섞어서 작성해야 하는 React의 사용법에 거부감을 가지는 개발자에게
Angular 처럼 template와 코드를 분리하여 작성할 수 있주는 효과가 있다.

또한 Vue.js는 template를 통해서 뿐만 아니라 React처럼 render 함수를 직접 코드로 작성하는 것도 지원한다.
(Angular 4의 컴파일러도 사용자가 작성한 template를 컴파일해서 코드를 생성해서 이를 가지고 렌더링을 수행한다. 그러나 Angular 4는 사용자가 직접 이 렌더링 코드를 작성하는 것은 지원하지 않는다.)

이렇게 Vue.js에서는 template를 통해서는 물론이고 직접 render 함수를 작성할 수도 있기 때문에,
어플리케이션을 만들때 다양한 로더나 컴파일러를 통해서 Angular 처럼 작성할 수도, JSX를 써서 React처럼 작성할 수도 있게 된다.

//template를 이용하는 경우
Vue.component( 'my-comp', {
    name : 'my-comp',
    template : `
        <div>
            <h1>My Component</h1>
       </div>
    `
});
//JSX를 사용하는 경우
Vue.component( 'my-comp', {
    name : 'my-comp',
    render : function( h ) {
        return (
           <div>
               <h1>My Component</h1>
           </div>
        );
    }
}); 
//render 함수를 직접 작성하는 경우
Vue.component( 'my-comp', {
    name : 'my-comp',
    render : function( createElement ) {
        return createElement( 'div', [
            createElement( 'h1', 'My Component' )
        ]);
    }
});

그런데 앞서 이야기 했듯이 Vue.js에서 template를 사용해서 View를 만들더라도 어디까지나 render 함수를 만들고 여기서 가상 DOM의 Node를 만들어 내는 코드를 생성하는 것이기 때문에,
template에 사용된 v-for나 v-if 같은 DOM Node의 동적인 생성, 삭제와 관련된 디렉티브에 관해서는 Angular의 ngFor(혹은 ng-repeat)와는 그 작동 방식이 완전히 다르다.

Angular(혹은 AngularJS)에는 template의 일부분을 떼어내어 조작할 수 있는 장치가 마련되어 있다.
AngularJS 1.x에서는 transclude라는 개념이고 Angular 4에서는 <ng-template> 태그로 대표되는 구조적 디렉티브(Structural Directive)가 그것이다.

Angular는 디렉티브에게 이 View의 떼어낸 일부분을 인자로 전달해서 사용할 수 있도록 되어 있어서,
디렉티브가 이 조각을 데이터에 의해 임의로 생성 삭제할 수 있게 되어 있다.
즉 Angular의 ngFor(ng-repeat)와 ngIf(ng-if) 디렉티브는 어플리케이션 실행시에 작동하는 런타임 요소다.

그러나 Vue.js의 v-for나 v-if는 template 컴파일시에 자바스크립트의 for문장 같은 코드로 변경되어 들어가 버린다.
즉 Vue.js의 v-for나 v-if는 컴파일러에 의해 처리되는 컴파일 요소(런타임 요소가 아니라) 라는 말이다.
실제로 Vue.js에서 v-for의 구현부를 찾아보면 compiler 부분에 위치하는 것을 볼 수 있다.(다음은 Vue.js v2.4.2의 소스의 일부분 이다.)

만약 다음과 같은 v-for를 사용한 template는


<div v-for="item in items" v-bind:key="item">
    Hello
</div>

다음과 같은 렌더링 코드로 바뀌게 된다.

_vm._l( _vm.items, function(item) {
    return _c( 'div', { key: item }, [ _vm._v( "Hello" ) ] )
})
// 여기서 _vm._l은 renderList로서 _vm.items를 for문으로 순회하는 역할을 한다. 그리고 _c는 createElement이고, _vm._v는 createTextNode를 나타낸다.

Vue.js의 문서를 살펴봐도 v-for나 v-if의 구현을 다음처럼 render 함수를 작성하도록 안내하고 있다.

보다 시피 v-for나 v-if 자체는 사라지고 자바스크립트 코드로 그냥 바뀌어 지는 것을 알 수 있다.

그런데 이렇게 되면 Vue.js에 DOM의 구조를 바꾸는 사용자 디렉티브 혹은 컴포넌트는 만들기가 어렵다는 이야기가 된다.
왜냐하면 그런 디렉티브 혹은 컴포넌트는 Vue.js 컴파일러에 구현되어야 하고, 이는 그 컴파일러 혹은 컴포넌트의 구현이 Vue.js 소스 자체에 포함되어야 한다는 이야기가 되기 때문이다.

또한 현재 Vue.js에는, HTML5에 도입된 <template> 태그(앞서 이야기했던 컴포넌트의 template가 아니다.)를 일반적으로 이용하는 방법에 대한 부분은 구현되어 있지 않은 듯하다.
원래 HTML5의 <template>태그는 DOM에는 존재하지만 그것은 인스턴스화 하기 전까지는 화면에 출력되지 않아야 한다.
그러나 Vue.js에서 다음처럼 컴포넌트를 만들어 테스트를 해보면 실제 렌더링된 DOM에는 <template> 태그는 나타나지 않고

Vue.component( 'my-comp', {
    name : 'my-comp',
    template : `
        <div>
            <h1>Test</h1>
            <template>
                <h2>Test2</h2>
            </template>
            <h3>Test3</h3>
        </div>
    `
});

즉 Vue.js에서 HTML5의 <template>태그는 무시되는 상황이다.

이렇다 보니 Vue.js에서 DOM의 구조적 변경을 위해서는, 내장되어 있는 v-for나 v-if를 이용하는 식으로만 작성되어야 하고, v-for나 v-if같은 구조적 변경을 직접 수행하는 디렉티브나 컴포넌트를 만들 방법은 딱히 보이지 않는다.
이것은 Vue.js에서 다소 아쉬운 점이다. 사실 개발자의 상상에 따라 다양한 DOM 구조 변경을 수행하는 유용한 디렉티브나 컴포넌트는 만들어 질 수 있을 텐데 그 방법이 제한되는 것이기 때문이다.

그런데 Vue.js에서도 View의 일부분을 떼어내어 그것을 반복적으로 생성할 수 있게 지원하는 부분이 하나 있다. 바로 scopedSlots가 그것이다.

이에 관해서는 다음글에서 이어가겠다.

ps) Vue.js에서 <template> 태그는, 디렉티브를 사용할 때 한단위로 작동할 노드를 묶는 역할로 사용할 수 있다. 예를 들어 다음과 같이 말이다.

<div>
    <template v-for="item in items">
        <p>Hello</p>
        <p>World</p>
    </template>
</div>

<!-- or -->

<div>
    <template v-if="isShow">
        <p>Hello</p>
        <p>World</p>
    </template>
</div>  

이때 최종적으로 <template> 태그는 출력되지 않기 때문에 원하는 결과를 달성할 수 있게 된다.

[출처] https://m.blog.naver.com/jjoommnn/221082930638