Hyunjung Im

Frontend Developer

github

React 함수형 컴포넌트

2023-01-13

함수형 프로그래밍

  • 불변성
  • 순수 함수 사용 (외부에 영향을 주지 않아야 한다.)
  • 함수형 프로그래밍에서는 배열 및 객체와 같은 가변 데이터 구조를 불변 데이터로 취급한다. 함수가 복사본을 기반으로 출력을 계산할 수 있도록 함수에 전달할 때 복사본을 만든다.

함수형 컴포넌트 사용 전

  • 클래스, 중첩된 고차 함수 컴포넌트를 사용하게 되면 코드 -> 복잡
  • 클래스형 컴포넌트 생명주기 api와 함께 조건에 따른 렌더링을 하는 등의 코드를 작성하다보면 코드가 지저분해지고… 이러한 것을 보완하기 위해 함수형 컴포넌트를 사용 react 클래스 구성 요소 api는 몇 년 동안 거의 변경되지 않았음 클래스 구성 요소 api는 react 컴파일러 프로토타이핑 작업 등을 복잡하게 만들 수 있음, 라이프 사이클도 혼란스러움

https://blog.logrocket.com/optimizing-performance-react-application/ https://beta.reactjs.org/learn/queueing-a-series-of-state-updates#react-batches-state-updates https://yceffort.kr/2022/04/deep-dive-in-react-rendering

리액트는 값 UI이다. UI가 문자열이나 배열과 마찬가지로 “값”이라는 것

React가 UI를 업데이트하는 방법

  • 가상 DOM 생성 -> 상태가 변경될 때마다 가상 돔 트리를 다시 만들고 결과를 이전 렌더링과 비교
  • 그런 다음 실제 DOM에서 변경된 요소만 업데이트한다. -> 이것을 diffing이라고 함

가상 DOM 개념을 사용하여 실제 DOM 조작하는 데 비용 많이 듦 -> 다시 렌더링하는 성능 비용을 최소화

  • React는 부모 구성 요소가 다시 렌더링되면 prop전달이 없어도 모든 자식이 리렌더링 됨

js가 컴파일되고 배포 준비가 되는 순간에 React.createElement()를 호출하여 변환된다. 결과물을 수집하고 리액트는 새로운 오브젝트 트리 (가상돔)와 비교하며 모든 변경 사항을 수집한다. 이렇게 비교하고 계산하는 과정을 리액트에서는 reconciliation 이라고 한다. 그런 다음 리액트는 계산된 모든 변경 사항을 하나의 동기 시퀀스로 DOM에 적용한다.

  1. 렌더 단계: 컴포넌트를 렌더링하고 변경 사항을 계산한다.
  2. 커밋 단계: 렌더 단계에서 계산된 변경 사항을 DOM에 적용한다.

리액트는 커밋 단계에서 DOM을 업데이트한 후 요청된 DOM 요소 및 컴포넌트 인스턴스를 가리키도록 모든 참조를 업데이트한다. 그리고 useLayoutEffect 훅을 동기적으로 실행한다. 그런 다음 리액트는 짧은 시간 제한을 설정하고 이 시간이 만료되면 useEffect 훅을 실행한다.

렌더링은 DOM 업데이트와 같지 않으며 결과적으로 어떠한 가시적인 변경도 일어나지 않고 컴포넌트가 리렌더링될 수 있다.

react-rendering

  1. Render phase 사이드 이펙트가 없는 순수함을 가진다. 일시 중지되거나 리액트에 의해 재시작 또는 중단될 수 있다.

  2. Commit phase DOM과 함께 작동할 수 있다. 사이드 이펙트를 실행하고 스케줄을 업데이트한다.

  3. Cleanup phase 컴포넌트가 제거되기 전에 실행해서 메모리 누수를 막는다.


리액트의 기본 동작은 상위 컴포넌트가 렌더링될 때 리액트가 해당 컴포넌트 내부의 모든 하위 컴포넌트를 순환하며 렌더링한다. 즉 컴포넌트를 렌더링하면 기본적으로 모든 하위 컴포넌트가 렌더링 된다.

기억할 것은 렌더링은 나쁜 게 아님. 이는 리액트가 실제로 DOM을 변경해야 하는지 여부를 아는 방법일 뿐.

리액트 렌더링 규칙

렌더링이 ‘순수’해야 하며 어떠한 사이드 이펙트도 없어야 한다.

렌더 로직은 다음을 수행해선 안된다.

  • 기존 변수 및 객체를 변경할 수 없다.
  • Math.random() 또는 Date.now()와 같은 임의의 값을 생성할 수 없다.
  • 네트워크 요청을 할 수 없다.
  • 상태 업데이트를 큐에 추가할 수 없다.

렌더 로직은 다음을 수행할 수 있다.

  • 렌더링 도중 새로 생성된 객체 변경
  • 오류 발생
  • 캐시된 값과 값이 아직 생성되지 않은 데이터에 대한 ‘지연 초기화’

컴포넌트 메타데이터와 파이버(Fibers)

  • 리액트는 애플리케이션에 현재 존재하는 모든 컴포넌트 인스턴스를 추적하는 내부 데이터 구조를 저장한다.이 데이터 구조의 핵심 부분은 ‘파이버(Fiber)‘라고 불리는 객체로 다음과 같은 메타 데이터 필드를 포함한다.
  • 컴포넌트 트리의 해당 지점에서 렌더링되어야 할 컴포넌트 타입
  • 해당 컴포넌트와 관련된 prop, 상태
  • 상위, 형제 및 하위 컴포넌트에 대한 포인터
  • 리액트가 렌더링 프로세스를 추적하는 데 사용되는 기타 내부 메타데이터

React.memo

  • 부모가 변경되어 렌더링이 자식에게 까지 영향을 끼칠 때 memo를 사용하면 자식이 받고있는 prop이 변경되지 않는 한 리렌더링이 일어나지 않게 한다.
  • 반면 props가 object 또는 array 등 참조값인 경우에는 렌더링시 다른 메모리 공간을 가리키기 때문에 재렌더링 된다.

useCallback

  • 리액트는 렌더링 될 때마다 함수들을 재정의한다. 함수가 항상 재정의되는 것을 방지하기 위해 렌더링 사이에 메모아이즈된 콜백을 반환하는 useCallback을 사용할 수 있다.
  • 종속성 배열이 변경되었을 때만 함수가 재정의됨

useMemo

  • 하위 구성 요소에 전달하는 prop이 배열 또는 객체인 경우 useMemo를 사용하여 렌더링 사이의 값을 메모아이즈 할 수 있다.

setState를 사용할 때 state를 바로 수정하는 방법은 함수형 프로그래밍에 맞지 않다. React가 제공하는 한 가지 개선 사항은 업데이터 함수에 콜백을 전달하는 것입니다. 이 콜백에서 이전 버전의 상태에 액세스할 수 있으며 여기에서 상태 값을 업데이트할 수 있습니다.

export default function App() {
  const [test, setTest] = useState(0);
  const [test2, setTest2] = useState(0);
  const [toggle, setToggle] = useState(true);

  // 버튼을 클릭할 때마다 실행되는 두 개의 useEffect가 있습니다.

  // 1번 문제
  useEffect(() => {
    setTest(5);
    setTest(test + 1);
  }, [toggle]);

  // 2번 문제
  useEffect(() => {
    setTest2(5);
    setTest2((prev) => prev + 1);
  }, [toggle]);

  console.log({
    test1: test, // 1. 버튼을 누를 때마다 어떤 값이 찍힐까요?
    test2: test2, // 2. 버튼을 누를 때마다 어떤 값이 찍힐까요?
  });

  return (
    <div className="App">
      <button onClick={() => setToggle(!toggle)}>test</button>
      <h1>Hello CodeSandbox</h1>
      <h2>Start editing to see some magic happen!</h2>
    </div>
  );
}