본 글은 리엑트를 왜 사용하는가에 대해 알기 위해서 "가상 돔과 리엑트의 렌더링 과정"을 중심으로 살펴본다.
목차
1. 브라우저의 동작 과정1: 최초 렌더링
2. 브라우저의 동작 과정2: 업데이트와 DOM 수정
2. 가상 돔 개념의 등장
3. react의 두 가지 동작 프로세스(render, commit)
4. 업데이트와 재조정(reconciliation)
5. 의의
1. 브라우저의 동작 과정1: 최초 렌더링
리엑트를 알려면 가상 돔이 왜 등장했는지 알아야 하고, 가상 돔의 등장 이유를 알려면 브라우저의 동작과정을 알아야 한다. 우선은 브라우저의 동작과정 중 최초 렌더링을 살펴보자.
html 파일 파싱, css 파일 파싱
브라우저는 서버로부터 받은 html 파일을 위에서부터 아래로 한줄 씩 파싱하여 DOM트리로 전환한다. DOM은 html을 브라우저가 메모리에 저장하고 다루기 용이한 객체 형태로 전환한 산물이다. html 파싱 중에 link나 style 태그 형식의 css 파일을 만나면 html 파싱을 중단하고 css를 파싱하여 CSSOM 트리를 만든다. 이는 엘리먼트(DOM 상에서는 노드)의 스타일 정보를 가지고 있다.
즉, DOM은 노드 간 관계와 속성, dom api 등을 가지고, CSSOM 노드의 스타일 정보를 가진다.
attachment, render tree 생성
DOM과 CSSOM이 생성되면 두 트리를 결합한다(attachment). 이렇게 결합된 트리를 render tree라 부른다
layout
render tree의 정보를 기반으로 레이아웃 규칙에 따라 요소들의 위치를 할당한다. 렌더 트리에는 요소 간의 종속 관계나 형제 관계, 스타일 값이 들어있지만, 이런 값들은 레이아웃 규칙에 따라 배치 양상이 달라진다. 이 부분은 연산비용이 크게 소모된다.
painting
레이아웃 작업이 끝난 후 요소를 유저가 볼 수 있도록 그려내는 작업이다. 이 부분은 연산비용이 크게 소모된다.
2. 브라우저의 동작 과정2: 업데이트와 DOM 수정
최초의 렌더링 이후, 자바스크립트 파일이나 script 태그를 이용해서 웹사이트는 유저와 상호작용을 할 수 있다. 그리고 이 과정에서 이벤트를 트리거하여 DOM을 수정한다. 자바스크립트가 DOM을 수정하면 브라우저는 이를 감지하고 위 렌더링 과정을 다시 반복한다. 이런 과정이 너무 자주 일어나면 브라우저 성능에 악영향을 끼치는데, 특히 layout과 painting은 연산 비용이 많이 많이 들어가는 작업이라 문제가 된다.(리렌더링 시 layout 작업을 reflow, painting 작업을 repainting)이라 부른다.
이런 고민은 웹사이트가 정적이던 과거 시기에는 크게 문제가 되지 않았다. 하지만, AJAX와 SPA 개념이 등장하면서 DOM을 조작할 일이 많아지고 사이트의 규모가 커지자 이 문제가 불거졌다. 그래서 이 문제를 해결하려면 필요한 연산 작업을 최대한 모아서 하고 DOM수정은 한번에 해야 했다.
2. 가상 돔 (virtual DOM)개념의 등장
가상 돔은 이런 문제를 해결할 방안 중 하나로 주목받았다. 특히 사이트 규모가 커짐에 따라 복잡한 계산을 모두 개발자가 직접하기에는 부담이 컸으므로 가상 돔은 더욱 중요해졌다.
그렇다면, 가상 돔이 무엇일까?
가상 돔은 브라우저의 실제 돔을 복제했지만, 일부 프로퍼티와 DOM API를 제외하여 가벼운 객체값이다. 예컨대, 리엑트에서 React.createElement("div")로 만든 요소는 실제 요소와 다르게 객체 내부에 프로퍼티가 아주 적다(요소의 종류, 속성, 스타일, children 정도가 전부다).
이 가상돔을 조작하면 실제 렌더링이 촉발되지 않고 메모리 상에서만 연산 작업이 이뤄진다. 그러니 실제 돔 조작보다 거의 모든 상황에서 훨씬 가볍고 빠를 수 밖에 없다. (모든 상황에서 실제 돔 조작보다 빠른 건 아니다. 그 이유는 나중에 살펴보자) 모든 연산 작업이 가상 돔 안에서 끝나고 나면, 이 변경 사항을 실제 돔에 반영한다.
결과적으로 연산작업을 모두 모아서 처리하고 실제 돔 조작을 최소화함으로써 성능을 크게 향상시킬 수 있다.
3. react의 두 가지 동작 프로세스(render, commit)
이제 리엑트의 돔 생성 및 업데이트 과정을 더 자세히 알아보자. 리엑트에서 개발자는 상태값을 변경하는 코드를 작성하고 렌더링 부분은 리엑트가 자동으로 진행한다. 리엑트의 렌더링 과정은 render와 commit으로 구분된다.
render
(1) 컴포넌트 호출
최초의 렌더링 과정에서 먼저 리엑트는 각 컴포넌트를 호출하여 react element를 생성한다. 일반적으로 많이 사용하는 함수형 컴포넌트에서 react element는 컴포넌트의 return 값인 JSX에 해당한다. JSX는 바벨의 컴파일을 거쳐서 react.createElement 등의 코드로 전환딘다. 이렇게 생성된 요소는 type, props, children 등 간단한 구조를 가지고 있다.
컴포넌트의 호출 과정은 재귀적이다. 예를 들면 App 컴포넌트 내부에는 순서대로 Box1, Box2 컴포넌트가 있고, 각 컴포넌트는 Box1__title, Box2__title 컴포넌트가 있다. 이러면 render 프로세스에서 컴포넌트 호출 시,
"App 호출>> Box1 >> Box1__title, Box2 (내부 컴포넌트 더 없음) >> Box2 >> Box2__title" 순으로 호출된다.
(2) Fiber node
파이버 노드는 상태값을 저장하는 공간으로 컴포넌트 단위로 생성된다. 최초의 렌더링 때는 state의 초기 값을 저장한다.
(3) 가상 돔 트리 생성
컴포넌트 호출로 생성된 리엑트 엘리먼트들은 마치 실제 돔처럼 연결되고 구조화된다. 이는 객체값의 형태로 표현된다.
commit
생성된 가상 돔을 토대로 실제 돔에 반영하여 브라우저의 렌더링 과정을 일으킨다.
정리하자면, 렌더링 단계에서 컴포넌트 호출로 가상 돔을 생성하고 파이버 노드에 상태값을 저장해서 연결한 후, 가상 돔을 구조화한다. 이후, 커밋 단계에서 가상 돔을 토대로 실제 돔에 반영하여 브라우저의 렌더링 과정을 일으킨다.
4. 업데이트와 재조정(reconciliation)
그렇다면, 상태값을 변경할 때 어떻게 UI가업데이트될까?
먼저, setter 등의 함수로 상태값을 변경하면 리엑트는 이를 감지하고 해당 값이 할당되어 있는 컴포넌트의 업데이트를 시작한다.
- 렌더링 프로세스에 따라 호출된 컴포넌트는 초기 상태값 대신, 파이버에 저장된 변경된 상태값을 가지고 리엑트 엘리먼트를 만든다.
- 재귀적 호출 법칙에 따라 하위 컴포넌트 역시 렌더링 프로세스가 발생한다.
디핑
리엑트는 differ 기능의 함수로 이전의 가상 돔과 새로운 가상 돔을 인자 삼아 둘을 비교한다. 그리고 변화된 부분을 체크하여 반환한다.
디핑의 알고리즘은 크게 두가지 경우다.
- 동일 위치 상의 엘리먼트가 타입이 동일할 시 | 내부 프로퍼티 중 변경 사항만 갱신, 돔 유지
- 다른 타입의 엘리먼트가 존재할 시 | 이전 가상 돔의 엘리먼트를 파괴, 새 가상 돔의 엘리먼트를 추가함.
이 부분은 리엑트에서 map 등의 반복적으로 생성된 요소에 key props를 추가하는 이유와 관련 있다. 배열로 만든 엘리먼트 사이에 새로운 걸 추가하거나 제외할 때 key props가 없다면 리엑트는 새로 추가된 요소보다 뒤에 있는 요소들을 모두 새로운 엘리먼트로 간주하고 전체를 갱신할 것이다. 반면, key props를 추가하면 배열로 만든 요소들 중간에 새로운 걸 추가해도 이 후의 요소들이 기존에 있던 요소들임을 인지할 수 있고 갱신 작업을 취소할 수 있다.
key props는 변하지 않는 고유한 값을 사용하는 게 좋다. 만약 배열의 회귀함수에서 index 값을 사용한다면, 배열 내 새로운 요소가 추가되거나 제거됨에 따라 key props도 변하므로 key의 사용 의의가 사라지니 주의하자.
재조정
이 반환 값은 patch 기능의 함수에서 실제 돔에 반영되고 결과적으로 가상 돔의 변화사항과 실제 돔을 일치시키는 작업이 이뤄진다. 이를 재조정(reconciliation)이라 부른다.
5. 의의
이제 자바스크립트의 실제 돔 업데이트 과정과 리엑트의 돔 업데이트 과정을 보면 차이를 알 수 있다.
- 최적화되지 않은 자바스크립트 | 연산 작업=> 실제 돔 수정 => ....(반복)=> 연산 작업 => 실제 돔 수정
- 리엑트 | 연산작업 => 가상 돔 수정 => ...(반복) => 디핑 및 재조정 => 실제 돔 수정
이처럼 리엑트는 "렌더링 없이 메모리에서만 적은 비용으로 수행되는 렌더링 작업 후 커밋 작업"을 함으로써 실제 돔 수정은 최소화한다.
역으로 말하자면, 한꺼번에 일어나는 돔 수정이 별로 없는 정적 웹사이트의 경우, 오히려 리엑트가 추가 작업을 하여 성능이 더 나쁠 수도 있다.
'react' 카테고리의 다른 글
[React] 리엑트 살펴보기3: 컴포넌트와 hooks의 등장배경(클래스 컴포넌트, 함수 컴포넌트) (0) | 2024.03.10 |
---|---|
[React] 리엑트 살펴보기2: 리렌더링 조건, 리렌더링 최적화 방법(memo, useMemo, useCallback, children props) (0) | 2024.03.04 |
[React] 컴포넌트 외부에 변수 선언과 컴포넌트 내부에 변수 선언 간 차이 (0) | 2024.03.04 |
[React] pagination: 무한 스크롤 구현하기(+SWR 라이브러리) (0) | 2024.02.19 |
[React] url에 특정한 쿼리스트링을 강제로 추가하기 (0) | 2024.02.18 |