본문 바로가기

개발 공부

Pub/Sub (3) - Next.js + Zustand

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