-
가상돔Study/React 2021. 9. 18. 18:25
리액트는 렌더링 성능을 위해 가상 돔을 활용한다.
브라우저에서 돔을 변경하는 것은 비교적 오래 걸리는 작업이다. 따라서 빠른 렌더링을 위해서는 돔 변경을 최소화 해야한다.
그래서 리액트는 메모리에 가상 돔을 올려 놓고 이전과 이후의 가상 돔을 비교해서 변경된 부분만 실제 돔에 반영하는 전략을 채택했다.
리액트 요소 이해하기
JSX문법으로 작성된 코드는 리액트의 createElement 함수로 변경된다.
리액트가 UI를 표현하기 위해 사용하는 리액트 요소의 구조를 살펴보자.
const elementJSX = <a href="https://shinyeong.com">click here</a> const element = React.createElement( 'a', { href: 'https://shinyeong.com' }, 'click here', );
첫줄의 elementJSX코드는 아래의 element 코드로 변경된다.
리액트 요소가 돔 요소로 만들어지는 과정
하나의 화면을 표현하기 위해 여러 개의 리액트 요소가 트리(tree)구조로 구성된다.
프로그램 화면은 여러 가지 이벤트를 통해서 다양한 모습으로 변화한다.
하나의 리액트 요소 트리는 시간에 따라 변화하는 화면의 한 순간을 나타낸다.
리액트에서 데이터 변경에 의한 화면 업데이트는 '렌더 단계(render phase)와 커밋단계(commit phase)를 거친다.
렌더 단계
실제 돔에 반영할 변경 사항을 파악하는 단계. 변경사항을 파악하기 위해 가상 돔을 이용한다.
커밋 단계
파악된 변경사항을 실제 돔에 반영하는 단계
가상돔
가상돔은 리액트 요소로부터 만들어진다.
리액트는 렌더링을 할 때마다 가상돔을 만들고 이전의 가상 돔과 비교한다.
이는 실제 돔의 변경 사항을 최소화하기 위한 과정이다.
리액트의 가상돔 과정은 다음과 같다.
변경사항이 생기면 ReactDOM.render()가 호출된다.
해당 과정은 위에서 언급했듯이 변경점을 찾는 과정(렌더 단계)과 변경점을 실제 UI에 적용하는 과정(커밋단계)으로 나뉜다.
- [변경점을 찾는 작업] React는 Virtual DOM을 통하여 브라우저 DOM에 전달하기 전에 Reconciliation라는 비교 과정을 선행하기 때문에 UI에 대한 제어를 최소화 시킨다.
- Render가 진행 될 때마다, 각 Element들은 key를 부여 받고, 해당 key를 통해 같은 Element라고 인식 되고 비교 된다. (물론, 계층이 달라지게 될 경우는 제외된다. 또, React diff 알고리즘은 필터 관련 기능에 취약하다고 한다. 많은 list를 필터하는 경우, 이것을 기억하고 해결책을 찾아보기로 기약하자.)
- 이런 비교 과정을 최소화 시키기 위해 shouldComponentUpdate(), PureComponent 를 활용한다.
코드로 살펴보는 리액트의 가상돔 과정
다음 코드는 할일의 우선순위를 상탯값으로 관리하는 코드다.
할일의 제목과 내용은 부모 컴포넌트가 속성 값으로 내려준다.
class Todo extends React.Component { state = { priority: 'high', }; onClick = () => { const { priority } = this.state; this.setState({ priority: priority === 'high' ? 'low' : 'high' }); }; render() { const { title, desc } = this.props; const { priority } = this.state; return ( <div> <Title title={title} /> <p>{desc}</p> <p>{priority === 'high' ? '우선순위 높음' : '우선순위 낮음'}</p> <button onClick={this.onClick}>우선순위 변경</button> </div> ); } }; class Title extends React.PureComponent { render() { const { title } = this.props; return <p style={{ color: 'blue' }}>{title}</p> } }; ReactDOM.render( <Todo title="리액트 공부하기" desc="실전 리액트를 열심히 읽는다" /> document.getElementById('root'), );
버튼을 클릭하면 priority의 상탯값이 변화되고 화면을 다시 그린다.
PureCoponent로 만들어진 Title 컴포넌트는 상탯값이나 속성값이 변경될 때만 render 메서드를 호출한다.
ReactDOM.render 함수로 전달된 리액트 요소 트리의 구조는 다음과 같다.
const initialElementTree = { type: Todo, props: { title: '리액트 공부하기', desc: '실전 리액트를 열심히 읽는다', }, // ... };
리액트가 initialElementTree를 이용해서 실제 돔을 만드는 과정을 따라가 보자.
먼저 Todo 컴포넌트의 렌더링 결과를 얻기 위해 Todo 컴포넌트의 렌더 함수를 호출한다.
// Todo 컴포넌트의 렌더 함수 호출 결과 const elementTree = { type: 'div', props: { children: [ { type: Title, props: { title: '리액트 공부하기' }, //... }, { type: 'p', props: { children: '리액트 스터디 모임을 갖는다' }, // ... }, { type: 'button', props: { onClick: function() { /* Todo 컴포넌트의 onClick 함수 */ }, children: '우선순위 변경', }, //... }, ], }, // ... }
트리의 루트는 div 태그로 변경된다.
아직 Title 컴포넌트가 존재하기 때문에 이 트리를 실제 돔으로 만들 수는 없다.
리액트 요소 트리가 실제 돔으로 만들어지기 위해서는 모든 리액트 요소의 type 속성 값이 문자열이어야 한다.
이는 type 속성값이 문자열이어야 HTML 태그로 변환할 수 있기 때문이다.
그러기 위해서는 모든 컴포넌트의 렌더 함수가 호출되어야 한다.
// Title 컴포넌트의 렌더 함수 호출 결과 const elementTree = { type: 'div', props: { children: [ { type: 'p', props: { children: [ { type: 'p', props: { style: { color: 'blue' }, children: '리액트 공부하기', }, //... }, { type: 'p', props: { children: '리액트 스터디 모임을 갖는다' }, // ... }, { type: 'button', props: { onClick: function() { /* Todo 컴포넌트의 onClick 함수 */ }, children: '우선순위 변경', }, //... }, ], }, // ... } ] } }
Title 컴포넌트로 표현됬던 리액트 요소가 p태그로 변경되었다.
이제 모든 리액트 요소의 type 속성 값이 문자열이므로 실제 돔을 만들 수 있다.
이와 같이 실제 돔을 만들 수 있는 리액트 요소 트리를 '가상 돔'이라고 한다.
더보기가상돔 : 최종 리액트 요소 트리를 만들기 위해 치환되는 컴포넌트의 리액트 요소도 메모리에 유지된다. 메모리에 저장된 컴포넌트의 리액트 요소는 렌더 단계의 효율을 높이는데 사용된다. 가상 돔은 UI에서 변경된 부분을 빨리 찾기 위한 개념이므로 컴포넌트의 리액트 요소도 가상 돔의 일부라고 생각할 수 있다.
최초의 리액트 요소 트리로부터 가상 돔을 만들고 이전 가상 돔과 비교해서 실제 돔에 반영할 내용을 결정하는 단계를 위에서 말했듯이 '렌더단계'라 한다.
리액트는 화면을 업데이트할 때, 이전의 가상 돔과 현재의 가상 돔을 비교해서 변경된 부분만 실제 돔에 반영한다.
브라우저에서 실제 돔을 변경하는 작업은 다른 작업에 비해 시간이 오래 걸리기 때문에 꼭 필요한 부분만 변경하는 것이 중요하다.
이 랜더단계는 ReactDOM.render 함수와 setState 메서드에 의해 시작된다.
지금까지의 관계를 정리하자면 아래와 같다.
지금까지 리액트 요소를 이용해 렌더 단계를 설명했지만, 엄밀히 말하면 리액트 요소는 파이버(fiber)라는 구조체로 변환된다. 파이버는 리액트 버전 16부터 도입된 구조체 이름이다. 파이버도 리액트 요소와 같이 속성값을 가지며 모든 type 속성값이 문자열이 될 때까지 연산하다는 사실에는 변함이 없다.
참조자료 :
- 실전 리액트 프로그래밍
- https://ryublock.tistory.com/41
'Study > React' 카테고리의 다른 글
Loading, Error 컴포넌트 생성하기 - 1. Skeleton Loading (0) 2021.07.11 코드기록:: map 메소드 활용한 feed (0) 2020.07.19 React Router (0) 2020.07.19 hook을 사용하는 이유 (0) 2020.07.16 fetch 함수와 활용예제. (0) 2020.07.14