From d91aaae7cb850a86ecc8cb6fb35825621a9d8a60 Mon Sep 17 00:00:00 2001 From: Nam Yooseong Date: Tue, 2 Jun 2026 22:35:29 +0900 Subject: [PATCH 1/6] =?UTF-8?q?docs:=202=EB=8B=A8=EA=B3=84=20=EB=AF=B8?= =?UTF-8?q?=EC=85=98=20=EC=9A=94=EA=B5=AC=EC=82=AC=ED=95=AD=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 --- 02-rendering-lists/README.md | 74 ++++++++++++++++++++++++++++++++++++ 1 file changed, 74 insertions(+) create mode 100644 02-rendering-lists/README.md diff --git a/02-rendering-lists/README.md b/02-rendering-lists/README.md new file mode 100644 index 0000000..3fcfcf1 --- /dev/null +++ b/02-rendering-lists/README.md @@ -0,0 +1,74 @@ +# 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) \ No newline at end of file From 540e7c101a72e064f52abf3472d6f9016b573bfc Mon Sep 17 00:00:00 2001 From: Nam Yooseong Date: Tue, 2 Jun 2026 22:36:00 +0900 Subject: [PATCH 2/6] =?UTF-8?q?feat:=20=EC=9D=8C=EC=8B=9D=EC=A0=90=20?= =?UTF-8?q?=EB=AA=A9=EB=A1=9D=20=ED=95=84=ED=84=B0=EB=A7=81=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/App.jsx | 69 +++++++++- .../CategoryFilter/CategoryFilter.jsx | 4 +- .../RestaurantList/RestaurantList.jsx | 121 +++++------------- 3 files changed, 97 insertions(+), 97 deletions(-) diff --git a/src/App.jsx b/src/App.jsx index c6c48d5..92cb1c8 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -2,21 +2,78 @@ import "./App.css"; import Header from "./components/Header/Header.jsx"; import CategoryFilter from "./components/CategoryFilter/CategoryFilter.jsx"; import RestaurantList from "./components/RestaurantList/RestaurantList.jsx"; -import RestaurantDetailModal from "./components/RestaurantDetailModal/RestaurantDetailModal.jsx"; -import AddRestaurantModal from "./components/AddRestaurantModal/AddRestaurantModal.jsx"; +import { useState } from "react"; +// import RestaurantDetailModal from "./components/RestaurantDetailModal/RestaurantDetailModal.jsx"; +// import AddRestaurantModal from "./components/AddRestaurantModal/AddRestaurantModal.jsx"; + +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: "기타", + }, +]; + +function filter(category) { + if (category === "전체") return RESTAURANTS; + else { + return RESTAURANTS.filter((restaurant) => restaurant.category === category); + } +} function App() { + const [category, setCategory] = useState("전체"); + const filteredRestaurants = filter(category); + + function handleChange(e) { + setCategory(e.target.value); + } + return ( <>
- - + +
- */} ); } diff --git a/src/components/CategoryFilter/CategoryFilter.jsx b/src/components/CategoryFilter/CategoryFilter.jsx index 9cf8219..c2789c5 100644 --- a/src/components/CategoryFilter/CategoryFilter.jsx +++ b/src/components/CategoryFilter/CategoryFilter.jsx @@ -1,6 +1,6 @@ import styles from "./CategoryFilter.module.css"; -export default function CategoryFilter() { +export default function CategoryFilter({ category, onChangeCategory }) { return (
` 등 닫히는 태그가 없는 요소는 self-closing tag로 작성해야 한다.(``) - -### 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가 빼기 연산자로 해석해 오류가 난다. - -```jsx -// 오류 -
- -// 해결: 대괄호 표기법 사용 -
-``` - -### htmlFor에 클래스명을 함께 쓰는 실수 -`for` 속성에 클래스명을 붙여 쓰는 HTML 습관이 그대로 남아 오류가 발생했다. `htmlFor`는 연결할 input의 `id`만 받아야 하고, 클래스는 별도의 `className`으로 분리해야 한다. +### Controlled Component의 필요성 +처음엔 CategoryFilter가 자체 state를 관리했고 RestaurantList는 고정된 데이터를 표시했다. 부모인 App에서 카테고리 선택값을 알 수 없었다. -```jsx -// 오류 -