고도연
Study

정리하기 파일

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

2.1 JSX란?

JSX는 자바스크립트 표준 코드가 아니므로 JSX는 반드시 트랜스파일러를 거쳐야 비로소 자바스크립트 런타임이 이해할 수 있는 의미 있는 자바스크립트 코드로 변환된다. JSX 내부에 트리 구조로 표현하고 싶은 다양한 것들을 작성해두고, 이 JSX를 트랜스파일이라는 과정을 거쳐 자바스크립트가 이해할 수 있는 코드로 변경하는 것이 목표이다.

2.1.1 JSX의 정의

JSX는 기본적으로 아래 4가지 컴포넌트를 기반으로 구성돼 있다.

JSXElementJSX를 구성하는 가장 기본 요소로, HTML의 요소와 비슷한 역할을 한다.
JSXAttributesJSXElement에 부여할 수 있는 속성을 의미한다.
JSXChildrenJSXElement의 자식 값을 나타낸다.
JSXStringsHTML에서 사용 가능한 문자열은 모두 JSXStrings에서도 사용 가능하다.

2.1.2 JSX 예제

// 대문자로 시작하는 컴포넌트를 만들어야만 사용 가능하다. (리액트에서 HTML 태그명과 구분 짓기 위해서)
// 하나의 요소로 구성된 가장 단순한 형태
const ComponentA = <A>안녕하세요.</A>
 
// 자식 없이 SelfClosingTag로 닫혀 있는 형태도 가능하다.
const ComponentB = <A />
 
// 옵션을 {}와 전개 연산자로 넣을 수 있다.
const ComponentC = <A {...{ required: true }} />
 
// 속성만 넣어도 가능하다.
const ComponentD = <A required />
 
// 속성과 속성을 넣을 수 있다.
const ComponentE = <A required={false} />
 
const ComponentF = (
	<A>
		{/* 문자열은 큰따옴표 및 작은따옴표 모두 가능하다. */}
		<B text="리액트" />
	</A>
)
 
const ComponentG = (
	<A>
		{/* 옵션의 값으로 JSXElement를 넣는 것 또한 올바른 문법이다.*/}
		<B optionalChildren={<>안녕하세요.</>} />
	</A>
)
 
const ComponentG = (
	<A>
		{/* 여러 개의 자식도 포함할 수 있다.*/}
		안녕허세요
		<B text="리액트" />
	</A>
)

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

@babel/plugin-transform-react-jsx

이 플러그인은 JSX 구문을 자바스크립트가 이해할 수 있는 형태로 변환한다.

2.2 가상 DOM과 리액트 파이버

리액트의 특징은 실제 DOM이 아닌 가상 DOM을 운영한다는 것이다.

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

DOM은 웹페이지에 대한 인터페이스로 브라우저가 웹페이지의 콘텐츠와 구조를 어떻게 보여줄지에 대한 정보를 담고 있다.

2.2.2 가상 DOM의 탄생 배경

웹페이지를 렌더링하는 과정은 매우 복잡하고 많은 비용이 든다.

사용자의 인터랙션에 따라 DOM의 모든 변경 사항을 추적하는 것은 너무나 수고스러운 일이다. 이러한 문제점을 해결하기 위해 탄생한 것이 바로 가상 DOM이다.

이렇게 DOM 계산을 브라우저가 아닌 메모리에서 계산하는 과정을 한 번 거치게 된다면 렌더링 과정을 최소화할 수 있다.

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

📍 리액트 파이버란?

  • 가상 DOM과 렌더링 과정 최적화를 가능하게 해주는 것
  • 가상 DOM과 실제 DOM을 비교해 변경 사항을 수집하고 변경 정보를 가지고 있는 파이버를 기준으로 화면에 렌더링을 요청하는 역할을 한다.
  • 리액트 아키텍처 내부에서 비동기로 이뤄진다.

📍 리액트 요소와 파이버의 차이점

  • 리액트 요소는 렌더링이 발생할 때마다 새롭게 생성되지만 파이버는 가급적이면 재사용된다.

📍 파이버가 실행되는 시점

  • state가 변경될 때
  • 생명주기 메서드가 실행될 때
  • DOM의 변경이 필요한 시점

📍 리액트 파이버 트리

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

📍 더블 버퍼링

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

📍 파이버의 작업 순서

  1. 리액트는 beginWork() 함수를 실행해 파이버 작업을 수행하는데, 더 이상 자식이 없는 파이버를 만날 때까지 트리 형식으로 시작된다.
  2. 1번에서 작업이 끝난다면 그다음 completeWork() 함수를 실행해 파이버 작업을 완료한다.
  3. 형제가 있다면 형제로 넘어간다.
  4. 2번, 3번이 모두 끝났다면 return으로 돌아가 자신의 작업이 완료됐음을 알린다.

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

함수형 컴포넌트는 무상태 함수형 컴포넌트(stateless functional component)라고 해서 별도의 상태 없이 단순히 어떠한 요소를 정적으로 렌더링하는 것이 목적이었다.

클래스형 컴포넌트에서 별다른 생명주기 메서드나 상태가 필요없이 render만 하는 경우에만 제한적으로 사용됐다.

그러나 훅이 등장한 이후 함수형 컴포넌트에서 상태나 생명주기 메서드 비슷한 작업을 흉내 낼 수 있게 되자 상대적으로 함수형 컴포넌트가 더 자주 쓰기 시작했다. (클래스형 컴포넌트는 보일러플레이트가 복잡하기 때문)

2.3.1 클래스형 컴포넌트

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

  • 마운트(mount) : 컴포넌트가 마운팅(생성)되는 시점
  • 업데이트(update) : 이미 생성된 컴포넌트의 내용이 변경(업데이트)되는 시점
  • 언마운트(unmount) : 컴포넌트가 더 이상 존재하지 않는 시점

📍 생명주기 메서드

render()컴포넌트가 UI를 렌더링하기 위해 쓰임. 부수 효과가 없어야 한다. (state를 변경하는 일 등)
componentDidMount()컴포넌트가 마운트되고 준비되는 즉시 실행
componentDidUpdate()컴포넌트 업데이트가 일어난 이후 바로 실행. state나 props의 변화에 따라 DOM을 업데이트하는 등에 쓰인다.
componentWillUnmount()컴포넌트가 언마운트되거나 더 이상 사용되지 않기 직전에 호출. 이벤트, 타이머를 지우거나, API 호출 취소 등의 작업을 하는 데 유용하다.
shouldComponentUpdate()컴포넌트에 영향을 받지 않는 변화에 대해 정의. state나 props의 변경으로 리액트 컴포넌트가 다시 리렌더링되는 것을 막을 때 사용한다.
static getDerivedStateFromProps()render()를 호출하기 직전에 호출. static으로 선언돼 있어 this에 접근할 수 없다.
getSnapShotBeforeUpdate()DOM이 업데이트되기 직전에 호출. 윈도우 크기를 조절하거나 스크롤 위치를 조정하는 등의 작업을 하는 데 유용하다.

📍 에러 상황에서 실행되는 메서드

static getDerivedStateFromError()자식 컴포넌트에서 에러가 발생했을 때 호출되는 에러 메서드. 반드시 state 값을 반환해야 한다.
componentDidCatchgetDerivedStateFromError에서 에러를 잡고 state를 결정한 이후에 실행된다.

📍 클래스형 컴포넌트의 한계

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

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

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

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

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

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

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

2.3.2 함수형 컴포넌트

클래스형 컴포넌트와 비교했을 때 달라진 점

  • render 내부에서 필요한 함수를 선언할 때 this 바인딩을 조심할 필요가 없다.
  • state는 객체가 아닌 각각의 원시값으로 관리되어 훨씬 사용하기가 편해졌다.
  • 렌더링하는 코드인 return에서 this사용 없이 props와 state에 접근할 수 있게 된다.

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

📍 생명주기 메서드의 부재

함수형 컴포넌트는 props를 받아 단순히 리액트 요소만 반환하는 함수인 반면, 클래스형 컴포넌트는 render 메서드가 있는 React.Component를 상속받아 구현하는 자바스크립트 클래스이다. 반면 함수형 컴포넌트는 useEffect 훅을 사용해 생명주기 메서드를 비슷하게 구현할 수 있다.

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

함수형 컴포넌트는 렌더링된 값을 고정하고, 클래스형 컴포넌트는 그렇지 못하다. 함수형 컴포넌트는 렌더링이 일어날 때마다 그 순간의 값인 props와 state를 기준으로 렌더링된다. 반면 클래스형 컴포넌트는 시간의 흐름에 따라 변화하는 this를 기준으로 렌더링이 일어난다.

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

브라우저에서의 렌더링이란 HTML과 CSS 리소스를 기반으로 웹페이지에 필요한 UI를 그리는 과정을 의미한다. 리액트의 렌더링은 브라우저가 렌더링에 필요한 DOM 트리를 만드는 과정을 의미한다.

2.4.1 리액트의 렌더링이란?

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

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

리액트에서 렌더링이 발생하는 시나리오

  1. 최초 렌더링
  2. 리렌더링 : 처음 애플리케이션에 진입했을 때 최초 렌더링이 발생한 이후로 발생하는 모든 렌더링을 의미
    • 컴포넌트의 상태가 변화되는 경우
    • 컴포넌트의 key props가 변경되는 경우
    • props가 변경되는 경우
    • 부모 컴포넌트가 렌더링될 경우

2.4.3 리액트의 렌더링 프로세스

렌더링 결과물은 JSX 문법으로 구성돼 있고, 이것이 자바스크립트로 컴파일되면서 React.createElement()를 호출하는 구문으로 변환된다.

2.4.4 렌더와 커밋

  • 렌더 단계 : 컴포넌트를 렌더링하고 변경 사항(type, props,key)을 계산하는 모든 작업을 말한다.
  • 커밋 단계 : 렌더 단계의 변경 사항을 실제 DOM에 적용해 사용자에게 보여주는 과정을 말한다. (생략 가능)

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

메모이제이션 연산의 결과값을 메모리에 저장해두고 이전 값과 결과가 동일할 때 재사용하는 기법

React에서 제공하는 메모이제이션 기법

React.memo()는 props의 값으로 변경을 확인한다.

useCallback()useMemo()는 dependency 배열 내부의 값으로 변경사항을 확인한다.

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

  • 렌더링도 비용이지만 메모리에 저장하는 것도 마찬가지로 비용이다.
  • 미리 개발자가 렌더링이 많이 될것 같은 부분을 예측해 메모이제이션하는, 섣부른 최적화는 옳지 못한 행동이다.

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

  • memo를 하지 않았을 때 치러야 할 잠재적인 위험 비용이 더 크다 (렌더링을 함으로써 발생하는 비용, 컴포넌트 내부의 복잡한 로직의 재실행, 구 트리와 신규 트리를 비교 등)
  • 리렌더링이 발생할 때 메모이제이션과 같은 별도 조치가 없다면 모든 객체는 재생성되고, 결과적으로 참조는 달라지게 된다.
  • 메모이제이션은 컴포넌트 자신의 리렌더링뿐만 아니라 이를 사용하는 쪽에서도 변하지 않는 고정된 값을 사용할 수 있다는 믿음을 줄 수 있다.