Virtual DOM 노드
Virtual DOM이란?
Virtual DOM은 DOM 트리를 표현하는 JavaScript 데이터 구조입니다. 이는 중첩된 Virtual DOM 노드인 _vnode_들로 구성됩니다.
일반적으로 Virtual DOM 트리는 이벤트 핸들러나 데이터 변경에 따라 매 렌더링 주기마다 재생성됩니다. Mithril.js는 vnode 트리를 이전 버전과 비교하여 변경된 부분의 DOM 요소만 수정합니다.
vnode를 자주 재생성하는 것이 비효율적으로 보일 수 있지만, 최신 JavaScript 엔진은 1밀리초 안에 수십만 개의 객체를 생성할 수 있습니다. 반면, DOM을 수정하는 것은 vnode를 생성하는 것보다 훨씬 더 많은 비용이 듭니다.
이러한 이유로 Mithril.js는 정교하고 고도로 최적화된 Virtual DOM 비교 알고리즘을 사용하여 DOM 업데이트 양을 최소화합니다. 또한 Mithril.js는 JavaScript 엔진에서 거의 네이티브 데이터 구조 접근 성능을 위해 컴파일되는 신중하게 설계된 vnode 데이터 구조를 생성합니다. Mithril.js는 vnode를 생성하는 함수 또한 적극적으로 최적화합니다.
Mithril.js가 모든 렌더링 시 Virtual DOM 트리 전체를 다시 생성하는 렌더링 모델을 지원하기 위해 이토록 많은 노력을 기울이는 이유는 선언적인 immediate mode API를 제공하기 위함이며, 이는 UI 복잡성을 훨씬 쉽게 관리할 수 있도록 해주는 렌더링 방식입니다.
Immediate mode가 왜 중요한지 설명하기 위해 DOM API와 HTML을 예로 들어보겠습니다. DOM API는 명령형 retained mode API이며, 1. DOM 트리를 절차적으로 조립하기 위한 정확한 명령을 작성하고, 2. 해당 트리를 업데이트하기 위한 또 다른 명령을 작성해야 합니다. DOM API의 명령형 특성상 코드 마이크로 최적화 기회가 많지만, 버그 발생 가능성이 높고 코드를 이해하기 어려워질 수 있습니다.
대조적으로 HTML은 immediate mode 렌더링 시스템에 더 가깝습니다. HTML을 사용하면 부모에 자식을 추가하는 것을 잊거나, 매우 깊은 트리를 렌더링할 때 스택 오버플로가 발생하는 것에 대해 걱정할 필요 없이 훨씬 더 자연스럽고 읽기 쉬운 방식으로 DOM 트리를 작성할 수 있습니다.
Virtual DOM은 임의의 데이터 변경에 UI를 효율적으로 동기화하기 위해 여러 DOM API 호출 세트를 수동으로 작성할 필요 없이 동적인 DOM 트리를 작성할 수 있도록 하여 HTML보다 한 단계 더 나아갑니다.
기본 사항
Virtual DOM 노드, 즉 _vnode_는 DOM 요소 또는 DOM의 일부를 나타내는 JavaScript 객체입니다. Mithril.js의 Virtual DOM 엔진은 vnode 트리를 사용하여 DOM 트리를 생성합니다.
Vnode는 m()
hyperscript 유틸리티를 사용하여 생성됩니다.
m('div', { id: 'test' }, 'hello');
Hyperscript는 컴포넌트 또한 사용할 수 있습니다.
// 컴포넌트 정의
var ExampleComponent = {
view: function (vnode) {
return m('div', vnode.attrs, ['Hello ', vnode.children]);
},
};
// 사용 예시
m(ExampleComponent, { style: 'color:red;' }, 'world');
// 결과 HTML:
// <div style="color:red;">Hello world</div>
구조
Virtual DOM 노드, 즉 _vnode_는 요소 또는 DOM의 일부를 나타내는 JavaScript 객체이며, 다음과 같은 속성을 가집니다.
속성 | 타입 | 설명 |
---|---|---|
tag | String|Object | DOM 요소의 nodeName 입니다. vnode가 fragment일 경우 [ 문자열, text vnode일 경우 # 문자열, trusted HTML vnode일 경우 < 문자열일 수 있습니다. 컴포넌트일 수도 있습니다. |
key | String? | 데이터 배열에서 DOM 요소를 해당 항목에 매핑하는 데 사용되는 값입니다. |
attrs | Object? | DOM 속성, 이벤트 핸들러, 프로퍼티 및 라이프사이클 메서드의 해시맵입니다. |
children | (Array|String|Number|Boolean)? | 대부분의 vnode 타입에서 children 속성은 vnode 배열입니다. text 및 trusted HTML vnode의 경우 children 속성은 문자열, 숫자 또는 부울입니다. |
text | (String|Number|Boolean)? | vnode에 text 노드가 유일한 자식으로 포함된 경우 children 대신 사용됩니다. 이는 성능상의 이유로 사용됩니다. 컴포넌트 vnode는 text 노드가 유일한 자식으로 있더라도 text 속성을 사용하지 않습니다. |
dom | Element? | vnode에 해당하는 DOM 요소를 가리킵니다. 이 속성은 oninit 라이프사이클 메서드에서 undefined 입니다. fragment 및 trusted HTML vnode에서 dom 은 범위의 첫 번째 요소를 가리킵니다. |
domSize | Number? | fragment 및 trusted HTML vnode에서만 설정되며, 다른 vnode 타입에서는 undefined 입니다. 이는 vnode가 나타내는 DOM 요소의 개수를 정의합니다 (dom 속성으로 참조되는 요소부터 시작). |
state | Object? | 다시 그리기 사이에 유지되는 객체입니다. 필요한 경우 코어 엔진에서 제공합니다. POJO 컴포넌트 vnode에서 state 는 컴포넌트 객체/클래스에서 프로토타입적으로 상속됩니다. 클래스 컴포넌트 vnode에서는 클래스의 인스턴스입니다. 클로저 컴포넌트에서는 클로저에서 반환된 객체입니다. |
events | Object? | 다시 그리기 사이에 유지되고 DOM API를 사용하여 제거할 수 있도록 이벤트 처리기를 저장하는 객체입니다. 이벤트 처리기가 정의되지 않은 경우 events 속성은 undefined 입니다. 이 속성은 Mithril.js 내부에서만 사용되며 사용하거나 수정하지 마십시오. |
instance | Object? | 컴포넌트의 경우 view 에서 반환된 값의 저장 위치입니다. 이 속성은 Mithril.js 내부에서만 사용되며 사용하거나 수정하지 마십시오. |
Vnode 타입
vnode의 tag
속성은 해당 타입을 결정합니다. 다섯 가지 vnode 타입이 있습니다.
Vnode 타입 | 예시 | 설명 |
---|---|---|
Element | {tag: "div"} | DOM 요소를 나타냅니다. |
Fragment | {tag: "[", children: []} | 부모 DOM 요소에 fragment 외 다른 요소도 포함될 수 있는 DOM 요소 목록을 나타냅니다. m() 헬퍼 함수를 사용하는 경우 fragment vnode는 m() 의 children 파라미터에 배열을 중첩하여 생성할 수 있습니다. m("[") 는 유효한 vnode를 생성하지 않습니다. |
Text | {tag: "#", children: ""} | DOM text 노드를 나타냅니다. |
Trusted HTML | {tag: "<", children: "<br>"} | HTML 문자열의 DOM 요소 목록을 나타냅니다. |
Component | {tag: ExampleComponent} | tag 가 view 메서드가 있는 JavaScript 객체인 경우 vnode는 컴포넌트를 렌더링하여 생성된 DOM을 나타냅니다. |
Virtual DOM 트리 내의 모든 것은 text를 포함하여 vnode입니다. m()
유틸리티는 children
인수를 자동으로 정규화하고 문자열을 text vnode로, 중첩된 배열을 fragment vnode로 변환합니다.
Element tag 이름과 컴포넌트만 m()
함수의 첫 번째 인수가 될 수 있습니다. 즉, [
, #
및 <
는 m()
에 대한 유효한 selector
인수가 아닙니다. Trusted HTML vnode는 m.trust()
를 통해 생성할 수 있습니다.
단형 클래스
mithril/render/vnode
모듈은 Mithril.js에서 모든 vnode를 생성하는 데 사용됩니다. 이를 통해 최신 JavaScript 엔진은 vnode를 항상 동일한 숨겨진 클래스로 컴파일하여 Virtual DOM 비교를 최적화할 수 있습니다.
vnode를 내보내는 라이브러리를 만들 때 높은 수준의 렌더링 성능을 보장하기 위해 단순한 JavaScript 객체를 작성하는 대신 이 모듈을 사용해야 합니다.
안티 패턴 피하기
vnode 재사용 피하기
Vnode는 특정 시점의 DOM 상태를 나타내는 것으로 간주됩니다. Mithril.js의 렌더링 엔진은 재사용된 vnode가 변경되지 않았다고 가정하므로 이전 렌더링에 사용된 vnode를 수정하면 정의되지 않은 동작이 발생합니다.
diff를 방지하기 위해 vnode를 제자리에서 재사용할 수도 있지만, onbeforeupdate
를 사용하는 것이 더 권장됩니다.
모델 데이터를 속성을 통해 컴포넌트에 직접 전달하지 않기
key
속성이 Mithril.js의 key 로직과 충돌하는 방식으로 데이터 모델에 나타날 수 있으며, 모델 자체가 onupdate
또는 onremove
와 같은 라이프사이클 훅과 이름을 공유하는 변경 가능한 인스턴스일 수 있습니다. 예를 들어 모델은 사용자 정의 가능한 색상 key를 나타내기 위해 key
속성을 사용할 수 있습니다. 이 속성이 변경되면 컴포넌트가 잘못된 데이터를 수신하거나 예기치 않게 위치를 변경하거나 기타 예기치 않은 원치 않는 동작이 발생할 수 있습니다. 대신 Mithril.js가 오해하지 않도록 속성으로 전달하십시오 (나중에 잠재적으로 변경하거나 프로토타입 메서드를 호출할 수 있습니다).
// 데이터 모델
var users = [
{ id: 1, name: 'John', key: 'red' },
{ id: 2, name: 'Mary', key: 'blue' },
];
// 나중에...
users[0].key = 'yellow';
// 피해야 할 방법
users.map(function (user) {
// John의 컴포넌트가 제거되고 다시 생성됩니다.
return m(UserComponent, user);
});
// 권장되는 방법
users.map(function (user) {
// Key가 명시적으로 추출됨: 데이터 모델에 자체 속성이 제공됨
return m(UserComponent, { key: user.id, model: user });
});
view 메서드에서 구문 사용 피하기
view 메서드 내 JavaScript 구문은 종종 HTML 트리의 자연스러운 중첩 구조를 변경해야 하므로 코드가 더 장황해지고 읽기 어려워집니다.
// 피해야 할 방법
var BadListComponent = {
view: function (vnode) {
var list = [];
for (var i = 0; i < vnode.attrs.items.length; i++) {
list.push(m('li', vnode.attrs.items[i]));
}
return m('ul', list);
},
};
대신 조건부 렌더링을 위한 삼항 연산자 및 목록과 유사한 구조를 위한 Array 메서드와 같은 JavaScript 표현식을 사용하는 것이 좋습니다.
// 권장되는 방법
var BetterListComponent = {
view: function (vnode) {
return m(
'ul',
vnode.attrs.items.map(function (item) {
return m('li', item);
})
);
},
};