이 글에서 정리하는 내용
Zustand를 사용할 때 리렌더링을 판단하는 기준을 React 렌더링 흐름과 연결해서 정리합니다. store 전체가 바뀌었는지가 아니라, 컴포넌트가 어떤 값을 선택해서 구독하고 있는지가 핵심입니다.
- React 리렌더링부터 확인해야 하는 이유
- Zustand에서 상태를 읽는다는 의미
- store 전체를 가져오면 생기는 일
- selector로 구독 범위 좁히기
- 여러 값을 묶어 가져올 때 다시 헷갈리는 지점
- Zustand 리렌더링을 볼 때 남길 기준
React 리렌더링부터 확인해야 하는 이유

Zustand 리렌더링을 이해하려고 할 때 바로 store부터 보면 오히려 기준이 흐려집니다. 전역 상태 라이브러리를 쓰면 “store 값이 바뀌면 다 다시 그려지는 것 아닌가?”라는 생각이 먼저 들기 때문입니다. 하지만 React 화면에서 먼저 봐야 할 것은 store의 크기가 아니라 컴포넌트가 어떤 상태 변경에 반응하도록 연결되어 있는지입니다.
React에서 리렌더링은 브라우저 화면 전체를 새로 만드는 과정과 같지 않습니다. 상태 변경이 발생하면 React는 컴포넌트를 다시 호출해서 다음 UI 모양을 계산합니다. 이후 이전 결과와 비교한 뒤 실제 DOM에 반영할 차이가 있을 때 필요한 변경만 commit 단계에서 처리합니다.
그래서 개발 중에 가 다시 찍힌다고 해서 실제 DOM이 전부 새로 만들어졌다고 판단하면 안 됩니다. 컴포넌트 함수가 다시 실행되는 것, React가 다음 UI를 계산하는 것, 브라우저 DOM이 실제로 바뀌는 것은 서로 다른 단계입니다.
이 구분이 없으면 Zustand를 사용할 때 리렌더링 로그만 보고 문제를 과하게 해석하기 쉽습니다. 로그가 찍힌 이유가 selector 범위 때문인지, 부모 컴포넌트 렌더링 때문인지, 개발 환경의 Strict Mode 때문인지 나눠서 봐야 합니다. 최적화보다 먼저 필요한 작업은 렌더링이 발생한 이유를 좁히는 것입니다.
Zustand에서 상태를 읽는다는 의미
Zustand에서 를 쓰면 store의 값을 단순히 꺼내오는 것처럼 보입니다. 하지만 React 컴포넌트 안에서 사용한다면 단순 조회보다 구독에 가깝습니다. 컴포넌트가 특정 상태를 사용하고, 그 값이 바뀌었을 때 다시 렌더링될 수 있는 연결을 만드는 것입니다.
예를 들어 관리자 화면에 카운트, 사용자 이름, 테마, 모달 열림 상태가 모두 들어 있는 store가 있다고 가정해보겠습니다. 화면은 여러 컴포넌트로 나뉘어 있지만, 각 컴포넌트가 실제로 필요한 값은 보통 하나나 두 개 정도입니다. 카운트를 보여주는 영역이 사용자 이름 변경까지 알 필요는 없고, 테마 버튼이 모달 열림 상태까지 구독할 이유도 없습니다.
import { create } from "zustand"; type DashboardState = { count: number; username: string; theme: "light" | "dark"; isModalOpen: boolean; increase: () => void; changeName: (name: string) => void; toggleTheme: () => void; openModal: () => void;
}; export const useDashboardStore = create<DashboardState>((set) => ({ count: 0, username: "guest", theme: "light", isModalOpen: false, increase: () => set((state) => ({ count: state.count + 1 })), changeName: (name) => set({ username: name }), toggleTheme: () => set((state) => ({ theme: state.theme === "light" ? "dark" : "light"})), openModal: () => set({ isModalOpen: true })})); 이 store 자체가 문제는 아닙니다. 하나의 store 안에 여러 상태가 모여 있어도, 컴포넌트가 필요한 조각만 읽는다면 구조를 충분히 관리할 수 있습니다. 문제는 컴포넌트가 화면에 쓰지 않는 값까지 함께 가져오는 순간부터 생깁니다.
처음에는 store 전체를 꺼내는 코드가 편합니다. 자동완성도 잘 되고, 필요한 값이 생길 때마다 같은 변수에서 꺼내면 되기 때문입니다. 하지만 편한 코드가 항상 좋은 구독 범위를 만들지는 않습니다. 리렌더링을 확인할 때는 “이 컴포넌트가 store에서 무엇을 가져왔는가”보다 “무엇의 변화에 반응하도록 연결됐는가”를 봐야 합니다.
store 전체를 가져오면 생기는 일
아래 컴포넌트는 화면에 만 보여주면 됩니다. 그런데 를 인자 없이 호출해서 store 전체를 가져오고 있습니다.
function CountView() { const store = useDashboardStore(); console.log("CountView render"); return ( <section> <p>현재 카운트: {store.count}</p> <button type="button" onClick={store.increase}> 증가 </button> </section> );
} 겉으로 보면 문제 없어 보입니다. 도 있고 도 있으니 기능은 정상 동작합니다. 하지만 이 컴포넌트는 만 필요한데도 store 전체를 바라보는 코드가 됐습니다.
이 상태에서 을 바꾸거나 을 바꾸는 동작이 생기면 어떻게 될까요. 가 실제 화면에서 사용하는 값은 뿐인데, store 전체를 가져온 구조 때문에 관련 없는 상태 변경에도 다시 실행될 수 있습니다. 화면 결과는 그대로일 수 있지만 렌더링 로그는 섞여 보입니다.
작은 예제에서는 이 차이가 크게 느껴지지 않습니다. 버튼 하나, 텍스트 하나만 있는 화면에서는 렌더링 로그가 조금 더 찍혀도 체감이 없습니다. 하지만 리스트가 길거나, 카드 컴포넌트가 많이 반복되거나, 모달과 필터가 한 화면에 같이 있는 관리자 UI에서는 어디서 다시 렌더링이 발생하는지 확인하기 어려워집니다.
이 지점에서 “Zustand가 느린가?”라고 보기 전에 먼저 확인할 부분이 있습니다. store가 큰 것이 문제인지, 컴포넌트가 store를 너무 크게 구독하고 있는지가 먼저입니다. 대부분의 경우 시작점은 라이브러리 교체가 아니라 selector 범위를 줄이는 작업입니다.
selector로 구독 범위 좁히기
Zustand selector는 store에서 필요한 값만 고르는 함수입니다. 문법만 보면 값을 꺼내는 콜백처럼 보이지만, 리렌더링 관점에서는 컴포넌트의 관심 범위를 좁히는 역할을 합니다.
function CountView() { const count = useDashboardStore((state) => state.count); const increase = useDashboardStore((state) => state.increase); console.log("CountView render"); return ( <section> <p>현재 카운트: {count}</p> <button type="button" onClick={increase}> 증가 </button> </section> );
} 이제 는 와 만 선택합니다. 이 바뀌어도 이 컴포넌트가 선택한 값은 바뀌지 않습니다. 그래서 화면이 어떤 상태에 관심이 있는지 코드에서 바로 드러납니다.
같은 store를 쓰더라도 다른 컴포넌트는 전혀 다른 값을 선택할 수 있습니다. 전역 상태라는 말 때문에 모든 컴포넌트가 같은 덩어리를 바라봐야 하는 것은 아닙니다.
function UserNameView() { const username = useDashboardStore((state) => state.username); const changeName = useDashboardStore((state) => state.changeName); console.log("UserNameView render"); return ( <label> 사용자 이름 <input value={username} onChange={(event) => changeName(event.target.value)} /> </label> );
} 이 컴포넌트는 에 관심이 있습니다. 카운트가 증가해도 사용자 이름을 표시하는 컴포넌트가 다시 렌더링될 필요는 없습니다. 반대로 이름을 입력할 때마다 카운트만 보여주는 컴포넌트가 다시 실행될 이유도 없습니다.
selector를 사용한다는 것은 단순히 코드를 짧게 쓰는 문제가 아닙니다. 컴포넌트가 어떤 상태 변경에 반응해야 하는지 명시하는 방식입니다. Zustand 리렌더링을 볼 때는 이 기준이 가장 먼저 와야 합니다.
렌더링 로그로 확인할 때 볼 부분
학습할 때는 각 컴포넌트 안에 를 찍어보면 차이가 빠르게 보입니다. 버튼을 눌렀을 때 만 다시 찍히는지이름 입력 시 만 다시 찍히는지 보는 식입니다.
다만 로그가 한 번 더 찍힌다고 해서 바로 성능 문제라고 판단하면 안 됩니다. 개발 환경에서는 React Strict Mode 때문에 렌더링이 한 번 더 호출되어 보일 수 있고, 부모 컴포넌트 렌더링 구조 때문에 자식 컴포넌트가 함께 실행되는 경우도 있습니다. 먼저 selector 범위가 맞는지 확인하고, 그 다음 실제 성능 문제가 있는지 봐야 합니다.
확인 순서는 단순하게 잡는 편이 낫습니다. 먼저 전체 store 구독 코드가 있는지 찾고, 다음으로 selector가 실제 화면에서 쓰는 값과 맞는지 봅니다. 그 다음에도 불필요한 렌더링이 남아 있다면 부모 컴포넌트 구조, memo 처리, 여러 값을 묶어 반환하는 selector를 확인합니다.
여러 값을 묶어 가져올 때 다시 헷갈리는 지점

실제 컴포넌트에서는 값 하나만 필요한 경우보다 여러 값이 필요한 경우가 더 많습니다. 그래서 아래처럼 객체로 묶어 반환하는 코드를 자주 떠올리게 됩니다.
function HeaderStatus() { const { username, theme } = useDashboardStore((state) => ({ username: state.username, theme: state.theme})); return ( <header> <span>{username}</span> <span>{theme}</span> </header> );
} 이 코드는 보기에는 자연스럽습니다. 컴포넌트에서 필요한 값만 골라서 객체로 반환하고, 구조 분해로 꺼내 쓰기 때문입니다. 하지만 여기서 다음 헷갈림이 생깁니다. selector가 실행될 때마다 새 객체를 만들어 반환하면이전 결과와 비교하는 방식에 따라 리렌더링이 예상보다 자주 발생할 수 있습니다.
값만 놓고 보면 과 이 그대로인 것처럼 보여도, 형태의 객체는 매번 새로 만들어질 수 있습니다. 이때 비교 기준을 모르면 “분명 값은 안 바뀐 것 같은데 왜 다시 렌더링되지?”라는 상황을 만나게 됩니다.
그래서 Zustand를 공부할 때 selector 다음에는 같은 얕은 비교 개념이 이어집니다. 이번 글에서 깊게 다루지는 않지만, 여러 값을 하나의 객체나 배열로 묶어 반환할 때는 “값은 같아 보여도 참조는 새로 만들어질 수 있다”는 점을 기억해야 합니다.
초반에는 두 가지 기준으로 나눠서 작성하면 됩니다. 값 하나를 가져올 때는 selector를 명확히 나누고, 여러 값을 묶어야 할 때는 다음 단계에서 얕은 비교가 필요한지 확인합니다. 처음부터 모든 컴포넌트에 비교 함수를 붙이는 방식은 오히려 코드 읽기를 어렵게 만들 수 있습니다.
action은 어떻게 가져오는 게 나을까
action 함수도 store의 일부입니다. 그래서같은 함수도 필요한 컴포넌트에서 selector로 가져오는 식으로 작성할 수 있습니다.
function ThemeButton() { const theme = useDashboardStore((state) => state.theme); const toggleTheme = useDashboardStore((state) => state.toggleTheme); return ( <button type="button" onClick={toggleTheme}> 현재 테마: {theme} </button> );
} 이 방식은 컴포넌트가 읽는 상태와 실행하는 action을 분리해서 보여줍니다. 이 바뀌면 화면 문구가 달라져야 하므로 다시 렌더링되는 것이 자연스럽습니다. 반면 함수 자체가 매번 새로 만들어지는 구조가 아니라면이 함수 때문에 불필요하게 화면이 흔들리는 상황은 줄어듭니다.
실제 작업에서는 action을 한 번에 여러 개 묶어서 가져오고 싶을 때가 있습니다. 그때도 객체 반환과 비교 기준을 같이 봐야 합니다. 단순히 “한 줄이 더 짧다”는 이유만으로 묶기보다이 컴포넌트가 어떤 값의 변화에 반응해야 하는지 먼저 정하는 쪽이 유지보수에 유리합니다.
Zustand 리렌더링을 볼 때 남길 기준
Zustand store는 여러 상태를 한곳에 모을 수 있습니다. 하지만 컴포넌트까지 store 전체를 바라보게 만들 필요는 없습니다. store를 크게 만들 수는 있어도, 각 컴포넌트의 구독 범위는 작게 잡는 것이 기본 기준입니다.
리렌더링 로그가 예상보다 많이 찍힐 때는 먼저 처럼 전체 store를 가져오는 코드가 있는지 확인합니다. 그 다음 해당 컴포넌트가 실제로 화면에 쓰는 값만 selector로 고르고 있는지 봅니다. 마지막으로 여러 값을 객체나 배열로 묶어 반환하고 있다면, 얕은 비교가 필요한 상황인지 다음 단계에서 점검하면 됩니다.
React 리렌더링은 컴포넌트 함수가 다시 호출되어 다음 UI를 계산하는 과정이고, Zustand selector는 어떤 store 변경에 그 컴포넌트가 반응할지 정하는 경계입니다. 이 둘을 분리해서 보면 “전역 상태를 쓰면 다 다시 렌더링된다”는 막연한 불안에서 벗어날 수 있습니다.
다음에 Zustand 코드를 수정할 때는 값을 어디에 저장할지보다 먼저, 각 컴포넌트가 어떤 값을 구독해야 하는지 확인하면 됩니다. 성능 최적화는 그 다음입니다. 처음부터 복잡한 비교 함수를 붙이기보다, store 전체 구독을 피하고 selector를 명확히 쓰는 것만으로도 리렌더링 흐름이 훨씬 읽기 쉬워집니다.