Zustand subscribeWithSelector·devtools: 선택 구독과 해제 실습

2026.09.15·약 7분·작성: 해비·블로그 소개
Zustand subscribeWithSelector·devtools: 선택 구독과 해제 실습 학습 표지

이 글은 프론트엔드 라이브러리 연결 과정의 일부입니다. React 컴포넌트·props·state·이벤트와 TypeScript 객체·배열 타입을 먼저 익혀 주세요. 원본 강의의 문장을 옮기는 대신 독립적인 예제와 확인 과제를 구성했습니다.

학습 목표: React 렌더 구독과 외부 리스너를 구분하고 등록한 구독을 정리합니다.

예상 학습 시간: 30분. 개인별 차이가 있으며 설치 시간은 제외합니다.

화면 구독과 부수 효과를 분리합니다

useStore(selector)는 컴포넌트가 화면에 필요한 값을 읽는 경로입니다. subscribeWithSelector는 선택한 값이 바뀌었을 때 외부 리스너를 호출할 수 있게 합니다. 리스너가 실행되는 것과 React가 렌더되는 것은 같은 사건이 아닙니다. 실습에서는 count 변경만 로그 문장에 반영하고 visits 변경은 무시합니다.

셀렉터·리스너·해제 함수

subscribe의 첫 인수는 관심 있는 값을 선택하고 두 번째 인수는 다음 값과 이전 값을 받습니다. 호출 결과는 구독 해제 함수입니다. 컴포넌트 수명에 맞춘 구독은 useEffect에서 등록하고 반환 함수로 정리합니다. 이 정리가 없으면 페이지를 나갔다 들어올 때 리스너가 누적될 수 있습니다. 개발 모드의 Effect 재실행도 고려해야 합니다.

const unsubscribe = useCounter.subscribe(
  (state) => state.count,
  (next, previous) => console.log(previous, next),
);

unsubscribe();

위 부분 예제의 unsubscribe는 리스너를 즉시 해제합니다. 실제 사용에서는 필요한 수명이 끝날 때 호출합니다. 두 배 값처럼 원래 값으로 계산할 수 있는 데이터는 별도 상태와 리스너로 다시 저장하기보다 count * 2로 계산하는 편이 단순합니다. 파생값을 저장하면 두 값의 불일치와 반복 갱신을 관리해야 합니다.

devtools는 변경을 보는 도구입니다

devtools 미들웨어는 브라우저의 Redux DevTools와 연결해 상태를 관찰합니다. Zustand가 Redux Toolkit store로 변환되는 것은 아닙니다. 이 예제에서는 로컬 Todo store의 바깥쪽에 devtools를 두고 개발 환경에서만 활성화합니다. 확장 프로그램이 없으면 DevTools 패널을 확인할 수 없습니다. 자동 테스트의 상태 검증과 브라우저 확장 화면 검증을 구분합니다.

미들웨어 중첩 순서는 타이핑과 wrapping 동작에 영향을 줄 수 있습니다. 모든 프로젝트에서 다섯 가지를 한꺼번에 쓰는 템플릿으로 외우지 마세요. PDF에 소개된 다섯 도구가 미들웨어 전체 목록이라는 의미도 아닙니다. 필요한 기능을 하나씩 추가하고 상태 변경 결과를 다시 확인합니다.

직접 해보기와 확인 질문

count 증가 → visits 증가 → 다른 메뉴 이동 → 돌아와 count 증가 순으로 조작하세요. 리스너 호출 횟수를 기록하세요.

풀이 기준 보기

visits 증가는 count 리스너를 호출하지 않아야 합니다. 컴포넌트를 떠났을 때 구독이 해제되고, 돌아와 등록한 리스너만 동작해야 합니다. 자동 테스트는 해제 후 재호출되지 않는 계약을 확인합니다.

공통 실습 실행

아래 공통 ZIP을 풀고 Node.js 24 환경에서 실행합니다. package-lock.json에 실습 버전을 고정했습니다. 본문의 경로와 ZIP의 경로는 같습니다.

npm ci
npm run dev

터미널에 표시된 주소를 열고 이 글에 해당하는 메뉴를 선택하세요. 서버 Todo만 별도 터미널의 npm run server가 필요합니다. 본문 코드에서 생략한 공통 설정과 UI 파일까지 ZIP에 포함되어 있습니다.

프론트엔드 라이브러리 공통 실습 ZIP

src/pages/middleware-page.tsx

공통 실습 ZIP 안의 전체 파일입니다. 이 파일 하나만으로 독립 실행되지는 않으며 import하는 파일은 ZIP에 함께 들어 있습니다.

import { useEffect, useState } from "react";
import { useCounter } from "../stores/counter";
import { Button } from "@/components/ui/button";

export default function MiddlewarePage() {
  const count = useCounter((state) => state.count);
  const visits = useCounter((state) => state.profile.visits);
  const increase = useCounter((state) => state.increase);
  const visit = useCounter((state) => state.visit);
  const [log, setLog] = useState("count 변경 대기");
  useEffect(
    () =>
      useCounter.subscribe(
        (state) => state.count,
        (next, previous) => setLog(`${previous} → ${next}`),
      ),
    [],
  );
  return (
    <section>
      <h2>미들웨어 관찰</h2>
      <p>
        count: {count} / visits: {visits}
      </p>
      <div className="flex gap-4">
        <Button onClick={increase}>count 증가</Button>
        <Button onClick={visit}>visits 증가</Button>
      </div>
      <p role="status">구독 기록: {log}</p>
      <p>visits만 바꾸면 count 구독 기록은 바뀌지 않습니다.</p>
    </section>
  );
}

UI 파일 범위: ZIP의 src/components/ui는 shadcn CLI 생성물이 아닙니다. 레지스트리 접속 실패로 Radix·Sonner·Embla를 사용하는 축약 학습용 구현을 작성했습니다. 공식 설치 절차는 별도 프로젝트에서 비교하세요. 공식 컴포넌트 전체 기능과 동일하다고 보장하지 않습니다.

검증 범위

Node.js 24.19.0, React 19.3.0, React Router 7.18.3, Zustand 5.0.15, TanStack Query 5.102.8, Tailwind CSS 4.3.3에서 타입 검사와 Vite 빌드를 확인했습니다. 자동 테스트는 jsdom 기반입니다. 실제 브라우저의 포커스·레이아웃·화면낭독기와 모든 네트워크 경쟁 상황은 별도 확인 대상입니다.

npm test
npm run build

공식 문서

2026-09-14 확인. 공식 최신 문서의 버전과 실습 고정 버전이 다를 수 있으므로 설치 버전은 ZIP을 기준으로 비교합니다.

이 글이 도움이 되었나요?

조회 중

Zustand 학습 순서

필수 14개 · 전체 15개

읽음 기록 관리

전체 과정 목차 (15개)
  1. 필수 길잡이 · Zustand 학습 로드맵: store·action·selector·persist 순서
  2. 필수 길잡이 · React state vs Zustand: 전역 상태가 필요한 기준
  3. 필수 학습 · Zustand란? React 상태 관리 선택 기준과 기본 Store
  4. 필수 학습 · Zustand 설치 사용법: 기본 Store 만들고 상태 연결하기
  5. 필수 학습 · Zustand state 사용법: 값 읽기와 변경 흐름 익히기
  6. 필수 학습 · Zustand action 사용법: 상태 변경 로직을 store로 분리하기
  7. 필수 학습 · Zustand selector 사용법: 필요한 상태만 가져와 리렌더링 줄이기
  8. 필수 학습 · Zustand 리렌더링 원리와 selector 최적화 방법
  9. 필수 학습 · Zustand persist 사용법: 새로고침 후 상태 저장하기
  10. 필수 학습 · Zustand persist 마이그레이션 기준: 저장된 상태 구조가 바뀔 때
  11. 선택 참고 · Zustand 상태 변경 후 리렌더링이 안 될 때 해결 방법
  12. 필수 선수 · Zustand 실무 사용 기준: store가 복잡해질 때 피할 실수
  13. 필수 학습 · Zustand combine·immer 실습: 타입 추론과 중첩 상태 불변성
  14. 필수 학습 · Zustand subscribeWithSelector·devtools: 선택 구독과 해제 실습 현재 글
  15. 필수 학습 · Zustand Todo 완성 실습: actions·선택 훅·persist 연결

새 글 받아보기

RSS 리더에서 BlogFlow의 새 글을 확인할 수 있습니다.

RSS 피드 구독하기

댓글 남기기