이 글에서 정리하는 내용
Zustand에서 action은 별도의 action 객체를 만드는 절차가 아니라, store 안에 상태 변경 규칙을 함수로 모아두는 방식입니다. 필터 상태 예시를 기준으로 컴포넌트에 흩어진 변경 로직을 action으로 옮기는 기준을 정리합니다.
- 컴포넌트 안의 변경 로직이 길어지는 순간
- Zustand에서 action은 무엇을 의미하는가
- 필터 상태로 보는 action 분리 기준
- 컴포넌트는 action을 호출하는 쪽으로 가볍게 둔다
- action 이름과 책임 범위를 정하는 기준
- action이 많아질 때 확인할 부분
- 정리
컴포넌트 안의 변경 로직이 길어지는 순간

상태 변경 로직은 처음에는 컴포넌트 안에 두는 쪽이 더 빠르게 느껴집니다. 버튼을 클릭하면 값을 바꾸고, 체크박스를 누르면 boolean 값을 뒤집고, 초기화 버튼을 누르면 기본값으로 되돌리면 됩니다. 코드 몇 줄로 끝나는 상황에서는 굳이 함수를 따로 빼야 하는지 잘 와닿지 않습니다.
문제는 같은 규칙이 두 군데 이상 생길 때부터입니다. 상품 목록 화면에 검색어, 카테고리, 품절 여부 필터가 있다고 하면, 필터 변경 로직은 검색 영역, 모바일 바텀시트, 상단 요약 칩, 초기화 버튼에서 반복될 수 있습니다. 이때 각 컴포넌트가 직접 상태 구조를 알고 값을 조립하면, 필터 항목이 하나 늘어날 때마다 여러 파일을 같이 고쳐야 합니다.
아래 코드는 겉보기에는 크게 나쁘지 않아 보입니다. 하지만 컴포넌트가 필터 객체의 구조를 알고 있고이전 상태를 복사한 뒤 어떤 필드만 바꿀지 직접 결정합니다. 작은 화면에서는 넘어갈 수 있지만같은 필터를 다른 컴포넌트에서도 건드리는 순간 수정 지점이 늘어납니다.
컴포넌트가 변경 규칙을 직접 들고 있는 형태
import { create } from "zustand"; const useProductFilterStore = create((set) => ({ filters: { keyword: "", category: "all", onlySoldOut: false}, setFilters: (filters) => set({ filters })})); function ProductFilterPanel() { const filters = useProductFilterStore((state) => state.filters); const setFilters = useProductFilterStore((state) => state.setFilters); const handleSoldOutChange = () => { setFilters({ ...filters, onlySoldOut: !filters.onlySoldOut}); }; const handleReset = () => { setFilters({ keyword: "", category: "all", onlySoldOut: false}); }; return ( <div> <button type="button" onClick={handleSoldOutChange}> 품절 상품만 보기 </button> <button type="button" onClick={handleReset}> 필터 초기화 </button> </div> );
} 이 방식의 불편함은 코드가 틀렸다는 데 있지 않습니다. 변경 규칙이 컴포넌트에 노출된다는 점이 문제입니다. 을 토글하는 규칙, 필터를 기본값으로 되돌리는 규칙은 화면 모양보다 상태 정책에 가깝습니다. 이런 규칙이 여러 컴포넌트에 흩어지면 같은 일을 하는 코드가 조금씩 다른 형태로 늘어납니다.
Zustand에서 action은 무엇을 의미하는가
Zustand에서 action은 어렵게 받아들일 필요가 없습니다. store 안에 들어 있는 상태 변경 함수라고 보면 됩니다. Zustand store에는 문자열, 숫자, 객체 같은 값뿐 아니라 함수도 함께 둘 수 있습니다. 이 함수 안에서 을 호출하면 store의 상태가 변경됩니다.
Redux를 먼저 접한 경우에는 action이라는 단어 때문에 action type, payload, reducer, dispatch까지 떠올리기 쉽습니다. Zustand에서는 그 구조를 반드시 만들 필요가 없습니다. 프로젝트 스타일에 따라 비슷한 구조를 만들 수는 있지만, 기본 학습 단계에서는 “상태 변경 규칙에 이름을 붙인 함수”로 이해하는 편이 덜 헷갈립니다.
컴포넌트가 직접 상태 객체를 조립하는 대신, store가 변경 규칙을 갖게 만들면 화면 코드가 읽기 쉬워집니다. 화면은 “품절 필터를 토글한다”, “필터를 초기화한다”처럼 사용자 행동을 action으로 요청하고, 실제로 어떤 필드가 어떻게 바뀌는지는 store 내부에서 처리합니다.
상태와 action을 함께 둔 store
import { create } from "zustand"; const initialFilters = { keyword: "", category: "all", onlySoldOut: false}; export const useProductFilterStore = create((set) => ({ filters: initialFilters, setKeyword: (keyword) => set((state) => ({ filters: { ...state.filters, keyword}})), setCategory: (category) => set((state) => ({ filters: { ...state.filters, category}})), toggleSoldOut: () => set((state) => ({ filters: { ...state.filters, onlySoldOut: !state.filters.onlySoldOut}})), resetFilters: () => set({ filters: initialFilters })})); 이제 필터 변경 규칙은 store 안으로 들어갔습니다. 컴포넌트는 필터 객체의 전체 구조를 몰라도 됩니다. 검색어를 바꾸고 싶으면 를 호출하고, 품절 여부를 뒤집고 싶으면 을 호출하면 됩니다.
여기서 에 객체를 바로 넘기는 경우와 함수를 넘기는 경우를 구분해서 보면 좋습니다. 이전 상태를 참고하지 않아도 되는 초기화는 처럼 바로 작성할 수 있습니다. 반대로 기존 값의 일부를 유지하거나 boolean 값을 반전해야 한다면 형태로 이전 상태를 기준으로 새 상태를 만드는 쪽이 맞습니다.
중첩 객체를 다룰 때도 한 번 더 확인해야 합니다. 은 store의 최상위 상태를 갱신하는 흐름으로 쓰이기 때문에만 바꾸고 싶다면 기존 내부 값을 직접 보존해줘야 합니다. 그래서 위 코드처럼 를 펼친 뒤 필요한 필드만 바꾸는 형태가 자주 나옵니다.
필터 상태로 보는 action 분리 기준
action을 만들 때 모든 함수를 같은 기준으로 나누면 오히려 store가 복잡해질 수 있습니다. 먼저 상태 변경의 성격을 나눠서 보면 판단이 쉬워집니다. 값 하나를 바꾸는 action, 현재 값을 기준으로 뒤집는 action, 여러 값을 한 번에 되돌리는 action은 서로 역할이 다릅니다.
값 하나를 바꾸는 action
검색어와 카테고리처럼 사용자가 선택한 값이 그대로 상태에 들어가는 경우에는 처럼 값 중심 이름을 쓰기 좋습니다. 이런 action은 인자로 받은 값을 상태에 반영하는 역할만 맡습니다.
setKeyword: (keyword) => set((state) => ({ filters: { ...state.filters, keyword}})) 이런 action은 단순하지만 의미가 있습니다. 컴포넌트마다 를 반복하지 않게 만들고, 검색어 변경 규칙이 나중에 바뀌어도 store 한 곳에서 수정할 수 있습니다.
현재 값을 기준으로 바꾸는 action
토글은 이전 상태를 반드시 봐야 합니다. 품절 여부, 모달 열림 여부, 선택 여부처럼 true와 false가 오가는 상태는 컴포넌트가 직접 값을 뒤집기보다 store action으로 옮기는 쪽이 깔끔합니다.
toggleSoldOut: () => set((state) => ({ filters: { ...state.filters, onlySoldOut: !state.filters.onlySoldOut}})) 이름도 보다 이 더 잘 맞는 경우가 있습니다. 은 외부에서 값을 직접 넘긴다는 느낌이고은 현재 값을 반대로 바꾼다는 규칙이 드러납니다. action 이름은 짧은 문법 문제가 아니라 나중에 코드를 읽는 기준이 됩니다.
여러 값을 한 번에 되돌리는 action
초기화는 action으로 분리했을 때 효과가 더 분명합니다. 필터 기본값이 여러 필드로 구성되어 있으면, 컴포넌트마다 초기값 객체를 반복해서 작성하기 쉽습니다. 기본값이 바뀌면 반복된 초기화 코드가 그대로 수정 대상이 됩니다.
const initialFilters = { keyword: "", category: "all", onlySoldOut: false}; resetFilters: () => set({ filters: initialFilters }) 초기값을 로 분리해두면 action에서도 재사용할 수 있고, 테스트하거나 수정할 때도 기준점이 명확합니다. 특히 관리자 화면이나 검색 화면처럼 필터 항목이 늘어나는 UI에서는 초기화 규칙을 한 곳에 두는 것이 나중에 차이를 만듭니다.
컴포넌트는 action을 호출하는 쪽으로 가볍게 둔다
action을 store로 옮기면 컴포넌트의 역할이 바뀝니다. 컴포넌트는 사용자의 이벤트를 받고, 필요한 상태와 action을 꺼내서 연결합니다. 상태 객체를 어떻게 복사할지, 어떤 필드를 유지해야 할지, 초기값이 무엇인지는 컴포넌트의 주된 관심사가 아닙니다.
function ProductFilterPanel() { const filters = useProductFilterStore((state) => state.filters); const setKeyword = useProductFilterStore((state) => state.setKeyword); const setCategory = useProductFilterStore((state) => state.setCategory); const toggleSoldOut = useProductFilterStore((state) => state.toggleSoldOut); const resetFilters = useProductFilterStore((state) => state.resetFilters); return ( <section> <input value={filters.keyword} onChange={(event) => setKeyword(event.target.value)} placeholder="상품명 검색" /> <select value={filters.category} onChange={(event) => setCategory(event.target.value)} > <option value="all">전체</option> <option value="outer">아우터</option> <option value="top">상의</option> </select> <button type="button" onClick={toggleSoldOut}> 품절 상품만 보기 </button> <button type="button" onClick={resetFilters}> 초기화 </button> </section> );
} 이 구조에서는 이벤트 핸들러 안에 상태 변경 규칙이 거의 남지 않습니다. 입력값을 읽어서 에 넘기거나, 버튼 클릭 시 을 호출하는 정도만 남습니다. 화면 코드를 읽을 때도 “어떤 UI가 어떤 action을 호출하는지”가 먼저 보입니다.
다만 action을 분리한다고 해서 모든 상태를 전역 store로 옮겨야 하는 것은 아닙니다. 한 컴포넌트 내부에서만 쓰이는 드롭다운 열림 여부나 임시 입력값은 가 더 단순할 수 있습니다. Zustand action으로 옮길 만한 상태는 여러 컴포넌트에서 공유되거나, 변경 규칙을 한 곳에 모아야 할 이유가 있는 상태입니다.
action 이름과 책임 범위를 정하는 기준
action 이름은 나중에 store를 다시 읽을 때 생각보다 큰 영향을 줍니다. 이름이 모호하면 컴포넌트에서는 짧아 보여도 store 내부를 계속 확인해야 합니다. 반대로 이름이 상태 변경 의도를 잘 드러내면, 화면 코드만 보고도 흐름을 어느 정도 따라갈 수 있습니다.
| 상황 | 이름 예시 | 판단 기준 |
|---|---|---|
| 값을 그대로 반영 | setKeyword, setCategory | 인자로 받은 값을 특정 상태에 넣는 경우 |
| 현재 값을 반대로 변경 | toggleSoldOut, toggleModal | 이전 상태를 기준으로 true/false를 바꾸는 경우 |
| 기본값으로 되돌림 | resetFilters, clearSelectedItems | 여러 상태를 정해진 기준으로 초기화하는 경우 |
| 여러 변경을 한 번에 처리 | applyFilterPreset | 단순 set보다 하나의 사용자 행동에 가까운 경우 |
은 값 중심 이름입니다. 입력값, 선택값처럼 외부에서 받은 값을 그대로 반영할 때 어울립니다. 은 현재 상태를 기준으로 반전할 때 어울립니다.은 상태를 비우거나 초기값으로 되돌릴 때 의미가 분명합니다.
주의할 부분은 action 하나에 너무 많은 책임을 넣는 경우입니다. 예를 들어 라는 action 하나에 검색어 변경, 카테고리 변경, 품절 토글, 초기화까지 모두 넣으면 컴포넌트 코드는 짧아질 수 있습니다. 하지만 action 내부에 조건문이 늘어나면서 결국 store가 읽기 어려워집니다.
처음에는 action을 너무 잘게 나누기보다, 실제 사용자 행동과 상태 변경 규칙을 기준으로 나누면 됩니다. “검색어를 바꾼다”, “품절 필터를 토글한다”, “필터를 초기화한다”처럼 화면에서 일어나는 일이 그대로 이름에 드러나면 적당한 출발점입니다.
action이 많아질 때 확인할 부분

action을 store 안에 모으다 보면 어느 순간 함수가 많아집니다. 이때 바로 복잡한 구조로 넘어가기보다 먼저 확인할 부분이 있습니다. action이 많아진 이유가 상태 자체가 많아서인지, 하나의 store가 여러 기능을 동시에 담당해서인지 나눠서 봐야 합니다.
필터와 모달, 선택된 상품, 장바구니 상태가 모두 하나의 store에 들어 있다면 action이 많아지는 것은 자연스러운 결과입니다. 이 경우에는 action 이름을 더 줄이는 것보다 store의 책임을 나누는 쪽을 고민해야 합니다. 필터는 필터 store로, 장바구니는 장바구니 store로 나누면 action도 기능 단위로 읽힙니다.
반대로 하나의 기능 안에서 action이 많아지는 경우도 있습니다. 필터 기능만 봐도처럼 늘어날 수 있습니다. 이때는 action이 많다는 사실보다 각 action이 서로 다른 사용자 행동을 표현하는지 확인하는 것이 우선입니다.
Zustand는 정해진 폴더 구조를 강제하지 않습니다. 이 자유도가 장점이지만, 처음 공부할 때는 기준이 없어서 오히려 헷갈릴 수 있습니다. action을 분리하는 단계에서는 먼저 한 store 안에서 상태와 변경 함수를 함께 관리해보고, 기능이 커질 때 store 파일을 나누는 식으로 넘어가면 흐름이 덜 끊깁니다.
정리
Zustand의 action은 상태 관리를 어렵게 만드는 추가 문법이 아닙니다. 컴포넌트에 흩어지기 쉬운 상태 변경 규칙을 store 안에 함수로 모아두는 방식입니다. 컴포넌트가 직접 객체를 복사하고 값을 조립하는 코드가 늘어난다면, 그 시점이 action으로 분리할 만한 지점입니다.
값 하나를 바꾸는 로직은 처럼 단순하게 둘 수 있고, 현재 상태를 기준으로 바꾸는 로직은 처럼 동작이 드러나게 이름을 붙일 수 있습니다. 여러 값을 한 번에 되돌리는 초기화 로직은 action으로 분리했을 때 특히 효과가 큽니다.
다음에 Zustand 코드를 다시 볼 때는 store 안에 상태만 있는지, 아니면 상태를 바꾸는 규칙까지 함께 정리되어 있는지 확인하면 됩니다. action이 제대로 나뉘면 컴포넌트는 화면 이벤트를 연결하는 역할에 가까워지고, 상태 변경의 기준은 store에서 한 번에 따라갈 수 있습니다.
같이 읽으면 좋은 글
- Zustand 학습 순서: store, action, selector, persist까지
- React 처음 배우는 순서: 컴포넌트부터 state, Zustand까지
- Zustand 리렌더링 문제 해결: 상태 변경 후 화면이 바뀌지 않을 때