frontend

React re-rendering 방지 디자인 패턴(2)

하리하링웹 2023. 1. 14. 15:13

React에서 re-rendering이 일어나는 조건은 크게 보면 3가지정도가 있다.

  1. 함수 내의 State가 변경되었을 때
  2. 부모 컴포넌트가 re-render되었을 때
  3. 컴포넌트의 Props값이 변경되었을 때

자세하게 설명하면 수많은 조건이 있겠지만 대부분의 re-rendering는 저 3가지 조건의 상호작용으로 일어나게 된다.

이번 글에서는 2.부모 컴포넌트가 re-render되었을 때 발생할 수 있는 re-rendering을 방지하는 방법에 대해 작성할 예정이다.

 

간단하게 예시 코드를 보자

import { useState } from 'react';

function BigComponent() {
  console.log('revaluating');
  return <div>complex calculations </div>;
}

export default function ScrollPage() {
  const [y, setY] = useState(0);

  return (
    <div onScroll={(e) => setY(e.currentTarget.scrollTop)} className="max-h-screen overflow-auto">
      <div className="h-[5000px]" />
      <div className="fixed top-20 left-20">
        <p>Scroll Height:{y}</p>
        <BigComponent />
      </div>
    </div>
  );
}

위 코드에서는 가장 바깥쪽에 있는 onScroll event에 의해 매 스크롤 마다 ScrollPage의 y값이 바뀌어 re-rendering이 일어나게 되며 아래에 있는 BigComponent에 대해 re-rendering이 일어나게 된다.

 

아래 사진에서도 매 번 BigComponent가 실행되는것을 확인할 수 있다.

example gif1

따라서 BigComponent 내부의 evaluation 과정에서 어쩌면 체감될정도의 성능상의 손해를 볼 가능성이 생기게 되어버린다. 

 

이 글에서는 이러한 re-render를 방지하기 위해 손쉽게 사용할 수 있는 디자인 패턴을 소개해준다. 

 

일단 위의 코드를 개선한 간단한 예시 코드이다.

import { PropsWithChildren, useState } from 'react';

function ScrollParent({ children }: PropsWithChildren) {
  const [y, setY] = useState(0);
  return (
    <div onScroll={(e) => setY(e.currentTarget.scrollTop)} className="max-h-screen overflow-auto">
      <div className="h-[5000px]" />
      <div className="fixed top-20 left-20">
        <p>Scroll Height:{y}</p>
        {children}
      </div>
    </div>
  );
}

function BigComponent() {
  console.log('re-revaluating');
  return <div>complex calculations </div>;
}

export default function ScrollPage() {
  return (
    <ScrollParent>
      <BigComponent />
    </ScrollParent>
  );
}

실제로 위 코드는 매 스크롤마다 BigComponent함수를 실행하지 않는다.

example gif2

 

여기서 중요한 것은 ScrollParent 함수는 명백하게 re-rendering이 일어나고 있다는 것이다.

 

즉 부모 컴포넌트가 re-render 되었기에 자식 컴포넌트도 re-render가 되어야 한다는 의미이기도 하다.

 

하지만 어째서 BigComponent는 re-render 되지 않을까? 

 

정답은 ScrollParent 함수 내부에서 BigComponent는 컴포넌트로 정의된것이 아니기 때문이다.

 

ScrollParent 함수의 코드를 봐보자

function ScrollParent({ children }: PropsWithChildren) {
  const [y, setY] = useState(0);
  return (
    <div onScroll={(e) => setY(e.currentTarget.scrollTop)} className="max-h-screen overflow-auto">
      <div className="h-[5000px]" />
      <div className="fixed top-20 left-20">
        <p>Scroll Height:{y}</p>
        {children} {/* <- <BigComponent /> */}
      </div>
    </div>
  );
}

여기서 BigComponent는 단순하게 ScrollParent의 children 이라는 props로 정의되어 내부존재하게 된다.

 

즉 ScrollParent가 re-render 되었음에도 BigComponent가 re-render 되지 않는 원리는 단순히 BigComponent가 ScrollParent의 props이기때문이다.

 

즉 부모 컴포넌트의 re-render로 인한 re-render는 자식 컴포넌트에만 일어나고 props에는 아무런 영향도 끼치지 않는다는 의미이다.

 

이러한 원리를 응용한 디자인 패턴은 지금까지 모르고 있었더라도 아마 알게모르게 사용하고 있었을 확률이 높다. 

 

대표적으로 어떤 레이아웃 컴포넌트라던가

import clsx from 'clsx';

export default function CommonLayout({ children, className }: ComponentDefaultPropsWithChildren) {
  return (
    <main className={clsx('relative mx-auto w-full max-w-[1280px]', className)}>{children}</main>
  );
}

 

https://jjongsk.tistory.com/entry/React%EB%A1%9C-%ED%86%A0%EC%8A%A4%ED%8A%B8-%EC%95%8C%EB%9E%8CToast-Notification-Provider-%EB%A7%8C%EB%93%A4%EC%96%B4%EB%B3%B4%EA%B8%B0

 

React로 토스트 알람(Toast Notification) Provider 만들어보기

위 사진에서 UI를 담당하는 Notification Component가 아니라 이를 구현하기 위한 Provider를 만들 예정이다. 필요한 기능들은 아래와 같다. 1. 토스트기능 2. Auto dismiss on/off 기능 3. dismiss ms 설정 기능 4. suc

jjongsk.tistory.com

위와 같은 Contex를 사용한 Provider를 만들때와 같은 경우가 있다.

    <NOTI_CONTEXT.Provider value={value}>
      {children}
      <ul className="fixed bottom-8 left-8 z-[999] flex flex-col items-start space-y-4">
        {notiList.map(({ notiId, ...notiProps }) => (
          <li key={notiId}>
            <Notification notiId={notiId} className="z-20" destroy={destroyNoti} {...notiProps} />
          </li>
        ))}
      </ul>
    </NOTI_CONTEXT.Provider>

Provider의 내부 코드 ( Provider가 실행되더라도 children은 re-render 되지 않는다. )

 

또한 응용해보면 

import { PropsWithChildren, useState } from 'react';

interface Props {
  big2: React.ReactNode;
  big3: React.ReactNode;
}
function ScrollParent({ children, big2, big3 }: PropsWithChildren<Props>) {
  const [y, setY] = useState(0);
  return (
    <div onScroll={(e) => setY(e.currentTarget.scrollTop)} className="max-h-screen overflow-auto">
      <div className="h-[5000px]" />
      <div className="fixed top-20 left-20">
        <p>Scroll Height:{y}</p>
        {children} {/* <- <BigComponent /> */}
        {big2} {/* <- <BigComponent2 /> */}
        {big3} {/* <- <BigComponent3 /> */}
      </div>
    </div>
  );
}

function BigComponent() {
  console.log('re-revaluating');
  return <div>complex calculations </div>;
}

function BigComponent2() {
  console.log('re-revaluating');
  return <div>complex calculations </div>;
}

function BigComponent3() {
  console.log('re-revaluating');
  return <div>complex calculations </div>;
}

export default function ScrollPage() {
  return (
    <ScrollParent big2={<BigComponent2 />} big3={<BigComponent3 />}>
      <BigComponent />
    </ScrollParent>
  );
}

이런 방식으로도 사용할 수 있게된다.

 

위의 예시들은 전부 부모 컴포넌트에서 re-rendering이 일어나도 자식 컴포넌트가 re-render 되지 않는 좋은 예시이며

이러한 원리에 대해 이해하고 있으면 프로젝트를 진행할 때 빈번하게 사용하게될 디자인 패턴중 하나라고 생각한다.