diff --git a/02-state-management-tools/2.2-Zustand/README.md b/02-state-management-tools/2.2-Zustand/README.md
new file mode 100644
index 0000000..c40ef92
--- /dev/null
+++ b/02-state-management-tools/2.2-Zustand/README.md
@@ -0,0 +1,46 @@
+# 02-2. 전역상태관리 - Zustand
+
+## 🎯 요구사항
+
+- Context API로 구성한 애플리케이션을 Zustand 기반 전역 상태로 마이그레이션하세요.
+- props에 대한 요구사항은 2-1 요구사항과 같습니다.
+- Zustand를 **왜** 사용하는지, Context API와 비교했을때 어떤 점이 달랐는지, 또 trade-off가 있는지 적어주세요.
+ - 기술적인 것도 좋고 개발자의 경험 측면에서도 좋습니다.
+- (선택): 카테고리 필터의 선택된 카테고리가 새로고침 후에도 유지되도록 구현해보세요
+
+### 😗구현 예시
+
+- 컴포넌트의 이름이나 구조를 정한 이유가 명확해야하며 타인에게 설명할 수 있어야합니다.
+- 아래 코드는 Zustand 스토어를 설정하는 예시입니다.
+
+```javascript
+import { create } from "zustand";
+
+// Zustand 스토어 예시
+export const useBear = create((set) => ({
+ // state
+ bears: 0,
+
+ // actions
+ increasePopulation: () => set((state) => ({ bears: state.bears + 1 })),
+ removeAllBears: () => set({ bears: 0 }),
+ updateBears: (newBears) => set({ bears: newBears }),
+}));
+```
+
+## ✅ 키워드
+
+- props drilling
+- 전역상태관리
+ - Zustand
+ - create
+ - set / get
+
+## 🧙♀️ 진행 가이드
+
+- 진행시간 : 2시간 내에 완료하는 것을 목표로 합니다.
+
+## 🔗 참고 문서
+
+- [Zustand 공식문서](https://recoiljs.org/docs/introduction/installation/)
+- [Zustand와 React Context](https://tkdodo.eu/blog/zustand-and-react-context)
\ No newline at end of file
diff --git a/README.md b/README.md
index 20db4bc..76b832a 100644
--- a/README.md
+++ b/README.md
@@ -1,169 +1,155 @@
-# Context API를 사용해서 전역상태관리하기
+# Zustand를 사용해서 전역 상태 관리하기
## 🎯 개인 목표 및 목표 달성을 위한 행동 가이드
이번 미션을 통해 다음과 같은 학습 경험들을 쌓는 것을 목표로 한다.
-1. `createContext`, `Provider`, `useContext` 세 가지 개념의 역할을 명확히 이해하고 직접 구현한다.
-2. props drilling이 발생하는 지점을 코드에서 직접 파악하고 Context로 해결하는 경험을 쌓는다.
-3. UI 상태와 데이터 상태를 구분해서 Context 적용 범위를 스스로 판단하는 능력을 기른다.
+1. Context API로 구현된 앱을 Zustand로 마이그레이션하면서 두 방식의 차이를 체감한다.
+2. `create`, `set`, `get`, selector 개념을 직접 사용하며 Zustand 스토어 구조를 익힌다.
+3. 전역 상태로 관리할 것과 로컬 상태로 유지할 것을 스스로 판단하는 능력을 기른다.
---
## 📝 기능 구현 목록
-- [x] `RestaurantsContext` 생성 및 `RestaurantsProvider` 구현
-- [x] `useRestaurants` 훅의 데이터(`restaurants`, `addRestaurant`, `isLoading`, `error`)를 Context로 관리
-- [x] `RestaurantList`에서 `useContext`로 `restaurants`, `isLoading`, `error` 직접 구독
-- [x] `AddRestaurantModal`에서 `useContext`로 `addRestaurant` 직접 호출 (`onSubmit` prop 제거)
-- [x] `App.jsx`에서 음식점 관련 state/handler 제거, UI 상태(`selectedCategory`, `clickedRestaurant`, `isAddRestaurantModalOpen`)만 유지
+- [x] `useRestaurantStore` 생성 — `restaurants`, `addRestaurant`, `isLoading`, `error`, `fetchRestaurants` 포함
+- [x] `RestaurantList`에서 selector로 스토어 구독, `useEffect`로 초기 데이터 fetch
+- [x] `AddRestaurantModal`에서 스토어의 `addRestaurant` 직접 호출
+- [x] `App.jsx`에서 `RestaurantsProvider` 제거
+- [x] `selectedCategory`를 스토어로 이동 및 `persist` 미들웨어로 새로고침 후에도 유지
---
## 📚 학습 내용
-### Context API란?
+### Zustand 핵심 개념
-컴포넌트 트리 전체에 데이터를 전달할 수 있는 React 내장 기능이다. props를 통해 중간 컴포넌트를 거치지 않고, 필요한 컴포넌트가 데이터를 직접 꺼내 쓸 수 있게 한다.
-
-### createContext / Provider / useContext
-
-세 가지 API가 각각 다른 역할을 담당한다.
-
-| 개념 | 역할 | 설명 |
-|---|---|---|
-| `createContext` | Context 객체 생성 | 데이터를 담을 컨테이너를 정의한다. 초기값을 인자로 받으며, Provider 없이 `useContext`를 호출했을 때 이 초기값이 반환된다. |
-| `Provider` | Context 값 공급 | `value` prop으로 전달한 데이터를 하위 컴포넌트 트리 전체에 주입한다. `value`가 변경되면 해당 Context를 구독 중인 모든 컴포넌트가 리렌더링된다. |
-| `useContext` | Context 값 구독 | 가장 가까운 상위 Provider의 `value`를 반환한다. Provider가 없으면 `createContext`의 초기값을 반환한다. |
+| 개념 | 설명 |
+|---|---|
+| `create` | 스토어를 생성한다. 반환값이 훅이라 `useRestaurantStore()`로 바로 사용한다. |
+| `set` | 상태를 업데이트한다. 얕은 병합(shallow merge)이라 바꾸지 않는 필드는 그대로 유지된다. |
+| `get` | 액션 안에서 현재 스토어 상태를 읽거나 다른 액션을 호출할 때 사용한다. |
+| selector | `useStore((state) => state.xxx)` 형태로 필요한 상태만 구독한다. 해당 값이 바뀔 때만 리렌더링된다. |
```js
-// 1. createContext — 공간 생성
-export const RestaurantsContext = createContext(null);
-
-// 2. Provider — 데이터를 채워서 하위 컴포넌트에 공급
-export function RestaurantsProvider({ children }) {
- const { restaurants, addRestaurant, isLoading, error } = useRestaurants();
- return (
-
- {children}
-
- );
-}
-
-// 3. useContext — 필요한 컴포넌트에서 직접 꺼냄
-const { restaurants } = useContext(RestaurantsContext);
+const useRestaurantStore = create((set, get) => ({
+ // 상태
+ restaurants: [],
+ isLoading: false,
+ error: null,
+
+ // 액션
+ fetchRestaurants: async () => {
+ set({ isLoading: true, error: null });
+ try {
+ const data = await getRestaurants();
+ set({ restaurants: data });
+ } catch {
+ set({ error: "음식점 목록을 불러오지 못했습니다." });
+ } finally {
+ set({ isLoading: false });
+ }
+ },
+ addRestaurant: async (restaurant) => {
+ await createRestaurant(restaurant);
+ await get().fetchRestaurants(); // 다른 액션 호출
+ },
+}));
```
-### React 18 vs React 19 Provider 문법 차이
+### persist 미들웨어
-React 19부터는 Context 객체 자체를 Provider로 사용할 수 있다. 공식문서가 React 19 기준으로 업데이트되었으므로 버전 확인이 필요하다.
+스토어의 상태를 localStorage에 자동으로 저장/복원해주는 Zustand 내장 미들웨어다. `partialize`로 저장할 상태만 선택할 수 있다.
-```jsx
-// React 18 — .Provider 필요
-
-
-// React 19 — Context 자체를 Provider로 사용 가능
-
+```js
+const useRestaurantStore = create(
+ persist(
+ (set, get) => ({ ... }),
+ {
+ name: "restaurant-storage",
+ partialize: (state) => ({ selectedCategory: state.selectedCategory }),
+ }
+ )
+);
```
+`restaurants`는 서버에서 매번 가져오므로 저장할 필요가 없고, `selectedCategory`만 persist 대상으로 지정했다.
+
---
-## 🤔 고민했던 문제와 해결 과정에서 배운 점
+## 🤔 Zustand를 왜 사용하는가 — Context API와 비교
-### 무엇을 Context에 넣을 것인가
+### Context API는 전역 상태 관리 도구가 아니다
-Context에 모든 state를 넣는 게 아니라, **데이터 도메인**과 **UI 상태**를 구분하는 것이 핵심이다.
+Context API는 원래 **prop drilling 해결 도구**다. 상태 관리를 하려면 `useState`/`useReducer`를 별도로 조합해야 하고, 그 결과물을 Provider로 감싸야 한다. 이번 마이그레이션에서 `useRestaurants` 훅 + `RestaurantsContext` + `useRestaurantsContext` 세 파일이 `useRestaurantStore` 하나로 줄어든 게 그 차이다.
-| 구분 | 상태 | 이유 |
-|---|---|---|
-| Context (데이터 도메인) | `restaurants`, `addRestaurant`, `isLoading`, `error` | 여러 컴포넌트에서 공유되는 서버 데이터 |
-| props / 로컬 state (UI 상태) | `selectedCategory`, `clickedRestaurant`, `isAddRestaurantModalOpen` | 특정 화면의 인터랙션 상태로, App이 관리하는 게 자연스러움 |
+### 달랐던 점
-판단 기준은 "그 데이터가 App 고유의 책임인가?"다. `addRestaurant`는 음식점 데이터 도메인의 책임이므로 Context가 적합하고, `selectedCategory`는 화면 UI의 책임이므로 로컬 state가 적합하다.
+**Provider가 없다**
-### Provider 위치 결정
+Context는 ``로 트리를 감싸야 했지만, Zustand는 Provider 없이 어떤 컴포넌트에서든 스토어에 바로 접근한다.
-처음에는 `RestaurantsProvider`를 App의 return 안에 배치했다. 이 경우 App 자신이 `useContext`로 Context 데이터를 꺼낼 수 없다는 문제가 생긴다. `useContext`는 자신보다 **상위에 있는** Provider를 찾기 때문이다.
+**리렌더링 최적화**
-해결책은 두 가지였다.
+Context는 value 안의 어떤 값이 바뀌어도 해당 Context를 구독하는 모든 컴포넌트가 리렌더링된다. Zustand는 selector로 필요한 상태만 구독하기 때문에, `restaurants`가 바뀌어도 `addRestaurant` 액션만 구독하는 컴포넌트는 리렌더링되지 않는다.
-1. Provider를 `main.jsx`로 올려서 App도 Context에 접근 가능하게 만들기
-2. App이 Context를 쓸 필요가 없도록 구조를 바꾸기
+```js
+// AddRestaurantModal — addRestaurant만 구독하므로
+// restaurants, isLoading, error가 바뀌어도 리렌더링되지 않는다
+const addRestaurant = useRestaurantStore((state) => state.addRestaurant);
+```
-`AddRestaurantModal`이 `addRestaurant`를 Context에서 직접 꺼내도록 하면 App은 `addRestaurant`를 전혀 알 필요가 없어진다. `isLoading`, `error`도 `RestaurantList`가 직접 보여주면 된다. 결과적으로 App이 Context를 쓰지 않아도 되는 구조가 만들어졌고, Provider 위치 문제도 자연스럽게 해결됐다.
+### Trade-off
-### filteredRestaurants를 어디서 처리할 것인가
+**Context가 나은 경우**
-기존에는 `App`이 `filteredRestaurants`를 계산해서 `RestaurantList`에 내려줬다. Context 도입 후 `restaurants`는 Context에서 오고, `selectedCategory`는 UI 상태로 props로 전달하게 됐다.
+- 외부 라이브러리 없이 React만으로 해결 가능
+- 테마, 로케일처럼 변경이 거의 없는 정적 값은 Context가 오히려 적합
+- Provider 범위로 상태의 생명주기가 명확하게 제어되어야 할 때
-```
-// 변경 후
-RestaurantList에서 Context로 restaurants를 꺼내고,
-props로 받은 selectedCategory로 필터링을 직접 처리
-```
+**Zustand의 단점**
-`selectedCategory`를 props로 유지한 이유: UI 상태이므로 Context보다 props가 더 자연스럽다. 미션 요구사항에도 "props를 쓴다면 그 이유를 PR에 적어주세요"라고 명시되어 있으므로, 이 판단 근거를 기록한다.
+- 스토어가 전역이라 어디서든 접근 가능한 게 장점이지만, 반대로 상태가 어디서 변경되는지 추적하기 어려워질 수 있다
+- Context는 Provider 범위로 상태의 생명주기가 명확한 반면, Zustand 스토어는 앱 전체에서 살아있다
---
-## 🛠 리팩토링
+## 🤔 고민했던 문제와 해결 과정에서 배운 점
-### App.jsx — 음식점 데이터 책임 제거
+### 무엇을 스토어에 넣을 것인가
-Context 도입 전 App은 `useRestaurants`를 직접 호출하고, 모든 handler를 가지고 있었다. 리팩토링 후 App은 UI 상태만 관리한다.
+스토어에 모든 상태를 넣는 게 아니라, **전역 상태가 필요한 조건**을 기준으로 판단했다.
-```js
-// before — App이 데이터와 UI 상태를 모두 관리
-const { restaurants, addRestaurant, isLoading, error } = useRestaurants();
-const filteredRestaurants = filterRestaurants(restaurants, selectedCategory);
-async function handleRestaurantSubmit(restaurant) { ... }
-
-// after — UI 상태만 남음
-const [selectedCategory, setSelectedCategory] = useState(ALL_CATEGORY);
-const [clickedRestaurant, setClickedRestaurant] = useState(null);
-const [isAddRestaurantModalOpen, setIsAddRestaurantModalOpen] = useState(false);
-```
+| 구분 | 상태 | 이유 |
+|---|---|---|
+| 스토어 (데이터 도메인) | `restaurants`, `addRestaurant`, `isLoading`, `error` | 여러 컴포넌트에서 공유되는 서버 데이터 |
+| 스토어 (persist 목적) | `selectedCategory` | UI 상태이지만 새로고침 후 유지를 위해 스토어로 이동 |
+| 로컬 state (UI 상태) | `clickedRestaurant`, `isAddRestaurantModalOpen` | 해당 컴포넌트에서만 쓰이는 인터랙션 상태 |
+
+`selectedCategory`는 본래 UI 상태이므로 `useState`가 자연스럽다. 다만 새로고침 후 유지(`persist`)는 Zustand 스토어에만 적용할 수 있기 때문에 기술적인 이유로 스토어로 이동했다. 이 판단은 목적이 명확하므로 정당하지만, persist 요구사항이 없었다면 로컬 state로 유지하는 것이 맞다.
+
+### fetchRestaurants 호출 위치
-### RestaurantList — Context 구독 및 필터링 내부화
+Zustand 스토어는 React 컴포넌트가 아니라 `useEffect`를 쓸 수 없다. 초기 데이터 fetch는 데이터를 보여주는 컴포넌트(`RestaurantList`)가 마운트될 때 `useEffect`로 호출하는 방식으로 해결했다.
```js
-// before
-export default function RestaurantList({ restaurants, onRestaurantClick }) { ... }
-
-// after
-export default function RestaurantList({ selectedCategory, onRestaurantClick }) {
- const { restaurants, isLoading, error } = useContext(RestaurantsContext);
- const filteredRestaurants = filterRestaurants(restaurants, selectedCategory);
- ...
-}
+const fetchRestaurants = useRestaurantStore((state) => state.fetchRestaurants);
+
+useEffect(() => {
+ fetchRestaurants();
+}, [fetchRestaurants]);
```
-### AddRestaurantModal — onSubmit prop 제거
+### 이벤트 핸들러와 스토어 액션의 분리
+
+스토어 액션은 순수한 값만 받도록 하고, 이벤트 객체 처리는 컴포넌트에 남겼다.
```js
-// before — App으로부터 onSubmit을 받아서 호출
-export default function AddRestaurantModal({ onSubmit, onClose }) {
- function handleFormSubmit(e) {
- e.preventDefault();
- onSubmit({ category, name, description });
- }
-}
+// 스토어 액션 — 값만 받는다
+setSelectedCategory: (category) => set({ selectedCategory: category }),
-// after — Context에서 addRestaurant를 직접 꺼내서 호출
-export default function AddRestaurantModal({ onClose }) {
- const { addRestaurant } = useContext(RestaurantsContext);
- async function handleFormSubmit(e) {
- e.preventDefault();
- try {
- await addRestaurant({ category, name, description });
- onClose();
- } catch {
- alert("음식점 추가에 실패했습니다. 다시 시도해주세요.");
- }
- }
+// 컴포넌트 — 이벤트에서 값을 꺼내는 건 UI 로직
+function handleCategoryChange(e) {
+ setSelectedCategory(e.target.value);
}
```
-
-### styled-components 선언 위치 — 컴포넌트 하단으로 이동
-
-파일을 열었을 때 가장 먼저 보고 싶은 건 컴포넌트 로직이지, 스타일 세부사항이 아니다. styled-components 선언이 상단에 있으면 컴포넌트 함수가 한참 아래로 밀려 가독성이 떨어진다. 모든 컴포넌트 파일에서 styled-components 선언을 컴포넌트 함수 아래로 이동했다.
diff --git a/package-lock.json b/package-lock.json
index e9995c3..d46a1e2 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -10,7 +10,8 @@
"dependencies": {
"react": "^18.2.0",
"react-dom": "^18.2.0",
- "styled-components": "^6.4.2"
+ "styled-components": "^6.4.2",
+ "zustand": "^5.0.14"
},
"devDependencies": {
"@types/react": "^18.2.66",
@@ -1189,13 +1190,13 @@
"version": "15.7.12",
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.12.tgz",
"integrity": "sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q==",
- "dev": true
+ "devOptional": true
},
"node_modules/@types/react": {
"version": "18.2.73",
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.73.tgz",
"integrity": "sha512-XcGdod0Jjv84HOC7N5ziY3x+qL0AfmubvKOZ9hJjJ2yd5EE+KYjWhdOjt387e9HPheHkdggF9atTifMRtyAaRA==",
- "dev": true,
+ "devOptional": true,
"dependencies": {
"@types/prop-types": "*",
"csstype": "^3.0.2"
@@ -4364,6 +4365,35 @@
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
+ },
+ "node_modules/zustand": {
+ "version": "5.0.14",
+ "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.14.tgz",
+ "integrity": "sha512-/8tAspM5LMPr28b3fwLYrtdj77ECpfZviaP75CMTnwO8ISyaE4GDIG/9rDDYq/cH9D2Xw2A2RXglLInmVBQB/g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.20.0"
+ },
+ "peerDependencies": {
+ "@types/react": ">=18.0.0",
+ "immer": ">=9.0.6",
+ "react": ">=18.0.0",
+ "use-sync-external-store": ">=1.2.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "immer": {
+ "optional": true
+ },
+ "react": {
+ "optional": true
+ },
+ "use-sync-external-store": {
+ "optional": true
+ }
+ }
}
}
}
diff --git a/package.json b/package.json
index f236331..fcd53d9 100644
--- a/package.json
+++ b/package.json
@@ -13,7 +13,8 @@
"dependencies": {
"react": "^18.2.0",
"react-dom": "^18.2.0",
- "styled-components": "^6.4.2"
+ "styled-components": "^6.4.2",
+ "zustand": "^5.0.14"
},
"devDependencies": {
"@types/react": "^18.2.66",
diff --git a/src/App.jsx b/src/App.jsx
index 85e4ae6..3531734 100644
--- a/src/App.jsx
+++ b/src/App.jsx
@@ -5,11 +5,15 @@ import RestaurantList from "./components/RestaurantList.jsx";
import { useState } from "react";
import RestaurantDetailModal from "./components/RestaurantDetailModal.jsx";
import AddRestaurantModal from "./components/AddRestaurantModal.jsx";
-import { ALL_CATEGORY } from "./constants/categories.js";
-import { RestaurantsProvider } from "./context/RestaurantsContext.jsx";
+import useRestaurantStore from "./store/useRestaurantStore.js";
function App() {
- const [selectedCategory, setSelectedCategory] = useState(ALL_CATEGORY);
+ const selectedCategory = useRestaurantStore(
+ (state) => state.selectedCategory,
+ );
+ const setSelectedCategory = useRestaurantStore(
+ (state) => state.setSelectedCategory,
+ );
const [clickedRestaurant, setClickedRestaurant] = useState(null);
const [isAddRestaurantModalOpen, setIsAddRestaurantModalOpen] =
useState(false);
@@ -36,7 +40,7 @@ function App() {
}
return (
-
+ <>
)}
-
+ >
);
}
diff --git a/src/components/AddRestaurantModal.jsx b/src/components/AddRestaurantModal.jsx
index d1639dc..2d9e4c5 100644
--- a/src/components/AddRestaurantModal.jsx
+++ b/src/components/AddRestaurantModal.jsx
@@ -2,13 +2,14 @@ import { useState } from "react";
import Modal from "./Modal.jsx";
import { CATEGORIES } from "../constants/categories.js";
import styled from "styled-components";
-import { useRestaurantsContext } from "../context/useRestaurantsContext.js";
+import useRestaurantStore from "../store/useRestaurantStore.js";
export default function AddRestaurantModal({ onClose }) {
const [category, setCategory] = useState("");
const [name, setName] = useState("");
const [description, setDescription] = useState("");
- const { addRestaurant } = useRestaurantsContext();
+
+ const addRestaurant = useRestaurantStore((state) => state.addRestaurant);
async function handleFormSubmit(e) {
e.preventDefault();
diff --git a/src/components/RestaurantList.jsx b/src/components/RestaurantList.jsx
index 21a0e93..1e4a969 100644
--- a/src/components/RestaurantList.jsx
+++ b/src/components/RestaurantList.jsx
@@ -1,15 +1,26 @@
import { CATEGORY_IMAGES } from "../constants/categoryImages.js";
import styled from "styled-components";
-import { useRestaurantsContext } from "../context/useRestaurantsContext.js";
import { filterRestaurants } from "../utils/filterRestaurants.js";
+import useRestaurantStore from "../store/useRestaurantStore.js";
+import { useEffect } from "react";
export default function RestaurantList({
selectedCategory,
onRestaurantClick,
}) {
- const { restaurants, isLoading, error } = useRestaurantsContext();
+ const restaurants = useRestaurantStore((state) => state.restaurants);
+ const isLoading = useRestaurantStore((state) => state.isLoading);
+ const error = useRestaurantStore((state) => state.error);
+ const fetchRestaurants = useRestaurantStore(
+ (state) => state.fetchRestaurants,
+ );
+
const filteredRestaurants = filterRestaurants(restaurants, selectedCategory);
+ useEffect(() => {
+ fetchRestaurants();
+ }, [fetchRestaurants]);
+
return (
<>
{isLoading && 불러오는 중...
}
diff --git a/src/context/RestaurantsContext.jsx b/src/context/RestaurantsContext.jsx
deleted file mode 100644
index 73f1d72..0000000
--- a/src/context/RestaurantsContext.jsx
+++ /dev/null
@@ -1,15 +0,0 @@
-import { createContext } from "react";
-import { useRestaurants } from "../hooks/useRestaurants.js";
-
-export const RestaurantsContext = createContext(null);
-
-export function RestaurantsProvider({ children }) {
- const { restaurants, addRestaurant, isLoading, error } = useRestaurants();
- return (
-
- {children}
-
- );
-}
diff --git a/src/context/useRestaurantsContext.js b/src/context/useRestaurantsContext.js
deleted file mode 100644
index 7de8263..0000000
--- a/src/context/useRestaurantsContext.js
+++ /dev/null
@@ -1,9 +0,0 @@
-import { useContext } from "react";
-import { RestaurantsContext } from "./RestaurantsContext";
-
-export function useRestaurantsContext() {
- const context = useContext(RestaurantsContext);
- if (context === null)
- throw new Error("RestaurantsProvider 내부에서만 사용할 수 있습니다.");
- return context;
-}
diff --git a/src/hooks/useRestaurants.js b/src/hooks/useRestaurants.js
deleted file mode 100644
index e9130eb..0000000
--- a/src/hooks/useRestaurants.js
+++ /dev/null
@@ -1,31 +0,0 @@
-import { useState, useEffect, useCallback } from "react";
-import { getRestaurants, createRestaurant } from "../api.js";
-
-export function useRestaurants() {
- const [restaurants, setRestaurants] = useState([]);
- const [isLoading, setIsLoading] = useState(false);
- const [error, setError] = useState(null);
-
- const fetchRestaurants = useCallback(async () => {
- setIsLoading(true);
- try {
- const data = await getRestaurants();
- setRestaurants(data);
- } catch (error) {
- setError("음식점 목록을 불러오지 못했습니다.");
- } finally {
- setIsLoading(false);
- }
- }, []);
-
- useEffect(() => {
- void fetchRestaurants();
- }, [fetchRestaurants]);
-
- async function addRestaurant(restaurant) {
- await createRestaurant(restaurant);
- await fetchRestaurants();
- }
-
- return { restaurants, addRestaurant, isLoading, error };
-}
diff --git a/src/store/useRestaurantStore.js b/src/store/useRestaurantStore.js
new file mode 100644
index 0000000..31a81f0
--- /dev/null
+++ b/src/store/useRestaurantStore.js
@@ -0,0 +1,41 @@
+import { create } from "zustand";
+import { persist, createJSONStorage } from "zustand/middleware";
+import { getRestaurants, createRestaurant } from "../api.js";
+import { ALL_CATEGORY } from "../constants/categories.js";
+
+const useRestaurantStore = create(
+ persist(
+ (set, get) => ({
+ // 상태
+ restaurants: [],
+ isLoading: false,
+ error: null,
+ selectedCategory: ALL_CATEGORY,
+
+ // 액션
+ fetchRestaurants: async () => {
+ set({ isLoading: true, error: null });
+ try {
+ const data = await getRestaurants();
+ set({ restaurants: data });
+ } catch {
+ set({ error: "음식점 목록을 불러오지 못했습니다." });
+ } finally {
+ set({ isLoading: false });
+ }
+ },
+ addRestaurant: async (restaurant) => {
+ await createRestaurant(restaurant);
+ await get().fetchRestaurants();
+ },
+ setSelectedCategory: (category) => set({ selectedCategory: category }),
+ }),
+ {
+ name: "restaurant-storage",
+ storage: createJSONStorage(() => sessionStorage),
+ partialize: (state) => ({ selectedCategory: state.selectedCategory }),
+ },
+ ),
+);
+
+export default useRestaurantStore;