Dev/React

모던 리액트 Deep Dive 2장

takeU 2023. 12. 12. 18:09
반응형

2. 리액트 핵심 요소 깊게 살펴보기

2.1. JSX란?

  • XML과 유사한 내장형 구문
  • 페이스북에서 개발
  • 자바스크립트 표준의 일부는 아님
    • 자바스크립트 엔진이나 브라우저에서 직접 실행되거나 표현되지 않음
    • 트랜스파일러로 변환이 필요

2.1.1. JSX의 정의

  1. JSXElement
    • JSX를 구성하는 가장 기본 요소
    • HTML의 element와 비슷한 역할
    • HTML 태그와 구분짓기 위해 대문자로 시작 (표준은 아님)
  2. JSXAttributes
    • JSXElement에 부여할 수 있는 속성
      • JSXSpreadAttributes - 전개 연산자와 동일
      • JSXAttribute - 속성
        • JSXAttributeKey
        • JSXAttributeValue
      • JSXElement - 다른 JSX 요소가 들어갈 수 있음 ( 잘 안쓰임 )
      • JSXFragment - <></>
  3. JSXChildren
    • 트리구조인 JSX에서 JSXElement의 자식 값
  4. JSXStrings
    • 큰따옴표 문자열 + 작은따옴표 문자열 + JSXText
      • JSXText - {,<,>,} 를 제외한 문자열

2.1.2. JSX 예제

const Component = (
    <A>
        {"example"}
    </A>
)

2.1.3. JSX는 어떻게 자바스크립트에서 변환될까?

@babel/plugin-transform-react-jsx - 리액트에서 JSX를 변환하는 플러그인

JSX

const ComponentA = <A required={true}>Hello</A>

const ComponentB = <>Hello</>

const ComponentC = (
    <div>
        <p>Hello</p>
    </div>
)

JS

'use strict'

var ComponentA = React.createElement(A, { required: true }, 'Hello')
var ComponentB = React.createElement(React.Fragment, null, 'Hello')
var ComponentC = React.createElement('div', null,
    React.createElement('p', null, 'Hello'))

2.2. 가상 DOM과 리액트 파이버

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

  1. 사용자가 요청한 주소를 방문해 HTML 파일 다운로드
  2. 브라우저 렌더링 엔진이 HTML 파싱 이후 DOM 트리 생성
  3. 2번 과정 중 CSS 파일을 만나면 해당 파일 다운로드
  4. CSSOM 트리 생성
  5. 2번의 DOM 노드를 순회. 이 때 사용자 화면에 보이는 요소만 방문
  6. 5번 과정 중 눈에 보이는 노드에 대한 CSS 정보를 CSSOM에서 찾아 적용
    1. 레이아웃(layout, reflow) - 브라우저 화면의 어느 좌표에 나타나야 하는지 계산하는 과정
    2. 페인팅(painting) - 레이아웃 단계를 거친 노드에 색과 같은 실제 유효한 모습을 그리는 과정

DOM이란?

  • Document Object Model
  • 웹페이지에 대한 인터페이스
  • 브라우저가 웹페이지의 콘텐츠와 구조를 어떻게 보여줄지에 대한 정보

2.2.2. 가상 DOM의 탄생 배경

전통적인 방식의 웹에서, 변화된 방식인 싱글 페이지 애플리케이션에서는 reflow와 repaint가 자주 발생하기 때문에 보다 효율적으로 화면을 그릴 필요가 있음

가상 DOM은 일반적인 DOM을 관리하는 브라우저보다 빠르다 알려져 있지만, 이는 사실이 아니다.

하지만, 대부분의 상황에서 웬만한 애플리케이션을 만들 수 있을 정도로 충분히 빠르다.

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

가상 DOM과 렌더링 과정 최적화를 가능하게 해주는 것

리액트 파이버란?

  • 리액트에서 관리하는 평범한 자바스크립트 객체
  • 파이버 재조정자(fiber reconciler)
    • 가상 DOM과 실제 DOM을 비교해 변경 사항을 수집해, 차이가 있으면 변경에 관련된 정보를 가지고 있는 파이버를 기준으로 화면에 렌더링을 요청
  • 파이버의 목표는 리액트 웹 애플리케이션에서 발생하는 애니메이션, 레이아웃, 이넡랙션에 올바른 결과물을 만드는 반응성 문제를 해결하는 것
    • 작업을 작은 단위로 분할하고 쪼갠 다음, 우선순위를 매김
    • 이러한 작업을 일시 중지하고 다시 시작할 수 있음
    • 이전에 했던 작업을 재사용하거나 필요하지 않은 경우 폐기 가능
  • 모든 과정은 비동기로 일어남 / 기존 조정 알고리즘은 스택 알고리즘으로 동기적이었음(비효율)

파이버는 어떻게 구현돼 있을까?

  • 파이버는 하나의 작업 단위로 구성
  • 리액트는 이러한 작업 단위를 하나씩 처리하고 finishedWork() 라는 작업으로 마무리
    • 렌더 단계에서 리액트는 사용자에게 노출되지 않는 모든 비동기 작업을 수행
    • 이 때, 파이버의 작업 우선순위를 지정하거나 중지시키거나 버리는 등의 작업을 수행
  • 커밋 단계에서는 DOM에 변경 사항을 반영하기 위한 작업 commitWork() 실행 - 동기
  • 파이버는 컴포넌트가 최초로 마운트되는 시점에 생성되어 가급적이면 재사용됨
  • 생성된 파이버는 state가 변경되거나 생명주기 메서드가 실행되거나 DOM 변경이 필요할 때 실행됨
  • 리액트는 파이버를 처리할 때 바로 처리할 수도 있고, 스케줄링도 가능함

리액트 파이버 트리

  • 현재 모습을 담은 파이버 트리
  • 작업 중인 상태를 나타내는 workInProgress 트리

이 두개의 파이버 트리로 구성되어 있으며, 작업이 끝나면 리액트는 포인터를 변경해 workInProgress 트리를 현재 트리로 바꾼다.

이를 더블 버퍼링 이라 부르며, 커밋 단계에서 수행된다.

파이버의 작업 순서

  1. beginWork() 함수를 실행해 파이버 작업을 수행. 자식이 없는 파이버를 만날 때까지 트리형식으로 진행됨 (재귀, dfs)
  2. completeWork() 를 실행해 파이버 작업을 완료
  3. 형제가 있다면 넘어감
  4. 모두 종료되면 return 으로 돌아가 완료를 알림
  • setState 와 같은 업데이트 요청이 들어오면 workInProgress 트리를 다시 빌드하는데, 이 때 파이버를 새로 만드는 것이 아닌 기존 파이버에서 업데이트된 props를 받아 내부에서 처리함

2.2.4. 파이버와 가상 DOM

  • 파이버는 리액트 컴포넌트에 대한 정보를 1:1로 가지고 있는 것
  • 다른 환경에서도 사용할 수 있기 때문에 가상 DOM과 동일한 개념이 아님
  • 렌더러가 다르더라도, 내부적으로 파이버를 통해 조정되는 과정은 동일하기 때문에, 동일한 재조정자를 사용할 수 있는 것

2.2.5. 정리

  • 가상 DOM과 리액트의 핵심은 브라우저의 DOM을 더욱 빠르게 그리고 반영하는 것이 아니라 값으로 UI를 표현하는 것
  • UI를 값으로 관리하고 흐름을 효율적으로 관리하기 위한 매커니즘이 리액트의 핵심

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

  • 이전에 함수형 컴포넌트가 있었지만 무상태로 어떠한 요소를 정적 랜더링하는 목적으로 사용
  • 16.8 버전의 훅이 소개된 이후로 함수형 컴포넌트가 각광받음

클래스형 컴포넌트의 한계

  • 데이터의 흐름추적하기 어려움
    • 여러 메서드들에서 state의 업데이트가 일어날 수 있어 숙련된 개발자라 해도 state의 흐름을 추적하기 매우 어려움
  • 애플리케이션 내부 로직의 재사용이 어려움
    • 고차 컴포넌트로 감싸거나 props로 넘겨주는 방식으로 재사용 할 수 있지만 클래스형 컴포넌트에서 매끄럽게 처리하기 쉽지 않음
    • 상속 클래스 역시 흐름을 쫒아야 하기 때문에 복잡도가 증가하고 코드의 흐름을 좇기 쉽지 않음
  • 기능이 많아질수록 컴포넌트의 크기가 커짐
    • 내부 로직이 많아질수록 데이터 흐름이 복잡해져 생명주기 메서드들의 사용이 잦아지며, 컴포넌트의 크기가 기하급수적으로 커짐
  • 클래스는 함수에 비해 상대적으로 어려움
  • 코드의 크기최적화하기 어려움
    • 클래스형 컴포넌트는 번들링 최적화에 불리한 조건을 가지고 있음
  • 핫 리로딩을 하는 데 상대적으로 불리함
    • 클래스 컴포넌트는 최초 랜더링시 instance를 생성하고 그 내부에서 state를 관리하는데 instance 내부의 render를 수정하게되면 새 instance를 만들어야하기 때문에 핫 리로딩시 초기화 됨

함수형 컴포넌트

  • 클래스형 컴포넌트와 비교했을때 간결해진 형태
  • this 바인딩이 필요 없음
  • state를 객체가 아닌 각각의 원시값으로 관리

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

생명주기 메서드의 부재

  • 함수형 컴포넌트에는 생명주기 메서드가 없음
  • 함수형은 props를 받아 리액트 요소만 반환하는 함수이지만, 클래스형 컴포넌트는 render 메서드가 있는 React.Component를 상속받아 구현
  • useEffect로 생명주기 메서드와 비슷하게 구현 가능하지만 어디까지나 비슷한 것이며 생명주기를 위한 훅이 아님

함수형 컴포넌트와 렌더링된 값

  • 함수형 컴포넌트는 렌더링 된 값을 고정하고, 클래스형 컴포넌트는 그렇지 못함
  • 함수형 컴포넌트는 렌더링 때마다 그 순간의 props와 state를 기준으로 렌더링 되지만 클래스형 컴포넌트는 시간의 흐름에 따라 변하는 this를 기준으로 렌더링 됨

클래스형 컴포넌트를 공부해야할까?

  • 알면 좋음
  • 자식 컴포넌트에 대한 에러 처리는 클래스형 컴포넌트에만 가능

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

리액트의 렌더링이란?

  • 리액트 애플리케이션 트리 안에 있는 모든 컴포넌트들이 현재 자신들이 가지고 있는 propsstate의 값을 기반으로 어떻게 UI를 구성하고 이를 바탕으로 어떤 DOM결과를 브라우저에 제공할 지 계산하는 일련의 과정

리액트의 렌더링이 일어나는 이유

  • 리액트에서 랜더링 발생 시나리오
    1. 최초 렌더링 : 최초 진입 시 브라우저에 정보를 제공하기 위해 최초 렌더링을 수행
    2. 리렌더링 : 최초 렌더링 이후 발생하는 모든 렌더링을 의미하며 발생하는 경우는 다음과 같음
      • 클래스형 컴포넌트 - setState, forceUpdate
      • 함수형 컴포넌트 useState()의 두번째 배열 요소 setter가 실행
      • 함수형 컴포넌트 useReducer()의 두번째 배열 요소 dispatch가 실행되는 경우
      • 컴포넌트의 key props가 변경되는 경우
        • react에서 key가 필요한 이유는 리렌더링이 발생하는 동안 형제 요소 사이에서 동일한 요소를 key를 통해 식별하기 때문
      • props가 변경되는 경우
      • 부모 컴포넌트가 렌더링될 경우 (부모 컴포넌트가 리렌더링 되면 자식 컴포넌트는 무조건 리렌더링)
    • mobx나 redux와 같은 상태 관리 패키지들은 언급된 방법중 하나를 이용하여 리렌더링을 발생

리액트의 렌더링 프로세스

  • 렌더링 프로세스가 시작되면 루트부터 아래쪽으로 업데이트가 필요하다고 지정돼 있는 컴포넌트를 찾음
  • 업데이트가 필요한 컴포넌트를 발견하면 클래스형 컴포넌트 일 경우 render()함수를 실행하고, 함수형 컴포넌트의 경우는 FunctionComponent() 그 차레를 호출한 뒤에 결과물을 저장
function Hello() {
    return (
        <TestComponent a=(35] b="yceffort">
            안녕하세요
        </TestComponent>
    )
}

// 위 JSX 문법은 다음과 같은 React.creater1ement를 호출해서 변환됨

function Hello() {
    return React.createElement (
        TestComponent,
        ( a: 35, b: 'yceffort' 3,
        '안녕하세요',
    )
}

// 결과물
{type: TestComponent, props: {a:35, b:"yceffort", children: "안녕하세요"}
  • 이렇게 변경사항을 수집하며 리액트 재조정 과정이 끝나면 모든 변경사항을 동기 시퀀스로 DOM에 적용

렌더와 커밋

  • 리액트의 렌더링은 렌더 단계와 커밋 단계라는 총 두 단계로 분리되어 있음
  • 렌더 단계는 컴포넌트를 렌더링하고 변경 사항을 계산하는 모든 작업
    • 컴포넌트를 실행해서(render 또는 return) 변경이 필요한 컴포넌트를 체크하는 단계이며 크게 type, props, key 세가지를 비교하여 체크
  • 커밋 단계는 렌더 단계의 변경 사항을 실제 DOM에 적용해 사용자에게 보여주는 과정
    • 적용 된 후, 모든 DOM 노드 및 인스턴스를 가리키도록 내부 참조를 업데이트 하고 생명주기 개념의 메서드들을 호출(클래스 컴포넌트 - componentDidMount, componentDidupdate / 함수형 컴포넌트 - useLayoutEffect)
    • 렌더링이 일어나도 커밋단계까지 갈 필요가 없다고 판단되면 DOM 업데이트가 일어나지 않음 ⇒ 커밋 단계는 생략될 수도 있음
스크린샷 2023-12-06 오후 10 53 43
  • 이러한 동기식 렌더링 방식은 렌더링 과정이 길어질 수록 성능저하와 다른 작업을 지연시킴
  • 의도된 우선순위로 컴포넌트를 최적화 렌더링 할 수 있는 비동기 렌더링(동시성 렌더링)이 리액트 18에서 도입 (Suspense)

2.5 컴포넌트와 함수의 무거운 연산을 기억해 두는 메모이제이션

책 저자의 사견

  • 리액트를 배우거나 깊이 이해하고 싶고, 시간을 투자할 여유가 있으면 섣부른 메모이제이션을 지양하는 자세를 가지면서 적용하기를 권장
  • 현업에서 사용하고 있거나 성능에 대해 깊이 연구해 볼 시간적 여유가 없는 상황이면 일단 의심되는 곳은 먼저 다 적용해 볼 것을 권장
  • 리액트 컴포넌트의 결과물을 다시 계산하고 실제 DOM까지 비교하고 작업하는 것이 더 무겁고 비싸기 때문에 섵부른 메모이제이션 최적화가 주는 이점이 더 클 수 있음

출처: 모던 리액트 Deep Dive