[TIL-0511] Zustand set() 함수의 사용 방식 2가지

jiny·2025년 5월 20일

캡스톤2

목록 보기
8/22

🌟 zustand store에서 set() 함수 정의

zustand 예제를 보면, set() 함수를 정의하는 방법이 크게 두 가지로 나뉜다.

방법 1: state 없이 바로 상태 업데이트 (정적 업데이트)

set({ isLogin: true })

방법 2: state를 통해 상태 업데이트 (동적 업데이트)

set((state) => ({ bears: state.bears + 1 }))

이 둘의 차이점이 무엇인지 알아보자.


🌟 정적 업데이트

  • 개념

    • 상태를 고정된 값으로 직접 설정하는 방식
    • 이전 상태와 상관없이 덮어씀
    set({ inLogin: true });
  • 사용 예시

    const useAuthStore = create((set) => ({
      isLogin: false,
      login: () => set({ isLogin: true }),
      logout: () => set({ isLogin: false }),
    }));
  • 특징

    • 새로운 값을 직접 지정해서 상태를 덮어씀

    • 이전 상태(state)를 참조하지 않음

    • 이전 상태가 필요 없을 때 간단하게 사용 가능함

    • 기존 상태 중 일부만 설정해도, 나머지는 자동으로 유지됨(전체 상태가 사라지지 않음)

      const useStore = create((set) => ({
        count: 0,
        text: "hello",
        setCount: (newCount: number) => set({ count: newCount }),
      }));

      이 상태에는 counttext가 있음

      set({ count: 5 })를 실행하면?

      • count는 5로 바뀜
      • text"hello" 그대로 유지됨
      • 즉, count만 바꾸고 싶을 때 text는 같이 보내지 않아도 됨

      ❓ 왜 그렇게 될까?

      • Zustand는 set()을 호출할 때 내부적으로 Object.assign(prevState, partial) 같은 식으로 기존 상태에 덮어쓰기(merge)를 함
      • 즉, 전체 상태를 완전히 새로 만드는 게 아니라 일부만 바꿔서 합쳐줌
    • 다만 immershallow 없이 깊은 객체를 수정할 때는 주의가 필요함

      얕은 복사 (Shallow Merge)

      const prevState = {
        user: {
          name: "Jiny",
          age: 20,
        },
        count: 0,
      };
      
      const newState = {
        user: { name: "Kim" }, // user 일부만 변경
      };
      
      const merged = { ...prevState, ...newState };
      
      // merged 결과
      {
        user: { name: "Kim" }, // age는 사라짐
        count: 0,
      }
      • Zustand의 set()은 위와 같이 얕게 병합
      • { ...prevState, ...newState }최상위 키 기준으로만 병합
      • user는 객체지만, 하위 프로퍼티까지 병합하지 않음
      • 즉, user라는 전체 키를 통째로 덮어쓰기만 할 수 있을 뿐, 그 안에 있는 name만 바꾸는 식으로는 작동하지 않음

      ❗문제 사례: 깊은 객체 수정 시 발생할 수 있는 실수

      const useStore = create((set) => ({
         user: {
           name: "Jiny",
           age: 20,
         },
         updateName: (newName: string) => set({ user: { name: newName }}), // 이 방식은 age를 덮어씀
       }))
      • 예상과 다른 결과: age가 사라지고 다음과 같이 바뀜
        user = {
           name: "new Name"
         }; // age 없음

      해결 방법 1: 이전 상태를 사용해서 수동 병합

      updateName: (newName: string) => set((state) => ({
        user: {
          ...state.user, // 기존 user 전체 복사
          name: newName, // 이름만 바꾸기
        },
      })),

      결과

       user = {
         name: "new name",
         age: 20, // 유지됨
       }

      해결 방법 2: immer 사용 (Zustand 내장 미들웨어)

      import { create } from "zustand";
      import { immer } from "zustand/middleware/immer";
      
      const useStore = create(
        immer((set) => ({
          user: {
            name: "Jiny",
            age: 20,
          },
          updateName: (newName: string) => set((state) => {
            state.user.name = newName; // 직관적으로 수정 가능
          }),
        }))
      );
      • immer를 쓰면 직접 객체를 수정하는 문법처럼 쓸 수 있으면서도 내부적으로는 불변성을 지킴
      • 깊은 객체 수정이 매우 편리해짐
  • 사용 상황

    • 상태의 새로운 값이 항상 고정값일 때
    • 이전 상태를 신경 쓸 필요가 없을 때

🌟 동적 업데이트

  • 개념: 상태를 이전 상태를 기반으로 계산해서 설정하는 방식

  • 사용 예시

    const useBearStore = create((set) => ({
      bears: 0,
      increase: () => set((state) => ({ bears: state.bears + 1 })),
    }));
  • 특징

    • 이전 상태(state)를 받아서 그걸 기반으로 새로운 상태를 계산
    • 상태 변경이 이전 상태에 의존할 때 반드시 이 방식을 사용해야 함
    • 동기화 문제 방지에 도움됨 (예: 상태 변경이 비동기적으로 여러 번 일어날 경우)
    • 리액트의 setState((prev) => newValue)와 거의 동일한 개념
  • 사용 상황

    • 상태를 누적하거나 조건에 따라 변경할 때
    • 동시성 문제를 방지하고 싶을 때
  • 동적 업데이트가 중요한 이유

    • React나 Zustand에서는 여러 상태 업데이트가 비동기적으로 일어날 수 있음

    • 이럴 때 set((state) => ...) 방식은 항상 최신 상태를 기준으로 변경해주기 때문에 안전함

    • 예시: 누적이 필요한 경우

      // ❌ 동시 호출 시 문제 생길 수 있음
      increment: () => set({ count: useStore.getState().count + 1});
      
      // ✅ 안전하게 이전 상태 기준으로 업데이트
      increment: () => set((state) => ({ count: state.count + 1 }));

🌟 왜 두 가지 방식이 있을까?

  • set({ ... })간단한 값 설정
  • set((state) => { ... })이전 상태를 이용해 계산할 때 필요
  • 다음과 같은 경우에는 꼭 함수 형태를 써야 함

    // ❌ 잘못된 방식
    set({ bears: bears + 1 }); // 이 bears 값은 스냅샷이라 최신 상태가 아닐 수 있음
    
    // ✅ 올바른 방식
    set((state) => ({ bears: state.bears + 1 }));

🌟 비교표

구분set({ ... })set((state) => ({ ... }))
설명상태를 고정 값으로 바꿀 때 사용이전 상태를 기준으로 새 상태 계산
상태 의존성없음있음
업데이트 방식고정된 값으로 직접 설정이전 상태 기반으로 계산
동시성 안전성취약안전
특징현재 상태를 보지 않고, 값을 "그대로 대입"상태 변경 시 "이전 상태" 필요할 때 사용
예시set({ isLogin: true })set((state) => ({ count: state.count + 1 }))

🌟 결론

  • set({ ... })단순한 고정 상태 설정일 때 사용해도 무방함
  • set((state) => ({ ... }))기존 상태에 의존하거나, 누적하거나, 조건 분기할 때 필수적으로 사용해야 함

0개의 댓글