From 1a655f317d1901933f0fd647103a4620e3c15577 Mon Sep 17 00:00:00 2001 From: Nam Yooseong Date: Mon, 15 Jun 2026 23:34:00 +0900 Subject: [PATCH 01/16] =?UTF-8?q?docs:=20=EB=B6=88=ED=95=84=EC=9A=94?= =?UTF-8?q?=ED=95=9C=20=ED=8C=8C=EC=9D=BC=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- 01-first-component/README.md | 46 ---------------------- 02-rendering-lists/README.md | 74 ------------------------------------ 03-modal/README.md | 24 ------------ 04-form/README.md | 38 ------------------ 05-effects/README.md | 49 ------------------------ 5 files changed, 231 deletions(-) delete mode 100644 01-first-component/README.md delete mode 100644 02-rendering-lists/README.md delete mode 100644 03-modal/README.md delete mode 100644 04-form/README.md delete mode 100644 05-effects/README.md diff --git a/01-first-component/README.md b/01-first-component/README.md deleted file mode 100644 index 72b1469..0000000 --- a/01-first-component/README.md +++ /dev/null @@ -1,46 +0,0 @@ -# 01. 컴포넌트 선언하고 사용하기: Component 기본 구조와 JSX - -## 🎯 요구 사항 -- `/templates` 에 있는 html 템플릿을 그대로 `App.jsx`에서 그릴 수 있게 해보세요. -- `App.jsx`를 여러 개의 컴포넌트로 분리해서 그려보세요. - - 스타일도 별도의 css파일로 분리하여 각 컴포넌트에서 import합니다. - - (선택) `module.css` 를 사용해 보세요. - -### 구현 결과 예시 -- 예를 들어, `App.jsx`의 return문을 아래와 같이 작성했을 때에 앱이 정상적으로 그려지도록 구현해 주세요. -- 컴포넌트의 이름이나 구조는 마음대로 변경해도 좋습니다 -```javascript -function App() { - return ( - <> -
-
- - -
- - - ); -} -``` - -## ✅ 키워드 -- JSX - - `class` -> `className` - - `for` -> `htmlFor` - - self closing tag - - Fragment - - `{}` 내에 쓸 수 있는 JS 식 -- React Component - - 기본 구조 - - export / import - -## 🧙‍♀️ 진행 가이드 -- 진행 시간: 1시간 내에 완료하는 것을 목표로 합니다. - -## 🔗 참고 문서 -- [Thinking in React](https://react.dev/learn/thinking-in-react)의 Step1-2 에 있는 것처럼 나만의 컴포넌트 단위를 나누어 보세요. -- [Your First Component](https://react.dev/learn/your-first-component) diff --git a/02-rendering-lists/README.md b/02-rendering-lists/README.md deleted file mode 100644 index 3fcfcf1..0000000 --- a/02-rendering-lists/README.md +++ /dev/null @@ -1,74 +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) \ No newline at end of file diff --git a/03-modal/README.md b/03-modal/README.md deleted file mode 100644 index bc74460..0000000 --- a/03-modal/README.md +++ /dev/null @@ -1,24 +0,0 @@ -# 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) \ No newline at end of file diff --git a/04-form/README.md b/04-form/README.md deleted file mode 100644 index b029ec9..0000000 --- a/04-form/README.md +++ /dev/null @@ -1,38 +0,0 @@ -# 04. 폼 UI 구현하기: controlled vs uncontrolled - -## 🎯 요구 사항 -- `Header`의 레스토랑 추가 버튼을 클릭하면 레스토랑 추가 폼이 모달로 뜨도록 구현해 주세요 - - 이전 단계에서 만들어두었던 `AddRestaurantModal`을 그대로 사용합니다. -- 카테고리를 선택하고, ``, ` + 메뉴 등 추가 정보를 입력해 주세요. + + + + + + + + ); +} diff --git a/src/components/AddRestaurantModal/AddRestaurantModal.jsx b/src/components/AddRestaurantModal/AddRestaurantModal.jsx deleted file mode 100644 index 0bb7c55..0000000 --- a/src/components/AddRestaurantModal/AddRestaurantModal.jsx +++ /dev/null @@ -1,80 +0,0 @@ -import { useState } from "react"; -import styles from "./AddRestaurantModal.module.css"; -import Modal from "../Modal/Modal.jsx"; -import { CATEGORIES } from "../../constants/categories.js"; - -export default function AddRestaurantModal({ onSubmit, onClose }) { - const [category, setCategory] = useState(""); - const [name, setName] = useState(""); - const [description, setDescription] = useState(""); - - function handleFormSubmit(e) { - e.preventDefault(); - onSubmit({ category, name, description }); - } - - return ( - -
-
- - -
- -
- - setName(e.target.value)} - required - /> -
- -
- - - - 메뉴 등 추가 정보를 입력해 주세요. - -
- -
- -
-
-
- ); -} diff --git a/src/components/AddRestaurantModal/AddRestaurantModal.module.css b/src/components/AddRestaurantModal/AddRestaurantModal.module.css deleted file mode 100644 index 58dd401..0000000 --- a/src/components/AddRestaurantModal/AddRestaurantModal.module.css +++ /dev/null @@ -1,77 +0,0 @@ -.formItem { - display: flex; - flex-direction: column; - - margin-bottom: 36px; -} - -.formItem label { - color: var(--grey-400); - font-size: 14px; -} - -.formItem--required label::after { - padding-left: 4px; - - color: var(--primary-color); - content: "*"; -} - -.formItem__helpText { - color: var(--grey-300); -} - -.formItem input, -.formItem textarea, -.formItem select { - padding: 8px; - margin: 6px 0; - - border: 1px solid var(--grey-200); - border-radius: 8px; - - font-size: 16px; -} - -.formItem textarea { - resize: none; -} - -.formItem select { - height: 44px; - - padding: 8px; - - border: 1px solid var(--grey-200); - border-radius: 8px; - - color: var(--grey-300); -} - -input[name="name"], -input[name="link"] { - height: 44px; -} - -.modal__buttonContainer { - display: flex; -} - -.button { - width: 100%; - height: 44px; - - margin-right: 16px; - - border: none; - border-radius: 8px; - - font-weight: 600; - cursor: pointer; -} - -.button--primary { - background: var(--primary-color); - - color: var(--grey-100); -} From b6bba54929722853f25a1d26a622e436d6a73728 Mon Sep 17 00:00:00 2001 From: Nam Yooseong Date: Tue, 16 Jun 2026 01:04:39 +0900 Subject: [PATCH 10/16] =?UTF-8?q?refactor:=20App.css=20Typography=20?= =?UTF-8?q?=EC=A0=9C=EA=B1=B0=20=EB=B0=8F=20App.jsx=20import=20=EA=B2=BD?= =?UTF-8?q?=EB=A1=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/App.css | 25 ------------------------- src/App.jsx | 10 +++++----- 2 files changed, 5 insertions(+), 30 deletions(-) diff --git a/src/App.css b/src/App.css index 6bfcc47..e467775 100644 --- a/src/App.css +++ b/src/App.css @@ -25,28 +25,3 @@ body { --grey-400: #344054; --grey-500: #000000; } - -/* Typography *************************************/ -.text-title { - font-size: 20px; - line-height: 24px; - font-weight: 600; -} - -.text-subtitle { - font-size: 18px; - line-height: 28px; - font-weight: 600; -} - -.text-body { - font-size: 16px; - line-height: 24px; - font-weight: 400; -} - -.text-caption { - font-size: 14px; - line-height: 20px; - font-weight: 400; -} diff --git a/src/App.jsx b/src/App.jsx index 485546e..de2481e 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -1,11 +1,11 @@ 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 Header from "./components/Header.jsx"; +import CategoryFilter from "./components/CategoryFilter.jsx"; +import RestaurantList from "./components/RestaurantList.jsx"; import { useState } from "react"; import { filterRestaurants } from "./utils/filterRestaurants.js"; -import RestaurantDetailModal from "./components/RestaurantDetailModal/RestaurantDetailModal.jsx"; -import AddRestaurantModal from "./components/AddRestaurantModal/AddRestaurantModal.jsx"; +import RestaurantDetailModal from "./components/RestaurantDetailModal.jsx"; +import AddRestaurantModal from "./components/AddRestaurantModal.jsx"; import { useRestaurants } from "./hooks/useRestaurants.js"; import { ALL_CATEGORY } from "./constants/categories.js"; From c7ff433bccbe4f9ba70e441be2f83413aeba6ce8 Mon Sep 17 00:00:00 2001 From: Nam Yooseong Date: Tue, 16 Jun 2026 01:04:46 +0900 Subject: [PATCH 11/16] =?UTF-8?q?docs:=20=EB=AF=B8=EC=85=98=20=ED=95=99?= =?UTF-8?q?=EC=8A=B5=20=EB=82=B4=EC=9A=A9=20=EB=B0=8F=20=EA=B5=AC=ED=98=84?= =?UTF-8?q?=20=EB=AA=A9=EB=A1=9D=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 397 ++++++++---------------------------------------------- 1 file changed, 59 insertions(+), 338 deletions(-) diff --git a/README.md b/README.md index e9671f0..0674fb9 100644 --- a/README.md +++ b/README.md @@ -1,387 +1,108 @@ -# API 요청과 비동기 처리 +# styled-components를 적용해서 리팩토링하기 ## 🎯 개인 목표 및 목표 달성을 위한 행동 가이드 이번 미션을 통해 다음과 같은 학습 경험들을 쌓는 것을 목표로 한다. -1. side effect가 무엇인지 이해하고, `useEffect`가 왜 필요한지 설명할 수 있다. -2. Promise가 무엇인지 이해하고, async/await가 Promise를 어떻게 다루는지 설명할 수 있다. -3. `fetch`로 GET/POST 요청을 보내고 응답을 처리하는 방법을 익히고, `await`를 어디에 붙여야 하는지 스스로 판단할 수 있다. +1. CSS Modules 방식과 CSS-in-JS 방식의 차이를 직접 마이그레이션하며 체감한다. +2. styled-components의 기본 사용법(기본 스타일링, 중첩 선택자, 컴포넌트 확장)을 익힌다. +3. 기존 코드의 구조를 유지하면서 스타일링 방식만 교체하는 리팩토링 경험을 쌓는다. --- ## 📝 기능 구현 목록 -- [x] API로 음식점 목록을 불러와 RestaurantList에 렌더링 -- [x] 음식점 추가 시 POST 요청 후 목록 재조회 +- [x] 모든 `.module.css` 파일을 제거하고 styled-components로 전환 +- [x] `Header`, `CategoryFilter`, `Modal`, `RestaurantList`, `RestaurantDetailModal`, `AddRestaurantModal` 전 컴포넌트에 styled-components 적용 +- [x] `App.css`는 전역 리셋 및 CSS 변수 정의 용도로만 유지 +- [x] CSS 변수(`var(--primary-color)` 등)를 styled-components 내부에서 활용 +- [x] `styled(Component)` 확장 패턴을 활용해 필수 입력 항목(`RequiredFormItem`) 스타일 분리 --- ## 📚 학습 내용 -### useEffect와 side effect +### styled-components란? -React 컴포넌트는 렌더링 중에 외부 시스템(서버, 타이머, DOM 직접 조작 등)에 영향을 주면 안 된다. 이런 작업을 side effect라고 하고, `useEffect`는 렌더링이 끝난 뒤 이를 안전하게 실행하는 공간이다. - -```jsx -useEffect(() => { - // 렌더링 이후 실행 — 서버 요청, 구독, DOM 조작 등 -}, []); -``` - -의존성 배열 `[]`를 넘기면 컴포넌트가 처음 마운트될 때 한 번만 실행된다. 배열을 아예 생략하면 매 렌더링마다 실행되어 무한 루프가 생길 수 있다. - -### useEffect 안에서 async/await 쓰는 법 - -`useEffect`의 콜백은 직접 `async`로 만들 수 없다. `async` 함수는 항상 Promise를 반환하는데, React는 `useEffect` 콜백의 반환값을 cleanup 함수로 기대하기 때문이다. - -```jsx -// ❌ 이렇게 하면 안 됨 — async 콜백이 Promise를 반환해서 React가 경고 -useEffect(async () => { - const data = await getRestaurants(); - setRestaurants(data); -}, []); - -// ✅ 내부에서 async 함수를 정의하고 호출 -useEffect(() => { - const fetchRestaurants = async () => { - const data = await getRestaurants(); - setRestaurants(data); - }; - fetchRestaurants(); -}, []); -``` - -### async/await를 어디에 붙여야 하는가 - -`await`는 Promise를 반환하는 함수 앞에 붙인다. `await`를 쓰는 함수 자신은 반드시 `async`여야 한다. 이 규칙이 호출 체인을 따라 전파된다. - -``` -fetch() → Promise 반환 -getRestaurants() → 내부에서 await fetch() → async 필요 -handleRestaurantSubmit() → 내부에서 await getRestaurants() → async 필요 -``` - -실수하기 쉬운 패턴: `async` 함수를 호출할 때 `await`를 빠뜨리면 Promise가 풀리기 전에 다음 줄이 실행된다. - -```jsx -// ❌ await 누락 — POST가 완료되기 전에 GET 실행, 새 항목이 목록에 없을 수 있음 -async function handleRestaurantSubmit(restaurant) { - addRestaurant(restaurant); // await 없음 - const data = await getRestaurants(); - setRestaurants(data); -} - -// ✅ POST 완료를 기다린 뒤 GET -async function handleRestaurantSubmit(restaurant) { - await addRestaurant(restaurant); - const data = await getRestaurants(); - setRestaurants(data); -} -``` - -### api.js 파일 위치 - -현재 `src/api.js`에 두었다. 프로젝트 규모에 따라 관례가 다르다. - -| 규모 | 구조 | 예시 | -|---|---|---| -| 소규모 | 단일 파일 | `src/api.js` | -| 중규모 | 도메인별 분리 | `src/api/restaurants.js`, `src/api/users.js` | -| 대규모 | services 레이어 | `src/services/restaurantService.js` | - -`api/`와 `services/`의 차이는 뉘앙스 차이다. `api/`는 서버와의 통신 함수 모음이라는 의미가 강하고, `services/`는 비즈니스 로직까지 포함할 수 있다는 의미로 쓰이기도 한다. 팀마다 다르므로 프로젝트 컨벤션을 따르면 된다. 현재 프로젝트처럼 API 함수가 몇 개 없을 때는 `src/api.js` 하나로 충분하다. - ---- - -## 🤔 고민했던 문제와 해결 과정에서 배운 점 - -### async 함수를 정의만 하고 호출하지 않은 문제 - -`useEffect` 안에서 async 함수를 정의했지만 호출하지 않아 API 요청이 실행되지 않았다. 같은 실수를 두 번 했다. - -```jsx -// ❌ 정의만 하고 호출하지 않음 — 아무 일도 일어나지 않음 -useEffect(() => { - async () => { - const data = await getRestaurants(); - setRestaurants(data); - }; -}, []); - -// ✅ 정의 후 호출 -useEffect(() => { - const fetchRestaurants = async () => { - const data = await getRestaurants(); - setRestaurants(data); - }; - fetchRestaurants(); // 호출 -}, []); -``` - -### 커스텀 훅 분리 — `useRestaurants` - -`App`이 렌더링 구조와 UI 상태를 다루는 컴포넌트인데, 서버 통신과 데이터 관리 로직까지 함께 들어있어 역할이 섞여 있었다. `restaurants` state, `useEffect`, fetch 로직을 커스텀 훅으로 추출했다. - -### `fetchRestaurants`를 `useCallback`으로 추출 - -`fetchRestaurants`가 `useEffect` 내부에 정의되어 있어 `addRestaurant`에서 같은 로직을 중복으로 작성해야 했다. 함수를 훅 스코프로 꺼내 재사용하려면 `useCallback`이 필요하다. - -`useCallback` 없이 일반 함수로 선언하면 렌더링마다 새 함수 참조가 생성되어 `useEffect([fetchRestaurants])`가 매 렌더링마다 실행되는 무한 루프가 발생한다. `useCallback`의 빈 의존성 배열 `[]`이 참조를 고정해 이를 방지한다. +CSS-in-JS 라이브러리로, JavaScript 파일 안에서 템플릿 리터럴 문법으로 CSS를 작성하고 이를 React 컴포넌트에 직접 연결하는 방식이다. ```js -// Before: fetchRestaurants가 useEffect 안에 갇혀 있어 addRestaurant에서 로직을 중복 작성 -export function useRestaurants() { - const [restaurants, setRestaurants] = useState([]); - - useEffect(() => { - const fetchRestaurants = async () => { - const data = await getRestaurants(); - setRestaurants(data); - }; - fetchRestaurants(); - }, []); - - async function addRestaurant(restaurant) { - await createRestaurant(restaurant); - const data = await getRestaurants(); // 중복 - setRestaurants(data); // 중복 - } - - return { restaurants, addRestaurant }; -} - -// After: useCallback으로 추출해 재사용, 의존성 배열도 명시적으로 선언 -export function useRestaurants() { - const [restaurants, setRestaurants] = useState([]); - - const fetchRestaurants = useCallback(async () => { - const data = await getRestaurants(); - setRestaurants(data); - }, []); - - useEffect(() => { - void fetchRestaurants(); - }, [fetchRestaurants]); - - async function addRestaurant(restaurant) { - await createRestaurant(restaurant); - await fetchRestaurants(); // 재사용 - } - - return { restaurants, addRestaurant }; -} +const Button = styled.button` + background-color: var(--primary-color); + color: white; + border-radius: 8px; + padding: 10px 20px; +`; ``` -App에서는 서버 통신 로직이 모두 사라지고, 모달 열림/닫힘 같은 UI 상태만 남았다. - -```jsx -// Before: App이 서버 통신까지 담당 -const [restaurants, setRestaurants] = useState([]); -useEffect(() => { /* fetch 로직 */ }, []); -async function handleRestaurantSubmit(...) { - await postRestaurant(...); - const data = await getRestaurants(); - setRestaurants(data); -} - -// After: 훅이 데이터 관리를 담당 -const { restaurants, addRestaurant } = useRestaurants(); -async function handleRestaurantSubmit(restaurant) { - await addRestaurant(restaurant); - setIsAddRestaurantModalOpen(false); -} -``` - -## 🛠 리팩토링 - -### state 네이밍 — 용도가 아닌 값의 성격으로 - -`filterCategory`는 이 state가 필터링에 쓰인다는 **용도**를 표현한다. state 이름은 어디에 쓰이는지가 아니라 **어떤 값을 담고 있는지**를 나타내는 게 좋다. +### CSS Modules vs styled-components trade-off -```jsx -// Before: 용도 표현 -const [filterCategory, setFilterCategory] = useState("전체"); - -// After: 값의 성격 표현 -const [selectedCategory, setSelectedCategory] = useState("전체"); -``` - -### API 함수명 — HTTP 메서드가 아닌 의도로 - -`postRestaurant`는 HTTP 메서드 이름(`post`)을 그대로 쓴 것이다. 함수명은 어떻게 동작하는지가 아니라 무엇을 하려는지를 나타내야 한다. - -```js -// Before: 구현 방법 표현 -export async function postRestaurant(restaurant) { ... } - -// After: 의도 표현 -export async function addRestaurant(restaurant) { ... } -``` +| | CSS Modules | styled-components | +|---|---|---| +| 스타일 위치 | 별도 `.module.css` 파일 | 컴포넌트 파일 내부 | +| 스코프 | 클래스명 해시로 자동 격리 | 컴포넌트 단위로 격리 | +| 동적 스타일링 | className 조건부 변경 필요 | props로 직접 처리 가능 | +| 가독성 | HTML 구조와 스타일 파일 분리 | 한 파일에서 구조+스타일 파악 가능 | +| 번들 크기 | 별도 런타임 없음 | styled-components 런타임 포함 | -### BASE_URL 상수 추출 +### 자식 선택자 중첩 -`http://localhost:3000`이 `getRestaurants`와 `addRestaurant` 두 곳에 반복됐다. 상수로 추출해 한 곳에서 관리한다. +styled-components는 Sass처럼 중첩 선택자를 지원한다. ```js -const BASE_URL = "http://localhost:3000"; -``` - -### try/catch 추가 +const Category = styled.div` + background: var(--lighten-color); -`fetch`는 네트워크 오류에서만 throw하고, HTTP 4xx/5xx 응답은 throw하지 않는다. 두 가지를 모두 처리하려면 `response.ok` 확인과 `try/catch`가 함께 필요하다. - -```js -export async function getRestaurants() { - try { - const response = await fetch(`${BASE_URL}/restaurants`); - if (!response.ok) throw new Error(`서버 오류: ${response.status}`); - const restaurants = await response.json(); - return restaurants; - } catch (error) { - console.error("음식점 목록 조회 실패:", error); - throw error; // 호출한 쪽이 에러를 알 수 있도록 다시 던짐 + img { + width: 36px; + height: 36px; } -} +`; ``` -catch에서 `throw error`를 다시 던지는 이유: 로그만 남기고 삼켜버리면 호출한 쪽(`App.jsx`)이 에러 발생 여부를 알 수 없다. - -### api.js에서 try/catch 제거 +### 컴포넌트 확장 (`styled(Component)`) -`api.js`에서 `try/catch`로 잡고 `throw`로 다시 던지는 건 `try/catch`를 안 쓴 것과 결과가 같다. 콘솔 로그만 남기고 에러를 그대로 위로 올리기 때문이다. 에러를 처리할 수 있는 곳(훅, 컴포넌트)에서만 잡도록 `api.js`는 단순하게 throw만 하게 변경했다. +기존 styled-component를 상속해서 스타일을 추가할 수 있다. ```js -// Before: try/catch로 잡았다가 다시 throw — 의미 없는 중간 처리 -export async function getRestaurants() { - try { - const response = await fetch(`${BASE_URL}/restaurants`); - if (!response.ok) throw new Error(`서버 오류: ${response.status}`); - return await response.json(); - } catch (error) { - console.error("음식점 목록 조회 실패:", error); - throw error; - } -} - -// After: 단순하게 throw만 -export async function getRestaurants() { - const response = await fetch(`${BASE_URL}/restaurants`); - if (!response.ok) throw new Error(`서버 오류: ${response.status}`); - return response.json(); -} -``` - -### 에러 핸들링 추가 +const FormItem = styled.div`...`; -에러는 사용자에게 보여줄 수 있는 가장 가까운 곳에서 한 번만 잡는다. 중간 함수(`addRestaurant`)에서 잡으면 에러가 거기서 소멸되어 최종 호출자가 실패 여부를 알 수 없다. - -- 초기 로딩 실패: `fetchRestaurants`의 `try/catch`에서 `error` state에 담아 화면에 표시 -- 음식점 추가 실패: `handleRestaurantSubmit`의 `try/catch`에서 alert로 사용자에게 알림 - -```js -// useRestaurants.js — 초기 로딩 에러 처리 -const fetchRestaurants = useCallback(async () => { - setIsLoading(true); - try { - const data = await getRestaurants(); - setRestaurants(data); - } catch (error) { - setError("음식점 목록을 불러오지 못했습니다."); - } finally { - setIsLoading(false); +const RequiredFormItem = styled(FormItem)` + label::after { + content: "*"; + color: var(--primary-color); } -}, []); - -// App.jsx — 추가 실패 에러 처리 -async function handleRestaurantSubmit(restaurant) { - try { - await addRestaurant(restaurant); - setIsAddRestaurantModalOpen(false); - } catch { - alert("음식점 추가에 실패했습니다. 다시 시도해주세요."); - } -} +`; ``` -### 매직 스트링 상수화 — `ALL_CATEGORY` - -`"전체"` 문자열이 `App.jsx`, `CategoryFilter.jsx`, `filterRestaurants.js` 세 곳에 흩어져 있었다. 하나라도 수정하면 나머지도 함께 바꿔야 하는 암묵적 결합이다. 상수로 추출해 한 곳에서 관리한다. - -```js -// Before: 세 곳에 흩어진 매직 스트링 -useState("전체"); - -if (category === "전체") return restaurants; - -// After: 상수로 단일화 -export const ALL_CATEGORY = "전체"; - -useState(ALL_CATEGORY); - -if (category === ALL_CATEGORY) return restaurants; -``` - -### import 확장자 일관성 +--- -Vite 프로젝트에서 일부 파일은 `.jsx`/`.js` 확장자를 명시하고 일부는 생략해 혼재된 상태였다. Vite는 ESM 표준에 가깝게 확장자 명시를 권장하므로 모든 로컬 import에 확장자를 추가했다. +## 🤔 고민했던 문제와 해결 과정에서 배운 점 -```js -// Before -import Modal from "../Modal/Modal"; -import { CATEGORIES } from "../../constants/categories"; +### 필수/선택 폼 항목의 `*` 표시 처리 -// After -import Modal from "../Modal/Modal.jsx"; -import { CATEGORIES } from "../../constants/categories.js"; -``` +원본 CSS Modules에서는 `.formItem--required` 클래스를 조건부로 붙이는 방식으로 필수 항목에만 `*`를 표시했다. -### isLoading state 추가 +styled-components로 전환할 때 `label::after { content: "*" }`를 `FormItem`에 바로 넣으면 모든 항목에 `*`가 붙는 문제가 생긴다. -데이터를 불러오는 동안 화면이 빈 상태로 보이는 문제를 해결하기 위해 `isLoading` state를 추가했다. `finally`를 사용해 성공/실패 여부와 관계없이 로딩 상태가 반드시 해제되도록 했다. +`styled(FormItem)`으로 확장한 `RequiredFormItem`을 별도로 만들어서 필수 항목(카테고리, 이름)에만 적용하는 방식으로 해결했다. -```js -const [isLoading, setIsLoading] = useState(false); - -const fetchRestaurants = useCallback(async () => { - setIsLoading(true); - try { - const data = await getRestaurants(); - setRestaurants(data); - } catch (error) { - setError("음식점 목록을 불러오지 못했습니다."); - } finally { - setIsLoading(false); // 성공/실패 모두 로딩 해제 - } -}, []); - -return { restaurants, addRestaurant, isLoading, error }; -``` +--- -### Modal Escape 키 처리 +## 🛠 리팩토링 -backdrop 클릭으로만 모달을 닫을 수 있어 키보드 사용자가 닫을 수 없는 접근성 문제가 있었다. `useEffect`로 keydown 이벤트를 등록해 Escape 키로도 닫히도록 했다. +### 컴포넌트 파일 구조 단순화 -cleanup 함수로 이벤트 리스너를 제거하지 않으면 모달이 닫혀도 리스너가 남아, 모달을 여러 번 열고 닫을수록 리스너가 누적되어 `onClose`가 중복 호출된다. +기존에는 컴포넌트마다 폴더를 만들어 `ComponentName/ComponentName.jsx` + `ComponentName.module.css` 구조였다. styled-components 전환 후 CSS 파일이 사라지면서 폴더 없이 `ComponentName.jsx` 단일 파일로 정리했다. -```js -useEffect(() => { - function handleKeyDown(e) { - if (e.key === "Escape") onClose(); - } - document.addEventListener("keydown", handleKeyDown); - return () => document.removeEventListener("keydown", handleKeyDown); -}, [onClose]); ``` +before: +components/ + Header/ + Header.jsx + Header.module.css -### Header 이미지 alt 중복 제거 - -버튼에 `aria-label="음식점 추가"`가 있는데 내부 이미지에도 `alt="음식점 추가"`가 있어 스크린 리더가 "음식점 추가 음식점 추가"를 두 번 읽는 문제가 있었다. 버튼 안의 이미지는 버튼 자체가 의미를 전달하므로 장식적 역할이다. `alt` 속성을 제거해 스크린 리더가 이미지를 건너뛰도록 했다. - -```jsx -// Before -음식점 추가 - -// After - +after: +components/ + Header.jsx ``` From 7717b52c342040f0bce2a1bde08df2cddf6fe69a Mon Sep 17 00:00:00 2001 From: Nam Yooseong Date: Tue, 16 Jun 2026 14:59:25 +0900 Subject: [PATCH 12/16] =?UTF-8?q?refactor:=20CSS=20=EB=B3=80=EC=88=98=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20=EB=B0=8F=20=ED=95=98=EB=93=9C=EC=BD=94?= =?UTF-8?q?=EB=94=A9=20=EC=83=89=EC=83=81=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/App.css | 2 ++ src/components/Header.jsx | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/App.css b/src/App.css index e467775..f5ae243 100644 --- a/src/App.css +++ b/src/App.css @@ -19,7 +19,9 @@ body { :root { --primary-color: #ec4a0a; --lighten-color: #f6a88a; + --grey-50: #fcfcfd; --grey-100: #ffffff; + --grey-150: #e9eaed; --grey-200: #d0d5dd; --grey-300: #667085; --grey-400: #344054; diff --git a/src/components/Header.jsx b/src/components/Header.jsx index f8a856a..81000dc 100644 --- a/src/components/Header.jsx +++ b/src/components/Header.jsx @@ -11,7 +11,7 @@ const Gnb = styled.header` `; const GnbTitle = styled.h1` - color: #fcfcfd; + color: var(--grey-50); font-size: 20px; line-height: 24px; font-weight: 600; From 16c744f584009e8c869fbdc7bd2c86402f58c538 Mon Sep 17 00:00:00 2001 From: Nam Yooseong Date: Tue, 16 Jun 2026 14:59:36 +0900 Subject: [PATCH 13/16] =?UTF-8?q?refactor:=20CategoryFilter=20select?= =?UTF-8?q?=EB=A5=BC=20=EB=B3=84=EB=8F=84=20styled=20=EC=BB=B4=ED=8F=AC?= =?UTF-8?q?=EB=84=8C=ED=8A=B8=EB=A1=9C=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/CategoryFilter.jsx | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/components/CategoryFilter.jsx b/src/components/CategoryFilter.jsx index ab92942..0fc45d3 100644 --- a/src/components/CategoryFilter.jsx +++ b/src/components/CategoryFilter.jsx @@ -6,22 +6,22 @@ const CategoryFilterSection = styled.section` justify-content: space-between; padding: 0 16px; margin-top: 24px; +`; - select { - height: 44px; - min-width: 125px; - border: 1px solid #d0d5dd; - border-radius: 8px; - background: transparent; - font-size: 16px; - padding: 8px; - } +const CategorySelect = styled.select` + height: 44px; + min-width: 125px; + border: 1px solid var(--grey-200); + border-radius: 8px; + background: transparent; + font-size: 16px; + padding: 8px; `; export default function CategoryFilter({ category, onCategoryChange }) { return ( - + ); } From 0172a4ece1fd668e8caf340807df787df94dc8e4 Mon Sep 17 00:00:00 2001 From: Nam Yooseong Date: Tue, 16 Jun 2026 14:59:46 +0900 Subject: [PATCH 14/16] =?UTF-8?q?refactor:=20AddRestaurantModal=20?= =?UTF-8?q?=ED=8F=BC=20=EC=9A=94=EC=86=8C=EB=A5=BC=20=EB=B3=84=EB=8F=84=20?= =?UTF-8?q?styled=20=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=EB=A1=9C=20?= =?UTF-8?q?=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/AddRestaurantModal.jsx | 94 +++++++++++++-------------- 1 file changed, 47 insertions(+), 47 deletions(-) diff --git a/src/components/AddRestaurantModal.jsx b/src/components/AddRestaurantModal.jsx index 4c9e17e..5a07adc 100644 --- a/src/components/AddRestaurantModal.jsx +++ b/src/components/AddRestaurantModal.jsx @@ -14,53 +14,53 @@ const FormItem = styled.div` line-height: 20px; font-weight: 400; } +`; - select { - padding: 8px; - margin: 6px 0; - border: 1px solid var(--grey-200); - border-radius: 8px; - font-size: 16px; - height: 44px; - color: var(--grey-300); +const RequiredFormItem = styled(FormItem)` + label::after { + padding-left: 4px; + color: var(--primary-color); + content: "*"; } +`; - input { - padding: 8px; - margin: 6px 0; - border: 1px solid var(--grey-200); - border-radius: 8px; - font-size: 16px; - height: 44px; - } +const FormSelect = styled.select` + padding: 8px; + margin: 6px 0; + border: 1px solid var(--grey-200); + border-radius: 8px; + font-size: 16px; + height: 44px; + color: var(--grey-300); +`; - textarea { - resize: none; - padding: 8px; - margin: 6px 0; - border: 1px solid var(--grey-200); - border-radius: 8px; - font-size: 16px; - } +const FormInput = styled.input` + padding: 8px; + margin: 6px 0; + border: 1px solid var(--grey-200); + border-radius: 8px; + font-size: 16px; + height: 44px; +`; - span { - color: var(--grey-300); - font-size: 14px; - line-height: 20px; - font-weight: 400; - } +const FormTextarea = styled.textarea` + resize: none; + padding: 8px; + margin: 6px 0; + border: 1px solid var(--grey-200); + border-radius: 8px; + font-size: 16px; `; -const ButtonContainer = styled.div` - display: flex; +const HelpText = styled.span` + color: var(--grey-300); + font-size: 14px; + line-height: 20px; + font-weight: 400; `; -const RequiredFormItem = styled(FormItem)` - label::after { - padding-left: 4px; - color: var(--primary-color); - content: "*"; - } +const ButtonContainer = styled.div` + display: flex; `; const Button = styled.button` @@ -69,12 +69,12 @@ const Button = styled.button` margin-right: 16px; border: none; border-radius: 8px; - cursor: pointer; - background: var(--primary-color); - color: var(--grey-100); font-size: 14px; line-height: 20px; font-weight: 600; + cursor: pointer; + background: var(--primary-color); + color: var(--grey-100); `; export default function AddRestaurantModal({ onSubmit, onClose }) { @@ -92,7 +92,7 @@ export default function AddRestaurantModal({ onSubmit, onClose }) {
- + - - - 메뉴 등 추가 정보를 입력해 주세요. + > + 메뉴 등 추가 정보를 입력해 주세요. From e25eccb82e039f445ea2637c0c661ba1b15c58bd Mon Sep 17 00:00:00 2001 From: Nam Yooseong Date: Tue, 16 Jun 2026 15:00:06 +0900 Subject: [PATCH 15/16] =?UTF-8?q?refactor:=20RestaurantList=20=EC=8B=9C?= =?UTF-8?q?=EB=A7=A8=ED=8B=B1=20=EA=B5=AC=EC=A1=B0=20=EA=B0=9C=EC=84=A0=20?= =?UTF-8?q?=EB=B0=8F=20styled=20=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=20?= =?UTF-8?q?=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - section > ul 이중 구조를 ul 단일 구조로 변경 (styled.section → styled.ul) - Info 내 자식 선택자(h3, p)를 RestaurantName, RestaurantDescription으로 분리 - 하드코딩 색상을 var(--grey-150)으로 교체 --- src/components/RestaurantList.jsx | 78 +++++++++++++++---------------- 1 file changed, 37 insertions(+), 41 deletions(-) diff --git a/src/components/RestaurantList.jsx b/src/components/RestaurantList.jsx index 0e205d9..26d0588 100644 --- a/src/components/RestaurantList.jsx +++ b/src/components/RestaurantList.jsx @@ -1,15 +1,13 @@ import { CATEGORY_IMAGES } from "../constants/categoryImages.js"; import styled from "styled-components"; -const List = styled.section` - display: flex; - flex-direction: column; +const List = styled.ul` padding: 0 16px; margin: 16px 0; `; const Restaurant = styled.li` - border-bottom: 1px solid #e9eaed; + border-bottom: 1px solid var(--grey-150); `; const Button = styled.button` @@ -45,50 +43,48 @@ const Info = styled.div` display: flex; flex-direction: column; justify-content: flex-start; +`; - h3 { - margin: 0; - font-size: 18px; - line-height: 28px; - font-weight: 600; - } +const RestaurantName = styled.h3` + margin: 0; + font-size: 18px; + line-height: 28px; + font-weight: 600; +`; - p { - display: -webkit-box; - padding-top: 8px; - overflow: hidden; - text-overflow: ellipsis; - -webkit-line-clamp: 2; - -webkit-box-orient: vertical; - font-size: 16px; - line-height: 24px; - font-weight: 400; - } +const RestaurantDescription = styled.p` + display: -webkit-box; + padding-top: 8px; + overflow: hidden; + text-overflow: ellipsis; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + font-size: 16px; + line-height: 24px; + font-weight: 400; `; export default function RestaurantList({ restaurants, onRestaurantClick }) { return ( -
    - {restaurants.map((restaurant) => { - return ( - - - - ); - })} -
+ {restaurants.map((restaurant) => { + return ( + + + + ); + })}
); } From 00cd29d188f79c535d37f95a9b9ef325d2643197 Mon Sep 17 00:00:00 2001 From: Nam Yooseong Date: Tue, 16 Jun 2026 15:00:30 +0900 Subject: [PATCH 16/16] =?UTF-8?q?docs:=20README=20=EC=9E=90=EC=8B=9D=20?= =?UTF-8?q?=EC=84=A0=ED=83=9D=EC=9E=90=20=EC=A4=91=EC=B2=A9=20=EB=B0=8F=20?= =?UTF-8?q?=EB=A6=AC=ED=8C=A9=ED=86=A0=EB=A7=81=20=ED=95=99=EC=8A=B5=20?= =?UTF-8?q?=EB=82=B4=EC=9A=A9=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 110 +++++++++++++++++++++++ src/components/RestaurantDetailModal.jsx | 23 ++--- 2 files changed, 122 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 0674fb9..357e2a0 100644 --- a/README.md +++ b/README.md @@ -87,6 +87,50 @@ styled-components로 전환할 때 `label::after { content: "*" }`를 `FormItem` `styled(FormItem)`으로 확장한 `RequiredFormItem`을 별도로 만들어서 필수 항목(카테고리, 이름)에만 적용하는 방식으로 해결했다. +### 자식 선택자 중첩 — 언제 쓰고 언제 피할까 + +styled-components는 Sass처럼 중첩 선택자를 지원하지만, "항상 피해야 한다"기보다는 **상황에 따라 다르다**가 정확하다. + +**중첩이 괜찮은 경우** — 부모와 항상 함께 쓰이는 단순 HTML 요소일 때는 별도 컴포넌트로 분리하는 게 오히려 과도한 추상화다. + +```js +// img는 Category 없이 독립적으로 쓰일 일이 없으므로 중첩이 합리적 +const Category = styled.div` + background: var(--lighten-color); + + img { + width: 36px; + height: 36px; + } +`; +``` + +`&:hover`, `&:focus` 같은 가상 선택자도 중첩이 자연스럽고 관용적인 방식이다. + +**중첩을 피하는 게 나은 경우** — 자식 요소가 의미 있는 스타일을 가지거나, 독립적으로 재사용될 가능성이 있을 때다. + +```js +// h3/p는 각자 의미 있는 스타일을 가지므로 분리 +const RestaurantName = styled.h3` + font-size: 18px; + font-weight: 600; +`; + +const RestaurantDescription = styled.p` + padding-top: 8px; + -webkit-line-clamp: 2; +`; +``` + +**판단 기준 요약** + +| | 중첩 OK | 분리 권장 | +|---|---|---| +| 스타일 복잡도 | 단순 (크기, 색상 1-2개) | 복잡한 스타일 블록 | +| 재사용 가능성 | 부모 없이 쓰일 일 없음 | 다른 곳에서도 쓰일 수 있음 | +| 조건부 스타일 | 없음 | props로 분기 필요 | +| 의미 전달 | 이름 필요 없음 | 이름이 코드 이해에 도움됨 | + --- ## 🛠 리팩토링 @@ -106,3 +150,69 @@ after: components/ Header.jsx ``` + +### 자식 태그 선택자 → 별도 styled 컴포넌트로 분리 + +의미 있는 콘텐츠 요소에 자식 태그 선택자를 사용하던 방식을 각각 별도의 styled 컴포넌트로 분리했다. + +```js +// before — 자식 태그 선택자 사용 +const Info = styled.div` + h3 { font-size: 18px; font-weight: 600; } + p { padding-top: 8px; } +`; + +// after — 각각 명시적인 컴포넌트로 분리 +const Info = styled.div`...`; +const RestaurantName = styled.h3`font-size: 18px; font-weight: 600;`; +const RestaurantDescription = styled.p`padding-top: 8px;`; +``` + +적용 파일: `CategoryFilter`, `RestaurantList`, `AddRestaurantModal` + +### RestaurantList 시맨틱 구조 개선 + +`styled.section`이 일반 `
    `을 감싸는 이중 구조를 `styled.ul`로 단일화했다. + +```jsx +// before — section > ul > li 이중 구조 +const List = styled.section`...`; +return ( + +
      + {/* styled.li */} +
    +
    +); + +// after — ul > li 단일 구조 +const List = styled.ul`...`; +return ( + + {/* styled.li */} + +); +``` + +### CSS 변수 누락 색상 추가 및 하드코딩 제거 + +코드에 하드코딩되어 있던 색상값을 CSS 변수로 정의하고 교체했다. + +```css +/* App.css에 추가 */ +--grey-50: #fcfcfd; /* 헤더 타이틀 텍스트 색상 */ +--grey-150: #e9eaed; /* 목록 구분선 색상 */ +``` + +| 파일 | before | after | +|---|---|---| +| `Header.jsx` | `color: #fcfcfd` | `color: var(--grey-50)` | +| `CategoryFilter.jsx` | `border: 1px solid #d0d5dd` | `border: 1px solid var(--grey-200)` | +| `RestaurantList.jsx` | `border-bottom: 1px solid #e9eaed` | `border-bottom: 1px solid var(--grey-150)` | + +이 작업의 배경 개념은 **Design Token(디자인 토큰)** 이다. 색상, 폰트 크기, 간격 같은 시각적 결정에 이름을 붙인 값으로 추상화한 것으로, `App.css`의 `:root` 블록이 바로 그 역할을 한다. + +- **단일 진실 공급원** — 브랜드 색상이 바뀌면 변수 한 줄만 수정하면 전체에 반영된다. +- **색상 분열 방지** — 변수 체계 없이 하드코딩하면 개발자마다 "비슷한 회색"을 다르게 써서 미묘하게 다른 색상이 혼재하게 된다. 이번에 발견한 `#fcfcfd`, `#e9eaed`가 그 사례이다. +- **디자인-개발 소통** — 디자이너가 "grey-200 사용"이라고 전달하면 개발자는 `var(--grey-200)`을 그대로 쓴다. 값이 아닌 이름으로 소통하므로 오역이 줄어든다. +- **의미 전달** — `#ec4a0a`는 어떤 색인지 알 수 없지만 `--primary-color`는 의도가 명확하다. diff --git a/src/components/RestaurantDetailModal.jsx b/src/components/RestaurantDetailModal.jsx index 15e003f..fd82ba9 100644 --- a/src/components/RestaurantDetailModal.jsx +++ b/src/components/RestaurantDetailModal.jsx @@ -1,14 +1,5 @@ import styled from "styled-components"; import Modal from "./Modal.jsx"; -const RestaurantInfo = styled.div` - margin-bottom: 24px; -`; - -const Description = styled.p` - font-size: 16px; - line-height: 24px; - font-weight: 400; -`; const ButtonContainer = styled.div` display: flex; @@ -20,12 +11,22 @@ const Button = styled.button` margin-right: 16px; border: none; border-radius: 8px; + font-size: 14px; + line-height: 20px; font-weight: 600; cursor: pointer; background: var(--primary-color); color: var(--grey-100); - font-size: 14px; - line-height: 20px; +`; + +const RestaurantInfo = styled.div` + margin-bottom: 24px; +`; + +const Description = styled.p` + font-size: 16px; + line-height: 24px; + font-weight: 400; `; export default function RestaurantDetailModal({ restaurant, onClose }) {