절대 좋은 방식이 아님을 먼저 말씀드립니다.
말그대로 급할 때 사용할 수 있는 야매일 뿐임을 말씀드립니다.
React로 개발을 하다 보면 상태 관리와 리렌더링 최적화는 항상 큰 도전입니다. 특히 zustand와 같은 상태 관리 라이브러리를 사용할 때, 상태 변경으로 인해 불필요한 리렌더링이 발생할 수 있습니다. 이런 상황에서 memoization을 활용해 성능 최적화를 할 수 있지만, 때로는 마땅히 로직을 넣을 컴포넌트가 없어 곤란할 때가 있습니다. 이 글에서는 빈 JSX를 이용해 memoization을 적용하는 "야매" 방법을 소개하겠습니다.
아래 예제 코드를 보겠습니다. SnackbarProvider 컴포넌트는 snackbarState를 구독하고, ThirdChild와 Child 컴포넌트 모두 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;
위 코드에서 useLogic은 ThirdChild 컴포넌트에 포함되어 있어 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 컴포넌트만 렌더링되고 나머지는 리렌더링이 사라지게 되었습니다
