미들웨어, 시간 여행 디버깅의 날개를 달다

9

발행일: 2025년 05월 10일

선택적 구독이라는 핵심 엔진을 완성했지만, 다이시 카토는 만족할 수 없었다. 자동차는 엔진만으로 달리지 않는다. 훌륭한 서스펜션, 정교한 계기판, 그리고 예상치 못한 상황에 대비하는 안전장치까지 갖춰야 비로소 진정한 ‘탈 것’이 된다. 지금의 Zustand는 강력한 엔진을 가졌지만, 아직은 뼈대만 앙상한 경주용 카트와 같았다.

“실제 애플리케이션은 훨씬 복잡해.”

카토는 실제 프로젝트에서 마주했던 문제들을 떠올렸다.

상태가 어떻게 변하는지 실시간으로 추적하고 싶을 때 (로깅).
API 호출 같은 비동기 작업을 처리하고 그 결과를 상태에 반영해야 할 때 (비동기 처리).
특정 상태 변경에 따라 부가적인 효과(side effect)를 일으키고 싶을 때.
혹은, 상태의 일부를 로컬 스토리지나 세션 스토리지에 저장하여 영속성을 부여하고 싶을 때.

지금의 Zustand 코어만으로는 이런 요구사항들을 우아하게 처리하기 어려웠다. 물론 사용자가 직접 setState 함수를 감싸거나 useEffect 안에서 복잡한 로직을 구현할 수도 있겠지만, 그건 Zustand가 추구하는 ‘간결함’과는 거리가 멀었다. 자칫 잘못하면 상태 관리 로직이 다시 컴포넌트 여기저기에 흩어져 버릴 수도 있었다.

‘핵심은 가볍게 유지하되, 확장할 수 있는 길을 열어둬야 해.’

그의 머릿속에 다시 한번 익숙한 개념이 떠올랐다. 미들웨어(Middleware).

Redux 같은 라이브러리에서 널리 쓰이는 패턴. 상태 변경 요청(혹은 상태 변경 자체)이 스토어에 도달하기 전, 혹은 상태가 변경된 후에 끼어들어 추가적인 작업을 수행하는 함수들의 연쇄 고리. 마치 고속도로의 톨게이트처럼, 데이터의 흐름 중간에 위치하여 검문하거나 부가 서비스를 제공하는 역할이었다.

“그래, 미들웨어다!”

카토는 다시 키보드 앞에 앉았다. 목표는 명확했다. Redux처럼 강력하지만, 그보다는 훨씬 간결하고 직관적인 미들웨어 API를 구현하는 것. Zustand의 create 함수가 미들웨어들을 인자로 받아, 스토어 생성 과정에 자연스럽게 통합시키는 방식을 구상했다.

// 미들웨어 적용 구상 스케치
import { create } from 'zustand';
import { devtools, persist } from 'zustand/middleware'; // 가상의 미들웨어들

const useStore = create(
  devtools(
    // 1. Redux DevTools 미들웨어 적용
    persist(
      // 2. 상태 영속성 미들웨어 적용
      (set, get) => ({
        // 3. 원래의 스토어 설정 함수
        bears: 0,
        increasePopulation: () => set((state) => ({ bears: state.bears + 1 })),
        removeAllBears: () => set({ bears: 0 }),
        // 비동기 예시 (미들웨어 없이도 가능하지만, 미들웨어로 더 깔끔하게 가능)
        fetchData: async () => {
          const response = await fetch('/api/data');
          const data = await response.json();
          set({ data });
        },
      }),
      { name: 'bear-storage' } // persist 미들웨어 옵션
    )
  )
);

그의 손가락이 다시 현란하게 움직였다. 미들웨어 함수들이 스토어의 setStategetState 함수를 감싸거나 확장할 수 있는 구조를 설계했다. zustand/middleware 경로 아래, 로깅, 비동기 처리 등을 위한 기본적인 미들웨어들이 하나씩 모습을 갖춰갔다. 사용자들은 필요에 따라 이 미들웨어들을 조합하여 자신만의 스토어를 구성할 수 있게 될 터였다.

그러던 중, 카토의 뇌리를 스치는 결정적인 아이디어가 있었다.

‘Redux DevTools… 시간 여행 디버깅!’

Redux가 가진 가장 강력한 무기 중 하나. 상태가 어떻게 변화해왔는지 타임라인으로 확인하고, 과거의 특정 시점으로 상태를 되돌려 버그를 추적할 수 있는 기능. 개발자에게는 그야말로 신의 축복과 같은 도구였다.

만약 Zustand도 Redux DevTools와 연동할 수 있다면? 비록 Redux와 내부 구조는 다르지만, DevTools가 이해할 수 있는 방식으로 상태 변경 내역을 전달할 수만 있다면? 그것은 Zustand의 사용성을 극적으로 끌어올릴 수 있는 필살기가 될 터였다. 마치 개발자에게 신의 도구를 쥐여주는 것과 같았다.

“해보자… 이건 무조건 넣어야 해!”

쉬운 작업은 아니었다. Redux DevTools가 기대하는 액션 형식과 상태 구조를 맞춰야 했고, Zustand의 상태 변경 방식을 그에 맞게 변환하여 전달하는 로직이 필요했다. 그는 Redux DevTools 확장 프로그램의 API 문서를 파고들며, 어떻게 통신해야 할지 연구했다.

몇 번의 실패와 디버깅 끝에, 마침내 그의 로컬 개발 환경 화면 한쪽에 익숙한 Redux DevTools 창이 떠올랐다. 그리고 Zustand 스토어의 상태 변경 내역이 실시간으로 기록되고, 시간 여행 슬라이더가 활성화되는 순간…

“됐다!”

카토는 자신도 모르게 환호성을 질렀다. 단순히 상태를 관리하는 것을 넘어, 개발 과정을 혁신적으로 개선할 수 있는 강력한 무기를 손에 넣은 것이다. 슬라이더를 움직이자 애플리케이션의 상태가 과거로 되돌아갔다. 버그가 발생했던 시점으로 정확히 이동하여 원인을 분석할 수 있게 된 것이다!

미들웨어를 통한 확장성, 그리고 Redux DevTools 연동을 통한 강력한 디버깅 능력. Zustand는 이제 단순함이라는 갑옷 위에 강력한 무기까지 갖추게 된 셈이었다.

‘단순함 속에 강력한 확장을.’

Zustand의 철학이 비로소 완성되는 순간이었다. 코어는 여전히 작고 가벼웠지만, 필요에 따라 얼마든지 강력한 기능을 추가할 수 있는 유연성. 이것이야말로 카토가 꿈꿔왔던 이상적인 상태 관리 솔루션의 모습이었다.

이제 이 작은 거인에게 남은 것은, 실제 전장으로 나서는 것뿐이었다. 혹독한 실전 테스트를 통해 그 가치를 증명하고, 세상의 개발자들에게 새로운 가능성을 제시하는 일. 카토의 심장은 다시 한번 뜨겁게 타오르기 시작했다.