리액트 렌더링 최적화가 진짜 진짜 급할 때 쓰는 야매방법

kiwon kim·2024년 6월 6일

Frontend

목록 보기
3/30

절대 좋은 방식이 아님을 먼저 말씀드립니다.

말그대로 급할 때 사용할 수 있는 야매일 뿐임을 말씀드립니다.

서론

React로 개발을 하다 보면 상태 관리와 리렌더링 최적화는 항상 큰 도전입니다. 특히 zustand와 같은 상태 관리 라이브러리를 사용할 때, 상태 변경으로 인해 불필요한 리렌더링이 발생할 수 있습니다. 이런 상황에서 memoization을 활용해 성능 최적화를 할 수 있지만, 때로는 마땅히 로직을 넣을 컴포넌트가 없어 곤란할 때가 있습니다. 이 글에서는 빈 JSX를 이용해 memoization을 적용하는 "야매" 방법을 소개하겠습니다.

문제 상황

아래 예제 코드를 보겠습니다. SnackbarProvider 컴포넌트는 snackbarState를 구독하고, ThirdChildChild 컴포넌트 모두 snackbarState를 사용합니다. 이로 인해 상태가 변경될 때마다 두 컴포넌트 모두 리렌더링이 됩니다.

ModalProvider, "Child"버튼의 컨테이너와 버튼, 그리고 각 Close 버튼을 담은 컨테이너들 모두 렌더링 되고 있습니다.

// snackbarStore.tsx

import { ReactNode, useCallback, useEffect, memo } from "react";
import { create } from "zustand";
import { devtools } from "zustand/middleware";

interface Snackbar {
  id: string;
  children: ReactNode;
}

interface SnackbarState {
  snackbars: Snackbar[];
  addSnackbar: (snackbar: Snackbar) => void;
  removeSnackbar: (id: string) => void;
}

const useSnackbarStore = create<SnackbarState>()(
  devtools((set) => ({
    snackbars: [],
    addSnackbar: (snackbar) =>
      set((state) => ({
        snackbars: [
          ...state.snackbars,
          { ...snackbar, id: `${Math.random()}` },
        ],
      })),
    removeSnackbar: (id) =>
      set((state) => ({
        snackbars: state.snackbars.filter((snackbar) => snackbar.id !== id),
      })),
  }))
);

export const SnackbarProvider: React.FC<{ children: ReactNode }> = ({
  children,
}) => {
  const snackbarStore = useSnackbarStore();

  return (
    <div>
      {children}
      {snackbarStore.snackbars.map((snackbar) => (
        <div key={snackbar.id}>
          {snackbar.children}
          <button onClick={() => snackbarStore.removeSnackbar(snackbar.id)}>
            Close
          </button>
        </div>
      ))}
    </div>
  );
};

// App.tsx
const App: React.FC = () => {
  return (
    <SnackbarProvider>
      <ThirdChild />
    </SnackbarProvider>
  );
};

const useLogic = () => {
  const snackbar = useSnackbarStore();
  const onOpenSnackbar = useCallback(() => {
    snackbar.addSnackbar({
      id: `${Math.random()}`,
      children: "jfdisoajdi",
    });
  }, [snackbar]);

  useEffect(() => {
    const interval = setInterval(() => {
      onOpenSnackbar();
    }, 3000);

    return () => clearInterval(interval);
  }, [onOpenSnackbar]);
};

const MemoLogic = memo(() => {
  return <></>;
});

const Child = () => {
  const snackbar = useSnackbarStore();
  const onOpenSnackbar = useCallback(() => {
    snackbar.addSnackbar({
      children: "jfdisoajdi",
      id: `${Math.random()}`,
    });
  }, [snackbar]);

  return <button onClick={onOpenSnackbar}>Child</button>;
};

const ThirdChild: React.FC = () => {
  useLogic();
  return (
    <div>
      <MemoLogic />
      <Child />
    </div>
  );
};

export default App;

해결책: 빈 JSX를 이용한 memoization

위 코드에서 useLogicThirdChild 컴포넌트에 포함되어 있어 snackbarState가 변경될 때마다 ThirdChild도 리렌더링됩니다. 이를 방지하기 위해 useLogic을 별도의 memoized 컴포넌트로 분리할 수 있습니다. 하지만 별도의 컴포넌트가 필요하지 않은 경우, 빈 JSX (<></>)를 이용해 memoization을 적용할 수 있습니다.

// 해결책: 빈 JSX를 이용한 memoization
const MemoLogic = memo(() => {
  useLogic();
  return <></>;
});

리팩토링

아래 코드는 useLogic을 빈 JSX에 담은 memoized 컴포넌트로 분리한 예제입니다.

// snackbarStore.tsx

import { ReactNode, useCallback, useEffect, memo } from "react";
import { create } from "zustand";
import { devtools } from "zustand/middleware";

interface Snackbar {
  id: string;
  children: ReactNode;
}

interface SnackbarState {
  snackbars: Snackbar[];
  addSnackbar: (snackbar: Snackbar) => void;
  removeSnackbar: (id: string) => void;
}

const useSnackbarStore = create<SnackbarState>()(
  devtools((set) => ({
    snackbars: [],
    addSnackbar: (snackbar) =>
      set((state) => ({
        snackbars: [
          ...state.snackbars,
          { ...snackbar, id: `${Math.random()}` },
        ],
      })),
    removeSnackbar: (id) =>
      set((state) => ({
        snackbars: state.snackbars.filter((snackbar) => snackbar.id !== id),
      })),
  }))
);

export const SnackbarProvider: React.FC<{ children: ReactNode }> = ({
  children,
}) => {
  const snackbarStore = useSnackbarStore();

  return (
    <div>
      {children}
      {snackbarStore.snackbars.map((snackbar) => (
        <div key={snackbar.id}>
          {snackbar.children}
          <button onClick={() => snackbarStore.removeSnackbar(snackbar.id)}>
            Close
          </button>
        </div>
      ))}
    </div>
  );
};

// App.tsx
const App: React.FC = () => {
  return (
    <SnackbarProvider>
      <ThirdChild />
    </SnackbarProvider>
  );
};

const useLogic = () => {
  const snackbar = useSnackbarStore();
  const onOpenSnackbar = useCallback(() => {
    snackbar.addSnackbar({
      id: `${Math.random()}`,
      children: "jfdisoajdi",
    });
  }, [snackbar]);

  useEffect(() => {
    const interval = setInterval(() => {
      onOpenSnackbar();
    }, 3000);

    return () => clearInterval(interval);
  }, [onOpenSnackbar]);
};

// 해결책: 빈 JSX를 이용한 memoization
const MemoLogic = memo(() => {
  useLogic();
  return <></>;
});

const Child = () => {
  const snackbar = useSnackbarStore();
  const onOpenSnackbar = useCallback(() => {
    snackbar.addSnackbar({
      children: "jfdisoajdi",
      id: `${Math.random()}`,
    });
  }, [snackbar]);

  return <button onClick={onOpenSnackbar}>Child</button>;
};

const ThirdChild: React.FC = () => {
  return (
    <div>
      <MemoLogic />
      <Child />
    </div>
  );
};

export default App;

결론

빈 JSX를 이용한 memoization은 상태 관리와 성능 최적화를 동시에 해결할 수 있는 유용한 방법입니다. 이 방법은 특히 상태 변경이 잦고, 특정 로직을 별도의 컴포넌트로 분리하기 애매한 경우에 유용합니다. 이 글에서 소개한 "야매" 방법이 여러분의 React 프로젝트에서 리렌더링 최적화에 도움이 되길 바랍니다.

이제 ModalProvider div 부분, zustand인 snackbarState를 구독하고 있는 Child 컴포넌트만 렌더링되고 나머지는 리렌더링이 사라지게 되었습니다

profile
FOR_THE_BEST_DEVELOPER

0개의 댓글