리액트에서 함수형 컴포넌트는 함수 내부에 비즈니스 로직을 담고 HTML 태그 형태를 반환하는 구조를 가진다.
이런 형태를 가진 파일은 JSX라 하고 이 JSX는 바벨에 의해 컴파일 되면 React.createElement()의 형태로 변환된다.
컴파일 전
const element = (
<h1 className="greeting">
Hello, world!
</h1>
);
컴파일 후
const element = React.createElement(
'h1',
{className: 'greeting'},
'Hello, world!'
);
컴파일이 되면
createElement() 함수에 컴포넌트 태그, 속성이 들어간 객체, 하위 children이 인자로 들어간다.
export function createElement(type, config, children) {
let propName;
// Reserved names are extracted
const props = {};
let key = null;
let ref = null;
let self = null;
let source = null;
// ref와 key가 있는지 확인
if (config != null) {
if (hasValidRef(config)) {
ref = config.ref;
}
if (hasValidKey(config)) {
key = '' + config.key;
}
// 중복확인 후 속성 부여
for (propName in config) {
if (
hasOwnProperty.call(config, propName) &&
!RESERVED_PROPS.hasOwnProperty(propName)
) {
props[propName] = config[propName];
}
}
}
// Children can be more than one argument, and those are transferred onto
// the newly allocated props object.
const childrenLength = arguments.length - 2;
if (childrenLength === 1) {
props.children = children;
} else if (childrenLength > 1) {
const childArray = Array(childrenLength);
for (let i = 0; i < childrenLength; i++) {
childArray[i] = arguments[i + 2];
}
props.children = childArray;
}
// Resolve default props
if (type && type.defaultProps) {
const defaultProps = type.defaultProps;
for (propName in defaultProps) {
if (props[propName] === undefined) {
props[propName] = defaultProps[propName];
}
}
}
return ReactElement(
type,
key,
ref,
self,
source,
ReactCurrentOwner.current,
props,
);
}
const ReactElement = function(type, key, ref, self, source, owner, props) {
const element = {
// This tag allows us to uniquely identify this as a React Element
$$typeof: REACT_ELEMENT_TYPE,
// Built-in properties that belong on the element
type: type,
key: key,
ref: ref,
props: props,
// Record the component responsible for creating this element.
_owner: owner,
};
return element;
};
ReactElement함수는 받은 인자들을 객체로 정리해서 반환해주는 역할이다.
결국
<h1 className="greeting">
Hello, world!
</h1>
형태의 태그는
const element = {
type: 'h1',
props: {
className: 'greeting',
children: 'Hello, React!'
}
}
의 형태로 반환된다.
결국 리액트는 React Element 객체들이 중첩된 형태로 메모리에 가상의 돔형태로 저장해 관리되는 것이다.
Fiber는 리액트에서 만든 reconciler 개념으로 리액트 v16부터 이전에 사용하던 Stack을 대체하여 사용하게되었다.
Stack은 v16이전 까지 React에서 사용하던 reconciler 이다.
이름처럼 자료구조 stack을 따온 구조이기 때문에 last-in first-out 메커니즘을 따른다.
이런 stack 구조를 브라우저에서 사용하면 문제가 프레임 드랍을 유발할 수 있다.
프레임은 사람이 화면을 볼때 1초에 30프레임은 되어야 화면이 끊김없이 매끄럽게 보여진다고 인식하는데 대부분의 기계들은 60프레임은 설정되어있다. 이를 초로 계산하면 1/60 = 16.67ms 즉, 16ms의 속도로 화면이 깜빡여야한다. 리액트에서 만약 16초보다 길게 렌더링작업을 한다면 프레임은 떨어지고 화면 끊김이 발생해 사용자에게 불편을 줄 것이다.
브라우저는 싱글스레드로 작동하기 때문에 브라우저에서 ui 작업과 자바스크립트 렌더링 작업을 번갈아서 작업해야한다.
이때 stack 구조로 리액트 작업을 하면 크게 두가지 문제가 발생한다.
stack에서는 특정 컴포넌트에서 업데이트가 발생하면 전체 컴포넌트를 다시 렌더링한다.
한번 시작한 렌더링은 모든 컴포넌트를 렌더링할 때까지 브라우저 콜스택이 자바스크립트에서 점유를 하게되고, 렌더링 시간이 오래걸리게 되면 프레임 드랍 문제가 발생한다.
렌더링 과정에서 사용자 이벤트 같은 즉각적인 반응을 원하는 이벤트들이 우선적으로 렌더링되어야 사용자 경험에 문제가 생기지 않는데 이런 우선순위를 구분할 장치가 없다.
우선되어야 할 이벤트들이 뒤로 밀리면 이또한 프레임 드랍이 발생한다.
이런 stack의 문제를 개선하기 위해 React에서 만든 구조가 Fiber이다.
Fiber의 특징은
먼저, 렌더링 작업 도중에 중지하고 후에 중지한 부분부터 재시작할 수 있기 때문에 렌더링 중간에 콜스택에서 자바스크립트 작업을 끊어줄 수 있다는 것이다.
그러면 렌더링 중간에 ui작업이 가능해지므로 프레임드랍이 생기는 경우가 현저히 낮아지게 된다.
작업에 우선순위가 생기기 때문에 애니메이션이나 사용자 이벤트같은 경우 즉각적으로 반응해 렌더링이 가능해졌다.
또 fiber노드는 연결리스트로 이어져 있기때문에 리렌더링을 할때 모든 컴포넌트를 다시 렌더링할 필요가 없고 간단히 복제를 해서 업데이트할 수 있다.
const Name = (props) => {
return(
<div className="name">
{props.name}
</div>
<span>name</span>
)
}
Name 컴포넌트의 child는 div
Sibling: 형제 컴포넌트가 연결리스트로 이어져있다. 위의 예시에서 div의 sibling는 span
Return: 부모컴포넌트
Alternate: 현재가 current이면 workInProgress에 복제된 자기자신이고 반대의 경우도 마찬가지
Output: leaf 노드. div나 span같은 호스트 컴포넌트들이다.
리액트 엘리먼트와 fiber노드에 대해서 살펴봤다.
다음은 이런 노드를 가지고 렌더링하는 과정을 보겠다.
render phase에는 컴포넌트 마다 크게
performUnitOfWork()
beginWork()
completeUnitOfWork()
completeWork()
가 있다.
이 순서로 계속 진행되는게 아니라
시작 노드부터 모든 children을 타고가면서 perform,begin을 하고
그 뒤에 complete 작업을 한다.