JSX로 작성한 컴포넌트들이 어떻게 React element로 변환되고, VDOM에 올리기 위해 어떤 방식으로 fiber로 확장하는지 알아보기
Host Component - div / input / button / etc -> HTML element 요소
Custom Component - 우리가 가장 자주 쓰는 컴포넌트 Function / Class Component
Static Component - Fragment / lazy / Context / memo
export function createElement(type, config, children) {
/*...*/
}
type: 컴포넌트의 종류에 따라 다음의 값이 사용됩니다.
호스트 컴포넌트: 태그 이름
커스텀 컴포넌트: 작성한 함수
스태틱 컴포넌트: 미리 정의된 Symbol 또는 memo, lazy와 같이 함수 호출을 통해 만들어진 React element
config: props를 담고 있는 객체입니다.
children: 코드에서는 자식을 하나의 인자가 받도록 되어 있지만 실제로는 여러 자식이 올 경우 3 ~ n번째의 인자에 자식들이 들어옵니다. function createElement(type, config, children1, children2, ..childrenN)의 형태가 됩니다.
// 우리가 자주 쓰는 jsx형태의 Component
const CustomComponent = () => '';
const App = () =>
<>
<CustomComponent />
<div id="host_component"></div>
</>
// Babel을 이용하여 Compnent -> React element로 바꾸기
const App = () => (
React.createElement(
React.Fragment, // type: Symbol.for('react.fragment')
{}, // config
React.createElement( // 첫번째 자식
CustomComponent, // type: function
{}, // config
// 자식은 없음
),
React.createElement( // 두번째 자식
"div", // type: tag name
{ id: "host_component" } // config
// 자식은 없음
)
);
)
// createElement가 무엇인가?
const RESERVED_PROPS = {
key: true,
ref: true,
};
function createElement(type, config, children) {
/* 1 React element에는 key와 ref라는 예약된 속성들이 존재합니다.
config에 예약 속성을 제외한 나머지를 props 객체에 저장합니다. */
let propName;
const props = {};
let key = null;
let ref = null;
if (config != null) {
if (hasValidRef(config)) { // config.ref !== undefined;
ref = config.ref;
}
if (hasValidKey(config)) { // config.key !== undefined;
key = '' + config.key;
}
// 예약된 속성을 제외한 나머지를 props 객체에 저장
for (propName in config) {
if (
/* 좀 더 쉽게 설명하기
config 객체에서만 사용할 수 있는 propName이 존재하며,
hasOwnProperty.call(config, propName)
&& RESERVED_PROPS(key랑 ref)가 아닌 거 */
hasOwnProperty.call(config, propName) &&
!RESERVED_PROPS.hasOwnProperty(propName)
) {
// 해시테이블 형식으로 저장
props[propName] = config[propName];
}
}
}
// 2 복수의 자식이 넘어온다면 하나의 배열에 저장합니다.
// argument란? 함수 내부에 들어오는 모든 인수들 저장소(배열로 저장됨 )-> js에서 함수 객체에 사용되는 프로퍼티
// 즉 arguments는 type, config, childern들이 저장되어 있는 배열인데, type이랑 config를 빼면 childrenLength가 됨.
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;
}
// 3 default props가 존재할 경우 props 객체에 적용합니다.
// 16 version function Component로 바뀌면서
// 잘 안 쓰임
if (type && type.defaultProps) {
const defaultProps = type.defaultProps;
for (propName in defaultProps) {
if (props[propName] === undefined) {
props[propName] = defaultProps[propName];
}
}
}
// 4 1 ~ 3에서 작성한 정보를 가지고 React element 객체를 만듭니다.
return ReactElement(
type,
key,
ref,
props,
/*...*/
);
}
fiber는 tag를 통해 판단한다.
const createFiber = function(
tag: WorkTag, // fiber 고유(어떤 형식인지)
pendingProps: mixed, // 바꿀 props
key: null | string, // 어떤 element인지 판단하는
mode: TypeOfMode, // mode
): Fiber {
return new FiberNode(tag, pendingProps, key, mode);
};
createFiberFromFragment는 포장지라고 생각하면 됨 prop가 존재하지 않고, children만 존재
Text는 React Native에서 사용하는 것이므로 패스
export function createFiberFromElement(
element: ReactElement,
mode: TypeOfMode,
expirationTime: ExpirationTime,
): Fiber {
const type = element.type;
const key = element.key;
const pendingProps = element.props;
const fiber = createFiberFromTypeAndProps(
type,
key,
pendingProps,
mode,
expirationTime,
);
return fiber;
}
fiber가 어떤 fiber인 지 구분하는 함수
import {
IndeterminateComponent,
ClassComponent,
MemoComponent,
/*...*/
} from 'shared/ReactWorkTags'; // fiber tag
import {
REACT_FRAGMENT_TYPE,
REACT_MEMO_TYPE,
/*...*/
} from 'shared/ReactSymbols'; // 스태틱 컴포넌트
export function createFiberFromTypeAndProps(
type: any,
key: null | string,
pendingProps: any,
mode: TypeOfMode,
expirationTime: ExpirationTime,
): Fiber {
let fiber;
// IndeterminateComponent는 undefined과 같은 존재
// fiber의 tag가 아직 아무것도 정해져 있는 상태
let fiberTag = IndeterminateComponent;
let resolvedType = type;
if (typeof type === 'function') {
// class component
if (shouldConstruct(type)) { // type.prototype && type.prototype.isReactComponent;
fiberTag = ClassComponent;
}
//else?? 여기서는 함수형 컴포넌트라고 단정지을 수 없습니다.
// type이 string이면 호스트 컴포넌트 입니다. ex) 'div', 'input'..
} else if (typeof type === 'string') {
fiberTag = HostComponent;
// 이하 스태틱 컴포넌트
} else {
getTag: switch (type) {
case REACT_FRAGMENT_TYPE:
return createFiberFromFragment(
pendingProps.children,
mode,
expirationTime,
key,
);
case REACT_CONCURRENT_MODE_TYPE:
fiberTag = Mode;
mode |= ConcurrentMode | BlockingMode | StrictMode;
break;
/*...*/
default: {
if (typeof type === 'object' && type !== null) {
switch (type.$$typeof) {
case REACT_MEMO_TYPE:
fiberTag = MemoComponent;
break getTag;
case REACT_LAZY_TYPE:
fiberTag = LazyComponent;
resolvedType = null;
break getTag;
/*...*/
}
}
let info = '';
invariant(
false,
'Element type is invalid: expected a string (for built-in ' +
'components) or a class/function (for composite components) ' +
'but got: %s.%s',
type == null ? type : typeof type,
info,
);
}
}
}
fiber = createFiber(fiberTag, pendingProps, key, mode);
fiber.elementType = type;
fiber.type = resolvedType;
fiber.expirationTime = expirationTime;
return fiber;
}
function Todo() {
const [todos, push] = useState([]);
return (
<>
<Input submit={todo => push(todos => [...todos, todo])} />
<OrderList list={todos} />
</>
);
};
function Input({ submit }) {
const [value, setValue] = useState("");
return (
<>
<input value={value} onChange={e => setValue(e.target.value)} />
<button
onClick={() => {
submit(value);
setValue("");
}}
>
submit
</button>
</>
);
}
function OrderList({ list }) {
return (
<ol>
{list.map(item => (
<li>{item}</li>
))}
</ol>
);
}
function App() { return <Todo /> };
ReactDOM.render(<App />, container);

