JSX is a syntax extension for JavaScript that lets you write HTML-like markup inside a JavaScript file.
JSX 는 JSXElement
, JSXAttributes
, JSXChildren
, JSXString
의 4가지 요소로 구성된다.
:
을 통해 두 개의 JSXIdentifier 를 이은 식별자. 3개 이상은 불가하다..
을 통해 여러 개의 JSXIdentifier 를 이은 식별자.// JSXNamespaceName
<Foo:bar></Foo:bar>
// JSXMemberExpression
<Layout.Navbar.Item></Layout.Navbar.Item>
JSXAttributeValue
에는 문자열 (큰 따옴표, 작은 따옴표) 및 다른 JSXElement 도 들어갈 수 있다.JSXAttributeValue
에는 JS 에서 쓰이는 AssignmentExpression, 즉 값의 할당에 쓰이는 표현식도 추가할 수 있다.JSXChildren
라고 한다.{
, }
, <
, >
를 제외한 나머지 문자열을 의미한다. JSX 문법과 혼동을 줄 수 있는 요소는 제외된다. (제외된 문자는 따옴표로 감싸서 문자열로 표현할 수 있다.)// JSXText 가 JSXChild 로 구성된 JSXElement.
<>JSXText 입니다.</>
<>{ '{}, <>' }</> // 제외된 문자도 아래처럼 사용 가능하다
// JSXChildExpression, JS 의 AssignmentExpression 에 해당되는 문법도 사용 가능하다.
<>{() => 'foo'}</>
<>{() => <OtherComponent />}</> // = <><OtherComponent></>
\
, "
, '
로 구성된 문자열은 별도의 이스케이프 방식을 사용하여 변환해야 한다.@babel/plugin-trasform-react-jsx
plugin 을 사용하여 JSX 를 ReactElement 로 변환하는 방식을 채택한다.어떻게 babel 에서는 별도의 장치 없이 JSX 를 변환할 수 있게 된걸까?
import React from 'react'
를 명시해야 했다.React.createElement
함수로 변환되기 때문에 반드시 React 모듈을 참조해야 했기 때문이다.'react/jsx-runtime'
모듈을 참조하도록 구조가 변경되었다.const Component = (
<A option="a" key="b">
Hello World
</A>
);
// React 17 이전 변환 결과
// createElement(Component, props, children) 함수로 변환된 결과다.
var Component = React.createElement(
A,
{ option: "a", key: "b" },
"Hello World"
);
// React 17 이후 변환 결과
import { jsx as _jsx } from "react/jsx-runtime";
var Component = _jsx(
A,
{
option: "a",
children: "Hello World",
},
{ key: "b" }
);
createElement
를 사용하지 않고 아래처럼 렌더링 할 컴포넌트를 사전에 할당한 후 넘겨도 되지 않나 싶다.// AS - IS : isHeading props 의 값에 따라 조건부 렌더링 진행
const TestHeading = ({ isHeading, children }: PropsWithChildren<PropsType>) => {
return isHeading ? <h1>{children}</h1> : <p>{children}</p>
}
// Book's Solution, createElement 를 직접 사용하여 렌더링 주체 변경
const TestHeading = ({ isHeading, children }: PropsWithChildren<PropsType>) => {
return createElement(isHeading ? 'h1' : 'p', {}, children)'
}
// My Solution : 굳이 createElement 을 사용하지 않고 렌더링 주체 (JSXElement) 변경
const TestHeading = ({ isHeading, children }: PropsWithChildren<PropsType>) => {
const RenderComponent = isHeading ? 'h1' : 'p'
return <RenderComponent>{children}</RenderComponent>
}
DOM 은 문서 (document) 와 문서 내부의 요소 (Element) 에 JS 로 접근할 수 있도록 설계된 Object 다.
브라우저에서 특정 화면을 보여주기 위한 렌더링 과정은 아래와 같이 작성할 수 있다.
이러한 문제점을 해결하기 위해 등장한 개념이 바로 가상 돔 (Virtual DOM) 이다.
react-dom
에서 관리하는 Virtual DOM Tree 를 생성하여 메모리에 적재하고, React 에서 DOM 의 변경 사항을 Trigger 할 경우 VDOM 에 우선 반영하여 변경에 대한 준비가 완료될 경우 실제 DOM Tree 에 변경 사항을 Update 한다.
컴포넌트 내부에 변경 사항이 발생하여 리렌더링이 진행될 경우, 기존의 DOM Tree 와 새롭게 구축된 DOM Tree 간의 비교 사항을 알아야 할 필요가 있다.
두 트리 간의 변경 사항을 비교하고 만약 변경 사항이 존재한다면 이를 실제 DOM Tree 에 적용하는 과정을 거치는데, 이때 두 트리를 비교하는 과정을 재조정 (Reconciliation) 이라 한다.
다만 N 개의 노드가 존재하는 두 트리 간의 비교를 위해서는 O(n^3)
의 복잡도가 들기 때문에 React 에서는 몇 가지 가정을 통한 휴리스틱 알고리즘을 세워 복잡도를 O(n)
으로 낮췄다.
기존 재조정 알고리즘의 구조는 Stack 기반이었기 때문에 해당 스택에 필요한 작업을 적재시키고 순차적으로 이를 해결하는 방식을 채택했다.
과거에는 해당 Stack 에 들어간 여러 개의 작업을 하나로 묶어 동기적으로 이를 진행했기에 때문에 중간의 작업을 취소하거나 작업 간의 우선 순위를 변경할 수 없었고, 중간에 작업이 지연될 경우 브라우저 렌더링도 같이 지연되는 문제가 있었다.
즉, 여러 작업 간의 우선 순위를 깡그리 무시한 채 화면에 변경 사항을 적용하는 일련의 과정을 하나의 큰 태스크로 놓고 실행하던 것이 기존의 Stack Reconciliation 의 문제였다.
따라서 이러한 문제를 해결하기 위해 React 16 버전 이상부터는 기존의 Stack 방식이 아닌 React Fiber 라는 새로운 아키텍쳐가 도입되었다.
Fiber Reconciliation 은 각각의 작업에 대한 우선 순위를 매기고 이에 대한 일시 정지, 재가동, 우선 가동을 가능하도록 설계되었다. 이러한 방식을 incremental rendering 이라 한다.
또한 Fiber Reconciliation 과정은 전부 비동기로 동작하기 때문에 Stack Reconciliation 과는 달리 선택적으로 작업을 취합하여 진행할 수 있다.
function FiberNode(
tag: WorkTag,
pendingProps: mixed,
key: null | string,
mode: TypeOfMode
) {
// Instance
this.tag = tag; // Fiber 의 종류를 의미 (FunctionalComponent, SuspenseComponent 등)
this.key = key; // React 내 key 속성
this.elementType = null;
this.type = null;
this.stateNode = null; // Fiber Node 와 연관된 실제 DOM 노드 및 컴포넌트 인스턴스 (클래스 컴포넌트일 경우) 를 의미
// Fiber
this.return = null; // 부모 Fiber Node
this.child = null; // 부모 Fiber Node 에서 첫 번째로 가진 자식 노드
this.sibling = null; // 자신의 바로 다음 형제 노드
this.index = 0; // 자신의 형제들 중에서 몇 번째 순서인지를 나타냄
this.ref = null; // DOM Node 혹은 컴포넌트 인스턴스의 상태 및 업데이트를 관리
this.pendingProps = pendingProps; // Fiber Node 생성 당시에는 렌더링 작업이 종료되지 않았으므로 인계 받은 props 를 pendingProps 으로 관리.
this.memoizedProps = null; // Render Phase 종료 이후 사용되었던 pendingProps 를 보관
this.updateQueue = null; // 상태 업데이트
this.memoizedState = null; // 함수형 컴포넌트 내에서 생성된 Hook list
this.dependencies = null; // 컴포넌트 내부의 여러 의존성을 관리하는 field
this.mode = mode; // 컴포넌트의 렌더링 모드를 설정
// Effects
this.flags = NoFlags; // Fiber Node 의 현재 Flag (Update, Mount, etc)
this.subtreeFlags = NoFlags; // 하위 Fiber Node Tree 의 상태 Flag
this.deletions = null; // 삭제 예정인 자식 노드를 담은 field
this.lanes = NoLanes;
this.childLanes = NoLanes;
this.alternate = null;
}
Fiber Tree 는 React 내부에서 두 개로 나뉘어 관리된다. 하나는 현재 모습을 담은 트리이며 다른 하나는 작업 중인 상태를 나타내는 트리다. 이러한 방식을 더블 버퍼링이라 한다.
변경 사항을 적용할 VDOM Tree 와 현재 화면에 보여지는 화면을 구성하는 VDOM Tree 를 둘 다 메모리에 적재하여 (Buffer) 변경 사항이 모두 완료되기 전까지는 이전에 구축한 트리를 보여줌으로서 두 트리를 교차하는 방식이다.
current
: 현재 렌더링 중인 화면을 구성하는 VDOM 트리workInProgress
: Render Phase 에서 작업 중인 변경 사항을 적용하는 VDOM 트리workInProgress 트리는 Render Phase 를 거쳐 Commit Phase 로 넘어갈 경우 포인터를 변경하여 current 트리로 변경한다.
beginWork
함수를 실행하여 상태가 변경된 컴포넌트를 찾고, 변경점을 찾았다면 작업을 수행한다.completeWork
메서드를 실행하여 Node 의 tag 에 맞는 Element 를 생성하여 Commit Phase 에 넘긴다.Fiber 가 Reconciliation 과정을 통해 실제 DOM 에 반영되는 프로세스는 이보다 훨씬 복잡하다