Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions .coderabbit.yaml
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
38 changes: 38 additions & 0 deletions 04-form/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# 04. 폼 UI 구현하기: controlled vs uncontrolled

## 🎯 요구 사항
- `Header`의 레스토랑 추가 버튼을 클릭하면 레스토랑 추가 폼이 모달로 뜨도록 구현해 주세요
- 이전 단계에서 만들어두었던 `AddRestaurantModal`을 그대로 사용합니다.
- 카테고리를 선택하고, `<input/>`, `<textarea/>`에 값을 입력한 뒤 '추가하기' 버튼을 클릭하면 레스토랑 목록에 추가되도록 구현해 주세요.
- 유효성 검사는 하지 않습니다. 아주 간단하게 입력값을 처리해보는 구현만 해도 충분합니다.
- id는 `Date.now()`값을 임의로 할당합니다.
- (optional) 재사용할 수 있는 Modal 컴포넌트를 만들어서 `AddRestaurantModal`, `RestaurantDetailModal`을 Modal 컴포넌트를 활용해 구현하는 것으로 개선해 보세요

## ✅ 키워드
- controlled vs uncontrolled
- children props

> 재사용할 수 있는 모달을 만들 때 `children`을 활용해 보세요. 아래와 같은 식으로 UI를 구성할 수 있습니다.

```javascript
// 설명을 위한 예시용 마크업입니다. 실제로 사용하는 마크업은 template/ 하위의 html을 참고하거나 직접 원하는대로 구현하여 사용해 주세요.
// 반드시 아래와 같은 형식으로 쓸 필요는 없습니다. 원하는 방식대로 재사용 가능한 <Modal/> 컴포넌트를 만들어 보세요.

// AddRestaurantModal.jsx
<Modal title="새로운 음식점" onClose={onClose}>
<form></form>
</Modal>

// RestaurantDetailModal.jsx
<Modal title={restaurant.name} onClose={onClose}>
<div className="restaurant-info"></div>
<div className="button-container"></div>
</Modal>
```

## 🧙‍♀️ 진행 가이드
- 진행 시간: (optional 제외) 2시간 내에 완료하는 것을 목표로 합니다.

## 🔗 참고 문서
- [Sharing State Between Components](https://react.dev/learn/sharing-state-between-components)
- [API Reference: <input>](https://react.dev/reference/react-dom/components/input)
189 changes: 120 additions & 69 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,130 +1,181 @@
# 조건부 렌더링 활용
# 재사용 가능한 컴포넌트 설계

## 🎯 개인 목표 및 목표 달성을 위한 행동 가이드

이번 미션을 통해 다음과 같은 학습 경험들을 쌓는 것을 목표로 한다.

1. 이벤트 핸들러를 통해 사용자 인터랙션에 반응하는 방법을 이해한다.
2. 조건부 렌더링(`&&`)을 활용해 상황에 따라 컴포넌트를 보여주고 숨긴다.
3. 어떤 값을 state로 선언해야 하는지 기준을 세운다.
1. controlled 컴포넌트와 uncontrolled 컴포넌트의 차이를 이해하고, 각각 어떤 상황에서 선택하는지 기준을 세운다.
2. 어떤 state를 어느 컴포넌트가 소유해야 하는지 판단하고, 필요한 경우 공통 부모로 끌어올리는 패턴(Lifting State Up)을 경험한다.
3. `children` props를 활용해 재사용 가능한 Modal 컴포넌트를 설계하는 방법을 익힌다.

---

## 📝 기능 구현 목록

- [x] 음식점 아이템 클릭 시 모달 열기
- [x] 닫기 버튼 또는 backdrop 클릭 시 모달 닫기
- [x] 클릭한 음식점 정보를 모달에 전달하여 표시
- [x] Header의 음식점 추가 버튼 클릭 시 AddRestaurantModal 열기
- [x] 추가하기 버튼 클릭 시 음식점 목록에 항목 추가
- [x] 재사용 가능한 Modal 컴포넌트로 AddRestaurantModal, RestaurantDetailModal 개선

---

## 📚 학습 내용

### 이벤트 핸들러와 데이터 전달
### Controlled vs Uncontrolled Component

이벤트 핸들러에 함수를 직접 연결하면 React가 이벤트 객체(`e`)를 자동으로 넘겨준다. 클릭된 아이템의 데이터를 함께 전달하려면 화살표 함수로 한 번 감싸야 한다.
컴포넌트의 중요한 정보가 **props**에 의해 결정되면 controlled, **지역 state**로 자체 관리되면 uncontrolled이다.

```jsx
// 이벤트 객체(e)만 전달됨
onClick={onRestaurantClick}
| | Controlled | Uncontrolled |
|---|---|---|
| 정보 출처 | props (부모가 제공) | 지역 state (자체 관리) |
| 부모의 영향 | 동작을 완전히 지정 가능 | 영향을 줄 수 없음 |
| 유연성 | 여러 컴포넌트와 조정 용이 | 독립적이지만 협력 어려움 |
| 사용 난이도 | 부모에서 props 설정 필요 | 설정이 적어 사용하기 쉬움 |

// 클릭된 restaurant 데이터를 직접 전달
onClick={() => onRestaurantClick(restaurant)}
```
**언제 선택하나?**
- **Controlled** — 부모 컴포넌트와 state를 공유하거나, 여러 컴포넌트의 동작을 함께 조정해야 할 때
- **Uncontrolled** — 부모와 상태를 공유할 필요 없이 독립적으로 동작해도 될 때

`() => onRestaurantClick(restaurant)`는 `map` 순회 중인 `restaurant`를 부모까지 전달하는 역할을 한다.
**이 미션에서의 선택:**

### 조건부 렌더링
controlled/uncontrolled는 컴포넌트 전체에 붙이는 레이블이 아니라, 어떤 정보가 어디서 관리되는지를 기준으로 판단한다. AddRestaurantModal은 두 가지가 혼재한다.

`{condition && <Component />}` 패턴으로 조건이 참일 때만 컴포넌트를 렌더링한다.
- **폼 데이터** (category, name, description) → 지역 state로 자체 관리 → **uncontrolled**
- **모달 열림/닫힘** → App이 소유 → **controlled**
Comment on lines +42 to +43

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

controlled/uncontrolled 용어 정의가 반대로 서술되어 있습니다.

Line 42-43에서 category, name, description을 “지역 state로 관리하므로 uncontrolled”라고 적었는데, React 기준으로는 입력값을 state로 바인딩해 관리하면 controlled input입니다. state 소유 위치(부모/자식)와 controlled 여부를 분리해서 서술하는 쪽이 정확합니다.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@README.md` around lines 42 - 43, Update the README wording to correct the
controlled/uncontrolled terminology: change the line about "폼 데이터 (category,
name, description) → 지역 state로 자체 관리 → uncontrolled" to state that binding
inputs to local React state (category, name, description) makes them controlled
inputs, and clarify separately that "모달 열림/닫힘 → App이 소유" refers to which
component owns the open/close state (parent-owned) not the controlledness of the
inputs; also add a short note that uncontrolled inputs are those managed by the
DOM (refs) rather than React state to avoid confusion.


```jsx
{clickedRestaurant && (
<RestaurantDetailModal restaurant={clickedRestaurant} onClose={handleModalClose} />
)}
```
모달 열림 상태를 App이 소유하는 이유는 트리거(Header의 추가 버튼)와 모달이 형제 관계이기 때문이다. 형제끼리는 서로의 state에 접근할 수 없으므로 공통 부모인 App으로 state를 끌어올렸다(Lifting State Up).

`onSubmit`, `onClose`는 state가 아닌 콜백이다. "어떤 값을 누가 소유하냐"의 문제가 아니라 이벤트를 부모로 전달하는 통로이므로 controlled/uncontrolled 구분과는 별개다.

### State로 선언할 것과 아닌 것
### form submit 기본 동작과 e.preventDefault()

**State가 필요한 경우**: 시간이 지나면서 변하고, 렌더링에 영향을 주며, 다른 state나 props로부터 계산할 수 없는 값
`<form onSubmit={handler}>`에서 submit 이벤트가 발생하면 브라우저는 기본적으로 페이지를 새로고침한다. React SPA에서는 이 기본 동작을 막아야 state 업데이트가 유지된다.

**State가 불필요한 경우**: 기존 state나 props로부터 계산 가능한 파생값(derived value)
리팩토링 후 `e.preventDefault()`는 폼을 소유한 AddRestaurantModal 내부에 있다. 완성된 데이터만 `onSubmit`을 통해 App으로 전달한다.

```jsx
// category state에서 계산 가능 → state 불필요
const filteredRestaurants = filterRestaurants(RESTAURANTS, category);
// AddRestaurantModal.jsx
function handleFormSubmit(e) {
e.preventDefault(); // 없으면 setRestaurants 실행 직후 페이지 리로드로 state 초기화
onSubmit({ category, name, description });
}

// clickedRestaurant state에서 계산 가능 → state 불필요
const isRestaurantDetailModalOpen = !!clickedRestaurant;
// App.jsx — 이벤트 객체가 아닌 데이터 객체를 받음
function handleRestaurantSubmit({ category, name, description }) {
setRestaurants([...restaurants, { id: Date.now(), category, name, description }]);
}
```

파생 변수는 state와 달리 setter가 없어 동기화 버그가 생기지 않고, 렌더링마다 자동으로 최신값을 계산한다.
### 폼 state를 부모로 끌어올리기 (Lifting State Up)

### `&&` 조건부 렌더링 주의사항
초기 구현에서는 `restaurants` state가 `App`에 있어서 폼 state도 `App`으로 끌어올려 props로 전달했다. 그러나 App이 실제로 필요한 것은 추가 완료 시점의 최종 데이터뿐이므로, 리팩토링을 통해 폼 state를 `AddRestaurantModal` 안으로 내리고 완성된 데이터만 부모로 전달하도록 변경했다. (자세한 내용은 리팩토링 섹션 참고)

`&&` 앞에 숫자나 빈 문자열 같은 falsy 값이 오면, `false`로 평가되지 않고 값 자체가 화면에 출력된다.
### 추가 후 상태 초기화

```jsx
// ⚠️ count가 0이면 "0"이 화면에 렌더링됨
{count && <Modal />}
음식점을 추가한 뒤 폼 입력값을 초기화하지 않으면 모달을 다시 열었을 때 이전 값이 남아있다.

초기 구현에서는 폼 state가 App에 있었기 때문에 추가 완료 시점에 명시적으로 초기화했다.

// ✅ 명시적으로 불리언으로 변환
{!!count && <Modal />}
{count > 0 && <Modal />}
```jsx
setIsAddRestaurantModalOpen(false);
setCategory("");
setName("");
setDescription("");
```

이 미션에서 `clickedRestaurant`는 객체 또는 `null`만 들어오므로 문제없지만, 숫자나 문자열을 조건으로 쓸 때는 주의해야 한다. `!!`를 사용하는 이유 중 하나이기도 하다.
리팩토링 후에는 폼 state를 AddRestaurantModal 내부로 내렸기 때문에 명시적 초기화가 불필요해졌다. `isAddRestaurantModalOpen`이 `false`가 되면 AddRestaurantModal이 언마운트되고, 다시 열릴 때 새로 마운트되면서 `useState("")`의 초기값으로 자동 초기화된다.

---

## 🤔 고민했던 문제와 해결 과정에서 배운 점

### 어떤 state를 어느 컴포넌트가 소유해야 하는가

음식점 추가 기능을 구현하면서 다음 state들이 필요하다고 생각했다.

- `restaurants` — 음식점 목록
- `category`, `name`, `description` — 폼 입력값
- `isAddRestaurantModalOpen` - 모달 열림/닫힘

이 state들을 어느 컴포넌트에 선언할지 결정하기 위해 "이 state를 누가 필요로 하는가"를 기준으로 판단했다.

### Lifting State Up
`restaurants`는 AddRestaurantModal(추가 시 갱신)과 RestaurantList(목록 렌더링) 모두 필요하다. 두 컴포넌트는 형제 관계라 서로의 state에 직접 접근할 수 없으므로, 공통 부모인 App으로 끌어올렸다(Lifting State Up).

클릭된 음식점 정보를 `RestaurantList`와 `RestaurantDetailModal` 두 컴포넌트가 공유해야 하므로, 공통 부모인 `App`에서 state를 관리한다.
`category`, `name`, `description`은 폼을 입력하는 동안 AddRestaurantModal 안에서만 쓰인다. 추가 완료 시점에 최종 데이터만 부모로 전달하면 되므로, 굳이 App까지 올릴 필요가 없다. AddRestaurantModal이 직접 소유한다.

### `!!` 이중 부정 연산자
`isAddRestaurantModalOpen`도 같은 기준으로 판단했다. 모달을 여는 트리거는 Header의 추가 버튼이고, 실제로 열리는 것은 AddRestaurantModal이다. 두 컴포넌트는 형제 관계라 서로의 state에 접근할 수 없으므로 공통 부모인 App이 소유해야 한다.

자바스크립트에서 값의 truthy/falsy 평가 결과를 명시적으로 불리언 값으로 변환하기 위해 사용된다. 주로 조건식의 결과를 일관된 불리언 타입으로 정규화할 때 활용된다.
### 식당을 추가해도 목록이 업데이트되지 않는 문제

`handleSubmit`에서 `setRestaurants`가 호출되는데도 목록이 바뀌지 않아서 원인을 찾아봤다. `e.preventDefault()`가 없어서 submit 시 페이지가 새로고침되고, state 업데이트가 반영되기 전에 초기 상태로 되돌아가는 것이었다. state 업데이트 자체는 올바르게 작성되어 있었지만 브라우저 기본 동작을 막지 않아서 생긴 문제였다.

### setRestaurants 인자 오류

처음에 `setRestaurants(...restaurants, { ... })`로 작성했다. 이렇게 하면 배열이 아닌 여러 인자를 `setRestaurants`에 넘기는 것이라 새 배열이 만들어지지 않는다. 배열 리터럴 안에서 스프레드해야 한다.

```jsx
!!null // → false (모달 닫힌 상태)
!!{ id: 1 } // → true (모달 열린 상태)
// ❌ 인자를 여러 개 전달하는 것
setRestaurants(...restaurants, { id: Date.now(), ... });

// ✅ 기존 항목을 펼쳐서 새 배열 생성
setRestaurants([...restaurants, { id: Date.now(), ... }]);
```

---

## 🤔 고민했던 문제와 해결 과정에서 배운 점

### 무엇을 state로 선언할지

가장 고민이 된 부분은 `restaurant`를 state로 만들어야 하는가였다. `filteredRestaurants`로 계산할 수 있을 것 같아 state가 불필요하다고 생각했는데, 실제로는 두 값의 역할이 다르다.
## 🛠 리팩토링

- `filteredRestaurants` — 화면에 보여줄 **목록** → `category`에서 파생, state 불필요
- `clickedRestaurant` — 모달에 보여줄 **선택된 하나** → 클릭 전까지 알 수 없으므로 state 필요
### 이벤트 핸들러 네이밍 규칙

**기준**: 다른 값으로부터 계산할 수 없다면 state, 계산할 수 있다면 파생 변수
`handle` + `[대상]` + `[동작]` 패턴을 사용한다. 대상은 항상 붙인다.

### isModalOpen과 clickedRestaurant를 따로 두면 생기는 문제
```jsx
handleFormSubmit // Form + Submit
handleCategoryChange // Category + Change
handleDetailModalClose // DetailModal + Close
```

처음엔 `isModalOpen` boolean과 `clickedRestaurant` 두 state를 동시에 관리하려 했다. 이 경우 둘을 항상 함께 업데이트해야 하는데, 하나라도 빠지면 모달은 열려 있지만 `clickedRestaurant`가 `null`인 상황이 발생해 런타임 에러가 난다.
props로 넘길 때는 `on-`으로 통일한다. `on-`은 인터페이스(계약), `handle-`은 구현이다.

```jsx
function handleRestaurantClick(restaurant) {
setClickedRestaurant(restaurant);
setIsModalOpen(true); // 둘 중 하나라도 빠지면 버그
}
// 정의는 handle-
function handleNameChange(e) { ... }

// props로 넘길 때는 on-
<AddRestaurantModal onNameChange={handleNameChange} />
```

**해결:** `clickedRestaurant` 하나로 통합. `null`이면 닫힌 상태, 값이 있으면 열린 상태다. `isRestaurantDetailModalOpen`은 이를 기반으로 계산한 파생 변수로 가독성을 확보했다.
### 폼 state를 자식으로 내리기

처음 구현에서는 폼 입력값(category, name, description)을 App에서 관리했다. `restaurants` state가 App에 있어서 폼 데이터도 App까지 올려야 한다고 생각했기 때문이다. 결과적으로 AddRestaurantModal에 props가 8개가 됐다.

그러나 App이 실제로 필요한 것은 **추가 완료 시점의 최종 데이터**뿐이다. 폼을 채우는 중간 과정의 입력값은 App이 알 필요가 없다. 폼 state를 AddRestaurantModal 안으로 내리고, 완료 시에만 부모로 전달하도록 변경했다.

```jsx
const [clickedRestaurant, setClickedRestaurant] = useState(null);
const isRestaurantDetailModalOpen = !!clickedRestaurant; // 파생 변수
// Before: App이 중간 입력값까지 관리
<AddRestaurantModal
category={category}
name={name}
description={description}
onCategoryChange={handleCategoryChange}
onNameChange={handleNameChange}
onDescriptionChange={handleDescriptionChange}
onSubmit={handleSubmit}
onClose={handleAddRestaurantModalClose}
/>

// After: 완성된 데이터만 부모로 전달
<AddRestaurantModal
onSubmit={handleRestaurantSubmit} // { category, name, description } 객체를 받음
onClose={handleAddRestaurantModalClose}
/>
```

---
props가 8개 → 2개로 줄었고, 폼 내부 관심사가 AddRestaurantModal 안에 캡슐화됐다.

## 🛠 리팩토링
### Modal 래퍼 div 제거

`display: none` / `display: block` 패턴은 템플릿(`templates/style.css`)에서 그대로 가져온 것이다. 템플릿은 순수 HTML/CSS 기반으로, 모달이 DOM에 항상 존재하면서 `.modal--open` 클래스를 붙이고 떼는 방식으로 보이고 숨겼다.

State 설계를 단계적으로 개선하며 사고 과정을 커밋으로 기록했다.
React로 전환하면서 조건부 렌더링(`{isOpen && <Modal />}`)을 사용하게 되면서 CSS 토글이 불필요해졌다. 템플릿의 구조를 그대로 쓰면서 생긴 불필요한 패턴을 뒤늦게 제거했다.

1. **Stage 1** — `isModalOpen` boolean state로 모달 열고 닫기만 구현 (restaurant 데이터 없음)
2. **Stage 2** — `isModalOpen` + `clickedRestaurant` 두 state로 실제 데이터 전달 (동기화 문제 내포)
3. **Stage 3** — `clickedRestaurant` 단일 state로 통합, `isRestaurantDetailModalOpen`을 파생 변수로 개선
래퍼 `<div>`도 함께 제거했다. backdrop과 container가 둘 다 `position: fixed`라 부모 요소의 레이아웃에 영향을 받지 않아 래퍼가 없어도 동작이 동일하다. 불필요한 DOM 노드를 줄이기 위해 Fragment로 교체했다.
51 changes: 43 additions & 8 deletions src/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,30 +6,59 @@ 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";

function App() {
const [category, setCategory] = useState("전체");
const [filterCategory, setFilterCategory] = useState("전체");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[배움]
저는 category로 선언했는데, filterCategory로 쓰면 필터용 카테고리라는 게 바로 읽히네요. 폼의 category랑 혼동될 여지도 없고 더 명확한 것 같아요.

const [clickedRestaurant, setClickedRestaurant] = useState(null);
const [isAddRestaurantModalOpen, setIsAddRestaurantModalOpen] =
useState(false);
const [restaurants, setRestaurants] = useState(RESTAURANTS);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[논의]
저는 RESTAURANTS 상수와 구분하려고 newRestaurants로 이름 지었는데, 상수는 대문자라 소문자 restaurants만으로도 충분히 구분되는 것 같더라고요. 어느 쪽이 더 자연스러운지 얘기해보면 좋을 것 같아요!


const isRestaurantDetailModalOpen = !!clickedRestaurant;
const filteredRestaurants = filterRestaurants(RESTAURANTS, category);
const filteredRestaurants = filterRestaurants(restaurants, filterCategory);

function handleChange(e) {
setCategory(e.target.value);
function handleFilterCategoryChange(e) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[배움]
handle + 대상 + 동작, propson-으로 통일하는 규칙을 쓰셨는데, 규칙이 있는 점이 가독성 면에서도 좋은 것 같아요!요?

setFilterCategory(e.target.value);
}

function handleRestaurantClick(restaurant) {
setClickedRestaurant(restaurant);
}

function handleModalClose() {
function handleDetailModalClose() {
setClickedRestaurant(null);
}

function handleAddButtonClick() {
setIsAddRestaurantModalOpen(true);
}
Comment on lines +33 to +35

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

상세 모달과 추가 모달이 동시에 열린 상태가 될 수 있습니다.

clickedRestaurantisAddRestaurantModalOpen가 독립이라, 상세 모달이 열린 상태에서도 추가 모달을 열 수 있습니다. 추가 모달 오픈 시 상세 모달 상태를 먼저 닫아 모달을 상호배타적으로 유지해 주세요.

🔧 제안 diff
   function handleAddButtonClick() {
+    setClickedRestaurant(null);
     setIsAddRestaurantModalOpen(true);
   }

Also applies to: 68-79

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/App.jsx` around lines 33 - 35, Ensure the add modal and detail modal are
mutually exclusive: update handleAddButtonClick to clear any open detail modal
(call setClickedRestaurant(null)) before opening the add modal
(setIsAddRestaurantModalOpen(true)), and likewise wherever you set
clickedRestaurant (the detail-opening logic) ensure you close the add modal
first by calling setIsAddRestaurantModalOpen(false) before setting
clickedRestaurant; reference functions/variables: handleAddButtonClick,
setIsAddRestaurantModalOpen, clickedRestaurant, setClickedRestaurant.


function handleAddRestaurantModalClose() {
setIsAddRestaurantModalOpen(false);
}

function handleRestaurantSubmit({ category, name, description }) {
setRestaurants((prev) => [
...prev,
{
id: Date.now(),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[배움]
저는 id 생성을 AddRestaurantModal에서 했는데, id는 음식점 데이터의 일부고 실제로 데이터를 관리하는 곳은 App이니까 생성 책임도 App에 있는 게 더 자연스러운 것 같아요. 저도 옮겨보겠습니다!

[제안]
Date.now()는 밀리초 단위라 짧은 시간 안에 두 번 호출되면 같은 값이 나올 수 있다고 해요. crypto.randomUUID()로 바꾸면 충돌 없는 고유 id를 보장할 수 있어요. 적용해보는 건 어떠신가요?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

제안 감사합니다. Date.now()를 사용하는 것보다 cryto.randomUUID()를 사용하는 것이 더 적절할 것 같네요!

category,
name,
description,
},
]);
setIsAddRestaurantModalOpen(false);
}

return (
<>
<Header />
<Header onAddButtonClick={handleAddButtonClick} />
<main>
<CategoryFilter category={category} onChangeCategory={handleChange} />
<CategoryFilter
category={filterCategory}
onCategoryChange={handleFilterCategoryChange}
/>
<RestaurantList
restaurants={filteredRestaurants}
onRestaurantClick={handleRestaurantClick}
Expand All @@ -39,7 +68,13 @@ function App() {
{isRestaurantDetailModalOpen && (
<RestaurantDetailModal
restaurant={clickedRestaurant}
onClose={handleModalClose}
onClose={handleDetailModalClose}
/>
)}
{isAddRestaurantModalOpen && (
<AddRestaurantModal
onSubmit={handleRestaurantSubmit}
onClose={handleAddRestaurantModalClose}
/>
)}
</aside>
Expand Down
Loading