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 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 diff --git a/README.md b/README.md index e206bd7..e9671f0 100644 --- a/README.md +++ b/README.md @@ -1,181 +1,387 @@ -# ์žฌ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ ์ปดํฌ๋„ŒํŠธ ์„ค๊ณ„ +# 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` ํ•˜๋‚˜๋กœ ์ถฉ๋ถ„ํ•˜๋‹ค. + +--- -์Œ์‹์ ์„ ์ถ”๊ฐ€ํ•œ ๋’ค ํผ ์ž…๋ ฅ๊ฐ’์„ ์ดˆ๊ธฐํ™”ํ•˜์ง€ ์•Š์œผ๋ฉด ๋ชจ๋‹ฌ์„ ๋‹ค์‹œ ์—ด์—ˆ์„ ๋•Œ ์ด์ „ ๊ฐ’์ด ๋‚จ์•„์žˆ๋‹ค. +## ๐Ÿค” ๊ณ ๋ฏผํ–ˆ๋˜ ๋ฌธ์ œ์™€ ํ•ด๊ฒฐ ๊ณผ์ •์—์„œ ๋ฐฐ์šด ์  + +### async ํ•จ์ˆ˜๋ฅผ ์ •์˜๋งŒ ํ•˜๊ณ  ํ˜ธ์ถœํ•˜์ง€ ์•Š์€ ๋ฌธ์ œ -์ดˆ๊ธฐ ๊ตฌํ˜„์—์„œ๋Š” ํผ state๊ฐ€ App์— ์žˆ์—ˆ๊ธฐ ๋•Œ๋ฌธ์— ์ถ”๊ฐ€ ์™„๋ฃŒ ์‹œ์ ์— ๋ช…์‹œ์ ์œผ๋กœ ์ดˆ๊ธฐํ™”ํ–ˆ๋‹ค. +`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("")`์˜ ์ดˆ๊ธฐ๊ฐ’์œผ๋กœ ์ž๋™ ์ดˆ๊ธฐํ™”๋œ๋‹ค. +### ์ปค์Šคํ…€ ํ›… ๋ถ„๋ฆฌ โ€” `useRestaurants` ---- +`App`์ด ๋ Œ๋”๋ง ๊ตฌ์กฐ์™€ UI ์ƒํƒœ๋ฅผ ๋‹ค๋ฃจ๋Š” ์ปดํฌ๋„ŒํŠธ์ธ๋ฐ, ์„œ๋ฒ„ ํ†ต์‹ ๊ณผ ๋ฐ์ดํ„ฐ ๊ด€๋ฆฌ ๋กœ์ง๊นŒ์ง€ ํ•จ๊ป˜ ๋“ค์–ด์žˆ์–ด ์—ญํ• ์ด ์„ž์—ฌ ์žˆ์—ˆ๋‹ค. `restaurants` state, `useEffect`, fetch ๋กœ์ง์„ ์ปค์Šคํ…€ ํ›…์œผ๋กœ ์ถ”์ถœํ–ˆ๋‹ค. -## ๐Ÿค” ๊ณ ๋ฏผํ–ˆ๋˜ ๋ฌธ์ œ์™€ ํ•ด๊ฒฐ ๊ณผ์ •์—์„œ ๋ฐฐ์šด ์  +### `fetchRestaurants`๋ฅผ `useCallback`์œผ๋กœ ์ถ”์ถœ -### ์–ด๋–ค state๋ฅผ ์–ด๋А ์ปดํฌ๋„ŒํŠธ๊ฐ€ ์†Œ์œ ํ•ด์•ผ ํ•˜๋Š”๊ฐ€ +`fetchRestaurants`๊ฐ€ `useEffect` ๋‚ด๋ถ€์— ์ •์˜๋˜์–ด ์žˆ์–ด `addRestaurant`์—์„œ ๊ฐ™์€ ๋กœ์ง์„ ์ค‘๋ณต์œผ๋กœ ์ž‘์„ฑํ•ด์•ผ ํ–ˆ๋‹ค. ํ•จ์ˆ˜๋ฅผ ํ›… ์Šค์ฝ”ํ”„๋กœ ๊บผ๋‚ด ์žฌ์‚ฌ์šฉํ•˜๋ ค๋ฉด `useCallback`์ด ํ•„์š”ํ•˜๋‹ค. -์Œ์‹์  ์ถ”๊ฐ€ ๊ธฐ๋Šฅ์„ ๊ตฌํ˜„ํ•˜๋ฉด์„œ ๋‹ค์Œ state๋“ค์ด ํ•„์š”ํ•˜๋‹ค๊ณ  ์ƒ๊ฐํ–ˆ๋‹ค. +`useCallback` ์—†์ด ์ผ๋ฐ˜ ํ•จ์ˆ˜๋กœ ์„ ์–ธํ•˜๋ฉด ๋ Œ๋”๋ง๋งˆ๋‹ค ์ƒˆ ํ•จ์ˆ˜ ์ฐธ์กฐ๊ฐ€ ์ƒ์„ฑ๋˜์–ด `useEffect([fetchRestaurants])`๊ฐ€ ๋งค ๋ Œ๋”๋ง๋งˆ๋‹ค ์‹คํ–‰๋˜๋Š” ๋ฌดํ•œ ๋ฃจํ”„๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค. `useCallback`์˜ ๋นˆ ์˜์กด์„ฑ ๋ฐฐ์—ด `[]`์ด ์ฐธ์กฐ๋ฅผ ๊ณ ์ •ํ•ด ์ด๋ฅผ ๋ฐฉ์ง€ํ•œ๋‹ค. -- `restaurants` โ€” ์Œ์‹์  ๋ชฉ๋ก -- `category`, `name`, `description` โ€” ํผ ์ž…๋ ฅ๊ฐ’ -- `isAddRestaurantModalOpen` - ๋ชจ๋‹ฌ ์—ด๋ฆผ/๋‹ซํž˜ +```js +// Before: fetchRestaurants๊ฐ€ useEffect ์•ˆ์— ๊ฐ‡ํ˜€ ์žˆ์–ด addRestaurant์—์„œ ๋กœ์ง์„ ์ค‘๋ณต ์ž‘์„ฑ +export function useRestaurants() { + const [restaurants, setRestaurants] = useState([]); -์ด state๋“ค์„ ์–ด๋А ์ปดํฌ๋„ŒํŠธ์— ์„ ์–ธํ• ์ง€ ๊ฒฐ์ •ํ•˜๊ธฐ ์œ„ํ•ด "์ด state๋ฅผ ๋ˆ„๊ฐ€ ํ•„์š”๋กœ ํ•˜๋Š”๊ฐ€"๋ฅผ ๊ธฐ์ค€์œผ๋กœ ํŒ๋‹จํ–ˆ๋‹ค. + useEffect(() => { + const fetchRestaurants = async () => { + const data = await getRestaurants(); + setRestaurants(data); + }; + fetchRestaurants(); + }, []); -`restaurants`๋Š” AddRestaurantModal(์ถ”๊ฐ€ ์‹œ ๊ฐฑ์‹ )๊ณผ RestaurantList(๋ชฉ๋ก ๋ Œ๋”๋ง) ๋ชจ๋‘ ํ•„์š”ํ•˜๋‹ค. ๋‘ ์ปดํฌ๋„ŒํŠธ๋Š” ํ˜•์ œ ๊ด€๊ณ„๋ผ ์„œ๋กœ์˜ state์— ์ง์ ‘ ์ ‘๊ทผํ•  ์ˆ˜ ์—†์œผ๋ฏ€๋กœ, ๊ณตํ†ต ๋ถ€๋ชจ์ธ App์œผ๋กœ ๋Œ์–ด์˜ฌ๋ ธ๋‹ค(Lifting State Up). + async function addRestaurant(restaurant) { + await createRestaurant(restaurant); + const data = await getRestaurants(); // ์ค‘๋ณต + setRestaurants(data); // ์ค‘๋ณต + } -`category`, `name`, `description`์€ ํผ์„ ์ž…๋ ฅํ•˜๋Š” ๋™์•ˆ AddRestaurantModal ์•ˆ์—์„œ๋งŒ ์“ฐ์ธ๋‹ค. ์ถ”๊ฐ€ ์™„๋ฃŒ ์‹œ์ ์— ์ตœ์ข… ๋ฐ์ดํ„ฐ๋งŒ ๋ถ€๋ชจ๋กœ ์ „๋‹ฌํ•˜๋ฉด ๋˜๋ฏ€๋กœ, ๊ตณ์ด App๊นŒ์ง€ ์˜ฌ๋ฆด ํ•„์š”๊ฐ€ ์—†๋‹ค. AddRestaurantModal์ด ์ง์ ‘ ์†Œ์œ ํ•œ๋‹ค. + return { restaurants, addRestaurant }; +} + +// After: useCallback์œผ๋กœ ์ถ”์ถœํ•ด ์žฌ์‚ฌ์šฉ, ์˜์กด์„ฑ ๋ฐฐ์—ด๋„ ๋ช…์‹œ์ ์œผ๋กœ ์„ ์–ธ +export function useRestaurants() { + const [restaurants, setRestaurants] = useState([]); -`isAddRestaurantModalOpen`๋„ ๊ฐ™์€ ๊ธฐ์ค€์œผ๋กœ ํŒ๋‹จํ–ˆ๋‹ค. ๋ชจ๋‹ฌ์„ ์—ฌ๋Š” ํŠธ๋ฆฌ๊ฑฐ๋Š” Header์˜ ์ถ”๊ฐ€ ๋ฒ„ํŠผ์ด๊ณ , ์‹ค์ œ๋กœ ์—ด๋ฆฌ๋Š” ๊ฒƒ์€ AddRestaurantModal์ด๋‹ค. ๋‘ ์ปดํฌ๋„ŒํŠธ๋Š” ํ˜•์ œ ๊ด€๊ณ„๋ผ ์„œ๋กœ์˜ state์— ์ ‘๊ทผํ•  ์ˆ˜ ์—†์œผ๋ฏ€๋กœ ๊ณตํ†ต ๋ถ€๋ชจ์ธ App์ด ์†Œ์œ ํ•ด์•ผ ํ•œ๋‹ค. + const fetchRestaurants = useCallback(async () => { + const data = await getRestaurants(); + setRestaurants(data); + }, []); -### ์‹๋‹น์„ ์ถ”๊ฐ€ํ•ด๋„ ๋ชฉ๋ก์ด ์—…๋ฐ์ดํŠธ๋˜์ง€ ์•Š๋Š” ๋ฌธ์ œ + useEffect(() => { + void fetchRestaurants(); + }, [fetchRestaurants]); -`handleSubmit`์—์„œ `setRestaurants`๊ฐ€ ํ˜ธ์ถœ๋˜๋Š”๋ฐ๋„ ๋ชฉ๋ก์ด ๋ฐ”๋€Œ์ง€ ์•Š์•„์„œ ์›์ธ์„ ์ฐพ์•„๋ดค๋‹ค. `e.preventDefault()`๊ฐ€ ์—†์–ด์„œ submit ์‹œ ํŽ˜์ด์ง€๊ฐ€ ์ƒˆ๋กœ๊ณ ์นจ๋˜๊ณ , state ์—…๋ฐ์ดํŠธ๊ฐ€ ๋ฐ˜์˜๋˜๊ธฐ ์ „์— ์ดˆ๊ธฐ ์ƒํƒœ๋กœ ๋˜๋Œ์•„๊ฐ€๋Š” ๊ฒƒ์ด์—ˆ๋‹ค. state ์—…๋ฐ์ดํŠธ ์ž์ฒด๋Š” ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ์ž‘์„ฑ๋˜์–ด ์žˆ์—ˆ์ง€๋งŒ ๋ธŒ๋ผ์šฐ์ € ๊ธฐ๋ณธ ๋™์ž‘์„ ๋ง‰์ง€ ์•Š์•„์„œ ์ƒ๊ธด ๋ฌธ์ œ์˜€๋‹ค. + async function addRestaurant(restaurant) { + await createRestaurant(restaurant); + await fetchRestaurants(); // ์žฌ์‚ฌ์šฉ + } -### setRestaurants ์ธ์ž ์˜ค๋ฅ˜ + return { restaurants, addRestaurant }; +} +``` -์ฒ˜์Œ์— `setRestaurants(...restaurants, { ... })`๋กœ ์ž‘์„ฑํ–ˆ๋‹ค. ์ด๋ ‡๊ฒŒ ํ•˜๋ฉด ๋ฐฐ์—ด์ด ์•„๋‹Œ ์—ฌ๋Ÿฌ ์ธ์ž๋ฅผ `setRestaurants`์— ๋„˜๊ธฐ๋Š” ๊ฒƒ์ด๋ผ ์ƒˆ ๋ฐฐ์—ด์ด ๋งŒ๋“ค์–ด์ง€์ง€ ์•Š๋Š”๋‹ค. ๋ฐฐ์—ด ๋ฆฌํ„ฐ๋Ÿด ์•ˆ์—์„œ ์Šคํ”„๋ ˆ๋“œํ•ด์•ผ ํ•œ๋‹ค. +App์—์„œ๋Š” ์„œ๋ฒ„ ํ†ต์‹  ๋กœ์ง์ด ๋ชจ๋‘ ์‚ฌ๋ผ์ง€๊ณ , ๋ชจ๋‹ฌ ์—ด๋ฆผ/๋‹ซํž˜ ๊ฐ™์€ UI ์ƒํƒœ๋งŒ ๋‚จ์•˜๋‹ค. ```jsx -// โŒ ์ธ์ž๋ฅผ ์—ฌ๋Ÿฌ ๊ฐœ ์ „๋‹ฌํ•˜๋Š” ๊ฒƒ -setRestaurants(...restaurants, { id: Date.now(), ... }); +// Before: App์ด ์„œ๋ฒ„ ํ†ต์‹ ๊นŒ์ง€ ๋‹ด๋‹น +const [restaurants, setRestaurants] = useState([]); +useEffect(() => { /* fetch ๋กœ์ง */ }, []); +async function handleRestaurantSubmit(...) { + await postRestaurant(...); + const data = await getRestaurants(); + setRestaurants(data); +} -// โœ… ๊ธฐ์กด ํ•ญ๋ชฉ์„ ํŽผ์ณ์„œ ์ƒˆ ๋ฐฐ์—ด ์ƒ์„ฑ -setRestaurants([...restaurants, { id: Date.now(), ... }]); +// After: ํ›…์ด ๋ฐ์ดํ„ฐ ๊ด€๋ฆฌ๋ฅผ ๋‹ด๋‹น +const { restaurants, addRestaurant } = useRestaurants(); +async function handleRestaurantSubmit(restaurant) { + await addRestaurant(restaurant); + setIsAddRestaurantModalOpen(false); +} ``` ---- - ## ๐Ÿ›  ๋ฆฌํŒฉํ† ๋ง -### ์ด๋ฒคํŠธ ํ•ธ๋“ค๋Ÿฌ ๋„ค์ด๋ฐ ๊ทœ์น™ +### state ๋„ค์ด๋ฐ โ€” ์šฉ๋„๊ฐ€ ์•„๋‹Œ ๊ฐ’์˜ ์„ฑ๊ฒฉ์œผ๋กœ -`handle` + `[๋Œ€์ƒ]` + `[๋™์ž‘]` ํŒจํ„ด์„ ์‚ฌ์šฉํ•œ๋‹ค. ๋Œ€์ƒ์€ ํ•ญ์ƒ ๋ถ™์ธ๋‹ค. +`filterCategory`๋Š” ์ด state๊ฐ€ ํ•„ํ„ฐ๋ง์— ์“ฐ์ธ๋‹ค๋Š” **์šฉ๋„**๋ฅผ ํ‘œํ˜„ํ•œ๋‹ค. state ์ด๋ฆ„์€ ์–ด๋””์— ์“ฐ์ด๋Š”์ง€๊ฐ€ ์•„๋‹ˆ๋ผ **์–ด๋–ค ๊ฐ’์„ ๋‹ด๊ณ  ์žˆ๋Š”์ง€**๋ฅผ ๋‚˜ํƒ€๋‚ด๋Š” ๊ฒŒ ์ข‹๋‹ค. ```jsx -handleFormSubmit // Form + Submit -handleCategoryChange // Category + Change -handleDetailModalClose // DetailModal + Close +// Before: ์šฉ๋„ ํ‘œํ˜„ +const [filterCategory, setFilterCategory] = useState("์ „์ฒด"); + +// After: ๊ฐ’์˜ ์„ฑ๊ฒฉ ํ‘œํ˜„ +const [selectedCategory, setSelectedCategory] = useState("์ „์ฒด"); ``` -props๋กœ ๋„˜๊ธธ ๋•Œ๋Š” `on-`์œผ๋กœ ํ†ต์ผํ•œ๋‹ค. `on-`์€ ์ธํ„ฐํŽ˜์ด์Šค(๊ณ„์•ฝ), `handle-`์€ ๊ตฌํ˜„์ด๋‹ค. +### API ํ•จ์ˆ˜๋ช… โ€” HTTP ๋ฉ”์„œ๋“œ๊ฐ€ ์•„๋‹Œ ์˜๋„๋กœ -```jsx -// ์ •์˜๋Š” handle- -function handleNameChange(e) { ... } +`postRestaurant`๋Š” HTTP ๋ฉ”์„œ๋“œ ์ด๋ฆ„(`post`)์„ ๊ทธ๋Œ€๋กœ ์“ด ๊ฒƒ์ด๋‹ค. ํ•จ์ˆ˜๋ช…์€ ์–ด๋–ป๊ฒŒ ๋™์ž‘ํ•˜๋Š”์ง€๊ฐ€ ์•„๋‹ˆ๋ผ ๋ฌด์—‡์„ ํ•˜๋ ค๋Š”์ง€๋ฅผ ๋‚˜ํƒ€๋‚ด์•ผ ํ•œ๋‹ค. -// props๋กœ ๋„˜๊ธธ ๋•Œ๋Š” on- - +```js +// Before: ๊ตฌํ˜„ ๋ฐฉ๋ฒ• ํ‘œํ˜„ +export async function postRestaurant(restaurant) { ... } + +// After: ์˜๋„ ํ‘œํ˜„ +export async function addRestaurant(restaurant) { ... } ``` -### ํผ state๋ฅผ ์ž์‹์œผ๋กœ ๋‚ด๋ฆฌ๊ธฐ +### BASE_URL ์ƒ์ˆ˜ ์ถ”์ถœ + +`http://localhost:3000`์ด `getRestaurants`์™€ `addRestaurant` ๋‘ ๊ณณ์— ๋ฐ˜๋ณต๋๋‹ค. ์ƒ์ˆ˜๋กœ ์ถ”์ถœํ•ด ํ•œ ๊ณณ์—์„œ ๊ด€๋ฆฌํ•œ๋‹ค. -์ฒ˜์Œ ๊ตฌํ˜„์—์„œ๋Š” ํผ ์ž…๋ ฅ๊ฐ’(category, name, description)์„ App์—์„œ ๊ด€๋ฆฌํ–ˆ๋‹ค. `restaurants` state๊ฐ€ App์— ์žˆ์–ด์„œ ํผ ๋ฐ์ดํ„ฐ๋„ App๊นŒ์ง€ ์˜ฌ๋ ค์•ผ ํ•œ๋‹ค๊ณ  ์ƒ๊ฐํ–ˆ๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค. ๊ฒฐ๊ณผ์ ์œผ๋กœ AddRestaurantModal์— props๊ฐ€ 8๊ฐœ๊ฐ€ ๋๋‹ค. +```js +const BASE_URL = "http://localhost:3000"; +``` -๊ทธ๋Ÿฌ๋‚˜ App์ด ์‹ค์ œ๋กœ ํ•„์š”ํ•œ ๊ฒƒ์€ **์ถ”๊ฐ€ ์™„๋ฃŒ ์‹œ์ ์˜ ์ตœ์ข… ๋ฐ์ดํ„ฐ**๋ฟ์ด๋‹ค. ํผ์„ ์ฑ„์šฐ๋Š” ์ค‘๊ฐ„ ๊ณผ์ •์˜ ์ž…๋ ฅ๊ฐ’์€ App์ด ์•Œ ํ•„์š”๊ฐ€ ์—†๋‹ค. ํผ state๋ฅผ AddRestaurantModal ์•ˆ์œผ๋กœ ๋‚ด๋ฆฌ๊ณ , ์™„๋ฃŒ ์‹œ์—๋งŒ ๋ถ€๋ชจ๋กœ ์ „๋‹ฌํ•˜๋„๋ก ๋ณ€๊ฒฝํ–ˆ๋‹ค. +### 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; // ํ˜ธ์ถœํ•œ ์ชฝ์ด ์—๋Ÿฌ๋ฅผ ์•Œ ์ˆ˜ ์žˆ๋„๋ก ๋‹ค์‹œ ๋˜์ง + } +} +``` -```jsx -// Before: App์ด ์ค‘๊ฐ„ ์ž…๋ ฅ๊ฐ’๊นŒ์ง€ ๊ด€๋ฆฌ - +catch์—์„œ `throw error`๋ฅผ ๋‹ค์‹œ ๋˜์ง€๋Š” ์ด์œ : ๋กœ๊ทธ๋งŒ ๋‚จ๊ธฐ๊ณ  ์‚ผ์ผœ๋ฒ„๋ฆฌ๋ฉด ํ˜ธ์ถœํ•œ ์ชฝ(`App.jsx`)์ด ์—๋Ÿฌ ๋ฐœ์ƒ ์—ฌ๋ถ€๋ฅผ ์•Œ ์ˆ˜ ์—†๋‹ค. -// After: ์™„์„ฑ๋œ ๋ฐ์ดํ„ฐ๋งŒ ๋ถ€๋ชจ๋กœ ์ „๋‹ฌ - +### 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("์Œ์‹์  ์ถ”๊ฐ€์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค. ๋‹ค์‹œ ์‹œ๋„ํ•ด์ฃผ์„ธ์š”."); + } +} ``` -props๊ฐ€ 8๊ฐœ โ†’ 2๊ฐœ๋กœ ์ค„์—ˆ๊ณ , ํผ ๋‚ด๋ถ€ ๊ด€์‹ฌ์‚ฌ๊ฐ€ AddRestaurantModal ์•ˆ์— ์บก์Аํ™”๋๋‹ค. +### ๋งค์ง ์ŠคํŠธ๋ง ์ƒ์ˆ˜ํ™” โ€” `ALL_CATEGORY` -### Modal ๋ž˜ํผ div ์ œ๊ฑฐ +`"์ „์ฒด"` ๋ฌธ์ž์—ด์ด `App.jsx`, `CategoryFilter.jsx`, `filterRestaurants.js` ์„ธ ๊ณณ์— ํฉ์–ด์ ธ ์žˆ์—ˆ๋‹ค. ํ•˜๋‚˜๋ผ๋„ ์ˆ˜์ •ํ•˜๋ฉด ๋‚˜๋จธ์ง€๋„ ํ•จ๊ป˜ ๋ฐ”๊ฟ”์•ผ ํ•˜๋Š” ์•”๋ฌต์  ๊ฒฐํ•ฉ์ด๋‹ค. ์ƒ์ˆ˜๋กœ ์ถ”์ถœํ•ด ํ•œ ๊ณณ์—์„œ ๊ด€๋ฆฌํ•œ๋‹ค. -`display: none` / `display: block` ํŒจํ„ด์€ ํ…œํ”Œ๋ฆฟ(`templates/style.css`)์—์„œ ๊ทธ๋Œ€๋กœ ๊ฐ€์ ธ์˜จ ๊ฒƒ์ด๋‹ค. ํ…œํ”Œ๋ฆฟ์€ ์ˆœ์ˆ˜ HTML/CSS ๊ธฐ๋ฐ˜์œผ๋กœ, ๋ชจ๋‹ฌ์ด DOM์— ํ•ญ์ƒ ์กด์žฌํ•˜๋ฉด์„œ `.modal--open` ํด๋ž˜์Šค๋ฅผ ๋ถ™์ด๊ณ  ๋–ผ๋Š” ๋ฐฉ์‹์œผ๋กœ ๋ณด์ด๊ณ  ์ˆจ๊ฒผ๋‹ค. +```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"; +``` -React๋กœ ์ „ํ™˜ํ•˜๋ฉด์„œ ์กฐ๊ฑด๋ถ€ ๋ Œ๋”๋ง(`{isOpen && }`)์„ ์‚ฌ์šฉํ•˜๊ฒŒ ๋˜๋ฉด์„œ CSS ํ† ๊ธ€์ด ๋ถˆํ•„์š”ํ•ด์กŒ๋‹ค. ํ…œํ”Œ๋ฆฟ์˜ ๊ตฌ์กฐ๋ฅผ ๊ทธ๋Œ€๋กœ ์“ฐ๋ฉด์„œ ์ƒ๊ธด ๋ถˆํ•„์š”ํ•œ ํŒจํ„ด์„ ๋’ค๋Šฆ๊ฒŒ ์ œ๊ฑฐํ–ˆ๋‹ค. +### isLoading state ์ถ”๊ฐ€ -๋ž˜ํผ `
`๋„ ํ•จ๊ป˜ ์ œ๊ฑฐํ–ˆ๋‹ค. backdrop๊ณผ container๊ฐ€ ๋‘˜ ๋‹ค `position: fixed`๋ผ ๋ถ€๋ชจ ์š”์†Œ์˜ ๋ ˆ์ด์•„์›ƒ์— ์˜ํ–ฅ์„ ๋ฐ›์ง€ ์•Š์•„ ๋ž˜ํผ๊ฐ€ ์—†์–ด๋„ ๋™์ž‘์ด ๋™์ผํ•˜๋‹ค. ๋ถˆํ•„์š”ํ•œ DOM ๋…ธ๋“œ๋ฅผ ์ค„์ด๊ธฐ ์œ„ํ•ด Fragment๋กœ ๊ต์ฒดํ–ˆ๋‹ค. +๋ฐ์ดํ„ฐ๋ฅผ ๋ถˆ๋Ÿฌ์˜ค๋Š” ๋™์•ˆ ํ™”๋ฉด์ด ๋นˆ ์ƒํƒœ๋กœ ๋ณด์ด๋Š” ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด `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 + +``` diff --git a/src/App.jsx b/src/App.jsx index 3c4bb06..485546e 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -4,22 +4,23 @@ import CategoryFilter from "./components/CategoryFilter/CategoryFilter.jsx"; import RestaurantList from "./components/RestaurantList/RestaurantList.jsx"; import { 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 { useRestaurants } from "./hooks/useRestaurants.js"; +import { ALL_CATEGORY } from "./constants/categories.js"; function App() { - const [filterCategory, setFilterCategory] = useState("์ „์ฒด"); + const [selectedCategory, setSelectedCategory] = useState(ALL_CATEGORY); const [clickedRestaurant, setClickedRestaurant] = useState(null); const [isAddRestaurantModalOpen, setIsAddRestaurantModalOpen] = useState(false); - const [restaurants, setRestaurants] = useState(RESTAURANTS); + const { restaurants, addRestaurant, isLoading, error } = useRestaurants(); const isRestaurantDetailModalOpen = !!clickedRestaurant; - const filteredRestaurants = filterRestaurants(restaurants, filterCategory); + const filteredRestaurants = filterRestaurants(restaurants, selectedCategory); - function handleFilterCategoryChange(e) { - setFilterCategory(e.target.value); + function handleCategoryChange(e) { + setSelectedCategory(e.target.value); } function handleRestaurantClick(restaurant) { @@ -38,26 +39,24 @@ function App() { setIsAddRestaurantModalOpen(false); } - function handleRestaurantSubmit({ category, name, description }) { - setRestaurants([ - ...restaurants, - { - id: Date.now(), - category, - name, - description, - }, - ]); - setIsAddRestaurantModalOpen(false); + async function handleRestaurantSubmit(restaurant) { + try { + await addRestaurant(restaurant); + setIsAddRestaurantModalOpen(false); + } catch { + alert("์Œ์‹์  ์ถ”๊ฐ€์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค. ๋‹ค์‹œ ์‹œ๋„ํ•ด์ฃผ์„ธ์š”."); + } } return ( <>
+ {isLoading &&

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

} + {error &&

{error}

} - - - - - - + {CATEGORIES.map((value) => ( + + ))}
diff --git a/src/components/CategoryFilter/CategoryFilter.jsx b/src/components/CategoryFilter/CategoryFilter.jsx index 184edb6..fdf1d99 100644 --- a/src/components/CategoryFilter/CategoryFilter.jsx +++ b/src/components/CategoryFilter/CategoryFilter.jsx @@ -1,3 +1,4 @@ +import { ALL_CATEGORY, CATEGORIES } from "../../constants/categories.js"; import styles from "./CategoryFilter.module.css"; export default function CategoryFilter({ category, onCategoryChange }) { @@ -11,13 +12,12 @@ export default function CategoryFilter({ category, onCategoryChange }) { value={category} onChange={onCategoryChange} > - - - - - - - + + {CATEGORIES.map((value) => ( + + ))} ); diff --git a/src/components/Header/Header.jsx b/src/components/Header/Header.jsx index 512a3c9..1451cbd 100644 --- a/src/components/Header/Header.jsx +++ b/src/components/Header/Header.jsx @@ -11,7 +11,7 @@ export default function Header({ onAddButtonClick }) { aria-label="์Œ์‹์  ์ถ”๊ฐ€" onClick={onAddButtonClick} > - ์Œ์‹์  ์ถ”๊ฐ€ + ); 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 ( <>
diff --git a/src/components/RestaurantDetailModal/RestaurantDetailModal.jsx b/src/components/RestaurantDetailModal/RestaurantDetailModal.jsx index 93256b1..9318891 100644 --- a/src/components/RestaurantDetailModal/RestaurantDetailModal.jsx +++ b/src/components/RestaurantDetailModal/RestaurantDetailModal.jsx @@ -1,5 +1,5 @@ import styles from "./RestaurantDetailModal.module.css"; -import Modal from "../Modal/Modal"; +import Modal from "../Modal/Modal.jsx"; export default function RestaurantDetailModal({ restaurant, onClose }) { return ( 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 { diff --git a/src/constants/categories.js b/src/constants/categories.js new file mode 100644 index 0000000..49b8f39 --- /dev/null +++ b/src/constants/categories.js @@ -0,0 +1,2 @@ +export const ALL_CATEGORY = "์ „์ฒด"; +export const CATEGORIES = ["ํ•œ์‹", "์ค‘์‹", "์ผ์‹", "์–‘์‹", "์•„์‹œ์•ˆ", "๊ธฐํƒ€"]; 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: "๊ธฐํƒ€", - }, -]; diff --git a/src/hooks/useRestaurants.js b/src/hooks/useRestaurants.js new file mode 100644 index 0000000..e9130eb --- /dev/null +++ b/src/hooks/useRestaurants.js @@ -0,0 +1,31 @@ +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/utils/filterRestaurants.js b/src/utils/filterRestaurants.js index 4069992..c45a877 100644 --- a/src/utils/filterRestaurants.js +++ b/src/utils/filterRestaurants.js @@ -1,6 +1,6 @@ +import { ALL_CATEGORY } from "../constants/categories.js"; + export function filterRestaurants(restaurants, category) { - if (category === "์ „์ฒด") return restaurants; - else { - return restaurants.filter((restaurant) => restaurant.category === category); - } + if (category === ALL_CATEGORY) return restaurants; + return restaurants.filter((restaurant) => restaurant.category === category); }