본문 바로가기
React

⚽️Zustand

by frontChoi 2025. 12. 1.
반응형

🛺 Zustand란

  • React 상태 관리 라이브러리

🚜 create 함수

  • Zustand에서 상태 저장소를 만드는 함수
import { create } from "zustand";

interface SeatInterface {
  seatNumber: number;
  setSeatNumber: () => void;
}

/* Create를 통해 저장소 생성 */
export const useSeatStore = create<SeatInterface>((set) => ({
  seatNumber: 0,
  setSeatNumber: () => set({ seatNumber: 1 }),
}));

 

 

🚍set 함수

  • 상태를 업데이트 하는 함수
import { create } from "zustand";

interface SeatInterface {
  seatNumber: number;
  setSeatNumber: () => void;
}

/* Create를 통해 저장소 생성 */
export const useSeatStore = create<SeatInterface>((set, get) => ({
  seatNumber: 0,
  // set함수를 통해 상태업데이트
  setSeatNumber: () => set({ seatNumber: get().seatNumber + 1 }),
}));

 

 

🚄get 함수

  • 상태를 조회하는 함수
import { create } from "zustand";

interface SeatInterface {
  seatNumber: number;
  setSeatNumber: () => void;
}

/* Create를 통해 저장소 생성 */
export const useSeatStore = create<SeatInterface>((set, get) => ({
  seatNumber: 0,
  // get함수를 통해 상태조회
  setSeatNumber: () => set({ seatNumber: get().seatNumber + 1 }),
}));

 

🚔 컴퍼넌트에서 사용

"use client";

import { useSeatStore } from "app/store/useSeatStore";
import React from "react";

export default function Seat() {
  // store의 state
  const seatNumber = useSeatStore((state) => state.seatNumber);
  // store의 actions
  const setSeatNumber = useSeatStore((state) => state.setSeatNumber);

  function changeInputEvent(e: React.ChangeEvent<HTMLInputElement>) {
    setSeatNumber();
  }

  return (
    <>
      <div className="relative z-0 w-full group">
        <input
          className="block py-2.5 px-0 w-full text-sm text-gray-900 bg-transparent border-0 border-b-2 border-gray-300 appearance-none dark:text-white dark:border-gray-600 dark:focus:border-blue-500 focus:outline-none focus:ring-0 focus:border-blue-600 peer"
          type="number"
          value={seatNumber}
          onChange={changeInputEvent}
          placeholder="값을 입력하세요"
        ></input>
      </div>
    </>
  );
}

 

 

🚈 다중 상태 선택

  • 컴퍼넌트에서는 한번에 하나씩만 상태를 가져와야 하지만 useShallow를 이용하면 상태/액션을 겍체 또는 배열 형태로 가져올 수 있다
"use client";

import { useSeatStore } from "app/store/useSeatStore";
import React from "react";
import { useShallow } from "zustand/shallow";

export default function Seat() {
  const seatState = useSeatStore(
    // useShallow를 통해 상태를 객체 또는 배열을 가져올 수 있다
    useShallow((state) => ({
      seatNumber: state.seatNumber,
      setSeatNumber: state.setSeatNumber,
    }))
  );
  
  function changeInputEvent(e: React.ChangeEvent<HTMLInputElement>) {
    // 객체로 접근
    seatState.setSeatNumber();
  }

  return (
    <>
      <div className="relative z-0 w-full group">
        <input
          className="block py-2.5 px-0 w-full text-sm text-gray-900 bg-transparent border-0 border-b-2 border-gray-300 appearance-none dark:text-white dark:border-gray-600 dark:focus:border-blue-500 focus:outline-none focus:ring-0 focus:border-blue-600 peer"
          type="number"
          value={seatState.seatNumber}
          onChange={changeInputEvent}
          placeholder="값을 입력하세요"
        ></input>
      </div>
    </>
  );
}

 

 

🚈 액션 분리

  • 가독성을 위해 State와 actions를 분리
import { create } from "zustand";

// 상태 인터페이스
interface State {
  seatNumber: number;
}

// 액션 인터페이스
interface Actions {
  actions: {
    setSeatNumber: () => void;
  };
}

// State와 Actions를 병합
export const useSeatStore = create<State & Actions>((set, get) => ({
  seatNumber: 0,
  actions: {
    setSeatNumber: () => set({ seatNumber: get().seatNumber + 1 }),
  },
}));
"use client";
import { useSeatStore } from "app/store/useSeatStore";
// 컴퍼넌트에서 사용
export default function Seat() {
  const seatNumber = useSeatStore((state) => state.seatNumber);
  const { setSeatNumber } = useSeatStore((state) => state.actions);

  function onChangeEvent(e: React.ChangeEvent<HTMLInputElement>) {
    setSeatNumber();
  }

  return (
    <>
      <input
        type="number"
        name="id"
        value={seatNumber}
        onChange={onChangeEvent}
        placeholder="ID"
        className="border p-2 rounded"
      ></input>
    </>
  );
}

 

 

🚋  초기화

import { create } from "zustand";

interface State {
  seatNumber: number;
}

interface Actions {
  actions: {
    setSeatNumber: () => void;
    // 리셋 함수 인자 keys
    resetData: (keys?: Array<keyof State>) => void;
  };
}

// 초기화 객체 선언 및 타입은 State
const initialState: State = {
  seatNumber: 0,
};

export const useSeatStore = create<State & Actions>((set, get) => ({
  seatNumber: 0,
  actions: {
    setSeatNumber: () => set({ seatNumber: get().seatNumber + 1 }),
    resetData: (keys) => {
      if (!keys) {
        set(initialState);
        return;
      }
			// Key리스트가 있을 경우 초기화
      keys.forEach((key) => {
        set({ [key]: initialState[key] });
      });
    },
  },
}));

 

 

🚋  상태의 타입 추론 (Combine)

  • 타입을 작성하지 않고 추론하도록 제공하는 combine미들웨어

 

combine을 사용하면 initialState의 channelId,channelName,channelUrl이 string으로 추론

const initialState = {
  channelId: "",
  channelName: "",
  channelUrl: "",
};
export const useChannelStore = create(
  devtools(
    // combine을 통해 객체의 key의 value를 통해 타입 추론
    combine(initialState, (set) => ({
      actions: {
        resetState: (keys: Array<keyof typeof initialState>) => {
          keys.forEach((key) => {
            set({ [key]: initialState[key] });
          });
        },
        setChannelState: <
          K extends keyof typeof initialState,
          T extends typeof initialState[K]
        >(
          key: K,
          val: T
        ) => {
          set({ [key]: val });
        },
      },
    }))
  )
);

 

 

🚘 중첩된 객체 변경 (Immer)

  • 객체안에 객체의 값을 변경할려면 기존의 속성값을 복사해야한다. 특정 하위속성을 변경할려면 복잡하다.
npm i immer
import { create } from "zustand";
import { immer } from "zustand/middleware/immer";
interface State {
  brandInfo: {
    brandId: string;
    brandName: string;
  };
}

interface Actions {
  actions: {
    setBrandId: (val: string) => void;
    setBrandName: (val: string) => void;
  };
}

const initialState: State = {
  brandInfo: {
    brandId: "",
    brandName: "",
  },
};

export const useBrandStore = create(
  // immer를 선언
  immer<State & Actions>((set) => ({
    ...initialState,
    actions: {
      setBrandId: (val) => {
        set((state) => {
          /* 객체복사가 아닌 체이닝으로 접근하여 변경 */
          state.brandInfo.brandId = val;
        });
      },
      setBrandName: (val) => {
        set((state) => {
          state.brandInfo.brandName = val;
        });
      },
    },
  }))
);

 

 

🚋 상태구독 (subscribeWithSelector)

  • 스토어 훅에서 subscribe를 사용하면, 스토어의 모든 상태 변경을 구독한다. 그리고 subscribe 함수 반환을 호출하면, 구독을 해제할 수 있다
import { create } from "zustand";
// 특정상태 구독을 위해 subscribeWithSelector 사용
import { subscribeWithSelector } from "zustand/middleware";

interface State {
  broadCastId: string;
  broadCastName: string;
}

interface Actions {
  actions: {
    setBrandCastId(val: string): void;
  };
}

const initialState: State = {
  broadCastId: "",
  broadCastName: "",
};

export const useBroadCastStore = create<State & Actions>()(
  // subscribeWithSelector으로 감싼다
  subscribeWithSelector((set) => ({
    ...initialState,
    actions: {
      setBrandCastId: (val) => {
        set({ broadCastId: val });
      },
    },
  }))
);

 

컴퍼넌트에서 사용

"use client";

import { useBroadCastStore } from "app/store/broadCastStore";
import { useEffect, useState } from "react";

export default function BroadCast() {
  const setBrandCastId = useBroadCastStore(
    (state) => state.actions.setBrandCastId
  );
  const [double, setDouble] = useState(1);

  // 구독처리 및 구독해제
  useEffect(() => {
    // 리스너이며, broadCastId가 변경이되면 트리거가 된다
    const unsubscribe = useBroadCastStore.subscribe(
      (state) => state.broadCastId,
      () => {
        // 2. 변경된 broadCastId를 감지하고, 지역컴퍼넌트의 값을 변경한다
        setDouble(double * 2);
      }
    );

    setTimeout(() => {
      // 1.5초후 broadCastId를 변경한다
      setBrandCastId("NEW ID");
    }, 5000);

    return () => {
      // 컴퍼넌트 소멸시점에는 구독해지를 한다
      unsubscribe();
    };
  }, []);

  return (
    <div className="container mx-auto pt-2 max-w-90 w-full md:max-w-2xl lg:max-w-5xl">
      <div className="min-w-0 flex-1">
        <span>Class Warfare</span> <br />
        <span>{double}</span>
      </div>
    </div>
  );
}
반응형

'React' 카테고리의 다른 글

🍏 React Hook  (0) 2025.10.24

댓글