React에서 상태를 다룰 때 가장 먼저 떠오르는 도구는 보통 useState다.
컴포넌트 안에서 바로 선언할 수 있고, 이해하기도 쉽다.
반면 Zustand는 전역 상태 관리 도구라는 인식이 강하다.
그래서 상태를 만들 때 종종 이런 고민이 생긴다.
"이건 그냥 useState로 두는 게 맞을까, 아니면 Zustand로 빼는 게 맞을까?"
처음에는 단순한 모달 상태 하나였는데,
조금 지나면 여러 컴포넌트에서 참조하고,
라우트가 바뀌어도 유지하고 싶고,
다른 액션과 함께 제어해야 하는 순간이 온다.
바로 이때 로컬 상태와 전역 상태의 경계가 헷갈리기 시작한다.
이번 글에서는 Zustand를 로컬 상태 대신 써도 되는 순간이 언제인지,
그리고 반대로 그냥 useState로 두는 편이 더 좋은 순간은 언제인지 정리해보려고 한다.
먼저 결론부터
아주 단순하게 정리하면 이렇다.
- 한 컴포넌트 안에서만 쓰이고 생명주기도 짧다면
useState - 여러 컴포넌트가 함께 제어해야 하거나, 컴포넌트 경계를 넘어 유지되어야 한다면
Zustand
즉, 중요한 것은 상태의 크기가 아니라 상태의 소유권과 공유 범위다.
로컬 상태로 두는 편이 자연스러운 경우
먼저 useState가 더 적합한 경우부터 보는 것이 좋다.
생각보다 많은 상태는 전역으로 올릴 필요가 없다.
1. 특정 컴포넌트 안에서만 쓰이는 UI 상태일 때
예를 들면 이런 것들이다.
- input의 현재 값
- 드롭다운 열림 여부
- 탭 hover 상태
- 아코디언 열림 여부
- 임시 에러 메시지 표시 여부
이런 상태는 보통 해당 컴포넌트 안에서만 의미가 있다.
다른 곳에서 참조할 필요도 없고, 컴포넌트가 사라지면 함께 사라져도 문제 없다.
예를 들어 아래 같은 상태는 굳이 전역으로 올릴 이유가 크지 않다.
function SearchInput() {
const [keyword, setKeyword] = useState("");
return (
<input
value={keyword}
onChange={(event) => setKeyword(event.target.value)}
/>
);
}
이 정도 상태를 Zustand로 빼면 오히려 읽는 사람이
"왜 이게 전역이어야 하지?"를 먼저 생각하게 된다.
2. 컴포넌트가 사라지면 같이 초기화되어야 하는 상태일 때
로컬 상태의 장점 중 하나는 생명주기가 컴포넌트와 함께 간다는 점이다.
예를 들어 모달 내부 폼 입력값처럼,
모달이 닫히면 상태도 같이 사라지는 편이 자연스러운 경우가 있다.
이런 상태를 전역 store에 넣어두면
다음에 모달을 다시 열었을 때 이전 입력값이 남아 있어 의도치 않은 UX가 될 수 있다.
즉, "자동으로 함께 사라지는 것"이 장점인 상태는
굳이 전역으로 올리지 않는 편이 더 낫다.
3. 부모-자식 관계만으로 충분히 전달 가능한 상태일 때
상태가 한두 단계 아래 컴포넌트에만 전달되면,
전역 상태보다 props나 local state가 더 단순할 수 있다.
전역 상태는 편리하지만, 모든 공유를 전역으로 해결하려고 하면
오히려 데이터 흐름이 덜 명확해질 수 있다.
그럼 언제 Zustand로 빼는 것이 자연스러울까
반대로 아래 상황에서는 useState보다 Zustand가 더 잘 맞는다.
1. 여러 컴포넌트가 같은 상태를 함께 읽고 수정해야 할 때
가장 대표적인 경우다.
예를 들어 헤더의 검색 필터, 본문 리스트, 우측 패널이
같은 정렬 옵션과 선택된 카테고리를 공유해야 한다고 해보자.
이걸 전부 useState와 props drilling으로 연결하기 시작하면
컴포넌트 구조가 깊어질수록 전달 비용이 커진다.
이럴 때는 공통 상태를 store로 올리는 편이 훨씬 단순하다.
import { create } from "zustand";
interface FilterState {
selectedCategory: string | null;
sortOrder: "latest" | "popular";
setSelectedCategory: (value: string | null) => void;
setSortOrder: (value: "latest" | "popular") => void;
}
export const useFilterStore = create<FilterState>((set) => ({
selectedCategory: null,
sortOrder: "latest",
setSelectedCategory: (selectedCategory) => set({ selectedCategory }),
setSortOrder: (sortOrder) => set({ sortOrder }),
}));
이 상태는 더 이상 특정 컴포넌트의 로컬 상태라기보다
화면 전체가 함께 쓰는 제어 상태에 가깝다.
2. 형제 컴포넌트 사이에서 상태를 자주 공유해야 할 때
부모를 통해 올렸다 내리는 방식이 가능하더라도,
형제 컴포넌트가 많아질수록 관리 포인트가 늘어난다.
예를 들어
- 왼쪽 필터 패널
- 상단 툴바
- 상품 목록
- 결과 요약 배너
모두가 같은 필터 상태를 봐야 한다면,
이 상태는 특정 컴포넌트의 소유물이라기보다 화면 단위의 공유 상태에 가깝다.
이런 경우는 useState보다 Zustand가 더 자연스럽다.
3. 라우트가 바뀌거나 컴포넌트가 재마운트되어도 유지되어야 할 때
어떤 상태는 컴포넌트 생명주기보다 더 오래 살아야 한다.
예를 들면 아래와 같다.
- 앱 전체 다크 모드
- 로그인 이후 유지되는 UI 설정
- 여러 페이지에서 공통으로 쓰는 필터 상태
- 전역 토스트/모달 제어 상태
이런 값은 특정 컴포넌트에 두면 언젠가 재마운트와 함께 사라질 수 있다.
그렇다면 애초에 store에서 관리하는 편이 더 맞다.
4. 여러 액션이 하나의 상태를 중심으로 연결될 때
로컬 상태는 단순한 값 하나를 바꾸는 데는 충분히 좋다.
하지만 상태 변화가 여러 액션과 연결되기 시작하면 store 쪽이 더 읽기 쉬워질 수 있다.
예를 들어 알림 센터 상태를 생각해보자.
- 열기
- 닫기
- 모두 읽음 처리
- 특정 알림 제거
이런 액션이 늘어나면 단순한 useState보다
상태와 액션을 한 곳에 모으는 편이 오히려 구조가 선명해진다.
"전역으로 올릴 수 있다"와 "전역으로 올려야 한다"는 다르다
Zustand를 쓰다 보면 상태를 쉽게 store로 뺄 수 있다.
그래서 로컬 상태도 전부 전역으로 올리고 싶어질 수 있다.
하지만 여기서 중요한 것은 가능 여부가 아니라 필요 여부다.
예를 들어 검색 input 값 하나를 생각해보자.
const [keyword, setKeyword] = useState("");
이 값이 현재는 input 하나에서만 쓰인다면 useState가 가장 자연스럽다.
그런데 다음과 같은 요구사항이 생기면 이야기가 달라진다.
- 다른 컴포넌트에서도 같은 검색어를 참조해야 한다
- 라우트 이동 후에도 유지하고 싶다
- 검색 조건 전체를 하나의 제어 상태로 묶고 싶다
즉, 상태는 처음에는 로컬이었다가,
시간이 지나며 공유 상태로 성격이 바뀔 수 있다.
내가 보기에는 Zustand는 바로 이 지점에서 가장 가치가 크다.
실무에서는 이런 기준으로 많이 판단한다
내 기준에서는 아래 질문을 해보면 꽤 잘 구분된다.
1. 이 상태는 누가 소유하는가
특정 컴포넌트만 소유한다면 로컬 상태가 자연스럽다.
화면 전체나 앱 전체가 함께 소유한다면 전역 상태가 더 낫다.
2. 이 상태를 다른 컴포넌트가 읽거나 수정해야 하는가
읽기만 하는지, 수정까지 해야 하는지도 중요하다.
여러 컴포넌트가 같은 상태를 변경해야 한다면 store의 가치가 커진다.
3. 컴포넌트가 사라져도 이 상태가 유지되어야 하는가
유지되어야 한다면 로컬 상태보다 store가 맞을 가능성이 높다.
4. props로 전달하기 시작했을 때 구조가 어색해지는가
상태를 위로 끌어올리다 보니
실제로는 사용하지 않는 중간 컴포넌트들이 props만 전달하고 있다면,
그건 공유 방식이 불편해졌다는 신호일 수 있다.
전역으로 빼면 오히려 손해인 경우도 있다
Zustand가 편하다고 해서 무조건 좋은 것은 아니다.
로컬 상태를 전역으로 옮기면 아래 같은 단점도 생긴다.
1. 상태의 생명주기가 길어져서 의도치 않게 값이 남는다
원래는 컴포넌트가 사라지면 초기화되어야 할 값이
store에 남아서 다음 진입 시 그대로 보일 수 있다.
2. 작은 UI 상태까지 전역으로 관리하면 구조가 오히려 시끄러워진다
토글 하나, input 하나, 임시 에러 메시지 하나까지 전역으로 빼면
코드가 단순해지는 것이 아니라 파일이 흩어지고 문맥이 끊긴다.
3. "누가 이 상태를 책임지는가"가 흐려질 수 있다
로컬 상태는 소유권이 분명하다.
하지만 전역 상태는 접근이 쉬운 만큼 책임이 흐려지기 쉽다.
즉, 접근성이 좋아졌다고 해서 설계가 좋아진 것은 아니다.
추천하는 판단 순서
실제로는 아래 순서로 생각하면 꽤 무난하다.
1. 먼저 로컬 상태로 시작한다
처음부터 전역으로 설계하려고 하지 않아도 된다.
많은 상태는 로컬로 시작해도 충분하다.
2. 공유 요구가 생기면 store로 옮길지 검토한다
여러 컴포넌트가 읽고 수정해야 하거나,
라우트 이동 후에도 유지해야 한다면 그때 전역화를 고민하면 된다.
3. 그래도 가장 작은 범위의 공유를 우선한다
무조건 앱 전체 전역 상태로 가지 말고,
"정말 이 범위까지 공유가 필요한가?"를 먼저 보는 편이 좋다.
마무리
Zustand를 로컬 상태 대신 써도 되는 순간은
단순히 "가능해서"가 아니라,
그 상태가 더 이상 한 컴포넌트의 문제만이 아닐 때라고 생각한다.
즉, 아래에 가까워질수록 Zustand가 자연스럽다.
- 여러 컴포넌트가 함께 읽고 수정한다
- 컴포넌트 생명주기를 넘어 유지되어야 한다
- props 전달이 구조적으로 불편해진다
- 상태와 액션을 한 곳에서 관리하는 편이 더 명확하다
반대로 아래에 가깝다면 그냥 useState가 더 낫다.
- 한 컴포넌트 안에서만 쓰인다
- 컴포넌트가 사라지면 같이 사라져야 한다
- UI의 아주 작은 상호작용만 담당한다
내 기준에서 좋은 상태 관리는
"전역으로 많이 올리는 것"이 아니라
"어디까지가 로컬이고 어디부터가 공유 상태인지 구분하는 것"에 더 가깝다.
그래서 어떤 상태를 store에 넣을지 고민될 때는
"이 상태를 정말 여러 곳이 함께 책임져야 하는가?"부터 먼저 물어보는 편이 가장 실용적이다.
'Develope > React' 카테고리의 다른 글
| Zustand에서 전역 상태와 서버 상태를 어떻게 구분할까? (0) | 2026.04.13 |
|---|---|
| Zustand store를 너무 크게 만들면 어떤 문제가 생길까? slice 패턴으로 나누는 기준 (0) | 2026.04.13 |
| Zustand에서 shallow 비교는 언제 필요할까? selector와 함께 이해하기 (0) | 2026.04.11 |
| Zustand Selector는 언제 써야 할까? 성능과 가독성 사이에서 판단하는 기준 (1) | 2026.04.09 |
| CRA에서 Vite + SSR로 마이그레이션하며 겪은 문제와 해결 과정 (0) | 2026.04.05 |