From 8657bfc6f061f8ea80a866585c794498c6dfe5e6 Mon Sep 17 00:00:00 2001 From: Yeryeong Kang Date: Mon, 8 Jun 2026 15:52:58 +0900 Subject: [PATCH 01/14] =?UTF-8?q?feat:=20=EC=9D=8C=EC=8B=9D=EC=A0=90=20?= =?UTF-8?q?=ED=81=B4=EB=A6=AD=20=EC=8B=9C=20=EC=83=81=EC=84=B8=20=EB=AA=A8?= =?UTF-8?q?=EB=8B=AC=20=EC=97=B4=EA=B8=B0/=EB=8B=AB=EA=B8=B0=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/App.jsx | 21 +++++++++++++++++-- src/components/Modal/AddRestaurantModal.jsx | 2 +- .../RestaurantList/RestaurantList.jsx | 8 +++++-- 3 files changed, 26 insertions(+), 5 deletions(-) diff --git a/src/App.jsx b/src/App.jsx index 5bdd188..f707746 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -9,6 +9,7 @@ import AddRestaurantModal from "./components/Modal/AddRestaurantModal"; function App() { const [category, setCategory] = useState("전체"); + const [clickedRestaurant, setClickedRestaurant] = useState(null); const filteredRestaurants = category === "전체" @@ -19,6 +20,14 @@ function App() { setCategory(e.target.value); }; + const handleRestaurantClick = (restaurant) => { + setClickedRestaurant(restaurant); + }; + + const handleModalClose = () => { + setClickedRestaurant(null); + }; + return ( <>
@@ -27,10 +36,18 @@ function App() { category={category} onChangeCategory={handleSelectChange} /> - + diff --git a/src/components/Modal/AddRestaurantModal.jsx b/src/components/Modal/AddRestaurantModal.jsx index 42c6056..36d11f5 100644 --- a/src/components/Modal/AddRestaurantModal.jsx +++ b/src/components/Modal/AddRestaurantModal.jsx @@ -4,7 +4,7 @@ import { CATEGORIES } from "../../constants/categories"; export default function AddRestaurantModal() { return ( -
+

새로운 음식점

diff --git a/src/components/RestaurantList/RestaurantList.jsx b/src/components/RestaurantList/RestaurantList.jsx index 5ed710b..fb56e8b 100644 --- a/src/components/RestaurantList/RestaurantList.jsx +++ b/src/components/RestaurantList/RestaurantList.jsx @@ -1,12 +1,16 @@ import styles from "./RestaurantList.module.css"; import { categoryImages } from "../../constants/categoryImages"; -export default function RestaurantList({ restaurants }) { +export default function RestaurantList({ restaurants, onRestaurantClick }) { return (
    {restaurants.map((restaurant) => ( -
  • +
  • onRestaurantClick(restaurant)} + >
    Date: Mon, 8 Jun 2026 15:53:07 +0900 Subject: [PATCH 02/14] =?UTF-8?q?feat:=20=EB=AA=A8=EB=8B=AC=EC=97=90=20?= =?UTF-8?q?=ED=81=B4=EB=A6=AD=ED=95=9C=20=EC=9D=8C=EC=8B=9D=EC=A0=90=20?= =?UTF-8?q?=EC=A0=95=EB=B3=B4=20=ED=91=9C=EC=8B=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/Modal/RestaurantDetailModal.jsx | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/components/Modal/RestaurantDetailModal.jsx b/src/components/Modal/RestaurantDetailModal.jsx index 2dcbc86..51ed8d5 100644 --- a/src/components/Modal/RestaurantDetailModal.jsx +++ b/src/components/Modal/RestaurantDetailModal.jsx @@ -1,20 +1,21 @@ import styles from "./Modal.module.css"; -export default function RestaurantDetailModal() { +export default function RestaurantDetailModal({ clickedRestaurant, onClose }) { return (
    -
    +
    -

    음식점 이름

    +

    + {clickedRestaurant.name} +

    -

    - 음식점 소개 문구 -

    +

    {clickedRestaurant.description}

    {/* 닫기 버튼 */}
    From 5d51bad281a7e181f80a281b85f90627e8c81418 Mon Sep 17 00:00:00 2001 From: Yeryeong Kang Date: Mon, 8 Jun 2026 15:53:17 +0900 Subject: [PATCH 03/14] =?UTF-8?q?docs:=20Step3=20README=20=EC=B4=88?= =?UTF-8?q?=EC=95=88=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 254 ++---------------------------------------------------- 1 file changed, 9 insertions(+), 245 deletions(-) diff --git a/README.md b/README.md index 58f25cc..a70b353 100644 --- a/README.md +++ b/README.md @@ -1,271 +1,35 @@ -# Self-Paced React Step 2 +# Self-Paced React Step 3 ## 🎯 개인 목표 및 목표 달성을 위한 행동 가이드 이번 미션을 통해 다음과 같은 학습 경험들을 쌓는 것을 목표로 한다. -**1. Props의 개념과 데이터 흐름 이해** +**1. 이벤트 핸들러를 통한 데이터 전달 이해** -부모 컴포넌트에서 자식 컴포넌트로 데이터를 전달하는 방식을 직접 구현하며 단방향 데이터 흐름을 체득한다. +자식 컴포넌트에서 발생한 이벤트가 부모로 어떻게 전달되는지 이해하고, +클릭 이벤트와 함께 데이터를 넘기는 패턴을 직접 구현한다. -**2. State와 useState 사용법 이해** +**2. 조건부 렌더링 이해** -`useState`를 활용하여 카테고리 필터 상태를 관리하고, 상태 변화에 따라 UI가 어떻게 반응하는지 이해한다. +`&&` 연산자를 활용하여 특정 조건일 때만 컴포넌트를 렌더링하는 방식을 익힌다. -**3. 목록 렌더링과 Key의 역할 이해** +**3. 파생 상태(Derived State) 이해** -배열 데이터를 컴포넌트 목록으로 렌더링하는 방법을 익히고, `key` props가 필요한 이유를 이해한다. +모달 열림 여부처럼 기존 상태에서 계산 가능한 값은 별도 state로 선언하지 않고 +기존 state를 활용하는 방식을 이해한다. --- ## 📝 기능 구현 목록 -**1. Props를 활용한 컴포넌트 데이터 연결** - -- `restaurants` 배열 분리 -- `restaurants` 배열을 `App.jsx`에서 `RestaurantList`의 props로 전달한다. -- `RestaurantList`는 전달받은 배열을 순회하여 각 음식점 정보를 렌더링한다. -- 목록 렌더링 시 각 항목에 `key` props를 부여한다. - -**2. 카테고리 필터 상태 관리** - -- `useState`로 현재 선택된 카테고리 상태를 관리한다. -- `CategoryFilter`에 현재 카테고리 값과 변경 핸들러를 props로 전달한다. -- 선택된 카테고리에 따라 `restaurants` 배열을 필터링한 `filteredRestaurants`를 `RestaurantList`에 전달한다. - ---- - ## 📚 학습 내용 -### 1. Props - -Props는 부모 컴포넌트가 자식 컴포넌트에 데이터를 전달하는 방법이다. 함수의 인자처럼 동작하며, 자식 컴포넌트는 전달받은 props를 읽기만 할 수 있고 직접 수정할 수 없다. - -```jsx -// 부모 컴포넌트 (App.jsx) -; - -// 자식 컴포넌트 (RestaurantList.jsx) -function RestaurantList({ restaurants }) { - return ( -
      - {restaurants.map((restaurant) => ( -
    • {restaurant.name}
    • - ))} -
    - ); -} -``` - -### 2. State와 useState - -State는 컴포넌트가 직접 소유하고 관리하는 데이터다. Props와 달리 컴포넌트 내부에서 변경할 수 있으며, 상태가 변경되면 해당 컴포넌트와 자식 컴포넌트가 리렌더링된다. - -`useState`는 상태를 선언하는 React Hook으로, 현재 상태값과 상태를 변경하는 함수를 배열로 반환한다. - -```jsx -const [category, setCategory] = useState("전체"); -``` - -- `category`: 현재 상태값 -- `setCategory`: 상태를 변경하는 함수. 호출 시 컴포넌트가 리렌더링된다. -- `"전체"`: 초기값 - -### 3. 목록 렌더링과 Key - -배열 데이터를 컴포넌트 목록으로 렌더링할 때는 `map()`을 사용한다. 이때 각 항목에 `key` props를 부여해야 한다. - -```jsx -restaurants.map((restaurant) => ( - -)); -``` - -`key`는 React가 목록에서 어떤 항목이 추가, 변경, 삭제되었는지 식별하기 위해 사용한다. 목록 내에서 고유한 값이어야 하며, 배열 인덱스보다 데이터 고유 id를 사용하는 것이 권장된다. - -### 4. BEM 네이밍과 CSS 모듈 표기법 - -BEM(Block Element Modifier)은 CSS 클래스 이름을 짓는 방법론이다. - -- **Block**: 독립적으로 의미 있는 UI 단위 (`.modal`, `.button`) -- **Element**: Block의 하위 요소, `__`로 연결 (`.modal__backdrop`, `.restaurant__name`) -- **Modifier**: Block/Element의 변형, `--`로 연결 (`.modal--open`, `.button--primary`) - -처음엔 하이픈(`-`)을 BEM 구분자로 혼동했다. `-`는 단어 구분자일 뿐이다. `form-item`은 하나의 Block 이름이며, 그 하위 요소는 `form-item__help-text`가 된다. - -CSS Modules에서 `--`가 포함된 클래스는 JS에서 감소 연산자로 파싱되어 오류가 발생한다. 따라서 접근 표기법 규칙을 다음과 같이 통일했다. - -```jsx -// block/element → 점 표기법 -className={styles.modal__backdrop} - -// modifier(--) → 대괄호 표기법 -className={styles["modal--open"]} -``` - ---- - ## 🤔 고민했던 문제와 해결 과정에서 배운 점 -### 1. 리스트 렌더링과 Props 전달 - -**고민했던 점** - -부모에서 넘겨준 배열 데이터를 자식 컴포넌트에서 받을 때 `restaurants`를 구조 분해 없이 그대로 받아 데이터를 제대로 꺼내지 못했다. 또한 `map()` 안에서 반환할 태그 구조를 잘못 작성하여 `
  • ` 안에 `
  • `가 중첩되는 문제가 발생했다. - -**해결 과정 및 배운 점** - -매개변수 자리에 구조 분해 할당(`{ restaurants }`)을 사용하여 props에서 배열 데이터를 바로 꺼내도록 수정했다. `map()`이 반환하는 요소의 범위를 다시 파악하고 태그 구조를 올바르게 정리했다. - -### 2. 조건부 이미지 렌더링 - -**고민했던 점** - -카테고리(`"한식"`, `"중식"` 등)에 따라 서로 다른 이미지를 렌더링해야 했다. 처음에는 `restaurants` 데이터에 이미지 경로를 직접 추가하는 방법을 고민했으나, 데이터와 UI 자원을 분리하는 것이 더 적절하다고 판단했다. - -**해결 과정 및 배운 점** - -카테고리명을 키, 이미지 경로를 값으로 하는 객체(`categoryImages`)를 컴포넌트 외부에 선언하고, `` 태그의 `src` 속성에 `categoryImages[restaurant.category]` 형태로 접근하여 복잡한 조건문 없이 카테고리에 맞는 이미지를 동적으로 렌더링했다. - -### 3. State와 배열 필터링 - -**고민했던 점** - -선택된 카테고리 상태에 따라 원본 배열에서 필요한 데이터만 추려내는 과정에서, 조건 분기를 `filter()` 내부에서 처리하려다 오류가 발생했다. - -**해결 과정 및 배운 점** - -조건 분기를 `filter()` 바깥으로 꺼내, 삼항 연산자로 `category === "전체"`일 때는 원본 배열을, 아닐 때는 필터링된 배열을 반환하도록 수정했다. `filter()`의 역할과 삼항 연산자의 분기 처리를 명확히 분리하는 것이 가독성과 동작 안정성 모두에 유리하다는 것을 배웠다. - -```jsx -const filteredRestaurants = - category === "전체" - ? restaurants - : restaurants.filter((r) => r.category === category); -``` - -### 4. `` 엘리먼트의 변화를 감지해 상태를 업데이트해야 했다. ``의 `value` 속성에 현재 상태를 연결하고, `onChange` 핸들러에서 `e.target.value`로 선택된 값만 추출하여 상태 변경 함수에 전달하도록 수정했다. React에서 ` onChangeCategory(e.target.value)}> - 전체 - 한식 - ... - -``` - ## 🛠 리팩토링 -1. **컴포넌트 단위 디렉토리 구조 재편** - - `src/` 루트에 혼재하던 파일들을 `src/components/` 하위에 컴포넌트별 폴더로 분리했다. - - 모달의 경우 두 컴포넌트가 공유하는 `Modal.module.css`를 유지하되, `AddRestaurantModal` 전용 폼 스타일만 `AddRestaurantModal.module.css`로 분리했다. 전용 스타일 양이 적은 컴포넌트까지 파일을 나누면 복잡도만 높아진다고 판단했다. - -2. **BEM 네이밍 적용 및 CSS 모듈 표기법 일관성 확보** - - 하이픈(`-`), `__`, `--`가 혼재하던 클래스명을 BEM 원칙에 맞게 정리했다. - - `block__element`는 점 표기법, `modifier(--)`는 JS 감소 연산자 충돌 방지를 위해 대괄호 표기법으로 통일했다. - -3. **이미지 모듈 임포트 방식 전환** - - `public/`에 저장하여 정적 경로로 참조하던 방식에서 `src/assets/`로 이동 후 모듈 import 방식으로 전환했다. - - 빌드 타임 에러 검출과 번들러의 자동 최적화·캐싱 기능을 활용할 수 있다. - -4. **상수 분리 및 디렉토리 정리** - - `categoryImages` 매핑 객체를 `src/constants/categoryImages.js`로 분리했다. - - `restaurants` 데이터를 `src/utils/`에서 의미상 더 적합한 `src/constants/`로 이동했다. - - 카테고리 목록 상수를 `src/constants/categories.js`로 분리하여 `CategoryFilter`와 `AddRestaurantModal`에서 공유하도록 했다. 두 컴포넌트가 사용하는 목록이 달라 `CATEGORIES`(6개 카테고리)와 `FILTER_OPTIONS`("전체" 포함 7개)를 각각 분리하고, `FILTER_OPTIONS`는 스프레드 문법으로 `CATEGORIES`를 재활용했다. - -```js -export const CATEGORIES = ["한식", "중식", "일식", "양식", "아시안", "기타"]; -export const FILTER_OPTIONS = ["전체", ...CATEGORIES]; -``` - -5. **이벤트 핸들러 위치 변경** - - `setCategory`를 props로 직접 전달하던 방식에서, `App.jsx`에 `handleSelectChange`를 정의하여 전달하는 방식으로 변경했다. - - setter를 직접 노출하면 내부 구현이 드러나고, 이후 카테고리 변경 시 추가 동작을 붙이기 어렵다는 점을 고려했다. - -6. **`map()` 인라인 방식 적용** - - 변수에 미리 담아 반환하던 방식에서, JSX 안에서 `map()`을 직접 호출하는 인라인 방식으로 변경했다. - ---- - ## 과거 코드와 비교 ### 달라진 점 -기준: 과거 동아리 미션 step2 코드 vs 현재 코드 - -**1. CSS 방식 — 전역 CSS → CSS Modules** - -| 구분 | 과거 코드 | 현재 코드 | -|------|---------|---------| -| 파일 위치 | `src/styles/components/CategoryFilter.css` | `src/components/CategoryFilter/CategoryFilter.module.css` | -| 클래스 적용 | `className="restaurant-filter"` | `className={styles["restaurant-filter"]}` | - -전역 CSS는 클래스명이 빌드 결과물에 그대로 노출되어, 다른 컴포넌트에 동일한 클래스명이 있으면 스타일이 충돌할 수 있다. CSS Modules는 빌드 시 `CategoryFilter_restaurant-filter__aBc3d` 같은 고유한 이름으로 변환하여 충돌을 방지한다. - -**2. 디렉토리 구조** - -| 구분 | 과거 코드 | 현재 코드 | -|------|---------|---------| -| 컴포넌트 | `src/components/Header.jsx` | `src/components/Header/Header.jsx` | -| 스타일 | `src/styles/components/Header.css` | `src/components/Header/Header.module.css` | - -과거엔 JSX와 CSS 파일이 서로 다른 폴더에 분리되어 있었다. 현재는 같은 컴포넌트의 파일을 하나의 폴더에 모아 응집도를 높였다. - -**3. 이미지 관리** - -| 구분 | 과거 코드 | 현재 코드 | -|------|---------|---------| -| 파일 위치 | `public/category-korean.png` | `src/assets/category-korean.png` | -| 경로 참조 | `src="/category-korean.png"` | `import koreanImg from "…"` → `src={koreanImg}` | - -정적 경로는 경로가 잘못되어도 빌드 시 에러가 발생하지 않아 런타임에서야 깨진 이미지를 확인할 수 있다. - ### 과거 코드에서 배운 점 - -**1. 데이터와 UI 분리, `map()`을 활용한 렌더링** - -Step 1에서 `RestaurantList`의 `
  • ` 6개와 `CategoryFilter`의 ` - -// ... - -// 수정 후: 배열 + map() -const OPTIONS = ["전체", "한식", "중식", ...]; -{OPTIONS.map((opt) => ( - -))} -``` - -**2. `id`와 `key`의 역할 차이** - -`CategoryFilter`에 `id`를 추가한 의도를 설명하는 과정에서 두 개념을 혼동하여 리뷰어와 깊은 토론을 거쳤다. - -- `id`: HTML DOM 전체에서 유일한 공개 식별자. `
  • onRestaurantClick(restaurant)} + onKeyDown={(e) => { + if (e.key === "Enter" || e.key === " ") { + onRestaurantClick(restaurant); + } + }} >
    Date: Mon, 8 Jun 2026 16:03:05 +0900 Subject: [PATCH 07/14] =?UTF-8?q?docs:=20=ED=95=99=EC=8A=B5=20=EB=82=B4?= =?UTF-8?q?=EC=9A=A9,=20=EB=A6=AC=ED=8C=A9=ED=86=A0=EB=A7=81,=20=EA=B3=BC?= =?UTF-8?q?=EA=B1=B0=20=EC=BD=94=EB=93=9C=20=EB=B9=84=EA=B5=90=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 --- README.md | 60 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/README.md b/README.md index 49be534..eac4582 100644 --- a/README.md +++ b/README.md @@ -69,6 +69,27 @@ setClickedRestaurant(restaurant); 모달 열림 여부를 관리하기 위해 별도 boolean state(`isModalOpen`)를 선언하지 않아도 된다. `clickedRestaurant`가 null이면 닫힘, 객체면 열림 — 기존 state에서 의미를 도출할 수 있기 때문이다. +### 4. 웹 접근성 (a11y) — 클릭 가능한 비버튼 요소 + +`
  • ` 같은 비인터랙티브 요소에 `onClick`을 달면 마우스 사용자는 동작하지만, 키보드 사용자는 접근할 수 없다. 다음 세 가지를 함께 추가해야 한다. + +- `role="button"`: 스크린 리더에게 이 요소가 버튼처럼 동작함을 알린다. +- `tabIndex={0}`: Tab 키로 포커스를 받을 수 있게 한다. +- `onKeyDown`: Enter 또는 Space 키 입력 시 onClick과 동일한 동작을 수행한다. + +```jsx +
  • onRestaurantClick(restaurant)} + onKeyDown={(e) => { + if (e.key === "Enter" || e.key === " ") { + onRestaurantClick(restaurant); + } + }} +> +``` + --- ## 🤔 고민했던 문제와 해결 과정에서 배운 점 @@ -95,8 +116,47 @@ const handleRestaurantClick = (restaurant) => { ## 🛠 리팩토링 +**1. 클릭 가능한 `
  • `에 웹 접근성 속성 추가** + +`
  • `에 `onClick`만 달면 키보드 사용자는 접근할 수 없다. 과거 코드 리뷰에서 받은 피드백을 반영하여 `role="button"`, `tabIndex={0}`, `onKeyDown`을 추가했다. + +--- + ## 과거 코드와 비교 ### 달라진 점 +**1. State 통합 — boolean 분리 → 단일 객체 state** + +| 구분 | 과거 코드 | 현재 코드 | +|------|---------|---------| +| 모달 상태 관리 | `selected` + `isModalOpen` 2개 | `clickedRestaurant` 1개 | + +과거엔 `selected`(선택된 음식점)와 `isModalOpen`(열림 여부)를 따로 관리했다. 두 상태는 항상 함께 변하기 때문에 불필요한 중복이 생긴다. 현재는 `clickedRestaurant`가 null이면 닫힘, 객체면 열림으로 하나의 state에서 두 의미를 모두 표현했다. + ### 과거 코드에서 배운 점 + +**1. 파생 상태는 state로 선언하지 않는다** + +리뷰어 피드백: *"두 상태가 서로 의존적인 관계인데 분리해서 관리할 경우 일관성이 깨질 수 있습니다. 불필요한 상태는 제거해 하나의 상태로 관리해보는 건 어떨까요?"* + +다른 state에서 계산 가능한 값을 별도 state로 선언하면 두 상태가 불일치할 위험이 생긴다. 기존 state만으로 표현 가능하다면 파생 상태로 처리하는 것이 더 안전하다. + +**2. 웹 접근성 — 클릭 가능한 `
  • `** + +리뷰어 피드백: *"`
  • `에 onClick 이벤트를 사용할 경우 role, tabIndex, onKeyDown을 추가해 키보드 사용자도 접근할 수 있도록 해야 합니다."* + +```jsx +
  • onRestaurantClick(restaurant)} + onKeyDown={(e) => { + if (e.key === "Enter" || e.key === " ") { + onRestaurantClick(restaurant); + } + }} +> +``` + +마우스 없이 Tab + Enter로도 동작하도록 보장하는 것이 웹 접근성의 기본이다. From 80c915faddf8d21b4e34b48b75b247e063bfb62f Mon Sep 17 00:00:00 2001 From: Yeryeong Kang Date: Sat, 13 Jun 2026 20:44:34 +0900 Subject: [PATCH 08/14] =?UTF-8?q?docs:=20README=20=ED=95=99=EC=8A=B5=20?= =?UTF-8?q?=EB=82=B4=EC=9A=A9=20=EB=B3=B4=EC=99=84=20=EB=B0=8F=20=EC=A4=91?= =?UTF-8?q?=EB=B3=B5=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 43 +++++++++++++++++++++++++++---------------- 1 file changed, 27 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index eac4582..5967581 100644 --- a/README.md +++ b/README.md @@ -24,20 +24,16 @@ **1. 음식점 아이템 클릭 시 상세 모달 열기** -- `clickedRestaurant` state를 App.jsx에 추가한다. (초기값: null) -- `handleRestaurantClick(restaurant)` 핸들러를 정의하여 `RestaurantList`에 전달한다. -- `RestaurantList`는 아이템 클릭 시 해당 restaurant 객체를 핸들러에 넘긴다. -- `clickedRestaurant`가 null이 아닐 때만 `RestaurantDetailModal`을 렌더링한다. +- 클릭한 음식점 정보를 state로 관리한다. +- 상태가 null이 아닐 때만 모달을 조건부 렌더링한다. **2. 모달 닫기** -- `handleModalClose` 핸들러를 정의하여 `setClickedRestaurant(null)`로 모달을 닫는다. -- 닫기 버튼과 backdrop 클릭 시 `onClose` props를 통해 핸들러를 호출한다. +- 닫기 버튼 또는 backdrop 클릭 시 모달을 닫는다. **3. 클릭한 음식점 정보 모달에 표시** -- `clickedRestaurant` 객체를 `RestaurantDetailModal`에 props로 전달한다. -- 모달에서 `clickedRestaurant.name`, `clickedRestaurant.description`을 렌더링한다. +- 클릭한 음식점의 이름과 설명을 모달에 렌더링한다. --- @@ -67,7 +63,12 @@ setClickedRestaurant(restaurant); ### 3. 파생 상태 (Derived State) -모달 열림 여부를 관리하기 위해 별도 boolean state(`isModalOpen`)를 선언하지 않아도 된다. `clickedRestaurant`가 null이면 닫힘, 객체면 열림 — 기존 state에서 의미를 도출할 수 있기 때문이다. +다른 state나 props로부터 계산할 수 있는 값은 별도 state로 선언하지 않는다. + +- **state가 필요한 경우**: 다른 값으로부터 계산할 수 없고, 시간이 지나면서 변하며, 렌더링에 영향을 주는 값 +- **state가 불필요한 경우**: 기존 state나 props에서 계산 가능한 파생값 + +파생값을 별도 state로 선언하면 두 state를 항상 함께 업데이트해야 하는데, 하나라도 빠지면 불일치 버그가 발생한다. ### 4. 웹 접근성 (a11y) — 클릭 가능한 비버튼 요소 @@ -98,17 +99,19 @@ setClickedRestaurant(restaurant); **고민했던 점** -`handleRestaurantClick` 핸들러에서 인자를 선언하지 않아 `setClickedRestaurant`에 항상 null이 저장되는 문제가 발생했다. +`RestaurantList`에서 클릭 시 `restaurant` 객체를 넘겨줬는데, `App`의 핸들러에 매개변수를 선언하지 않아 항상 `null`(초기값)이 유지되는 문제가 발생했다. **해결 과정 및 배운 점** -RestaurantList에서 `onClick={() => onRestaurantClick(restaurant)}`로 restaurant 객체를 인자로 넘기고, App의 핸들러에서 `(restaurant) => setClickedRestaurant(restaurant)` 형태로 받아야 한다는 것을 이해했다. 이벤트가 발생하는 곳에서 인자를 넘기고, 핸들러에서 매개변수로 받는 흐름을 체득했다. +넘겨주는 쪽과 받는 쪽이 짝이 맞아야 한다는 것을 이해했다. 인자를 넘겨도 매개변수가 없으면 그냥 사라진다. ```jsx -// RestaurantList — 클릭 시 restaurant 객체를 넘김 -onClick={() => onRestaurantClick(restaurant)} +// ❌ 매개변수 없음 — restaurant가 무시됨 +const handleRestaurantClick = () => { + setClickedRestaurant(restaurant); // restaurant를 알 수 없어 에러 or null 유지 +}; -// App.jsx — 넘겨받은 객체를 state에 저장 +// ✅ 매개변수 선언 — 넘겨받은 객체를 state에 저장 const handleRestaurantClick = (restaurant) => { setClickedRestaurant(restaurant); }; @@ -132,7 +135,6 @@ const handleRestaurantClick = (restaurant) => { |------|---------|---------| | 모달 상태 관리 | `selected` + `isModalOpen` 2개 | `clickedRestaurant` 1개 | -과거엔 `selected`(선택된 음식점)와 `isModalOpen`(열림 여부)를 따로 관리했다. 두 상태는 항상 함께 변하기 때문에 불필요한 중복이 생긴다. 현재는 `clickedRestaurant`가 null이면 닫힘, 객체면 열림으로 하나의 state에서 두 의미를 모두 표현했다. ### 과거 코드에서 배운 점 @@ -140,7 +142,16 @@ const handleRestaurantClick = (restaurant) => { 리뷰어 피드백: *"두 상태가 서로 의존적인 관계인데 분리해서 관리할 경우 일관성이 깨질 수 있습니다. 불필요한 상태는 제거해 하나의 상태로 관리해보는 건 어떨까요?"* -다른 state에서 계산 가능한 값을 별도 state로 선언하면 두 상태가 불일치할 위험이 생긴다. 기존 state만으로 표현 가능하다면 파생 상태로 처리하는 것이 더 안전하다. +과거엔 선택된 음식점(`selected`)과 모달 열림 여부(`isModalOpen`)를 따로 관리했다. 두 state는 항상 함께 변해야 하는데, 하나라도 업데이트를 빠뜨리면 모달은 열려 있지만 데이터가 없는 상황이 생긴다. + +```jsx +// ❌ 과거: 두 state 동기화 필요 +const [selected, setSelected] = useState(null); +const [isModalOpen, setIsModalOpen] = useState(false); + +// ✅ 현재: 단일 state로 통합 — null이면 닫힘, 객체면 열림 +const [clickedRestaurant, setClickedRestaurant] = useState(null); +``` **2. 웹 접근성 — 클릭 가능한 `
  • `** From cd7c4e0b7460418a7095296fe08c5720d2dc461e Mon Sep 17 00:00:00 2001 From: Yeryeong Kang Date: Sat, 13 Jun 2026 21:34:13 +0900 Subject: [PATCH 09/14] =?UTF-8?q?docs:=20state=20=EC=84=A0=EC=96=B8=20?= =?UTF-8?q?=EA=B8=B0=EC=A4=80=20=EA=B3=A0=EB=AF=BC=20=EB=82=B4=EC=9A=A9=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 --- README.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/README.md b/README.md index 5967581..71ace53 100644 --- a/README.md +++ b/README.md @@ -117,6 +117,21 @@ const handleRestaurantClick = (restaurant) => { }; ``` +### 2. 무엇을 state로 선언할지 + +**고민했던 점** + +`clickedRestaurant`를 state로 선언해야 할지 헷갈렸다. `filteredRestaurants`처럼 기존 값에서 계산하면 되지 않을까 생각했다. + +**해결 과정 및 배운 점** + +구현하면서 결론은 냈지만 명확한 기준 없이 나눈 것 같았다. 팀원 코드 리뷰를 하면서 기준이 확실해졌다. + +- `filteredRestaurants` — `category`가 정해지면 계산 가능. 클릭 전에도 값이 존재한다. +- `clickedRestaurant` — 클릭 전엔 무엇이 선택될지 알 수 없어 파생할 근거 자체가 없다. 클릭이라는 사건이 일어나야 비로소 생기는 값이다. + +파생 상태의 기준이 "계산 가능하냐"인데, `clickedRestaurant`는 계산할 근거가 없으므로 state로 선언해야 한다. + ## 🛠 리팩토링 **1. 클릭 가능한 `
  • `에 웹 접근성 속성 추가** From 96d6a5df81285d3aa1e55dd220787c270e4a493a Mon Sep 17 00:00:00 2001 From: Kang Yeryeong Date: Sat, 13 Jun 2026 21:35:26 +0900 Subject: [PATCH 10/14] Update README.md --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 71ace53..f60fa30 100644 --- a/README.md +++ b/README.md @@ -132,6 +132,8 @@ const handleRestaurantClick = (restaurant) => { 파생 상태의 기준이 "계산 가능하냐"인데, `clickedRestaurant`는 계산할 근거가 없으므로 state로 선언해야 한다. +--- + ## 🛠 리팩토링 **1. 클릭 가능한 `
  • `에 웹 접근성 속성 추가** From ae70d6dc9b77a64d8670160225ba36da85f70c34 Mon Sep 17 00:00:00 2001 From: Yeryeong Kang Date: Sat, 13 Jun 2026 21:37:35 +0900 Subject: [PATCH 11/14] =?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 | 8 ++++---- src/components/RestaurantList/RestaurantList.jsx | 4 ++-- src/constants/categoryImages.js | 2 +- src/constants/restaurants.js | 2 +- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/App.jsx b/src/App.jsx index f707746..6010cea 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"; @@ -13,8 +13,8 @@ function App() { const filteredRestaurants = category === "전체" - ? restaurants - : restaurants.filter((restaurant) => restaurant.category === category); + ? RESTAURANTS + : RESTAURANTS.filter((restaurant) => restaurant.category === category); const handleSelectChange = (e) => { setCategory(e.target.value); @@ -37,7 +37,7 @@ function App() { onChangeCategory={handleSelectChange} /> 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 ed98543238391b51f21a891007851cef42c22513 Mon Sep 17 00:00:00 2001 From: Yeryeong Kang Date: Sun, 14 Jun 2026 15:16:29 +0900 Subject: [PATCH 12/14] =?UTF-8?q?refactor:=20handleModalClose=20=E2=86=92?= =?UTF-8?q?=20handleDetailModalClose,=20prop=EB=AA=85=20=EC=86=8C=EB=AC=B8?= =?UTF-8?q?=EC=9E=90=EB=A1=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/App.jsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/App.jsx b/src/App.jsx index 6010cea..07a5830 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"; @@ -24,7 +24,7 @@ function App() { setClickedRestaurant(restaurant); }; - const handleModalClose = () => { + const handleDetailModalClose = () => { setClickedRestaurant(null); }; @@ -37,7 +37,7 @@ function App() { onChangeCategory={handleSelectChange} /> @@ -45,7 +45,7 @@ function App() { {clickedRestaurant && ( )} From 5d53713c555772e4e0f0c7badf9cf575795eefa7 Mon Sep 17 00:00:00 2001 From: Yeryeong Kang Date: Sun, 14 Jun 2026 15:16:49 +0900 Subject: [PATCH 13/14] =?UTF-8?q?refactor:=20
  • =20=ED=81=B4=EB=A6=AD=20?= =?UTF-8?q?=EC=9D=B4=EB=B2=A4=ED=8A=B8=20=E2=86=92=20
  • ))}
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 9d0be27a886ff5052ba4f1104444f5ca9a36537c Mon Sep 17 00:00:00 2001 From: Yeryeong Kang Date: Sun, 14 Jun 2026 15:17:11 +0900 Subject: [PATCH 14/14] =?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 | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index f60fa30..1bd88b4 100644 --- a/README.md +++ b/README.md @@ -136,9 +136,17 @@ const handleRestaurantClick = (restaurant) => { ## 🛠 리팩토링 -**1. 클릭 가능한 `
  • `에 웹 접근성 속성 추가** +**1. 클릭 가능한 `
  • ` — `