김은정
Study

정리하기 파일

2.1 JSX란?

2.1.1 JSX의 정의

  • JSXElement : JSX를 구성하는 가장 기본 요소
  • JSXElementName : JSXElement의 요소 이름으로 쓸 수 있는 것
  • JSXAttributes : JSXElement에 부여할 수 있는 속성
  • JSXChildren : JSXElement의 자식 값
  • JSXStrings

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

JSXElement를 렌더링할 때 굳이 요소 전체를 감싸지 않더라도 처리할 수 있음.

→ props 여부에 따라 children 요소만 달라지는 경우에서 중복 코드 최소화 가능

← JSXElement만 다르고, JSXAttributes, JSXChildren이 동일한 상황이기 때문

import { createElement } from "react";
 
function TextOrHeading({
  isHeading,
  children,
}: PropsWithChildren<{ isHeading: boolean }>) {
  return createElement(isHeading ? "hi" : "span", {
    className: "text",
    children,
  });
}

2.2 가상 DOM과 리액트 파이버

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

  • DOM 노드에 CSS를 적용하는 과정
    • 레이아웃(layout, reflow) : 각 노드가 브라우저 화면의 어느 좌표에 정확히 나타나야 하는지 계산하는 과정
    • 페인팅(painting) : 레이아웃 단계를 거친 노드에 색과 같은 실제 유효한 모습을 그리는 과정

2.2.2 가상 DOM의 탄생 배경

  • 레이아웃은 필연적으로 리페인팅이 발생하기 때문에 더 많은 비용이 든다.

  • SPA는 하나의 페이지에서 계속해서 요소의 위치를 재계산함

    → 페이지 깜빡임 없이 자연스러운 웹페이지 탐색 가능

    → DOM을 관리하는 과정에서의 비용 부담이 커짐

    ⇒ 가상 DOM 등장

  • 가상 DOM : 웹 페이지가 표시해야할 DOM을 일단 메모리에 저장하고 리액트가 실제 변경에 대한 준비가 완료되었을 때 실제 브라우저의 DOM에 반영

    ⇒ 실제로는 여러 번 발생했을 렌더링 과정을 최소화할 수 있음

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

  • 리액트 파이버

    • 리액트에서 관리하는 평범한 자바스크립트 객체
    • 재조정 : 가상 DOM과 실제 DOM을 비교해 변경 사항을 수집하며, 만약 이 둘 사이에 차이가 있으면 변경에 관련된 정보를 가지고 있는 파이버를 기준으로 화면에 렌더링을 요청하는 역할을 함
  • 과거 리액트의 조정 알고리즘은 스택 알고리즘으로 구현됨 → 스택이 빌 때까지 동기적 작업 수행 → 비효율

    ⇒ 스택 조정자 대신 파이버 라는 개념을 탄생

  • 렌더링 단계

    1. 렌더 단계 : 파이버의 작업 finishedWork(), 우선순위를 지정하거나 중지시키거나 버리는 등의 작업 (비동기)
    2. 커밋 단계 : DOM에 실제 변경 사항을 반영하기 위한 작업, commitWork() 실행 (동기)
  • 리액트 요소는 렌더링이 발생할 때마다 새롭게 생성되지만 파이버는 가급적이면 재사용된다.

    ← 최초 렌더링 이후 업데이트가 발생할 경우, 파이버는 이미 존재하므로 되도록 새로 생성하지 않고 기존 파이버에서 업데이트된 props를 받아 파이버 내부에서 처리

  • 파이버의 주요 속성

    • tag : 하나의 element에 하나가 생성되는 1:1로 매칭된 정보를 가지고 있는 것
    • child, sibling, return : 자식,형제,부모 파이버
    • index : 여러 형제들 사이에서 자신의 위치가 몇 번째인지 숫자로 표현
  • 리액트 파이버 트리

    • 더블 버퍼링 : 리액트 파이버의 작업이 끝난 후, 리액트는 단순히 포인터만 변경해 workInProgress 트리를 현재 트리로 바꿔버린다.

      ← 불완전한 트리를 보여주지 않기 위해

    • 현재는 우선순위가 높은 다른 업데이트가 오면 현재 업데이트 작업을 일시 중단하거나 새롭게 만들거나, 폐기할 수 있다.(비동기)

2.2.4 파이버와 가상 DOM

  • 리액트 컴포넌트에 대한 정보를 1:1로 가지고 있는 것이 파이버, 파이버는 리액트 아키텍처 내부에서 비동기로 이뤄짐.

  • 이러한 비동기 작업과 달리, 실제 브라우저 구조인 DOM에 반영하는 것은 동기적으로 일어나야하고, 처리하는 작업이 많아 화면에 불완전하게 표시될 수 있는 가능성이 높음

    ⇒ 이러한 작업을 가상에서, 즉 메모리상에서 먼저 수행해서 최종 결과물만 실제 DOM에 적용

  • 가상 DOM과 리액트의 핵심은 값으로 UI를 표현하는 것.

  • 화면에 표시되는 UI를 자바스크립트의 문자열, 배열 등과 마찬가지로 값으로 관리하고 이러한 흐름을 효율적으로 관리하기 위한 메커니즘이 바로 리액트의 핵심이다.

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

2.3.1 클래스형 컴포넌트

  • React.ComponentReact.PureComponent의 차이, shouldComponentUpdate
    • PureComponent는 얕은 비교만 수행하기 때문에 state 객체와 같은 복잡한 구조의 데이터 변경 감지 X

      → 컴포넌트가 얕은 비교를 했을 때 일치하지 않는 일이 더 잦다면 비교가 무의미해짐. 성능에 역효과

      → PureComponent는 필요한 곳에만 사용하기

  • 클래스형 컴포넌트의 생명주기 메서드
    • 생명주기 메서드가 실행되는 시점
      • 마운트 : 컴포넌트가 마운팅(생성)되는 시점
      • 업데이트 : 이미 생성된 컴포넌트의 내용이 변경(업데이트)뙤는 시점
      • 언마운트 : 컴포넌트가 더 이상 존재하지 않는 시점
    • 생명주기 메서드
      • render : 컴포넌트가 UI를 렌더링하기 위해 쓰임

      • componentDidMount : 클래스형 컴포넌트가 마운트되고 준비되는 즉시 실행

      • componentDidUpdate : 컴포넌트 업데이트가 일어난 이후 바로 실행

      • componentWillUnmount : 컴포넌트가 언마운트되거나 더 이상 사용되지 않기 직전에 호출

        → 메모리 누수나 불필요한 작동을 막기 위한 클린업 함수를 호출할 때 최적

      • shouldComponentUpdate : state나 props의 변경으로 리액트 컴포넌트가 다시 리렌더링되는 것을 막고자 할 때 사용

      • getDerivedStateFromError : 자식 컴포넌트에서 에러가 발생했을 때 호출되는 에러 메서드

        → 하위 컴포넌트에서 에러 발생 시 자식 리액트 컴포넌트를 어떻게 렌더링할 지 결정

      • componentDidCatch : 자식 컴포넌트에서 에러가 발생했을 때 실행되며, getDerivedStateFromError에서 에러를 잡고 state를 결정한 후에 실행

        → 리액트에서 에러 발생 시 메서드에서 제공되는 에러 정보를 바탕으로 로깅하는 등의 용도로 사용

  • 클래스형 컴포넌트의 한계
    • 데이터의 흐름 추적 어려움
    • 애플리케이션 내부 로직의 재사용 어려움 : 공통 로직이 많아질수록 이를 감싸는 고차 컴포넌트, props가 많아지는 래퍼 지옥에 빠져들 위험성이 커짐
    • 기능이 많아질수록 컴포넌트의 크기가 커짐
    • 클래스는 함수에 비해 상대적으로 어려움
    • 코드 크기 최적화 어려움
    • 핫 리로딩을 하는데 상대적으로 불리
      • 핫 리로딩 : 코드에 변경사항이 발생했을 때 앱을 다시 시작하지 않고서도 해당 변경된 코드만 업데이트해 변경 사항을 빠르게 적용하는 기법

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

  • 생명주기 메서드의 부재

    ← 함수형 컴포넌트는 props를 받아 단순히 리액트 요소만 반환하는 함수지만, 클래스형 컴포넌트는 render 메서드가 있는 React.Component를 상속받아 구현하기 때문

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

    ← 함수형 컴포넌트는 렌더링된 값을 기준으로 렌더링하고, 클래스형 컴포넌트는 시간의 흐름에 따라 변화하는 this를 기준으로 렌더링

    • 클래스형 컴포넌트 : this에 바인딩된 props 사용. this가 가리키는 객체(컴포넌트의 인스턴스 멤버)는 변경가능한 값
    • 함수형 컴포넌트 : props를 인수로 받음 ⇒ 불변한 값.

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

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

  • 컴포넌트의 key props가 변경되는 경우, 리렌더링
    • 리액트에서의 key : 리렌더링이 발생하는 동안 형제 요소들 사이에서 동일한 요소를 식별함.
    • 리렌더링이 발생하면 current 트리와 workInProgress 트리 사이에서 변경 사항을 구별해야 하는데, 이 두 트리 사이에서 같은 컴포넌트인지 구별하는 값으로 사용됨.
    • 변경 사항을 구별하는 작업은 리렌더링이 필요한 컴포넌트를 최소화해야 하므로 반드시 필요한 작업임.
    • key가 존재한다면 두 트리 사이에서 통일한 key를 가지고 있는 컴포넌트를 기준으로 구별할 수 있지만, key가 없다면 단순히 파이버 내부의 sibling 인덱스만을 기준으로 판단함.

2.4.4 렌더와 커밋

  • 렌더 단계 : 컴포넌트의 렌더링하고 변경 사항을 계산하는 작업. 렌더링 프로세스에서 컴포넌트를 실행한 결과와 이전 가상 DOM을 비교하는 과정을 거쳐 변경이 필요한 컴포넌트를 체크하는 단계
  • 커밋 단계 : 렌더 단계의 변경 사항을 실제 DOM에 적용해 사용자에게 보여주는 과정

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

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

  • 가벼운 작업 자체는 메모이제이션해서 자바스크립트 메모리 어딘가에 두었다가 그것을 다시 꺼내오는 것보다는 매번 작업을 수행해 반환하는 것이 더 빠를 수 있다.
  • 메모이제이션에도 비용이 든다. 값을 비교하고 렌더링 또는 재계산이 필요한지 확인하는 작업, 그리고 이전에 결과물을 저장해두었다가 다시 꺼내와야 한다는 두 가지 비용이 있다.
  • 항상 메모이제이션은 신중하게 접근해야하며 섣부른 최적화는 항상 경계해야 한다.

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

  • 잘못된 memo로 지불해야하는 비용은 바로 prop에 대한 얕은 비교가 발생하면서 지불해야 하는 비용이다.
  • 반면 memo를 하지 않았을 때 발생할 수 있는 문제는
    • 렌더링을 함으로써 발생하는 비용
    • 컴포넌트 내부의 복잡한 로직의 재실행
    • 그리고 위 두 가지 모두가 모든 자식 컴포넌트에서 반복해서 일어남
    • 리액트가 구 트리와 신규 트리를 비교
  • 메모이제이션은 하지 않는 것보다 메모이제이션했을 때 더 많은 이점을 누릴 수 있다.