Frontend/Zustand

[Zustand] 상태 및 액션 설계 패턴 (작성 중)

Rayched 2025. 6. 25. 15:36
목차
1. 상태 설계 패턴
2. 액션 설계 패턴

1. 상태 설계 패턴

① Store 구조화 기본 원칙

 

Zustand의 Store는 상태, 액션(함수) 계산된 값(Computed Value), 총 세가지로 나눠서 설계를 한다.

const useCountStore = create((set, get) => ({
    count: 0,
    setCount: () => set((state) => ({count: state.count + 1})),
    doubleCount: () => get().count * 2
}));

 

상단의 소스코드에서 count는 상태에 해당하며

setCount는 count의 값을 1 증가하는 액션을 수행하는, 상태를 변경하는 함수이고

그리고 doubleCount는 get() 함수를 통해 상태의 값을 가져와서 해당 값에 숫자 2를 곱한다.

 

즉, 계산된 값에 해당한다고 볼 수 있다.

 

이런 식으로 Store를 구성하는 요소의 역할을 명확하게 구분해두면

코드의 가독성이 좋아지고, 유지보수하기 더 수월해진다.


② 상태 선택 패턴 (Selector)

 

컴포넌트에서 Store를 호출해서 사용할 경우, 해당 컴포넌트에서 필요한 값만 선택해서 사용할 수 있다.

import {useTestStore} from "zustand";

interface I_TestStore {
    Count: number;
    Text: string;
    Increase: (state: number) => void;
    TextChange: (newValue: string) => void;
};

const useTestStore = create<I_TestStore>((set, get) => ({
    Count: 0,
    Text: "",
    Increase: () => set((state) => ({Count: state.Count + 1})),
    TextChange: (newValue) => set(() => ({Text: newValue}))
}));

function App() {
    const { Count, Text, Increase, TextChange } = useTestStore();

    const TextSwitching = () => {
        if (Text !== "") {
        	TextChange("");
        } else {
        	TextChange("Hello");
        }
    };

    return (
        <div>
            <h3>Click Button's</h3>
            <div className="BtnBox">
                <button onClick={() => Increase(Count + 1)}>Count +</button>
                <button onClick={TextSwitching}>{Text !== "" ? "Hide" : "Show"}</button>
            </div>
            <div>
                <h4>Count: {Count}</h4>
                <h4>Text: {Text}</h4>
            </div>
        </div>
    );
}

예제 실행 결과

예제에서 'Count +' 버튼을 클릭하면, TestStore의 Count 상태의 값이 1 증가하고

상태의 값이 변경됐기 때문에, 상태 Count와 연결된 컴포넌트가 Re-Rendering된다.

 

반대로 'Show(Hide)' 버튼을 클릭하면, TestStore의 상태 Text의 값이 "Hello"나 "" 중 하나로 바뀌고

마찬가지로 상태의 값이 변경됐기 때문에 상태 Text와 연결된 컴포넌트가 Re-rendering된다.

 

이를 통해서 Store에서 필요한 상태만 가져오고, 해당 상태의 값이 업데이트되면

해당 상태와 연결된 컴포넌트만 선택적으로 Re-rendering되는 것을 알 수 있다.


③ 상태 Update

import {create} from "zustand";

const useTestStore = create((set, get) => ({
    Count: 0,
    setCount: () => set((state) => ({Count: state.count + 1})), //O
    /**
    * get(): 상태의 값을 가져오는 함수
    */
}))

 

Zustand의 Store는 'set()' 함수를 통해서 상태를 수정하는데

Zustand의 전역 상태, Store는 객체의 형태를 하고 있기 때문에

 

이를 수정하는 set() 함수또한 상태의 불변성을 유지하기 위해서

객체를 반환하는 형태로 코드를 작성해야한다.


④ 복잡한 상태 구조 다루기

 

Store, 상태의 구조가 중첩된 객체거나 배열 같이 복잡한 구조를 가지고 있다면

아래와 같은 방식으로 작성을 해서 상태의 불변성을 유지해야 한다.

 

* 중첩된 객체의 경우

const useProfileStore = create((set) => ({
    profile: {
        Name: "",
        Age: 0,
        userId: "",
        extraInfo: {
        	Favorites: "",
            /**
            * {Favorites, isMarried, HomeAddress} 등등이 있다고 가정
            */
        }	
    },
    //사용자가 새로 입력한 정보, 혹은 수정한
    updateExtraInfo: (newInfo) => set((state) => ({
        /**
         *'...'연산자를 통해 기존 정보, 새로 추가한 정보(newInfo)를 
         * 새로운 객체에 분해 할당, 기존 정보와 새로운 정보가 합쳐진
         * 새로운 객체를 상태의 값으로 업데이트한다.
        */
        profile: {
            ...state.profile,
            extraInfo: {
                ...state.profile.extraInfo,
                ...newInfo
            }
        }
    }))
}));

 

* 배열인 경우

const useArrStore = create((set) => ({
    Arrs: ["A", "B", "C"],
    
    //상태 Arrs에 새로운 데이터를 추가하는 addDatas 함수
    addDatas: (newData) => set((state) => ({
        Arrs: [...state.Arrs, newDatas]
    }))
}));

 

중첩 객체, 배열 어느 쪽이든 기존 값과 새로운 값을 '...'(Spread 연산자)를 통해서

객체나 배열 내부에 존재하는 데이터를 다 꺼내주고,

기존 값 + 새로운 값이 합쳐진 새로운 객체(배열)를 반환하는 것으로 상태의 불변성을 유지시킨다.