From 942616f2ef4420e0518a17d0b7a151c6ede26566 Mon Sep 17 00:00:00 2001 From: Yeryeong Kang Date: Tue, 16 Jun 2026 01:02:04 +0900 Subject: [PATCH 01/16] =?UTF-8?q?chore:=20styled-components=20=ED=8C=A8?= =?UTF-8?q?=ED=82=A4=EC=A7=80=20=EC=84=A4=EC=B9=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package-lock.json | 103 +++++++++++++++++++++++++++++++++++++++++++--- package.json | 3 +- 2 files changed, 100 insertions(+), 6 deletions(-) diff --git a/package-lock.json b/package-lock.json index a5fc36a..e9995c3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,8 @@ "version": "0.0.0", "dependencies": { "react": "^18.2.0", - "react-dom": "^18.2.0" + "react-dom": "^18.2.0", + "styled-components": "^6.4.2" }, "devDependencies": { "@types/react": "^18.2.66", @@ -372,6 +373,21 @@ "node": ">=6.9.0" } }, + "node_modules/@emotion/is-prop-valid": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.4.0.tgz", + "integrity": "sha512-QgD4fyscGcbbKwJmqNvUMSE02OsHUa+lAWKdEUIJKgqe5IwRSKd7+KhibEWdaKwgjLj0DRSHA9biAIqGBk05lw==", + "license": "MIT", + "dependencies": { + "@emotion/memoize": "^0.9.0" + } + }, + "node_modules/@emotion/memoize": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.9.0.tgz", + "integrity": "sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ==", + "license": "MIT" + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.20.2", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.20.2.tgz", @@ -1513,6 +1529,15 @@ "node": ">=6" } }, + "node_modules/camelize": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/camelize/-/camelize-1.0.1.tgz", + "integrity": "sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/caniuse-lite": { "version": "1.0.30001603", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001603.tgz", @@ -1588,11 +1613,31 @@ "node": ">= 8" } }, + "node_modules/css-color-keywords": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/css-color-keywords/-/css-color-keywords-1.0.0.tgz", + "integrity": "sha512-FyyrDHZKEjXDpNJYvVsV960FiqQyXc/LlYmsxl2BcdMb2WPx0OGRVgTg55rPSyLSNMqP52R9r8geSp7apN3Ofg==", + "license": "ISC", + "engines": { + "node": ">=4" + } + }, + "node_modules/css-to-react-native": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/css-to-react-native/-/css-to-react-native-3.2.0.tgz", + "integrity": "sha512-e8RKaLXMOFii+02mOlqwjbD00KSEKqblnpO9e++1aXS1fPQOpS1YoqdVHBqPjHNoxeF2mimzVqawm2KCbEdtHQ==", + "license": "MIT", + "dependencies": { + "camelize": "^1.0.0", + "css-color-keywords": "^1.0.0", + "postcss-value-parser": "^4.0.2" + } + }, "node_modules/csstype": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", - "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", - "dev": true + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "license": "MIT" }, "node_modules/data-view-buffer": { "version": "1.0.1", @@ -3444,6 +3489,12 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "license": "MIT" + }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -3909,6 +3960,48 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/styled-components": { + "version": "6.4.2", + "resolved": "https://registry.npmjs.org/styled-components/-/styled-components-6.4.2.tgz", + "integrity": "sha512-xZBhBJsMtGqb+aKcwKgaT+BtuFums9VynX2JRvXJGTx5UfZzN12rk5r4nVdhXYvRw+hE7yiYxVrOqJZaK2+Txg==", + "license": "MIT", + "dependencies": { + "@emotion/is-prop-valid": "1.4.0", + "css-to-react-native": "3.2.0", + "csstype": "3.2.3", + "stylis": "4.3.6" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/styled-components" + }, + "peerDependencies": { + "css-to-react-native": ">= 3.2.0", + "react": ">= 16.8.0", + "react-dom": ">= 16.8.0", + "react-native": ">= 0.68.0" + }, + "peerDependenciesMeta": { + "css-to-react-native": { + "optional": true + }, + "react-dom": { + "optional": true + }, + "react-native": { + "optional": true + } + } + }, + "node_modules/stylis": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.3.6.tgz", + "integrity": "sha512-yQ3rwFWRfwNUY7H5vpU0wfdkNSnvnJinhF9830Swlaxl03zsOjCfmX0ugac+3LtK0lYSgwL/KXc8oYL3mG4YFQ==", + "license": "MIT" + }, "node_modules/supports-color": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", diff --git a/package.json b/package.json index 8d4410d..f236331 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,8 @@ }, "dependencies": { "react": "^18.2.0", - "react-dom": "^18.2.0" + "react-dom": "^18.2.0", + "styled-components": "^6.4.2" }, "devDependencies": { "@types/react": "^18.2.66", From c43c3d82cd52398927953414c2dec383d88fbd74 Mon Sep 17 00:00:00 2001 From: Yeryeong Kang Date: Tue, 16 Jun 2026 01:02:20 +0900 Subject: [PATCH 02/16] =?UTF-8?q?refactor:=20Header=20styled-components?= =?UTF-8?q?=EB=A1=9C=20=EC=A0=84=ED=99=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/Header/Header.jsx | 55 +++++++++++++++++++------ src/components/Header/Header.module.css | 32 -------------- 2 files changed, 43 insertions(+), 44 deletions(-) delete mode 100644 src/components/Header/Header.module.css diff --git a/src/components/Header/Header.jsx b/src/components/Header/Header.jsx index e414039..3d21c38 100644 --- a/src/components/Header/Header.jsx +++ b/src/components/Header/Header.jsx @@ -1,18 +1,49 @@ -import styles from "./Header.module.css"; import addButtonImg from "../../assets/add-button.png"; +import styled from "styled-components"; + +const Gnb = styled.header` + display: flex; + justify-content: space-between; + align-items: center; + height: 64px; + + padding: 0 16px; + + background-color: var(--primary-color); +`; + +const GnbTitle = styled.h1` + font-size: 20px; + line-height: 24px; + font-weight: 600; + color: #fcfcfd; +`; + +const GnbButton = styled.button` + height: 40px; + + border: none; + border-radius: 8px; + background: transparent; + + font-size: 24px; + cursor: pointer; +`; + +const GnbButtonImg = styled.img` + display: block; + width: 40px; + height: 40px; + object-fit: contain; +`; export default function Header({ onClick }) { return ( -
-

점심 뭐 먹지

- -
+ + 점심 뭐 먹지 + + + + ); } diff --git a/src/components/Header/Header.module.css b/src/components/Header/Header.module.css deleted file mode 100644 index 32df2e1..0000000 --- a/src/components/Header/Header.module.css +++ /dev/null @@ -1,32 +0,0 @@ -.gnb { - display: flex; - justify-content: space-between; - align-items: center; - height: 64px; - - padding: 0 16px; - - background-color: var(--primary-color); -} - -.gnb__title { - color: #fcfcfd; -} - -.gnb__button { - height: 40px; - - border: none; - border-radius: 8px; - background: transparent; - - font-size: 24px; - cursor: pointer; -} - -.gnb__button img { - display: block; - width: 40px; - height: 40px; - object-fit: contain; -} From 699674049202cdcfa29f7b811f3ab5db7ec09507 Mon Sep 17 00:00:00 2001 From: Yeryeong Kang Date: Tue, 16 Jun 2026 01:02:37 +0900 Subject: [PATCH 03/16] =?UTF-8?q?refactor:=20CategoryFilter=20styled-compo?= =?UTF-8?q?nents=EB=A1=9C=20=EC=A0=84=ED=99=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../CategoryFilter/CategoryFilter.jsx | 35 +++++++++++++++---- .../CategoryFilter/CategoryFilter.module.css | 22 ------------ 2 files changed, 28 insertions(+), 29 deletions(-) delete mode 100644 src/components/CategoryFilter/CategoryFilter.module.css diff --git a/src/components/CategoryFilter/CategoryFilter.jsx b/src/components/CategoryFilter/CategoryFilter.jsx index 651dbd2..2966076 100644 --- a/src/components/CategoryFilter/CategoryFilter.jsx +++ b/src/components/CategoryFilter/CategoryFilter.jsx @@ -1,21 +1,42 @@ -import styles from "./CategoryFilter.module.css"; import { FILTER_OPTIONS } from "../../constants/categories"; +import styled from "styled-components"; + +const FilterContainer = styled.section` + display: flex; + justify-content: space-between; + + padding: 0 16px; + margin-top: 24px; +`; + +const FilterSelect = styled.select` + height: 44px; + min-width: 125px; + + border: 1px solid #d0d5dd; + border-radius: 8px; + background: transparent; + + font-size: 16px; + padding: 8px; +`; export default function CategoryFilter({ category, onChangeCategory }) { return ( -
- -
+ + ); } diff --git a/src/components/CategoryFilter/CategoryFilter.module.css b/src/components/CategoryFilter/CategoryFilter.module.css deleted file mode 100644 index 46eda3d..0000000 --- a/src/components/CategoryFilter/CategoryFilter.module.css +++ /dev/null @@ -1,22 +0,0 @@ -.restaurant-filter__container { - display: flex; - justify-content: space-between; - - padding: 0 16px; - margin-top: 24px; -} - -.restaurant-filter__container select { - height: 44px; - min-width: 125px; - - border: 1px solid #d0d5dd; - border-radius: 8px; - background: transparent; - - font-size: 16px; -} - -.restaurant-filter { - padding: 8px; -} From bbff309915a7855bfb35b57703c03d820651f823 Mon Sep 17 00:00:00 2001 From: Yeryeong Kang Date: Tue, 16 Jun 2026 01:02:53 +0900 Subject: [PATCH 04/16] =?UTF-8?q?refactor:=20RestaurantList=20styled-compo?= =?UTF-8?q?nents=EB=A1=9C=20=EC=A0=84=ED=99=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../RestaurantList/RestaurantList.jsx | 111 ++++++++++++++---- .../RestaurantList/RestaurantList.module.css | 71 ----------- 2 files changed, 88 insertions(+), 94 deletions(-) delete mode 100644 src/components/RestaurantList/RestaurantList.module.css diff --git a/src/components/RestaurantList/RestaurantList.jsx b/src/components/RestaurantList/RestaurantList.jsx index 45ae6c8..1009a82 100644 --- a/src/components/RestaurantList/RestaurantList.jsx +++ b/src/components/RestaurantList/RestaurantList.jsx @@ -1,35 +1,100 @@ -import styles from "./RestaurantList.module.css"; import { CATEGORY_IMAGES } from "../../constants/categoryImages"; +import styled from "styled-components"; + +const ListContainer = styled.section` + display: flex; + flex-direction: column; + + padding: 0 16px; + margin: 16px 0; +`; + +const RestaurantUl = styled.ul``; + +const Restaurant = styled.li` + padding: 16px 8px; + border-bottom: 1px solid #e9eaed; +`; + +const RestaurantButton = styled.button` + display: flex; + align-items: flex-start; + width: 100%; + + background: none; + border: none; + padding: 0; + cursor: pointer; + text-align: left; + font: inherit; +`; + +const RestaurantCategory = styled.div` + display: flex; + justify-content: center; + align-items: center; + width: 64px; + height: 64px; + min-width: 64px; + min-height: 64px; + margin-right: 16px; + border-radius: 50%; + background: var(--lighten-color); +`; + +const CategoryIcon = styled.img` + width: 36px; + height: 36px; +`; + +const RestaurantInfo = styled.div` + display: flex; + flex-direction: column; + justify-content: flex-start; +`; + +const RestaurantName = styled.h3` + margin: 0; + font-size: 18px; + line-height: 28px; + font-weight: 600; +`; + +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) => ( -
  • - -
  • + + + + ))} -
-
+ + ); } diff --git a/src/components/RestaurantList/RestaurantList.module.css b/src/components/RestaurantList/RestaurantList.module.css deleted file mode 100644 index 1d91c5d..0000000 --- a/src/components/RestaurantList/RestaurantList.module.css +++ /dev/null @@ -1,71 +0,0 @@ -.restaurant-list__container { - display: flex; - flex-direction: column; - - padding: 0 16px; - margin: 16px 0; -} - -.restaurant { - padding: 16px 8px; - - border-bottom: 1px solid #e9eaed; -} - -.restaurant__button { - display: flex; - align-items: flex-start; - width: 100%; - - background: none; - border: none; - padding: 0; - cursor: pointer; - text-align: left; - font: inherit; -} - -.restaurant__category { - display: flex; - justify-content: center; - align-items: center; - width: 64px; - height: 64px; - min-width: 64px; - min-height: 64px; - - margin-right: 16px; - - border-radius: 50%; - background: var(--lighten-color); -} - -.restaurant__category-icon { - width: 36px; - height: 36px; -} - -.restaurant__info { - display: flex; - flex-direction: column; - justify-content: flex-start; -} - -.restaurant__name { - margin: 0; -} - -.restaurant__distance { - color: var(--primary-color); -} - -.restaurant__description { - display: -webkit-box; - - padding-top: 8px; - - overflow: hidden; - text-overflow: ellipsis; - -webkit-line-clamp: 2; - -webkit-box-orient: vertical; -} From 1dc5e96d3e3400c04d0599bd07d9cfbc64a44cca Mon Sep 17 00:00:00 2001 From: Yeryeong Kang Date: Tue, 16 Jun 2026 01:03:08 +0900 Subject: [PATCH 05/16] =?UTF-8?q?refactor:=20Modal=20styled-components?= =?UTF-8?q?=EB=A1=9C=20=EC=A0=84=ED=99=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/Modal/AddRestaurantModal.jsx | 135 +++++++++++++----- .../Modal/AddRestaurantModal.module.css | 54 ------- src/components/Modal/Modal.jsx | 35 ++++- src/components/Modal/Modal.module.css | 70 --------- .../Modal/RestaurantDetailModal.jsx | 57 ++++++-- 5 files changed, 178 insertions(+), 173 deletions(-) delete mode 100644 src/components/Modal/AddRestaurantModal.module.css delete mode 100644 src/components/Modal/Modal.module.css diff --git a/src/components/Modal/AddRestaurantModal.jsx b/src/components/Modal/AddRestaurantModal.jsx index d813278..8e54ac0 100644 --- a/src/components/Modal/AddRestaurantModal.jsx +++ b/src/components/Modal/AddRestaurantModal.jsx @@ -1,8 +1,95 @@ -import modalStyles from "./Modal.module.css"; -import formStyles from "./AddRestaurantModal.module.css"; import { CATEGORIES } from "../../constants/categories"; import { useState } from "react"; import Modal from "./Modal"; +import styled from "styled-components"; + +const FormItem = styled.div` + display: flex; + flex-direction: column; + margin-bottom: 36px; + + label { + color: var(--grey-400); + font-size: 14px; + } + + input, + select { + padding: 8px; + margin: 6px 0; + border: 1px solid var(--grey-200); + border-radius: 8px; + font-size: 16px; + } + + textarea { + padding: 8px; + margin: 6px 0; + border: 1px solid var(--grey-200); + border-radius: 8px; + font-size: 16px; + resize: none; + } + + select { + height: 44px; + padding: 8px; + border: 1px solid var(--grey-200); + border-radius: 8px; + color: var(--grey-300); + } + + input[name="name"] { + height: 44px; + } + + ${(props) => + props.$required && + `label::after {padding-left: 4px; + color: var(--primary-color); + content: "*";}`} +`; + +const FormHelpText = styled.span` + color: var(--grey-300); + font-size: 14px; + line-height: 20px; + font-weight: 400; +`; + +const TextCaption = styled.label` + font-size: 14px; + line-height: 20px; + font-weight: 400; +`; + +const ModalButtonContainer = styled.div` + display: flex; +`; + +const Button = styled.button` + width: 100%; + height: 44px; + margin-right: 16px; + border: none; + border-radius: 8px; + cursor: pointer; + + ${(props) => + props.$primary && + ` + background: var(--primary-color); + color: var(--grey-100); + `} + + &:last-child { + margin-right: 0; + } + + font-size: 14px; + line-height: 20px; + font-weight: 400; +`; export default function AddRestaurantModal({ onSubmit, onClose }) { const [category, setCategory] = useState(""); @@ -18,12 +105,8 @@ export default function AddRestaurantModal({ onSubmit, onClose }) {
{/* 카테고리 */} -
- + + 카테고리 -
+ {/* 음식점 이름 */} -
- + + 이름 -
+ {/* 설명 */} -
- + + 설명 - - 메뉴 등 추가 정보를 입력해 주세요. - -
+ 메뉴 등 추가 정보를 입력해 주세요. + {/* 추가 버튼 */} -
- -
+ + +
); diff --git a/src/components/Modal/AddRestaurantModal.module.css b/src/components/Modal/AddRestaurantModal.module.css deleted file mode 100644 index 82a980e..0000000 --- a/src/components/Modal/AddRestaurantModal.module.css +++ /dev/null @@ -1,54 +0,0 @@ -.form-item { - display: flex; - flex-direction: column; - - margin-bottom: 36px; -} - -.form-item label { - color: var(--grey-400); - font-size: 14px; -} - -.form-item--required label::after { - padding-left: 4px; - - color: var(--primary-color); - content: "*"; -} - -.form-item__help-text { - color: var(--grey-300); -} - -.form-item input, -.form-item textarea, -.form-item select { - padding: 8px; - margin: 6px 0; - - border: 1px solid var(--grey-200); - border-radius: 8px; - - font-size: 16px; -} - -.form-item textarea { - resize: none; -} - -.form-item 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; -} diff --git a/src/components/Modal/Modal.jsx b/src/components/Modal/Modal.jsx index 456873b..c0f55b6 100644 --- a/src/components/Modal/Modal.jsx +++ b/src/components/Modal/Modal.jsx @@ -1,13 +1,38 @@ -import modalStyles from "./Modal.module.css"; +import styled from "styled-components"; + +const ModalBackdrop = styled.div` + position: fixed; + top: 0; + right: 0; + bottom: 0; + left: 0; + background: rgba(0, 0, 0, 0.35); +`; + +const ModalContainer = styled.div` + position: fixed; + bottom: 0; + width: 100%; + padding: 32px 16px; + border-radius: 8px 8px 0px 0px; + background: var(--grey-100); +`; + +const ModalTitle = styled.h2` + margin-bottom: 36px; + font-size: 20px; + line-height: 24px; + font-weight: 600; +`; export default function Modal({ title, onClose, children }) { return ( <> -
-
-

{title}

+ + + {title} {children} -
+ ); } diff --git a/src/components/Modal/Modal.module.css b/src/components/Modal/Modal.module.css deleted file mode 100644 index 38cfd97..0000000 --- a/src/components/Modal/Modal.module.css +++ /dev/null @@ -1,70 +0,0 @@ -.modal { - display: none; -} - -.modal--open { - display: block; -} - -.modal__backdrop { - position: fixed; - top: 0; - right: 0; - bottom: 0; - left: 0; - - background: rgba(0, 0, 0, 0.35); -} - -.modal__container { - position: fixed; - bottom: 0; - width: 100%; - - padding: 32px 16px; - - border-radius: 8px 8px 0px 0px; - background: var(--grey-100); -} - -.modal__title { - margin-bottom: 36px; -} - -.modal__button-container { - display: flex; -} - -.button { - width: 100%; - height: 44px; - - margin-right: 16px; - - border: none; - border-radius: 8px; - - font-weight: 600; - cursor: pointer; -} - -.button:last-child { - margin-right: 0; -} - -.button--secondary { - border: 1px solid var(--grey-300); - background: transparent; - - color: var(--grey-300); -} - -.button--primary { - background: var(--primary-color); - - color: var(--grey-100); -} - -.modal__restaurant-info { - margin-bottom: 24px; -} diff --git a/src/components/Modal/RestaurantDetailModal.jsx b/src/components/Modal/RestaurantDetailModal.jsx index fcc38a3..9661c5a 100644 --- a/src/components/Modal/RestaurantDetailModal.jsx +++ b/src/components/Modal/RestaurantDetailModal.jsx @@ -1,21 +1,56 @@ -import styles from "./Modal.module.css"; import Modal from "./Modal"; +import styled from "styled-components"; + +const ModalInfo = styled.div` + margin-bottom: 24px; +`; + +const ModalButtonContainer = styled.div` + display: flex; +`; + +const TextBody = styled.p` + font-size: 16px; + line-height: 24px; + font-weight: 400; +`; + +const Button = styled.button` + width: 100%; + height: 44px; + margin-right: 16px; + border: none; + border-radius: 8px; + cursor: pointer; + + ${(props) => + props.$primary && + ` + background: var(--primary-color); + color: var(--grey-100); + `} + + &:last-child { + margin-right: 0; + } + + font-size: 14px; + line-height: 20px; + font-weight: 400; +`; export default function RestaurantDetailModal({ clickedRestaurant, onClose }) { return ( -
-

{clickedRestaurant.description}

-
+ + {clickedRestaurant.description} + {/* 닫기 버튼 */} -
- -
+ +
); } From 541f2a58217dccf3ed327fa5c381abaa9847ebbe Mon Sep 17 00:00:00 2001 From: Yeryeong Kang Date: Tue, 16 Jun 2026 01:03:23 +0900 Subject: [PATCH 06/16] =?UTF-8?q?style:=20App.css=20Typography=20=EC=84=B9?= =?UTF-8?q?=EC=85=98=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 | 25 ------------------------- 1 file changed, 25 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; -} From 4aa4e145c7c6ce7d827d0676d80afcabe53431a7 Mon Sep 17 00:00:00 2001 From: Yeryeong Kang Date: Tue, 16 Jun 2026 11:16:13 +0900 Subject: [PATCH 07/16] =?UTF-8?q?refactor:=20typography.js=20=EC=B6=94?= =?UTF-8?q?=EC=B6=9C=20=EB=B0=8F=20=EA=B0=81=20=EC=BB=B4=ED=8F=AC=EB=84=8C?= =?UTF-8?q?=ED=8A=B8=EC=97=90=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../CategoryFilter/CategoryFilter.jsx | 6 ++--- src/components/Header/Header.jsx | 9 ++----- src/components/Modal/AddRestaurantModal.jsx | 15 ++++------- src/components/Modal/Modal.jsx | 5 ++-- .../Modal/RestaurantDetailModal.jsx | 9 +++---- .../RestaurantList/RestaurantList.jsx | 9 +++---- src/styles/typography.js | 25 +++++++++++++++++++ 7 files changed, 42 insertions(+), 36 deletions(-) create mode 100644 src/styles/typography.js diff --git a/src/components/CategoryFilter/CategoryFilter.jsx b/src/components/CategoryFilter/CategoryFilter.jsx index 2966076..3db552d 100644 --- a/src/components/CategoryFilter/CategoryFilter.jsx +++ b/src/components/CategoryFilter/CategoryFilter.jsx @@ -1,10 +1,10 @@ import { FILTER_OPTIONS } from "../../constants/categories"; import styled from "styled-components"; +import { textBody } from "../../styles/typography"; const FilterContainer = styled.section` display: flex; justify-content: space-between; - padding: 0 16px; margin-top: 24px; `; @@ -12,12 +12,10 @@ const FilterContainer = styled.section` const FilterSelect = styled.select` height: 44px; min-width: 125px; - border: 1px solid #d0d5dd; border-radius: 8px; background: transparent; - - font-size: 16px; + ${textBody} padding: 8px; `; diff --git a/src/components/Header/Header.jsx b/src/components/Header/Header.jsx index 3d21c38..6a75a87 100644 --- a/src/components/Header/Header.jsx +++ b/src/components/Header/Header.jsx @@ -1,31 +1,26 @@ import addButtonImg from "../../assets/add-button.png"; import styled from "styled-components"; +import { textTitle } from "../../styles/typography"; const Gnb = styled.header` display: flex; justify-content: space-between; align-items: center; height: 64px; - padding: 0 16px; - background-color: var(--primary-color); `; const GnbTitle = styled.h1` - font-size: 20px; - line-height: 24px; - font-weight: 600; + ${textTitle} color: #fcfcfd; `; const GnbButton = styled.button` height: 40px; - border: none; border-radius: 8px; background: transparent; - font-size: 24px; cursor: pointer; `; diff --git a/src/components/Modal/AddRestaurantModal.jsx b/src/components/Modal/AddRestaurantModal.jsx index 8e54ac0..0da9582 100644 --- a/src/components/Modal/AddRestaurantModal.jsx +++ b/src/components/Modal/AddRestaurantModal.jsx @@ -2,6 +2,7 @@ import { CATEGORIES } from "../../constants/categories"; import { useState } from "react"; import Modal from "./Modal"; import styled from "styled-components"; +import { textCaption } from "../../styles/typography"; const FormItem = styled.div` display: flex; @@ -10,7 +11,7 @@ const FormItem = styled.div` label { color: var(--grey-400); - font-size: 14px; + ${textCaption} } input, @@ -52,15 +53,11 @@ const FormItem = styled.div` const FormHelpText = styled.span` color: var(--grey-300); - font-size: 14px; - line-height: 20px; - font-weight: 400; + ${textCaption} `; const TextCaption = styled.label` - font-size: 14px; - line-height: 20px; - font-weight: 400; + ${textCaption} `; const ModalButtonContainer = styled.div` @@ -86,9 +83,7 @@ const Button = styled.button` margin-right: 0; } - font-size: 14px; - line-height: 20px; - font-weight: 400; + ${textCaption} `; export default function AddRestaurantModal({ onSubmit, onClose }) { diff --git a/src/components/Modal/Modal.jsx b/src/components/Modal/Modal.jsx index c0f55b6..b8a5d99 100644 --- a/src/components/Modal/Modal.jsx +++ b/src/components/Modal/Modal.jsx @@ -1,4 +1,5 @@ import styled from "styled-components"; +import { textTitle } from "../../styles/typography"; const ModalBackdrop = styled.div` position: fixed; @@ -20,9 +21,7 @@ const ModalContainer = styled.div` const ModalTitle = styled.h2` margin-bottom: 36px; - font-size: 20px; - line-height: 24px; - font-weight: 600; + ${textTitle} `; export default function Modal({ title, onClose, children }) { diff --git a/src/components/Modal/RestaurantDetailModal.jsx b/src/components/Modal/RestaurantDetailModal.jsx index 9661c5a..5a6180e 100644 --- a/src/components/Modal/RestaurantDetailModal.jsx +++ b/src/components/Modal/RestaurantDetailModal.jsx @@ -1,5 +1,6 @@ import Modal from "./Modal"; import styled from "styled-components"; +import { textBody, textCaption } from "../../styles/typography"; const ModalInfo = styled.div` margin-bottom: 24px; @@ -10,9 +11,7 @@ const ModalButtonContainer = styled.div` `; const TextBody = styled.p` - font-size: 16px; - line-height: 24px; - font-weight: 400; + ${textBody} `; const Button = styled.button` @@ -34,9 +33,7 @@ const Button = styled.button` margin-right: 0; } - font-size: 14px; - line-height: 20px; - font-weight: 400; + ${textCaption} `; export default function RestaurantDetailModal({ clickedRestaurant, onClose }) { diff --git a/src/components/RestaurantList/RestaurantList.jsx b/src/components/RestaurantList/RestaurantList.jsx index 1009a82..79c4c65 100644 --- a/src/components/RestaurantList/RestaurantList.jsx +++ b/src/components/RestaurantList/RestaurantList.jsx @@ -1,5 +1,6 @@ import { CATEGORY_IMAGES } from "../../constants/categoryImages"; import styled from "styled-components"; +import { textSubtitle, textBody } from "../../styles/typography"; const ListContainer = styled.section` display: flex; @@ -55,9 +56,7 @@ const RestaurantInfo = styled.div` const RestaurantName = styled.h3` margin: 0; - font-size: 18px; - line-height: 28px; - font-weight: 600; + ${textSubtitle} `; const RestaurantDescription = styled.p` @@ -67,9 +66,7 @@ const RestaurantDescription = styled.p` text-overflow: ellipsis; -webkit-line-clamp: 2; -webkit-box-orient: vertical; - font-size: 16px; - line-height: 24px; - font-weight: 400; + ${textBody} `; export default function RestaurantList({ restaurants, onRestaurantClick }) { diff --git a/src/styles/typography.js b/src/styles/typography.js new file mode 100644 index 0000000..e02d084 --- /dev/null +++ b/src/styles/typography.js @@ -0,0 +1,25 @@ +import { css } from "styled-components"; + +export const textTitle = css` + font-size: 20px; + line-height: 24px; + font-weight: 600; +`; + +export const textSubtitle = css` + font-size: 18px; + line-height: 28px; + font-weight: 600; +`; + +export const textBody = css` + font-size: 16px; + line-height: 24px; + font-weight: 400; +`; + +export const textCaption = css` + font-size: 14px; + line-height: 20px; + font-weight: 400; +`; From 274c1468c818757a64015bf1d305bda59f9605a3 Mon Sep 17 00:00:00 2001 From: Yeryeong Kang Date: Tue, 16 Jun 2026 11:23:10 +0900 Subject: [PATCH 08/16] =?UTF-8?q?refactor:=20Button,=20ModalButtonContaine?= =?UTF-8?q?r=20=EC=A4=91=EB=B3=B5=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/Modal/AddRestaurantModal.jsx | 28 +---------------- src/components/Modal/Modal.jsx | 28 ++++++++++++++++- .../Modal/RestaurantDetailModal.jsx | 30 ++----------------- 3 files changed, 30 insertions(+), 56 deletions(-) diff --git a/src/components/Modal/AddRestaurantModal.jsx b/src/components/Modal/AddRestaurantModal.jsx index 0da9582..ac03be0 100644 --- a/src/components/Modal/AddRestaurantModal.jsx +++ b/src/components/Modal/AddRestaurantModal.jsx @@ -1,6 +1,6 @@ import { CATEGORIES } from "../../constants/categories"; import { useState } from "react"; -import Modal from "./Modal"; +import Modal, { Button, ModalButtonContainer } from "./Modal"; import styled from "styled-components"; import { textCaption } from "../../styles/typography"; @@ -60,32 +60,6 @@ const TextCaption = styled.label` ${textCaption} `; -const ModalButtonContainer = styled.div` - display: flex; -`; - -const Button = styled.button` - width: 100%; - height: 44px; - margin-right: 16px; - border: none; - border-radius: 8px; - cursor: pointer; - - ${(props) => - props.$primary && - ` - background: var(--primary-color); - color: var(--grey-100); - `} - - &:last-child { - margin-right: 0; - } - - ${textCaption} -`; - export default function AddRestaurantModal({ onSubmit, onClose }) { const [category, setCategory] = useState(""); const [name, setName] = useState(""); diff --git a/src/components/Modal/Modal.jsx b/src/components/Modal/Modal.jsx index b8a5d99..d64649b 100644 --- a/src/components/Modal/Modal.jsx +++ b/src/components/Modal/Modal.jsx @@ -1,5 +1,5 @@ import styled from "styled-components"; -import { textTitle } from "../../styles/typography"; +import { textTitle, textCaption } from "../../styles/typography"; const ModalBackdrop = styled.div` position: fixed; @@ -24,6 +24,32 @@ const ModalTitle = styled.h2` ${textTitle} `; +export const ModalButtonContainer = styled.div` + display: flex; +`; + +export const Button = styled.button` + width: 100%; + height: 44px; + margin-right: 16px; + border: none; + border-radius: 8px; + cursor: pointer; + + ${(props) => + props.$primary && + ` + background: var(--primary-color); + color: var(--grey-100); + `} + + &:last-child { + margin-right: 0; + } + + ${textCaption} +`; + export default function Modal({ title, onClose, children }) { return ( <> diff --git a/src/components/Modal/RestaurantDetailModal.jsx b/src/components/Modal/RestaurantDetailModal.jsx index 5a6180e..e53b280 100644 --- a/src/components/Modal/RestaurantDetailModal.jsx +++ b/src/components/Modal/RestaurantDetailModal.jsx @@ -1,41 +1,15 @@ -import Modal from "./Modal"; +import Modal, { Button, ModalButtonContainer } from "./Modal"; import styled from "styled-components"; -import { textBody, textCaption } from "../../styles/typography"; +import { textBody } from "../../styles/typography"; const ModalInfo = styled.div` margin-bottom: 24px; `; -const ModalButtonContainer = styled.div` - display: flex; -`; - const TextBody = styled.p` ${textBody} `; -const Button = styled.button` - width: 100%; - height: 44px; - margin-right: 16px; - border: none; - border-radius: 8px; - cursor: pointer; - - ${(props) => - props.$primary && - ` - background: var(--primary-color); - color: var(--grey-100); - `} - - &:last-child { - margin-right: 0; - } - - ${textCaption} -`; - export default function RestaurantDetailModal({ clickedRestaurant, onClose }) { return ( From a0ce07b1a7807d8524e68dbfbb4cc0aba7f7268a Mon Sep 17 00:00:00 2001 From: Yeryeong Kang Date: Tue, 16 Jun 2026 11:31:30 +0900 Subject: [PATCH 09/16] =?UTF-8?q?docs:=20styled-components=20=EB=AF=B8?= =?UTF-8?q?=EC=85=98=20README=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- 01-styled-components/README.md | 91 ++++++++ 05-effects/README.md | 56 ----- README.md | 367 +++++++-------------------------- 3 files changed, 166 insertions(+), 348 deletions(-) create mode 100644 01-styled-components/README.md delete mode 100644 05-effects/README.md diff --git a/01-styled-components/README.md b/01-styled-components/README.md new file mode 100644 index 0000000..3776be6 --- /dev/null +++ b/01-styled-components/README.md @@ -0,0 +1,91 @@ +# 01. styled-components를 적용해서 리팩토링하기 + +## 🎯 요구사항 + +- 초록스터디-`self-paced-react`의 step5 코드를 가져와 스타일링하는 미션입니다. +- styled-components 라이브러리를 이용해서 컴포넌트들에게 css를 입히고 스타일링 해보세요. +- styled-components를 왜 사용하는지, 별도의 css파일로 분리한 방법과 어떤 trade-off가 있는지 PR에 적어주세요. + - (선택) 브라우저가 웹 페이지를 렌더링하는 구조와 과정에 대해서도 공부해보세요. + - **🔑keywords** : DOM Tree, CSSOM Tree, Render Tree +- 프로젝트내 별도의 css파일은 존재하지 않아야합니다.❌ + - App.css는 허용되며 아래의 코드와 동일해야합니다. + +```css +* { + padding: 0; + margin: 0; + box-sizing: border-box; +} + +ul, +li { + list-style: none; +} + +html, +body { + font-family: sans-serif; + font-size: 16px; +} + +/* Colors *****************************************/ +:root { + --primary-color: #ec4a0a; + --lighten-color: #f6a88a; + --grey-100: #ffffff; + --grey-200: #d0d5dd; + --grey-300: #667085; + --grey-400: #344054; + --grey-500: #000000; +} +``` + +### 😗구현 예시 + +- 컴포넌트의 이름이나 구조는 마음대로 변경해도 좋습니다. + +```javascript +import React from "react"; +import styled from "styled-components"; + +// Button 컴포넌트를 styled-components로 정의 +const Button = styled.button` + background-color: #ec4a0a; + color: white; + border: none; + border-radius: 8px; + padding: 10px 20px; + font-size: 16px; + cursor: pointer; + + &:hover { + background-color: #f6a88a; + } +`; + +const App = () => { + return ( +
+

Hello, styled-components!

+ +
+ ); +}; + +export default App; +``` + +## ✅ 키워드 + +- **styled-component** + - Css in JS + - Scoped Styling + +## 🧙‍♀️ 진행 가이드 + +- 진행시간 : 1시간 내에 완료하는 것을 목표로 합니다. +- vscode 사용시 extension에서 `vscode-styled-components`를 설치해주세요. + +## 🔗 참고 문서 + +- [styled-components 공식문서](https://styled-components.com/docs) diff --git a/05-effects/README.md b/05-effects/README.md deleted file mode 100644 index ebebf77..0000000 --- a/05-effects/README.md +++ /dev/null @@ -1,56 +0,0 @@ -# 05. API 연동하기: side-effect(feat. effects) - -## 🎯 요구 사항 - -- API로 레스토랑 목록을 불러와 ``에 내려줍니다. - - 로딩 상태, 에러 상태 등은 고려하지 않습니다. -- 레스토랑 추가 모달에서 추가하기 버튼을 클릭하면 POST 요청을 보냅니다. 모달이 닫히고, 레스토랑 목록을 다시 불러옵니다. - -## ✅ 키워드 - -- effect (feat. side effect) - - useEffect - -## 🧙‍♀️ 진행 가이드 - -- 진행 시간: 2시간 내에 완료하는 것을 목표로 합니다. - -### `json-server`로 가짜 서버 띄워 활용하기 - -연습용 앱이기 때문에 [`json-server`](https://github.com/typicode/json-server)를 활용해 간단한 가짜 REST API를 구축해 사용합니다. - -- `npm run server`를 실행합니다. (혹은 `npx json-server db.json` 를 직접 실행해도 상관없습니다) -- `GET http://localhost:3000/restaurants`으로 `db.json`에 있는 레스토랑 목록을 불러올 수 있습니다. - -```javascript -// GET 예시 -const response = await fetch("http://localhost:3000/restaurants"); - -// POST 예시 -const response = await fetch("http://localhost:3000/restaurants", { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify(restaurant), -}); -``` - -## 🔗 참고 문서 - -- [Synchronizing with Effects](https://react.dev/learn/synchronizing-with-effects) - > Effects let you specify side effects that are caused by rendering itself, rather than by a particular event. - - [API Reference: useEffect](https://react.dev/reference/react/useEffect) - > useEffect is a React Hook that lets you synchronize a component with an external system. -- [API Reference: useSate > updater function](https://react.dev/reference/react/useState#updating-state-based-on-the-previous-state) - > `set` function에 함수를 넘겨주면 `updater function`으로 동작합니다. 함수가 아닌 값을 넘겨줄 때와 어떻게 다른지 알아보세요. - -구현을 다 해본 뒤에 Introduction에서 살펴보았던 설계 원칙과 관련해 조금 더 학습해보고 싶다면 아래 문서들도 추가로 확인해 보세요. - -- [You Might Not Need an Effect](https://react.dev/learn/you-might-not-need-an-effect) - > You do need Effects to synchronize with external systems. - > In React, data flows from the parent components to their children. -- [Components and Hooks must be pure](https://react.dev/reference/rules/components-and-hooks-must-be-pure) - > Purity in Components and Hooks is a key rule of React that makes your app predictable, easy to debug, and allows React to automatically optimize your code. - > Side effects should not run in render, as React can render components multiple times to create the best possible user experience. - > One important principle in React is local reasoning: the ability to understand what a component or hook does by looking at its code in isolation. Hooks should be treated like “black boxes” when they are called. diff --git a/README.md b/README.md index 0a708d8..672dc8c 100644 --- a/README.md +++ b/README.md @@ -1,346 +1,129 @@ -# Self-Paced React Step 5 +# styled-components를 적용해서 리팩토링하기 ## 🎯 개인 목표 및 목표 달성을 위한 행동 가이드 이번 미션을 통해 다음과 같은 학습 경험들을 쌓는 것을 목표로 한다. -**1. side effect와 useEffect 이해** - -컴포넌트 렌더링 자체가 아닌, 외부 시스템과의 동기화가 side effect임을 이해하고, -왜 useEffect 안에서 처리해야 하는지 설명할 수 있게 된다. - -**2. fetch를 통한 API 연동 흐름 이해** - -GET 요청으로 목록을 불러와 state에 저장하고, -POST 요청 후 목록을 다시 불러오는 전체 흐름을 직접 구현한다. - -**3. useEffect 의존성 배열 이해** - -빈 배열(`[]`)과 값이 있는 배열의 차이를 이해하고, -effect가 언제 실행되는지 의도적으로 제어할 수 있게 된다. +- styled-components의 기본 문법과 사용법을 익히고, CSS-in-JS 방식이 기존 별도 CSS 파일 방식과 어떤 차이가 있는지 직접 체감한다. +- 단순히 동작하는 코드를 넘어, 컴포넌트마다 스코프가 격리된 스타일을 작성하는 습관을 형성한다. +- styled-components를 처음 접하더라도 공식 문서를 스스로 찾아 읽고 적용하는 자기주도 학습 역량을 키운다. +- CSS 파일 방식 vs CSS-in-JS 방식의 trade-off를 정리해 PR에 나만의 언어로 서술한다. ## 📝 기능 구현 목록 -**1. API로 레스토랑 목록 불러오기** - -- 앱 마운트 시 GET 요청으로 레스토랑 목록을 불러온다. - -**2. 레스토랑 추가 시 POST 요청** - -- 추가하기 버튼 클릭 시 POST 요청을 보내고, 완료 후 목록을 다시 불러온다. +- `styled-components` 패키지 설치 +- 모든 컴포넌트의 CSS Module 파일을 제거하고 styled-components로 전환 + - `Header`, `CategoryFilter`, `RestaurantList` + - `Modal`, `AddRestaurantModal`, `RestaurantDetailModal` +- `props`를 활용한 조건부 스타일링 적용 (`$required`, `$primary`) +- `App.css`에서 Typography 유틸리티 클래스 제거 (각 컴포넌트 스타일로 이동) ## 📚 학습 내용 -### 1. side effect - -컴포넌트 함수는 순수해야 한다. 동일한 props/state를 받으면 항상 동일한 UI를 반환해야 하고, 렌더링 중에 외부에 영향을 주어서는 안 된다. API 호출, DOM 직접 조작, 타이머 설정처럼 외부 시스템과 상호작용하는 작업을 side effect라 한다. - -컴포넌트 함수는 렌더링할 때마다 실행된다. 거기에 API 호출을 직접 넣으면 렌더링될 때마다 요청이 날아가버린다. 그래서 side effect는 렌더링 함수 본문이 아닌, `useEffect`를 통해 렌더링 이후에 실행되도록 분리해야 한다. - -### 2. useEffect와 useCallback - -두 훅 모두 의존성 배열을 사용하지만 역할이 다르다. - -| 훅 | 역할 | 실행 시점 | -|---|---|---| -| `useEffect` | side effect 실행 | 의존성이 바뀔 때마다 안의 코드 실행 | -| `useCallback` | 함수 참조 유지 | 의존성이 바뀔 때만 함수를 새로 만들고, 그 외엔 이전 참조 반환 | - -**useEffect** - -외부 시스템과 동기화할 때 사용한다. 렌더링 이후 실행되며, 의존성 배열로 실행 시점을 제어한다. - -| 의존성 배열 | 실행 시점 | -|---|---| -| 없음 | 매 렌더링마다 | -| `[]` | 마운트 시 한 번 | -| `[value]` | 마운트 + value 변경 시 | - -**왜 같이 쓰는가** - -컴포넌트가 리렌더링될 때마다 함수는 새로 만들어져 참조(주소)가 달라진다. `useEffect` 의존성 배열에 함수를 넣으면, 리렌더링마다 참조가 바뀌어 effect가 반복 실행되는 무한루프가 생긴다. - -```js -// 리렌더링마다 새 참조 → 무한루프 -const fetchRestaurants = async () => { ... }; // 매번 0x001, 0x002... -useEffect(() => { fetchRestaurants(); }, [fetchRestaurants]); -``` - -`useCallback`으로 감싸면 의존성이 바뀌지 않는 한 같은 참조를 반환해 루프가 끊긴다. - -```js -const fetchRestaurants = useCallback(async () => { - const data = await getRestaurants(); - setNewRestaurants(data); -}, []); // 의존성 없음 → 항상 같은 참조 - -useEffect(() => { - void fetchRestaurants(); -}, [fetchRestaurants]); // 참조가 안 바뀌니 최초 1번만 실행 -``` - -**useCallback 의존성 배열** - -"이 함수가 읽는 값"을 넣는다. 함수 안에서 바뀔 수 있는 값을 참조하면 그 값이 의존성이 된다. - -```js -// [] — 외부 값을 읽지 않으므로 항상 같은 참조 -const fetchRestaurants = useCallback(async () => { - const data = await getRestaurants(); - setNewRestaurants(data); -}, []); - -// [category] — category가 바뀌면 함수를 새로 만들어야 함 -const fetchByCategory = useCallback(async () => { - const data = await getRestaurants(category); - setNewRestaurants(data); -}, [category]); -``` - -### 3. fetch / async / await - -**Promise** — 비동기 작업의 결과를 나타내는 객체다. 지금은 없지만 나중에 값을 주겠다는 약속이며, `await`로 그 값이 준비될 때까지 기다릴 수 있다. - -**fetch** — 브라우저 내장 함수로 HTTP 요청을 보낸다. 기본값은 GET이며, 두 번째 인자로 method, headers, body를 지정할 수 있다. Promise를 반환한다. - -**async / await** — `await`는 Promise가 이행될 때까지 기다린 뒤 결과값을 반환하며, `async`로 선언된 함수 안에서만 사용할 수 있다. `fetch`와 `response.json()` 모두 Promise를 반환하므로 둘 다 `await`가 필요하다. - -```jsx -const fetchRestaurants = async () => { - const response = await fetch("http://localhost:3000/restaurants"); - const data = await response.json(); // 응답 body를 JSON으로 파싱 - setRestaurants(data); -}; -``` - -### 4. 커스텀 훅 - -React hooks(`useState`, `useEffect` 등)를 사용하는 일반 함수다. 이름은 `use`로 시작해야 한다. 컴포넌트에서 데이터/로직을 분리할 때 사용한다. - -```js -export function useRestaurants() { - const [restaurants, setRestaurants] = useState([]); - // ...state, effect, 함수 정의 - - return { restaurants, registerRestaurant, error, isLoading }; -} - -// App에서 -const { restaurants, registerRestaurant } = useRestaurants(); -``` - -훅은 "어떻게 데이터를 다루는가"를 담당하고, 컴포넌트는 "어떻게 UI를 보여주는가"만 담당하도록 분리된다. - -### 5. api.js 분리 - -HTTP 요청은 React와 무관한 로직이다. `api.js`로 분리하면 컴포넌트/훅은 "어떻게 요청을 보내는지"를 몰라도 된다. - -```js -// api.js — HTTP만 담당 -export async function getRestaurants() { - const response = await fetch(`${BASE_URL}/restaurants`); - if (!response.ok) throw new Error("Failed to fetch"); - return response.json(); -} -``` - -`response.ok`가 `false`(4xx, 5xx)여도 `fetch`는 에러를 던지지 않는다. 수동으로 체크해서 throw해야 한다. +### 1. styled-components 기본 문법 +- `styled.태그명` 뒤에 백틱으로 CSS를 작성하면 해당 태그에 스타일이 적용된 React 컴포넌트가 만들어진다. +- 백틱 안에서 자식 요소 선택자(`label { }`, `input { }`)를 중첩해서 쓸 수 있다. styled-components가 컴파일 시 고유 클래스명(예: `.sc-abc123`)을 생성하고, 중첩 선택자를 `.sc-abc123 input { }` 형태로 변환하기 때문이다. +- `&`는 생성된 클래스명 자신을 가리키는 선택자로, `&:hover`, `&:last-child` 같은 의사 클래스에 사용한다. -### 6. 에러 처리 레이어 +### 2. Scoped Styling 원리 +- 런타임에 컴포넌트마다 고유 클래스명을 생성해 `