React Deep Dive 2강 - 2

RookieAND·2024년 3월 9일
0

React Deep Dive

목록 보기
3/8
post-thumbnail

✒️ Class Component 살펴보기

✏️ Class Component 의 구조

class Welcome extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      count: 0,
    };
    this.handleClick = this.handleClick.bind(this);
  }

  render() {
    const {
      props: { name },
    } = this;

    return <h1>Hello, {name}</h1>;
  }
}
  • 클래스 컴포넌트는 기본적으로 React.Component 혹은 React.PureComponent 를 확장해야 한다.
  • 컴포넌트 클래스의 생성자 내부에는 super 키워드를 사용하여 상위 클래스인 React.Component 의 생성자를 호출한다.
  • state 기본 값 또한 constructor 내부에 정의했으나, ES2022 를 기준으로 추가된 클래스 필드 문법을 사용하면 클래스 내부에 state 를 바로 정의할 수 있다.

✏️ Class Component 의 LifeCycle

  • 클래스 컴포넌트 내부에서는 컴포넌트가 Mount, Update, Unmount 되면서 실행되는 여러 LifeCycle 메서드를 사용할 수 있다.
  • 함수형 컴포넌트에서는 이러한 생명 주기 메서드를 사용하지 못하지만 일부 라이브러리의 경우 아직 클래스 컴포넌트의 생명주기 메서드에 의존하고 있다.

생명주기 메서드가 실행되는 시점은 크게 세 가지로 정의할 수 있다.

  1. Mount : 컴포넌트가 생성되는 시점
  2. Update : 생성된 컴포넌트의 내용이 변경되어 업데이트 되는 시점
  3. Unmount : 생성된 컴포넌트가 사라져 존재하지 않는 시점

✏️ LifeCycle Method 목록

  1. render()
  • 클래스 컴포넌트의 필수 값으로 쓰이며, 컴포넌트가 UI 를 렌더링하기 위해 쓰인다.
  • 해당 메서드는 항상 순수 함수로 구성되어야 한다 (같은 입력이 들어가면 같은 결과물을 반환)
  1. componentDidMount()
  • 클래스 컴포넌트가 Mount 된 직후 실행되는 생명주기 메서드이다.
  1. componentDidUpdate()
  • 컴포넌트가 Update 되고 난 이후 실행된다.
  1. componentWillUnmount()
  • 컴포넌트가 Unmount 되기 직전에 실행된다. 주로 cleanup 함수를 실행할 때 쓰인다.
  1. shouldComponentUpdate
  • state 및 props 의 변경으로 리렌더링이 트리거 될 때, 해당 메서드를 활용하여 컴포넌트의 업데이트를 제한할 수 있다.
  1. static getDerivedStateFromProps
  • render 메서드가 실행되기 직전에 호출되며 componentWillReceiveProps 를 대체하는 메서드이다.
  • 이후 렌더링 시점에 인계 받은 props 를 바탕으로 현재 state 를 변경할 때 쓰인다.
  1. getSnapShotBeforeUpdate
  • DOM 이 실제로 업데이트 되기 전에 실행되는 메서드이며, 여기서 반환된 값은 componentDidUpdate 로 이전된다.
  1. getDerivedStateFromError()
  • 자식 컴포넌트에서 에러가 발생할 경우 호출되는 에러 메서드이며, 반드시 state 값을 반환해야 한다.
  • react-error-boundary 라이브러리에서는 자식 컴포넌트에서 발생한 에러를 받아 state 로 보관하기 위해 해당 생명주기 메서드를 쓴다.
static getDerivedStateFromError(error: Error) {
    return { didCatch: true, error }; // catch 된 에러를 state 로 보관
}
  1. componentDidCatch
  • 자식 컴포넌트에서 에러가 발생할 경우 getDerivedStateFromError 에서 에러를 잡고 state 를 반환한 후 실행된다.
  • 해당 메서드는 두 개의 인자를 받으며, 첫 번째 인자는 발생한 에러이며 두 번째 인자는 에러를 발생시킨 컴포넌트 정보다.
  • react-error-boundary 라이브러리에서는 자식 컴포넌트에서 발생한 에러를 받아 이를 기반으로 onError props 로 인계 받은 함수를 실행시킨다.
componentDidCatch(error: Error, info: ErrorInfo) {
    this.props.onError?.(error, info);
}

✏️ 클래스 컴포넌트의 한계

클래스 컴포넌트는 아래와 같은 한계를 가진다.

  1. 데이터의 흐름을 추적하기 어렵다
  2. 애플리케이션의 내부 로직의 재사용이 어렵다.
  3. 기능이 많아질수록 컴포넌트의 규모가 커지고 추후 분리가 어렵다.
  4. 함수형 컴포넌트에 비해 러닝 커브가 존재한다.
  5. 기본적으로 작성해야 하는 코드의 양이 많기에 코드 최적화가 어렵다.
  6. Hot Reload 에 불리함이 있다.

✏️ Class Component VS Functional Component

  1. 생명주기 메서드의 부재
  • 생명주기 메서드는 React.Component 클래스 내부의 메서드이기 때문에 이를 사용하지 않는 함수형 컴포넌트의 경우 더 이상 해당 메서드에 접근할 수 없다.
  • 다만 useEffect Hook 을 통한 Passive Effect 를 기반으로 기존에 사용하던 componentDidUpdate 와 같은 생명주기 메서드를 부분적으로 대체할 수 있다.
  • 하지만 useEffect 는 해당 생명주기 메서드를 대체하기 위한 목적이 아닌 Passive Effect 를 실행하기 위한 훅임을 명심하자.
  1. 함수형 컴포넌트의 state 와 props 는 생성될 당시의 값에 대한 SnapShot 이다.
  • 클래스형 컴포넌트의 경우 내부에서 변경된 state 와 props 에 바로 접근할 수 있다. 이는 두 값을 Class 내부의 instance 에서 보관하기 때문이다.
  • 하지만 함수형 컴포넌트의 경우 props 는 인자로 받으며, state 의 경우 컴포넌트 외부의 공간에서 Closure 로 관리된다.
  • 따라서 매 리렌더링이 일어날 때마다 반환된 props 와 state 를 기반으로만 동작하며, 해당 값이 변경되었다면 이후 렌더링에 반영된다.

과거 state 에 대하여 정리한 블로그 포스팅 링크

✒️ React Rendering

✏️ 리액트에서의 렌더링이란?

  • 현재 컴포넌트의 state 와 prop를 기반으로 컴포넌트에게 어떻게 UI 를 구성하고 이를 DOM 에 적용할지를 계산하는 과정이다.

✏️ Rendering Process in React

React 의 Rendering 과정은 크게 아래와 같이 세 단계로 나뉠 수 있다.

  1. Trigger Render
  • 컴포넌트가 처음 렌더링 되거나, 컴포넌트 내부의 리렌더링을 유발시키는 요소 (props, state change) 가 존재할 경우 리렌더링이 발생한다.
  • JSX 코드는 babel 같은 트랜스파일러에 의해 변환되어 React.createElement 코드로 변환되고, 해당 함수는 ReactElement 객체를 반환한다.
  1. Render Phase
  • React 에서 관리하는 Virtual DOM 을 조작하는 일련의 과정이다.
  • 컴포넌트의 호출은 Render Phase 에서 실행되며 반환된 ReactElement 는 Fiber Node로 확장되어 이를 기반으로 Virtual DOM Tree 를 생성한다.
  • 기존의 Stack 기반의 재조정 과정에서는 각 작업 간의 우선 순위를 지정할 수 없었으나 Fiber Architecture 를 도입하며 각 과정을 취소, 중지, 재시작할 수 있게 되었다.

Render Phase 에서는 변경 사항이 화면에 반영되는 것이 아니다.

  • Render Phase 는 각 컴포넌트가 호출되어 반환된 ReactElement 가 Fiber 로 확장되어 Virtual DOM 에 반영되는 과정이다.
  • 변경 사항이 적용된 Virtual DOM 을 기반으로 실제 DOM Tree 를 재구성한다는 의미가 절대 아니다.
  1. Commit Phase
  • Render Phase 에서 재조정된 Virtual DOM 을 실제 DOM 에 적용하는 단계다.
  • Commit Phase 또한 Virtual DOM 의 변경 사항을 DOM 에 Mount 한다는 의미지, 실제 Paint 작업까지 실행되는 것은 아니다.
  • 해당 단계는 Production, Development 모드와 관계 없이 일관된 화면 업데이트를 위해 동기적으로 실행된다.

Commit Phase 또한 변경 사항이 화면에 반영되는 것이 아니다.

  • Commit Phase 의 경우는 동기적으로 실행되기에 DOM 을 조작하기 위한 작업을 Call Stack 에 적재시킨다.
  • 해당 단계가 모두 끝나고 Call Stack 이 비어야 브라우저에서 비로소 화면을 그리는 작업을 시작한다.
  • 정리하자면 Virtual DOM 에 존재하는 변경 사항을 실제 DOM 에 반영하는 과정이 Commit Phase 라고 할 수 있다.

✏️ 언제 리렌더링이 발생하는가?

  1. 상위 컴포넌트로부터 내려 받은 props 의 변화
  2. 컴포넌트 내부의 state 값의 update 발생
  3. 상위 컴포넌트에서 리렌더링이 발생한 경우

리액트에서 리렌더링은 상위 컴포넌트로부터 순차적으로 일어나므로, 자식 컴포넌트의 state 및 props 가 변경되지 않았음에도 같이 리렌더링 됨을 알아야 한다.

✒️ Memoization

✏️ 메모이제이션이 항상 만능일까?

메모이제이션은 절대 공짜가 아니다. 특정 값을 메모리에 적재하는 것은 비용을 수반하며 어떤 경우에는 오히려 값을 보관하는 것이 새로운 값을 매번 생성하는 것보다 비효율적일 수 있다.

또한 메모이제이션의 경우 이전의 값과 새롭게 생성된 값 중 어떤 값을 반환하는지를 결정하는 과정을 필수적으로 거치기 때문에 추가적인 비용이 발생한다.

✏️ 메모이제이션에 대한 책의 생각, 그리고 나의 생각

책에서는 메모이제이션이 늘 최적화의 도움을 준다고 이야기 했으나 필자의 의견은 살짝 다르다.

  1. React 내부에서 Memoization 이 필요한 작업이 그렇게 많은가?

메모이제이션은 공짜가 아니며 특정 값을 별도의 공간에 보관하는 비용과 deps 의 동등성 비교를 통해 두 값중 어떤 값을 반환할지를 결정하는 비용을 수반한다.

또한 메모이제이션은 함수와 값의 생성까지는 막지 못하기에 prop로 참조형 값을 넘겨주거나 특정 값을 추출하기까지 오랜 시간이 걸리는 케이스가 아닌 이상은 오히려 메모이제이션이 불필요해질 수 있다.

  1. 섣부른 Memoization 은 때로 개발자에게 혼선을 줄 수 있다.

리렌더링이 반드시 발생해야 하는 지점에서도 잘못된 메모이제이션으로 인한 결과로 의도치 못한 결과를 맞이할 수 있다.

특정 함수나 특정 값을 props 로 넘기는 게 아닌 이상 이를 사용하는 컴포넌트 입장에서는 결국 매 렌더링마다 새로운 값 혹은 함수를 생성해야 한다.

  1. 리렌더링이 반드시 나쁜 것은 아니라고 생각한다.

사용자의 의도와는 다르게 리렌더링이 계속 생기는 것은 나쁘지만, 반드시 실행되어야 하는 리렌더링이 잘못된 메모이제이션으로 인해 트리거 되지 않는 케이스도 문제라 생각한다.

리렌더링 또한 특정 컴포넌트의 내부 값이 업데이트 되어 발생하는 경우와 컴포넌트가 아예 Unmount 된 이후 Mount 되는 케이스가 있는데, 전자가 후자보다 비용이 압도적으로 적다.

따라서 Update 로 인한 리렌더링을 방지하는 비용이 메모이제이션으로 인한 비용보다 과연 더 클지를 항상 고민해보는 게 좋다고 생각한다. 현재 브라우저와 사용자의 디바이스 성능은 계속해서 좋아지고 있기 때문에, 규모와 로직이 작은 컴포넌트까지 메모이제이션을 굳이 해야 하는가에 대한 고민을 하면 어떨까 싶다.

profile
항상 왜 이걸 써야하는지가 궁금한 사람

0개의 댓글