From 37961e092b3736e8d2c4e9ca1afd135aafea41ba Mon Sep 17 00:00:00 2001 From: Nam Yooseong Date: Mon, 8 Jun 2026 01:26:59 +0900 Subject: [PATCH 01/13] =?UTF-8?q?docs:=205=EB=8B=A8=EA=B3=84=20=EB=AF=B8?= =?UTF-8?q?=EC=85=98=20=EC=9A=94=EA=B5=AC=EC=82=AC=ED=95=AD=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- 05-effects/README.md | 49 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 05-effects/README.md diff --git a/05-effects/README.md b/05-effects/README.md new file mode 100644 index 0000000..3c445ae --- /dev/null +++ b/05-effects/README.md @@ -0,0 +1,49 @@ +# 05. API ์—ฐ๋™ํ•˜๊ธฐ: side-effect(feat. effects) + +## ๐ŸŽฏ ์š”๊ตฌ ์‚ฌํ•ญ +- API๋กœ ๋ ˆ์Šคํ† ๋ž‘ ๋ชฉ๋ก์„ ๋ถˆ๋Ÿฌ์™€ ``์— ๋‚ด๋ ค์ค๋‹ˆ๋‹ค. + - ๋กœ๋”ฉ ์ƒํƒœ, ์—๋Ÿฌ ์ƒํƒœ ๋“ฑ์€ ๊ณ ๋ คํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. +- ๋ ˆ์Šคํ† ๋ž‘ ์ถ”๊ฐ€ ๋ชจ๋‹ฌ์—์„œ ์ถ”๊ฐ€ํ•˜๊ธฐ ๋ฒ„ํŠผ์„ ํด๋ฆญํ•˜๋ฉด POST ์š”์ฒญ์„ ๋ณด๋ƒ…๋‹ˆ๋‹ค. ๋ชจ๋‹ฌ์ด ๋‹ซํžˆ๊ณ , ๋ ˆ์Šคํ† ๋ž‘ ๋ชฉ๋ก์„ ๋‹ค์‹œ ๋ถˆ๋Ÿฌ์˜ต๋‹ˆ๋‹ค. + +## โœ… ํ‚ค์›Œ๋“œ +- effect (feat. side effect) + - useEffect + +## ๐Ÿง™โ€โ™€๏ธ ์ง„ํ–‰ ๊ฐ€์ด๋“œ + +- ์ง„ํ–‰ ์‹œ๊ฐ„: 2์‹œ๊ฐ„ ๋‚ด์— ์™„๋ฃŒํ•˜๋Š” ๊ฒƒ์„ ๋ชฉํ‘œ๋กœ ํ•ฉ๋‹ˆ๋‹ค. + +### `json-server`๋กœ ๊ฐ€์งœ ์„œ๋ฒ„ ๋„์›Œ ํ™œ์šฉํ•˜๊ธฐ +์—ฐ์Šต์šฉ ์•ฑ์ด๊ธฐ ๋•Œ๋ฌธ์— [`json-server`](https://github.com/typicode/json-server)๋ฅผ ํ™œ์šฉํ•ด ๊ฐ„๋‹จํ•œ ๊ฐ€์งœ REST API๋ฅผ ๊ตฌ์ถ•ํ•ด ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค. +- `npm run server`๋ฅผ ์‹คํ–‰ํ•ฉ๋‹ˆ๋‹ค. (ํ˜น์€ `npx json-server db.json` ๋ฅผ ์ง์ ‘ ์‹คํ–‰ํ•ด๋„ ์ƒ๊ด€์—†์Šต๋‹ˆ๋‹ค) +- `GET http://localhost:3000/restaurants`์œผ๋กœ `db.json`์— ์žˆ๋Š” ๋ ˆ์Šคํ† ๋ž‘ ๋ชฉ๋ก์„ ๋ถˆ๋Ÿฌ์˜ฌ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. +```javascript +// GET ์˜ˆ์‹œ +const response = await fetch("http://localhost:3000/restaurants"); + +// POST ์˜ˆ์‹œ +const response = await fetch("http://localhost:3000/restaurants", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(restaurant), +}); +``` + +## ๐Ÿ”— ์ฐธ๊ณ  ๋ฌธ์„œ +- [Synchronizing with Effects](https://react.dev/learn/synchronizing-with-effects) +> Effects let you specify side effects that are caused by rendering itself, rather than by a particular event. + - [API Reference: useEffect](https://react.dev/reference/react/useEffect) + > useEffect is a React Hook that lets you synchronize a component with an external system. +- [API Reference: useSate > updater function](https://react.dev/reference/react/useState#updating-state-based-on-the-previous-state) + > `set` function์— ํ•จ์ˆ˜๋ฅผ ๋„˜๊ฒจ์ฃผ๋ฉด `updater function`์œผ๋กœ ๋™์ž‘ํ•ฉ๋‹ˆ๋‹ค. ํ•จ์ˆ˜๊ฐ€ ์•„๋‹Œ ๊ฐ’์„ ๋„˜๊ฒจ์ค„ ๋•Œ์™€ ์–ด๋–ป๊ฒŒ ๋‹ค๋ฅธ์ง€ ์•Œ์•„๋ณด์„ธ์š”. + +๊ตฌํ˜„์„ ๋‹ค ํ•ด๋ณธ ๋’ค์— Introduction์—์„œ ์‚ดํŽด๋ณด์•˜๋˜ ์„ค๊ณ„ ์›์น™๊ณผ ๊ด€๋ จํ•ด ์กฐ๊ธˆ ๋” ํ•™์Šตํ•ด๋ณด๊ณ  ์‹ถ๋‹ค๋ฉด ์•„๋ž˜ ๋ฌธ์„œ๋“ค๋„ ์ถ”๊ฐ€๋กœ ํ™•์ธํ•ด ๋ณด์„ธ์š”. +- [You Might Not Need an Effect](https://react.dev/learn/you-might-not-need-an-effect) +> You do need Effects to synchronize with external systems. +> In React, data flows from the parent components to their children. +- [Components and Hooks must be pure](https://react.dev/reference/rules/components-and-hooks-must-be-pure) +> Purity in Components and Hooks is a key rule of React that makes your app predictable, easy to debug, and allows React to automatically optimize your code. +> Side effects should not run in render, as React can render components multiple times to create the best possible user experience. +> One important principle in React is local reasoning: the ability to understand what a component or hook does by looking at its code in isolation. Hooks should be treated like โ€œblack boxesโ€ when they are called. \ No newline at end of file From d7676f4b72accb2cc07d5c20091abcbdf2b804dc Mon Sep 17 00:00:00 2001 From: Nam Yooseong Date: Mon, 8 Jun 2026 17:00:53 +0900 Subject: [PATCH 02/13] =?UTF-8?q?feat:=20API=20=EC=97=B0=EB=8F=99=ED=95=98?= =?UTF-8?q?=EA=B8=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/App.jsx | 27 ++++++++++++------------ src/api.js | 15 +++++++++++++ src/constants/restaurants.js | 41 ------------------------------------ 3 files changed, 29 insertions(+), 54 deletions(-) create mode 100644 src/api.js delete mode 100644 src/constants/restaurants.js diff --git a/src/App.jsx b/src/App.jsx index 3c4bb06..ad97202 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -2,22 +2,29 @@ import "./App.css"; import Header from "./components/Header/Header.jsx"; import CategoryFilter from "./components/CategoryFilter/CategoryFilter.jsx"; import RestaurantList from "./components/RestaurantList/RestaurantList.jsx"; -import { useState } from "react"; +import { useEffect, useState } from "react"; import { filterRestaurants } from "./utils/filterRestaurants.js"; -import { RESTAURANTS } from "./constants/restaurants.js"; import RestaurantDetailModal from "./components/RestaurantDetailModal/RestaurantDetailModal.jsx"; import AddRestaurantModal from "./components/AddRestaurantModal/AddRestaurantModal.jsx"; +import { getRestaurants, postRestaurant } from "./api.js"; function App() { const [filterCategory, setFilterCategory] = useState("์ „์ฒด"); const [clickedRestaurant, setClickedRestaurant] = useState(null); const [isAddRestaurantModalOpen, setIsAddRestaurantModalOpen] = useState(false); - const [restaurants, setRestaurants] = useState(RESTAURANTS); + const [restaurants, setRestaurants] = useState([]); const isRestaurantDetailModalOpen = !!clickedRestaurant; const filteredRestaurants = filterRestaurants(restaurants, filterCategory); + useEffect(() => { + (async () => { + const restaurants = await getRestaurants(); + setRestaurants(restaurants); + })(); + }, []); + function handleFilterCategoryChange(e) { setFilterCategory(e.target.value); } @@ -38,17 +45,11 @@ function App() { setIsAddRestaurantModalOpen(false); } - function handleRestaurantSubmit({ category, name, description }) { - setRestaurants([ - ...restaurants, - { - id: Date.now(), - category, - name, - description, - }, - ]); + async function handleRestaurantSubmit({ category, name, description }) { + await postRestaurant({ category, name, description }); setIsAddRestaurantModalOpen(false); + const restaurants = await getRestaurants(); + setRestaurants(restaurants); } return ( diff --git a/src/api.js b/src/api.js new file mode 100644 index 0000000..14a8ff5 --- /dev/null +++ b/src/api.js @@ -0,0 +1,15 @@ +export async function getRestaurants() { + const response = await fetch("http://localhost:3000/restaurants"); + const jsonData = await response.json(); + return jsonData; +} + +export async function postRestaurant(restaurant) { + await fetch("http://localhost:3000/restaurants", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(restaurant), + }); +} diff --git a/src/constants/restaurants.js b/src/constants/restaurants.js deleted file mode 100644 index 7fc92cf..0000000 --- a/src/constants/restaurants.js +++ /dev/null @@ -1,41 +0,0 @@ -export const RESTAURANTS = [ - { - id: "a01", - name: "ํ”ผ์–‘์ฝฉํ• ๋งˆ๋‹ˆ", - description: - "ํ‰์–‘ ์ถœ์‹ ์˜ ํ• ๋จธ๋‹ˆ๊ฐ€ ์ˆ˜์‹ญ ๋…„๊ฐ„ ์šด์˜ํ•ด์˜จ ๋น„์ง€ ์ „๋ฌธ์  ํ”ผ์–‘์ฝฉ ํ• ๋งˆ๋‹ˆ. ๋‘๋ถ€๋ฅผ ๋นผ์ง€ ์•Š์€ ๋˜๋น„์ง€๋ฅผ ๋ง›๋ณผ ์ˆ˜ ์žˆ๋Š” ๊ณณ์œผ๋กœ, โ€˜ํ”ผ์–‘โ€™์€ ํ‰์•ˆ๋„ ์‚ฌํˆฌ๋ฆฌ๋กœ โ€˜ํ‰์–‘โ€™์„ ์˜๋ฏธํ•œ๋‹ค. ๋”ธ๊ณผ ํ•จ๊ป˜ ์šด์˜ํ•˜๋Š” ์ด๊ณณ์—์„  ๋งท๋Œ๋กœ ์ง์ ‘ ๊ฐ„ ์ฝฉ๋งŒ์„ ์‚ฌ์šฉํ•˜๋ฉฐ, ์ผ์ฒด์˜ ์กฐ๋ฏธ๋ฃŒ๋ฅผ ๋„ฃ์ง€ ์•Š์€ ๊ฑด๊ฐ•์‹์„ ์„ ๋ณด์ธ๋‹ค. ์ฝฉ๋น„์ง€์™€ ํ”ผ์–‘ ๋งŒ๋‘๊ฐ€ ์ด๊ณณ์˜ ๋Œ€ํ‘œ ๋ฉ”๋‰ด์ง€๋งŒ, ํ• ๋จธ๋‹ˆ๊ฐ€ ์˜›๋‚  ๋ฐฉ์‹์„ ๊ณ ์ˆ˜ํ•˜๋ฉฐ ๋งŒ๋“ค์–ด๋‚ด๋Š” ๋น„์ง€์ „๊ณจ ๋˜ํ•œ ์ด ์ง‘์˜ ์—ญ์‚ฌ๋ฅผ ๋А๋‚„ ์ˆ˜ ์žˆ๋Š” ํŠน๋ณ„ํ•œ ๋ฉ”๋‰ด๋‹ค. ๋ฐ˜์ฐฌ์€ ์†๋‹˜๋“ค์ด ๋จน๊ณ  ์‹ถ์€ ๋งŒํผ ๋œ์–ด ๋จน์„ ์ˆ˜ ์žˆ๊ฒŒ ์ค€๋น„๋ผ ์žˆ๋‹ค.", - category: "ํ•œ์‹", - }, - { - id: "a02", - name: "์นœ์นœ", - description: - "Since 2004 ํŽธ๋ฆฌํ•œ ๊ตํ†ต๊ณผ ์ฃผ์ฐจ, ๊ทธ๋ฆฌ๊ณ  ๊ด€๋ก๋งŒํผ ๊นŠ์€ ๋ง›๊ณผ ์ •์„ฑ์œผ๋กœ ์ •ํ†ต ์ค‘์‹์˜ ์„ธ๊ณ„๋ฅผ ํŽผ์ณ๊ฐ‘๋‹ˆ๋‹ค", - category: "์ค‘์‹", - }, - { - id: "a03", - name: "์ž‡์‡ผ์šฐ", - description: - "์ž‡์‡ผ์šฐ๋Š” ์ •ํ†ต ์ž๊ฐ€์ œ๋ฉด ์‚ฌ๋ˆ„๋ผ ์šฐ๋™์ด ๋Œ€ํ‘œ๋ฉ”๋‰ด์ž…๋‹ˆ๋‹ค. ๊ธฐ์ˆ ์€ ์ •์„ฑ์„ ์ด๊ธธ ์ˆ˜ ์—†๋‹ค๋Š” ์‹ ๋…์œผ๋กœ ๋ชจ๋“  ์Œ์‹์— ์ตœ์„ ์„ ๋‹คํ•˜๋Š” ์ž‡์‡ผ์šฐ๋Š” ๊ณ ๊ฐ ํ•œ๋ถ„ ํ•œ๋ถ„๊ป˜ ์ตœ์„ ์„ ๋‹คํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค", - category: "์ผ์‹", - }, - { - id: "a04", - name: "์ดํƒœ๋ฆฌํ‚ค์นœ", - description: "๋Š˜ ๋ณ€ํ™”๋ฅผ ์ถ”๊ตฌํ•˜๋Š” ์ดํƒœ๋ฆฌํ‚ค์นœ์ž…๋‹ˆ๋‹ค.", - category: "์–‘์‹", - }, - { - id: "a05", - name: "ํ˜ธ์•„๋นˆ ์‚ผ์„ฑ์ ", - description: "ํ‘ธ์งํ•œ ์–‘์— ๊ตญ๋ฌผ์ด ์ผํ’ˆ์ธ ์Œ€๊ตญ์ˆ˜", - category: "์•„์‹œ์•ˆ", - }, - { - id: "a06", - name: "๋„์Šคํƒ€์ฝ”์Šค ์„ ๋ฆ‰์ ", - description: "๋ฉ•์‹œ์นธ ์บ์ฃผ์–ผ ๊ทธ๋ฆด", - category: "๊ธฐํƒ€", - }, -]; From c68ea82fe3fea995dc8c04c5aaf6ea1ba70dd459 Mon Sep 17 00:00:00 2001 From: Nam Yooseong Date: Tue, 9 Jun 2026 09:23:23 +0900 Subject: [PATCH 03/13] chore: add .coderabbit.yaml --- .coderabbit.yaml | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 .coderabbit.yaml diff --git a/.coderabbit.yaml b/.coderabbit.yaml new file mode 100644 index 0000000..ecb145c --- /dev/null +++ b/.coderabbit.yaml @@ -0,0 +1,14 @@ +language: ko-KR +early_access: false +reviews: + profile: "chill" + request_changes_workflow: false + high_level_summary: true + poem: false + review_status: true + collapse_walkthrough: false + auto_review: + enabled: true + drafts: false +chat: + auto_reply: true From 7daef97af40df38bcb5a494a1abd2bb342aac8b4 Mon Sep 17 00:00:00 2001 From: Nam Yooseong Date: Sat, 13 Jun 2026 18:27:28 +0900 Subject: [PATCH 04/13] =?UTF-8?q?refactor:=20useRestaurants=20=EC=BB=A4?= =?UTF-8?q?=EC=8A=A4=ED=85=80=20=ED=9B=85=20=EB=B6=84=EB=A6=AC=20=EB=B0=8F?= =?UTF-8?q?=20api.js=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/App.jsx | 31 +++++++++++-------------------- src/api.js | 34 +++++++++++++++++++++++----------- src/hooks/useRestaurants.js | 22 ++++++++++++++++++++++ 3 files changed, 56 insertions(+), 31 deletions(-) create mode 100644 src/hooks/useRestaurants.js diff --git a/src/App.jsx b/src/App.jsx index ad97202..b72f5b9 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -2,31 +2,24 @@ import "./App.css"; import Header from "./components/Header/Header.jsx"; import CategoryFilter from "./components/CategoryFilter/CategoryFilter.jsx"; import RestaurantList from "./components/RestaurantList/RestaurantList.jsx"; -import { useEffect, useState } from "react"; +import { useState } from "react"; import { filterRestaurants } from "./utils/filterRestaurants.js"; import RestaurantDetailModal from "./components/RestaurantDetailModal/RestaurantDetailModal.jsx"; import AddRestaurantModal from "./components/AddRestaurantModal/AddRestaurantModal.jsx"; -import { getRestaurants, postRestaurant } from "./api.js"; +import { useRestaurants } from "./hooks/useRestaurants.js"; function App() { - const [filterCategory, setFilterCategory] = useState("์ „์ฒด"); + const [selectedCategory, setSelectedCategory] = useState("์ „์ฒด"); const [clickedRestaurant, setClickedRestaurant] = useState(null); const [isAddRestaurantModalOpen, setIsAddRestaurantModalOpen] = useState(false); - const [restaurants, setRestaurants] = useState([]); + const { restaurants, addRestaurant } = useRestaurants(); const isRestaurantDetailModalOpen = !!clickedRestaurant; - const filteredRestaurants = filterRestaurants(restaurants, filterCategory); + const filteredRestaurants = filterRestaurants(restaurants, selectedCategory); - useEffect(() => { - (async () => { - const restaurants = await getRestaurants(); - setRestaurants(restaurants); - })(); - }, []); - - function handleFilterCategoryChange(e) { - setFilterCategory(e.target.value); + function handleCategoryChange(e) { + setSelectedCategory(e.target.value); } function handleRestaurantClick(restaurant) { @@ -45,11 +38,9 @@ function App() { setIsAddRestaurantModalOpen(false); } - async function handleRestaurantSubmit({ category, name, description }) { - await postRestaurant({ category, name, description }); + async function handleRestaurantSubmit(restaurant) { + await addRestaurant(restaurant); setIsAddRestaurantModalOpen(false); - const restaurants = await getRestaurants(); - setRestaurants(restaurants); } return ( @@ -57,8 +48,8 @@ function App() {
{ + const fetchRestaurants = async () => { + const data = await getRestaurants(); + setRestaurants(data); + }; + fetchRestaurants(); + }, []); + + async function addRestaurant(restaurant) { + await createRestaurant(restaurant); + const data = await getRestaurants(); + setRestaurants(data); + } + + return { restaurants, addRestaurant }; +} From 3c649692a0bb66c625e9a2ed67e4d215e0d9f329 Mon Sep 17 00:00:00 2001 From: Nam Yooseong Date: Sat, 13 Jun 2026 18:27:41 +0900 Subject: [PATCH 05/13] =?UTF-8?q?docs:=205=EB=8B=A8=EA=B3=84=20=ED=95=99?= =?UTF-8?q?=EC=8A=B5=20=EB=82=B4=EC=9A=A9=20=EB=B0=8F=20=EB=A6=AC=ED=8C=A9?= =?UTF-8?q?=ED=86=A0=EB=A7=81=20=EB=82=B4=EC=9A=A9=20=EC=97=85=EB=8D=B0?= =?UTF-8?q?=EC=9D=B4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 276 +++++++++++++++++++++++++++++++++--------------------- 1 file changed, 168 insertions(+), 108 deletions(-) diff --git a/README.md b/README.md index e206bd7..d69c73c 100644 --- a/README.md +++ b/README.md @@ -1,181 +1,241 @@ -# ์žฌ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ ์ปดํฌ๋„ŒํŠธ ์„ค๊ณ„ +# API ์š”์ฒญ๊ณผ ๋น„๋™๊ธฐ ์ฒ˜๋ฆฌ ## ๐ŸŽฏ ๊ฐœ์ธ ๋ชฉํ‘œ ๋ฐ ๋ชฉํ‘œ ๋‹ฌ์„ฑ์„ ์œ„ํ•œ ํ–‰๋™ ๊ฐ€์ด๋“œ ์ด๋ฒˆ ๋ฏธ์…˜์„ ํ†ตํ•ด ๋‹ค์Œ๊ณผ ๊ฐ™์€ ํ•™์Šต ๊ฒฝํ—˜๋“ค์„ ์Œ“๋Š” ๊ฒƒ์„ ๋ชฉํ‘œ๋กœ ํ•œ๋‹ค. -1. controlled ์ปดํฌ๋„ŒํŠธ์™€ uncontrolled ์ปดํฌ๋„ŒํŠธ์˜ ์ฐจ์ด๋ฅผ ์ดํ•ดํ•˜๊ณ , ๊ฐ๊ฐ ์–ด๋–ค ์ƒํ™ฉ์—์„œ ์„ ํƒํ•˜๋Š”์ง€ ๊ธฐ์ค€์„ ์„ธ์šด๋‹ค. -2. ์–ด๋–ค state๋ฅผ ์–ด๋А ์ปดํฌ๋„ŒํŠธ๊ฐ€ ์†Œ์œ ํ•ด์•ผ ํ•˜๋Š”์ง€ ํŒ๋‹จํ•˜๊ณ , ํ•„์š”ํ•œ ๊ฒฝ์šฐ ๊ณตํ†ต ๋ถ€๋ชจ๋กœ ๋Œ์–ด์˜ฌ๋ฆฌ๋Š” ํŒจํ„ด(Lifting State Up)์„ ๊ฒฝํ—˜ํ•œ๋‹ค. -3. `children` props๋ฅผ ํ™œ์šฉํ•ด ์žฌ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ Modal ์ปดํฌ๋„ŒํŠธ๋ฅผ ์„ค๊ณ„ํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ์ตํžŒ๋‹ค. +1. side effect๊ฐ€ ๋ฌด์—‡์ธ์ง€ ์ดํ•ดํ•˜๊ณ , `useEffect`๊ฐ€ ์™œ ํ•„์š”ํ•œ์ง€ ์„ค๋ช…ํ•  ์ˆ˜ ์žˆ๋‹ค. +2. Promise๊ฐ€ ๋ฌด์—‡์ธ์ง€ ์ดํ•ดํ•˜๊ณ , async/await๊ฐ€ Promise๋ฅผ ์–ด๋–ป๊ฒŒ ๋‹ค๋ฃจ๋Š”์ง€ ์„ค๋ช…ํ•  ์ˆ˜ ์žˆ๋‹ค. +3. `fetch`๋กœ GET/POST ์š”์ฒญ์„ ๋ณด๋‚ด๊ณ  ์‘๋‹ต์„ ์ฒ˜๋ฆฌํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ์ตํžˆ๊ณ , `await`๋ฅผ ์–ด๋””์— ๋ถ™์—ฌ์•ผ ํ•˜๋Š”์ง€ ์Šค์Šค๋กœ ํŒ๋‹จํ•  ์ˆ˜ ์žˆ๋‹ค. --- ## ๐Ÿ“ ๊ธฐ๋Šฅ ๊ตฌํ˜„ ๋ชฉ๋ก -- [x] Header์˜ ์Œ์‹์  ์ถ”๊ฐ€ ๋ฒ„ํŠผ ํด๋ฆญ ์‹œ AddRestaurantModal ์—ด๊ธฐ -- [x] ์ถ”๊ฐ€ํ•˜๊ธฐ ๋ฒ„ํŠผ ํด๋ฆญ ์‹œ ์Œ์‹์  ๋ชฉ๋ก์— ํ•ญ๋ชฉ ์ถ”๊ฐ€ -- [x] ์žฌ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ Modal ์ปดํฌ๋„ŒํŠธ๋กœ AddRestaurantModal, RestaurantDetailModal ๊ฐœ์„  +- [x] API๋กœ ์Œ์‹์  ๋ชฉ๋ก์„ ๋ถˆ๋Ÿฌ์™€ RestaurantList์— ๋ Œ๋”๋ง +- [x] ์Œ์‹์  ์ถ”๊ฐ€ ์‹œ POST ์š”์ฒญ ํ›„ ๋ชฉ๋ก ์žฌ์กฐํšŒ --- ## ๐Ÿ“š ํ•™์Šต ๋‚ด์šฉ -### Controlled vs Uncontrolled Component +### useEffect์™€ side effect -์ปดํฌ๋„ŒํŠธ์˜ ์ค‘์š”ํ•œ ์ •๋ณด๊ฐ€ **props**์— ์˜ํ•ด ๊ฒฐ์ •๋˜๋ฉด controlled, **์ง€์—ญ state**๋กœ ์ž์ฒด ๊ด€๋ฆฌ๋˜๋ฉด uncontrolled์ด๋‹ค. +React ์ปดํฌ๋„ŒํŠธ๋Š” ๋ Œ๋”๋ง ์ค‘์— ์™ธ๋ถ€ ์‹œ์Šคํ…œ(์„œ๋ฒ„, ํƒ€์ด๋จธ, DOM ์ง์ ‘ ์กฐ์ž‘ ๋“ฑ)์— ์˜ํ–ฅ์„ ์ฃผ๋ฉด ์•ˆ ๋œ๋‹ค. ์ด๋Ÿฐ ์ž‘์—…์„ side effect๋ผ๊ณ  ํ•˜๊ณ , `useEffect`๋Š” ๋ Œ๋”๋ง์ด ๋๋‚œ ๋’ค ์ด๋ฅผ ์•ˆ์ „ํ•˜๊ฒŒ ์‹คํ–‰ํ•˜๋Š” ๊ณต๊ฐ„์ด๋‹ค. -| | Controlled | Uncontrolled | -|---|---|---| -| ์ •๋ณด ์ถœ์ฒ˜ | props (๋ถ€๋ชจ๊ฐ€ ์ œ๊ณต) | ์ง€์—ญ state (์ž์ฒด ๊ด€๋ฆฌ) | -| ๋ถ€๋ชจ์˜ ์˜ํ–ฅ | ๋™์ž‘์„ ์™„์ „ํžˆ ์ง€์ • ๊ฐ€๋Šฅ | ์˜ํ–ฅ์„ ์ค„ ์ˆ˜ ์—†์Œ | -| ์œ ์—ฐ์„ฑ | ์—ฌ๋Ÿฌ ์ปดํฌ๋„ŒํŠธ์™€ ์กฐ์ • ์šฉ์ด | ๋…๋ฆฝ์ ์ด์ง€๋งŒ ํ˜‘๋ ฅ ์–ด๋ ค์›€ | -| ์‚ฌ์šฉ ๋‚œ์ด๋„ | ๋ถ€๋ชจ์—์„œ props ์„ค์ • ํ•„์š” | ์„ค์ •์ด ์ ์–ด ์‚ฌ์šฉํ•˜๊ธฐ ์‰ฌ์›€ | - -**์–ธ์ œ ์„ ํƒํ•˜๋‚˜?** -- **Controlled** โ€” ๋ถ€๋ชจ ์ปดํฌ๋„ŒํŠธ์™€ state๋ฅผ ๊ณต์œ ํ•˜๊ฑฐ๋‚˜, ์—ฌ๋Ÿฌ ์ปดํฌ๋„ŒํŠธ์˜ ๋™์ž‘์„ ํ•จ๊ป˜ ์กฐ์ •ํ•ด์•ผ ํ•  ๋•Œ -- **Uncontrolled** โ€” ๋ถ€๋ชจ์™€ ์ƒํƒœ๋ฅผ ๊ณต์œ ํ•  ํ•„์š” ์—†์ด ๋…๋ฆฝ์ ์œผ๋กœ ๋™์ž‘ํ•ด๋„ ๋  ๋•Œ +```jsx +useEffect(() => { + // ๋ Œ๋”๋ง ์ดํ›„ ์‹คํ–‰ โ€” ์„œ๋ฒ„ ์š”์ฒญ, ๊ตฌ๋…, DOM ์กฐ์ž‘ ๋“ฑ +}, []); +``` -**์ด ๋ฏธ์…˜์—์„œ์˜ ์„ ํƒ:** +์˜์กด์„ฑ ๋ฐฐ์—ด `[]`๋ฅผ ๋„˜๊ธฐ๋ฉด ์ปดํฌ๋„ŒํŠธ๊ฐ€ ์ฒ˜์Œ ๋งˆ์šดํŠธ๋  ๋•Œ ํ•œ ๋ฒˆ๋งŒ ์‹คํ–‰๋œ๋‹ค. ๋ฐฐ์—ด์„ ์•„์˜ˆ ์ƒ๋žตํ•˜๋ฉด ๋งค ๋ Œ๋”๋ง๋งˆ๋‹ค ์‹คํ–‰๋˜์–ด ๋ฌดํ•œ ๋ฃจํ”„๊ฐ€ ์ƒ๊ธธ ์ˆ˜ ์žˆ๋‹ค. -controlled/uncontrolled๋Š” ์ปดํฌ๋„ŒํŠธ ์ „์ฒด์— ๋ถ™์ด๋Š” ๋ ˆ์ด๋ธ”์ด ์•„๋‹ˆ๋ผ, ์–ด๋–ค ์ •๋ณด๊ฐ€ ์–ด๋””์„œ ๊ด€๋ฆฌ๋˜๋Š”์ง€๋ฅผ ๊ธฐ์ค€์œผ๋กœ ํŒ๋‹จํ•œ๋‹ค. AddRestaurantModal์€ ๋‘ ๊ฐ€์ง€๊ฐ€ ํ˜ผ์žฌํ•œ๋‹ค. +### useEffect ์•ˆ์—์„œ async/await ์“ฐ๋Š” ๋ฒ• -- **ํผ ๋ฐ์ดํ„ฐ** (category, name, description) โ†’ ์ง€์—ญ state๋กœ ์ž์ฒด ๊ด€๋ฆฌ โ†’ **uncontrolled** -- **๋ชจ๋‹ฌ ์—ด๋ฆผ/๋‹ซํž˜** โ†’ App์ด ์†Œ์œ  โ†’ **controlled** +`useEffect`์˜ ์ฝœ๋ฐฑ์€ ์ง์ ‘ `async`๋กœ ๋งŒ๋“ค ์ˆ˜ ์—†๋‹ค. `async` ํ•จ์ˆ˜๋Š” ํ•ญ์ƒ Promise๋ฅผ ๋ฐ˜ํ™˜ํ•˜๋Š”๋ฐ, React๋Š” `useEffect` ์ฝœ๋ฐฑ์˜ ๋ฐ˜ํ™˜๊ฐ’์„ cleanup ํ•จ์ˆ˜๋กœ ๊ธฐ๋Œ€ํ•˜๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค. -๋ชจ๋‹ฌ ์—ด๋ฆผ ์ƒํƒœ๋ฅผ App์ด ์†Œ์œ ํ•˜๋Š” ์ด์œ ๋Š” ํŠธ๋ฆฌ๊ฑฐ(Header์˜ ์ถ”๊ฐ€ ๋ฒ„ํŠผ)์™€ ๋ชจ๋‹ฌ์ด ํ˜•์ œ ๊ด€๊ณ„์ด๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค. ํ˜•์ œ๋ผ๋ฆฌ๋Š” ์„œ๋กœ์˜ state์— ์ ‘๊ทผํ•  ์ˆ˜ ์—†์œผ๋ฏ€๋กœ ๊ณตํ†ต ๋ถ€๋ชจ์ธ App์œผ๋กœ state๋ฅผ ๋Œ์–ด์˜ฌ๋ ธ๋‹ค(Lifting State Up). +```jsx +// โŒ ์ด๋ ‡๊ฒŒ ํ•˜๋ฉด ์•ˆ ๋จ โ€” async ์ฝœ๋ฐฑ์ด Promise๋ฅผ ๋ฐ˜ํ™˜ํ•ด์„œ React๊ฐ€ ๊ฒฝ๊ณ  +useEffect(async () => { + const data = await getRestaurants(); + setRestaurants(data); +}, []); + +// โœ… ๋‚ด๋ถ€์—์„œ async ํ•จ์ˆ˜๋ฅผ ์ •์˜ํ•˜๊ณ  ํ˜ธ์ถœ +useEffect(() => { + const fetchRestaurants = async () => { + const data = await getRestaurants(); + setRestaurants(data); + }; + fetchRestaurants(); +}, []); +``` -`onSubmit`, `onClose`๋Š” state๊ฐ€ ์•„๋‹Œ ์ฝœ๋ฐฑ์ด๋‹ค. "์–ด๋–ค ๊ฐ’์„ ๋ˆ„๊ฐ€ ์†Œ์œ ํ•˜๋ƒ"์˜ ๋ฌธ์ œ๊ฐ€ ์•„๋‹ˆ๋ผ ์ด๋ฒคํŠธ๋ฅผ ๋ถ€๋ชจ๋กœ ์ „๋‹ฌํ•˜๋Š” ํ†ต๋กœ์ด๋ฏ€๋กœ controlled/uncontrolled ๊ตฌ๋ถ„๊ณผ๋Š” ๋ณ„๊ฐœ๋‹ค. +### async/await๋ฅผ ์–ด๋””์— ๋ถ™์—ฌ์•ผ ํ•˜๋Š”๊ฐ€ -### form submit ๊ธฐ๋ณธ ๋™์ž‘๊ณผ e.preventDefault() +`await`๋Š” Promise๋ฅผ ๋ฐ˜ํ™˜ํ•˜๋Š” ํ•จ์ˆ˜ ์•ž์— ๋ถ™์ธ๋‹ค. `await`๋ฅผ ์“ฐ๋Š” ํ•จ์ˆ˜ ์ž์‹ ์€ ๋ฐ˜๋“œ์‹œ `async`์—ฌ์•ผ ํ•œ๋‹ค. ์ด ๊ทœ์น™์ด ํ˜ธ์ถœ ์ฒด์ธ์„ ๋”ฐ๋ผ ์ „ํŒŒ๋œ๋‹ค. -`
`์—์„œ submit ์ด๋ฒคํŠธ๊ฐ€ ๋ฐœ์ƒํ•˜๋ฉด ๋ธŒ๋ผ์šฐ์ €๋Š” ๊ธฐ๋ณธ์ ์œผ๋กœ ํŽ˜์ด์ง€๋ฅผ ์ƒˆ๋กœ๊ณ ์นจํ•œ๋‹ค. React SPA์—์„œ๋Š” ์ด ๊ธฐ๋ณธ ๋™์ž‘์„ ๋ง‰์•„์•ผ state ์—…๋ฐ์ดํŠธ๊ฐ€ ์œ ์ง€๋œ๋‹ค. +``` +fetch() โ†’ Promise ๋ฐ˜ํ™˜ +getRestaurants() โ†’ ๋‚ด๋ถ€์—์„œ await fetch() โ†’ async ํ•„์š” +handleRestaurantSubmit() โ†’ ๋‚ด๋ถ€์—์„œ await getRestaurants() โ†’ async ํ•„์š” +``` -๋ฆฌํŒฉํ† ๋ง ํ›„ `e.preventDefault()`๋Š” ํผ์„ ์†Œ์œ ํ•œ AddRestaurantModal ๋‚ด๋ถ€์— ์žˆ๋‹ค. ์™„์„ฑ๋œ ๋ฐ์ดํ„ฐ๋งŒ `onSubmit`์„ ํ†ตํ•ด App์œผ๋กœ ์ „๋‹ฌํ•œ๋‹ค. +์‹ค์ˆ˜ํ•˜๊ธฐ ์‰ฌ์šด ํŒจํ„ด: `async` ํ•จ์ˆ˜๋ฅผ ํ˜ธ์ถœํ•  ๋•Œ `await`๋ฅผ ๋น ๋œจ๋ฆฌ๋ฉด Promise๊ฐ€ ํ’€๋ฆฌ๊ธฐ ์ „์— ๋‹ค์Œ ์ค„์ด ์‹คํ–‰๋œ๋‹ค. ```jsx -// AddRestaurantModal.jsx -function handleFormSubmit(e) { - e.preventDefault(); // ์—†์œผ๋ฉด setRestaurants ์‹คํ–‰ ์งํ›„ ํŽ˜์ด์ง€ ๋ฆฌ๋กœ๋“œ๋กœ state ์ดˆ๊ธฐํ™” - onSubmit({ category, name, description }); +// โŒ await ๋ˆ„๋ฝ โ€” POST๊ฐ€ ์™„๋ฃŒ๋˜๊ธฐ ์ „์— GET ์‹คํ–‰, ์ƒˆ ํ•ญ๋ชฉ์ด ๋ชฉ๋ก์— ์—†์„ ์ˆ˜ ์žˆ์Œ +async function handleRestaurantSubmit(restaurant) { + addRestaurant(restaurant); // await ์—†์Œ + const data = await getRestaurants(); + setRestaurants(data); } -// App.jsx โ€” ์ด๋ฒคํŠธ ๊ฐ์ฒด๊ฐ€ ์•„๋‹Œ ๋ฐ์ดํ„ฐ ๊ฐ์ฒด๋ฅผ ๋ฐ›์Œ -function handleRestaurantSubmit({ category, name, description }) { - setRestaurants([...restaurants, { id: Date.now(), category, name, description }]); +// โœ… POST ์™„๋ฃŒ๋ฅผ ๊ธฐ๋‹ค๋ฆฐ ๋’ค GET +async function handleRestaurantSubmit(restaurant) { + await addRestaurant(restaurant); + const data = await getRestaurants(); + setRestaurants(data); } ``` -### ํผ state๋ฅผ ๋ถ€๋ชจ๋กœ ๋Œ์–ด์˜ฌ๋ฆฌ๊ธฐ (Lifting State Up) +### api.js ํŒŒ์ผ ์œ„์น˜ -์ดˆ๊ธฐ ๊ตฌํ˜„์—์„œ๋Š” `restaurants` state๊ฐ€ `App`์— ์žˆ์–ด์„œ ํผ state๋„ `App`์œผ๋กœ ๋Œ์–ด์˜ฌ๋ ค props๋กœ ์ „๋‹ฌํ–ˆ๋‹ค. ๊ทธ๋Ÿฌ๋‚˜ App์ด ์‹ค์ œ๋กœ ํ•„์š”ํ•œ ๊ฒƒ์€ ์ถ”๊ฐ€ ์™„๋ฃŒ ์‹œ์ ์˜ ์ตœ์ข… ๋ฐ์ดํ„ฐ๋ฟ์ด๋ฏ€๋กœ, ๋ฆฌํŒฉํ† ๋ง์„ ํ†ตํ•ด ํผ state๋ฅผ `AddRestaurantModal` ์•ˆ์œผ๋กœ ๋‚ด๋ฆฌ๊ณ  ์™„์„ฑ๋œ ๋ฐ์ดํ„ฐ๋งŒ ๋ถ€๋ชจ๋กœ ์ „๋‹ฌํ•˜๋„๋ก ๋ณ€๊ฒฝํ–ˆ๋‹ค. (์ž์„ธํ•œ ๋‚ด์šฉ์€ ๋ฆฌํŒฉํ† ๋ง ์„น์…˜ ์ฐธ๊ณ ) +ํ˜„์žฌ `src/api.js`์— ๋‘์—ˆ๋‹ค. ํ”„๋กœ์ ํŠธ ๊ทœ๋ชจ์— ๋”ฐ๋ผ ๊ด€๋ก€๊ฐ€ ๋‹ค๋ฅด๋‹ค. -### ์ถ”๊ฐ€ ํ›„ ์ƒํƒœ ์ดˆ๊ธฐํ™” +| ๊ทœ๋ชจ | ๊ตฌ์กฐ | ์˜ˆ์‹œ | +|---|---|---| +| ์†Œ๊ทœ๋ชจ | ๋‹จ์ผ ํŒŒ์ผ | `src/api.js` | +| ์ค‘๊ทœ๋ชจ | ๋„๋ฉ”์ธ๋ณ„ ๋ถ„๋ฆฌ | `src/api/restaurants.js`, `src/api/users.js` | +| ๋Œ€๊ทœ๋ชจ | services ๋ ˆ์ด์–ด | `src/services/restaurantService.js` | -์Œ์‹์ ์„ ์ถ”๊ฐ€ํ•œ ๋’ค ํผ ์ž…๋ ฅ๊ฐ’์„ ์ดˆ๊ธฐํ™”ํ•˜์ง€ ์•Š์œผ๋ฉด ๋ชจ๋‹ฌ์„ ๋‹ค์‹œ ์—ด์—ˆ์„ ๋•Œ ์ด์ „ ๊ฐ’์ด ๋‚จ์•„์žˆ๋‹ค. +`api/`์™€ `services/`์˜ ์ฐจ์ด๋Š” ๋‰˜์•™์Šค ์ฐจ์ด๋‹ค. `api/`๋Š” ์„œ๋ฒ„์™€์˜ ํ†ต์‹  ํ•จ์ˆ˜ ๋ชจ์Œ์ด๋ผ๋Š” ์˜๋ฏธ๊ฐ€ ๊ฐ•ํ•˜๊ณ , `services/`๋Š” ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง๊นŒ์ง€ ํฌํ•จํ•  ์ˆ˜ ์žˆ๋‹ค๋Š” ์˜๋ฏธ๋กœ ์“ฐ์ด๊ธฐ๋„ ํ•œ๋‹ค. ํŒ€๋งˆ๋‹ค ๋‹ค๋ฅด๋ฏ€๋กœ ํ”„๋กœ์ ํŠธ ์ปจ๋ฒค์…˜์„ ๋”ฐ๋ฅด๋ฉด ๋œ๋‹ค. ํ˜„์žฌ ํ”„๋กœ์ ํŠธ์ฒ˜๋Ÿผ API ํ•จ์ˆ˜๊ฐ€ ๋ช‡ ๊ฐœ ์—†์„ ๋•Œ๋Š” `src/api.js` ํ•˜๋‚˜๋กœ ์ถฉ๋ถ„ํ•˜๋‹ค. -์ดˆ๊ธฐ ๊ตฌํ˜„์—์„œ๋Š” ํผ state๊ฐ€ App์— ์žˆ์—ˆ๊ธฐ ๋•Œ๋ฌธ์— ์ถ”๊ฐ€ ์™„๋ฃŒ ์‹œ์ ์— ๋ช…์‹œ์ ์œผ๋กœ ์ดˆ๊ธฐํ™”ํ–ˆ๋‹ค. +--- + +## ๐Ÿค” ๊ณ ๋ฏผํ–ˆ๋˜ ๋ฌธ์ œ์™€ ํ•ด๊ฒฐ ๊ณผ์ •์—์„œ ๋ฐฐ์šด ์  + +### async ํ•จ์ˆ˜๋ฅผ ์ •์˜๋งŒ ํ•˜๊ณ  ํ˜ธ์ถœํ•˜์ง€ ์•Š์€ ๋ฌธ์ œ + +`useEffect` ์•ˆ์—์„œ async ํ•จ์ˆ˜๋ฅผ ์ •์˜ํ–ˆ์ง€๋งŒ ํ˜ธ์ถœํ•˜์ง€ ์•Š์•„ API ์š”์ฒญ์ด ์‹คํ–‰๋˜์ง€ ์•Š์•˜๋‹ค. ๊ฐ™์€ ์‹ค์ˆ˜๋ฅผ ๋‘ ๋ฒˆ ํ–ˆ๋‹ค. ```jsx -setIsAddRestaurantModalOpen(false); -setCategory(""); -setName(""); -setDescription(""); +// โŒ ์ •์˜๋งŒ ํ•˜๊ณ  ํ˜ธ์ถœํ•˜์ง€ ์•Š์Œ โ€” ์•„๋ฌด ์ผ๋„ ์ผ์–ด๋‚˜์ง€ ์•Š์Œ +useEffect(() => { + async () => { + const data = await getRestaurants(); + setRestaurants(data); + }; +}, []); + +// โœ… ์ •์˜ ํ›„ ํ˜ธ์ถœ +useEffect(() => { + const fetchRestaurants = async () => { + const data = await getRestaurants(); + setRestaurants(data); + }; + fetchRestaurants(); // ํ˜ธ์ถœ +}, []); ``` -๋ฆฌํŒฉํ† ๋ง ํ›„์—๋Š” ํผ state๋ฅผ AddRestaurantModal ๋‚ด๋ถ€๋กœ ๋‚ด๋ ธ๊ธฐ ๋•Œ๋ฌธ์— ๋ช…์‹œ์  ์ดˆ๊ธฐํ™”๊ฐ€ ๋ถˆํ•„์š”ํ•ด์กŒ๋‹ค. `isAddRestaurantModalOpen`์ด `false`๊ฐ€ ๋˜๋ฉด AddRestaurantModal์ด ์–ธ๋งˆ์šดํŠธ๋˜๊ณ , ๋‹ค์‹œ ์—ด๋ฆด ๋•Œ ์ƒˆ๋กœ ๋งˆ์šดํŠธ๋˜๋ฉด์„œ `useState("")`์˜ ์ดˆ๊ธฐ๊ฐ’์œผ๋กœ ์ž๋™ ์ดˆ๊ธฐํ™”๋œ๋‹ค. +### e.preventDefault()๋ฅผ ์ œ๊ฑฐํ•˜๋ฉด ์•ˆ ๋œ๋‹ค๋Š” ๊ฒƒ์„ ๋†“์นœ ๋ฌธ์ œ ---- +API ์—ฐ๋™์œผ๋กœ ์ „ํ™˜ํ•˜๋ฉด์„œ "์ด์ œ ์„œ๋ฒ„์— ์ œ์ถœํ•˜๋‹ˆ๊นŒ `e.preventDefault()`๊ฐ€ ํ•„์š” ์—†๋‹ค"๊ณ  ์ƒ๊ฐํ–ˆ๋‹ค. ๊ทธ๋Ÿฌ๋‚˜ `e.preventDefault()`๋Š” ์„œ๋ฒ„ ์ œ์ถœ๊ณผ ๋ฌด๊ด€ํ•˜๊ฒŒ **๋ธŒ๋ผ์šฐ์ €์˜ ๊ธฐ๋ณธ ํผ ๋™์ž‘(ํŽ˜์ด์ง€ ์ƒˆ๋กœ๊ณ ์นจ)์„ ๋ง‰๋Š” ์—ญํ• **์ด๋‹ค. -## ๐Ÿค” ๊ณ ๋ฏผํ–ˆ๋˜ ๋ฌธ์ œ์™€ ํ•ด๊ฒฐ ๊ณผ์ •์—์„œ ๋ฐฐ์šด ์  +`e.preventDefault()` ์—†์ด ํผ์„ submitํ•˜๋ฉด React์˜ `fetch` ํ˜ธ์ถœ๊ณผ ๋ฌด๊ด€ํ•˜๊ฒŒ ๋ธŒ๋ผ์šฐ์ €๊ฐ€ ํŽ˜์ด์ง€๋ฅผ ์ƒˆ๋กœ๊ณ ์นจํ•œ๋‹ค. `e.preventDefault()`๋Š” ํ•ญ์ƒ ํ•„์š”ํ•˜๋‹ค. -### ์–ด๋–ค state๋ฅผ ์–ด๋А ์ปดํฌ๋„ŒํŠธ๊ฐ€ ์†Œ์œ ํ•ด์•ผ ํ•˜๋Š”๊ฐ€ +--- -์Œ์‹์  ์ถ”๊ฐ€ ๊ธฐ๋Šฅ์„ ๊ตฌํ˜„ํ•˜๋ฉด์„œ ๋‹ค์Œ state๋“ค์ด ํ•„์š”ํ•˜๋‹ค๊ณ  ์ƒ๊ฐํ–ˆ๋‹ค. +## ๐Ÿ›  ๋ฆฌํŒฉํ† ๋ง -- `restaurants` โ€” ์Œ์‹์  ๋ชฉ๋ก -- `category`, `name`, `description` โ€” ํผ ์ž…๋ ฅ๊ฐ’ -- `isAddRestaurantModalOpen` - ๋ชจ๋‹ฌ ์—ด๋ฆผ/๋‹ซํž˜ +### state ๋„ค์ด๋ฐ โ€” ์šฉ๋„๊ฐ€ ์•„๋‹Œ ๊ฐ’์˜ ์„ฑ๊ฒฉ์œผ๋กœ -์ด state๋“ค์„ ์–ด๋А ์ปดํฌ๋„ŒํŠธ์— ์„ ์–ธํ• ์ง€ ๊ฒฐ์ •ํ•˜๊ธฐ ์œ„ํ•ด "์ด state๋ฅผ ๋ˆ„๊ฐ€ ํ•„์š”๋กœ ํ•˜๋Š”๊ฐ€"๋ฅผ ๊ธฐ์ค€์œผ๋กœ ํŒ๋‹จํ–ˆ๋‹ค. +`filterCategory`๋Š” ์ด state๊ฐ€ ํ•„ํ„ฐ๋ง์— ์“ฐ์ธ๋‹ค๋Š” **์šฉ๋„**๋ฅผ ํ‘œํ˜„ํ•œ๋‹ค. state ์ด๋ฆ„์€ ์–ด๋””์— ์“ฐ์ด๋Š”์ง€๊ฐ€ ์•„๋‹ˆ๋ผ **์–ด๋–ค ๊ฐ’์„ ๋‹ด๊ณ  ์žˆ๋Š”์ง€**๋ฅผ ๋‚˜ํƒ€๋‚ด๋Š” ๊ฒŒ ์ข‹๋‹ค. -`restaurants`๋Š” AddRestaurantModal(์ถ”๊ฐ€ ์‹œ ๊ฐฑ์‹ )๊ณผ RestaurantList(๋ชฉ๋ก ๋ Œ๋”๋ง) ๋ชจ๋‘ ํ•„์š”ํ•˜๋‹ค. ๋‘ ์ปดํฌ๋„ŒํŠธ๋Š” ํ˜•์ œ ๊ด€๊ณ„๋ผ ์„œ๋กœ์˜ state์— ์ง์ ‘ ์ ‘๊ทผํ•  ์ˆ˜ ์—†์œผ๋ฏ€๋กœ, ๊ณตํ†ต ๋ถ€๋ชจ์ธ App์œผ๋กœ ๋Œ์–ด์˜ฌ๋ ธ๋‹ค(Lifting State Up). +```jsx +// Before: ์šฉ๋„ ํ‘œํ˜„ +const [filterCategory, setFilterCategory] = useState("์ „์ฒด"); -`category`, `name`, `description`์€ ํผ์„ ์ž…๋ ฅํ•˜๋Š” ๋™์•ˆ AddRestaurantModal ์•ˆ์—์„œ๋งŒ ์“ฐ์ธ๋‹ค. ์ถ”๊ฐ€ ์™„๋ฃŒ ์‹œ์ ์— ์ตœ์ข… ๋ฐ์ดํ„ฐ๋งŒ ๋ถ€๋ชจ๋กœ ์ „๋‹ฌํ•˜๋ฉด ๋˜๋ฏ€๋กœ, ๊ตณ์ด App๊นŒ์ง€ ์˜ฌ๋ฆด ํ•„์š”๊ฐ€ ์—†๋‹ค. AddRestaurantModal์ด ์ง์ ‘ ์†Œ์œ ํ•œ๋‹ค. +// After: ๊ฐ’์˜ ์„ฑ๊ฒฉ ํ‘œํ˜„ +const [selectedCategory, setSelectedCategory] = useState("์ „์ฒด"); +``` -`isAddRestaurantModalOpen`๋„ ๊ฐ™์€ ๊ธฐ์ค€์œผ๋กœ ํŒ๋‹จํ–ˆ๋‹ค. ๋ชจ๋‹ฌ์„ ์—ฌ๋Š” ํŠธ๋ฆฌ๊ฑฐ๋Š” Header์˜ ์ถ”๊ฐ€ ๋ฒ„ํŠผ์ด๊ณ , ์‹ค์ œ๋กœ ์—ด๋ฆฌ๋Š” ๊ฒƒ์€ AddRestaurantModal์ด๋‹ค. ๋‘ ์ปดํฌ๋„ŒํŠธ๋Š” ํ˜•์ œ ๊ด€๊ณ„๋ผ ์„œ๋กœ์˜ state์— ์ ‘๊ทผํ•  ์ˆ˜ ์—†์œผ๋ฏ€๋กœ ๊ณตํ†ต ๋ถ€๋ชจ์ธ App์ด ์†Œ์œ ํ•ด์•ผ ํ•œ๋‹ค. +### API ํ•จ์ˆ˜๋ช… โ€” HTTP ๋ฉ”์„œ๋“œ๊ฐ€ ์•„๋‹Œ ์˜๋„๋กœ -### ์‹๋‹น์„ ์ถ”๊ฐ€ํ•ด๋„ ๋ชฉ๋ก์ด ์—…๋ฐ์ดํŠธ๋˜์ง€ ์•Š๋Š” ๋ฌธ์ œ +`postRestaurant`๋Š” HTTP ๋ฉ”์„œ๋“œ ์ด๋ฆ„(`post`)์„ ๊ทธ๋Œ€๋กœ ์“ด ๊ฒƒ์ด๋‹ค. ํ•จ์ˆ˜๋ช…์€ ์–ด๋–ป๊ฒŒ ๋™์ž‘ํ•˜๋Š”์ง€๊ฐ€ ์•„๋‹ˆ๋ผ ๋ฌด์—‡์„ ํ•˜๋ ค๋Š”์ง€๋ฅผ ๋‚˜ํƒ€๋‚ด์•ผ ํ•œ๋‹ค. -`handleSubmit`์—์„œ `setRestaurants`๊ฐ€ ํ˜ธ์ถœ๋˜๋Š”๋ฐ๋„ ๋ชฉ๋ก์ด ๋ฐ”๋€Œ์ง€ ์•Š์•„์„œ ์›์ธ์„ ์ฐพ์•„๋ดค๋‹ค. `e.preventDefault()`๊ฐ€ ์—†์–ด์„œ submit ์‹œ ํŽ˜์ด์ง€๊ฐ€ ์ƒˆ๋กœ๊ณ ์นจ๋˜๊ณ , state ์—…๋ฐ์ดํŠธ๊ฐ€ ๋ฐ˜์˜๋˜๊ธฐ ์ „์— ์ดˆ๊ธฐ ์ƒํƒœ๋กœ ๋˜๋Œ์•„๊ฐ€๋Š” ๊ฒƒ์ด์—ˆ๋‹ค. state ์—…๋ฐ์ดํŠธ ์ž์ฒด๋Š” ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ์ž‘์„ฑ๋˜์–ด ์žˆ์—ˆ์ง€๋งŒ ๋ธŒ๋ผ์šฐ์ € ๊ธฐ๋ณธ ๋™์ž‘์„ ๋ง‰์ง€ ์•Š์•„์„œ ์ƒ๊ธด ๋ฌธ์ œ์˜€๋‹ค. +```js +// Before: ๊ตฌํ˜„ ๋ฐฉ๋ฒ• ํ‘œํ˜„ +export async function postRestaurant(restaurant) { ... } -### setRestaurants ์ธ์ž ์˜ค๋ฅ˜ +// After: ์˜๋„ ํ‘œํ˜„ +export async function addRestaurant(restaurant) { ... } +``` -์ฒ˜์Œ์— `setRestaurants(...restaurants, { ... })`๋กœ ์ž‘์„ฑํ–ˆ๋‹ค. ์ด๋ ‡๊ฒŒ ํ•˜๋ฉด ๋ฐฐ์—ด์ด ์•„๋‹Œ ์—ฌ๋Ÿฌ ์ธ์ž๋ฅผ `setRestaurants`์— ๋„˜๊ธฐ๋Š” ๊ฒƒ์ด๋ผ ์ƒˆ ๋ฐฐ์—ด์ด ๋งŒ๋“ค์–ด์ง€์ง€ ์•Š๋Š”๋‹ค. ๋ฐฐ์—ด ๋ฆฌํ„ฐ๋Ÿด ์•ˆ์—์„œ ์Šคํ”„๋ ˆ๋“œํ•ด์•ผ ํ•œ๋‹ค. +### BASE_URL ์ƒ์ˆ˜ ์ถ”์ถœ -```jsx -// โŒ ์ธ์ž๋ฅผ ์—ฌ๋Ÿฌ ๊ฐœ ์ „๋‹ฌํ•˜๋Š” ๊ฒƒ -setRestaurants(...restaurants, { id: Date.now(), ... }); +`http://localhost:3000`์ด `getRestaurants`์™€ `addRestaurant` ๋‘ ๊ณณ์— ๋ฐ˜๋ณต๋๋‹ค. ์ƒ์ˆ˜๋กœ ์ถ”์ถœํ•ด ํ•œ ๊ณณ์—์„œ ๊ด€๋ฆฌํ•œ๋‹ค. -// โœ… ๊ธฐ์กด ํ•ญ๋ชฉ์„ ํŽผ์ณ์„œ ์ƒˆ ๋ฐฐ์—ด ์ƒ์„ฑ -setRestaurants([...restaurants, { id: Date.now(), ... }]); +```js +const BASE_URL = "http://localhost:3000"; ``` ---- +### ๋ฐ˜ํ™˜๊ฐ’ ๋ณ€์ˆ˜๋ช… โ€” `jsonData` โ†’ `restaurants` -## ๐Ÿ›  ๋ฆฌํŒฉํ† ๋ง +`response.json()`์œผ๋กœ ํŒŒ์‹ฑ์ด ์™„๋ฃŒ๋œ ๋’ค์—๋„ `jsonData`๋ผ๋Š” ์ด๋ฆ„์„ ์“ฐ๋ฉด ํ˜•์‹(JSON)์„ ๊ฐ•์กฐํ•˜๊ฒŒ ๋œ๋‹ค. ํŒŒ์‹ฑ ํ›„์—๋Š” ์‹ค์ œ ๋‚ด์šฉ์„ ๋‚˜ํƒ€๋‚ด๋Š” ์ด๋ฆ„์ด ๋” ๋ช…ํ™•ํ•˜๋‹ค. -### ์ด๋ฒคํŠธ ํ•ธ๋“ค๋Ÿฌ ๋„ค์ด๋ฐ ๊ทœ์น™ +```js +// Before +const jsonData = await response.json(); -`handle` + `[๋Œ€์ƒ]` + `[๋™์ž‘]` ํŒจํ„ด์„ ์‚ฌ์šฉํ•œ๋‹ค. ๋Œ€์ƒ์€ ํ•ญ์ƒ ๋ถ™์ธ๋‹ค. +// After +const restaurants = await response.json(); +``` -```jsx -handleFormSubmit // Form + Submit -handleCategoryChange // Category + Change -handleDetailModalClose // DetailModal + Close +### try/catch ์ถ”๊ฐ€ + +`fetch`๋Š” ๋„คํŠธ์›Œํฌ ์˜ค๋ฅ˜์—์„œ๋งŒ throwํ•˜๊ณ , HTTP 4xx/5xx ์‘๋‹ต์€ throwํ•˜์ง€ ์•Š๋Š”๋‹ค. ๋‘ ๊ฐ€์ง€๋ฅผ ๋ชจ๋‘ ์ฒ˜๋ฆฌํ•˜๋ ค๋ฉด `response.ok` ํ™•์ธ๊ณผ `try/catch`๊ฐ€ ํ•จ๊ป˜ ํ•„์š”ํ•˜๋‹ค. + +```js +export async function getRestaurants() { + try { + const response = await fetch(`${BASE_URL}/restaurants`); + if (!response.ok) throw new Error(`์„œ๋ฒ„ ์˜ค๋ฅ˜: ${response.status}`); + const restaurants = await response.json(); + return restaurants; + } catch (error) { + console.error("์Œ์‹์  ๋ชฉ๋ก ์กฐํšŒ ์‹คํŒจ:", error); + throw error; // ํ˜ธ์ถœํ•œ ์ชฝ์ด ์—๋Ÿฌ๋ฅผ ์•Œ ์ˆ˜ ์žˆ๋„๋ก ๋‹ค์‹œ ๋˜์ง + } +} ``` -props๋กœ ๋„˜๊ธธ ๋•Œ๋Š” `on-`์œผ๋กœ ํ†ต์ผํ•œ๋‹ค. `on-`์€ ์ธํ„ฐํŽ˜์ด์Šค(๊ณ„์•ฝ), `handle-`์€ ๊ตฌํ˜„์ด๋‹ค. +catch์—์„œ `throw error`๋ฅผ ๋‹ค์‹œ ๋˜์ง€๋Š” ์ด์œ : ๋กœ๊ทธ๋งŒ ๋‚จ๊ธฐ๊ณ  ์‚ผ์ผœ๋ฒ„๋ฆฌ๋ฉด ํ˜ธ์ถœํ•œ ์ชฝ(`App.jsx`)์ด ์—๋Ÿฌ ๋ฐœ์ƒ ์—ฌ๋ถ€๋ฅผ ์•Œ ์ˆ˜ ์—†๋‹ค. -```jsx -// ์ •์˜๋Š” handle- -function handleNameChange(e) { ... } +### ์ปค์Šคํ…€ ํ›… ๋ถ„๋ฆฌ โ€” `useRestaurants` -// props๋กœ ๋„˜๊ธธ ๋•Œ๋Š” on- - -``` +`App`์ด ๋ Œ๋”๋ง ๊ตฌ์กฐ์™€ UI ์ƒํƒœ๋ฅผ ๋‹ค๋ฃจ๋Š” ์ปดํฌ๋„ŒํŠธ์ธ๋ฐ, ์„œ๋ฒ„ ํ†ต์‹ ๊ณผ ๋ฐ์ดํ„ฐ ๊ด€๋ฆฌ ๋กœ์ง๊นŒ์ง€ ํ•จ๊ป˜ ๋“ค์–ด์žˆ์–ด ์—ญํ• ์ด ์„ž์—ฌ ์žˆ์—ˆ๋‹ค. `restaurants` state, `useEffect`, fetch ๋กœ์ง์„ ์ปค์Šคํ…€ ํ›…์œผ๋กœ ์ถ”์ถœํ–ˆ๋‹ค. -### ํผ state๋ฅผ ์ž์‹์œผ๋กœ ๋‚ด๋ฆฌ๊ธฐ +```js +// hooks/useRestaurants.js +export function useRestaurants() { + const [restaurants, setRestaurants] = useState([]); -์ฒ˜์Œ ๊ตฌํ˜„์—์„œ๋Š” ํผ ์ž…๋ ฅ๊ฐ’(category, name, description)์„ App์—์„œ ๊ด€๋ฆฌํ–ˆ๋‹ค. `restaurants` state๊ฐ€ App์— ์žˆ์–ด์„œ ํผ ๋ฐ์ดํ„ฐ๋„ App๊นŒ์ง€ ์˜ฌ๋ ค์•ผ ํ•œ๋‹ค๊ณ  ์ƒ๊ฐํ–ˆ๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค. ๊ฒฐ๊ณผ์ ์œผ๋กœ AddRestaurantModal์— props๊ฐ€ 8๊ฐœ๊ฐ€ ๋๋‹ค. + async function fetchRestaurants() { + const data = await getRestaurants(); + setRestaurants(data); + } -๊ทธ๋Ÿฌ๋‚˜ App์ด ์‹ค์ œ๋กœ ํ•„์š”ํ•œ ๊ฒƒ์€ **์ถ”๊ฐ€ ์™„๋ฃŒ ์‹œ์ ์˜ ์ตœ์ข… ๋ฐ์ดํ„ฐ**๋ฟ์ด๋‹ค. ํผ์„ ์ฑ„์šฐ๋Š” ์ค‘๊ฐ„ ๊ณผ์ •์˜ ์ž…๋ ฅ๊ฐ’์€ App์ด ์•Œ ํ•„์š”๊ฐ€ ์—†๋‹ค. ํผ state๋ฅผ AddRestaurantModal ์•ˆ์œผ๋กœ ๋‚ด๋ฆฌ๊ณ , ์™„๋ฃŒ ์‹œ์—๋งŒ ๋ถ€๋ชจ๋กœ ์ „๋‹ฌํ•˜๋„๋ก ๋ณ€๊ฒฝํ–ˆ๋‹ค. + useEffect(() => { + fetchRestaurants(); + }, []); -```jsx -// Before: App์ด ์ค‘๊ฐ„ ์ž…๋ ฅ๊ฐ’๊นŒ์ง€ ๊ด€๋ฆฌ - - -// After: ์™„์„ฑ๋œ ๋ฐ์ดํ„ฐ๋งŒ ๋ถ€๋ชจ๋กœ ์ „๋‹ฌ - -``` - -props๊ฐ€ 8๊ฐœ โ†’ 2๊ฐœ๋กœ ์ค„์—ˆ๊ณ , ํผ ๋‚ด๋ถ€ ๊ด€์‹ฌ์‚ฌ๊ฐ€ AddRestaurantModal ์•ˆ์— ์บก์Аํ™”๋๋‹ค. + async function addRestaurant(restaurant) { + await addRestaurant(restaurant); + await fetchRestaurants(); + } -### Modal ๋ž˜ํผ div ์ œ๊ฑฐ + return { restaurants, addRestaurant }; +} +``` -`display: none` / `display: block` ํŒจํ„ด์€ ํ…œํ”Œ๋ฆฟ(`templates/style.css`)์—์„œ ๊ทธ๋Œ€๋กœ ๊ฐ€์ ธ์˜จ ๊ฒƒ์ด๋‹ค. ํ…œํ”Œ๋ฆฟ์€ ์ˆœ์ˆ˜ HTML/CSS ๊ธฐ๋ฐ˜์œผ๋กœ, ๋ชจ๋‹ฌ์ด DOM์— ํ•ญ์ƒ ์กด์žฌํ•˜๋ฉด์„œ `.modal--open` ํด๋ž˜์Šค๋ฅผ ๋ถ™์ด๊ณ  ๋–ผ๋Š” ๋ฐฉ์‹์œผ๋กœ ๋ณด์ด๊ณ  ์ˆจ๊ฒผ๋‹ค. +App์—์„œ๋Š” ์„œ๋ฒ„ ํ†ต์‹  ๋กœ์ง์ด ๋ชจ๋‘ ์‚ฌ๋ผ์ง€๊ณ , ๋ชจ๋‹ฌ ์—ด๋ฆผ/๋‹ซํž˜ ๊ฐ™์€ UI ์ƒํƒœ๋งŒ ๋‚จ์•˜๋‹ค. -React๋กœ ์ „ํ™˜ํ•˜๋ฉด์„œ ์กฐ๊ฑด๋ถ€ ๋ Œ๋”๋ง(`{isOpen && }`)์„ ์‚ฌ์šฉํ•˜๊ฒŒ ๋˜๋ฉด์„œ CSS ํ† ๊ธ€์ด ๋ถˆํ•„์š”ํ•ด์กŒ๋‹ค. ํ…œํ”Œ๋ฆฟ์˜ ๊ตฌ์กฐ๋ฅผ ๊ทธ๋Œ€๋กœ ์“ฐ๋ฉด์„œ ์ƒ๊ธด ๋ถˆํ•„์š”ํ•œ ํŒจํ„ด์„ ๋’ค๋Šฆ๊ฒŒ ์ œ๊ฑฐํ–ˆ๋‹ค. +```jsx +// Before: App์ด ์„œ๋ฒ„ ํ†ต์‹ ๊นŒ์ง€ ๋‹ด๋‹น +const [restaurants, setRestaurants] = useState([]); +useEffect(() => { /* fetch ๋กœ์ง */ }, []); +async function handleRestaurantSubmit(...) { + await postRestaurant(...); + const data = await getRestaurants(); + setRestaurants(data); +} -๋ž˜ํผ `
`๋„ ํ•จ๊ป˜ ์ œ๊ฑฐํ–ˆ๋‹ค. backdrop๊ณผ container๊ฐ€ ๋‘˜ ๋‹ค `position: fixed`๋ผ ๋ถ€๋ชจ ์š”์†Œ์˜ ๋ ˆ์ด์•„์›ƒ์— ์˜ํ–ฅ์„ ๋ฐ›์ง€ ์•Š์•„ ๋ž˜ํผ๊ฐ€ ์—†์–ด๋„ ๋™์ž‘์ด ๋™์ผํ•˜๋‹ค. ๋ถˆํ•„์š”ํ•œ DOM ๋…ธ๋“œ๋ฅผ ์ค„์ด๊ธฐ ์œ„ํ•ด Fragment๋กœ ๊ต์ฒดํ–ˆ๋‹ค. +// After: ํ›…์ด ๋ฐ์ดํ„ฐ ๊ด€๋ฆฌ๋ฅผ ๋‹ด๋‹น +const { restaurants, addRestaurant } = useRestaurants(); +async function handleRestaurantSubmit(restaurant) { + await addRestaurant(restaurant); + setIsAddRestaurantModalOpen(false); +} +``` From 9f4fff24dd4d06596ff75c7c1b819afcc6ed16dc Mon Sep 17 00:00:00 2001 From: Nam Yooseong Date: Sat, 13 Jun 2026 19:20:53 +0900 Subject: [PATCH 06/13] =?UTF-8?q?refactor:=20=EC=B9=B4=ED=85=8C=EA=B3=A0?= =?UTF-8?q?=EB=A6=AC=20=EC=83=81=EC=88=98=20=EC=B6=94=EC=B6=9C=20=EB=B0=8F?= =?UTF-8?q?=20map=EC=9C=BC=EB=A1=9C=20=EC=98=B5=EC=85=98=20=EB=A0=8C?= =?UTF-8?q?=EB=8D=94=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 --- .../AddRestaurantModal/AddRestaurantModal.jsx | 12 ++++++------ src/components/CategoryFilter/CategoryFilter.jsx | 12 ++++++------ src/constants/categories.js | 1 + 3 files changed, 13 insertions(+), 12 deletions(-) create mode 100644 src/constants/categories.js diff --git a/src/components/AddRestaurantModal/AddRestaurantModal.jsx b/src/components/AddRestaurantModal/AddRestaurantModal.jsx index 8aee4d1..0e3e3e5 100644 --- a/src/components/AddRestaurantModal/AddRestaurantModal.jsx +++ b/src/components/AddRestaurantModal/AddRestaurantModal.jsx @@ -1,6 +1,7 @@ import { useState } from "react"; import styles from "./AddRestaurantModal.module.css"; import Modal from "../Modal/Modal"; +import { CATEGORIES } from "../../constants/categories"; export default function AddRestaurantModal({ onSubmit, onClose }) { const [category, setCategory] = useState(""); @@ -27,12 +28,11 @@ export default function AddRestaurantModal({ onSubmit, onClose }) { required > - - - - - - + {CATEGORIES.map((value) => ( + + ))}
diff --git a/src/components/CategoryFilter/CategoryFilter.jsx b/src/components/CategoryFilter/CategoryFilter.jsx index 184edb6..60f56b7 100644 --- a/src/components/CategoryFilter/CategoryFilter.jsx +++ b/src/components/CategoryFilter/CategoryFilter.jsx @@ -1,3 +1,4 @@ +import { CATEGORIES } from "../../constants/categories"; import styles from "./CategoryFilter.module.css"; export default function CategoryFilter({ category, onCategoryChange }) { @@ -12,12 +13,11 @@ export default function CategoryFilter({ category, onCategoryChange }) { onChange={onCategoryChange} > - - - - - - + {CATEGORIES.map((value) => ( + + ))} ); diff --git a/src/constants/categories.js b/src/constants/categories.js new file mode 100644 index 0000000..cac9f9e --- /dev/null +++ b/src/constants/categories.js @@ -0,0 +1 @@ +export const CATEGORIES = ["ํ•œ์‹", "์ค‘์‹", "์ผ์‹", "์–‘์‹", "์•„์‹œ์•ˆ", "๊ธฐํƒ€"]; From 89c13e6e94cf6f5c1b3cf48f533e540b907e1654 Mon Sep 17 00:00:00 2001 From: Nam Yooseong Date: Sat, 13 Jun 2026 19:21:51 +0900 Subject: [PATCH 07/13] =?UTF-8?q?refactor:=20RestaurantList=20button=20?= =?UTF-8?q?=EC=A0=91=EA=B7=BC=EC=84=B1=20=EA=B0=9C=EC=84=A0=20=EB=B0=8F=20?= =?UTF-8?q?=ED=95=A8=EC=88=98=ED=98=95=20=EC=97=85=EB=8D=B0=EC=9D=B4?= =?UTF-8?q?=ED=8A=B8=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../RestaurantList/RestaurantList.jsx | 38 +++++++++---------- .../RestaurantList/RestaurantList.module.css | 10 ++++- 2 files changed, 27 insertions(+), 21 deletions(-) diff --git a/src/components/RestaurantList/RestaurantList.jsx b/src/components/RestaurantList/RestaurantList.jsx index 7fbdb78..0150f7f 100644 --- a/src/components/RestaurantList/RestaurantList.jsx +++ b/src/components/RestaurantList/RestaurantList.jsx @@ -7,26 +7,24 @@ export default function RestaurantList({ restaurants, onRestaurantClick }) {
    {restaurants.map((restaurant) => { return ( -
  • onRestaurantClick(restaurant)} - > -
    - {restaurant.category} -
    -
    -

    - {restaurant.name} -

    -

    - {restaurant.description} -

    -
    +
  • +
  • ); })} diff --git a/src/components/RestaurantList/RestaurantList.module.css b/src/components/RestaurantList/RestaurantList.module.css index a1f4ffe..f7bdbdf 100644 --- a/src/components/RestaurantList/RestaurantList.module.css +++ b/src/components/RestaurantList/RestaurantList.module.css @@ -7,12 +7,20 @@ } .restaurant { + border-bottom: 1px solid #e9eaed; +} + +.restaurant__button { display: flex; align-items: flex-start; + width: 100%; padding: 16px 8px; - border-bottom: 1px solid #e9eaed; + background: none; + border: none; + cursor: pointer; + text-align: left; } .restaurant__category { From f9cc036d85d7d5621fd1ddbcc27d079bb78f5d40 Mon Sep 17 00:00:00 2001 From: Nam Yooseong Date: Sun, 14 Jun 2026 18:42:56 +0900 Subject: [PATCH 08/13] =?UTF-8?q?refactor:=20fetchRestaurants=EB=A5=BC=20u?= =?UTF-8?q?seCallback=EC=9C=BC=EB=A1=9C=20=EC=B6=94=EC=B6=9C=ED=95=B4?= =?UTF-8?q?=EC=84=9C=20=EC=9E=AC=EC=82=AC=EC=9A=A9=20=EB=B0=8F=20=EB=AC=B8?= =?UTF-8?q?=EC=84=9C=20=EC=97=85=EB=8D=B0=EC=9D=B4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 41 ++++++++++++++++++++++++++++++------- src/hooks/useRestaurants.js | 18 ++++++++-------- 2 files changed, 43 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index d69c73c..49fdb28 100644 --- a/README.md +++ b/README.md @@ -197,23 +197,50 @@ catch์—์„œ `throw error`๋ฅผ ๋‹ค์‹œ ๋˜์ง€๋Š” ์ด์œ : ๋กœ๊ทธ๋งŒ ๋‚จ๊ธฐ๊ณ  ์‚ผ์ผœ `App`์ด ๋ Œ๋”๋ง ๊ตฌ์กฐ์™€ UI ์ƒํƒœ๋ฅผ ๋‹ค๋ฃจ๋Š” ์ปดํฌ๋„ŒํŠธ์ธ๋ฐ, ์„œ๋ฒ„ ํ†ต์‹ ๊ณผ ๋ฐ์ดํ„ฐ ๊ด€๋ฆฌ ๋กœ์ง๊นŒ์ง€ ํ•จ๊ป˜ ๋“ค์–ด์žˆ์–ด ์—ญํ• ์ด ์„ž์—ฌ ์žˆ์—ˆ๋‹ค. `restaurants` state, `useEffect`, fetch ๋กœ์ง์„ ์ปค์Šคํ…€ ํ›…์œผ๋กœ ์ถ”์ถœํ–ˆ๋‹ค. +### `fetchRestaurants`๋ฅผ `useCallback`์œผ๋กœ ์ถ”์ถœ + +`fetchRestaurants`๊ฐ€ `useEffect` ๋‚ด๋ถ€์— ์ •์˜๋˜์–ด ์žˆ์–ด `addRestaurant`์—์„œ ๊ฐ™์€ ๋กœ์ง์„ ์ค‘๋ณต์œผ๋กœ ์ž‘์„ฑํ•ด์•ผ ํ–ˆ๋‹ค. ํ•จ์ˆ˜๋ฅผ ํ›… ์Šค์ฝ”ํ”„๋กœ ๊บผ๋‚ด ์žฌ์‚ฌ์šฉํ•˜๋ ค๋ฉด `useCallback`์ด ํ•„์š”ํ•˜๋‹ค. + +`useCallback` ์—†์ด ์ผ๋ฐ˜ ํ•จ์ˆ˜๋กœ ์„ ์–ธํ•˜๋ฉด ๋ Œ๋”๋ง๋งˆ๋‹ค ์ƒˆ ํ•จ์ˆ˜ ์ฐธ์กฐ๊ฐ€ ์ƒ์„ฑ๋˜์–ด `useEffect([fetchRestaurants])`๊ฐ€ ๋งค ๋ Œ๋”๋ง๋งˆ๋‹ค ์‹คํ–‰๋˜๋Š” ๋ฌดํ•œ ๋ฃจํ”„๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค. `useCallback`์˜ ๋นˆ ์˜์กด์„ฑ ๋ฐฐ์—ด `[]`์ด ์ฐธ์กฐ๋ฅผ ๊ณ ์ •ํ•ด ์ด๋ฅผ ๋ฐฉ์ง€ํ•œ๋‹ค. + ```js -// hooks/useRestaurants.js +// Before: fetchRestaurants๊ฐ€ useEffect ์•ˆ์— ๊ฐ‡ํ˜€ ์žˆ์–ด addRestaurant์—์„œ ๋กœ์ง์„ ์ค‘๋ณต ์ž‘์„ฑ +export function useRestaurants() { + const [restaurants, setRestaurants] = useState([]); + + useEffect(() => { + const fetchRestaurants = async () => { + const data = await getRestaurants(); + setRestaurants(data); + }; + fetchRestaurants(); + }, []); + + async function addRestaurant(restaurant) { + await createRestaurant(restaurant); + const data = await getRestaurants(); // ์ค‘๋ณต + setRestaurants(data); // ์ค‘๋ณต + } + + return { restaurants, addRestaurant }; +} + +// After: useCallback์œผ๋กœ ์ถ”์ถœํ•ด ์žฌ์‚ฌ์šฉ, ์˜์กด์„ฑ ๋ฐฐ์—ด๋„ ๋ช…์‹œ์ ์œผ๋กœ ์„ ์–ธ export function useRestaurants() { const [restaurants, setRestaurants] = useState([]); - async function fetchRestaurants() { + const fetchRestaurants = useCallback(async () => { const data = await getRestaurants(); setRestaurants(data); - } + }, []); useEffect(() => { - fetchRestaurants(); - }, []); + void fetchRestaurants(); + }, [fetchRestaurants]); async function addRestaurant(restaurant) { - await addRestaurant(restaurant); - await fetchRestaurants(); + await createRestaurant(restaurant); + await fetchRestaurants(); // ์žฌ์‚ฌ์šฉ } return { restaurants, addRestaurant }; diff --git a/src/hooks/useRestaurants.js b/src/hooks/useRestaurants.js index dbb5c26..0a17a30 100644 --- a/src/hooks/useRestaurants.js +++ b/src/hooks/useRestaurants.js @@ -1,21 +1,21 @@ -import { useState, useEffect } from "react"; +import { useState, useEffect, useCallback } from "react"; import { getRestaurants, createRestaurant } from "../api.js"; export function useRestaurants() { const [restaurants, setRestaurants] = useState([]); - useEffect(() => { - const fetchRestaurants = async () => { - const data = await getRestaurants(); - setRestaurants(data); - }; - fetchRestaurants(); + const fetchRestaurants = useCallback(async () => { + const data = await getRestaurants(); + setRestaurants(data); }, []); + useEffect(() => { + void fetchRestaurants(); + }, [fetchRestaurants]); + async function addRestaurant(restaurant) { await createRestaurant(restaurant); - const data = await getRestaurants(); - setRestaurants(data); + await fetchRestaurants(); } return { restaurants, addRestaurant }; From ae3919b867cbe95b825f74d64ac01d1a4407d6cb Mon Sep 17 00:00:00 2001 From: Nam Yooseong Date: Sun, 14 Jun 2026 19:40:24 +0900 Subject: [PATCH 09/13] =?UTF-8?q?refactor:=20=EC=97=90=EB=9F=AC=20?= =?UTF-8?q?=ED=95=B8=EB=93=A4=EB=A7=81=20=EB=B0=8F=20isLoading=20state=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/App.jsx | 15 +++++++++++---- src/api.js | 29 +++++++++-------------------- src/hooks/useRestaurants.js | 15 ++++++++++++--- 3 files changed, 32 insertions(+), 27 deletions(-) diff --git a/src/App.jsx b/src/App.jsx index b72f5b9..485546e 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -7,13 +7,14 @@ import { filterRestaurants } from "./utils/filterRestaurants.js"; import RestaurantDetailModal from "./components/RestaurantDetailModal/RestaurantDetailModal.jsx"; import AddRestaurantModal from "./components/AddRestaurantModal/AddRestaurantModal.jsx"; import { useRestaurants } from "./hooks/useRestaurants.js"; +import { ALL_CATEGORY } from "./constants/categories.js"; function App() { - const [selectedCategory, setSelectedCategory] = useState("์ „์ฒด"); + const [selectedCategory, setSelectedCategory] = useState(ALL_CATEGORY); const [clickedRestaurant, setClickedRestaurant] = useState(null); const [isAddRestaurantModalOpen, setIsAddRestaurantModalOpen] = useState(false); - const { restaurants, addRestaurant } = useRestaurants(); + const { restaurants, addRestaurant, isLoading, error } = useRestaurants(); const isRestaurantDetailModalOpen = !!clickedRestaurant; const filteredRestaurants = filterRestaurants(restaurants, selectedCategory); @@ -39,14 +40,20 @@ function App() { } async function handleRestaurantSubmit(restaurant) { - await addRestaurant(restaurant); - setIsAddRestaurantModalOpen(false); + try { + await addRestaurant(restaurant); + setIsAddRestaurantModalOpen(false); + } catch { + alert("์Œ์‹์  ์ถ”๊ฐ€์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค. ๋‹ค์‹œ ์‹œ๋„ํ•ด์ฃผ์„ธ์š”."); + } } return ( <>
    + {isLoading &&

    ๋ถˆ๋Ÿฌ์˜ค๋Š” ์ค‘...

    } + {error &&

    {error}

    } { - const data = await getRestaurants(); - setRestaurants(data); + setIsLoading(true); + try { + const data = await getRestaurants(); + setRestaurants(data); + } catch (error) { + setError("์Œ์‹์  ๋ชฉ๋ก์„ ๋ถˆ๋Ÿฌ์˜ค์ง€ ๋ชปํ–ˆ์Šต๋‹ˆ๋‹ค."); + } finally { + setIsLoading(false); + } }, []); useEffect(() => { @@ -18,5 +27,5 @@ export function useRestaurants() { await fetchRestaurants(); } - return { restaurants, addRestaurant }; + return { restaurants, addRestaurant, isLoading, error }; } From 6c5d4f8095373ca07f3475b068a7547963862b3d Mon Sep 17 00:00:00 2001 From: Nam Yooseong Date: Sun, 14 Jun 2026 19:40:33 +0900 Subject: [PATCH 10/13] =?UTF-8?q?refactor:=20ALL=5FCATEGORY=20=EC=83=81?= =?UTF-8?q?=EC=88=98=ED=99=94=20=EB=B0=8F=20import=20=ED=99=95=EC=9E=A5?= =?UTF-8?q?=EC=9E=90=20=EC=9D=BC=EA=B4=80=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/AddRestaurantModal/AddRestaurantModal.jsx | 4 ++-- src/components/CategoryFilter/CategoryFilter.jsx | 4 ++-- .../RestaurantDetailModal/RestaurantDetailModal.jsx | 2 +- src/constants/categories.js | 1 + src/utils/filterRestaurants.js | 8 ++++---- 5 files changed, 10 insertions(+), 9 deletions(-) diff --git a/src/components/AddRestaurantModal/AddRestaurantModal.jsx b/src/components/AddRestaurantModal/AddRestaurantModal.jsx index 0e3e3e5..0bb7c55 100644 --- a/src/components/AddRestaurantModal/AddRestaurantModal.jsx +++ b/src/components/AddRestaurantModal/AddRestaurantModal.jsx @@ -1,7 +1,7 @@ import { useState } from "react"; import styles from "./AddRestaurantModal.module.css"; -import Modal from "../Modal/Modal"; -import { CATEGORIES } from "../../constants/categories"; +import Modal from "../Modal/Modal.jsx"; +import { CATEGORIES } from "../../constants/categories.js"; export default function AddRestaurantModal({ onSubmit, onClose }) { const [category, setCategory] = useState(""); diff --git a/src/components/CategoryFilter/CategoryFilter.jsx b/src/components/CategoryFilter/CategoryFilter.jsx index 60f56b7..fdf1d99 100644 --- a/src/components/CategoryFilter/CategoryFilter.jsx +++ b/src/components/CategoryFilter/CategoryFilter.jsx @@ -1,4 +1,4 @@ -import { CATEGORIES } from "../../constants/categories"; +import { ALL_CATEGORY, CATEGORIES } from "../../constants/categories.js"; import styles from "./CategoryFilter.module.css"; export default function CategoryFilter({ category, onCategoryChange }) { @@ -12,7 +12,7 @@ export default function CategoryFilter({ category, onCategoryChange }) { value={category} onChange={onCategoryChange} > - + {CATEGORIES.map((value) => (
    ); diff --git a/src/components/Modal/Modal.jsx b/src/components/Modal/Modal.jsx index 1164c19..7c55306 100644 --- a/src/components/Modal/Modal.jsx +++ b/src/components/Modal/Modal.jsx @@ -1,6 +1,15 @@ +import { useEffect } from "react"; import styles from "./Modal.module.css"; export default function Modal({ children, title, onClose }) { + useEffect(() => { + function handleKeyDown(e) { + if (e.key === "Escape") onClose(); + } + document.addEventListener("keydown", handleKeyDown); + return () => document.removeEventListener("keydown", handleKeyDown); + }, [onClose]); + return ( <>
    From 6f208ecbb8a4584229514a37a7e7c82fd9e67624 Mon Sep 17 00:00:00 2001 From: Nam Yooseong Date: Sun, 14 Jun 2026 19:40:48 +0900 Subject: [PATCH 12/13] =?UTF-8?q?docs:=20=EB=A6=AC=ED=8C=A9=ED=86=A0?= =?UTF-8?q?=EB=A7=81=20=EB=82=B4=EC=9A=A9=20README=20=EC=97=85=EB=8D=B0?= =?UTF-8?q?=EC=9D=B4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 139 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 139 insertions(+) diff --git a/README.md b/README.md index 49fdb28..a752a60 100644 --- a/README.md +++ b/README.md @@ -266,3 +266,142 @@ async function handleRestaurantSubmit(restaurant) { setIsAddRestaurantModalOpen(false); } ``` + +### api.js์—์„œ try/catch ์ œ๊ฑฐ + +`api.js`์—์„œ `try/catch`๋กœ ์žก๊ณ  `throw`๋กœ ๋‹ค์‹œ ๋˜์ง€๋Š” ๊ฑด `try/catch`๋ฅผ ์•ˆ ์“ด ๊ฒƒ๊ณผ ๊ฒฐ๊ณผ๊ฐ€ ๊ฐ™๋‹ค. ์ฝ˜์†” ๋กœ๊ทธ๋งŒ ๋‚จ๊ธฐ๊ณ  ์—๋Ÿฌ๋ฅผ ๊ทธ๋Œ€๋กœ ์œ„๋กœ ์˜ฌ๋ฆฌ๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค. ์—๋Ÿฌ๋ฅผ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ๋Š” ๊ณณ(ํ›…, ์ปดํฌ๋„ŒํŠธ)์—์„œ๋งŒ ์žก๋„๋ก `api.js`๋Š” ๋‹จ์ˆœํ•˜๊ฒŒ throw๋งŒ ํ•˜๊ฒŒ ๋ณ€๊ฒฝํ–ˆ๋‹ค. + +```js +// Before: try/catch๋กœ ์žก์•˜๋‹ค๊ฐ€ ๋‹ค์‹œ throw โ€” ์˜๋ฏธ ์—†๋Š” ์ค‘๊ฐ„ ์ฒ˜๋ฆฌ +export async function getRestaurants() { + try { + const response = await fetch(`${BASE_URL}/restaurants`); + if (!response.ok) throw new Error(`์„œ๋ฒ„ ์˜ค๋ฅ˜: ${response.status}`); + return await response.json(); + } catch (error) { + console.error("์Œ์‹์  ๋ชฉ๋ก ์กฐํšŒ ์‹คํŒจ:", error); + throw error; + } +} + +// After: ๋‹จ์ˆœํ•˜๊ฒŒ throw๋งŒ +export async function getRestaurants() { + const response = await fetch(`${BASE_URL}/restaurants`); + if (!response.ok) throw new Error(`์„œ๋ฒ„ ์˜ค๋ฅ˜: ${response.status}`); + return response.json(); +} +``` + +### ์—๋Ÿฌ ํ•ธ๋“ค๋ง ์ถ”๊ฐ€ + +์—๋Ÿฌ๋Š” ์‚ฌ์šฉ์ž์—๊ฒŒ ๋ณด์—ฌ์ค„ ์ˆ˜ ์žˆ๋Š” ๊ฐ€์žฅ ๊ฐ€๊นŒ์šด ๊ณณ์—์„œ ํ•œ ๋ฒˆ๋งŒ ์žก๋Š”๋‹ค. ์ค‘๊ฐ„ ํ•จ์ˆ˜(`addRestaurant`)์—์„œ ์žก์œผ๋ฉด ์—๋Ÿฌ๊ฐ€ ๊ฑฐ๊ธฐ์„œ ์†Œ๋ฉธ๋˜์–ด ์ตœ์ข… ํ˜ธ์ถœ์ž๊ฐ€ ์‹คํŒจ ์—ฌ๋ถ€๋ฅผ ์•Œ ์ˆ˜ ์—†๋‹ค. + +- ์ดˆ๊ธฐ ๋กœ๋”ฉ ์‹คํŒจ: `fetchRestaurants`์˜ `try/catch`์—์„œ `error` state์— ๋‹ด์•„ ํ™”๋ฉด์— ํ‘œ์‹œ +- ์Œ์‹์  ์ถ”๊ฐ€ ์‹คํŒจ: `handleRestaurantSubmit`์˜ `try/catch`์—์„œ alert๋กœ ์‚ฌ์šฉ์ž์—๊ฒŒ ์•Œ๋ฆผ + +```js +// useRestaurants.js โ€” ์ดˆ๊ธฐ ๋กœ๋”ฉ ์—๋Ÿฌ ์ฒ˜๋ฆฌ +const fetchRestaurants = useCallback(async () => { + setIsLoading(true); + try { + const data = await getRestaurants(); + setRestaurants(data); + } catch (error) { + setError("์Œ์‹์  ๋ชฉ๋ก์„ ๋ถˆ๋Ÿฌ์˜ค์ง€ ๋ชปํ–ˆ์Šต๋‹ˆ๋‹ค."); + } finally { + setIsLoading(false); + } +}, []); + +// App.jsx โ€” ์ถ”๊ฐ€ ์‹คํŒจ ์—๋Ÿฌ ์ฒ˜๋ฆฌ +async function handleRestaurantSubmit(restaurant) { + try { + await addRestaurant(restaurant); + setIsAddRestaurantModalOpen(false); + } catch { + alert("์Œ์‹์  ์ถ”๊ฐ€์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค. ๋‹ค์‹œ ์‹œ๋„ํ•ด์ฃผ์„ธ์š”."); + } +} +``` + +### ๋งค์ง ์ŠคํŠธ๋ง ์ƒ์ˆ˜ํ™” โ€” `ALL_CATEGORY` + +`"์ „์ฒด"` ๋ฌธ์ž์—ด์ด `App.jsx`, `CategoryFilter.jsx`, `filterRestaurants.js` ์„ธ ๊ณณ์— ํฉ์–ด์ ธ ์žˆ์—ˆ๋‹ค. ํ•˜๋‚˜๋ผ๋„ ์ˆ˜์ •ํ•˜๋ฉด ๋‚˜๋จธ์ง€๋„ ํ•จ๊ป˜ ๋ฐ”๊ฟ”์•ผ ํ•˜๋Š” ์•”๋ฌต์  ๊ฒฐํ•ฉ์ด๋‹ค. ์ƒ์ˆ˜๋กœ ์ถ”์ถœํ•ด ํ•œ ๊ณณ์—์„œ ๊ด€๋ฆฌํ•œ๋‹ค. + +```js +// Before: ์„ธ ๊ณณ์— ํฉ์–ด์ง„ ๋งค์ง ์ŠคํŠธ๋ง +useState("์ „์ฒด"); + +if (category === "์ „์ฒด") return restaurants; + +// After: ์ƒ์ˆ˜๋กœ ๋‹จ์ผํ™” +export const ALL_CATEGORY = "์ „์ฒด"; + +useState(ALL_CATEGORY); + +if (category === ALL_CATEGORY) return restaurants; +``` + +### import ํ™•์žฅ์ž ์ผ๊ด€์„ฑ + +Vite ํ”„๋กœ์ ํŠธ์—์„œ ์ผ๋ถ€ ํŒŒ์ผ์€ `.jsx`/`.js` ํ™•์žฅ์ž๋ฅผ ๋ช…์‹œํ•˜๊ณ  ์ผ๋ถ€๋Š” ์ƒ๋žตํ•ด ํ˜ผ์žฌ๋œ ์ƒํƒœ์˜€๋‹ค. Vite๋Š” ESM ํ‘œ์ค€์— ๊ฐ€๊น๊ฒŒ ํ™•์žฅ์ž ๋ช…์‹œ๋ฅผ ๊ถŒ์žฅํ•˜๋ฏ€๋กœ ๋ชจ๋“  ๋กœ์ปฌ import์— ํ™•์žฅ์ž๋ฅผ ์ถ”๊ฐ€ํ–ˆ๋‹ค. + +```js +// Before +import Modal from "../Modal/Modal"; +import { CATEGORIES } from "../../constants/categories"; + +// After +import Modal from "../Modal/Modal.jsx"; +import { CATEGORIES } from "../../constants/categories.js"; +``` + +### isLoading state ์ถ”๊ฐ€ + +๋ฐ์ดํ„ฐ๋ฅผ ๋ถˆ๋Ÿฌ์˜ค๋Š” ๋™์•ˆ ํ™”๋ฉด์ด ๋นˆ ์ƒํƒœ๋กœ ๋ณด์ด๋Š” ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด `isLoading` state๋ฅผ ์ถ”๊ฐ€ํ–ˆ๋‹ค. `finally`๋ฅผ ์‚ฌ์šฉํ•ด ์„ฑ๊ณต/์‹คํŒจ ์—ฌ๋ถ€์™€ ๊ด€๊ณ„์—†์ด ๋กœ๋”ฉ ์ƒํƒœ๊ฐ€ ๋ฐ˜๋“œ์‹œ ํ•ด์ œ๋˜๋„๋ก ํ–ˆ๋‹ค. + +```js +const [isLoading, setIsLoading] = useState(false); + +const fetchRestaurants = useCallback(async () => { + setIsLoading(true); + try { + const data = await getRestaurants(); + setRestaurants(data); + } catch (error) { + setError("์Œ์‹์  ๋ชฉ๋ก์„ ๋ถˆ๋Ÿฌ์˜ค์ง€ ๋ชปํ–ˆ์Šต๋‹ˆ๋‹ค."); + } finally { + setIsLoading(false); // ์„ฑ๊ณต/์‹คํŒจ ๋ชจ๋‘ ๋กœ๋”ฉ ํ•ด์ œ + } +}, []); + +return { restaurants, addRestaurant, isLoading, error }; +``` + +### Modal Escape ํ‚ค ์ฒ˜๋ฆฌ + +backdrop ํด๋ฆญ์œผ๋กœ๋งŒ ๋ชจ๋‹ฌ์„ ๋‹ซ์„ ์ˆ˜ ์žˆ์–ด ํ‚ค๋ณด๋“œ ์‚ฌ์šฉ์ž๊ฐ€ ๋‹ซ์„ ์ˆ˜ ์—†๋Š” ์ ‘๊ทผ์„ฑ ๋ฌธ์ œ๊ฐ€ ์žˆ์—ˆ๋‹ค. `useEffect`๋กœ keydown ์ด๋ฒคํŠธ๋ฅผ ๋“ฑ๋กํ•ด Escape ํ‚ค๋กœ๋„ ๋‹ซํžˆ๋„๋ก ํ–ˆ๋‹ค. + +cleanup ํ•จ์ˆ˜๋กœ ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ๋ฅผ ์ œ๊ฑฐํ•˜์ง€ ์•Š์œผ๋ฉด ๋ชจ๋‹ฌ์ด ๋‹ซํ˜€๋„ ๋ฆฌ์Šค๋„ˆ๊ฐ€ ๋‚จ์•„, ๋ชจ๋‹ฌ์„ ์—ฌ๋Ÿฌ ๋ฒˆ ์—ด๊ณ  ๋‹ซ์„์ˆ˜๋ก ๋ฆฌ์Šค๋„ˆ๊ฐ€ ๋ˆ„์ ๋˜์–ด `onClose`๊ฐ€ ์ค‘๋ณต ํ˜ธ์ถœ๋œ๋‹ค. + +```js +useEffect(() => { + function handleKeyDown(e) { + if (e.key === "Escape") onClose(); + } + document.addEventListener("keydown", handleKeyDown); + return () => document.removeEventListener("keydown", handleKeyDown); +}, [onClose]); +``` + +### Header ์ด๋ฏธ์ง€ alt ์ค‘๋ณต ์ œ๊ฑฐ + +๋ฒ„ํŠผ์— `aria-label="์Œ์‹์  ์ถ”๊ฐ€"`๊ฐ€ ์žˆ๋Š”๋ฐ ๋‚ด๋ถ€ ์ด๋ฏธ์ง€์—๋„ `alt="์Œ์‹์  ์ถ”๊ฐ€"`๊ฐ€ ์žˆ์–ด ์Šคํฌ๋ฆฐ ๋ฆฌ๋”๊ฐ€ "์Œ์‹์  ์ถ”๊ฐ€ ์Œ์‹์  ์ถ”๊ฐ€"๋ฅผ ๋‘ ๋ฒˆ ์ฝ๋Š” ๋ฌธ์ œ๊ฐ€ ์žˆ์—ˆ๋‹ค. ๋ฒ„ํŠผ ์•ˆ์˜ ์ด๋ฏธ์ง€๋Š” ๋ฒ„ํŠผ ์ž์ฒด๊ฐ€ ์˜๋ฏธ๋ฅผ ์ „๋‹ฌํ•˜๋ฏ€๋กœ ์žฅ์‹์  ์—ญํ• ์ด๋‹ค. `alt` ์†์„ฑ์„ ์ œ๊ฑฐํ•ด ์Šคํฌ๋ฆฐ ๋ฆฌ๋”๊ฐ€ ์ด๋ฏธ์ง€๋ฅผ ๊ฑด๋„ˆ๋›ฐ๋„๋ก ํ–ˆ๋‹ค. + +```jsx +// Before +์Œ์‹์  ์ถ”๊ฐ€ + +// After + +``` From 1f0cb944257589607e9345f20ef5961d9f748492 Mon Sep 17 00:00:00 2001 From: Nam Yooseong Date: Mon, 15 Jun 2026 21:40:16 +0900 Subject: [PATCH 13/13] =?UTF-8?q?docs:=20README=20=EC=97=85=EB=8D=B0?= =?UTF-8?q?=EC=9D=B4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 128 +++++++++++++++++++++++------------------------------- 1 file changed, 54 insertions(+), 74 deletions(-) diff --git a/README.md b/README.md index a752a60..e9671f0 100644 --- a/README.md +++ b/README.md @@ -119,80 +119,6 @@ useEffect(() => { }, []); ``` -### e.preventDefault()๋ฅผ ์ œ๊ฑฐํ•˜๋ฉด ์•ˆ ๋œ๋‹ค๋Š” ๊ฒƒ์„ ๋†“์นœ ๋ฌธ์ œ - -API ์—ฐ๋™์œผ๋กœ ์ „ํ™˜ํ•˜๋ฉด์„œ "์ด์ œ ์„œ๋ฒ„์— ์ œ์ถœํ•˜๋‹ˆ๊นŒ `e.preventDefault()`๊ฐ€ ํ•„์š” ์—†๋‹ค"๊ณ  ์ƒ๊ฐํ–ˆ๋‹ค. ๊ทธ๋Ÿฌ๋‚˜ `e.preventDefault()`๋Š” ์„œ๋ฒ„ ์ œ์ถœ๊ณผ ๋ฌด๊ด€ํ•˜๊ฒŒ **๋ธŒ๋ผ์šฐ์ €์˜ ๊ธฐ๋ณธ ํผ ๋™์ž‘(ํŽ˜์ด์ง€ ์ƒˆ๋กœ๊ณ ์นจ)์„ ๋ง‰๋Š” ์—ญํ• **์ด๋‹ค. - -`e.preventDefault()` ์—†์ด ํผ์„ submitํ•˜๋ฉด React์˜ `fetch` ํ˜ธ์ถœ๊ณผ ๋ฌด๊ด€ํ•˜๊ฒŒ ๋ธŒ๋ผ์šฐ์ €๊ฐ€ ํŽ˜์ด์ง€๋ฅผ ์ƒˆ๋กœ๊ณ ์นจํ•œ๋‹ค. `e.preventDefault()`๋Š” ํ•ญ์ƒ ํ•„์š”ํ•˜๋‹ค. - ---- - -## ๐Ÿ›  ๋ฆฌํŒฉํ† ๋ง - -### state ๋„ค์ด๋ฐ โ€” ์šฉ๋„๊ฐ€ ์•„๋‹Œ ๊ฐ’์˜ ์„ฑ๊ฒฉ์œผ๋กœ - -`filterCategory`๋Š” ์ด state๊ฐ€ ํ•„ํ„ฐ๋ง์— ์“ฐ์ธ๋‹ค๋Š” **์šฉ๋„**๋ฅผ ํ‘œํ˜„ํ•œ๋‹ค. state ์ด๋ฆ„์€ ์–ด๋””์— ์“ฐ์ด๋Š”์ง€๊ฐ€ ์•„๋‹ˆ๋ผ **์–ด๋–ค ๊ฐ’์„ ๋‹ด๊ณ  ์žˆ๋Š”์ง€**๋ฅผ ๋‚˜ํƒ€๋‚ด๋Š” ๊ฒŒ ์ข‹๋‹ค. - -```jsx -// Before: ์šฉ๋„ ํ‘œํ˜„ -const [filterCategory, setFilterCategory] = useState("์ „์ฒด"); - -// After: ๊ฐ’์˜ ์„ฑ๊ฒฉ ํ‘œํ˜„ -const [selectedCategory, setSelectedCategory] = useState("์ „์ฒด"); -``` - -### API ํ•จ์ˆ˜๋ช… โ€” HTTP ๋ฉ”์„œ๋“œ๊ฐ€ ์•„๋‹Œ ์˜๋„๋กœ - -`postRestaurant`๋Š” HTTP ๋ฉ”์„œ๋“œ ์ด๋ฆ„(`post`)์„ ๊ทธ๋Œ€๋กœ ์“ด ๊ฒƒ์ด๋‹ค. ํ•จ์ˆ˜๋ช…์€ ์–ด๋–ป๊ฒŒ ๋™์ž‘ํ•˜๋Š”์ง€๊ฐ€ ์•„๋‹ˆ๋ผ ๋ฌด์—‡์„ ํ•˜๋ ค๋Š”์ง€๋ฅผ ๋‚˜ํƒ€๋‚ด์•ผ ํ•œ๋‹ค. - -```js -// Before: ๊ตฌํ˜„ ๋ฐฉ๋ฒ• ํ‘œํ˜„ -export async function postRestaurant(restaurant) { ... } - -// After: ์˜๋„ ํ‘œํ˜„ -export async function addRestaurant(restaurant) { ... } -``` - -### BASE_URL ์ƒ์ˆ˜ ์ถ”์ถœ - -`http://localhost:3000`์ด `getRestaurants`์™€ `addRestaurant` ๋‘ ๊ณณ์— ๋ฐ˜๋ณต๋๋‹ค. ์ƒ์ˆ˜๋กœ ์ถ”์ถœํ•ด ํ•œ ๊ณณ์—์„œ ๊ด€๋ฆฌํ•œ๋‹ค. - -```js -const BASE_URL = "http://localhost:3000"; -``` - -### ๋ฐ˜ํ™˜๊ฐ’ ๋ณ€์ˆ˜๋ช… โ€” `jsonData` โ†’ `restaurants` - -`response.json()`์œผ๋กœ ํŒŒ์‹ฑ์ด ์™„๋ฃŒ๋œ ๋’ค์—๋„ `jsonData`๋ผ๋Š” ์ด๋ฆ„์„ ์“ฐ๋ฉด ํ˜•์‹(JSON)์„ ๊ฐ•์กฐํ•˜๊ฒŒ ๋œ๋‹ค. ํŒŒ์‹ฑ ํ›„์—๋Š” ์‹ค์ œ ๋‚ด์šฉ์„ ๋‚˜ํƒ€๋‚ด๋Š” ์ด๋ฆ„์ด ๋” ๋ช…ํ™•ํ•˜๋‹ค. - -```js -// Before -const jsonData = await response.json(); - -// After -const restaurants = await response.json(); -``` - -### try/catch ์ถ”๊ฐ€ - -`fetch`๋Š” ๋„คํŠธ์›Œํฌ ์˜ค๋ฅ˜์—์„œ๋งŒ throwํ•˜๊ณ , HTTP 4xx/5xx ์‘๋‹ต์€ throwํ•˜์ง€ ์•Š๋Š”๋‹ค. ๋‘ ๊ฐ€์ง€๋ฅผ ๋ชจ๋‘ ์ฒ˜๋ฆฌํ•˜๋ ค๋ฉด `response.ok` ํ™•์ธ๊ณผ `try/catch`๊ฐ€ ํ•จ๊ป˜ ํ•„์š”ํ•˜๋‹ค. - -```js -export async function getRestaurants() { - try { - const response = await fetch(`${BASE_URL}/restaurants`); - if (!response.ok) throw new Error(`์„œ๋ฒ„ ์˜ค๋ฅ˜: ${response.status}`); - const restaurants = await response.json(); - return restaurants; - } catch (error) { - console.error("์Œ์‹์  ๋ชฉ๋ก ์กฐํšŒ ์‹คํŒจ:", error); - throw error; // ํ˜ธ์ถœํ•œ ์ชฝ์ด ์—๋Ÿฌ๋ฅผ ์•Œ ์ˆ˜ ์žˆ๋„๋ก ๋‹ค์‹œ ๋˜์ง - } -} -``` - -catch์—์„œ `throw error`๋ฅผ ๋‹ค์‹œ ๋˜์ง€๋Š” ์ด์œ : ๋กœ๊ทธ๋งŒ ๋‚จ๊ธฐ๊ณ  ์‚ผ์ผœ๋ฒ„๋ฆฌ๋ฉด ํ˜ธ์ถœํ•œ ์ชฝ(`App.jsx`)์ด ์—๋Ÿฌ ๋ฐœ์ƒ ์—ฌ๋ถ€๋ฅผ ์•Œ ์ˆ˜ ์—†๋‹ค. - ### ์ปค์Šคํ…€ ํ›… ๋ถ„๋ฆฌ โ€” `useRestaurants` `App`์ด ๋ Œ๋”๋ง ๊ตฌ์กฐ์™€ UI ์ƒํƒœ๋ฅผ ๋‹ค๋ฃจ๋Š” ์ปดํฌ๋„ŒํŠธ์ธ๋ฐ, ์„œ๋ฒ„ ํ†ต์‹ ๊ณผ ๋ฐ์ดํ„ฐ ๊ด€๋ฆฌ ๋กœ์ง๊นŒ์ง€ ํ•จ๊ป˜ ๋“ค์–ด์žˆ์–ด ์—ญํ• ์ด ์„ž์—ฌ ์žˆ์—ˆ๋‹ค. `restaurants` state, `useEffect`, fetch ๋กœ์ง์„ ์ปค์Šคํ…€ ํ›…์œผ๋กœ ์ถ”์ถœํ–ˆ๋‹ค. @@ -267,6 +193,60 @@ async function handleRestaurantSubmit(restaurant) { } ``` +## ๐Ÿ›  ๋ฆฌํŒฉํ† ๋ง + +### state ๋„ค์ด๋ฐ โ€” ์šฉ๋„๊ฐ€ ์•„๋‹Œ ๊ฐ’์˜ ์„ฑ๊ฒฉ์œผ๋กœ + +`filterCategory`๋Š” ์ด state๊ฐ€ ํ•„ํ„ฐ๋ง์— ์“ฐ์ธ๋‹ค๋Š” **์šฉ๋„**๋ฅผ ํ‘œํ˜„ํ•œ๋‹ค. state ์ด๋ฆ„์€ ์–ด๋””์— ์“ฐ์ด๋Š”์ง€๊ฐ€ ์•„๋‹ˆ๋ผ **์–ด๋–ค ๊ฐ’์„ ๋‹ด๊ณ  ์žˆ๋Š”์ง€**๋ฅผ ๋‚˜ํƒ€๋‚ด๋Š” ๊ฒŒ ์ข‹๋‹ค. + +```jsx +// Before: ์šฉ๋„ ํ‘œํ˜„ +const [filterCategory, setFilterCategory] = useState("์ „์ฒด"); + +// After: ๊ฐ’์˜ ์„ฑ๊ฒฉ ํ‘œํ˜„ +const [selectedCategory, setSelectedCategory] = useState("์ „์ฒด"); +``` + +### API ํ•จ์ˆ˜๋ช… โ€” HTTP ๋ฉ”์„œ๋“œ๊ฐ€ ์•„๋‹Œ ์˜๋„๋กœ + +`postRestaurant`๋Š” HTTP ๋ฉ”์„œ๋“œ ์ด๋ฆ„(`post`)์„ ๊ทธ๋Œ€๋กœ ์“ด ๊ฒƒ์ด๋‹ค. ํ•จ์ˆ˜๋ช…์€ ์–ด๋–ป๊ฒŒ ๋™์ž‘ํ•˜๋Š”์ง€๊ฐ€ ์•„๋‹ˆ๋ผ ๋ฌด์—‡์„ ํ•˜๋ ค๋Š”์ง€๋ฅผ ๋‚˜ํƒ€๋‚ด์•ผ ํ•œ๋‹ค. + +```js +// Before: ๊ตฌํ˜„ ๋ฐฉ๋ฒ• ํ‘œํ˜„ +export async function postRestaurant(restaurant) { ... } + +// After: ์˜๋„ ํ‘œํ˜„ +export async function addRestaurant(restaurant) { ... } +``` + +### BASE_URL ์ƒ์ˆ˜ ์ถ”์ถœ + +`http://localhost:3000`์ด `getRestaurants`์™€ `addRestaurant` ๋‘ ๊ณณ์— ๋ฐ˜๋ณต๋๋‹ค. ์ƒ์ˆ˜๋กœ ์ถ”์ถœํ•ด ํ•œ ๊ณณ์—์„œ ๊ด€๋ฆฌํ•œ๋‹ค. + +```js +const BASE_URL = "http://localhost:3000"; +``` + +### try/catch ์ถ”๊ฐ€ + +`fetch`๋Š” ๋„คํŠธ์›Œํฌ ์˜ค๋ฅ˜์—์„œ๋งŒ throwํ•˜๊ณ , HTTP 4xx/5xx ์‘๋‹ต์€ throwํ•˜์ง€ ์•Š๋Š”๋‹ค. ๋‘ ๊ฐ€์ง€๋ฅผ ๋ชจ๋‘ ์ฒ˜๋ฆฌํ•˜๋ ค๋ฉด `response.ok` ํ™•์ธ๊ณผ `try/catch`๊ฐ€ ํ•จ๊ป˜ ํ•„์š”ํ•˜๋‹ค. + +```js +export async function getRestaurants() { + try { + const response = await fetch(`${BASE_URL}/restaurants`); + if (!response.ok) throw new Error(`์„œ๋ฒ„ ์˜ค๋ฅ˜: ${response.status}`); + const restaurants = await response.json(); + return restaurants; + } catch (error) { + console.error("์Œ์‹์  ๋ชฉ๋ก ์กฐํšŒ ์‹คํŒจ:", error); + throw error; // ํ˜ธ์ถœํ•œ ์ชฝ์ด ์—๋Ÿฌ๋ฅผ ์•Œ ์ˆ˜ ์žˆ๋„๋ก ๋‹ค์‹œ ๋˜์ง + } +} +``` + +catch์—์„œ `throw error`๋ฅผ ๋‹ค์‹œ ๋˜์ง€๋Š” ์ด์œ : ๋กœ๊ทธ๋งŒ ๋‚จ๊ธฐ๊ณ  ์‚ผ์ผœ๋ฒ„๋ฆฌ๋ฉด ํ˜ธ์ถœํ•œ ์ชฝ(`App.jsx`)์ด ์—๋Ÿฌ ๋ฐœ์ƒ ์—ฌ๋ถ€๋ฅผ ์•Œ ์ˆ˜ ์—†๋‹ค. + ### api.js์—์„œ try/catch ์ œ๊ฑฐ `api.js`์—์„œ `try/catch`๋กœ ์žก๊ณ  `throw`๋กœ ๋‹ค์‹œ ๋˜์ง€๋Š” ๊ฑด `try/catch`๋ฅผ ์•ˆ ์“ด ๊ฒƒ๊ณผ ๊ฒฐ๊ณผ๊ฐ€ ๊ฐ™๋‹ค. ์ฝ˜์†” ๋กœ๊ทธ๋งŒ ๋‚จ๊ธฐ๊ณ  ์—๋Ÿฌ๋ฅผ ๊ทธ๋Œ€๋กœ ์œ„๋กœ ์˜ฌ๋ฆฌ๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค. ์—๋Ÿฌ๋ฅผ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ๋Š” ๊ณณ(ํ›…, ์ปดํฌ๋„ŒํŠธ)์—์„œ๋งŒ ์žก๋„๋ก `api.js`๋Š” ๋‹จ์ˆœํ•˜๊ฒŒ throw๋งŒ ํ•˜๊ฒŒ ๋ณ€๊ฒฝํ–ˆ๋‹ค.