12. React Compiler 이해하기
https://emewjin.github.io/understanding-react-compiler/
- 리액트의 핵심 아키텍쳐는 사용자가 제공한 함수(컴포넌트)를 반복해서 호출하는 것
- 이는 멘탈모델을 단순화 하여 인기를 얻었으나, 성능 문제가 발생 할 수 있는 지점을 만듦
- 일반적으로 함수가 비용이 큰 작업을 수행 시, 앱이 느려진다.
- 따라서 개발자들이 직접 어떤 함수를 어떻게 다시 실행해야하는지 지정해줬어야 함.
- 리액트팀은 리액트 컴파일러라는 도구로, 개발자가 수작업으로 해야하던 성능튜닝을 자동화함.
컴파일 및 최적화
-
리액트 컴파일러는, 사용자가 작성한 것과 기능적으로 동일한 코드를 생성하지만,
-
그 코드의 일부를 리액트 개발자가 작성한 코드에 대한 호출로 감싸준다.
-
이렇게 하면 작성자가 의도한 것에 대해서 더 많은 것도 수행하는 것으로 코드가 재작성된다.
-
생성되는 코드는 궁극적으로 AST (Abstract Syntax Tree)와 다른 중간 언어에서 비롯된다.
-
리액트 컴파일러의 경우도 AST 와 중간언어를 모두 사용하여 작성한 코드로부터 새로운 리액트 코드를 생성한다.
- 리액트 컴파일러도 리액트 자체와 마찬가지로 다른 사람의 코드에 불과하다.
- 따라서, 컴파일러 / 트랜스파일러 / 옵티마아저 등을 신비한 블랙박스라고 생각하지 말기.
리액트의 핵심 아키텍쳐
function App(){
// items fetching..
return <List items={items}/>
}
function List({items}) {
const pItems = processItems(items);
const listItems = pItems.map((item)=> <li>{item}</li>)
return <ul>{listItems}</ul>
}
-
함수는 자식을 포함하는
ul
객체(여러개의 li객체)와 같은 일반 자바스크립트 객체를 반환한다. -
ul과 li와 같은 객체 중 일부는 리액트에 내장되어있다.
-
다른 객체들은
List
와 같이 우리가 직접 생성하는 객체들. -
궁극적으로 리액트는 이러한 모든 객체에서 React Fiber트리라고 불리는 트리를 만든다.
-
트리의 각 노드를 파이버 또는 파이버 노드라고 부른다.
-
UI를 표현하기 위해 노드로 구성 된 자체 자바스크립트 객체 트리를 만드는 것을 VDOM만들기라고 한다.
-
리액트는 트리의 각 노드에서 두 개의 분기를 유지한다.
- 분기 1: 해당 분기의 현재 상태를 나타내며 DOM과 일치
- 분기 2: 작업 진행 중 상태를 나타내며, 함수가 다시 실행 될 때 반환된 트리와 일치
-
그런 다음, 두 트리를 비교하여 실제 DOM에 어떤 변경이 필요한지 DOM이 작업진행중 상태의 트리와 일치되게 만든다. 이 과정을 재조정(Reconcillation)이라고 한다.
-
따라서, 앱에 추가하는 기능에 따라 리액트가 UI를 업데이트 해야한다고 생각할 때 마다,
List
함수가 반복되어 호출될 수 있음. -> 요건 정말 리액트의 멘탈모델을 간단하게 만든다. -
But..processItems 함수가 매우 느리다면?! List 대한 모든 호출이 느려지고, 앱 전체가 느려질 수 밖에 없다.
리액트 컴파일러
컴파일 전
function App() {
return <List items={items} />;
}
function List({ items }) {
const [selItem, setSelItem] = useState(null);
const [itemEvent, dispatcher] = useReducer(reducer, {});
const [sort, setSort] = useState(0);
const pItems = processItems(items);
const listItems = pItems.map((item) => <li>{item}</li>);
return <ul>{listItems}</ul>;
}
컴파일 후
function App() {
const $ = _c(1);
let t0;
if ($[0] === Symbol.for('react.memo_cache_sentinel')) {
t0 = <List items={items} />;
$[0] = t0;
} else {
t0 = $[0];
}
return t0;
}
function List(t0) {
const $ = _c(6);
const { items } = t0;
useState(null);
let t1;
if ($[0] === Symbol.for('react.memo_cache_sentinel')) {
t1 = {};
$[0] = t1;
} else {
t1 = $[0];
}
useReducer(reducer, t1);
useState(0);
let t2;
if ($[1] !== items) {
const pItems = processItems(items);
let t3;
if ($[3] === Symbol.for('react.memo_cache_sentinel')) {
t3 = (item) => <li>{item}</li>;
$[3] = t3;
} else {
t3 = $[3];
}
t2 = pItems.map(t3);
$[1] = items;
$[2] = t2;
} else {
t2 = $[2];
}
const listItems = t2;
let t3;
if ($[4] !== listItems) {
t3 = <ul>{listItems}</ul>;
$[4] = listItems;
$[5] = t3;
} else {
t3 = $[5];
}
return t3;
}
const $ = _c(6);
_c
함수는 훅을 사용하여 저장되는 배열을 생성한다.- 위의 코드에서는 계속해서 캐시배열에 저장하고, 그게 아니면 캐시 배열에서 가져오고 있다.
- 이런식으로 리액트는 값을 캐싱하고, 함수 호출의 결과를 자동으로 메모이제이션 한다.
- 출력되는 코드는 우리가 작성한 코드와 기능적으로 동일하나, 캐싱과 관련된 코드가 포함되어 있으므로 리액트에서 함수를 반복해서 호출 시, 성능이 저하되는 것을 방지 할 수 있다.
장치 메모리를 위한 프로세서 주기 트레이딩
- 메모이제이션 / 캐싱은 처리랴을 메모리로 교환한다.
- 프로세서가 비용이 큰 연산을 실행하는 부담을 줄이는 대신, 메모리의 공간을 내어주는 것