From f10aeaf5e88c40506aa9da02cb65f220bf94c3d2 Mon Sep 17 00:00:00 2001 From: Yeryeong Kang Date: Mon, 8 Jun 2026 16:15:05 +0900 Subject: [PATCH 01/15] =?UTF-8?q?docs:=20Step=204=20README=20=EC=B4=88?= =?UTF-8?q?=EC=95=88=20=EC=9E=91=EC=84=B1=20-=20=EA=B0=9C=EC=9D=B8=20?= =?UTF-8?q?=EB=AA=A9=ED=91=9C=20=EB=B0=8F=20=ED=96=89=EB=8F=99=20=EA=B0=80?= =?UTF-8?q?=EC=9D=B4=EB=93=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- 03-modal/README.md | 29 --------- 04-form/README.md | 42 +++++++++++++ README.md | 147 ++++----------------------------------------- 3 files changed, 53 insertions(+), 165 deletions(-) delete mode 100644 03-modal/README.md create mode 100644 04-form/README.md diff --git a/03-modal/README.md b/03-modal/README.md deleted file mode 100644 index 6a97651..0000000 --- a/03-modal/README.md +++ /dev/null @@ -1,29 +0,0 @@ -# 03. 모달 UI 구현하기: side-effect(feat. event handler) - -## 🎯 요구 사항 - -- `RestaurantList` 의 아이템을 클릭하면, 클릭한 아이템의 정보를 보여주는 모달이 뜨도록 변경해 주세요. '확인' 버튼을 클릭하거나 모달 뒤의 backdrop을 클릭하면 모달이 닫혀야 합니다. - - (작은 단계로 구현해보기 1) 아이템을 클릭하면 정해진 텍스트를 그대로 보여주는 모달을 열고 닫습니다. - - (작은 단계로 구현해보기 2) 클릭한 아이템의 정보를 모달에 내려줄 수 있도록 개선합니다. - -### 구현 결과 예시 - -```javascript -// App.jsx -{isModalOpen && } -``` - -## ✅ 키워드 - -- event handler (feat. side effect) -- conditional rendering -- lifting state up - -## 🧙‍♀️ 진행 가이드 - -- 진행 시간: 1시간 내에 완료하는 것을 목표로 합니다. - -## 🔗 참고 문서 - -- [Thinking in React](https://react.dev/learn/thinking-in-react)의 Step5 -- [Responding to Events](https://react.dev/learn/responding-to-events) diff --git a/04-form/README.md b/04-form/README.md new file mode 100644 index 0000000..6518ccb --- /dev/null +++ b/04-form/README.md @@ -0,0 +1,42 @@ +# 04. 폼 UI 구현하기: controlled vs uncontrolled + +## 🎯 요구 사항 + +- `Header`의 레스토랑 추가 버튼을 클릭하면 레스토랑 추가 폼이 모달로 뜨도록 구현해 주세요 + - 이전 단계에서 만들어두었던 `AddRestaurantModal`을 그대로 사용합니다. +- 카테고리를 선택하고, ``, ` - + 메뉴 등 추가 정보를 입력해 주세요. From cf0b3697717589d77943dcaf641710d268b5223c Mon Sep 17 00:00:00 2001 From: Yeryeong Kang Date: Mon, 8 Jun 2026 17:49:31 +0900 Subject: [PATCH 05/15] =?UTF-8?q?docs:=20Step=204=20README=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=20=EA=B5=AC=ED=98=84=20=EB=AA=A9=EB=A1=9D,=20?= =?UTF-8?q?=ED=95=99=EC=8A=B5=20=EB=82=B4=EC=9A=A9,=20=EA=B3=A0=EB=AF=BC?= =?UTF-8?q?=ED=96=88=EB=8D=98=20=EB=AC=B8=EC=A0=9C=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 77 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 77 insertions(+) diff --git a/README.md b/README.md index e160265..966b6b5 100644 --- a/README.md +++ b/README.md @@ -24,10 +24,87 @@ DOM에서 직접 읽는 방식(uncontrolled)의 차이를 이해하고, ## 📝 기능 구현 목록 +**1. 헤더 추가 버튼 클릭 시 음식점 추가 모달 열기** + +- `isAddModalOpen` boolean state를 App.jsx에 추가한다. (초기값: false) +- `handleAddModalOpen` 핸들러를 정의하여 `Header`에 전달한다. +- `isAddModalOpen`이 true일 때만 `AddRestaurantModal`을 렌더링한다. + +**2. 음식점 추가 폼 제출 시 목록에 추가** + +- `restaurants` 상수를 state로 변환한다. +- `AddRestaurantModal`에서 category, name, description을 controlled input으로 관리한다. +- 폼 제출 시 `e.preventDefault()`로 새로고침을 막고, `onSubmit`으로 입력값을 App에 전달한다. +- App의 `handleFormSubmit`에서 목록에 추가하고 모달을 닫는다. + +```jsx +const handleFormSubmit = (newRestaurant) => { + setNewRestaurants([...newRestaurants, newRestaurant]); + setIsAddModalOpen(false); +}; +``` + +**3. 모달 닫기** + +- backdrop 클릭 또는 추가 완료 시 `onClose`를 통해 `setIsAddModalOpen(false)` 호출한다. + +--- + ## 📚 학습 내용 +### 1. Controlled vs Uncontrolled Input + +폼 입력값을 다루는 두 가지 방식이다. + +**Controlled** — 입력값을 React state로 관리한다. `value`로 state를 바인딩하고, `onChange`로 변화를 감지해 state를 업데이트한다. React가 값의 단일 출처(source of truth)가 된다. + +```jsx +const [name, setName] = useState(""); + + setName(e.target.value)} +/> +``` + +**Uncontrolled** — state 없이, 제출 시점에 DOM에서 값을 직접 읽는다. `e.target.elements` 또는 `FormData`를 활용한다. + +```jsx +const handleSubmit = (e) => { + e.preventDefault(); + const name = e.target.elements.name.value; +}; +``` + +| | Controlled | Uncontrolled | +|---|---|---| +| 값 관리 | React state | DOM | +| 값 접근 시점 | 언제든지 | 제출 시 | +| 실시간 유효성 검사 | 가능 | 어려움 | +| 코드량 | 많음 | 적음 | + +이번 미션에서는 Controlled 방식을 사용했다. + +### 2. 배열 state 업데이트 + +배열 state에 항목을 추가할 때 `push`는 직접 변경이라 리렌더링이 트리거되지 않는다. 스프레드 연산자로 새 배열을 만들어야 한다. + +```jsx +setNewRestaurants([...newRestaurants, newRestaurant]); +``` + +--- + ## 🤔 고민했던 문제와 해결 과정에서 배운 점 +### 1. e.preventDefault() 위치 + +폼 submit 이벤트의 기본 동작(페이지 새로고침)을 막으려면 `e.preventDefault()`를 이벤트가 발생하는 곳에서 호출해야 한다. App의 `handleFormSubmit`이 받는 건 이벤트가 아니라 음식점 객체이기 때문에, `AddRestaurantModal` 내부의 `handleSubmit`에서 처리해야 한다. + +### 2. ``에 달아야 한다. ` + + {/* 추가 버튼 */} +
+ +
+ + ); } diff --git a/src/components/Modal/Modal.jsx b/src/components/Modal/Modal.jsx new file mode 100644 index 0000000..c55fc15 --- /dev/null +++ b/src/components/Modal/Modal.jsx @@ -0,0 +1,13 @@ +import modalStyles from "./Modal.module.css"; + +export default function Modal({ title, onClose, children }) { + return ( +
+
+
+

{title}

+ {children} +
+
+ ); +} diff --git a/src/components/Modal/RestaurantDetailModal.jsx b/src/components/Modal/RestaurantDetailModal.jsx index 51ed8d5..fcc38a3 100644 --- a/src/components/Modal/RestaurantDetailModal.jsx +++ b/src/components/Modal/RestaurantDetailModal.jsx @@ -1,26 +1,21 @@ import styles from "./Modal.module.css"; +import Modal from "./Modal"; export default function RestaurantDetailModal({ clickedRestaurant, onClose }) { return ( -
-
-
-

- {clickedRestaurant.name} -

-
-

{clickedRestaurant.description}

-
- {/* 닫기 버튼 */} -
- -
+ +
+

{clickedRestaurant.description}

-
+ {/* 닫기 버튼 */} +
+ +
+ ); } From 159d61b0dacc0020e59a16e2848a0bee6ca42393 Mon Sep 17 00:00:00 2001 From: Yeryeong Kang Date: Mon, 8 Jun 2026 18:13:00 +0900 Subject: [PATCH 07/15] =?UTF-8?q?docs:=20Step=204=20README=20optional=20?= =?UTF-8?q?=EB=82=B4=EC=9A=A9=20=EC=B6=94=EA=B0=80=20=E2=80=94=20children?= =?UTF-8?q?=20prop,=20=EB=A6=AC=ED=8C=A9=ED=86=A0=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/README.md b/README.md index 966b6b5..075bc9c 100644 --- a/README.md +++ b/README.md @@ -48,6 +48,12 @@ const handleFormSubmit = (newRestaurant) => { - backdrop 클릭 또는 추가 완료 시 `onClose`를 통해 `setIsAddModalOpen(false)` 호출한다. +**[optional] 재사용 가능한 Modal 컴포넌트 구현** + +- `backdrop`, `container`, `title`을 공통으로 갖는 `Modal` 컴포넌트를 만든다. +- `title`, `onClose`를 props로, 각 모달의 고유 내용은 `children`으로 받는다. +- `AddRestaurantModal`, `RestaurantDetailModal`이 `Modal`을 내부에서 사용하도록 리팩토링한다. + --- ## 📚 학습 내용 @@ -93,6 +99,30 @@ const handleSubmit = (e) => { setNewRestaurants([...newRestaurants, newRestaurant]); ``` +### 3. children prop + +컴포넌트 태그 사이에 넣은 JSX가 `children`이라는 특수 prop으로 전달된다. + +```jsx + +
...
{/* 이게 children */} +
+ +function Modal({ title, onClose, children }) { + return ( +
+
+
+

{title}

+ {children} {/*
...
이 여기에 렌더링됨 */} +
+
+ ); +} +``` + +공통 구조(껍데기)는 `Modal`이 담당하고, 각 모달은 고유한 내용만 children으로 전달하면 된다. + --- ## 🤔 고민했던 문제와 해결 과정에서 배운 점 @@ -107,6 +137,10 @@ setNewRestaurants([...newRestaurants, newRestaurant]); ## 🛠 리팩토링 +**1. 재사용 가능한 Modal 컴포넌트 추출** + +`AddRestaurantModal`과 `RestaurantDetailModal`이 backdrop, container, title 구조를 중복으로 갖고 있었다. 공통 구조를 `Modal` 컴포넌트로 분리하고, 각 모달은 `children`으로 고유 내용만 넘기도록 리팩토링했다. + ## 과거 코드와 비교 ### 달라진 점 From b9ccde5b6ad8dab65c5d06e52076320110a885bf Mon Sep 17 00:00:00 2001 From: Yeryeong Kang Date: Mon, 8 Jun 2026 22:37:19 +0900 Subject: [PATCH 08/15] =?UTF-8?q?refactor:=20=EB=B0=B0=EC=97=B4=20state=20?= =?UTF-8?q?=EC=97=85=EB=8D=B0=EC=9D=B4=ED=8A=B8=EB=A5=BC=20=ED=95=A8?= =?UTF-8?q?=EC=88=98=ED=98=95=20=EC=97=85=EB=8D=B0=EC=9D=B4=ED=8A=B8?= =?UTF-8?q?=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/App.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/App.jsx b/src/App.jsx index 55e608c..8560479 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -14,7 +14,7 @@ function App() { const [newRestaurants, setNewRestaurants] = useState(restaurants); const handleFormSubmit = (newRestaurant) => { - setNewRestaurants([...newRestaurants, newRestaurant]); + setNewRestaurants((prev) => [...prev, newRestaurant]); setIsAddModalOpen(false); }; From a5a90a185674bef085d617e81e6be49035f7ae87 Mon Sep 17 00:00:00 2001 From: Yeryeong Kang Date: Mon, 8 Jun 2026 22:37:27 +0900 Subject: [PATCH 09/15] =?UTF-8?q?refactor:=20id=20=EC=83=9D=EC=84=B1?= =?UTF-8?q?=EC=9D=84=20Date.now()=EC=97=90=EC=84=9C=20crypto.randomUUID()?= =?UTF-8?q?=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/Modal/AddRestaurantModal.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/Modal/AddRestaurantModal.jsx b/src/components/Modal/AddRestaurantModal.jsx index 7d355af..d813278 100644 --- a/src/components/Modal/AddRestaurantModal.jsx +++ b/src/components/Modal/AddRestaurantModal.jsx @@ -11,7 +11,7 @@ export default function AddRestaurantModal({ onSubmit, onClose }) { const handleSubmit = (e) => { e.preventDefault(); - onSubmit({ id: Date.now(), category, name, description }); + onSubmit({ id: crypto.randomUUID(), category, name, description }); }; return ( From 7ad43954af6037b7cc039c618abf1bb9846b54b0 Mon Sep 17 00:00:00 2001 From: Yeryeong Kang Date: Mon, 8 Jun 2026 22:37:45 +0900 Subject: [PATCH 10/15] =?UTF-8?q?docs:=20Step=204=20README=20=EA=B3=BC?= =?UTF-8?q?=EA=B1=B0=20=EC=BD=94=EB=93=9C=EC=99=80=20=EB=B9=84=EA=B5=90=20?= =?UTF-8?q?=EB=B0=8F=20=EB=A6=AC=ED=8C=A9=ED=86=A0=EB=A7=81=20=EB=82=B4?= =?UTF-8?q?=EC=9A=A9=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 50 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/README.md b/README.md index 075bc9c..e209802 100644 --- a/README.md +++ b/README.md @@ -141,8 +141,58 @@ function Modal({ title, onClose, children }) { `AddRestaurantModal`과 `RestaurantDetailModal`이 backdrop, container, title 구조를 중복으로 갖고 있었다. 공통 구조를 `Modal` 컴포넌트로 분리하고, 각 모달은 `children`으로 고유 내용만 넘기도록 리팩토링했다. +**2. 배열 state 업데이트를 함수형 업데이트로 변경** + +`[...newRestaurants, newRestaurant]`에서 `(prev) => [...prev, newRestaurant]`로 변경했다. 이전 state에 의존하는 업데이트는 함수형 업데이트가 더 안전하다. + +**3. id 생성을 `crypto.randomUUID()`로 변경** + +`Date.now()` 대신 `crypto.randomUUID()`를 사용하도록 변경했다. 밀리초 단위 충돌 가능성을 제거하고 고유성을 보장한다. + ## 과거 코드와 비교 ### 달라진 점 +**1. 폼 입력값 읽기 방식 — Uncontrolled → Controlled** + +| 구분 | 과거 코드 | 현재 코드 | +|------|---------|---------| +| 방식 | Uncontrolled (`FormData`) | Controlled (`useState`) | +| 값 접근 | 제출 시 DOM에서 읽음 | state로 실시간 관리 | +| 코드량 | 적음 | 많음 | + +과거 코드는 state 없이 폼 제출 시 `FormData`로 DOM에서 한 번에 읽었다. 각 input에 `name` 속성이 있으면 키-값 쌍으로 꺼낼 수 있어 코드가 간결하다. + +```jsx +// 과거 — Uncontrolled +const fd = new FormData(e.currentTarget); +onAdd({ category: fd.get("category"), name: fd.get("name") }); + +// 현재 — Controlled +const [name, setName] = useState(""); + setName(e.target.value)} /> +``` + ### 과거 코드에서 배운 점 + +**1. 함수형 업데이트** + +리뷰어 코드에서 배열 state 업데이트 시 함수형 업데이트를 사용했다. + +```jsx +// 기존 코드 — newRestaurants를 직접 참조 +setNewRestaurants([...newRestaurants, newRestaurant]); + +// 함수형 업데이트 — React가 최신 state를 prev로 전달 +setNewRestaurants((prev) => [...prev, newRestaurant]); +``` + +React의 state 업데이트는 즉시 반영되지 않아 렌더링 사이클 사이에 `newRestaurants`가 오래된 값일 수 있다. `prev`는 React가 보장하는 최신 state이므로, 이전 state를 기반으로 새 state를 만들 때는 함수형 업데이트가 더 안전하다. + +`prev`는 관례적인 이름일 뿐이며, 각 setter에 묶인 해당 state의 최신값이 전달된다. state가 여러 개여도 setter가 다르므로 섞이지 않는다. + +**2. `crypto.randomUUID()`** + +리뷰어 피드백: *"아주 짧은 시간 안에 여러 항목이 추가될 경우 `Date.now()`는 중복된 id가 생성될 가능성이 있습니다. `crypto.randomUUID()`를 고려해보세요."* + +`Date.now()`는 밀리초 단위 숫자라 짧은 시간 안에 두 번 호출되면 같은 값이 나올 수 있다. `crypto.randomUUID()`는 브라우저 내장 함수로 `"550e8400-e29b-41d4-a716-446655440000"` 형태의 충돌 없는 고유 ID를 생성한다. From 5b05db4bdd9bf17945229215a617d7797f586653 Mon Sep 17 00:00:00 2001 From: Yeryeong Kang Date: Sat, 13 Jun 2026 21:39:14 +0900 Subject: [PATCH 11/15] =?UTF-8?q?refactor:=20=EC=83=81=EC=88=98=EB=AA=85?= =?UTF-8?q?=20=EB=8C=80=EB=AC=B8=EC=9E=90=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/App.jsx | 4 ++-- src/components/RestaurantList/RestaurantList.jsx | 4 ++-- src/constants/categoryImages.js | 2 +- src/constants/restaurants.js | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/App.jsx b/src/App.jsx index 8560479..ff29a45 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -1,6 +1,6 @@ import { useState } from "react"; import "./App.css"; -import { restaurants } from "./constants/restaurants"; +import { RESTAURANTS } from "./constants/restaurants"; import Header from "./components/Header/Header"; import CategoryFilter from "./components/CategoryFilter/CategoryFilter"; import RestaurantList from "./components/RestaurantList/RestaurantList"; @@ -11,7 +11,7 @@ function App() { const [category, setCategory] = useState("전체"); const [clickedRestaurant, setClickedRestaurant] = useState(null); const [isAddModalOpen, setIsAddModalOpen] = useState(false); - const [newRestaurants, setNewRestaurants] = useState(restaurants); + const [newRestaurants, setNewRestaurants] = useState(RESTAURANTS); const handleFormSubmit = (newRestaurant) => { setNewRestaurants((prev) => [...prev, newRestaurant]); diff --git a/src/components/RestaurantList/RestaurantList.jsx b/src/components/RestaurantList/RestaurantList.jsx index 30292c2..47334f5 100644 --- a/src/components/RestaurantList/RestaurantList.jsx +++ b/src/components/RestaurantList/RestaurantList.jsx @@ -1,5 +1,5 @@ import styles from "./RestaurantList.module.css"; -import { categoryImages } from "../../constants/categoryImages"; +import { CATEGORY_IMAGES } from "../../constants/categoryImages"; export default function RestaurantList({ restaurants, onRestaurantClick }) { return ( @@ -20,7 +20,7 @@ export default function RestaurantList({ restaurants, onRestaurantClick }) { >
{restaurant.category} diff --git a/src/constants/categoryImages.js b/src/constants/categoryImages.js index dd6d29e..daba5dd 100644 --- a/src/constants/categoryImages.js +++ b/src/constants/categoryImages.js @@ -5,7 +5,7 @@ import westernImg from "../assets/category-western.png"; import asianImg from "../assets/category-asian.png"; import etcImg from "../assets/category-etc.png"; -export const categoryImages = { +export const CATEGORY_IMAGES = { 한식: koreanImg, 중식: chineseImg, 일식: japaneseImg, diff --git a/src/constants/restaurants.js b/src/constants/restaurants.js index 30fe97e..7fc92cf 100644 --- a/src/constants/restaurants.js +++ b/src/constants/restaurants.js @@ -1,4 +1,4 @@ -export const restaurants = [ +export const RESTAURANTS = [ { id: "a01", name: "피양콩할마니", From bb28d0666566e9a1f2c7ea1d3aaed62427a5dccc Mon Sep 17 00:00:00 2001 From: Yeryeong Kang Date: Sat, 13 Jun 2026 22:27:09 +0900 Subject: [PATCH 12/15] =?UTF-8?q?docs:=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84=20=EB=AA=A9=EB=A1=9D=20=EB=8B=A8=EC=88=9C=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 25 +++++++------------------ 1 file changed, 7 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index e209802..7de88b8 100644 --- a/README.md +++ b/README.md @@ -26,33 +26,22 @@ DOM에서 직접 읽는 방식(uncontrolled)의 차이를 이해하고, **1. 헤더 추가 버튼 클릭 시 음식점 추가 모달 열기** -- `isAddModalOpen` boolean state를 App.jsx에 추가한다. (초기값: false) -- `handleAddModalOpen` 핸들러를 정의하여 `Header`에 전달한다. -- `isAddModalOpen`이 true일 때만 `AddRestaurantModal`을 렌더링한다. +- 음식점 추가 모달 열림 여부를 boolean state로 관리한다. +- 버튼 클릭 시 모달을 조건부 렌더링한다. **2. 음식점 추가 폼 제출 시 목록에 추가** -- `restaurants` 상수를 state로 변환한다. -- `AddRestaurantModal`에서 category, name, description을 controlled input으로 관리한다. -- 폼 제출 시 `e.preventDefault()`로 새로고침을 막고, `onSubmit`으로 입력값을 App에 전달한다. -- App의 `handleFormSubmit`에서 목록에 추가하고 모달을 닫는다. - -```jsx -const handleFormSubmit = (newRestaurant) => { - setNewRestaurants([...newRestaurants, newRestaurant]); - setIsAddModalOpen(false); -}; -``` +- 폼 입력값을 controlled input으로 관리한다. +- 폼 제출 시 입력값을 상위 컴포넌트로 전달해 목록에 추가하고 모달을 닫는다. **3. 모달 닫기** -- backdrop 클릭 또는 추가 완료 시 `onClose`를 통해 `setIsAddModalOpen(false)` 호출한다. +- backdrop 클릭 또는 폼 제출 완료 시 모달을 닫는다. **[optional] 재사용 가능한 Modal 컴포넌트 구현** -- `backdrop`, `container`, `title`을 공통으로 갖는 `Modal` 컴포넌트를 만든다. -- `title`, `onClose`를 props로, 각 모달의 고유 내용은 `children`으로 받는다. -- `AddRestaurantModal`, `RestaurantDetailModal`이 `Modal`을 내부에서 사용하도록 리팩토링한다. +- 두 모달의 공통 구조를 `Modal` 컴포넌트로 추출한다. +- 각 모달의 고유 내용은 `children`으로 전달한다. --- From 8e860fa36fe33762a48adf1b1f3f3809a133db06 Mon Sep 17 00:00:00 2001 From: Yeryeong Kang Date: Sun, 14 Jun 2026 16:03:18 +0900 Subject: [PATCH 13/15] =?UTF-8?q?refactor:=20button=20=EA=B5=90=EC=B2=B4,?= =?UTF-8?q?=20Modal=20=EB=9E=98=ED=8D=BC=20div=20=EC=A0=9C=EA=B1=B0=20?= =?UTF-8?q?=EB=B0=8F=20CSS=20=ED=86=A0=EA=B8=80=20=ED=8C=A8=ED=84=B4=20?= =?UTF-8?q?=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/App.jsx | 4 +- src/components/Modal/Modal.jsx | 4 +- .../RestaurantList/RestaurantList.jsx | 48 ++++++++----------- .../RestaurantList/RestaurantList.module.css | 16 +++++-- 4 files changed, 38 insertions(+), 34 deletions(-) diff --git a/src/App.jsx b/src/App.jsx index ff29a45..ef1e696 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -31,7 +31,7 @@ function App() { setClickedRestaurant(restaurant); }; - const handleModalClose = () => { + const handleDetailModalClose = () => { setClickedRestaurant(null); }; @@ -60,7 +60,7 @@ function App() { {clickedRestaurant && ( )} {isAddModalOpen && ( diff --git a/src/components/Modal/Modal.jsx b/src/components/Modal/Modal.jsx index c55fc15..456873b 100644 --- a/src/components/Modal/Modal.jsx +++ b/src/components/Modal/Modal.jsx @@ -2,12 +2,12 @@ import modalStyles from "./Modal.module.css"; export default function Modal({ title, onClose, children }) { return ( -
+ <>

{title}

{children}
-
+ ); } diff --git a/src/components/RestaurantList/RestaurantList.jsx b/src/components/RestaurantList/RestaurantList.jsx index 47334f5..45ae6c8 100644 --- a/src/components/RestaurantList/RestaurantList.jsx +++ b/src/components/RestaurantList/RestaurantList.jsx @@ -6,33 +6,27 @@ export default function RestaurantList({ restaurants, onRestaurantClick }) {
    {restaurants.map((restaurant) => ( -
  • onRestaurantClick(restaurant)} - onKeyDown={(e) => { - if (e.key === "Enter" || e.key === " ") { - 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 ffa6c7f..1d91c5d 100644 --- a/src/components/RestaurantList/RestaurantList.module.css +++ b/src/components/RestaurantList/RestaurantList.module.css @@ -7,14 +7,24 @@ } .restaurant { - display: flex; - align-items: flex-start; - padding: 16px 8px; border-bottom: 1px solid #e9eaed; } +.restaurant__button { + display: flex; + align-items: flex-start; + width: 100%; + + background: none; + border: none; + padding: 0; + cursor: pointer; + text-align: left; + font: inherit; +} + .restaurant__category { display: flex; justify-content: center; From 2d14a66949ec1698b113a4448e35f46a129b8a1e Mon Sep 17 00:00:00 2001 From: Yeryeong Kang Date: Sun, 14 Jun 2026 16:03:26 +0900 Subject: [PATCH 14/15] =?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=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index 7de88b8..faba713 100644 --- a/README.md +++ b/README.md @@ -138,6 +138,10 @@ function Modal({ title, onClose, children }) { `Date.now()` 대신 `crypto.randomUUID()`를 사용하도록 변경했다. 밀리초 단위 충돌 가능성을 제거하고 고유성을 보장한다. +**4. Modal 래퍼 `
` → Fragment, CSS 토글 패턴 제거** + +템플릿(순수 HTML/CSS)에서 `.modal--open` 클래스를 붙이고 떼는 방식으로 모달을 보이고 숨겼는데, React 조건부 렌더링으로 전환하면서 CSS 토글이 불필요해졌다. 래퍼 `
`도 Fragment로 교체했다. backdrop과 container가 모두 `position: fixed`라 부모 요소의 레이아웃에 영향을 받지 않기 때문에 래퍼가 없어도 동작이 동일하다. + ## 과거 코드와 비교 ### 달라진 점 From 84774adb56bba9541b4b576f445aba456a0e9eba Mon Sep 17 00:00:00 2001 From: Yeryeong Kang Date: Sun, 14 Jun 2026 17:10:42 +0900 Subject: [PATCH 15/15] =?UTF-8?q?refactor:=20=EC=9D=B4=EB=B2=A4=ED=8A=B8?= =?UTF-8?q?=20=ED=95=B8=EB=93=A4=EB=9F=AC=20=EB=84=A4=EC=9D=B4=EB=B0=8D=20?= =?UTF-8?q?on-/handle-=20=EB=8C=80=EC=B9=AD=20=ED=86=B5=EC=9D=BC=20?= =?UTF-8?q?=EB=B0=8F=20id=20=EC=83=9D=EC=84=B1=20=EC=9C=84=EC=B9=98=20App?= =?UTF-8?q?=EC=9C=BC=EB=A1=9C=20=EC=9D=B4=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 24 +++++++++++++++++-- src/App.jsx | 13 ++++++---- .../CategoryFilter/CategoryFilter.jsx | 4 ++-- src/components/Header/Header.jsx | 4 ++-- src/components/Modal/AddRestaurantModal.jsx | 8 +++---- 5 files changed, 38 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index faba713..71b298f 100644 --- a/README.md +++ b/README.md @@ -118,12 +118,20 @@ function Modal({ title, onClose, children }) { ### 1. e.preventDefault() 위치 -폼 submit 이벤트의 기본 동작(페이지 새로고침)을 막으려면 `e.preventDefault()`를 이벤트가 발생하는 곳에서 호출해야 한다. App의 `handleFormSubmit`이 받는 건 이벤트가 아니라 음식점 객체이기 때문에, `AddRestaurantModal` 내부의 `handleSubmit`에서 처리해야 한다. +폼 submit 이벤트의 기본 동작(페이지 새로고침)을 막으려면 `e.preventDefault()`를 이벤트가 발생하는 곳에서 호출해야 한다. App의 `handleFormSubmit`이 받는 건 이벤트가 아니라 음식점 객체이기 때문에, `AddRestaurantModal` 내부의 `handleFormSubmit`에서 처리해야 한다. ### 2. ``에 달아야 한다. ` diff --git a/src/components/Header/Header.jsx b/src/components/Header/Header.jsx index 9702af9..6a05ff9 100644 --- a/src/components/Header/Header.jsx +++ b/src/components/Header/Header.jsx @@ -1,7 +1,7 @@ import styles from "./Header.module.css"; import addButtonImg from "../../assets/add-button.png"; -export default function Header({ onClick }) { +export default function Header({ onAddModalOpen }) { return (

점심 뭐 먹지

@@ -9,7 +9,7 @@ export default function Header({ onClick }) { type="button" className={styles.gnb__button} aria-label="음식점 추가" - onClick={onClick} + onClick={onAddModalOpen} > 음식점 추가 diff --git a/src/components/Modal/AddRestaurantModal.jsx b/src/components/Modal/AddRestaurantModal.jsx index d813278..1478295 100644 --- a/src/components/Modal/AddRestaurantModal.jsx +++ b/src/components/Modal/AddRestaurantModal.jsx @@ -4,19 +4,19 @@ import { CATEGORIES } from "../../constants/categories"; import { useState } from "react"; import Modal from "./Modal"; -export default function AddRestaurantModal({ onSubmit, onClose }) { +export default function AddRestaurantModal({ onFormSubmit, onClose }) { const [category, setCategory] = useState(""); const [name, setName] = useState(""); const [description, setDescription] = useState(""); - const handleSubmit = (e) => { + const handleFormSubmit = (e) => { e.preventDefault(); - onSubmit({ id: crypto.randomUUID(), category, name, description }); + onFormSubmit({ category, name, description }); }; return ( -
+ {/* 카테고리 */}