[4,5단계] 천동현 미션 제출합니다#96
Conversation
Co-authored-by: Copilot <copilot@github.com>
Co-authored-by: Copilot <copilot@github.com>
Co-authored-by: Copilot <copilot@github.com>
There was a problem hiding this comment.
Code Review
This pull request implements a restaurant management application with listing, filtering, and modal features, and adds Prettier configuration. Feedback addressed missing backdrop click functionality, keyboard accessibility, and the requirement to re-fetch data after additions. Minor typo and style fixes were also suggested.
|
|
||
| return ( | ||
| <div className="modal modal--open"> | ||
| <div className="modal-backdrop"></div> |
There was a problem hiding this comment.
이 부분 추가해주세요. 현재 추가하기 모달 뒤의 backdrop을 클릭하면 모달이 닫히지 않고 있어요.
There was a problem hiding this comment.
Pull request overview
This PR advances the self-paced React exercise from a static template into a small state-driven React app, aligning the project with the later mission steps around component composition, modal control, filtering, and API effects.
Changes:
- Splits the lunch app UI into React components (
Header,CategoryFilter,RestaurantList, detail/add modals) managed fromApp. - Adds category-based filtering, restaurant detail viewing, and restaurant creation flows.
- Connects the app to the local
json-serverAPI, imports the shared styling/assets into the React app, and adds Prettier configuration.
Reviewed changes
Copilot reviewed 23 out of 31 changed files in this pull request and generated 10 comments.
Show a summary per file
| File | Description |
|---|---|
| vite.config.js | Reformats Vite config. |
| templates/style.css | Reformats the reference template stylesheet. |
| templates/index.html | Reformats the reference HTML template. |
| src/RestaurantList.jsx | Adds restaurant list rendering and click handling. |
| src/RestaurantDetailModal.jsx | Adds the restaurant detail modal component. |
| src/main.jsx | Reformats the React entry point. |
| src/Header.jsx | Adds the header and add button trigger. |
| src/CategoryFilter.jsx | Adds the controlled category filter. |
| src/App.jsx | Centralizes state, API fetching, filtering, and modal orchestration. |
| src/App.css | Ports the template styles into the React app. |
| src/AddRestaurantModal.jsx | Adds the restaurant creation form modal. |
| README.md | Reformats the root guide. |
| public/category-western.png | Adds western category icon asset. |
| public/category-korean.png | Adds Korean category icon asset. |
| public/category-japanese.png | Adds Japanese category icon asset. |
| public/category-etc.png | Adds etc category icon asset. |
| public/category-chinese.png | Adds Chinese category icon asset. |
| public/category-asian.png | Adds Asian category icon asset. |
| public/add-button.png | Adds add-button asset. |
| package.json | Adds Prettier and keeps app scripts/dependencies updated. |
| index.html | Reformats the Vite HTML shell. |
| 06-references/README.md | Reformats the references guide. |
| 05-effects/README.md | Reformats the effects/API mission guide. |
| 04-form/README.md | Reformats the form mission guide. |
| 03-modal/README.md | Reformats the modal mission guide. |
| 02-rendering-lists/README.md | Reformats the list-rendering mission guide. |
| 01-first-component/README.md | Reformats the component basics guide. |
| 00-introduction/README.md | Reformats the introduction material. |
| .prettierrc | Adds repository Prettier rules. |
| .eslintrc.cjs | Reformats ESLint config. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
There was a problem hiding this comment.
안녕하세요 동현님~ 반갑습니다. 리뷰어로서는 처음 뵙는 것 같네요. 잘 부탁드립니다 🙇🏻 거두절미 하고 바로 코드 리뷰로 가볼게요. 코드의 작성에 대한 것들은 코드 블럭에 적어두었습니다!
좋았던 점
-
REST API 규약 준수
POST 요청에서 Content-Type 헤더와 JSON.stringify를 빠짐없이 챙긴 부분이 좋았어요👍🏻 -
함수형 업데이트 패턴 사용
setRestaurants(prevRestaurants => [...prevRestaurants, createdRestaurant]);
setRestaurants(restaurants => ...) 대신 prevRestaurants를 인자로 받는 함수형 업데이트를 사용하신게 좋았어요. 비동기 상황에서 최신 상태를 항상 보장받을 수 있는 올바른 패턴입니다. 👍🏻
개선하면 좋을 점
- PR Text에 스터디 요구사항으로 주어진 컴포넌트 구조도와 실행 영상,사진이 없어서 아쉬웠어요. 요구사항이니 꼭 챙겨주시고 만약 실수로 누락되었다면 반드시 첨부해주세요. PR을 쓰는 궁극적인 이유는 "타인이 내 코드를 쉽게 보게함"에 있고 그림이 있다면 금상첨화입니다.
- 파일들이
/src안에 그냥 배열되어있어요. 지금은 아주 작은 단위의 프로젝트라 상관없을지 모르지만 프로젝트가 커지고 관리해야할 파일들이 많아지면 폴더를 통해 역할을 분리하는 것이 좋아보입니다. 이런걸 디자인 패턴이라고 부르는데 제가 참고했었던 블로그 링크를 드릴게요
useEffect에 대하여
useEffect가 처음 다루다보면 감을 잡기 어려운 것에 대하여 완전히 공감합니다. 저도 그랬거든요 😵💫 제가 이해하고 있는 방식을 공유드리며 티키타카를 기대하면 좋을 것 같네요.
React 컴포넌트의 역할은 동현님도 알고 계시다시피 "상태를 받아 화면에 그리는 것"이에요. 그런데 React는 Frontend 기술이기에 화면을 그리는 것 이외의 데이터를 불러오는 것과 같은 별개로 해야할 작업들이 많습니다. JSX로 이루어진 컴포넌트를 도안라고 비유하면 데이터를 불러오는 건 물감, useEffect는 물감을 가게에서 가져오는 것이라고 비유해볼 수도 있을 것 같네요!
이 별개로 해야할 작업들에는 API 호출(데이터), timer, event listener가 있는데 이런것들을 다 합쳐서 Side Effect라고 불러요. useEffect는 이것들을 처리하는 공간입니다.
직접 작성하신 코드를 예를 들어 설명을 마저 해볼게요.
useEffect(() => {
fetChRestaurants(); // 렌더링과 무관하게 "서버에서 데이터를 가져오는 작업"
}, []);이건 이렇게 표현할 수 있는거죠.
"컴포넌트가 화면에 나타난 뒤에 서버에서 음식점 목록을 가져와서 상태를 업데이트해줘"
컴포넌트 화면 렌더링 -> 서버에서 음식점 목록 챙겨와 -> 상태 업데이트해
이런 흐름을 가지고 있습니다. 그럼 이제 의문이 들어요.
Q. useEffect는 해당 컴포넌트가 화면에 렌더링될때 한번만 실행되는건가?
A: 아닙니다. useEffect가 실행되어야하는 트리거를 "의존성 배열"이 관리하고 있는거에요.
| 배열 | 실행 시점 |
|---|---|
| [] (빈 배열) | 컴포넌트가 처음 마운트될 때 딱 한 번 |
| [category] | 마운트 + category 값이 바뀔 때마다 |
| 배열 없음 | 렌더링할 때마다 매번 |
만약, 요구사항이 카테고리별로 서버에서 필터링된 데이터를 가져와라! 였다면 이렇게 바뀌겠죠?
useEffect(() => {
fetchRestaurantsByCategory(category);
}, [category]); // category가 바뀔 때마다 다시 fetch추가로 제미나이와 코파일럿이 찝어준 부분들에 대해서도 생각해보시면 좋을 것 같아요. 제가 말해야하는 내용을 그대로 말하고 있네요 😆 혹시 이야기를 더 나눠보고 싶은 것들이 있으시다면 코드 리팩토링 후에 적어주시면 저도 보고 생각해본 후 답변 드려보도록 하겠습니다. 수고하셨습니다~~!!
| const [isAddModalOpen, setIsAddModalOpen] = useState(false); | ||
|
|
||
| useEffect(() => { | ||
| const fetChRestaurants = async () => { |
There was a problem hiding this comment.
fetchRestaurants 가 되어야할 것 같네요! 변수명에 오타가 있습니다.
| @@ -50,9 +50,25 @@ const restaurants = [ | |||
| ]; | |||
There was a problem hiding this comment.
이 부분은 step4를 진행하시고 step5를 진행하셔서 남아있는 부분이라고 이해하겠습니다 😄
There was a problem hiding this comment.
사실 필요 없는 데이터라고 생각해요. 이전에는 서버에서 데이터를 받아오고 있지 않아 상수로 선언하여 사용했으나 이제는 필요 없는 데이터가 된것이라고 생각합니다.
| } catch (error) { | ||
| console.error("음식점 추가 중 오류가 발생했습니다:", error); | ||
| } |
There was a problem hiding this comment.
tyr-catch 문 사용하신 부분은 매우 좋았어요 👍🏻 다만 에러가 잡혀 음식점 추가 중에 오류가 생겼을 때 console로만 찍어내는 것이 아닌 사용자에게 인터렉션이 필요한 부분이라고 생각해요. 사용성이 좋게는 기존의 모달을 확장성있게 사용하여도 되고 최소한의 방식으로는 alert가 있을 것 같네요! 참고로 사용자가 버튼을 눌렀지만 아무런 반응이 없으면 사이트 이탈률이 높아집니다.
| const handleOpenAddModal = () => { | ||
| setIsAddModalOpen(true); | ||
| }; | ||
|
|
||
| const handleCloseAddModal = () => { | ||
| setIsAddModalOpen(false); | ||
| }; | ||
|
|
||
| const handleAddRestaurant = async newRestaurant => { | ||
| try { | ||
| const response = await fetch("http://localhost:3000/restaurants", { | ||
| method: "POST", | ||
| headers: { | ||
| "Content-Type": "application/json", | ||
| }, | ||
| body: JSON.stringify(newRestaurant), | ||
| }); | ||
|
|
||
| if (!response.ok) { | ||
| throw new Error("음식점 추가에 실패했습니다."); | ||
| } | ||
|
|
||
| const createdRestaurant = await response.json(); | ||
| setRestaurants(prevRestaurants => [...prevRestaurants, createdRestaurant]); | ||
| handleCloseAddModal(); | ||
| } catch (error) { | ||
| console.error("음식점 추가 중 오류가 발생했습니다:", error); | ||
| } | ||
| }; |
There was a problem hiding this comment.
App.jsx 가 가지는 책임이 너무 많아보여요. 이 블로그 를 한번 읽어보셨으면 해요.
추가로 리액트에서는 각 핸들러 함수의 재료가 되는 setter함수들이 그대로 컴포넌트에 전달되고 각각의 컴포넌트 함수에서 핸들러로 감싸서 이벤트에 등록시키는 방식이 좀 더 상용화된(?), 국룰(?) 방법입니다!
|
|
||
| return ( | ||
| <div className="modal modal--open"> | ||
| <div className="modal-backdrop"></div> |
There was a problem hiding this comment.
이 부분 추가해주세요. 현재 추가하기 모달 뒤의 backdrop을 클릭하면 모달이 닫히지 않고 있어요.
| @@ -1,10 +1,35 @@ | |||
| export default function AddRestaurantModal() { | |||
| export default function AddRestaurantModal({ onClose, onAddRestaurant }) { | |||
| const handleAddRestaurant = e => { | |||
There was a problem hiding this comment.
const handleAddRestaurant = (e) => {
...
}이렇게 감싸져야한다고 생각해요!
| const handleAddRestaurant = e => { | ||
| e.preventDefault(); | ||
| const form = e.target; | ||
| const category = form.category.value; | ||
| const name = form.name.value; | ||
| const description = form.description.value; | ||
|
|
||
| if (!category || !name) { | ||
| alert('카테고리와 이름은 필수 입력 항목입니다.'); | ||
| return; | ||
| } | ||
|
|
||
| const newRestaurant = { | ||
| id: `a${Date.now()}`, | ||
| category, | ||
| name, | ||
| description, | ||
| }; | ||
|
|
||
| onAddRestaurant(newRestaurant); | ||
| form.reset(); | ||
|
|
||
| onClose(); | ||
| }; |
There was a problem hiding this comment.
이 부분에서는 두가지 정도를 이야기해보면 좋을거 같아요.
- 비제어 컴포넌트 방식으로 작성해주셨어요. 그 이유를 공유해주실 수 있을까요?
- 유효성 검사 중복이 되고 있어요. 만약 의도하신 것이라면 왜 그렇게 하셨는지 이유가 궁금해요.
// HTML에 이미 있음 <select name="category" required> <input type="text" name="name" required /> // JS에서 또 체크 if (!category || !name) { alert(...) }
There was a problem hiding this comment.
@realcdh 이 부분도 원래 작성하신 의도가 궁금합니다!
| if (!response.ok) { | ||
| throw new Error("음식점 추가에 실패했습니다."); | ||
| } |
There was a problem hiding this comment.
throw로 '음식점 추가에 실패했습니다' 를 넣어주셨어요. 하단의 catch에서 이 문구를 받아서 결국 에러를 출력해보면
출력 결과: "음식점 추가 중 오류가 발생했습니다: Error: 음식점 추가에 실패했습니다."
이렇게 나올거에요. 이건 유의미한 에러 트래킹이 아니며 실제 프로덕션 상황에서 에러가 생겼을 때 원인을 한눈에 파악할 수 없는 방법일 것 같습니다. 어떤 방식으로 에러를 트래킹하는 것이 좋을까요? throw는 언제 사용하는것이 적절할까요? 동현님이 학습하신 후에 공유해주시면 좋을 것 같아요.
keyword)
There was a problem hiding this comment.
@realcdh 이 부분에서 학습 하신 내용 공유주시면 좋을 것 같아요!
|
안녕하세요 범수 리뷰어님! 😊 피드백 주신 코드들, 그리고 보이지 않던 이미지 및 영상 링크까지 모두 수정하여 방금 다시 푸시해 두었습니다! 🙇♂️ 바쁘신 와중에 제가 놓친 부분들까지 꼼꼼하게 짚어주셔서 다시 한번 감사드립니다. 편하신 시간에 확인 부탁드리겠습니다. 남은 하루도 좋은 시간 보내세요! ✨ |
Indigochi1d
left a comment
There was a problem hiding this comment.
크게 수정할 사항은 없어보이나 몇가지 컨벤션에 대해서 코멘트 남겼어요. 처음 리뷰 때 질문드린 사항에 대해서 답변주시면 좋을 것 같아요~
| const fetchRestaurants = async () => { | ||
| try { | ||
| const response = await fetch(RESTAURANTS_API_URL); | ||
| const data = await response.json(); |
There was a problem hiding this comment.
data라는 변수 네이밍이 너무 포괄적인것 같아요. 이름을 지어주시겠어요?
| const RESTAURANTS_API_URL = 'http://localhost:3000/restaurants'; | ||
|
|
||
| export default function useRestaurants() { | ||
| const [restaurants, setRestaurants] = useState(initialRestaurants); |
There was a problem hiding this comment.
선언을 initialRestaurants로 하신 이유가 있을까요? RESTAURANTS_API_URL에서 받아온 데이터를 사용하여 덮어쓰고 있는데 이런 방식으로 구현하신 이유가 궁금해요.
| setIsAddModalOpen(false); | ||
| }; | ||
|
|
||
| const handleAddRestaurant = async e => { |
| type="submit" | ||
| disabled={isSubmitting}> | ||
| {isSubmitting ? '추가 중...' : '추가하기'} |
🙇♂️ 안녕하세요, 김범수 리뷰어님!
이번 미션 리뷰를 맡아주셔서 진심으로 감사드립니다. 5월 4일 자 작업 내용으로 PR 올립니다!
바닐라 JS에서 리액트로 넘어오면서 컴포넌트와 상태를 다루는 것이 낯설었지만, 어찌어찌 부딪혀가며 미션 요구사항 자체는 모두 구현을 완료했습니다! 하지만 코드가 돌아가게 만드는 데 급급해서 아직 완벽하게 제 것으로 소화하지 못한 개념들이 많은 것 같습니다. 😅 부족한 점이 많겠지만 따뜻하고 날카로운 피드백 부탁드리겠습니다!
🛠️ 주요 구현 사항
App컴포넌트에서 전체 데이터를 상태로 관리하여 조건부 렌더링을 구현했습니다.[cite: 5]📸 실행 화면 및 컴포넌트 구조도
https://github.com/user-attachments/assets/bcae3e4e-7121-4423-b5bd-d45847cbbe45

🙋♂️ 리뷰어님, 질문이 있습니다!
Q.
useEffect의 정확한 용도와 동작 원리가 도통 이해되지 않습니다 😭미션을 진행하면서 Side Effects를 다루기 위해
useEffect를 코드에 적용해 보긴 했는데요. 솔직히 말씀드리면 이 훅이 정확히 언제, 왜 실행되는지 머릿속에 전혀 그려지지 않고 도통 이해가 되지 않습니다.단순히 '화면이 그려진 후 실행된다' 정도로만 감을 잡고 쓰다 보니, 의존성 배열(dependency array)에 무엇을 넣고 빼야 하는지 등등 무척 막막하게 느껴집니다ㅜㅜ. 리뷰어님께서는 처음
useEffect를 학습하실 때 어떤 식으로 개념을 잡으셨는지, 초보자가 직관적으로 이해할 수 있는 꿀팁이나 비유가 있다면 소중한 조언을 꼭 부탁드리고 싶습니다!바쁘신 와중에 제 코드를 리뷰해 주셔서 다시 한번 감사드립니다. 코멘트 남겨주시면 열심히 고민하고 수정해 보겠습니다. 좋은 하루 보내세요! ✨