IT일상

React Context를 쓰면 전체가 리렌더링된다고? 본문

프론트엔드

React Context를 쓰면 전체가 리렌더링된다고?

solo5star 2023. 4. 30. 19:34
리뉴얼 된 블로그로 보기: https://solo5star.dev/posts/42/

 

const App = () => {
  const [mousePosition, setMousePosition] = useState<[number, number]>([0, 0]);

  // 마우스 움직임을 감지하여 mousePosition 상태 계속 업데이트
  useEffect(() => {
    document.addEventListener('mousemove', (event) => {
      setMousePosition([event.clientX, event.clientY]);
    });
  }, []);

  return (
    <MouseMovementContext.Provider value={mouseMovementContextValue}>
      <Header />

      <Article title="오늘도 즐거운 하루" content="오늘 새벽에 좀 많이 추워서 그런지 아침에 힘들었어요. 일교차가 크니 조심하도록 합시다.">
        <MousePositionMeter />
      </Article>
      <Article title="냉동피자가 4천원이면 꽤 괜찮은데?" content="피자치즈 얹어먹으면 더더욱 맛있습니다. 근데 집에 피자치즈가 있어야 합니다." />
      <Article title="힘들었던 일주일이 드디어 끝" content="너무 빡센거같다. 야근도 적당히 해야지!!" />

      <Footer />
    </MouseMovementContext.Provider>
  );
};

이런 구조의 앱이 있다고 가정해봅시다.

 

MouseMovementContext라는게 있고 마우스가 움직이면 상태를 계속 업데이트합니다.

 

MouseMovementContext로 부터 마우스의 위치 상태 값을 받는 컴포넌트는 MousePositionMeter 밖에 없습니다.

 

과연 Header, Article, Footer 모두 리-렌더링 될까요?

 

 

네! 전부 리-렌더링 됩니다. App에서 setState를 함으로서 App에서 리-렌더링이 발생하는데, 함수에서 또 Header, Article, Footer를 호출하여 리-렌더링하는 식이기 때문에

 

결과적으로 전체가 리-렌더링되고 있습니다.

 

 

내용물 끌어올리기

https://overreacted.io/ko/before-you-memo/#%ED%95%B4%EA%B2%B0-%EB%B0%A9%EB%B2%95-2-%EB%82%B4%EC%9A%A9%EB%AC%BC%EC%9D%84-%EB%81%8C%EC%96%B4%EC%98%AC%EB%A6%AC%EA%B8%B0

 

memo()를 하기 전에

자연스럽게 이끌어내는 렌더링 최적화

overreacted.io

내용물 끌어올리기에 대한 이해는 이 글이 많이 참고가 되었습니다. 꼭 읽어보시면 좋겠습니다.

 

const MouseMovementProvider = ({ children }: PropsWithChildren) => {
  const [mousePosition, setMousePosition] = useState<[number, number]>([0, 0]);

  // 마우스 움직임을 감지하여 mousePosition 상태 계속 업데이트
  useEffect(() => {
    document.addEventListener('mousemove', (event) => {
      setMousePosition([event.clientX, event.clientY]);
    });
  }, []);

  return (
    <MouseMovementContext.Provider value={mouseMovementContextValue}>
      {children}
    </MouseMovementContext.Provider>
  );
}

const App = () => {
  return (
    <MouseMovementProvider>
      <Header />

      <Article title="오늘도 즐거운 하루" content="오늘 새벽에 좀 많이 추워서 그런지 아침에 힘들었어요. 일교차가 크니 조심하도록 합시다.">
        <MousePositionMeter />
      </Article>
      <Article title="냉동피자가 4천원이면 꽤 괜찮은데?" content="피자치즈 얹어먹으면 더더욱 맛있습니다. 근데 집에 피자치즈가 있어야 합니다." />
      <Article title="힘들었던 일주일이 드디어 끝" content="너무 빡센거같다. 야근도 적당히 해야지!!" />

      <Footer />
    </MouseMovementProvider>
  );
};

내용물 끌어올리기를 적용하면 이와 같은 코드가 됩니다.

 

Header, Article, Footer를 children을 통해서 전달해주는데, 이 컴포넌트들은 App이 렌더링하여 MouseMovementProvider에 넘겨주기 때문에 App이 리-렌더링 되지 않는 이상 Header, Article, Footer은 리-렌더링 되지 않습니다.

 

즉 MouseMovementProvider가 계속 리-렌더링되어도 Header, Article, Footer는 리-렌더링 되지 않습니다.

 

MousePositionMeter 에서는 useContext(MouseMovementContext) 를 사용하기 때문에 리-렌더링이 계속 되고 있네요.

 

1) Context를 사용했을 때 전체가 리-렌더링 된다는 것과

2) 전체 리-렌더링을 피할 수 있는 방법 (내용물 끌어올리기)

 

이렇게 알아보았습니다. React Context API를 쓰는데 있어 참고가 되면 좋겠습니다!

 

 

Code Sandbox

 

 

전체 코드(더럽습니다)

주의: 코드를 대충 작성해서 더럽습니다... 깊게 이해하려 시도하지 마세요.

 

App.tsx

import React, { useState, useEffect, useMemo, useContext, forwardRef, useRef } from 'react';
import { PropsWithChildren } from 'react';
import './App.css';

const withDetectRender = <P,>(name: string, fc: React.FC<P>): React.FC<P> => {
  return (...args: Parameters<typeof fc>): ReturnType<typeof fc> => {
    const ref = useRef<HTMLDivElement>(null);
    console.log('render', name);

    const $div = ref.current;
    if ($div) {
      $div.classList.remove('rerender');
      $div.offsetHeight;
      $div.classList.add('rerender');
    }

    return (
      <div className="render-unit rerender" data-name={name} ref={ref}>
        {fc(...args)}
      </div>
    )
  };
}

const ArticleContent = withDetectRender('ArticleContent', (props: { content: string }) => {
  const {content} = props;

  return <p>{content}</p>;
});

const Article: React.FC<PropsWithChildren<{ title: string, content: string }>> = withDetectRender('Article', (props) => {
  const { title, content, children } = props;

  return (
    <article style={{ width: '600px' }}>
      <h1>{title}</h1>

      <ArticleContent content={content} />

      {children}
    </article>
  );
});

export const MouseMovementContext = React.createContext<{
  mousePosition: [number, number];
}>({
  mousePosition: [0, 0]
});

const MousePositionMeter = withDetectRender('MousePointerMeter', () => {
  const { mousePosition } = useContext(MouseMovementContext);

  return (
    <p>YOUR MOUSE POSITION: [{mousePosition.join(', ')}]</p>
  );
});

const Footer = withDetectRender('Footer', () => {
  return (
    <footer>
      <MousePositionMeter />
    </footer>
  );
});

const Button: React.FC<{ name: string }> = withDetectRender('Button', (props) => {
  const { name } = props;

  return (
    <button>{name}</button>
  );
})

const Header = withDetectRender('Header', () => {
  return (
    <header>
      <p>쓸데없는 일기 적는 앱</p>
      <ul style={{ display: 'flex' }}>
        <Button name="홈" />
        <Button name="글 목록" />
        <Button name="사이트 정보" />
      </ul>
    </header>
  )
})

const MouseMovementProvider: React.FC<PropsWithChildren> = withDetectRender('MouseMovementProvider', (props) => {
  const { children } = props;
  const [mousePosition, setMousePosition] = useState<[number, number]>([0, 0]);

  useEffect(() => {
    document.addEventListener('mousemove', (event) => setMousePosition([event.clientX, event.clientY]));
  }, []);

  const mouseMovementContextValue = useMemo(() => ({ mousePosition }), [mousePosition]);

  return (
    <MouseMovementContext.Provider value={mouseMovementContextValue}>
      {children}
    </MouseMovementContext.Provider>
  )
})

const App = withDetectRender('App', () => {
  return (
    <MouseMovementProvider>
      <Header />

      <Article title="오늘도 즐거운 하루" content="오늘 새벽에 좀 많이 추워서 그런지 아침에 힘들었어요. 일교차가 크니 조심하도록 합시다.">
        <MousePositionMeter />
      </Article>
      <Article title="냉동피자가 4천원이면 꽤 괜찮은데?" content="피자치즈 얹어먹으면 더더욱 맛있습니다. 근데 집에 피자치즈가 있어야 합니다." />
      <Article title="힘들었던 일주일이 드디어 끝" content="너무 빡센거같다. 야근도 적당히 해야지!!" />

      <Footer />
    </MouseMovementProvider>
  );
});

export default App;

 

 

App.css

p, h1, ul {
  margin: 0;
  padding: 0;
}

@keyframes rerender-border {
  from {
    border-color: rgb(0, 255, 42);
  }
  to {
    color: initial;
  }
}

@keyframes rerender-label {
  from { color: rgb(0, 255, 42); }
  to { color: initial; }
}

body {
  transition: transform 0.5s;
  transform: skew(0, -10deg);
  transform-origin: 50% 50%;
  transform-style: preserve-3d;
}

body * {
  transform-style: preserve-3d;
}

.render-unit {
  margin: 4px;
  margin-top: 24px;
  padding: 4px;
  position: relative;
  border: 1px dashed hsl(0, 0%, 80%);

  transform-origin: top left;
  transition: transform 0.5s;
}

body:hover .render-unit {
  transform: translateX(10px) translateY(10px);
}

.render-unit.rerender {
  animation: rerender-border 0.5s linear;
  animation-fill-mode: forwards;
}

.render-unit.rerender::before {
  display: inline-block;
  content: '⚛' attr(data-name);
  font-size: 12px;
  position: absolute;
  left: 0;
  bottom: 100%;

  animation: rerender-label 0.5s linear;
  animation-fill-mode: forwards;
}
Comments