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/03-modal/README.md b/03-modal/README.md
new file mode 100644
index 0000000..bc74460
--- /dev/null
+++ b/03-modal/README.md
@@ -0,0 +1,24 @@
+# 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)
\ No newline at end of file
diff --git a/README.md b/README.md
index 415ca95..1d0f3a4 100644
--- a/README.md
+++ b/README.md
@@ -1,93 +1,130 @@
-# Props와 State
+# 조건부 렌더링 활용
## 🎯 개인 목표 및 목표 달성을 위한 행동 가이드
이번 미션을 통해 다음과 같은 학습 경험들을 쌓는 것을 목표로 한다.
-1. Props를 통해 부모 컴포넌트에서 자식 컴포넌트로 데이터를 전달하는 방식을 이해한다.
-2. State를 사용해 컴포넌트 내부의 동적 상태를 관리한다.
-3. Props와 State를 조합하여 부모-자식 간 양방향 데이터 흐름을 구현한다.
+1. 이벤트 핸들러를 통해 사용자 인터랙션에 반응하는 방법을 이해한다.
+2. 조건부 렌더링(`&&`)을 활용해 상황에 따라 컴포넌트를 보여주고 숨긴다.
+3. 어떤 값을 state로 선언해야 하는지 기준을 세운다.
---
## 📝 기능 구현 목록
-- [x] RestaurantList에 restaurants 배열을 props로 전달
-- [x] 배열 데이터를 map()으로 동적 렌더링
-- [x] 각 리스트 항목에 key prop 추가
-- [x] CategoryFilter에서 선택된 카테고리 상태 관리
-- [x] App 컴포넌트에서 카테고리별 필터링 로직 구현
-- [x] 필터된 데이터를 RestaurantList에 props로 전달
-- [x] 카테고리별 동적 이미지 매핑
+- [x] 음식점 아이템 클릭 시 모달 열기
+- [x] 닫기 버튼 또는 backdrop 클릭 시 모달 닫기
+- [x] 클릭한 음식점 정보를 모달에 전달하여 표시
---
## 📚 학습 내용
-### Props (속성)
-- 부모 컴포넌트에서 자식 컴포넌트로 데이터를 전달하는 메커니즘이다.
-- 자식 컴포넌트 함수의 매개변수로 받는다: `function Component({ prop1, prop2 }) { }`
-- Props는 읽기 전용이므로 자식에서 직접 수정할 수 없다.
-
-### State (상태)
-- 컴포넌트 내부에서 변경 가능한 데이터를 관리한다.
-- `useState` 훅으로 선언한다: `const [state, setState] = useState(초기값)`
-- State가 변경되면 컴포넌트가 리렌더링된다.
-- State는 각 컴포넌트 인스턴스마다 독립적으로 존재한다.
-
-### Props vs State
-- Props: 부모 → 자식, 읽기 전용
-- State: 컴포넌트 내부, 변경 가능
-- State를 변경하려면 setter 함수(`setState`)를 사용한다.
-
-### 배열 렌더링과 Key
-- 배열을 렌더링할 때 `map()` 메서드를 사용한다.
-- 각 항목에 고유한 `key` prop을 부여해야 한다.
-- key는 React가 어떤 항목이 변경/추가/삭제되었는지 식별하는 데 사용된다.
-- 안정적인 고유값(예: id)을 key로 사용하고, index는 피한다.
-
-### 동적 데이터 매핑
-- 객체를 사용해 카테고리와 이미지 등을 매핑할 수 있다.
-- `const CATEGORY_IMAGES = { "한식": koreanImg, ... }`
-- 필요한 값을 동적으로 조회: `CATEGORY_IMAGES[restaurant.category]`
+### 이벤트 핸들러와 데이터 전달
----
+이벤트 핸들러에 함수를 직접 연결하면 React가 이벤트 객체(`e`)를 자동으로 넘겨준다. 클릭된 아이템의 데이터를 함께 전달하려면 화살표 함수로 한 번 감싸야 한다.
-## 🤔 고민했던 문제와 해결 과정에서 배운 점
+```jsx
+// 이벤트 객체(e)만 전달됨
+onClick={onRestaurantClick}
+
+// 클릭된 restaurant 데이터를 직접 전달
+onClick={() => onRestaurantClick(restaurant)}
+```
+
+`() => onRestaurantClick(restaurant)`는 `map` 순회 중인 `restaurant`를 부모까지 전달하는 역할을 한다.
+
+### 조건부 렌더링
+
+`{condition && }` 패턴으로 조건이 참일 때만 컴포넌트를 렌더링한다.
+
+```jsx
+{clickedRestaurant && (
+
+)}
+```
+
+### State로 선언할 것과 아닌 것
+
+**State가 필요한 경우**: 시간이 지나면서 변하고, 렌더링에 영향을 주며, 다른 state나 props로부터 계산할 수 없는 값
+
+**State가 불필요한 경우**: 기존 state나 props로부터 계산 가능한 파생값(derived value)
+
+```jsx
+// category state에서 계산 가능 → state 불필요
+const filteredRestaurants = filterRestaurants(RESTAURANTS, category);
+
+// clickedRestaurant state에서 계산 가능 → state 불필요
+const isRestaurantDetailModalOpen = !!clickedRestaurant;
+```
+
+파생 변수는 state와 달리 setter가 없어 동기화 버그가 생기지 않고, 렌더링마다 자동으로 최신값을 계산한다.
+
+### `&&` 조건부 렌더링 주의사항
-### Controlled Component의 필요성
-처음엔 CategoryFilter가 자체 state를 관리했고 RestaurantList는 고정된 데이터를 표시했다. 부모인 App에서 카테고리 선택값을 알 수 없었다.
+`&&` 앞에 숫자나 빈 문자열 같은 falsy 값이 오면, `false`로 평가되지 않고 값 자체가 화면에 출력된다.
-**해결:** State를 부모 App으로 올렸다. CategoryFilter는 props로 `category`와 `onChangeCategory`를 받아 controlled component가 되었고, 필터링 로직은 App에서 처리하게 됐다. 이를 통해 부모-자식 간 데이터 흐름이 단방향으로 명확해졌다.
+```jsx
+// ⚠️ count가 0이면 "0"이 화면에 렌더링됨
+{count && }
-### 동적 이미지 매핑의 필요성
-처음엔 모든 음식점이 한식 이미지(`koreanImg`)만 표시됐다. 음식점 객체에 `category` 필드가 있는데도 활용하지 않고 있었다.
+// ✅ 명시적으로 불리언으로 변환
+{!!count && }
+{count > 0 && }
+```
-**해결:** `CATEGORY_IMAGES` 객체를 만들어 카테고리별 이미지를 매핑했다. `src={CATEGORY_IMAGES[restaurant.category]}`로 각 음식점에 맞는 이미지가 동적으로 표시되도록 수정했다.
+이 미션에서 `clickedRestaurant`는 객체 또는 `null`만 들어오므로 문제없지만, 숫자나 문자열을 조건으로 쓸 때는 주의해야 한다. `!!`를 사용하는 이유 중 하나이기도 하다.
+
+### Lifting State Up
+
+클릭된 음식점 정보를 `RestaurantList`와 `RestaurantDetailModal` 두 컴포넌트가 공유해야 하므로, 공통 부모인 `App`에서 state를 관리한다.
+
+### `!!` 이중 부정 연산자
+
+자바스크립트에서 값의 truthy/falsy 평가 결과를 명시적으로 불리언 값으로 변환하기 위해 사용된다. 주로 조건식의 결과를 일관된 불리언 타입으로 정규화할 때 활용된다.
+
+```jsx
+!!null // → false (모달 닫힌 상태)
+!!{ id: 1 } // → true (모달 열린 상태)
+```
---
-## 🛠 리팩토링
+## 🤔 고민했던 문제와 해결 과정에서 배운 점
-1. Props와 State 책임 분리
+### 무엇을 state로 선언할지
- - 이유: 초기 구현에서 각 컴포넌트가 자신의 state를 독립적으로 관리하고 있어, 부모 컴포넌트가 상태 변화를 알 수 없었다. 이로 인해 필터링이 제대로 작동하지 않았다.
+가장 고민이 된 부분은 `restaurant`를 state로 만들어야 하는가였다. `filteredRestaurants`로 계산할 수 있을 것 같아 state가 불필요하다고 생각했는데, 실제로는 두 값의 역할이 다르다.
- - 개선: State를 부모 App 컴포넌트로 올렸다(state lifting). CategoryFilter는 선택된 카테고리를 props로 받아 표시만 하고, 변경 시 콜백 함수를 통해 부모에 알린다. 이제 App이 중앙에서 상태를 관리하고, RestaurantList에 필터된 데이터를 props로 전달한다.
+- `filteredRestaurants` — 화면에 보여줄 **목록** → `category`에서 파생, state 불필요
+- `clickedRestaurant` — 모달에 보여줄 **선택된 하나** → 클릭 전까지 알 수 없으므로 state 필요
-2. 동적 이미지 매핑으로 하드코딩 제거
+**기준**: 다른 값으로부터 계산할 수 없다면 state, 계산할 수 있다면 파생 변수
- - 이유: 모든 음식점에 한식 이미지만 매핑되어 있었다. 각 음식점의 `category` 필드를 활용하지 않고 있었고, 새로운 카테고리 추가 시 컴포넌트 코드를 수정해야 했다.
+### isModalOpen과 clickedRestaurant를 따로 두면 생기는 문제
- - 개선: `CATEGORY_IMAGES` 객체를 만들어 카테고리와 이미지를 매핑했다. 이제 음식점 데이터의 `category`에 따라 자동으로 올바른 이미지가 표시된다. 새로운 카테고리를 추가할 때도 객체에만 항목을 추가하면 된다.
+처음엔 `isModalOpen` boolean과 `clickedRestaurant` 두 state를 동시에 관리하려 했다. 이 경우 둘을 항상 함께 업데이트해야 하는데, 하나라도 빠지면 모달은 열려 있지만 `clickedRestaurant`가 `null`인 상황이 발생해 런타임 에러가 난다.
-3. 상수와 유틸 함수를 별도 파일로 분리
+```jsx
+function handleRestaurantClick(restaurant) {
+ setClickedRestaurant(restaurant);
+ setIsModalOpen(true); // 둘 중 하나라도 빠지면 버그
+}
+```
+
+**해결:** `clickedRestaurant` 하나로 통합. `null`이면 닫힌 상태, 값이 있으면 열린 상태다. `isRestaurantDetailModalOpen`은 이를 기반으로 계산한 파생 변수로 가독성을 확보했다.
+
+```jsx
+const [clickedRestaurant, setClickedRestaurant] = useState(null);
+const isRestaurantDetailModalOpen = !!clickedRestaurant; // 파생 변수
+```
+
+---
+
+## 🛠 리팩토링
- - 이유: App.jsx에 RESTAURANTS 데이터와 filter 함수가 모두 포함되어 있어서 컴포넌트 로직과 비즈니스 로직이 섞여 있었다. 파일이 길어지고 가독성이 떨어졌다.
+State 설계를 단계적으로 개선하며 사고 과정을 커밋으로 기록했다.
- - 개선:
- - `src/constants/restaurants.js` — RESTAURANTS 배열 분리
- - `src/constants/categoryImages.js` — CATEGORY_IMAGES 매핑 객체 분리
- - `src/utils/filterRestaurants.js` — filterRestaurants 함수 분리 (함수명, 함수 형태 수정)
-
- App.jsx는 이제 필요한 상수와 함수를 import해서 사용하므로 역할이 명확해졌다. 또한 각 모듈이 독립적이므로 재사용성이 높아졌고, 테스트하거나 수정할 때 해당 파일만 건드리면 된다.
\ No newline at end of file
+1. **Stage 1** — `isModalOpen` boolean state로 모달 열고 닫기만 구현 (restaurant 데이터 없음)
+2. **Stage 2** — `isModalOpen` + `clickedRestaurant` 두 state로 실제 데이터 전달 (동기화 문제 내포)
+3. **Stage 3** — `clickedRestaurant` 단일 state로 통합, `isRestaurantDetailModalOpen`을 파생 변수로 개선
diff --git a/src/App.jsx b/src/App.jsx
index d4b6f6d..86cbe2d 100644
--- a/src/App.jsx
+++ b/src/App.jsx
@@ -5,28 +5,44 @@ 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 RestaurantDetailModal from "./components/RestaurantDetailModal/RestaurantDetailModal.jsx";
function App() {
const [category, setCategory] = useState("전체");
+ const [clickedRestaurant, setClickedRestaurant] = useState(null);
+ const isRestaurantDetailModalOpen = !!clickedRestaurant;
const filteredRestaurants = filterRestaurants(RESTAURANTS, category);
function handleChange(e) {
setCategory(e.target.value);
}
+ function handleRestaurantClick(restaurant) {
+ setClickedRestaurant(restaurant);
+ }
+
+ function handleModalClose() {
+ setClickedRestaurant(null);
+ }
+
return (
<>
-
+
- {/* */}
+
>
);
}
diff --git a/src/components/RestaurantDetailModal/RestaurantDetailModal.jsx b/src/components/RestaurantDetailModal/RestaurantDetailModal.jsx
index fc9aca5..00e7937 100644
--- a/src/components/RestaurantDetailModal/RestaurantDetailModal.jsx
+++ b/src/components/RestaurantDetailModal/RestaurantDetailModal.jsx
@@ -1,17 +1,20 @@
import styles from "./RestaurantDetailModal.module.css";
-export default function RestaurantDetailModal() {
+export default function RestaurantDetailModal({ restaurant, onClose }) {
return (
-
+
-
음식점 이름
+
+ {restaurant.name}
+
-
음식점 소개 문구
+
{restaurant.description}
diff --git a/src/components/RestaurantList/RestaurantList.jsx b/src/components/RestaurantList/RestaurantList.jsx
index 41ac3a3..7fbdb78 100644
--- a/src/components/RestaurantList/RestaurantList.jsx
+++ b/src/components/RestaurantList/RestaurantList.jsx
@@ -1,13 +1,17 @@
import styles from "./RestaurantList.module.css";
import { CATEGORY_IMAGES } from "../../constants/categoryImages.js";
-export default function RestaurantList({ restaurants }) {
+export default function RestaurantList({ restaurants, onRestaurantClick }) {
return (