모던리액트 1주차 스터디 발표자료 - 2장

2장 리액트 핵심요소 깊게 살펴보기
강석우's avatar
Mar 09, 2024
모던리액트 1주차 스터디 발표자료 - 2장

JSX

JSX 의 구성요소

  • JSXElement

    • JSXOpeningElement

    • JSXClosingElement

    • JSXSelfClosingElement

    • JSXFragment

<div></div> // JSXOpeningElement, JSXClosingElement
<div/> // JSXSelfClosingElement
<></> // JSXFragment
  • JSXAttributes

  • JSXChildren - JSXElement의 자식 값

JSX 변환

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

// react.Create
var ComponentA = React.createElement(
 A,
 {
 required: true,
 },
 'Hello World',
)
var ComponentB = React.createElement(React.Fragment, null, 'Hello World')

JSX 변환되는 특성을 활용한 예제

// ❌ props 여부에 따라 children 요소만 달라지는 경우 
// 굳이 번거롭게 전체 내용을 삼항 연산자로 처리할 필요가 없다.
// 이 경우 불필요한 코드 중복이 일어난다.
function TextOrHeading({
 isHeading,
 children,
}: PropsWithChildren<{ isHeading: boolean }>) {
 return isHeading ? (
 <h1 className="text">{children}</h1>
 ) : (
 <span className="text">{children}</span>
 )
}

// ⭕ JSX가 변환되는 특성을 활용한다면 다음과 같이 간결하게 처리할 수 있다.
import { createElement } from 'react'
function TextOrHeading({
 isHeading,
 children,
}: PropsWithChildren<{ isHeading: boolean }>) {
 return createElement(
 isHeading ? 'h1' : 'span',
 { className: 'text' },
 children,
 )
}

사용하면 코드 효율상으로는 좋을 것 같지만 팀 프로젝트를 진행하게 될 때 가독성이 좋을까??

가상 DOM과 파이버

브라우저 상에서 화면을 그리는 과정

  1. html 다운

  2. DOM 트리 구성

  3. css 다운

  4. cssom 구성

  5. display : none 제외한 DOM node 순회

  6. css 입히기

가상돔이란?

  • 리액트에서 변경이 일어난 dom에 대해서만 계산하기 위해서 만들어진 개념.

  • 브라우저가 아닌 메모리에서 계산을 할 수 있도록 만들어졌다.

리액트 파이버란?

가상돔을 위한 아키텍쳐로서 리액트 웹 애플리케이션에서 발생하는 애니메이션, 레이아웃, 그리고 사용자 인터렉션에 올바른 결과물을 만드는 반응성 문제를 해결하려는 목적으로 만들어졌다.

  • 리액트에서 관리하는 자바스크립트 객체

  • 재조정자가 파이버를 관리

    • 가상돔과 실제돔을 비교해 변경사항 수집하고 변경 자료를 현재 저장된 파이버 기준으로 렌더링 요청한다.

파이버가 하는 일

  • 작업을 작은 단위로 분할하고 쪼갠 다음, 우선순위를 매긴다.

  • 이러한 작업을 일시 중지하고 나중에 다시 시작할 수 있다.

  • 이전에 했던 작업을 다시 재사용하거나 필요하지 않은 경우에는 폐기할 수 있다

파이버의 작업 처리 단계

  • 렌더 단계에서 리액트는 모든 비동기 작업을 수행한다.
    파이버의 작업, 우선순위를 지정하거나 중지시키거나 버리는 등의 작업이 일어난다.

  • 커밋 단계에서는 DOM에 실제 변경 사항을 반영하기 위한 작업, commitWork()가 실행되는데, 이 과정은 동기식으로 일어나고 중단될 수도 없다.

생성된 파이버는 state가 변경되거나 생명주기 메서드가 실행되거나 DOM의 변경이 필요한 시점 등에 실행한다.
또한 리액트는 파이버를 처리할 때마다 이러한 작업을 직접 바로 처리하기도 하고 스케줄링하기도 한다. ( 우선순위에 따라 작업을 처리 // 애니메이션 우선처리 )

리액트 파이버 트리

현재 모습의 파이버트리, workinProgress 트리 두개로 이루어져 있다.
리액트 파이버의 작업이 끝나면 리액트는 단순히 포인터만 변경해 workInProgress 트리를 현재 트리로 바꿔버리는데 이를 더블 버퍼링 이라 한다.

더블 버퍼링

  • 리액트 파이버의 작업이 끝나면 리액트는 단순히 포인터만 변경해 workInProgress 트리를 현재 트리로 바꿔버리는 기술.

  • 다 그리지 못한 모습을 노출 시키지 않기 위해 사용

  • 리액트의 커밋 단계에서 수행

파이버의 작업 순서

  1. 리액트의 beginWork() 함수 실행. (자식이 없는 파이버를 만날 때까지 트리형식으로 시작)

  2. completeWork() 함수 실행을 통해 파이버 작업 완료

  3. 형제가 있다면 넘어가기

  4. return 으로 돌아가 작업 완료

<A1>
 <B1>안녕하세요</B1>
 <B2>
  <C1>
   <D1 />
   <D2 />
  </C1>
 </B2>
 <B3 />
</A1>

state 업데이트가 일어나게 되면 해당 부분 트리만 업데이트한다.

정리

가상 DOM과 리액트의 핵심은 브라우저의 DOM을 더욱 빠르게 그리고 반영하는 것이 아니라 바로 값으로 UI를 표현하는 것이다.
화면에 표시되는 UI를 자바스크립트의 문자열, 배열 등과 마찬가지로 값으로 관리하고 이러한 흐름을 효율적으로 관리하기 위한 메커니즘이 바로 리액트의 핵심이다.

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

클래스 컴포넌트의 생명주기 메서스

mount, update, unmount 세 가지로 크게 나눌 수 있다.

클래스 컴포넌트를 사용하지 않는 이유

  • 데이터의 흐름을 추적하기 어렵다.

  • 애플리케이션 내부 로직의 재사용이 어렵다.

    • 사용하려하면 할수록 고차컴포넌트와 prop 이 많아지는 래퍼지옥에 빠지게 된다.

  • 기능이 많아질수록 컴포넌트의 크기가 커진다.

  • 클래스는 함수에 비해 상대적으로 어렵다.

  • 코드 크기를 최적화하기 어렵다.

  • 핫 리로딩을 하는 데 상대적으로 불리하다.

    • 핫리로딩 : 코드에 변경 사항이 발생했을 때 앱을 다시 시작하지 않고서도 해당 변경된 코드만 업데이트하여 변경 사항을 빠르게 적용하는 기법

    • 클래스형 컴포넌트는 render 수정시 instance를 새로 생성하게 되며 state 값이 초기화 되기 때문에 핫리로딩에 불리하다. 함수형 컴포넌트는 state값을 함수가 아닌 클로저에 넣어놓기 때문에 다시 실행되어도 state값을 잃지 않는다.

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

함수 컴포넌트

클래스 컴포넌트

생명주기의 부재

사용 불가능

사용 가능

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

렌더링된 값 고정

렌더링된 값 고정 안함

생명주기의 부재

클래스 컴포넌트의 경우 사용 가능하다. but 함수 컴포넌트는 불가능 하다.

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

클래스 컴포넌트 : 렌더링된 값을 고정하지 않음 => this 객체가 변화되기 때문에 생명주기 메서드가 변경된 값을 읽을 수있다.

함수 컴포넌트 : 렌더링 된 값을 고정

예시 ) setTimeout()

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

렌더링이란

브라우저에서의 렌더링이란 쉽게 말해 HTML과 CSS 리소스를 기반으로 웹페이지에 필요한 UI를 그리는 과정을 의미

리액트에서의 렌더링이란 리액트 애플리케이션 트리 안에 있는 모든 컴포넌트들이 현재 자신들이 가지고 있는 props와 state의 값을 기반으로 어떻게 UI를 구성하고 이를 바탕으로 어떤 DOM 결과를 브라우저에 제공할 것인지 계산하는 일련의 과정을 의미한다.

렌더링이 일어나는 경우

  1. 최초 렌더링

  2. 리렌더링 : 최초 렌더링 이후 일어나는 모든 렌더링을 의미한다.

    1. 클래스 컴포넌트의 setState가 실행되는 경우

    2. 클래스 컴포넌트의 forceUpdate가 실행되는 경우

    3. 함수 컴포넌트의 useState()의 두 번째 배열 요소인 setter가 실행되는 경우

    4. 함수 컴포넌트의 useReducer()의 두 번째 배열 요소인 dispatch가 실행되는 경우

    5. 컴포넌트의 key props가 변경되는 경우

      1. 리액트에서 배열을 사용하게 되면 key 를 입력하지 않았다고 warning이 뜨게 되는 경우가 많은데 이유는 여기에 있다.

      2. 리렌더링이 발생하면 current 트리와 workInProgress 트리 사이에서 어떠한 컴포넌트가 변경이 있었는지 구별해야 하는데, 이 두 트리 사이에서 같은 컴포넌트인지 구별하는 값이 바로 key다.
        useMemo 를 사용해 선언한 값은 배열에 key를 넣지 않은 경우 useState의 호출에도 변경되지 않는데 key 를 랜덤값으로 넣어준다면 컴포넌트 구분이 명확하게 되지 않기 때문에 key값이 변경될 때마다 리렌더링을 일으키게 된다.

    6. props가 변경되는 경우

    7. 부모 컴포넌트가 렌더링될 경우

리액트의 렌더링 단계

렌더단계

  • 렌더링 프로세스에서 컴포넌트를 실행해(render() 또는 return) 이 결과와 이전 가상 DOM을 비교하는 과정을 거쳐 변경이 필요한 컴포넌트를 체크

  • 주요 비교점은 type, props, key이다. 이 세가지 중 한가지라도 변경사항이 있다면 변경이 필요한 컴포넌트로 체크

커밋단계

  • 렌더 단계의 변경 사항을 실제 DOM에 적용해 사용자에게 보여주는 과정

  • 리액트가 먼저 DOM을 커밋 단계에서 업데이트한다면 모든 DOM 노드 및 인스턴스를 가리키도록 리액트 내부 참조를 업데이트하고 생명주기 개념이 있는 클래스 컴포넌트에서는 componentDidMount, componentDidUpdate 메서드를 호출, 함수 컴포넌트에서는 useLayoutEffect 훅을 호출한다.

이 두 가지의 렌더링 방식은 항상 동기식으로 작동한다. 따라서 렌더링이 오래걸릴수록 애플리케이션의 성능 저하로 이어진다.

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

섣부른 최적화는 독이다, 꼭 필요한 곳에만 메모이제이션을 추가하자

memo로 지불해야 하는 비용

  • props에 대한 얕은 비교

vs

렌더링 과정의 비용은 비싸다, 모조리 메모이제이션해 버리자

memo를 하지 않았을 때 발생할 수 잇는 문제

  • 렌더링을 함으로써 발생하는 비용

  • 컴포넌트 내부의 복잡한 로직의 재실행

  • 그리고 위 두 가지 모두가 모든 자식 컴포넌트에서 반복해서 일어남

  • 리액트가 구 트리와 신규 트리를 비교

책의 저자는 아래와 같이 적었다.

최적화에 대한 확신이 없다면 가능한 한 모든 곳에 메모이제이션을 활용한 최적화를 하는 것이 좋다.

정확한 공부와 이해를 바탕으로 쓸 수 있다면 적재적소에 쓰도록 하자. 일단 그 전까지는 최대한 많은 곳에 넣도록 해야할 것 같다.

Share article

석우의 개발블로그