본문 바로가기

독서 기록

02. 리액트 핵심 요소 깊게 살펴보기 [모던 리액트 Deep Dive]

JSX란?

JSX(JavaScript XML)는 JavaScript 코드 안에서 HTML을 작성할 수 있게 해주는 문법이다. JSX를 사용하면 React 컴포넌트를 직관적이고 가독성 있게 작성할 수 있다. JSX 코드는 브라우저가 직접 이해할 수 없으므로, Babel 같은 트랜스파일러를 통해 JavaScript 코드로 변환된다. 이 변환된 코드는 React의 React.createElement() 함수를 사용해 DOM 요소를 생성하게 된다.

JSX의 기본 구성 요소

  1. JSXElement:
    • HTML 구문 외에도 사용자 정의 컴포넌트 태그를 나타낸다. 사용자 정의 태그는 대문자로 시작해야 한다.
  2. JSXAttributes:
    • JSXElement에 부여할 수 있는 속성들로, HTML 속성처럼 동작한다. props를 통해 컴포넌트에 데이터를 전달하는 데 사용된다.
  3. JSXChildren:
    • JSXElement 내부에 포함된 자식 요소를 의미한다. 다른 JSXElement, 문자열, 숫자, 또는 React 컴포넌트가 될 수 있다.
  4. JSXStrings:
    • JSX에서 사용 가능한 문자열로, 큰따옴표(" "), 작은따옴표(' '), 백틱(```) 등을 사용해 표현할 수 있다.

 

const ComponentG =(
<A>
< B optionalChildren={<>안녕하세요.</>)} />
</A>
)

JSX문법에는 있지만 실제로 리액트에서 사용하지 않는 것도 있다. 

 

React에서의 가상 DOM과 Fiber

DOM(Document Object Model)은 웹 페이지의 구조를 표현하는 트리이다. DOM 변경은 레이아웃 재계산과 리페인팅으로 이어져 성능 비용이 발생한다. 특히 SPA(Single Page Application)에서는 이러한 변경이 빈번하게 발생할 수 있다.

이를 최적화하기 위해 React는 가상 DOM을 사용한다. 가상 DOM은 실제 DOM에 변화가 발생하기 전에 메모리에서 미리 변경 사항을 계산해 최소한의 DOM 조작만 수행하게 한다.

React Fiber는 React의 재조정 엔진으로, 가상 DOM과 실제 DOM을 비교해 변경 사항을 수집하고, 필요한 경우 렌더링을 요청한다. Fiber는 비동기적으로 작동하여 UI를 부드럽게 업데이트할 수 있다.

 

 

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

클래스형 컴포넌트:

  • Props는 부모 컴포넌트가 자식 컴포넌트에 속성을 전달하는 역할을 한다.
  • State는 컴포넌트 내에서 관리되는 동적인 데이터로, 상태가 변경될 때마다 컴포넌트가 리렌더링된다.
  • 메서드는 컴포넌트 내부에서 정의된 함수로, 이벤트 핸들러나 기타 로직을 처리하는 데 사용된다.
  • 클래스형 컴포넌트의 생명주기 메서드는 컴포넌트가 DOM에 삽입되거나 업데이트되거나 제거될 때 특정 작업을 수행할 수 있다.

 

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

  • 데이터 흐름을 추적하기 어렵고, 로직의 재사용이 힘들며, 기능이 많아질수록 코드가 복잡해진다.
  • 리액트의 생명주기 메서드를 이해하고 관리해야 하므로 코드가 장황해질 수 있다.

함수형 컴포넌트:

  • 최근 React에서는 함수형 컴포넌트가 더 선호된다.
    함수형 컴포넌트는 훅(Hooks)을 통해 상태 관리와 생명주기 메서드를 간단하게 구현할 수 있다.
  • Hooks는 useState, useEffect 등을 사용하여 함수형 컴포넌트에서도 상태를 관리하고 부수 효과를 처리할 수 있다.
import { useState } from 'react'

type SampleProps = {
  required?: boolean // ?: 선택적 속성임을 의미 (ts)
  text: string
}

export function SampleComponent({ required, text }: SampleProps) {
  const [count, setCount] = useState<number>(0)
  const [isLimited, setIsLimited] = useState<boolean>(false)

  function handleClick() {
    const newValue = count + 1
    setCount(newValue)
    setIsLimited(newValue >= 10)
  }

  return (
    <h2>
      Sample Component
      <div>{required ? '필수' : '필수 아님'}</div>
      <div>문자: {text}</div>
      <div>count: {count}</div>
      <button onClick={handleClick} disabled={isLimited}>
        증가
      </button>
    </h2>
  )
}

 

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

브라우저의 렌더링 : HTML, CSS 리소스를 기반으로 웹페이지에 필요한 UI를 그리는 과정

리액트의 렌더링 : 브라우저가 렌더링에 필요한 DOM 트리를 만드는 과정

 => 리액트 애플리케이션 트리 안에 있는 모든 컴포넌트들이 자신들의 props와 state를 기반으로 어떻게 UI를 구성하고, 어떤 DOM결과를 부라우저에 제공할지 계산하는 과정!

 

< 언제 렌더링이 발생하는가? > - 리액트 렌더링 발생 시나리오

최초 렌더링 : 사용자의 초기 애플리케이션 진입 

리렌더링 : 최초 렌더링 이후의 모든 렌더링

  •  useState()의 두 번째 배열 요소인 setter가 실행되는 경우
  • useReducer()의 두 번째 배열 요소인 dispatch가 실행되는 경우
  • 컴포넌트의 key props가 변경되는 경우 (자식 컴포넌트가 있다면, 자식도 무조건 리렌더링 발생)

(클래스형 컴포넌트에서도 유사한 동작시 리렌더링 발생)

 

리액트에서 key는 모든 컴포넌트에서 사용할 수 있는 특수 props이다.

 -> 리렌더링 中 컴포넌트간 구별에 사용하여 리렌더링 최소화

 

< 렌더링이 어떤 과정을 거쳐 수행되는가? > - 리액트의 재조정

렌더링 프로세스 시작  -> 컴포넌트의 루트에서부터 업데이트가 필요한 모든 컴포넌트를 찾음

-> 발견 시 render()함수 실행 / FunctionComponent() 호출 후 결과물(return) 저장 (JSX)

-> JS로 컴파일 되며 React.createElement()로 변환됨 -> DOM에 적용

 

render : 컴포넌트를 렌더링하고 변경 사항을 계산하는 모든 작업 

             결과와 이전 가상 DOM에서 type, props, key등을 비교

commit : render단계의 변경 사항을 실제 DOM에 적용

=> 브라우저의 렌더링 

 

! 렌더링이 일어났다고 해서 가시적인 변경이 꼭 있지는 않다. ! 

commit할 필요 없다면(변경 사항이 감지되지 않는다면) DOM update하지 않음!

상위 컴포넌트의 렌더링은 하위 모든 컴포넌트의 리렌더링을 트리거하기에 주의해야 한다.

React 클래스형 컴포넌트의 생명주기

 

메모이제이션

리액트에서 제공하는 API : useMemo, useCallback, memo

언제 사용하는 것이 좋을까...? 

꼭 필요한 곳에만 사용하자 vs 렌더링 과정은 비싸니 모두 메모이제이션 해버리자

 

리액트 공식 문서 中

더보기

useMemo는 성능 최적화를 위해 사용할 수는 있지만 의미상으로 그것이 보장된다고 생각하자는 마세요. 가까운 미래에 리액트에 서는 이전에 메모이제이션된 값들의 일부를 잊어버리고” 다음 렌더링 시에 그것들을 재계산하는 방향을 택할지도 모르겠습니다. 예를 들巴 오프스크린 컴포넌트의 메모리를 해제하는 등이 있을 수 있습니다. useMemo를 사용하지 않고도 작동할 수 있도록 코드를 작성하고 그것을 추가해 성능을 최적화하세요. 

애플리케이션을 어느 정도 만든 후, 개발자 도구나 useEffect를 통해 렌더링 발생을 확인하고, 필요한곳에만 최적화하자!

(깊이 공부가 되었다면, 이 방식을 채택!)

 

처음부터 메모이제이션을 적용해도 괜찮은 경우

  • 해당 컴포넌트가 렌더링이 자주 일어날 것이 확정된 경우
  • 해당 렌더링에 비싼 연산이 포함되어 있고, 자식 컴포넌트 또한 많이 가지고 있는 경우

BUT, 잘못된 memo로 지불해야하는 비용은 props에 대한 얕은 비교가 발생하며 지불해야하는 비용뿐이다.

(메모이제이션 하지 않더라도, 이전 렌더링 결과와 비교를 위해 어차피 이전 결과는 저장해두어야함..)

 

이는 메모이제이션을 하지 않았을 때 발생할 수 있는 문제에 비해서 그 비용이 작다.

=> 최적화에 대한 확신이 없다면 가능한 한 모든 곳에 메모이제이션을 활용하자 !!!

 

 

결론

React 개발에서는 클래스형 컴포넌트와 함수형 컴포넌트를 모두 이해하는 것이 중요하다. 현재 함수형 컴포넌트가 주로 사용되지만, 클래스형 컴포넌트에 대한 기본적인 지식은 여전히 필요하다. 성능 최적화와 코드 유지보수를 위해 메모이제이션과 같은 기법을 적절히 사용하는 것이 중요하다.

'독서 기록' 카테고리의 다른 글

01. 자바스크립트 [모던 리액트 Deep Dive]  (3) 2024.09.03