Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
441 changes: 281 additions & 160 deletions README.md

Large diffs are not rendered by default.

55 changes: 55 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
"server": "npx json-server db.json"
},
"dependencies": {
"@tanstack/react-query": "^5.101.2",
"@tanstack/react-query-devtools": "^5.101.2",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"styled-components": "^6.4.2",
Expand Down
8 changes: 1 addition & 7 deletions src/App.jsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
import "./App.css";
import { useEffect, useState } from "react";
import { useState } from "react";
import Header from "./components/Header/Header";
import CategoryFilter from "./components/CategoryFilter/CategoryFilter";
import RestaurantList from "./components/RestaurantList/RestaurantList";
import RestaurantDetailModal from "./components/Modal/RestaurantDetailModal";
import AddRestaurantModal from "./components/Modal/AddRestaurantModal";
import useRestaurantStore from "./store/useRestaurantStore";
import useFilterStore from "./store/useFilterStore";

export default function App() {
Expand All @@ -23,11 +22,6 @@ export default function App() {
const handleAddModalOpen = () => setIsAddModalOpen(true);
const handleAddModalClose = () => setIsAddModalOpen(false);

const fetchRestaurants = useRestaurantStore((state) => state.fetchRestaurants);
useEffect(() => {
fetchRestaurants();
}, [fetchRestaurants]);

return (
<>
<Header onAddModalOpen={handleAddModalOpen} />
Expand Down
23 changes: 9 additions & 14 deletions src/components/Modal/AddRestaurantModal.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,27 +3,22 @@ import { useState } from "react";
import Modal from "./Modal";
import styled from "styled-components";
import { textCaption } from "../../styles/typography";
import useRestaurantStore from "../../store/useRestaurantStore";
import { useAddRestaurantMutation } from "../../queries/useAddRestaurantMutation";

export default function AddRestaurantModal({ onClose }) {
const registerRestaurant = useRestaurantStore((state) => state.registerRestaurant);
const { mutate } = useAddRestaurantMutation();

const [category, setCategory] = useState("");
const [name, setName] = useState("");
const [description, setDescription] = useState("");

const handleSubmit = async (e) => {
const handleSubmit = (e) => {
e.preventDefault();
try {
await registerRestaurant({
id: crypto.randomUUID(),
category,
name,
description,
});
onClose();
} catch {
alert("음식점 추가에 실패했습니다. 다시 시도해주세요.");
}
onClose();
mutate(
{ category, name, description },
{ onError: () => alert("음식점 추가에 실패했습니다. 다시 시도해주세요.") },
);
};

return (
Expand Down
16 changes: 10 additions & 6 deletions src/components/RestaurantList/RestaurantList.jsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
import { CATEGORY_IMAGES } from "../../constants/categoryImages";
import styled from "styled-components";
import { textSubtitle, textBody } from "../../styles/typography";
import useRestaurantStore from "../../store/useRestaurantStore";
import { ALL_CATEGORY } from "../../constants/categories";
import { useRestaurantsQuery } from "../../queries/useRestaurantsQuery";

export default function RestaurantList({
selectedCategory,
onRestaurantClick,
}) {
const newRestaurants = useRestaurantStore((state) => state.newRestaurants);
const isLoading = useRestaurantStore((state) => state.isLoading);
const error = useRestaurantStore((state) => state.error);
const { data: newRestaurants, isLoading, error } = useRestaurantsQuery();

if (isLoading) return <StatusText>로딩중입니다.</StatusText>;
if (error) return <StatusText>{error.message}</StatusText>;
Comment on lines +13 to +14

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[배움]
early return 방식을 하면 early return 이후 newRestaurants가 반드시 존재한다는 게 보장돼서 제 코드에서 restaurants ?? [] 같은 방어 코드가 필요 없어진다는 점에서 인상적입니다.


const filteredRestaurants =
selectedCategory === ALL_CATEGORY
Expand All @@ -19,8 +20,6 @@ export default function RestaurantList({

return (
<ListContainer>
{isLoading && <p>로딩중입니다.</p>}
{error && <p>{error}</p>}
<RestaurantUl>
{filteredRestaurants.map((restaurant) => (
<Restaurant key={restaurant.id}>
Expand All @@ -45,6 +44,11 @@ export default function RestaurantList({
);
}

const StatusText = styled.p`
padding: 16px 8px;
color: var(--grey-300);
`;

const ListContainer = styled.section`
display: flex;
flex-direction: column;
Expand Down
1 change: 1 addition & 0 deletions src/constants/queryKeys.js

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[배움]
queryKey를 상수로 분리한 게 좋았어요. 여러 곳에서 같은 key를 참조할 때 오타 방지가 되고, 나중에 key 구조를 바꿀 때도 한 곳만 수정하면 되니까요.

한 가지 더 알면 좋을 패턴인데, 프로젝트가 커질수록 Query Key Factory를 많이 쓴다고 합니다.

export const restaurantKeys = {
  all: () => ["restaurants"],
  detail: (id) => ["restaurants", id],
};

지금처럼 배열 상수로 두면 나중에 상세 페이지나 필터 조건이 생겼을 때 key 간의 계층 관계를 표현하기 어려운데, Factory 패턴은 restaurantKeys.all()을 invalidate하면 하위 key(detail 등)가 전부 무효화되는 장점이 있어요. 지금 규모에선 오버엔지니어링일 수 있지만, 최종 프로젝트에 적용해보면 좋을 것 같습니다!

Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const RESTAURANTS_QUERY_KEY = ["restaurants"];
9 changes: 8 additions & 1 deletion src/main.jsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,16 @@
import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App.jsx";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";

const queryClient = new QueryClient();

ReactDOM.createRoot(document.getElementById("root")).render(
<React.StrictMode>
<App />
<QueryClientProvider client={queryClient}>
<App />
<ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>
</React.StrictMode>,
);
32 changes: 32 additions & 0 deletions src/queries/useAddRestaurantMutation.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { addRestaurant } from "../api";
import { RESTAURANTS_QUERY_KEY } from "../constants/queryKeys";

export function useAddRestaurantMutation() {
const queryClient = useQueryClient();

return useMutation({
mutationFn: addRestaurant,
onMutate: async (newRestaurant) => {
await queryClient.cancelQueries({ queryKey: RESTAURANTS_QUERY_KEY });
const previous = queryClient.getQueryData(RESTAURANTS_QUERY_KEY);
const optimisticItem = {
id: `optimistic-${Date.now()}`,
...newRestaurant,
};
queryClient.setQueryData(RESTAURANTS_QUERY_KEY, (old) => {
const current = Array.isArray(old) ? old : [];
return [...current, optimisticItem];
});
return { previous };
},
onError: (err, _, context) => {
if (context?.previous) {
queryClient.setQueryData(RESTAURANTS_QUERY_KEY, context.previous);
}
},
onSettled: () => {
queryClient.invalidateQueries({ queryKey: RESTAURANTS_QUERY_KEY });
},
});
}
10 changes: 10 additions & 0 deletions src/queries/useRestaurantsQuery.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { useQuery } from "@tanstack/react-query";
import { getRestaurants } from "../api";
import { RESTAURANTS_QUERY_KEY } from "../constants/queryKeys";

export function useRestaurantsQuery() {
return useQuery({
queryKey: RESTAURANTS_QUERY_KEY,
queryFn: getRestaurants,
});
}
27 changes: 0 additions & 27 deletions src/store/useRestaurantStore.js

This file was deleted.