React 리렌더링 최적화

2025. 4. 23. 21:53JavaScript

반응형

React 렌더링

React 공식 문서를 확인해보면 트리거 단계에서는 두 가지 이유로 렌더를 발생시킵니다.

  1. 처음 컴포넌트를 렌더해야할 때
  2. 컴포넌트 또는 부모 컴포넌트의 상태가 업데이트 될 때(리렌더링)

컴포넌트 리렌더링 트리거 조건

  1. state가 업데이트 된 경우
  2. 부모 컴포넌트가 리렌더링 된 경우
  3. context가 업데이트된 경우

참조: https://blog.teamelysium.kr/react-rerendering-optimization#85d7fe53c34946599068bace7ccedbc3

React 리렌더링 최적화

합성(Composition)을 사용하여 리렌더링 방지

상태를 자식 컴포넌트로 내리기

이 패턴은 무거운 컴포넌트가 상태를 관리하고 있고, 해당 상태가 렌더 트리의 작은 일부 영역에서만 사용되는 경우에만 유용합니다.
예를들어 페이지의 대부분을 렌더링하는 복잡한 컴포넌트에서 버튼을 클릭하여 다이얼로그를 여닫는 경우입니다.

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로 받기

이 패턴은 상태 변경을 작은 컴포넌트로 캡술화합니다. 이 패턴은 렌더 트리의 느린 부분을 감싼 요소에서 상태가 사용되기 때문에 상태를 추출하기 어렵다는 것입니다.

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>
  );
};

React.memo를 사용하여 리렌더링 방지

원시 자료형이 아닌 타입을 props로 받는 경우

원시 자료형이 아닌 props는 memo된 컴포넌트에 리렌더링을 발생시킵니다. 이를 방지하기 위해서 useMemo 혹은 useCallback으로 해당 props를 memo 해야합니다.

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로 메모해야 리렌더링이 발생하지 않습니다.

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/useCallback이 필수인 경우

만약 자식 컴포넌트가 React.memo로 감싸진 경우, 모든 비 원시타입인 props들은 memo되어야 합니다.
(Object는 useMemo / 콜백함수는 useCallback)

무거운 계산 작업을 최적화하기 위한 useMemo

useMemo는 React가 컴포넌트를 렌더링할 때 마다 무거운 작업을 수행하는 것을 피가히 위해 사용됩니다. 다만 useMemo를 사용하면 메모리를 추가로 소비하고 초기 렌더링을 조금 느리게 만듭니다. React에서 이루어지는 대부분의 무거운 작업은 컴포넌트를 마운팅하고 업데이트하는 것입니다. 일반적으로 존재하는 일부 또는 생성된 렌더트리의 결과물은 항상 새 엘리먼트를 반환합니다. useMemo를 사용하여 React 엘리먼트를 메모하면 새 엘리먼트가 생성되는 것을 방지할 수 있습니다.

const Component = () => { // 1. 리렌더
    const slowComponent = useMemo(() => {
        return <SlowComponent /> // 2. 리렌더되지 않음
    }, [])

    return (
        <>
            <Somthing />
            {slowComponent} // 2. 리렌더되지 않음
            <SomethingElse />
        </>
    )
}

리스트 리렌더링 성능 향상

리스트에서 key 값 지정

리스트의 컴포넌트 속성으로 key의 값을 랜덤 값으로 사용해서는 안 됩니다. 상태 또는 관리되지 않는 요소에 버그가 발생할 수 있습니다.

배열의 인덱스 값을 리스트 원소의 key 값으로 사용하는 것도 지양합니다. 배열이 업데이트 되지 않는 경우에는 괜찮지만, 데이터가 삽입되거나 정렬되는 경우 원소 데이터에 인덱스가 변경될 수 있으므로 잘 변경되지 않는 고유한 id 값을 사용하는 것이 좋습니다.

컨텍스트에 의한 리렌더링 방지

컨텍스트 값을 메모하기

만약 ContextProvider가 앱의 root 부근에 있지 않고, 부모에 의해 리렌더링될 수 있는 가능성이 있다면 Context 값을 memo 하여 리렌더링을 최적화할 수 있습니다.

cosnt Component = () => {
    const memoValue = useMemo(() => ({value}), []);

    return (
        <Context.Provider value={memoValue}>
            {children}
        </Context.Provider>
    )
}

데이터와 API를 분리하기

컨텍스트가 데이터와 API(getter, setter)의 조합이라면 데이터와 API를 서로 다른 Provider로 분리할 수 있습니다. 이렇게하면 API를 사용하고 있는 컴포넌트는 데이터가 변경되더라도 리렌더링되지 않습니다.

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>
    );
}

데이터를 청크로 분할

컨텍스트의 독립된 몇몇 데이터 청크들을 관리한다면, 이 청크들은 작은 provider로 분리할 수 있ㅅ습니다. 이렇게 하면 일부 청크를 제공하는 provider의 consumer 컴포넌트만 리렌더링 됩니다.

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>
    )
}
반응형

'JavaScript' 카테고리의 다른 글

React Hook  (0) 2025.02.26
Vite  (0) 2025.01.11
Node.js  (0) 2023.07.26
Next Js  (0) 2022.12.29
asnyc & await  (0) 2022.04.20