선행 지식
Critical Rendering Path
- 브라우저가 서버에 웹 페이지에 대한 요청을 하게 되면, 서버는 response 의 header 나 data 로 html, css, js 파일을 반환한다.
- 이후 브라우저는 받아온 html, css, js 파일을 가지고 viewport 라는 브라우저의 화면 부분에 알맞게 뿌려주게 된다. 이 받아온 html, css, js 를 가지고 viewport에 뿌려주는 과정을 CRP 라고 한다.
Critical Rendering Path 의 과정
- DOM
- html 의 태그들을 통해 DOM 을 구성한다. DOM은 페이지의 모든 컨텐츠를 포함한다. (실제 브라우저 화면에 보이지 않는 모든 태그를 포함한다.)
- CSSOM
- DOM을 스타일링 하기 위한 스타일 정보를 포함한다. CSS 는 렌더링을 막는다. 모든 CSS를 처리하고 수신할 때 까지 페이지 렌더링을 막는다. CSSOM이 완료 될 때까지 새로운 콘텐츠를 렌더링 할 수 없다.
- 렌더 트리
- DOM과 CSSOM을 결합한다. 렌더 트리는 오직 보여지는 콘텐츠만을 캡쳐한다. 그래서 display: none이 적용 되어있다면, 해당 요소 및 그 하위 요소는 포함 되지 않는다.
- Layout
- 요소들이 화면에 배치되는 위치와 방법, 각 요소들의 크기와 관련된 위치를 결정하는 연산
- 레이아웃의 성능은 DOM 의 영향을 받는다. DOM 노드가 적어야 이 연산이 빠르게 종료된다.
- 가장 무거운 연산
- Paint
- 화면에 픽셀을 그리는 연산
- 두번째로 무거운 연산
Virtual DOM
- 배경
- DOM을 직접 조작하면 Layout 연산이 일어나게 되는데, 이 Layout 연산이 굉장히 무거운 연산이다.
- React는 SPA 프레임워크이다. SPA 환경에서는 DOM 트리 노드 개수가 많고, DOM 의 변경이 자주 일어난다.
- 이전 DOM 트리에서 새로운 DOM 트리로 최소한의 연산을 통해 변경 되는 것이 Layout 연산을 줄일 수 있는 방법이다.
- Virtual DOM
- 변경이 되어도 CRP 후속 연산이 따르지 않는, 메모리에만 올려놓은 DOM
- Virtual DOM 을 이용해 재조정이라는 연산을 통해, DOM 중에서 업데이트 해야하는 element 만 찾아서 실제 DOM 을 변경
Virtual DOM 과 DOM 의 차이점
- 실제 DOM 에 비해 Virtual DOM 이 더 적은 정보를 가지고 있어 가볍다.
- Virtual DOM 은 메모리 상에서만 올라가 있으며 Virtual DOM 트리가 변경되어도, 렌더링을 하지 않는다.
재조정(Reconciliation)
- React 가 Virtual DOM 을 통해 변경된 element 들을 빠르게 찾기 위해 채택한 방법
동기
- 하나의 트리를 가지고 다른 트리로 변환 할 때 최소 연산 수를 구하는 알고리즘의 복잡도는
- React는 두가지 가정을 추가하여 휴리스틱하게 알고리즘을 변경해 으로 복잡도를 낮춤
- 서로 다른 타입의 두 element는 서로 다른 트리를 만들어 낸다.
- 개발자가 key props 를 통해, 여러 렌더링 사이에서 어떤 자식 element 가 변경되지 않아야 할 지 표시해 줄 수 있다.
- 비교 알고리즘 (Diffing Algorithm)
- 두 개의 트리를 비교할 때, 두 트리의 root element 부터 비교한다.
- root element 타입이 다른 경우
- 이전 DOM의 root 이하 노드들은 모두 unmount
- 새로운 DOM의 root 이하 노드들은 mount
- 이전 트리와 연관된 state들이 사라짐 (unmount 되므로)
- root element 타입이 같은 경우
- 두 element 속성을 확인하여, 변경된 속성들만 갱신
- state 는 유지됨
- root element 타입이 다른경우 예제 코드
import React, { useEffect, useState } from "react"; const Test = (props) => { const [list, setList] = useState([]); const [toggle, setToggle] = useState(false); useEffect(() => { setList([1, 2, 3]); }, []); const onClick = () => { console.dir(toggle); setToggle(!toggle); }; return ( <section> <button onClick={onClick}>click me</button> {toggle ? ( <div> {list.map((item, idx) => { return <TestElement key={idx} name={"div"} />; })} </div> ) : ( <section> {list.map((item, idx) => { return <TestElement key={idx} name={"section"} />; })} </section> )} </section> ); }; const TestElement = ({ name }) => { useEffect(() => { console.dir(`${name} is Mount`); return () => { console.dir(`${name} is UnMount`); }; }, []); return <div>{name}</div>; }; export default Test;
결과:
실제로 하위 노드들이 전부 unmount 되고 새 노드가 mount 된다.
- root element 타입이 같은 경우 예제 코드
import React, { useEffect, useState } from "react"; const Test = (props) => { const [list, setList] = useState([]); const [toggle, setToggle] = useState(false); useEffect(() => { setList([1, 2, 3]); }, []); const onClick = () => { console.dir(toggle); setToggle(!toggle); }; return ( <section> <button onClick={onClick}>click me</button> { <div> {toggle ? list.map((item, idx) => { return <TestElement key={idx} name={"true"} />; }) : list.map((item, idx) => { return <TestElement key={idx} name={"false"} />; })} </div> } </section> ); }; const TestElement = ({ name }) => { useEffect(() => { console.dir(`${name} is Mount`); return () => { console.dir(`${name} is UnMount`); }; }, []); return <div>{name}</div>; }; export default Test;
결과:
내부적으로 엘리먼트를 재활용 하는 듯 하다. (key 값이 변하지 않았으므로, 실제로 key 값이 변하게 되면 각각 unmount 된다.) 실제로 아래와 같이 key 값을 다르게 준다면 unmount 된다.
<section> <button onClick={onClick}>click me</button> { <div> {toggle ? list.map((item, idx) => { return <TestElement key={idx} name={"true"} />; }) : list.map((item, idx) => { return <TestElement key={2 - idx} name={"false"} />; })} </div> } </section>
- key 속성을 통해 변경될 element 인지 아닌지를 개발자가 구분 할 수 있다.
- child 노드에 대한 재귀적 처리
- React 는 단순히 동시에 두 리스트를 순회하고 차이점이 있으면 변경을 생성함
- 왼쪽의 경우 아래에서 위로 파싱하며 변경된 점이 third 노드의 추가 라고 인지하겠지만, 오른쪽의 경우 아래에서 위로 파싱하며 변경된 것을 비교하므로 전부 달라졌다고 인지 할 것이다. 사실 리스트의 앞에 push 한 것 뿐인데도 말이다.
- key 속성
- React 는 key를 통해 위의 경우의 문제점을 해결했다.
- React 는 key를 통해 기존 트리와 이후 트리의 자식 노드들을 비교한다.
- key를 통한 비교를 통해 Connecticut 노드가 새로 추가 되었을 뿐 나머지 두개 노드는 변경할 필요 없다는 것을 알게 되었다.
- key 정하는 법
- key는 엘리먼트들의 식별자를 사용 하는 것이 가장 편함
- key는 형제 노드 사이에서 유일하면 되고, 전역적으로 유일 할 필요 없음
- 배열의 인덱스를 key로 사용하는 것은 권장하지 않음
- 목록들이 재배열 (sorting 같은 것) 되는 경우 비효율적으로 동작함
- 재배열 되지 않는 경우는 사용해도 됨
결론
- Virtual DOM 은 React 가 실제 DOM 트리의 효율적인 변경을 위해 메모리 상에만 올려놓은 가상의 DOM 이다.
- root element 타입이 다르면 이전 트리를 완전히 버린다.
- 버려진 노드들은 unmount, 새로 변경된 노드들은 mount 된다. 이를 통해 state가 소실 될 수 있다.
- list node 들을 변경 할 때, key 를 사용하면 react 가 더 효율적으로 DOM 을 변경 해준다.
- key 는 식별자를 사용하는 것이 제일 편하고, 형제들 끼리만 유일하면 된다.
참조
Critical Rendering Path:
Reconciliation: