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 }), }));이 상태에는
count와text가 있음
❓
set({ count: 5 })를 실행하면?
count는 5로 바뀜text는"hello"그대로 유지됨- 즉,
count만 바꾸고 싶을 때text는 같이 보내지 않아도 됨
❓ 왜 그렇게 될까?
- Zustand는
set()을 호출할 때 내부적으로Object.assign(prevState, partial)같은 식으로 기존 상태에 덮어쓰기(merge)를 함- 즉, 전체 상태를 완전히 새로 만드는 게 아니라 일부만 바꿔서 합쳐줌
다만 immer나 shallow 없이 깊은 객체를 수정할 때는 주의가 필요함
얕은 복사 (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) => ({ ... }))는 기존 상태에 의존하거나, 누적하거나, 조건 분기할 때 필수적으로 사용해야 함