에듀테크 프로젝트 대시보드
가상 DOM & 재조정
React 같은 라이브러리는 화면 전체를 다시 그리는 것처럼 코드를 짜도, 실제로는 바뀐 부분만 콕 집어 고친다. 그 비밀이 가상 DOM과 diff 알고리즘이다.
브라우저의 DOM은 노드 하나를 만들거나 속성을 바꿀 때마다 스타일 계산·레이아웃·페인트가 연쇄적으로 일어날 수 있는 무거운 객체다. 목록 100개 중 1개만 바뀌었는데 innerHTML로 전부 새로 그리면, 브라우저는 99개의 멀쩡한 노드까지 부수고 다시 만든다. 입력 포커스·스크롤 위치·애니메이션 상태도 함께 날아간다.
그래서 "바뀐 곳만 찾아서 그 부분만 DOM API를 호출"하는 게 핵심이다. 문제는 — 손으로 그 코드를 짜면 복잡하고 버그가 많다는 것.
가상 DOM(VNode)은 그냥 평범한 자바스크립트 객체 트리다. 만들고 버리는 비용이 거의 없다.
// <li class="done">우유 사기</li> 를 표현한 VNode { type: "li", props: { class: "done", key: 3 }, children: [ "우유 사기" ] }
UI 함수는 현재 상태(state)를 받아 VNode 트리를 통째로 반환한다. 상태가 바뀌면 새 트리를 다시 통째로 만든다. 여기까지는 비싸지 않다 — 진짜 DOM은 아직 안 건드렸으니까.
이전 VNode 트리(old)와 새 VNode 트리(new)를 같은 위치끼리 짝지어 내려가며 비교한다.
| 비교 결과 | 수행할 DOM 작업 |
|---|---|
| old 없음 / new 있음 | CREATE — 노드 생성 후 삽입 |
| old 있음 / new 없음 | REMOVE — 노드 제거 |
타입이 다름 (div↔span) | REPLACE — 통째로 교체 |
| 둘 다 텍스트인데 내용이 다름 | TEXT — nodeValue만 갱신 |
| 타입 같음, 속성만 다름 | PROPS — 바뀐 속성만 set/remove |
| 타입 같음 | → 자식들을 재귀적으로 다시 diff |
비교가 끝나면 "패치 목록"이 남고, 이걸 실제 DOM에 한 번에 적용한다. 아무것도 안 바뀐 노드는 건드리지 않는다.
버튼으로 상태를 바꾸면, 아래에서 (1) 실제로 다시 그려지는 DOM, (2) 새 VNode 트리(바뀐 노드 하이라이트), (3) diff가 만들어낸 패치 목록을 볼 수 있다.
① 실제 DOM (다시 그려진 부분만 색으로 반짝)
② 새 VNode 트리
③ diff가 만든 패치 목록
"key 사용"을 끄고 맨 앞에 추가나 순서 섞기를 눌러 보자. key가 없으면 diff는 자식을 인덱스(위치) 순서로만 짝짓는다. 맨 앞에 하나 끼워 넣으면 1번은 2번 자리로, 2번은 3번 자리로… 전부 한 칸씩 밀려서 모든 항목의 텍스트가 바뀐 것으로 인식된다. 패치가 잔뜩 생긴다.
반대로 각 항목에 고유 key가 있으면, diff는 "3번 key를 가진 노드는 이전에도 있었고 내용도 같네 → DOM 노드 재사용, 위치만 이동(MOVE)"이라고 판단한다. 패치가 1~2개로 줄어든다. 리스트를 렌더링할 때 key를 꼭 넣으라는 이유가 이것이다.
0,1,2…)를 쓰면 순서가 바뀔 때 결국 인덱스 매칭과 똑같아진다. key는 데이터 자체의 고유 ID를 써야 한다.두 트리의 "최소 편집 거리"를 엄밀하게 구하는 범용 알고리즘은 O(n³)이라 실전에선 못 쓴다. 그래서 라이브러리들은 몇 가지 휴리스틱으로 O(n)까지 낮춘다.
key로 같은 레벨 안에서의 이동·재사용을 빠르게 매칭한다.완벽하진 않지만 "거의 항상 충분히 좋은" 결과를, 사람이 손으로 짠 것보다 안전하게 만들어 준다.
key를 붙이자.