props, state 를 통해 UI를 어떻게 구성할지 컴포넌트에게 요청하는 작업
루트 컴포넌트에서 시작해서 업데이트가 필요하다고 플래그가 지정된 모든 컴포넌트를 찾는다.
지정되었다면 FunctionComponent()
를 호출하고 렌더링 결과를 저장한다.
// 일반적인 jsx문법
return <SomeComponent a={42} b="testing">Text here</SomeComponent>
// 이것을 호출해서 변환된다.
return React.createElement(SomeComponent, {a: 42, b: "testing"}, "Text Here")
// 호출결과 element를 나타내는 객체로 변환된다.
{type: SomeComponent, props: {a: 42, b: "testing"}, children: ["Text Here"]}
컴포넌트 반환값은 React.createELement()
를 호출 후 반환한다.
createElement
는 UI 구조를 설명하는 React Element를 리턴한다.
모든 컴포넌트에서 이런 렌더링 반환값을 수집한다.
react는 DOM을 가상DOM과 비교하며 변경사항을 수집한다. (이를 reconciliation
라고 한다.)
모든 변경사항을 하나의 동기 시퀀스로 DOM에 적용한다.
Render phase
: 컴포넌트를 렌더링하고 변경사항을 계산하는 모든 작업
Commit phase
: DOM에 변경사항을 적용하는 과정
리액트가 DOM을 업데이트한 후,
요청된 DOM 노드 및 컴포넌트 인스턴스를 가리키도록 모든 참조를 업데이트한다.
그 다음,
useLayoutEffect
훅을 호출한다.
리액트는 짧은 timeout를 세팅한 후, 만료되면 useEffect
를 호출한다.
이를 Passive Effects
라고 한다.
이번에 리액트 18에서 나온 Concurrent Mode의 경우, 브라우저가 이벤트를 처리할 수 있도록 렌더링 단계에서 작업을 일시 중지 할 수 있다.
리액트는 해당 작업을 나중에 다시 시작하거나, 버리거나, 다시 계산할 수 있다.
렌더링이 패스가 된 이후에도, 리액트는 커밋단계를 한단계 동기적으로 실행한다.
렌더링은 DOM을 업데이트하는 것이 아니고
어떤 가시적인 변경 없이도 렌더링 될 수 있다는 것이다.
react가 컴포넌트를 렌더링하는 경우
컴포넌트는 이전과 같은 렌더링 결과물을 리턴해서 아무 변화가 일어나지 않을 수 있다.
Concurrent Mode
에서 react는 컴포넌트 렌더링 작업을 여러번 할 수 있지만,
다른 업데이트로 인해 현재 작업이 무효화되면 매번 렌더링 결과물을 버린다.
최초 렌더링 이후, react가 re-rendering을 queueing 하는 방법은 여려가지가 있다.
useState
의 setter
useReducer
의 dispatch
ReactDOM.render
를 호출하는 것 (react 18 deprecated)
react의 기본적인 동작은 부모 컴포넌트가 렌더링되면, 모든 자식 컴포넌트를 순차적으로 re-rendering한다.
Ex.
A > B > C > D
순서로 컴포넌트 트리가 있다고 가정하자.
B
에 카운터를 올리는 버튼이 있고 이를 클릭했다.
B
의 setState
가 호출되어 B
의 re-rendering이 렌더링 큐로 들어간다.
react는 트리 최상단부터 렌더링 패스를 시작한다.
A
는 업데이트가 필요하다는 플래그가 없으므로 지나친다.
B
는 업데이트가 필요하다는 플래그가 있으므로 B
를 re-rendering한다.
B
는 C
를 리턴한다.
C
는 업데이트가 필요하다는 플래그가 없었다.
그러나 부모인 B
가 re-rendering했으므로 react는 하위 컴포넌트인 C
를 re-rendering한다.
C
는 D
를 리턴한다.
D
도 마찬가지로, 업데이트 플래그가 없지만, 상위 컴포넌트가 re-rendering했으므로 re-rendering한다.
즉,
렌더링은 하위에 있는 모든 컴포넌트를 렌더링하는 것이다.
props
가 변경되었는지 신경쓰지 않는다.
상위 컴포넌트가 rendering하면, 하위 컴포넌트도 무조건 re-rendering 된다.
즉,
루트에서 setState
를 호출한다는 것은, 컴포넌트 트리에 있는 모든 컴포넌트를 re-rendering 한다는 것을 의미한다.
rendering은 순수해야하고 부수작용이 없어야한다.
prop
가 변경되는 것이 부수효과이고 이는 무언가를 망가뜨릴 수 있다.
ajax 호출도 부수효과를 일으키고, 예기치 못한 결과를 만들 수 있다.
rendering 로직이 할 수 없는 것
존재하는 변수나 객체를 변경해서는 안된다.
Math.random()
Date.now()
와 같은 랜덤값을 생성할 수 없다.
네트워크 요청을 할 수 없다.
state
업데이트
rendering 로직이 할 수 있는 것
렌더링 도중에 새롭게 만들어진 객체를 변경
에러 던지기
아직 만들어지지 않은 데이터를 lazy 초기화하는 일(ex. 캐시)
react는 앱에 존재하는 모든 현재 컴포넌트 인스턴스를 추적하는 내부 데이터 구조를 가지고 있다.
이 데이터 구조의 핵심 부분은 아래와 같은 metadata 필드를 포함하고 있는 fiber
라는 객체이다.
컴포넌트 트리의 특정 시점에서 rendering해야하는 컴포넌트 타입의 유형
이 컴포넌트와 관련된 prop, state의 상태
부모, 형제, 자식 컴포넌트에 대한 포인터
react가 rendering 프로세스를 추적하는데 사용되는 기타 metadata
rendering 패스 동안, react는 fiber
객체의 트리를 순회하고
새로운 rendering 결과를 계산한 결과인 업데이트된 트리를 생성한다.
컴포넌트를 rendering하면 fiber
에 연결 후, linkedList를 가져오고
다른 훅을 호출할 때마다 훅에 저장된 적절한 값을 반환한다.
react는 기존 컴포넌트 트리와 DOM 구조를 가능한 많이 재사용하면서 re-rendering 효율성을 추구한다.
동일한 유형의 컴포넌트 or HTML 노드를 트리의 동일한 위치에 rendering하도록 요청하면, react는 해당 컴포넌트 or HTML 노드를 만드는 대신 해당 업데이트만 적용한다.
즉, react에 대한 컴포넌트 타입을 같은 위치에 rendering하도록 요청이 있으면, 컴포넌트 인스턴스를 유지한다.
react rendering 로직은 elements의 type
필드를 ===
를 통해 먼저 비교한다.
element가