Zustand로 Pub/Sub을 구현하는 방법
React와 Next.js에서 컴포넌트 간 이벤트 전달이 필요할 때 Context API를 쓰는 경우가 많지만, 규모가 커지면 다음과 같은 문제가 생깁니다.
- Provider 트리 복잡도 증가
- 비효율적인 리렌더링
- SSR이나 페이지 이동 시 상태 손실
이를 해결할 수 있는 대안이 바로 Zustand입니다.
Zustand는 아주 가볍고 빠르며, 상태를 컴포넌트 외부에서 정의하기 때문에 자연스럽게 Pub/Sub 구조를 갖습니다.
Pub/Sub 모델에서의 역할 매핑
| Pub/Sub 개념 | Zustand에서 |
| Publisher | 상태를 set()하는 쪽 |
| Subscriber | useStore(selector)로 구독하는 쪽 |
| Event Broker |
Zustand 스토어 그 자체
|
동기화 예제 : 검색어 입력
검색어 입력 (Publisher) 컴포넌트
먼저 전역 상태 저장소를 Zustand로 생성해보겠습니다.
stores/searchStore.ts
import { create } from 'zustand';
interface SearchStore {
keyword: string;
setKeyword: (value: string) => void;
}
export const useSearchStore = create<SearchStore>(set => ({
keyword: '',
setKeyword: (value) => set({ keyword: value.trim() })
}));
- keyword 상태와 그를 변경하는 setKeyword 함수를 포함한 전역 상태 스토어
- 문자열 상태지만, 추후 다양한 필터나 로딩 상태도 추가 가능
components/SearchInput.tsx
'use client';
import { useSearchStore } from '@/stores/searchStore';
export default function SearchInput() {
const setKeyword = useSearchStore(state => state.setKeyword);
return (
<input
type="text"
placeholder="검색어를 입력하세요"
onChange={(e) => setKeyword(e.target.value)}
className="border px-4 py-2 rounded"
/>
);
}
- 사용자가 입력하는 순간마다 setKeyword()를 통해 상태를 발행 (Publish)
- use client 지시어는 Next.js 13 App Router에서 필수
검색 결과 (Subscriber) 컴포넌트
이제 keyword 상태를 구독해서 결과를 보여주는 구독자 컴포넌트를 만들어봅니다.
components/SearchResult.tsx
'use client';
import { useEffect, useState } from 'react';
import { useSearchStore } from '@/stores/searchStore';
import { debounce } from 'lodash';
export default function SearchResult() {
const keyword = useSearchStore(state => state.keyword);
const [result, setResult] = useState('');
// 검색 로직 (debounce 처리)
const search = debounce((kw: string) => {
if (!kw) return setResult('');
setResult(`"${kw}"에 대한 검색 결과입니다.`);
}, 300);
useEffect(() => {
search(keyword);
return () => {
search.cancel();
};
}, [keyword]);
return (
<p className="mt-4 text-blue-600">{result}</p>
);
}
- keyword 상태를 구독하여 자동 업데이트됨 (Subscribe)
- useEffect()와 lodash.debounce()를 조합하여 검색 요청 최적화
예제 설명
이 구조는 전형적인 Pub/Sub 구조입니다:
- SearchInput은 상태를 발행 (Publisher)
- SearchResult는 상태를 구독 (Subscriber)
- 이 둘은 서로를 전혀 참조하지 않음
- 중간의 zustand store가 이벤트 브로커 역할
이 구조 덕분에 어떤 컴포넌트든, 어떤 페이지든 useSearchStore()만 사용하면 전역 검색 상태를 공유할 수 있게 됩니다.
페이지 전환에도 상태가 유지되는 이유
Zustand는 기본적으로 상태를 메모리에 보관하기 때문에, 페이지 전환이 이루어져도 상태는 그대로 유지됩니다.
- Zustand의 스토어는 컴포넌트 외부에 선언되어 있으므로, React lifecycle과 별개로 존재
- React Context와 달리 Provider가 필요 없어 Next.js App Router 구조에서도 안정적
- 심지어 persist 미들웨어를 쓰면 로컬스토리지까지 연동 가능 (아래 예시)
import { create } from 'zustand';
import { persist } from 'zustand/middleware';
export const useSearchStore = create(
persist<SearchStore>(
(set) => ({
keyword: '',
setKeyword: (value) => set({ keyword: value })
}),
{ name: 'search-storage' } // localStorage key
)
);
- 이로 인해 브라우저 새로고침 후에도 상태 유지 가능해짐
요약
| 기능 | 구현 방식 |
| Pub/Sub 패턴 | Zustand store를 중심으로 상태 발행/구독 |
| Publisher | 검색어 입력 컴포넌트에서 setKeyword() 호출 |
| Subscriber | 결과 컴포넌트에서 keyword 상태 구독 |
| 최적화 | debounce로 API 호출 제어 |
| 상태 유지 | 컴포넌트 외부 store + persist 옵션 |
'개발 공부' 카테고리의 다른 글
| 거시적인 관점에서 코드를 짜기 (0) | 2025.09.08 |
|---|---|
| try-catch & then-catch (1) | 2025.08.03 |
| Pub/Sub (2) - Angular + RxJS (0) | 2025.06.29 |
| Pub/Sub (1) (0) | 2025.06.28 |
| Node.js, Redis (0) | 2025.06.25 |