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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 54 additions & 0 deletions 02-state-management-tools/2.1-ContextAPI/READEME.md
Original file line number Diff line number Diff line change
@@ -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 (
<UserContext.Provider value={{ user, setUser }}>
{children}
</UserContext.Provider>
);
};

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/)
261 changes: 106 additions & 155 deletions README.md
Original file line number Diff line number Diff line change
@@ -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 (
<RestaurantsContext.Provider value={{ restaurants, addRestaurant, isLoading, error }}>
{children}
</RestaurantsContext.Provider>
);
}

// 3. useContext — 필요한 컴포넌트에서 직접 꺼냄
const { restaurants } = useContext(RestaurantsContext);
```

### 컴포넌트 확장 (`styled(Component)`)
### React 18 vs React 19 Provider 문법 차이

기존 styled-component를 상속해서 스타일을 추가할 수 있다.
React 19부터는 Context 객체 자체를 Provider로 사용할 수 있다. 공식문서가 React 19 기준으로 업데이트되었으므로 버전 확인이 필요하다.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[배움]
리액트 버전에 따른 문법 차이가 있는 줄 몰랐는데 덕분에 알게되었습니다. Provider를 사용할 때 버전 확인을 할 필요가 있겠네요!


```js
const FormItem = styled.div`...`;
```jsx
// React 18 — .Provider 필요
<RestaurantsContext.Provider value={...}>

const RequiredFormItem = styled(FormItem)`
label::after {
content: "*";
color: var(--primary-color);
}
`;
// React 19 — Context 자체를 Provider로 사용 가능
<RestaurantsContext value={...}>
```

---

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

### 필수/선택 폼 항목의 `*` 표시 처리
### 무엇을 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`이 일반 `<ul>`을 감싸는 이중 구조를 `styled.ul`로 단일화했다.
### RestaurantList — Context 구독 및 필터링 내부화

```jsx
// before — section > ul > li 이중 구조
const List = styled.section`...`;
return (
<List>
<ul>
<Restaurant /> {/* styled.li */}
</ul>
</List>
);

// after — ul > li 단일 구조
const List = styled.ul`...`;
return (
<List>
<Restaurant /> {/* styled.li */}
</List>
);
```js
// before
export default function RestaurantList({ restaurants, onRestaurantClick }) { ... }

// after
export default function RestaurantList({ selectedCategory, onRestaurantClick }) {
const { restaurants, isLoading, error } = useContext(RestaurantsContext);
const filteredRestaurants = filterRestaurants(restaurants, selectedCategory);
...
}
```

### CSS 변수 누락 색상 추가 및 하드코딩 제거
### AddRestaurantModal — onSubmit prop 제거

코드에 하드코딩되어 있던 색상값을 CSS 변수로 정의하고 교체했다.

```css
/* App.css에 추가 */
--grey-50: #fcfcfd; /* 헤더 타이틀 텍스트 색상 */
--grey-150: #e9eaed; /* 목록 구분선 색상 */
```js
// before — App으로부터 onSubmit을 받아서 호출
export default function AddRestaurantModal({ onSubmit, onClose }) {
function handleFormSubmit(e) {
e.preventDefault();
onSubmit({ category, name, description });
}
}

// after — Context에서 addRestaurant를 직접 꺼내서 호출
export default function AddRestaurantModal({ onClose }) {
const { addRestaurant } = useContext(RestaurantsContext);
async function handleFormSubmit(e) {
e.preventDefault();
try {
await addRestaurant({ category, name, description });
onClose();
} catch {
alert("음식점 추가에 실패했습니다. 다시 시도해주세요.");
}
}
}
```

| 파일 | 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` 블록이 바로 그 역할을 한다.
### styled-components 선언 위치 — 컴포넌트 하단으로 이동

- **단일 진실 공급원** — 브랜드 색상이 바뀌면 변수 한 줄만 수정하면 전체에 반영된다.
- **색상 분열 방지** — 변수 체계 없이 하드코딩하면 개발자마다 "비슷한 회색"을 다르게 써서 미묘하게 다른 색상이 혼재하게 된다. 이번에 발견한 `#fcfcfd`, `#e9eaed`가 그 사례이다.
- **디자인-개발 소통** — 디자이너가 "grey-200 사용"이라고 전달하면 개발자는 `var(--grey-200)`을 그대로 쓴다. 값이 아닌 이름으로 소통하므로 오역이 줄어든다.
- **의미 전달** — `#ec4a0a`는 어떤 색인지 알 수 없지만 `--primary-color`는 의도가 명확하다.
파일을 열었을 때 가장 먼저 보고 싶은 건 컴포넌트 로직이지, 스타일 세부사항이 아니다. styled-components 선언이 상단에 있으면 컴포넌트 함수가 한참 아래로 밀려 가독성이 떨어진다. 모든 컴포넌트 파일에서 styled-components 선언을 컴포넌트 함수 아래로 이동했다.
Loading