본문 바로가기
Programming/13. Book

모던 리액트 Deep Dive - 2장

by @sangseophwang 2024. 3. 2.

 

JSX (Javascript Syntax eXtension)

  • 자바스크립트 확장 문법
  • HTML이나 XML을 자바스크립트 내부에 표현하는 것이 유일한 목적 x
    • 트랜스파일러에서 다양한 속성을 가진 트리 구조를 토큰화해 ECMAScript로 변환
    • 내부에 트리 구조로 표현하고 싶은 다양한 것들을 작성한 후 자바스크립트가 이해할 수 있는 코드로 변환

JSX의 정의

1. JSXElement

JSXElement는 JSX 문법을 사용하여 생성한 React 컴포넌트를 나타낸다.
JSXElement는 HTML 태그와 유사한 구조를 가지며, React 컴포넌트를 생성하고 렌더링하는 데 사용된다.

구성 요소설명예시
JSXOpeningElement JSXElement의 시작 부분을 나타내며, <로 시작하고 태그 이름과 속성을 포함합니다. <div>
JSXClosingElement JSXElement의 끝 부분을 나타내며, </로 시작하고 태그 이름을 포함합니다. </div>
JSXSelfClosingElement 자체 닫히는 태그를 나타내며, <로 시작하고 태그 이름과 속성을 포함하며, /로 닫습니다. <img src="image.jpg" />
JSXFragment 빈 태그로 이루어진 구조를 나타내며, 여러 컴포넌트를 묶어서 반환할 수 있습니다. <></>
JSXElement 위의 구성 요소를 조합하여 JSXElement를 생성할 수 있습니다. <div><span>Hello</span></div>

JSX가 대문자로 시작하는 이유? 이후 추가될 html 태그에 대한 가능성 때문

2. JSXElementName

구성 요소설명예시
JSXIdentifier JSXElementName의 식별자로, 소문자로 시작하는 컴포넌트의 이름입니다. <div>
JSXNamespacedName 네임스페이스가 있는 JSXElementName으로, 콜론으로 구분된 접두사를 포함한 이름입니다. my:component
JSXMemberExpression 객체의 속성에 접근하는 멤버 표현식으로 구성된 JSXElementName입니다. <MyComponent.Item>

3. JSXAttributes

JSXAttributes는 JSXElement의 속성을 나타낸다.
JSXAttributes는 HTML 태그의 속성과 유사한 방식으로 작성되며, 컴포넌트에 추가적인 정보를 전달하는 데 사용된다.

구성 요소설명예시
JSXAttribute JSX 요소에 추가되는 속성을 나타냅니다. type="text"
JSXAttributeName JSX 속성의 이름을 나타냅니다. type
JSXAttributeValue JSX 속성의 값을 나타내며, 문자열 또는 중괄호 {}로 감싼 표현식이 될 수 있습니다. "text" 또는 {this.state.text}
JSXSpreadAttribute 객체의 속성들을 JSX 요소에 스프레드 문법을 사용하여 전개합니다. {...props}

4. JSXChildren

JSXChild는 JSXChildren을 구성하는 자식 요소를 나타낸다. JSXChild는 JSXText, JSXElement, JSXFragment, JSXChildExpression 중 하나일 수 있으며, 여러 개의 자식 요소가 포함될 수 있다.

구성 요소설명예시
JSXText JSXElement 안에 직접 작성된 텍스트 내용을 나타내며, HTML 태그가 아닌 일반 텍스트로 처리됩니다. "Hello, world!"
JSXElement HTML 태그나 다른 컴포넌트를 나타내며, 중첩된 구조로 작성할 수 있고 JSXChildren을 가질 수 있습니다. <div>Hello, world!</div>
JSXFragment 여러 자식 요소를 그룹화하지만 추가적인 DOM 엘리먼트를 추가하지 않는 컨테이너 역할을 합니다. <><ChildA /><ChildB /></>
JSXChildExpression 중괄호 {}로 감싼 JavaScript 표현식을 나타냅니다. JSXElement 안에서 동적인 값을 표현할 때 사용됩니다. {true ? <ChildA /> : <ChildB />}

JSX는 어떻게 자바스크립트로 변환될까?

  • 바벨과 같은 컴파일러로 변환
  • @babel/plugin-transform-react-jsx
// 변환 결과

import { createElement } from 'react';

const TextOrHeading = ({ isHeading, children }: PropsWithChildren) => {
	return createElement(
		isHeading ? 'h1' : 'span',
		{ className: 'text' },
		children
	)
}
  • Next.js에서는 12버전부터 내장 컴파일러에 swc를 활용
    • 빠른 빌드 타임
    • Rust 기반
      • Rust: 병렬 처리를 고려해 설계된 언어
      • swc는 의존성이 없는 파일들을 동시에 변환 가능

가상 DOM과 리액트 파이버

1. DOM과 브라우저 렌더링 과정

  • 브라우저가 사용자 요청 주소를 방문해 HTML 다운로드
  • 브라우저 렌더링 엔진이 HTML을 파싱해 DOM 노드로 구성된 트리(DOM) 구성
  • CSS 다운로드
  • CSS도 다운로드, 파싱해 CSS 트리 (CSSOM) 구성
  • DOM 노드 순회
    • 이 때 display: none; 과 같이 사용자 화면에 보이지 않는 요소는 작업 x
  • 눈에 보이는 노드를 대상으로 해당 노드에 대한 CSSOM 정보 찾고 적용
    • 레이아웃 (layout, reflow): 각 노드가 브라우저 화면의 어느 좌표에 나타내야 하는지 계산하는 과정
    • 페인팅 (painting): 레이아웃 단계를 거친 노드에 색과 같은 실제 유효한 모습을 그리는 과정


2. 가상 DOM의 탄생 배경

  • 특정한 요소의 노출 여부나 사이즈가 변경되는 경우
    • 레이아웃 → 리페인팅 (하위 요소 포함)
    • 많은 비용 발생
  • 가상 DOM은 웹페이지가 표시해야 할 DOM을 일단 메모리에 저장하고 리액트(react-dom)가 실제 변경에 대한 준비가 됐을 때 실제 브라우저의 DOM에 반영

3. 가상 DOM을 위한 아키텍처, 리액트 파이버

  • 리액트 파이버
    • 리액트에서 관리하는 평범한 자바스크립트 객체
    • 파이버 재조정자(fiber reconciler)가 관리
    • 가상 DOM과 실제 DOM을 비교해 변경 사항을 수집하고, 둘 사이 차이가 있으면 변경에 관련된 정보를 가진 파이버 기준으로 화면에 렌더링 요청
  • 파이버의 역할
    • 작업을 작은 단위로 분할, 쪼갠 다음 우선순위 선정
    • 이러한 작업을 일시 중지하고 나중에 다시 시작
    • 이전에 했던 작업을 다시 재사용하거나 필요하지 않은 경우 폐기
  • 이 모든 과정이 비동기로 일어난다!

현대 일반적인 모니터는 보통 초당 60회 화면 갱신을 한다. 화면을 한번 갱신하고 다음 갱신하기까지 1/60초, 즉 16ms가 걸린다. 우리가 작성한 코드가 연속적으로 16ms 이상의 시간을 소비하면서 연속적으로 실행된다면 UI 업데이트 횟수는 모니터의 주사율을 따라갈 수 없다. 예를 들어 사용자가 화면을 스크롤하는 중 16ms를 넘어가는 코드가 실행되면 빠르게 갱신되지 못하고 끊기는 현상이 발생한다. 리액트 파이버 구조에서는 UI 갱신 작업을 작은 단위로 나누어 내부적으로 스케줄링함으로써 React 사용자가 신경쓰지 않더라도 대규모 UI 갱신에도 16ms를 초과하지 않도록 작업했다.

출처: Naver D2

4. 리액트 동작 단계

컴포넌트 함수가 호출되면 객체, VirtualDOM이 반환되고 이것이 Real DOM에 반영

파이버는 각 엘리먼트의 상태를 가지고 있는 객체

리액트 컴포넌트에 대한 정보를 1:1로 가지고 있는 것

  • Render: JSX 선언 또는 React.createElement() 를 통해 일반 객체인 React 엘리먼트 생성 (사용자에게 노출되지 않는 모든 비동기 작업 수행)
  • Reconcile: 이전에 렌더링된 실제 DOM 트리와 새로 렌더링할 React 엘리먼트를 비교해 변경점 적용
  • Commit: 새로운 DOM 엘리먼트를 브라우저 뷰에 커밋 (commitWork())
  • Update: props, state 변경 시 해당 컴포넌트와 하위 컴포넌트에 대해 위 과정 반복
  • 파이버는 컴포넌트가 최초로 마운트되는 시점에 생성되어 이후에는 가급적이면 재사용한다.


5. 파이버의 주요 속성

  • tag: 하나의 엘리먼트에 매칭된 정보
    • FunctionalComponent, SuspenseComponent, LazyComponent 등
  • stateNode: 파이버 자체에 대한 참조 정보.
  • child: 하나의 자식 요소씩. 나머지는 sibling.

리액트의 핵심 원칙은 UI를 문자열, 숫자, 배열과 같은 값으로 관리한다는 것

변수에 UI 관련 값을 보관하고, 리액트의 자바스크립트 코드 흐름에 따라 관리하고 표현

더블 버퍼링: 작업 중인 상태를 나타내는 workInProgress 트리를 현재 트리로 바꾸는 기술

→ 보이지 않는 곳에서 미리 그린 다음, 완성되면 현재 상태를 새로운 그림으로 변경

→ 커밋 단계에서 수행

파이버는 트리 형태로 재귀 방식으로 실행

클래스형 컴포넌트

  • constructor()
    • 생성자 함수. 컴포넌트 초기화 시점에 호출. super() 로 상속받은 상위 컴포넌트(React.Component 또는 React.PureComponent)의 생성자 함수를 먼저 호출해 상위 컴포넌트에 접근할 수 있게 해줌.
  • props
    • 컴포넌트에 특정 속성을 전달하는 용도.
  • state
    • 컴포넌트 내부에서 관리하는 값 (반드시 객체, 변화가 있을 때 리렌더링)
  • 메서드
    • 일반 함수로 선언된 메서드에서 this 바인딩 이용
    • 화살표 함수 사용
    • 렌더링 함수 내부에서 새롭게 만들어 전달

생명주기 메서드

  • mount: 컴포넌트가 생성되는 시점
  • update: 이미 생성된 컴포넌트의 내용이 변경되는 시점
  • unmount: 컴포넌트가 더이상 존재하지 않는 시점
  • render()
    • 컴포넌트 UI 렌더링에 쓰임
    • 마운트, 업데이트 과정에서 일어남
    • 항상 순수해야 하며 부수 효과가 없어야 함
  • componentDidMount()
    • 컴포넌트가 마운트되고 즉시 실행
    • 함수 내부에서 this.setState()로 state 값 변경 가능
    • 브라우저가 UI를 업데이트하기 전에 실행되어 변경되는 것을 눈치챌 수 없음
  • componentDidUpdate()
    • 컴포넌트 업데이트가 일어난 이후 실행
    • state나 props의 변화에 따라 DOM 업데이트
  • componentWillUnmount()
    • 컴포넌트가 언마운트되거나 더 이상 사용되지 않기 직전에 호출
    • 메모리 누수나 불필요한 작동을 막기 위한 클린업 함수 호출
      • removeEventListener, clearInterval, api 호출 취소 등
  • shouldComponentUpdate()
    • state나 props의 변경으로 리액트 컴포넌트가 다시 리렌더링되는 것을 막고 싶을 때
    • 컴포넌트에 영향을 받지 않는 변화에 대해 정의
    • ‘어떠한 조건이 아닐 때’만 컴포넌트를 업데이트하도록!
  • getDerivedStateFromProps()
    • render() 호출 직전 호출
    • 이해가 잘..
  • getSnapShotBeforeUpdate()
    • DOM 업데이트 직전 호출
    • 여기서 반환되는 값은 componentDidUpdate로 전달
  • componentDidCatch
    • 자식 컴포넌트에서 에러가 발생했을 때 실행
    • try-catch 의 catch와 같은 역할?
    • 커밋 단계에서 실행 → 부수 효과 수행 가능

Component vs PureComponent

  • Component는 state가 업데이트되는 대로 렌더링이 일어난다.
  • PureComponent는 state 값에 얕은 비교를 수행해 결과가 다를 때만 렌더링한다.

단점

  • 데이터 흐름 추적 어려움
  • 애플리케이션 내부 로직 재사용 어려움
  • 기능이 많아질수록 컴포넌트 크기 증가
  • 클래스 자체의 어려움

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

  • 리액트의 렌더링
    • 브라우저가 렌더링에 필요한 DOM 트리를 만드는 과정
    • 리액트 애플리케이션 트리 안에 있는 모든 컴포넌트들이 현재 자신들이 가지고 있는 props와 state 값을 기반으로 어떻게 UI를 구성하고 이를 바탕으로 어떤 DOM 결과를 브라우저에 제공할 것인지 계산하는 일련의 과정
  • 리렌더링 케이스
    • useState setter가 실행되는 경우
    • useReducer dispatch가 실행되는 경우
    • 컴포넌트 key props가 변경되는 경우 → sibling
    • props가 변경되는 경우
    • 부모 컴포넌트가 렌더링되는 경우
  • 렌더링 프로세스
    • 루트에서부터 아래쪽으로 내려가면서 업데이트가 필요하다고 지정된 모든 컴포넌트 탐색
    • 함수형 컴포넌트의 경우 FuntionComponent()를 호출한 뒤 결과물 저장
    • 렌더링 결과물은 JSX로 구성되어 있고 JS로 컴파일되면서 React.createElement() 호출 구문으로 변환
    • {type: TestComponent, props: {a: 35, b: ‘test’, children: ‘안녕’}}
    • 결과물 수집 후 가상 DOM과 비교해 실제 DOM에 반영하기 위해 재조정
    • 재조정이 끝나면 하나의 동기 시퀀스로 DOM에 적용해 결과물 노출

렌더와 커밋

  • 렌더
    • 컴포넌트를 실행해 이전 가상 DOM과 비교하는 과정을 거쳐 변경이 필요한 컴포넌트 체크
    • type, props, key
  • 커밋
    • 렌더 단계의 변경 사항을 실제 DOM에 적용해 사용자에게 보여줌
  • 렌더링이 일어난다고 해서 무조건 DOM 업데이트가 일어나지 않는다
    • 변경 사항이 감지되지 않으면 생략 가능
  • 리액트 18에서는 동시성 렌더링 도입
    • 렌더링 중 렌더 단계가 비동기로 작동해 특정 렌더링의 우선순위를 낮추거나, 필요하다면 중단-재시작, 경우에 따라서는 포기

메모이제이션

  • 꼭 필요한 곳에만 메모이제이션을 추가하자
    • 값 비교, 재연산, 저장 후 꺼내는 등의 비용이 들기 때문에 적절히 사용해야 한다.
    • useMemo는 제거될 수도 있다.

 

1장 숙제

Q: 마이크로 태스크 큐는 동기 코드와 마찬가지로 렌더링이 전혀 일어나지 않다가 100_000까지 끝난 후에야 한번에 렌더링이 일어나는가?

A: 실행 순서는 마이크로 태스크 큐 → 렌더링 → 태스크 큐 순으로 이루어진다. 그러므로 동기 코드와 실행한 마이크로 태스크 큐가 렌더링 단계에서 화면에 같이 보이게 된다.

  1. 마이크로 태스크 큐에 있는 마이크로 태스크를 FIFO로 순차 실행한다.
  2. 마이크로 태스크 큐가 비면, 렌더링 작업을 수행한다.
  3. 렌더링 작업 후에는 매크로 태스크 큐(=태스크 큐)에 있는 태스크를 실행한다.
  4. 매크로 태스크 큐의 작업이 1개 실행되고, 다시 1번으로 돌아간다.
반응형

댓글