본문 바로가기
Front-End Study/모던 리액트 딥다이브 스터디

모던 리액트 딥다이브 6회차 [2-3, 2-4, 2-5]

by 코딩기 2024. 6. 17.
728x90

2.3.1 클래스 컴포넌트


🔖오래된 코드의 유지보수나 라이브러리 사용을 효율적으로 하기 위해선 역사의 뒤안길로 사라진 클래스형 컴포넌트를 알 필요가 있다.

  • 기본적으로 클래스 컴포넌트는 클래스 선언과 extends 키워드를 통한 컴포넌트 상속으로 이루어진다.

  • 클래스 컴포넌트를 만들기 위해 extends 하는 컴포넌트이고, shouldComponentUpdate 컴포넌트를 다루는데서 둘의 차이가 생긴다.

    • React.Component

    • React.PureComponent

    // 사용 예시
    import React from "react";
    
    class Sample extends React.Component {
      render() {
        return <h2>Hi~</h2>;
      }
    }
  • 컴포넌트를 만들 때 사용하는 props, state, 메서드의 정의

    import React from "react";
    
    interface SampleProps {
      required?: boolean;
      text: string;
    }
    
    interface SampleState {
      count: number;
      isLimited?: boolean;
    }
    
    class SampleComponent extends React.Component<SampleProps, SampleState> {
      private constructor(props: SampleProps) {
        super(props);
        this.state = {
          count: 0,
          isLimited: false,
        };
      }
    
      private handleClick = () => {
        const newValue = this.state.count + 1;
        this.setState({ count: newValue, isLimited: newValue >= 10 });
      };
    
      public render() {
        const {
          props: { required, text },
          state: { count, isLimited },
        } = this;
    
        return (
          <div>
            <p>You clicked {this.state.count} times</p>
            <button onClick={this.handleClick}>Click me</button>
          </div>
        );
      }
    }
    
    export default SampleComponent;
    • constructor() : 컴포넌트 내부에 존재할 시 컴포넌트 초기화 시점에 호출, state를 초기화하며, super() 를 통해 상속받은 상위 컴포넌트에 접근

    • props : 컴포넌트에 특정 속성을 전달하는 용도, 선언한 형태에 맞게 사용

    • state : 컴포넌트 내부에서 관리하는 값, 항상 객체여야 하고 값의 변화 감지 시 리렌더링

    • 메서드 : 렌더링 함수 내부에서 사용, DOM이벤트와 함께 사용하며 방식은 3가지로 분류

      1. constructor 에서 this 바인딩 하기
      type Props = Record<string, never>;
      
      interface State {
        count: number;
      }
      
      class SampleComponent extends Component<Props, state> {
        // 생략
        // handleClick의 this를 현재 클래스로 바인딩
        private constructor(props: Props) {
          this.handleClick = this.handleClick(this);
        }
      
        // 사용
        public render() {
          // 생략
          return <button onClick={this.handleClick}>증가</button>;
        }
      }
      1. 화살표 함수 사용 : 작성 시점에 this가 상위 스코프로 결정되는 화살표 함수 사용(바인딩 X)

      2. 렌더링 함수 내부에서 함수를 바로 만들어 사용 : 렌더링이 일어날 때마다 함수가 새로 작성되므로 최적화가 어렵다 -> 비추🚫

      render() {
          // 생략
          return <button onClick={() => this.handleClick()}>증가</button>
      }

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

🏷️ 클래스 컴포넌트를 공부할 땐 항상 생명 주기와 생명 주기의 각 시점들을 염두에 두자!

  • 마운드(mount) : 컴포넌트가 마운팅(생성)되는 시점

  • 업데이트(update) : 생성된 컴포넌트의 내용이 변경되는 시점

  • 언마운트(unmount) : 컴포넌트가 더 이상 존재하지 않는 시점

alt text


  • render() : 클래스 컴포넌트의 유일 필수 값, UI 렌더링 목적

    • render() 는 항상 순수해야 하고 부수 효과(side effect)가 없어야한다.

    • 항상 간결하고 깔끔하게 작성해야 하고 state를 직접 호출해선 안된다.

  • componentDidMount() : 마운트 시 처음으로 실행되는 메서드

    • this.setState()를 통해 state 값을 변경하는 것이 가능하다.

    • State 가 변경되면 브라우저가 UI를 업데이트하기 전에 실행하여 사용자가 렌더링 하는 것을 눈치챌 수 없게 한다.

    • 만능이 아니므로 굳이 이 메서드가 아니더라도 작업할 수 있는 경우엔 사용하지 않는 것이 좋다. (API 호출 후 업데이트, DOM 의존성 작업 등에만 사용)

  • compoenetDidUpdate() : 컴포넌트 업데이트가 일어난 이후 실행하며 상태값이나 props의 변화에 따라 DOM을 업데이트 하는데 쓰인다.

    • this.setState 사용 시 조건을 명확히 걸지 않는다면 계속해서 호출될 수 있다. (성능 문제 야기)
  • componentWillUnmount() : 컴포넌트가 언마운트 되거나 사용되지 않을 때 호출하며 클린업 함수 호출에 최적화 되어있다.

    • this.setState() 호출이 불가하다.
  • shouldComponentUpdate() : stateprops의 변경에 의한 리렌더링을 방지하고 싶을 때 사용하며 기본적으로 특정한 최적화 상황에서만 사용해야 한다.

    // props의 title이나 state의 input 값이 다를 경우에만 true (리렌더링)
    shouldComponentUpdate(nextProps: Props, nextState: State) {
      return this.props.title !== nextProps.title || this.state.input !== nextState.input
    }

🏷️ 위에서 언급한 ComponentpureComponent의 차이가 바로 shouldComponentUpdate()를 어떻게 다루느냐에 있다.

  • 기본적으로 shouldComponentUpdate()의 리턴값은 true이고, 다시 말해 이는 stateprops의 변경 여부를 신경쓰지 않고 실행되기만 하면 리렌더링 한다는 의미이다.

  • pureComponent 도 마찬가지로 shouldComponentUpdate()가 구현되어 있지만, propsstate를 얕은 비교를 통해 비교하여 변경된 것이 없다면 false를 반환하여 리렌더링을 방지한다.

pureComponent() 는 얕은 비교만 수행하므로 무분별하게 사용 시 제대로 동작하지 않을 수 있다.

  • static getDerivedStateFromProps() : 다음에 올 props를 바탕으로 현재 props를 변경할 때 사용한다.

    • static 으로 선언되어 this에 접근할 수 없다.

    • 반환하는 객체의 내용은 모두 state로 들어간다.

    • null 반환 시 아무 일도 일어나지 않는다.

  • getSnapShotBeforeUpdate() : 이름에서도 알 수 있듯이 새로 업데이트 될 클래스가 렌더링 되기 전 이전 클래스의 값을 기억해놨다가 새로운 클래스로 전송한다.

    • 업데이트 전에 윈도우 크기를 조절하거나 스크롤 위치 조절 등에 사용한다.

❗다음 메서드들은 기존과 다르게 Error 발생 상황에서 사용하는 메서드이다.

  • getDerivedStateFromError() : 자식 컴포넌트에서 에러가 발생할 시 사용되는 메서드이다.

    • static 한 컴포넌트로 하위 컴포넌트에서 발생한 에러를 인수로 받는다.

    • 반드시 state 값을 반환해야 하고, 에러를 받아 적절하게 렌더링하는 용도로 사용한다.

    • 에러에 따른 상태 state를 반환하는 작업만을 진행해야 하며 render 에서 호출되는 메서드이기 때문에 불필요한 부수 효과를 추가할 필요는 없다. (console.error() 사용 등)

  • componentDidCatch() : 자식 컴포넌트에서 에러가 발생했을 때 실행되며, getDerivedStateFromError() 가 에러를 잡고 state를 반환한 후에 실행된다.

    • 부수 효과 사용이 가능하다.

    • 커밋 단계에서 실행되어 에러 발생 시 에러를 바탕으로 정보를 로깅하는 용도로도 사용 가능하다.

    • 에러 경계 컴포넌트인 ErrorBoundary 를 여러 개 만들어서 사용 가능하여 컴포넌트 별로 에러 상황에 대처할 수 있다.

    • 에러 발생 영역만 따로 처리하여 에러가 앱 전체로 퍼지는 걸 막을 수 있다.


😅 클래스 컴포넌트의 한계

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

    • 서로 다른 여러 메서드에서 state의 업데이트가 일어날 수 있어 이를 추적하여 렌더링이 일어나는 과정을 판단하기 어렵다.
  • 앱 내부 로직의 재사용이 어렵다.

    • 재사용 시 컴포넌트를 또 다른 고차 컴포넌트에 넘겨야 하는데, 이런 식의 작업을 반복하면 래퍼 지옥에 빠질 수 있다.
  • 기능이 많을 수록 컴포넌트의 크기가 커진다.

    • 내부의 로직과 컴포넌트가 처리하는 데이터가 커질 수록 컴포넌트 자체의 크기가 너무 커진다.
  • 클래스가 어렵다..

    • 클래스보다 함수에 익숙한 개발자가 많고, JS 환경 자체에서 클래스를 사용하는 것이 쉽지 않다.
  • 코드 크기를 최적화 하기 어렵다.

    • 사용하지 않는 메서드도 모두 정의해야 하기 때문에 최종적으로 번들링 시 쓸데 없는 비용이 추가된다.
  • 핫 리로딩을 하는데 상대적으로 불리하다.

    • 코드에 변경사항이 발생했을 때 리렌더링 없이도 변경된 코드만 업데이트 하는 기법이다.

    • 클래스 컴포넌트 사용 시에는 instance 내부의 render 값이 수정되어 코드가 초기화 된다.


2.3.2 함수 컴포넌트


🔖 단순히 무상태 컴포넌트 구현을 위해 사용했으나, Hook의 출현 이후로 각광받고 있다.

  • 클래스 컴포넌트와 다르게 훨씬 더 간결하게 stateprops를 다룰 수 있다.

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


🔖 클래스와 함수 컴포넌트 사이에는 어떤 차이가 있고, 왜 클래스 컴포넌트는 도태되어갈까?

  • 생명주기 메서드의 부재

    • 함수 컴포넌트에는 생명주기 메서드가 존재하지 않는다. (React.Component 클래스를 상속받아 메서드를 사용하기 때문)

    • 비슷한 훅으로 useEffect가 있지만, 이는 생명주기를 흉내내어 동기적으로 state 를 활용해 부수 효과를 만들 뿐, 같은 개념이 아니다.

  • 함수 컴포넌트의 렌더링 값 고정

    // 함수형 컴포넌트
    function funcComponent(props) {
      const showMessage = () => {
        alert(`hello, ${props.user}`);
      };
    
      const handleClick = () => {
        setTimeout(showMessage, 3000);
      };
    
      return <button onClick={handleClick}>클릭</button>;
    }
    
    // 클래스형 컴포넌트
    class classComponent extends React.Component {
      private showMessage = () => {
        alert(`hello, ${this.props.user}`);
      }
    
      private handleClick = () => {
        setTimeout(this.showMessage, 3000);
      }
    
      return <button onClick={this.handleClick}>클릭</button>+
    }
    • 위 예제는 동일한 결과를 가질 것 같지만, 차이가 존재한다.

    • 클래스는 props의 값을 항상 this로부터 가져오기 때문에, props는 불변이나 this가 변경 가능하여 setTimeout에 의한 3초간 props가 변경된다면 변경된 값을 출력하게 된다.

    • 결국 함수 컴포넌트가 짱이다..👍

  • 클래스 컴포넌트를 공부해야 할까?

    • 결론은 현재 널리 사용하는 함수 컴포넌트를 중점적으로 확실하게 공부하되, 클래스 컴포넌트가 아예 없어질 전망은 없으므로, 다양한 상황에 대비해서 조금씩은 공부하는 것이 좋을 듯 하다.
  • 링크 : 리액트의 클래스형 컴포넌트


2.3.4 책 정리 + 주관적인 정리


🔖 책 정리

  • 숙련된 리액트 개발자가 되기 위해선 어떤 고민들을 통해 리액트가 발전해 왔는지를 알아야 할 필요가 있다.

  • 함수 컴포넌트를 먼저 익혀 숙달되면 클래스를 익혀 리액트를 완전히 정복해보자

🏷️ 주관적인 정리

  • 예전에 프론트를 하겠다고 맘먹기 전에 백을 위해 자바 공부를 했었는데, 그 때 자주 사용하던 클래스의 개념이 나와 반가웠다.

  • 리액트의 전반적인 흐름과 확실한 숙달을 위해서 현재 사용하는 함수 컴포넌트의 윗 세대인 클래스 또한 어느정도 공부하고 관련 지식을 알고 있으면 좋을 것 같다.

  • 반갑긴 하지만 리액트에선 별로 사용하고 싶지 않다..


2.4 렌더링은 왜 일어나는가?

✨ 리액트의 '렌더링'이란 브라우저의 렌더링에 필요한 DOM 트리를 만드는 작업을 의미하며, 개발자라면 렌더링의 과정을 공부하고 이를 효율적으로 운용해야 한다.


2.4.1 리액트의 렌더링이란?


🔖 리액트 앱 트리 안에 있는 모든 컴포넌트들이 자신들의 propsstate를 기반으로 UI를 구성하고 DOM 결과를 브라우저에 제공하는 일련의 과정을 의미한다.


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


🔖 렌더링 발생 시나리오

  • 최초 렌더링 : 사용자가 처음 접속했을 때를 위해 최초 렌더링 시행

  • 리렌더링 : 최초 이후 발생하는 모든 렌더링

    • 클래스 컴포넌트의 setState, forceUpdate 가 실행되는 경우
    • 함수 컴포넌트의 useState의 요소인 setter, useReducer의 요소인 dispatch 가 실행되는 경우
    • 컴포넌트의 key props 가 변경되는 경우

key props는 하위 컴포넌트를 선언할 때 사용하며, 동일 요소를 식별하기 위해 사용한다.(ex. 배열을 통해 동일한 요소들을 렌더링하는 경우)

  • 앞서 보았던 파이버가 요소를 판별하는 것을 생각하면 key props를 사용하는 이유를 유추할 수 있다.

✅ 결론적으로 리액트 상태관리와 관련된 라이브러리들이 리렌더링을 발생시키는 것이 아닌, 내부에서 useState 를 사용하여 리렌더링을 발생시키는 것이다.


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


🔖 리액트의 렌더링 과정을 알아보자

  • 렌더링 시작 시 리액트는 루트부터 하위까지 업데이트가 필요한 부분을 찾는다.

  • 업데이트 필요 부분을 찾으면 클래스의 경우 render() 실행, 함수의 경우 함수 자체를 호출한다.

  • 이런 과정을 통해 가상 DOM과 비교한 렌더링 결과물을 실제 DOM에 반영한다.

  • 이는 렌더와 커밋이라는 두 단계로 분리되어 실행된다.


2.4.4 렌더와 커밋


🏷️ 렌더

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

    • 컴포넌트를 실행해 가상 DOM과 비교하여 업데이트할 항목을 체크하는 모든 과정을 의미한다.

    • 비교하는 것은 크게 type, props, key 이다.

🏷️ 커밋

  • 렌더 단계을 통해 도출한 모든 변경 사항을 실제 DOM에 적용해 사용자에게 보여주는 과정

    • 여기까지 완료해야 브라우저의 렌더링 발생

    • 업데이트된 항목에 리액트의 내부 참조를 업데이트하여 연결하고, 클래스의 경우 componentDidMount(), commponentDidUpdate() 를 실행하고, 함수의 경우 useLayoutEffect() 훅을 실행한다.

❗여기서 알 수 있는 사실은 꼭 렌더 단계가 실행된다고 해서 커밋 단계가 실행되진 않는다는 것이다.

  • 변경 사항 계산 시 변경 사항이 없다면, 커밋 단계가 생략될 수 있다.

  • 이는 브라우저의 DOM 업데이트가 발생되지 않는 것으로 이어진다.

✅ 위 두 과정으로 인해 리액트의 렌더링은 항상 동기식으로 이루어지며, 이는 불필요한 렌더링이 발생할 경우 성능 저하로 이어질 수 있음을 보여준다.


2.4.5 일반적인 렌더링 시나리오 살펴보기

🔖 예시를 통해 렌더링 시나리오 살펴보기

import React, { useState } from "react";
import ReactDOM from "react-dom";

const B = memo(() => {
  return <div>안녕~</div>;
});

function App() {
  // useState를 사용하여 count 상태를 정의
  const [count, setCount] = useState(0);

  // 버튼 클릭 시 호출되는 함수
  const handleClick = () => {
    setCount(count + 1);
  };

  // 컴포넌트의 JSX 반환
  return (
    <div>
      <h1>Counter: {count}</h1>
      <button onClick={handleClick}>Increment</button>
      <B />
    </div>
  );
}
  • 기본적으로 컴포넌트 렌더링은 별도의 조치가 없다면 해당 컴포넌트의 모든 하위에 영향을 미친다.

  • React.memo를 사용해 변경되지 않는 컴포넌트의 리렌더링을 방지할 수 있다.


2.4.6 책 정리 + 주관적인 정리


🔖 책 정리

  • 개발자는 항상 리액트의 눈에 보이지 않는 렌더링을 인지하고 다룰 수 있어야 한다.

  • 리액트의 렌더링 시나리오를 정확히 이해하여 불필요한 리렌더링을 방지하고 성능 좋은 리액트 앱을 만들자!

🏷️ 주관적인 정리

  • UI가 변경되는 것이 곧 리액트의 리렌더링이라고 생각해왔는데, 눈에 보이지 않는 렌더링도 존재한다니 가슴이 답답해진다.

  • 하지만 이를 잘 인지하고 공부하여 이해하는 것이 리액트의 근본을 파는 것이고 곧 실력있는 개발자가 되는 길이므로 열심히 공부해야 할 듯 하다.


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

React.memo, useCallback, useMemo를 통해 현명하고 효율적으로 메모이제이션 하는 방법을 알아보자


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


🔖 메모이제이션 또한 비용이 드는 엄연한 작업이므로 항상 신중하게 사용해야 한다.

function sum(a, b) {
  return a + b;
}
  • 위 예제 같은 경우 메모이제이션과 매번 계산, JS에 맡기는 것들 중 뭐가 더 나을까?

  • 극단적이긴 하나 엄연히 메모이제이션 또한 메모리를 사용해 기존 값을 가져오는 것과 이를 비교하는 판단에 드는 비용이 있으므로, 섣부르게 사용하는 것을 방지해야 한다.

개발자에게 메모이제이션 할 선택권을 쥐어준 것부터 알아서 유도리 있게 사용하라는 방증이다. 😠


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


🔖 일반적으로 컴포넌트들은 비싼 연산이 포함되고 자식 컴포넌트가 많이 딸려있을 확률이 높다.

  • 이런 전제를 깔고 가는 경우 메모이제이션 하는 것이 확실히 도움이 될 것이다.

    • memo 를 컴포넌트의 사용에 따라 잘 살펴보고 적용

    • memo 를 그냥 다 박아버리기

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

  • 리액트는 이전 결과물을 다음 결과와 비교하기 위해 저장해야 하고, 이를 비교하면서 발생하는 비용이 memo로 지불하는 비용이다.

  • 하지만 memo를 사용하지 않을 시 발생할 잠재적 비용이 훨씬 크다.

    • 렌더링 시 발생 비용

    • 컴포넌트 내부 복잡한 로직의 재실행

    • 자식에서 위 두 과정이 반복

    • 리액트의 재조정 과정

🔖 useCallbackuseMemo

  • 의존성 배열 비교 및 필요에 따른 값 재계산 과정과 메모이제이션을 통한 과정에서 발생하는 비용을 항상 고민해야 한다.

    // 함수 메모이제이션 예제
    function useMath(number) {
      const [double, setDouble] = useState(0);
      const [tripple, setTripple] = useState(0);
    
      useEffect(() => {
        setDouble(number * 2);
        setTripple(number * 3);
      }, [number]);
    
      return { double, tripple };
    }
    
    function App() {
      const [counter, setCounter] = useState(0);
      const value = useMath(10);
    
      useEffect(() => {
        console.log("hi");
      }, [value]);
    
      const handleClick = () => {
        setCounter((prev) => prev + 1);
      };
    
      return (
        <>
          <h1>{counter}</h1>
          <button onClick={handleClick}>클릭</button>
        </>
      );
    }
  • 위 예제의 경우, 실제로 변하는 값이 없음에도 불구하고 console.log('hi') 구문이 실행된다.

  • App 컴포넌트가 호출되며 계속해서 useMath 훅을 호출하고 참조가 변경되기 때문이다.

  • 따라서 useMath의 리턴값을 useMemo()를 통해 감싸면, 비교를 수행하며 변하지 않은 값으로 인한 렌더링을 방지할 수 있다.


책 정리 및 주관적인 정리


🔖 책 정리

  • 아직 리액트에 대한 이해가 부족하다면, 섣부른 메모이제이션을 지양하며 어느 지점에서 이를 사용하면 좋을지 고민하는 형식으로 메모이제이션을 차근차근 적용해야 한다.

  • 하지만 현업에서 이미 종사하고 있거나 성능에 대해 깊게 연구할 여유가 없다면 일단 다 적용해보는 것이 좋다. 섣부른 메모이제이션의 비용이 다른 모든 비용보다 싼 경우가 많다.

🏷️ 주관적인 정리

  • 세가지의 메모이제이션용 훅들에 대해 알고는 있었지만 사용해본 경험이 많지 않았다. 이번에 공부하면서 정확한 사용 용도와 사용법, 그리고 사용에 대한 고민까지 다양하게 접할 수 있어 좋았다.

  • 현재 스프린트 미션이나 기초 프로젝트 결과물에도 적용해보면서 사용감을 익히는 것이 좋을 듯 하다. 책에서 막 갈기라고 했다고 진짜 아무데나 갈기지 말자.

  • 링크 : 전부 메모이제이션 할까요?