-
Notifications
You must be signed in to change notification settings - Fork 0
[Step2] hippo - Props와 State #2
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
meteorqz6
wants to merge
6
commits into
hippo-step1
Choose a base branch
from
hippo-step2
base: hippo-step1
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
d91aaae
docs: 2단계 미션 요구사항 추가
meteorqz6 540e7c1
feat: 음식점 목록 필터링 기능 구현
meteorqz6 f9f702c
docs: README 추가
meteorqz6 9132e85
refactor: 상수와 유틸 함수를 별도 파일로 분리
meteorqz6 51a67cc
docs: README 업데이트
meteorqz6 a8fe31a
chore: add .coderabbit.yaml
meteorqz6 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,74 @@ | ||
| # 02. 목록 UI 구현하기: Props와 State | ||
|
|
||
| ## 🎯 요구 사항 | ||
| - `RestaurantList` 가 restaurants 배열을 받아서 그릴 수 있도록 변경해 보세요. | ||
| - restaurants 배열을 `RestaurantList` 의 props로 내려받도록 변경해 보세요. | ||
| - 카테고리 필터에 따라 필터된 음식점 목록을 보여줄 수 있도록 변경해 보세요. | ||
|
|
||
| ### 구현 결과 예시 | ||
| ```javascript | ||
| // App.jsx | ||
| <CategoryFilter category={category} onChangeCategory={setCategory} /> | ||
| <RestaurantList restaurants={filteredRestaurants} /> | ||
| ``` | ||
| ```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) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,102 +1,93 @@ | ||
| # Component 기본 구조와 JSX | ||
| # Props와 State | ||
|
|
||
| ## 🎯 개인 목표 및 목표 달성을 위한 행동 가이드 | ||
|
|
||
| 이번 미션을 통해 다음과 같은 학습 경험들을 쌓는 것을 목표로 한다. | ||
|
|
||
| 1. HTML 템플릿을 그대로 React 컴포넌트로 변환하는 과정을 직접 경험한다. | ||
| 2. 컴포넌트 단위로 UI를 나누는 기준을 스스로 정해본다. | ||
| 3. CSS Module을 사용해보고, 일반 CSS import 방식과의 차이를 체감한다. | ||
| 1. Props를 통해 부모 컴포넌트에서 자식 컴포넌트로 데이터를 전달하는 방식을 이해한다. | ||
| 2. State를 사용해 컴포넌트 내부의 동적 상태를 관리한다. | ||
| 3. Props와 State를 조합하여 부모-자식 간 양방향 데이터 흐름을 구현한다. | ||
|
|
||
| --- | ||
|
|
||
| ## 📝 기능 구현 목록 | ||
|
|
||
| - [x] Header 컴포넌트 — 타이틀, 음식점 추가 버튼 | ||
| - [x] CategoryFilter 컴포넌트 — 카테고리 드롭다운 필터 | ||
| - [x] RestaurantList 컴포넌트 — 음식점 목록 (6개) | ||
| - [x] RestaurantDetailModal 컴포넌트 — 음식점 상세 정보 모달 | ||
| - [x] AddRestaurantModal 컴포넌트 — 음식점 추가 폼 모달 | ||
| - [x] 각 컴포넌트별 CSS Module 분리 및 import | ||
| - [x] RestaurantList에 restaurants 배열을 props로 전달 | ||
| - [x] 배열 데이터를 map()으로 동적 렌더링 | ||
| - [x] 각 리스트 항목에 key prop 추가 | ||
| - [x] CategoryFilter에서 선택된 카테고리 상태 관리 | ||
| - [x] App 컴포넌트에서 카테고리별 필터링 로직 구현 | ||
| - [x] 필터된 데이터를 RestaurantList에 props로 전달 | ||
| - [x] 카테고리별 동적 이미지 매핑 | ||
|
|
||
| --- | ||
|
|
||
| ## 📚 학습 내용 | ||
|
|
||
| ### HTML 태그 | ||
| - `label`의 `for` 속성 | ||
|
|
||
| `for`에 지정한 `id`와 일치하는 입력 요소를 레이블과 연결한다. 연결되면 레이블 텍스트를 클릭했을 때 해당 입력 요소로 포커스가 이동하고, 스크린 리더가 입력 요소를 읽을 때 레이블 텍스트를 함께 읽어줘서 접근성이 향상된다. | ||
|
|
||
| - `select`의 `required` 속성 | ||
|
|
||
| 폼 제출 시 해당 `select`에서 값을 반드시 선택해야 한다는 유효성 검사 조건을 브라우저에 부여한다. 값이 선택되지 않은 채 제출을 시도하면 브라우저가 자동으로 경고 메시지를 표시하고 제출을 막는다. 이때 `<option value="">선택해 주세요</option>`처럼 `value`가 빈 문자열인 기본 옵션이 있어야 `required`가 정상적으로 동작한다. | ||
|
|
||
| ### JSX 변환 규칙 | ||
| - HTML의 `class`는 JSX에서 `className`으로 작성해야 한다. | ||
| - HTML의 `for`는 JSX에서 `htmlFor`로 작성해야 한다. | ||
| - `<img>`, `<input>` 등 닫히는 태그가 없는 요소는 self-closing tag로 작성해야 한다.(`<img />`) | ||
|
|
||
| ### CSS Module 사용법 | ||
| - `import styles from "./Component.module.css"` 형태로 객체로 가져온다. | ||
| - 클래스 적용 시 `className={styles.클래스명}` 형태로 사용한다. | ||
| - `App.css`처럼 전역 스타일은 `import "./App.css"` 로 import하면 앱 전체에 적용되고, 어느 컴포넌트에서든 문자열로 바로 쓸 수 있다. (`className="text-title"`) | ||
| - CSS Module 클래스와 전역 클래스를 함께 쓸 때는 템플릿 리터럴을 사용한다. | ||
|
|
||
| ```jsx | ||
| className={`${styles.name} text-title`} | ||
| ``` | ||
|
|
||
| ### CSS Module의 특징 | ||
| - 클래스명이 빌드 시 고유한 해시값으로 변환된다. (ex: `gnb__title` → `_gnb__title_abc12_1`) | ||
| - 덕분에 서로 다른 컴포넌트에서 동일한 클래스명을 사용해도 충돌하지 않는다. | ||
| - 일반 CSS import는 전역으로 적용되어 클래스명 충돌 위험이 있지만, CSS Module은 컴포넌트 단위로 스코프가 격리된다. | ||
|
|
||
| ### React 컴포넌트 기본 구조 | ||
| - 컴포넌트는 반드시 `return`이 있어야 화면에 렌더링된다. | ||
| - 이미지 asset은 `import`로 가져온 후 `src={변수명}` 형태로 사용한다. | ||
| ### 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]` | ||
|
|
||
| --- | ||
|
|
||
| ## 🤔 고민했던 문제와 해결 과정에서 배운 점 | ||
|
|
||
| ### CSS Module에서 하이픈이 포함된 클래스명 사용 | ||
| CSS 클래스명에 하이픈(-)이 포함된 경우 점 표기법(`styles.class-name`)으로 쓰면 JavaScript가 빼기 연산자로 해석해 오류가 난다. | ||
| ### Controlled Component의 필요성 | ||
| 처음엔 CategoryFilter가 자체 state를 관리했고 RestaurantList는 고정된 데이터를 표시했다. 부모인 App에서 카테고리 선택값을 알 수 없었다. | ||
|
|
||
| ```jsx | ||
| // 오류 | ||
| <div className={styles.restaurant-filter-container}> | ||
| **해결:** State를 부모 App으로 올렸다. CategoryFilter는 props로 `category`와 `onChangeCategory`를 받아 controlled component가 되었고, 필터링 로직은 App에서 처리하게 됐다. 이를 통해 부모-자식 간 데이터 흐름이 단방향으로 명확해졌다. | ||
|
|
||
| // 해결: 대괄호 표기법 사용 | ||
| <div className={styles["restaurant-filter-container"]}> | ||
| ``` | ||
| ### 동적 이미지 매핑의 필요성 | ||
| 처음엔 모든 음식점이 한식 이미지(`koreanImg`)만 표시됐다. 음식점 객체에 `category` 필드가 있는데도 활용하지 않고 있었다. | ||
|
|
||
| ### htmlFor에 클래스명을 함께 쓰는 실수 | ||
| `for` 속성에 클래스명을 붙여 쓰는 HTML 습관이 그대로 남아 오류가 발생했다. `htmlFor`는 연결할 input의 `id`만 받아야 하고, 클래스는 별도의 `className`으로 분리해야 한다. | ||
| **해결:** `CATEGORY_IMAGES` 객체를 만들어 카테고리별 이미지를 매핑했다. `src={CATEGORY_IMAGES[restaurant.category]}`로 각 음식점에 맞는 이미지가 동적으로 표시되도록 수정했다. | ||
|
|
||
| ```jsx | ||
| // 오류 | ||
| <label htmlFor="category text-caption"> | ||
| --- | ||
|
|
||
| // 해결 | ||
| <label htmlFor="category" className="text-caption"> | ||
| ``` | ||
| ## 🛠 리팩토링 | ||
|
|
||
| ### BEM 네이밍의 `--` 구분자 오타 | ||
| `form-item--required`처럼 BEM 수정자(modifier)는 `--`(더블 대시)를 사용한다. 싱글 대시(`-`)로 잘못 쓰면 CSS 스타일이 적용되지 않아 원인을 찾기 어렵다. | ||
| 1. Props와 State 책임 분리 | ||
|
|
||
| --- | ||
| - 이유: 초기 구현에서 각 컴포넌트가 자신의 state를 독립적으로 관리하고 있어, 부모 컴포넌트가 상태 변화를 알 수 없었다. 이로 인해 필터링이 제대로 작동하지 않았다. | ||
|
|
||
| ## 🛠 리팩토링 | ||
| - 개선: State를 부모 App 컴포넌트로 올렸다(state lifting). CategoryFilter는 선택된 카테고리를 props로 받아 표시만 하고, 변경 시 콜백 함수를 통해 부모에 알린다. 이제 App이 중앙에서 상태를 관리하고, RestaurantList에 필터된 데이터를 props로 전달한다. | ||
|
|
||
| 1. CSS 클래스 네이밍 BEM 원칙 적용 | ||
| 2. 동적 이미지 매핑으로 하드코딩 제거 | ||
|
|
||
| - 이유: 템플릿의 CSS를 그대로 복사해 사용하다 보니 `modal-backdrop`, `modal-container`처럼 블록과 엘리먼트 관계임에도 단순 하이픈(-)으로 연결된 클래스가 혼재했다. BEM 원칙상 엘리먼트는 `__`로 구분해야 하는데, 이를 지키지 않아 클래스 간의 계층 구조가 명확히 드러나지 않았다. | ||
| - 이유: 모든 음식점에 한식 이미지만 매핑되어 있었다. 각 음식점의 `category` 필드를 활용하지 않고 있었고, 새로운 카테고리 추가 시 컴포넌트 코드를 수정해야 했다. | ||
|
|
||
| - 개선: `modal-backdrop` → `modal__backdrop`, `modal-container` → `modal__container` 등 엘리먼트 관계에 해당하는 클래스를 `__`로 통일하였다. 그 결과 블록/엘리먼트는 점 표기법(`styles.modal__backdrop`), modifier만 대괄호 표기법(`styles["modal--open"]`)으로 쓰는 일관된 규칙이 생겼다. | ||
| - 개선: `CATEGORY_IMAGES` 객체를 만들어 카테고리와 이미지를 매핑했다. 이제 음식점 데이터의 `category`에 따라 자동으로 올바른 이미지가 표시된다. 새로운 카테고리를 추가할 때도 객체에만 항목을 추가하면 된다. | ||
|
|
||
| 2. CSS Module 클래스 접근 표기법 일관성 확보 | ||
| 3. 상수와 유틸 함수를 별도 파일로 분리 | ||
|
|
||
| - 이유: 일부 클래스는 `styles.gnb__title`처럼 점 표기법으로, 일부는 `styles["restaurant-filter-container"]`처럼 대괄호 표기법으로 혼용되어 코드의 일관성이 떨어졌다. 두 표기법이 섞이면 어떤 기준으로 선택해야 하는지 불명확하다. | ||
| - 이유: App.jsx에 RESTAURANTS 데이터와 filter 함수가 모두 포함되어 있어서 컴포넌트 로직과 비즈니스 로직이 섞여 있었다. 파일이 길어지고 가독성이 떨어졌다. | ||
|
|
||
| - 개선: BEM 리팩토링을 통해 클래스명에서 단순 하이픈을 제거하여, block과 element는 항상 점 표기법, modifier(`--`)는 JS에서 감소 연산자로 해석되므로 반드시 대괄호 표기법을 사용한다는 명확한 기준을 만들었다. | ||
| - 개선: | ||
| - `src/constants/restaurants.js` — RESTAURANTS 배열 분리 | ||
| - `src/constants/categoryImages.js` — CATEGORY_IMAGES 매핑 객체 분리 | ||
| - `src/utils/filterRestaurants.js` — filterRestaurants 함수 분리 (함수명, 함수 형태 수정) | ||
|
|
||
| App.jsx는 이제 필요한 상수와 함수를 import해서 사용하므로 역할이 명확해졌다. 또한 각 모듈이 독립적이므로 재사용성이 높아졌고, 테스트하거나 수정할 때 해당 파일만 건드리면 된다. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[제안]
핸들러를 먼저 만들어서 카테고리 필터에 넘겨주셨네요.
전 요구사항을 그대로 반영해서 이런 방식을 생각해보지 못했는데, 핸들러가 어떤 파일에 있는 것이 더 자연스러운지 스터디 때 이야기해보면 좋을 것 같아요!