결론부터 먼저 말씀드리자면, 내용물 끌어올리기를 적용하였다면 그렇지 않습니다.
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
를 호출하여 리-렌더링하는 식이기 때문에, 결과적으로 전체가 리-렌더링되고 있습니다.
내용물 끌어올리기
내용물 끌어올리기에 대한 이해는 이 글이 많이 참고가 되었습니다. 꼭 읽어보시면 좋겠습니다.
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)
를 사용하기 때문에 리-렌더링이 계속 되고 있네요.
Context
를 사용했을 때 전체가 리-렌더링 된다는 것과- 전체 리-렌더링을 피할 수 있는 방법 (내용물 끌어올리기)
이렇게 알아보았습니다. React Context API를 쓰는데 있어 참고가 되면 좋겠습니다!
내용물 끌어올리기가 적용되지 않았을 때
마우스를 움직여 동작을 확인해보세요!
YOUR MOUSE POSITION: [0, 0]
YOUR MOUSE POSITION: [0, 0]
내용물 끌어올리기가 적용되었을 때
마우스를 움직여 동작을 확인해보세요!
YOUR MOUSE POSITION: [0, 0]
YOUR MOUSE POSITION: [0, 0]
전체 코드
주의: 코드를 대충 작성해서 더럽습니다... 깊게 이해하려 시도하지 마세요.
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;