diff --git a/02-rendering-lists/README.md b/02-rendering-lists/README.md deleted file mode 100644 index a054777..0000000 --- a/02-rendering-lists/README.md +++ /dev/null @@ -1,80 +0,0 @@ -# 02. 목록 UI 구현하기: Props와 State - -## 🎯 요구 사항 - -- `RestaurantList` 가 restaurants 배열을 받아서 그릴 수 있도록 변경해 보세요. - - restaurants 배열을 `RestaurantList` 의 props로 내려받도록 변경해 보세요. -- 카테고리 필터에 따라 필터된 음식점 목록을 보여줄 수 있도록 변경해 보세요. - -### 구현 결과 예시 - -```javascript -// App.jsx - - -``` - -```javascript -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: "기타", - }, -]; -``` - -## ✅ 키워드 - -- Props -- State - - useState -- Keys - -> [Rendering Lists](https://react.dev/learn/rendering-lists) 문서에 ['Why does React need keys?'](https://react.dev/learn/rendering-lists#why-does-react-need-keys)는 지금 꼭 이해하지 않아도 괜찮습니다. 그냥 React에서 목록을 동적으로 그릴 때에는 이런 것들을 사용해야 하는구나~ 정도로만 알고 일단 넘어가세요. 우선 사용하는 법에 익숙해지는 것이 먼저입니다 :) - -## 🧙‍♀️ 진행 가이드 - -- 진행 시간: 1시간 내에 완료하는 것을 목표로 합니다. - -## 🔗 참고 문서 - -- [Thinking in React](https://react.dev/learn/thinking-in-react)의 Step3-4 -- [Passing Props to a Component](https://react.dev/learn/passing-props-to-a-component) -- [Rendering Lists](https://react.dev/learn/rendering-lists) -- [State: A Component's Memory](https://react.dev/learn/state-a-components-memory) - - [API Reference: useState](https://react.dev/reference/react/useState) diff --git a/03-modal/README.md b/03-modal/README.md new file mode 100644 index 0000000..6a97651 --- /dev/null +++ b/03-modal/README.md @@ -0,0 +1,29 @@ +# 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/CLAUDE.md b/CLAUDE.md index 8bae7a1..f243167 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -16,6 +16,11 @@ 스텝 번호 매핑: step1 → step1, step2 → step2, step3/4 → step3_4, step5 → step5 +## 커밋 규칙 + +- 단계를 나눠서 작업할 때는 각 단계가 끝날 때마다 커밋을 찍는다 +- 커밋 명령어는 Claude가 파일 단위로 제안하고, 사용자가 직접 실행한다 + ## README 양식 heading 구조는 고정 템플릿이며 절대 변경하지 않는다. README는 포트폴리오 용도로 작성한다. diff --git a/README.md b/README.md index 58f25cc..1bd88b4 100644 --- a/README.md +++ b/README.md @@ -1,197 +1,152 @@ -# 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를 활용한 컴포넌트 데이터 연결** +**1. 음식점 아이템 클릭 시 상세 모달 열기** -- `restaurants` 배열 분리 -- `restaurants` 배열을 `App.jsx`에서 `RestaurantList`의 props로 전달한다. -- `RestaurantList`는 전달받은 배열을 순회하여 각 음식점 정보를 렌더링한다. -- 목록 렌더링 시 각 항목에 `key` props를 부여한다. +- 클릭한 음식점 정보를 state로 관리한다. +- 상태가 null이 아닐 때만 모달을 조건부 렌더링한다. -**2. 카테고리 필터 상태 관리** +**2. 모달 닫기** -- `useState`로 현재 선택된 카테고리 상태를 관리한다. -- `CategoryFilter`에 현재 카테고리 값과 변경 핸들러를 props로 전달한다. -- 선택된 카테고리에 따라 `restaurants` 배열을 필터링한 `filteredRestaurants`를 `RestaurantList`에 전달한다. +- 닫기 버튼 또는 backdrop 클릭 시 모달을 닫는다. ---- - -## 📚 학습 내용 +**3. 클릭한 음식점 정보 모달에 표시** -### 1. Props +- 클릭한 음식점의 이름과 설명을 모달에 렌더링한다. -Props는 부모 컴포넌트가 자식 컴포넌트에 데이터를 전달하는 방법이다. 함수의 인자처럼 동작하며, 자식 컴포넌트는 전달받은 props를 읽기만 할 수 있고 직접 수정할 수 없다. - -```jsx -// 부모 컴포넌트 (App.jsx) -; - -// 자식 컴포넌트 (RestaurantList.jsx) -function RestaurantList({ restaurants }) { - return ( -
    - {restaurants.map((restaurant) => ( -
  • {restaurant.name}
  • - ))} -
- ); -} -``` +--- -### 2. State와 useState +## 📚 학습 내용 -State는 컴포넌트가 직접 소유하고 관리하는 데이터다. Props와 달리 컴포넌트 내부에서 변경할 수 있으며, 상태가 변경되면 해당 컴포넌트와 자식 컴포넌트가 리렌더링된다. +### 1. 조건부 렌더링 -`useState`는 상태를 선언하는 React Hook으로, 현재 상태값과 상태를 변경하는 함수를 배열로 반환한다. +특정 조건일 때만 컴포넌트를 렌더링할 때 `&&` 연산자를 사용한다. ```jsx -const [category, setCategory] = useState("전체"); +{clickedRestaurant && } ``` -- `category`: 현재 상태값 -- `setCategory`: 상태를 변경하는 함수. 호출 시 컴포넌트가 리렌더링된다. -- `"전체"`: 초기값 +좌측 값이 falsy(null, undefined, false)이면 렌더링하지 않고, truthy이면 우측 컴포넌트를 렌더링한다. -### 3. 목록 렌더링과 Key +### 2. State 불변성과 리렌더링 -배열 데이터를 컴포넌트 목록으로 렌더링할 때는 `map()`을 사용한다. 이때 각 항목에 `key` props를 부여해야 한다. +React에서 state는 직접 수정하면 리렌더링이 트리거되지 않는다. 반드시 setter 함수를 통해야 React가 변화를 감지하고 화면을 다시 그린다. ```jsx -restaurants.map((restaurant) => ( +// ❌ 직접 수정 — 리렌더링 안 됨 +clickedRestaurant = restaurant; -)); +// ✅ setter 사용 — 리렌더링 트리거 +setClickedRestaurant(restaurant); ``` -`key`는 React가 목록에서 어떤 항목이 추가, 변경, 삭제되었는지 식별하기 위해 사용한다. 목록 내에서 고유한 값이어야 하며, 배열 인덱스보다 데이터 고유 id를 사용하는 것이 권장된다. +### 3. 파생 상태 (Derived State) -### 4. BEM 네이밍과 CSS 모듈 표기법 +다른 state나 props로부터 계산할 수 있는 값은 별도 state로 선언하지 않는다. -BEM(Block Element Modifier)은 CSS 클래스 이름을 짓는 방법론이다. +- **state가 필요한 경우**: 다른 값으로부터 계산할 수 없고, 시간이 지나면서 변하며, 렌더링에 영향을 주는 값 +- **state가 불필요한 경우**: 기존 state나 props에서 계산 가능한 파생값 -- **Block**: 독립적으로 의미 있는 UI 단위 (`.modal`, `.button`) -- **Element**: Block의 하위 요소, `__`로 연결 (`.modal__backdrop`, `.restaurant__name`) -- **Modifier**: Block/Element의 변형, `--`로 연결 (`.modal--open`, `.button--primary`) +파생값을 별도 state로 선언하면 두 state를 항상 함께 업데이트해야 하는데, 하나라도 빠지면 불일치 버그가 발생한다. -처음엔 하이픈(`-`)을 BEM 구분자로 혼동했다. `-`는 단어 구분자일 뿐이다. `form-item`은 하나의 Block 이름이며, 그 하위 요소는 `form-item__help-text`가 된다. +### 4. 웹 접근성 (a11y) — 클릭 가능한 비버튼 요소 -CSS Modules에서 `--`가 포함된 클래스는 JS에서 감소 연산자로 파싱되어 오류가 발생한다. 따라서 접근 표기법 규칙을 다음과 같이 통일했다. +`
  • ` 같은 비인터랙티브 요소에 `onClick`을 달면 마우스 사용자는 동작하지만, 키보드 사용자는 접근할 수 없다. 다음 세 가지를 함께 추가해야 한다. -```jsx -// block/element → 점 표기법 -className={styles.modal__backdrop} +- `role="button"`: 스크린 리더에게 이 요소가 버튼처럼 동작함을 알린다. +- `tabIndex={0}`: Tab 키로 포커스를 받을 수 있게 한다. +- `onKeyDown`: Enter 또는 Space 키 입력 시 onClick과 동일한 동작을 수행한다. -// modifier(--) → 대괄호 표기법 -className={styles["modal--open"]} +```jsx +
  • onRestaurantClick(restaurant)} + onKeyDown={(e) => { + if (e.key === "Enter" || e.key === " ") { + onRestaurantClick(restaurant); + } + }} +> ``` --- ## 🤔 고민했던 문제와 해결 과정에서 배운 점 -### 1. 리스트 렌더링과 Props 전달 - -**고민했던 점** - -부모에서 넘겨준 배열 데이터를 자식 컴포넌트에서 받을 때 `restaurants`를 구조 분해 없이 그대로 받아 데이터를 제대로 꺼내지 못했다. 또한 `map()` 안에서 반환할 태그 구조를 잘못 작성하여 `
  • ` 안에 `
  • `가 중첩되는 문제가 발생했다. - -**해결 과정 및 배운 점** - -매개변수 자리에 구조 분해 할당(`{ restaurants }`)을 사용하여 props에서 배열 데이터를 바로 꺼내도록 수정했다. `map()`이 반환하는 요소의 범위를 다시 파악하고 태그 구조를 올바르게 정리했다. - -### 2. 조건부 이미지 렌더링 - -**고민했던 점** - -카테고리(`"한식"`, `"중식"` 등)에 따라 서로 다른 이미지를 렌더링해야 했다. 처음에는 `restaurants` 데이터에 이미지 경로를 직접 추가하는 방법을 고민했으나, 데이터와 UI 자원을 분리하는 것이 더 적절하다고 판단했다. - -**해결 과정 및 배운 점** - -카테고리명을 키, 이미지 경로를 값으로 하는 객체(`categoryImages`)를 컴포넌트 외부에 선언하고, `` 태그의 `src` 속성에 `categoryImages[restaurant.category]` 형태로 접근하여 복잡한 조건문 없이 카테고리에 맞는 이미지를 동적으로 렌더링했다. - -### 3. State와 배열 필터링 +### 1. 이벤트 핸들러 인자 전달 **고민했던 점** -선택된 카테고리 상태에 따라 원본 배열에서 필요한 데이터만 추려내는 과정에서, 조건 분기를 `filter()` 내부에서 처리하려다 오류가 발생했다. +`RestaurantList`에서 클릭 시 `restaurant` 객체를 넘겨줬는데, `App`의 핸들러에 매개변수를 선언하지 않아 항상 `null`(초기값)이 유지되는 문제가 발생했다. **해결 과정 및 배운 점** -조건 분기를 `filter()` 바깥으로 꺼내, 삼항 연산자로 `category === "전체"`일 때는 원본 배열을, 아닐 때는 필터링된 배열을 반환하도록 수정했다. `filter()`의 역할과 삼항 연산자의 분기 처리를 명확히 분리하는 것이 가독성과 동작 안정성 모두에 유리하다는 것을 배웠다. +넘겨주는 쪽과 받는 쪽이 짝이 맞아야 한다는 것을 이해했다. 인자를 넘겨도 매개변수가 없으면 그냥 사라진다. ```jsx -const filteredRestaurants = - category === "전체" - ? restaurants - : restaurants.filter((r) => r.category === category); +// ❌ 매개변수 없음 — restaurant가 무시됨 +const handleRestaurantClick = () => { + setClickedRestaurant(restaurant); // restaurant를 알 수 없어 에러 or null 유지 +}; + +// ✅ 매개변수 선언 — 넘겨받은 객체를 state에 저장 +const handleRestaurantClick = (restaurant) => { + setClickedRestaurant(restaurant); +}; ``` -### 4. `` 엘리먼트의 변화를 감지해 상태를 업데이트해야 했다. ``의 `value` 속성에 현재 상태를 연결하고, `onChange` 핸들러에서 `e.target.value`로 선택된 값만 추출하여 상태 변경 함수에 전달하도록 수정했다. React에서 ` onChangeCategory(e.target.value)}> - 전체 - 한식 - ... - -``` +- `filteredRestaurants` — `category`가 정해지면 계산 가능. 클릭 전에도 값이 존재한다. +- `clickedRestaurant` — 클릭 전엔 무엇이 선택될지 알 수 없어 파생할 근거 자체가 없다. 클릭이라는 사건이 일어나야 비로소 생기는 값이다. -## 🛠 리팩토링 +파생 상태의 기준이 "계산 가능하냐"인데, `clickedRestaurant`는 계산할 근거가 없으므로 state로 선언해야 한다. -1. **컴포넌트 단위 디렉토리 구조 재편** - - `src/` 루트에 혼재하던 파일들을 `src/components/` 하위에 컴포넌트별 폴더로 분리했다. - - 모달의 경우 두 컴포넌트가 공유하는 `Modal.module.css`를 유지하되, `AddRestaurantModal` 전용 폼 스타일만 `AddRestaurantModal.module.css`로 분리했다. 전용 스타일 양이 적은 컴포넌트까지 파일을 나누면 복잡도만 높아진다고 판단했다. +--- -2. **BEM 네이밍 적용 및 CSS 모듈 표기법 일관성 확보** - - 하이픈(`-`), `__`, `--`가 혼재하던 클래스명을 BEM 원칙에 맞게 정리했다. - - `block__element`는 점 표기법, `modifier(--)`는 JS 감소 연산자 충돌 방지를 위해 대괄호 표기법으로 통일했다. +## 🛠 리팩토링 -3. **이미지 모듈 임포트 방식 전환** - - `public/`에 저장하여 정적 경로로 참조하던 방식에서 `src/assets/`로 이동 후 모듈 import 방식으로 전환했다. - - 빌드 타임 에러 검출과 번들러의 자동 최적화·캐싱 기능을 활용할 수 있다. +**1. 클릭 가능한 `
  • ` — ` diff --git a/src/components/RestaurantList/RestaurantList.jsx b/src/components/RestaurantList/RestaurantList.jsx index 5ed710b..45ae6c8 100644 --- a/src/components/RestaurantList/RestaurantList.jsx +++ b/src/components/RestaurantList/RestaurantList.jsx @@ -1,27 +1,32 @@ import styles from "./RestaurantList.module.css"; -import { categoryImages } from "../../constants/categoryImages"; +import { CATEGORY_IMAGES } from "../../constants/categoryImages"; -export default function RestaurantList({ restaurants }) { +export default function RestaurantList({ restaurants, onRestaurantClick }) { return (
      {restaurants.map((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; 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: "피양콩할마니",