정리하기 파일
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을 비교해 변경 사항을 수집하며, 만약 이 둘 사이에 차이가 있으면 변경에 관련된 정보를 가지고 있는 파이버를 기준으로 화면에 렌더링을 요청하는 역할을 함
-
과거 리액트의 조정 알고리즘은 스택 알고리즘으로 구현됨 → 스택이 빌 때까지 동기적 작업 수행 → 비효율
⇒ 스택 조정자 대신 파이버 라는 개념을 탄생
-
렌더링 단계
- 렌더 단계 : 파이버의 작업
finishedWork()
, 우선순위를 지정하거나 중지시키거나 버리는 등의 작업 (비동기) - 커밋 단계 : 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.Component
와React.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를 하지 않았을 때 발생할 수 있는 문제는
- 렌더링을 함으로써 발생하는 비용
- 컴포넌트 내부의 복잡한 로직의 재실행
- 그리고 위 두 가지 모두가 모든 자식 컴포넌트에서 반복해서 일어남
- 리액트가 구 트리와 신규 트리를 비교
- 메모이제이션은 하지 않는 것보다 메모이제이션했을 때 더 많은 이점을 누릴 수 있다.