이 문서의 내용은 대부분 아래 두 문서에서 가져와 정리한 것입니다.
React 공식 문서에서 같이 확인하면 좋을 내용은 (🌐 공식 문서 참고) 링크를 붙였습니다. 그리고 독자의 이해를 돕기 위해 각 최적화 방법에 대한 일러스트를 추가했습니다. 일러스트는 React 공식 문서의 렌더 트리 일러스트를 참고하였습니다.
React 리렌더링 최적화 방법을 조사하게 된 계기
사내 프론트엔드의 서비스의 규모가 커지고 복잡해지면서 상태 관리와 리렌더링으로 인한 성능 이슈가 발생했습니다. 그래서 React가 어떻게 렌더링을 하는지, 그리고 리렌더링을 최적화하는 방법은 무엇인지 알아보았습니다.
React 화면 업데이트 과정
“렌더링”은 React에서 컴포넌트를 화면에 업데이트하는 과정 중 하나입니다. 그래서 렌더링을 이해하기 위해서는 React가 어떻게 컴포넌트를 화면에 업데이트하는지 알아야 합니다. React는 컴포넌트를 화면에 업데이트하기 위해 트리거, 렌더링, 커밋이라는 세 단계의 작업을 수행합니다.
1단계: 트리거
React 공식 문서에 따르면, 트리거 단계에서는 두 가지 이유로 렌더를 발생시킵니다.
- 처음 컴포넌트를 렌더해야 할 때 (초기 렌더링)
- 컴포넌트 또는 부모 컴포넌트의 상태가 업데이트될 때 (리렌더링)
컴포넌트 리렌더링 트리거 조건 4가지
위에서 언급한 조건 이외에도 컴포넌트가 리렌더링되는 몇 가지 조건이 있는데, 이들을 종합하여 4가지 조건으로 정리할 수 있습니다.
- 상태(state)가 업데이트된 경우
React는 컴포넌트의 상태(state)가 업데이트되면 컴포넌트를 리렌더링합니다. (🌐 공식 문서 참고) 단, setState를 호출하여도 이전 상태와 값이 차이나지 않는 경우(Object.is로 비교)에는 컴포넌트를 리렌더링하지 않습니다.
- 부모 컴포넌트가 리렌더링된 경우
React는 일반적으로 부모 컴포넌트가 리렌더링될 때마다 재귀적으로 자식 컴포넌트를 리렌더링합니다. (🌐 공식 문서 참고) 그 이유는 React가 컴포넌트를 렌더링(호출)하여 반환된 자식 컴포넌트를 재귀적으로 렌더링(호출)하기 때문입니다. 단,
React.memo
를 사용하여 메모이제이션(Memoization)된 컴포넌트, 그리고 props로 받은 컴포넌트를 렌더 함수에 삽입하는 경우(대표적으로 children
)에는 이 규칙에서 제외됩니다.- 컨텍스트(context)가 업데이트된 경우
컨텍스트(context)가 업데이트되면 React는 해당 컨텍스트를
useContext
훅으로 참조하는 모든 컴포넌트를 자동으로 리렌더링합니다. (🌐 공식 문서 참고)- 커스텀 훅 내부에서 상태 또는 컨텍스트가 업데이트된 경우
커스텀 훅 내부에 있는 모든 것은 커스텀 훅을 사용하는 컴포넌트에 속해 있습니다. 그래서 커스텀 훅의 상태 또는 컨텍스트가 업데이트되면, 훅을 사용하는 컴포넌트가 리렌더링됩니다.
잘못 알려진 리렌더링 트리거 조건
- 컴포넌트의 props가 변경되는 경우
기본적으로 props는 리렌더링에 영향을 주지 않습니다. 단, memo된 컴포넌트의 경우 props가 변경되어야만 리렌더링됩니다.
- 컴포넌트의 props가 없는 경우
부모 컴포넌트가 리렌더링된다면 자식 컴포넌트는 props 유무에 상관 없이 리렌더링됩니다. 단, props가 없는 memo된 컴포넌트는 리렌더링되지 않습니다.
2단계: 렌더
“Rendering” is React calling your components - react.dev
React 공식 문서에 따르면 렌더링이란 React가 컴포넌트를 호출하는 것을 의미합니다. 렌더 단계에서 React는 컴포넌트를 렌더링(호출)하여 화면에 표시할 내용을 파악합니다.
- 초기 렌더링의 경우, React는 루트 컴포넌트를 호출합니다.
- 초기 렌더링을 트리거하는 방법은 createRoot를 호출하고 초기 렌더링할 컴포넌트로 render 메서드를 호출하는 것입니다.
import Image from './Image.js'; import { createRoot } from 'react-dom/client'; const root = createRoot(document.getElementId('root'); root.render(<Image />); // 초기 렌더링 트리거!
- 리렌더링의 경우, React는 상태가 업데이트되어 렌더가 트리거된 함수 컴포넌트를 호출합니다.
- 이 때 React는 호출된 컴포넌트에서 반환된 자식 컴포넌트를 재귀적으로 호출합니다. 그리고 화면에 표시되어야 하는 내용을 모두 파악할 때까지 재귀적으로 자식 컴포넌트를 호출합니다.
- 리렌더링하는 동안 React는 이전 렌더링과 현재 렌더링 사이의 변경된 속성을 계산합니다. 그리고 DOM에 업데이트해야 할 최소한의 변경 사항을 계산합니다.
“렌더”라는 이름 때문에 오해할 수 있지만, 렌더 단계에서 화면에 컴포넌트를 표시하지 않는다는 점에 유의해주세요.
Reconciliation (재조정)
이전 렌더링 출력 결과와 비교하여 DOM에 업데이트해야 할 최소한의 변경 사항을 효율적으로 계산하는 작업을 Reconciliation(재조정)이라고 부릅니다. 과거 리액트 공식 문서(링크)에서도 사용된 용어지만 최근에는 공식 문서의 일부에서만 작게 언급됩니다. 그러나 최신 NextJS 공식 문서 등, 업계에서 아직 널리 사용되는 용어이므로 알아두면 좋습니다.
3단계: 커밋
컴포넌트를 렌더링(호출)한 후 커밋 단계에서 React는 DOM을 수정합니다.
- 초기 렌더링의 경우, React는
appendChild()
DOM API를 사용하여 렌더 단계에서 생성한 모든 DOM 노드를 화면에 표시합니다.
- 리렌더링의 경우, React는 렌더 단계에서 계산했던 결과를 바탕으로 DOM 노드를 업데이트하여 변경 사항을 화면에 반영합니다.
커밋 이후: 브라우저 렌더링
커밋 단계에서 React가 DOM을 업데이트하면 브라우저는 화면을 다시 그립니다. 이 작업을 브라우저 렌더링이라고 부릅니다.
React 컴포넌트 리렌더링 최적화
합성(Composition)을 사용하여 리렌더링 방지
⛔ 안티 패턴: 컴포넌트 안에서 컴포넌트 정의하기
이 패턴은 렌더링 될 때 마다 컴포넌트를 새로 생성하므로 성능이 떨어지는 문제와 상태가 초기화되는 문제가 발생할 수 있습니다. (🌐 공식 문서 참고)
BAD CASE
const Component = () => { // 1. 리렌더링 const SlowComponent = () => <Something />; // 2. 리렌더링 될 때 마다 함수가 새로 생성되어 컴포넌트를 새로 생성 return ( <SlowComponent /> // 3. 다시 마운트 ) }
컴포넌트는 아래와 같이 항상 최상위 스코프에서 정의해야 합니다.
GOOD CASE
const SlowComponent = () => <Something />; // 2. (다시 마운트되지 않는)동일한 컴포넌트인 상태 const Component = () => { // 1. 리렌더링 return ( <SlowComponent /> // 3. 다시 렌더링됨 ) }
✅ 상태를 자식 컴포넌트로 내리기
이 패턴은 무거운 컴포넌트가 상태를 관리하고 있고, 해당 상태가 렌더 트리의 작은 일부 영역에서만 사용되는 경우에만 유용합니다. 일반적인 예는 페이지의 대부분을 렌더링하는 복잡한 컴포넌트에서 버튼을 클릭하여 다이얼로그를 여닫는 경우입니다.
이 경우 모달 다이얼로그 표시를 제어하는 상태와 다이얼로그, 그리고 업데이트를 트리거하는 버튼을 작은 컴포넌트로 캡슐화하는 것이 좋습니다. 이렇게 하면 나머지 큰 컴포넌트는 상태가 변해도 리렌더되지 않습니다.
BAD CASE
const Component = () => { const [open, setOpen] = useState(false); // 1. 리렌더링 트리거 return ( <Something> <Button onClick={() => setOpen(true)} /> {/* 1. 리렌더링 트리거 */} {isOpen && <ModalDialog />} <VerySlowComponent /> {/* 2. 리렌더링 */} </Something> ); };
GOOD CASE
const ButtonWithDialog = () => { const [open, setOpen] = useState(false); // 1. 리렌더링 트리거 return ( <> <Button onClick={() => setOpen(true)} /> {/* 1. 리렌더링 트리거 */} {isOpen && <ModalDialog />} </> ); }; const Component = () => { return ( <Something> <ButtonWithDialog /> <VerySlowComponent /> {/* 2. 영향을 받지 않음 */} </Something> ); };
✅ children
을 props
로 받기 (상태를 children
으로 감싸기)
이 패턴은 상태를 자식 컴포넌트로 내리기 패턴과 유사하게, 상태 변경을 작은 컴포넌트로 캡슐화합니다. 앞서 살펴본 패턴과의 차이점은, 렌더 트리의 느린 부분을 감싼 요소에서 상태가 사용되기 때문에 상태를 추출하기 어렵다는 것입니다. 일반적으로 컴포넌트의 루트 원소에 붙는
onScroll
또는 onMouseMove
콜백이 예시가 될 수 있습니다.이 경우 상태 관리 및 상태를 사용하는 컴포넌트는 작은 컴포넌트로 추출하고, 느린 컴포넌트는 추출된 컴포넌트의
children
으로 전달하는 것이 좋습니다. 작은 컴포넌트 입장에서 보면 children
은 props
일 뿐이라서 상태 변화에 영향을 받지 않습니다. 따라서 children
은 리렌더링되지 않습니다. BAD CASE
const Component = () => { const [value, setValue] = useState({}); // 1. 리렌더링 트리거 return ( <div onScroll={(e) => setValue(e)}> {/* 1. 리렌더링 트리거 */ } <VerySlowComponent /> {/* 2. 리렌더 */} </div> ); };
GOOD CASE
const ComponentWithScroll = ({children}: {children: React.ReactNode}) => { const [value, setValue] = useState({}); // 1. 리렌더링 트리거 return ( <div onScroll={(e) => setValue(e) }> {/* 1. 리렌더링 트리거 */ } {children} {/* 2. props라서 리렌더링 영향 받지 않음 */} </div> ); }; const Component = () => { return ( <ComponentWithScroll> <VerySlowComponent /> {/* 3. 영향 받지 않음 */} </ComponentWithScroll> ); };
✅ 컴포넌트를 props
로 받기
이 패턴은
children
을 props
로 받기 패턴과 동일합니다. 작은 컴포넌트 내에 상태를 캡슐화하고, 무거운 컴포넌트를 props
로 받습니다. props
는 상태 변경에 영향을 받지 않기 때문에 props
로 받은 무거운 컴포넌트는 리렌더링되지 않습니다.컴포넌트를
children
으로 받을 수 없는 경우에는 별도의 props
로 받는 이 패턴이 유용합니다. BAD CASE
const Component = () => { const [value, setValue] = useState({}); // 1. 리렌더링이 트리거됨 return ( <div onScroll={(e) => setValue(e)}> {/* 1. 리렌더링이 트리거됨 */ } <SlowComponent1 /> {/* 2. 리렌더링 */} <Something /> {/* 2. 리렌더링 */} <SlowComponent2 /> {/* 2. 리렌더링 */} </div> ); };
GOOD CASE
const ComponentWithScroll = ({left, right}: { left: React.ReactNode, right: React.ReactNode }) => { const [value, setValue] = useState({}); // 1. 리렌더링이 트리거됨 return ( <div onScroll={(e) => setValue(e) }> {/* 1. 리렌더링이 트리거됨 */ } {left} {/* 2. props이므로 리렌더링되지 않음 */} <Something /> {right} {/* 2. props이므로 리렌더링되지 않음 */} </div> ); }; const Component = () => { return ( <ComponentWithScroll> left={<SlowComponent1 />} {/* 2. 리렌더링의 영향을 받지 않음 */} right={<SlowComponent2 />} {/* 2. 리렌더링의 영향을 받지 않음 */} /> ); };
React.memo
를 사용하여 리렌더링 방지
React.memo
를 사용하면 컴포넌트의 props
가 변경되지 않은 경우 리렌더링을 건너뛸 수 있습니다. React.memo
는 상태나 변경된 데이터에 의존하지 않는 무거운 컴포넌트를 렌더링할 경우에 유용합니다. BAD CASE
const Parent = () => { // 1. 리렌더 return <Child />; // 2. 1에 의해 리렌더 };
GOOD CASE
const MemoizedChild = React.memo(Child); const Parent = () => { // 1. 리렌더 return <MemoizedChild />; // 2. 영향 받지 않음 }
✅ 원시 자료형이 아닌 타입을 props
로 받는 경우
원시 자료형이 아닌
props
(예: Object
)는 memo된 컴포넌트에 리렌더링을 발생시킵니다. 이를 방지하기 위해서 useMemo
또는 useCallback
으로 해당 props
를 memo해야 합니다. BAD CASE
const MemoizedChild = React.memo(Child); const Parent = () => { // 1. 리렌더 (Parent의 부모 컴포넌트에 의해) return ( <MemoizedChild value={{value}} // 2. 1이 리렌더 될 때 마다 객체가 새로 생성되므로, 리렌더됨. /> ) }
GOOD CASE
const MemoizedChild = React.memo(Child); const Parent = () => { // 1. 리렌더 (Parent의 부모 컴포넌트에 의해) const cachedValue = useMemo(() => ({value}), []); // 2. 1이 리렌더 되어도 값 유지 return ( <MemoizedChild value={cachedValue} // 3. 리렌더되지 않음 /> ) }
✅ 컴포넌트 또는 children
을 props
로 받는 경우
props
로 넘겨주는 컴포넌트를 전부 React.memo
로 메모해야 리렌더링이 발생하지 않습니다. BAD CASE
const MemoizedChild = React.memo(Child); const Parent = () => { // 1. 리렌더 (Parent의 부모 컴포넌트에 의해) return ( <MemoizedChild left={<Something />}> // 2. Something으로 인한 리렌더 <GrandChild /> // 2. 1에 의한 리렌더 </MemoizedChild> ) }
GOOD CASE
const MemoizedSomething = React.memo(Something); const MemoizedGrandChild = React.memo(GrandChild); cosnt Parent = () => { // 1. 리렌더 (Parent의 부모 컴포넌트에 의해) return ( <Child left={<MemoizedSomething />} // 2. 리렌더되지 않음 <MemoizedGrandChild /> // 2. 리렌더되지 않음 </Child> ) }
useMemo
/useCallback
을 사용하여 리렌더링 성능 향상
useMemo
의 개념 및 사용법에 대해서는 React 공식 문서의 useMemo를, useCallback
의 개념 및 사용법에 대해서는 React 공식 문서의 useCallback을 참고해주세요.⛔ 안티 패턴: props
에 대한 불필요한 useMemo
/useCallback
일반적으로 부모 컴포넌트가 리렌더링되면 자식 컴포넌트는 props의 업데이트 여부와 관계 없이 리렌더링됩니다. memo된 자식 컴포넌트에 넘겨주는
props
값이 아니라면 useMemo
또는 useCallback
을 사용할 필요가 없습니다.const Perent = () => { // 1. 리렌더 const cachedValue = useMemo(() => ({value}) return <Child value={cachedValue} /> // 2. 리렌더 (value 값 변경 여부와 관계 없이) }
✅ useMemo
/useCallback
이 필수인 경우
만약 자식 컴포넌트가
React.memo
로 감싸진 경우, 모든 비 원시타입인 props
값들은 memo되어야 합니다. - Object의 경우:
useMemo
- 콜백 함수의 경우:
useCallback
const MemoizedChild = React.memo(Child) const Parent = () => { // 1. 리렌더 const cachedValue = useMemo(() => ({ value })); return <MemoizedChild value={cachedValue} /> // 2. 리렌더되지 않음 }
또한 만약 컴포넌트가
useEffect
, useMemo
, useCallback
등의 훅에서 비 원시타입 값(Object, function, 등…)을 의존성으로 사용하고 있다면 이 값은 memo되어야 합니다.const Parent = () => { const cachedValue = useMemo(() => ({value})) useEffect(() => { // do something }, [cachedValue]); return ... }
✅ 무거운 계산 작업을 최적화하기 위한 useMemo
useMemo
는 React가 컴포넌트를 렌더링할 때 마다 무거운 작업을 수행하는 것을 피하기 위해 사용됩니다. 다만 useMemo
를 사용하면 메모리를 추가로 소비하고 초기 렌더링을 조금 느리게 만듭니다. 그래서 모든 계산에 useMemo
를 사용할 필요는 없습니다.React에서 이루어지는 대부분의 무거운 작업은 컴포넌트를 마운팅하고 업데이트하는 것입니다. 일반적으로, 존재하는 렌더트리의 일부 또는 생성된 렌더트리의 결과물은 항상 새 엘리먼트를 반환합니다.
useMemo
를 사용하여 React 엘리먼트를 메모하면 새 엘리먼트가 생성되는 것을 방지할 수 있습니다.한편 순수 자바스크립트 코드에서 수행되는 일반적인 작업(정렬, 필터링, 등…)은 컴포넌트를 업데이트하는 작업에 비해서는 일반적으로 무시하도 좋을 수준으로 가벼운 작업입니다. 그래서 굳이 일반적인 자바스크립트 코드에 대해서
useMemo
를 사용할 필요는 없습니다. BAD CASE
const Component = () => { // 1. 리렌더 return ( <> <Something /> <SlowComponent /> // 2. 리렌더 <SomethingElse /> </> ) }
GOOD CASE
const Component = () => { // 1. 리렌더 const slowComponent = useMemo(() => { return <SlowComponent /> // 2. 리렌더되지 않음 }, []) return ( <> <Somthing /> {slowComponent} // 2. 리렌더되지 않음 <SomethingElse /> </> ) }
리스트 리렌더링 성능 향상
React는 JSX 엘리먼트 배열을 렌더링할 수 있습니다. 그래서 배열의 원소를 특정 컴포넌트로 표시해야 할 일이 있다면,
map
함수를 사용하여 JSX 엘리먼트 배열을 만들게 됩니다. 이렇게 JSX 엘리먼트 배열을 렌더링하는 것을 리스트 렌더링(Rendering List)이라고 부릅니다. (🌐 공식 문서 참고)리스트 렌더링 시에 주의해야 할 점은 2가지가 있습니다.
- 배열로 반환되는 컴포넌트의
key
속성(attribute)으로 고유한string
값을 지정합니다.
- 리렌더링을 방지하기 위해
React.memo
를 사용합니다.
BAD CASE
const Component = () => { // 1. 리렌더 return ( <> {items.map((item) => ( <Child item={item} key={item.id} /> // 2. 리렌더, key는 리렌더에 영향을 주지 않는다. ))} </> ) }
GOOD CASE
const MemoizedChild = React.memo(Child) const Component = () => { // 1. 리렌더 return ( <> {items.map((item) => ( <MemoizedChild item={item} key={item.id} /> // 2. 리렌더되지 않음 (item이 변경되지 않는다고 가정) ))} </> ) )
⛔ 안티 패턴: 리스트에서 key
에 랜덤 값을 사용하기
리스트의 컴포넌트 속성(attribute)로
key
의 값을 랜덤 값으로 사용해서는 안됩니다. (🌐 공식 문서 참고) 상태 또는 관리되지 않는 요소(input 등)에 버그가 발생할 수 있습니다. const ChildMemo = React.memo(Child) const Component = () => { // 1. 리렌더 return ( <> {items.map((item) => ( <ChildMemo item={item} key={Math.random()} /> // 2. key 값이 랜덤이라 다시 마운트됨! ))} </> ) )
⚠️ 주의: 배열의 인덱스 값 리스트 원소의 key 값으로 사용하는 경우
- 배열이 업데이트되지 않는 경우, 배열의 인덱스를 컴포넌트의
key
속성 값으로 지정해도 괜찮습니다.
- 하지만 배열에 데이터가 삽입되거나 정렬되는 경우, 원소의 데이터에 인덱스가 변경될 수 있으므로 잘 변경되지 않는 고유한 id 값을 사용하는 것이 좋습니다.
- [’B’, ‘C’, ‘A’]라는 배열에서 ‘C’의 인덱스는 1입니다. 배열을 오름차순으로 정렬하면 [’A’, ‘B’, ‘C’]이므로 ‘C’의 인덱스는 2로 바뀌게 됩니다.
- 이때 인덱스를 key 값으로 사용하면 컴포넌트가 상태를 가지고 있거나
<input />
같은 제어되지 않는 원소가 있을 때 버그가 발생할 수 있습니다. - 또한
React.memo
로 래핑된 엘리먼트가 있으면 성능이 떨어지는 문제가 있습니다.
컨텍스트에 의한 리렌더링 방지
✅ 컨텍스트 값을 memo하기
- Context Provider가 앱의 root 부근에 있지 않고,
- 부모에 의해 리렌더링될 수 있는 가능성이 있다면,
Context 값을 memo하여 리렌더링을 최적화할 수 있습니다. (🌐 공식 문서 참고)
BAD CASE
const Component = () => { return ( <Context.Provider value={{value}}> {children} </Context.Provider> ) }
GOOD CASE
cosnt Component = () => { const memoValue = useMemo(() => ({value}), []); return ( <Context.Provider value={memoValue}> {children} </Context.Provider> ) }
✅ 데이터와 API를 분리하기
컨텍스트가 데이터와 API (getter, setter)의 조합이라면, 데이터와 API를 서로 다른 Provider로 분리할 수 있습니다. 이렇게 하면 API를 사용하고 있는 컴포넌트는 데이터가 변경되더라도 리렌더링되지 않습니다. (🌐 공식 문서 참고)
BAD CASE
const Component = () => { const [state, setState] = useState(); // 1. 상태 변경 const value = useMemo( () => { data: state, api: (data) => setState(data), }), [state] ); return ( <Context.Provider value={value}> // 2. 모든 provider consumer 리렌더 {children} </Context.Provider> ) }
GOOD CASE
const Component = ({children}) => { const [state, setState] = useState(); // 1. 상태 변경 return ( <DataContext.Provider value={state}> <ApiContext.Provider value={setState}> // 2. api provider consumer만 리렌더 {children} </ApiContext.Provider> </DataContext.Provider> ); }
이 패턴을 사용하여 React에서 Form 리렌더링을 최적화하는 예제는 Nadia Makarevich의 How to write performant React apps with Context 문서를 참고해주세요.
✅ 데이터를 청크로 분할
컨텍스트의 독립된 몇몇 데이터 청크들을 관리한다면, 이 청크들을 작은 provider로 분리할 수 있습니다. 이렇게 하면 일부 청크를 제공하는 provider의 consumer 컴포넌트만 리렌더됩니다.
BAD CASE
const Component = () => { const [first, setFirst] = useState(); // first 상태 변경 const [second, setSecond] = useState(); const value = useMemo( () => {({ first: first, second: second, }), [first, second], ); return ( <Context.Provider value={value}> // 2. 모든 consumer 리렌더 {children} </Context.Provider> ) }
GOOD CASE
const Component = () => { const [first, setFirst] = useState(); // first 상태 변경 const [second, setSecond] = useState(); return ( <Data1Context.Provider value={first}> <Data2Context.Provider value={second}> // 2. second 상태 consumer는 리렌더링되지 않음 {children} </Data2Context.Provider> </Data2Context.Provider> ) }
✅ Context Selector
컨텍스트 값의 일부분을 사용하는 컴포넌트는 리렌더링을 방지할 수 없습니다.
useMemo
훅을 사용해서 사용하는 값이 변경되지 않더라도 리렌더링이 트리거됩니다.하지만 Context Selector는 고차 컴포넌트 패턴(Higher-Order Component(HOC), 컴포넌트를 props로 받아서 새 컴포넌트를 반환하는 함수)와
React.memo
를 사용하여 리렌더링이 트리거되지 않도록 만들 수 있습니다.React 공식 문서에 따르면 고차 컴포넌트는 현대 React 코드에서는 일반적으로 사용되지 않는다고 하므로, 일부 특수한 경우에만 사용하는 것이 좋겠습니다.
BAD CASE
const useSomething = () => { const { something } = useContext(Context); // 1. something이 변하지 않아도 리렌더 트리거 발생. return useMemo(() => something, [something]); // 2. 리렌더 방지에 도움 안됨 }; const Component = () => { const { something } = useSomething(); // 3. something이 변하지 않아도 리렌더 발생. return ... }
- Context가 업데이트된다.
- React는 somthing 값이 변경되었는지 여부와 관계 없이 useContext가 포함된 커스텀 훅을 사용중인 모든 컴포넌트를 리렌더링한다.
GOOD CASE
const withSomething = (Component) => { // 1. 컴포넌트가 메모됨. const MemoizedComponent = React.memo(Component); return () => { const { something } = useSomething(); return <MemoizedComponent something={something} /> // 2. something이 변할 때만 리렌더됨 } } const Component = withSomething(({something}) => { // 3. something이 변할 때만 리렌더됨 return ... });
- Context가 업데이트된다.
- memo된 컴포넌트는 props가 변할 때만 리렌더링된다.
- 따라서 props로 받는 memo된 somthing이 변하지 않으면 memo된 컴포넌트는 리렌더링되지 않는다.
Context selector에 대한 자세한 내용은 Nadia Makarevich의 High-Order Components in React Hooks era 문서를 참고해주세요.
이미 잘 만들어진 Context Selector를 사용하고 싶다면, npm에 올라온 use-context-selector 패키지를 참고해주세요.