diff --git a/02-state-management-tools/2.1-ContextAPI/READEME.md b/02-state-management-tools/2.1-ContextAPI/READEME.md new file mode 100644 index 0000000..bd02ad7 --- /dev/null +++ b/02-state-management-tools/2.1-ContextAPI/READEME.md @@ -0,0 +1,54 @@ +# 02-1. 전역상태관리 - Context API + +## 🎯 요구사항 +- Context API를 사용해서 애플리케이션 내의 **props drilling** 문제를 해결하세요. + - props로 **똑같은** 데이터 혹은 함수를 전달하지 않도록 해야합니다. + - props를 쓴다면 그 이유를 PR에 적어주세요. +- PR에 Context API를 **왜** 사용하는지, 기존의 코드구조와 어떤 **trade-off**가 있는지 적어주세요. +- Context API와 데이터를 사용하는 Component 사이의 **관계를 도식화**하고 이미지를 PR에 첨부해주세요. + - 실제 코드와 상관없이 일반적인 관계를 나타내야합니다. + - 도식화 방식은 자유롭게 하셔도 좋습니다. + - (추천) **Figma** + +### 😗구현 예시 +- 컴포넌트의 이름이나 구조는 마음대로 변경해도 좋습니다. +- 아래의 코드는 Context를 설정하는 예시입니다. + +```javascript +import { createContext, useState } from "react"; + +// Context 생성 +const UserContext = createContext({ + user: { name: "", email: "" }, + setUser: () => {} +}); + +// Provider 컴포넌트 +export const UserProvider = ({ children }) => { + const [user, setUser] = useState({ name: "John Doe", email: "johndoe@example.com" }); + + return ( + + {children} + + ); +}; + +export default UserContext; + +``` + +## ✅ 키워드 +- props drilling +- 전역상태관리 + - Context + - Provider + - Consumer +- Hook : useContext + +## 🧙‍♀️ 진행 가이드 +- 진행시간 : 2시간 내에 완료하는 것을 목표로 합니다. + +## 🔗 참고 문서 +- [Context API 공식문서](https://ko.legacy.reactjs.org/docs/context.html) +- [리액트를 다루는 기술(저:김민준(velopert))](https://thebook.io/080203/0501/) \ No newline at end of file diff --git a/README.md b/README.md index 357e2a0..20db4bc 100644 --- a/README.md +++ b/README.md @@ -1,218 +1,169 @@ -# styled-components를 적용해서 리팩토링하기 +# Context API를 사용해서 전역상태관리하기 ## 🎯 개인 목표 및 목표 달성을 위한 행동 가이드 이번 미션을 통해 다음과 같은 학습 경험들을 쌓는 것을 목표로 한다. -1. CSS Modules 방식과 CSS-in-JS 방식의 차이를 직접 마이그레이션하며 체감한다. -2. styled-components의 기본 사용법(기본 스타일링, 중첩 선택자, 컴포넌트 확장)을 익힌다. -3. 기존 코드의 구조를 유지하면서 스타일링 방식만 교체하는 리팩토링 경험을 쌓는다. +1. `createContext`, `Provider`, `useContext` 세 가지 개념의 역할을 명확히 이해하고 직접 구현한다. +2. props drilling이 발생하는 지점을 코드에서 직접 파악하고 Context로 해결하는 경험을 쌓는다. +3. UI 상태와 데이터 상태를 구분해서 Context 적용 범위를 스스로 판단하는 능력을 기른다. --- ## 📝 기능 구현 목록 -- [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`) 스타일 분리 +- [x] `RestaurantsContext` 생성 및 `RestaurantsProvider` 구현 +- [x] `useRestaurants` 훅의 데이터(`restaurants`, `addRestaurant`, `isLoading`, `error`)를 Context로 관리 +- [x] `RestaurantList`에서 `useContext`로 `restaurants`, `isLoading`, `error` 직접 구독 +- [x] `AddRestaurantModal`에서 `useContext`로 `addRestaurant` 직접 호출 (`onSubmit` prop 제거) +- [x] `App.jsx`에서 음식점 관련 state/handler 제거, UI 상태(`selectedCategory`, `clickedRestaurant`, `isAddRestaurantModalOpen`)만 유지 --- ## 📚 학습 내용 -### styled-components란? +### Context API란? -CSS-in-JS 라이브러리로, JavaScript 파일 안에서 템플릿 리터럴 문법으로 CSS를 작성하고 이를 React 컴포넌트에 직접 연결하는 방식이다. +컴포넌트 트리 전체에 데이터를 전달할 수 있는 React 내장 기능이다. props를 통해 중간 컴포넌트를 거치지 않고, 필요한 컴포넌트가 데이터를 직접 꺼내 쓸 수 있게 한다. -```js -const Button = styled.button` - background-color: var(--primary-color); - color: white; - border-radius: 8px; - padding: 10px 20px; -`; -``` +### createContext / Provider / useContext -### CSS Modules vs styled-components trade-off +세 가지 API가 각각 다른 역할을 담당한다. -| | CSS Modules | styled-components | +| 개념 | 역할 | 설명 | |---|---|---| -| 스타일 위치 | 별도 `.module.css` 파일 | 컴포넌트 파일 내부 | -| 스코프 | 클래스명 해시로 자동 격리 | 컴포넌트 단위로 격리 | -| 동적 스타일링 | className 조건부 변경 필요 | props로 직접 처리 가능 | -| 가독성 | HTML 구조와 스타일 파일 분리 | 한 파일에서 구조+스타일 파악 가능 | -| 번들 크기 | 별도 런타임 없음 | styled-components 런타임 포함 | - -### 자식 선택자 중첩 - -styled-components는 Sass처럼 중첩 선택자를 지원한다. +| `createContext` | Context 객체 생성 | 데이터를 담을 컨테이너를 정의한다. 초기값을 인자로 받으며, Provider 없이 `useContext`를 호출했을 때 이 초기값이 반환된다. | +| `Provider` | Context 값 공급 | `value` prop으로 전달한 데이터를 하위 컴포넌트 트리 전체에 주입한다. `value`가 변경되면 해당 Context를 구독 중인 모든 컴포넌트가 리렌더링된다. | +| `useContext` | Context 값 구독 | 가장 가까운 상위 Provider의 `value`를 반환한다. Provider가 없으면 `createContext`의 초기값을 반환한다. | ```js -const Category = styled.div` - background: var(--lighten-color); - - img { - width: 36px; - height: 36px; - } -`; +// 1. createContext — 공간 생성 +export const RestaurantsContext = createContext(null); + +// 2. Provider — 데이터를 채워서 하위 컴포넌트에 공급 +export function RestaurantsProvider({ children }) { + const { restaurants, addRestaurant, isLoading, error } = useRestaurants(); + return ( + + {children} + + ); +} + +// 3. useContext — 필요한 컴포넌트에서 직접 꺼냄 +const { restaurants } = useContext(RestaurantsContext); ``` -### 컴포넌트 확장 (`styled(Component)`) +### React 18 vs React 19 Provider 문법 차이 -기존 styled-component를 상속해서 스타일을 추가할 수 있다. +React 19부터는 Context 객체 자체를 Provider로 사용할 수 있다. 공식문서가 React 19 기준으로 업데이트되었으므로 버전 확인이 필요하다. -```js -const FormItem = styled.div`...`; +```jsx +// React 18 — .Provider 필요 + -const RequiredFormItem = styled(FormItem)` - label::after { - content: "*"; - color: var(--primary-color); - } -`; +// React 19 — Context 자체를 Provider로 사용 가능 + ``` --- ## 🤔 고민했던 문제와 해결 과정에서 배운 점 -### 필수/선택 폼 항목의 `*` 표시 처리 +### 무엇을 Context에 넣을 것인가 -원본 CSS Modules에서는 `.formItem--required` 클래스를 조건부로 붙이는 방식으로 필수 항목에만 `*`를 표시했다. +Context에 모든 state를 넣는 게 아니라, **데이터 도메인**과 **UI 상태**를 구분하는 것이 핵심이다. -styled-components로 전환할 때 `label::after { content: "*" }`를 `FormItem`에 바로 넣으면 모든 항목에 `*`가 붙는 문제가 생긴다. +| 구분 | 상태 | 이유 | +|---|---|---| +| Context (데이터 도메인) | `restaurants`, `addRestaurant`, `isLoading`, `error` | 여러 컴포넌트에서 공유되는 서버 데이터 | +| props / 로컬 state (UI 상태) | `selectedCategory`, `clickedRestaurant`, `isAddRestaurantModalOpen` | 특정 화면의 인터랙션 상태로, App이 관리하는 게 자연스러움 | -`styled(FormItem)`으로 확장한 `RequiredFormItem`을 별도로 만들어서 필수 항목(카테고리, 이름)에만 적용하는 방식으로 해결했다. +판단 기준은 "그 데이터가 App 고유의 책임인가?"다. `addRestaurant`는 음식점 데이터 도메인의 책임이므로 Context가 적합하고, `selectedCategory`는 화면 UI의 책임이므로 로컬 state가 적합하다. -### 자식 선택자 중첩 — 언제 쓰고 언제 피할까 +### Provider 위치 결정 -styled-components는 Sass처럼 중첩 선택자를 지원하지만, "항상 피해야 한다"기보다는 **상황에 따라 다르다**가 정확하다. +처음에는 `RestaurantsProvider`를 App의 return 안에 배치했다. 이 경우 App 자신이 `useContext`로 Context 데이터를 꺼낼 수 없다는 문제가 생긴다. `useContext`는 자신보다 **상위에 있는** Provider를 찾기 때문이다. -**중첩이 괜찮은 경우** — 부모와 항상 함께 쓰이는 단순 HTML 요소일 때는 별도 컴포넌트로 분리하는 게 오히려 과도한 추상화다. +해결책은 두 가지였다. -```js -// img는 Category 없이 독립적으로 쓰일 일이 없으므로 중첩이 합리적 -const Category = styled.div` - background: var(--lighten-color); +1. Provider를 `main.jsx`로 올려서 App도 Context에 접근 가능하게 만들기 +2. App이 Context를 쓸 필요가 없도록 구조를 바꾸기 - img { - width: 36px; - height: 36px; - } -`; -``` +`AddRestaurantModal`이 `addRestaurant`를 Context에서 직접 꺼내도록 하면 App은 `addRestaurant`를 전혀 알 필요가 없어진다. `isLoading`, `error`도 `RestaurantList`가 직접 보여주면 된다. 결과적으로 App이 Context를 쓰지 않아도 되는 구조가 만들어졌고, Provider 위치 문제도 자연스럽게 해결됐다. -`&:hover`, `&:focus` 같은 가상 선택자도 중첩이 자연스럽고 관용적인 방식이다. +### filteredRestaurants를 어디서 처리할 것인가 -**중첩을 피하는 게 나은 경우** — 자식 요소가 의미 있는 스타일을 가지거나, 독립적으로 재사용될 가능성이 있을 때다. +기존에는 `App`이 `filteredRestaurants`를 계산해서 `RestaurantList`에 내려줬다. Context 도입 후 `restaurants`는 Context에서 오고, `selectedCategory`는 UI 상태로 props로 전달하게 됐다. -```js -// h3/p는 각자 의미 있는 스타일을 가지므로 분리 -const RestaurantName = styled.h3` - font-size: 18px; - font-weight: 600; -`; - -const RestaurantDescription = styled.p` - padding-top: 8px; - -webkit-line-clamp: 2; -`; +``` +// 변경 후 +RestaurantList에서 Context로 restaurants를 꺼내고, +props로 받은 selectedCategory로 필터링을 직접 처리 ``` -**판단 기준 요약** - -| | 중첩 OK | 분리 권장 | -|---|---|---| -| 스타일 복잡도 | 단순 (크기, 색상 1-2개) | 복잡한 스타일 블록 | -| 재사용 가능성 | 부모 없이 쓰일 일 없음 | 다른 곳에서도 쓰일 수 있음 | -| 조건부 스타일 | 없음 | props로 분기 필요 | -| 의미 전달 | 이름 필요 없음 | 이름이 코드 이해에 도움됨 | +`selectedCategory`를 props로 유지한 이유: UI 상태이므로 Context보다 props가 더 자연스럽다. 미션 요구사항에도 "props를 쓴다면 그 이유를 PR에 적어주세요"라고 명시되어 있으므로, 이 판단 근거를 기록한다. --- ## 🛠 리팩토링 -### 컴포넌트 파일 구조 단순화 - -기존에는 컴포넌트마다 폴더를 만들어 `ComponentName/ComponentName.jsx` + `ComponentName.module.css` 구조였다. styled-components 전환 후 CSS 파일이 사라지면서 폴더 없이 `ComponentName.jsx` 단일 파일로 정리했다. - -``` -before: -components/ - Header/ - Header.jsx - Header.module.css - -after: -components/ - Header.jsx -``` - -### 자식 태그 선택자 → 별도 styled 컴포넌트로 분리 +### App.jsx — 음식점 데이터 책임 제거 -의미 있는 콘텐츠 요소에 자식 태그 선택자를 사용하던 방식을 각각 별도의 styled 컴포넌트로 분리했다. +Context 도입 전 App은 `useRestaurants`를 직접 호출하고, 모든 handler를 가지고 있었다. 리팩토링 후 App은 UI 상태만 관리한다. ```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;`; +// before — App이 데이터와 UI 상태를 모두 관리 +const { restaurants, addRestaurant, isLoading, error } = useRestaurants(); +const filteredRestaurants = filterRestaurants(restaurants, selectedCategory); +async function handleRestaurantSubmit(restaurant) { ... } + +// after — UI 상태만 남음 +const [selectedCategory, setSelectedCategory] = useState(ALL_CATEGORY); +const [clickedRestaurant, setClickedRestaurant] = useState(null); +const [isAddRestaurantModalOpen, setIsAddRestaurantModalOpen] = useState(false); ``` -적용 파일: `CategoryFilter`, `RestaurantList`, `AddRestaurantModal` - -### RestaurantList 시맨틱 구조 개선 - -`styled.section`이 일반 `