김종이
Study

2.1 JSX란?


JSX는 리액트가 등장하면서 페이스북에서 소개한 새로운 구문으로 XML과 유사한 내장형 구문이다. JSX가 포함된 코드를 아무런 처리 없이 그대로 실행하면 에러가 발생한다. JSX는 반드시 트랜스파일러를 거쳐야 자바스크립트 코드로 변환된다. JSX의 설계 목적은 다양한 트랜스 파일러에서 다양한 속성의 트리 구조를 토큰화하여 ECMAScript로 변환하는 것이다.

JSX는 하기 4가지 컴포넌트를 기반으로 구성되어 있다.


1. JSXElement

JSXELement가 되기 위해서 다음과 같은 형태 중 하나여야 한다.


  • JSXOpeningElement
    • JSX를 구성하는 가장 기본요소로 HTML의 요소와 비슷한 역할
    • <JSXElemnt JSXAttributes(optional)>
  • JSXClosingElement
    • JSXOpeningElement가 종료됐음을 알리는 요소로 JSXOpeningElement와 쌍으로 쓰이는 요소
    • <JSXElemnt />
  • JSXSelfClosingElement
    • 요소가 시작되고 스스로 종료되는 형태를 의미로 내부적으로 자식을 포함할 수 없다.
    • <JSXElement JSXAttributes(optional) />
  • JSXFragment
    • 아무런 요소가 없는 형태
    • <>JSXChildren(optional)</>

JSXELementName은 JSXElement의 요소 이름으로 쓸 수 있는 것을 의미한다.


  • JSXldentifier
    • JSX 내부에서 사용할 수 있는 식별자를 의미
function Example1(){
  return <$></$>
}
 
function Example2(){
  return <_></_>
}
 
// 불가능
function Example3(){
  return <1></1>
}
  • JSXNamespacedName
    • JSXldentifier:JSXldentifier의 조합으로 리액트에서 사용되지는 않는다. :로 묶을 수 있는 JSXldentifier는 한개 뿐이다.
function Example() {
  return <foo:bar></foo:bar>;
}
  • JSXMemberExpression
    • JSXldentifier.JSXldentifier의 조합으로 리액트에서 사용되지는 않는다. JSXldentifier를 .로 여러개 잇는 것도 가능하다.
function Example() {
  return <foo.bar.baz></foo.bar.baz>;
}

2. JSXAttributes

JSXAttributes은 JSXElement에 부여할 수 있는 속성을 의미한다.


// JSXAttributeValue 속성의 키에 큰따옴표로 구성된 문자열을 할당
function Example1() {
  return <Component ex="ex"></Component>;
}
 
// JSXAttributeValue의 값으로 다른 JSX 요소를 할당
function Example2() {
  return <Component attribute=<div>paper</div>></Component>;
}

3. JSXChildren

JSXChildren은 JSXElement의 자식 값을 나타낸다. JSX로 부모와 자식 관계를 나타낼 수 있다.


  • JSXChild
    • JSXChildren을 이루는 기본 단위
function Example1() {
  return (
    <Component>
      <>{"{}"}</>
    </Component>
  );
}
 
// 리액트에서 렌더링 시 'foo' 라는 문자열이 생성
function Example2() {
  return <Component>{(() => "foo")()}</Component>;
}

4. JSXStrings

JSXStrings은 HTML에서 사용 가능한 모든 문자열이 사용 가능하다. \로 시작하는 이스케이프 문자 형태소 또한 제약없이 사용할 수 있다.


JSX가 JS에서 변환되는 방식

JSX는 @babel/plugin-transform-react-jsx 플러그인을 통해 JS로 변환된다.


const ComponentA = <A required={true}>Paper</A>;
 
const ComponentB = <>Paper</>;
 
const ComponentC = (
  <div>
    <span>Paper</span>
  </div>
);
  • @babel/plugin-transform-react-jsx로 변환한 결과
'use strict’
 
var ComponentA =React.createElement(
 A,
  {
    required: true,
  },
'Paper',
)
 
var ComponentB = React.createElement(React.Fragment, null, ’Paper’)
 
 
var ComponentC = React.createElement(
      ’div’,
      null,
      React.createElement(’span’, null, ’Paper'),
	)
  • @babel/plugin-transform-react-jsx로 변환한 결과
  • 리액트 17, 바벨 7.9.0 이후 버전에서 추가된 자동 런타임으로 트랜스파일한 결과
'use strict’
 
var _jsxRuntime = require (’custom-jsx-library/jsx-runtime')
 
var ComponentA = (0, _jsxRuntime.jsx)(A,
{
  required: true,
  children : 'Paper' ,
})
 
var ComponentB = (0, _jsxRuntime.jsx)(_jsxRuntime.Fragment, {
  children : 'Paper’,
})
 
var ComponentC = (0, _jsxRuntime.jsx)('div’, {
  children: (0, _jsxRuntime.jsx)(’span’, {
    children: ’Paper’
  }),
})

JSX 반환값은 결국 React.createElement로 귀결된다. 때에 따라 직접 createElement를 사용해 컴포넌트를 구성하는 편이 효율적일 수 있다.

2.2 가상 DOM과 리액트 파이버


DOM과 브라우저 렌더링 과정

DOM은 웹페이지에 대한 인터페이스로 브라우저가 웹페이지의 콘텐츠와 구조를 어떻게 보여줄 지에 대한 정보를 담고있다.


  1. 사용자가 브라우저를 통해 웹사이트에 접속하면 서버로부터 HTML, CSS 등 웹사이트에 필요한 리소스를 다운
  2. 브라우저가 HTML의 원시 바이트를 읽어와 인코딩에 따라 개별 문자로 변환, 브라우저가 문자열을 W3C 표준에 지정된 고유 토큰으로 변환해 DOM을 생성
  3. CSS 파일을 만나면 브라우저가 CSSOM 트리를 생성
  4. 이 둘을 결합해 렌더링 트리를 생성
  5. 레이아웃 단계에서 각 요스의 정확한 위치와 크기를 계산
  6. 페인팅 단계에서 렌더링 트리의 각 노드를 화면에 그리는 과정을 거침

가상 DOM의 탄생 배경

인터랙션 하나하나에 모든 DOM의 변경을 제공하지 않고 결과적으로 만들어지는 하나의 DOM 결과물만 제공하는 가상 DOM

가상 DOM으로 인해 여러번 발생했을 렌더링 과정을 최소화 할 수 있다.


리액트 파이버란?

리액트 파이버란 자바스크립트 객체이다. 파이버는 가상 DOM과 실제 DOM을 비교해 변경 사항을 수집하고 차이가 있으면 화면에 렌더링을 요청하는 역할로 모든 과정은 비동기로 일어난다.


  1. 렌더 단계에서 리액트는 모든 비동기 작업을 수행한다.
  2. 커밋 단계에서 DOM에 실제 변경 사항을 반영하기 위해 commitWork()를 실행한다. 이는 동기식으로 일어난다.

파이버는 컴포넌트가 최초로 마운트 되는 시점에 생성되며 이후에는 가급적 재사용 된다.

리액트 파이버 트리

리액트 파이버 트리는 리액트 내부에서 두 개가 존재한다.

  • current 트리
  • workInProgress 트리 : 작업 중인 상태를 나타냄

리액트 파이버의 작업이 끝나면 리액트는 포인터만 변경해 workInProgress트리를 현재 트리로 스위칭하는 더블버퍼링을 시행한다. 더블버퍼링 기법을 사용하는 이유는 불완전한 UI가 노출되지 않기 위함이며 커밋 단계에서 더블버퍼링이 수행된다.

파이버와 가상 DOM

  • 파이버는 리액트 아키택처 내부에서 비동기로 이뤄진다.
  • 실제 브라우저 구조인 DOM에 반영되는 것은 동기적으로 일어난다.
  • 화면에 불완전한 요소를 표시하지 않도록 하기 위해 이러한 변경사항을 업데이트 하는 작업을 가상에서, 메모리 상에서 먼저 수행해 최종 결과물만 실제 브라우저 DOM에 적용한다.

가상 DOM과 리액트의 핵심은 브라우저 DOM을 빠르게 그리는 것이 아니라 값으로 UI를 표현하고 관리하는 것이다.

2.3 클래스형 컴포넌트와 함수형 컴포넌트

함수형 컴포넌트 vs 클래스형 컴포넌트

  • 생명주기 메서드의 부재

    • 생명주기 메서드는 React.Component에서 오기 때문에 함수형 컴포넌트에서 사용할 수 없다.
    • 함수형 컴포넌트는 useEffect를 통해 생명주기 메서드인 componentDidMount, componentDidUpdate, componentWillUnmount를 비슷하게 구현
      • 그러나 useEffect는 생명주기를 위한 훅이 아니라 컴포넌트의 state를 활용해 동기적으로 부수 효과를 만드는 메커니즘
  • 렌더링 된 값

    • 함수형 컴포넌트는 렌더링 된 값을 고정
    • 클래스형 컴포넌트는 렌더링 된 값을 고정하지 않는다.
      • 클래스형 컴포넌트의 props는 외부에서 변경되지 않는 이상 불변 값
      • 클래스형 컴포넌트는 props의 값을 this에서 가져오기 때문에, this가 가리키는 객체, 컴포넌트의 인스턴스의 멤버는 변경 가능한 값이다.
  • this에 바인딩 된 props를 사용하는 클래스형 컴포넌트와 다르게, 함수형 컴포넌트는 props를 인수로 받는다.

  • this와 다르게 props는 인수로 받기 때문에 컴포넌트는 그 값을 변경할 수 없고 해당 값을 그대로 사용한다.

  • 함수형 컴포넌트는 렌더링이 일어날 때마다 그 순간의 값인 Props와 state를 기준으로 렌더링

  • 클래스형 컴포넌트는 시간의 흐름에 따라 변화하는 this를 기준으로 렌더링

2.4 렌더링은 어떻게 일어나는가?

리액트 렌더링이란?

리액트에서의 렌더링은 리액트의 모든 컴포넌트들이 자신들이 가지고 있는 props와 state의 값을 기반으로 어떻게 UI를 구성하고 이를 바탕으로 어떤 DOM 결과를 브라우저에 제공할 지 계산하는 일련의 과정


  • 리액트 렌더링의 이유

    • 최초 렌더링
    • 리렌더링
      • 클래스형 컴포넌트의 setState가 실행될 때
      • 클래스형 컴포넌트의 forceUpdate가 실행될 때
      • 함수형 컴포넌트의 useState()의 setter가 실행될 때
      • 함수형 컴포넌트의 useReducer()의 dispatch가 실행될 때
      • 컴포넌트의 key props가 변경될 때
        • 리액트에서의 key는 리렌더링이 발생하는 동안 형제 요소들 사이에서 동일한 요소를 식별하는 값
        • current 트리와 workInProgress 트리 사이에서 어떠한 컴포넌트가 변경이 있었는 지 구별하는 값이 key다.
        • <Child key={Math.random()} J> 와 같은 랜덤한 값을 key에 할당하면 memo로 선언해도 매번 리렌더링이 일어나기에 key를 활용해 강제로 리렌더링을 일으키는 것이 가능하다.
      • 부모 컴포넌트가 리렌더링 되면 자식 컴포넌트도 리렌더링 된다.
  • 리액트 렌더링 프로세스

    • 함수형 컴포넌트의 경우에 FunctionComponent()를 호출한 뒤에 그 결과물을 저장
    • 렌더링 결과물은 JSX 문법으로, JS로 컴파일 되면서 React.createElement()를 호출하는 구문으로 변환
    • 렌더링 프로세스가 실행되면서 각 컴포넌트의 렌더링 결과물을 수집한 다음 모든 변경 사항을 차례차례 수집
    • 모든 변경 사항을 하나의 동기 시퀀스로 DOM에 적용
  • 렌더와 커밋

    • 렌더 단계는 컴포넌트를 렌더링하고 변경사항을 계산하는 과정

      • 비교하는 것은 크게 type, props, key
    • 커밋 단계는 렌더 단계의 변경 사항을 실제 DOM에 적용하고 사용자에게 보여주는 과정

      • 리액트가 먼저 DOM을 커밋 단계에서 업데이트 => 모든 DOM 노드 및 인스턴스를 가리키도록 리액트 내부의 참조를 업데이트 => useLayoutEffect 훅을 호출
    • 비동기 렌더링, 동시성 렌더링이 리액트 18에서 도입

      • 상대적으로 빠르게 렌더링 할 수 있는 컴포넌트를 먼저 렌더링하는 등, 의도된 우선순위로 컴포넌트를 렌더링해 최적화 하는 기법
    • 렌더링하는 작업은 모든 하위 컴포넌트에 영향을 미친다.

      • 부모가 변경됐다면 props가 변경됐는지와 상관없이 무조건 자식 컴포넌트도 리렌더링 된다.