diff --git a/.claude/agents/analyzer.md b/.claude/agents/analyzer.md
new file mode 100644
index 0000000..9f079cb
--- /dev/null
+++ b/.claude/agents/analyzer.md
@@ -0,0 +1,58 @@
+---
+name: analyzer
+description: 기존 코드를 분석하고 설명합니다. 코드 구조 파악, 함수 동작 이해, 파일 관계 파악이 필요할 때 호출됩니다.
+tools: Read, Grep, Glob
+---
+
+당신은 코드 분석가입니다.
+
+## 역할
+
+- 소스코드 구조 분석
+- 함수/컴포넌트 동작 설명
+- 파일 간 관계 설명
+- 데이터 흐름 파악
+
+## 분석 시 포함할 내용
+
+### 파일 분석
+
+- 파일의 목적
+- 주요 export
+- 의존성 (import)
+- 다른 파일과의 관계
+
+### 함수/컴포넌트 분석
+
+- 역할/목적
+- 파라미터 설명
+- 반환값
+- 사용 예시
+- 관련 함수
+
+## 응답 형식
+
+```
+📂 [파일명] 분석
+
+## 목적
+
+[이 파일이 하는 일]
+
+## 구조
+
+[주요 구성 요소]
+
+## 핵심 코드
+
+[중요한 부분 설명]
+
+## 관련 파일
+
+- [연관 파일들]
+```
+
+## 원칙
+
+- 설명만 하고 수정 제안은 하지 않음
+- 학습자가 이해할 수 있는 수준으로 설명
diff --git a/.claude/agents/checker.md b/.claude/agents/checker.md
new file mode 100644
index 0000000..77e9a7d
--- /dev/null
+++ b/.claude/agents/checker.md
@@ -0,0 +1,57 @@
+---
+name: checker
+description: 과제 구현을 검증하고 피드백을 제공합니다. 구현 확인, 테스트, 요구사항 충족 여부 확인이 필요할 때 호출됩니다.
+tools: Read, Grep, Glob, Bash
+---
+
+당신은 과제 검증자입니다.
+
+## 역할
+
+- 구현 결과 검증
+- 요구사항 충족 확인
+- 코드 품질 피드백
+- 개선점 제안
+
+## 검증 체크리스트
+
+1. [ ] 요구사항 충족 여부
+2. [ ] 코드 동작 여부
+3. [ ] 에러 없음
+4. [ ] 유의점 준수 여부
+
+## 응답 형식
+
+### 통과 시
+
+```
+✅ 검증 통과!
+
+## 잘한 점
+
+- [잘한 것 1]
+- [잘한 것 2]
+
+## 개선 제안 (선택)
+
+- [더 좋게 할 수 있는 점]
+
+➡️ /project:done 으로 태스크를 완료하세요.
+```
+
+### 미통과 시
+
+```
+❌ 수정 필요
+
+## 문제점
+
+- [문제 1]
+- [문제 2]
+
+## 수정 방향
+
+- [어떻게 고쳐야 하는지]
+
+힌트가 필요하면 /project:hint 를 사용하세요.
+```
diff --git a/.claude/agents/guide.md b/.claude/agents/guide.md
new file mode 100644
index 0000000..28c9597
--- /dev/null
+++ b/.claude/agents/guide.md
@@ -0,0 +1,55 @@
+---
+name: guide
+description: 학습 가이드와 힌트를 제공합니다. 막혔을 때, 개념 설명이 필요할 때, 접근 방법을 모를 때 호출됩니다.
+tools: Read, Grep, Glob, WebSearch, WebFetch
+---
+
+당신은 학습 가이드입니다.
+
+## 역할
+
+- 개념 설명
+- 단계적 힌트 제공
+- 접근 방법 제안
+- 참고 자료 안내
+
+## 힌트 제공 원칙
+
+### ⛔ 절대 금지
+
+- 정답 코드 전체를 바로 제공
+- 구현을 대신 해주기
+- 복사-붙여넣기만 하면 되는 코드 제공
+
+### ✅ 해야 할 것
+
+- 단계적 힌트 (Level 1 → 2 → 3 → 4)
+- 스스로 생각하게 유도하는 질문
+- 관련 개념 설명
+- 공식 문서 참조 안내
+
+## 힌트 레벨
+
+**Level 1** (방향성):
+"이 문제는 [개념]을 활용하면 됩니다"
+
+**Level 2** (구체적 방향):
+"[개념]을 사용해서 [구체적 접근]을 해보세요"
+
+**Level 3** (코드 스니펫):
+"[핵심 코드 패턴] 형태로 시작해보세요"
+
+**Level 4** (거의 정답, 최후의 수단):
+"[구체적 코드]를 추가하면 됩니다"
+
+## 힌트 요청 시 응답 형식
+
+```
+💡 힌트 (Level [N])
+
+[힌트 내용]
+
+---
+
+더 구체적인 힌트가 필요하면 말씀해주세요.
+```
diff --git a/.claude/agents/task-manager.md b/.claude/agents/task-manager.md
new file mode 100644
index 0000000..ae9a059
--- /dev/null
+++ b/.claude/agents/task-manager.md
@@ -0,0 +1,82 @@
+---
+name: task-manager
+description: 학습 태스크 관리를 담당합니다. 태스크 시작, 완료, 진행 상황 확인, 커밋 안내가 필요할 때 호출됩니다.
+tools: Read, Write, Edit, Bash, Glob, Grep
+---
+
+당신은 학습 태스크 매니저입니다.
+
+## 역할
+
+- 태스크 목록 관리 (`.claude/state/tasks.md`)
+- 진행 상황 추적 (`.claude/state/progress.json`)
+- 로그 작성 (`.claude/state/logs/`)
+- 세션 관리
+- 커밋 안내
+
+## 세션 시작 시 (/start)
+
+1. 오늘 날짜의 세션 로그 확인/생성: `.claude/state/logs/session-YYYY-MM-DD.md`
+2. `.claude/state/progress.json`에서 현재 상태 확인
+3. `.claude/state/tasks.md`에서 현재 태스크 확인
+4. 이전 세션의 "다음 세션에서 할 일" 확인
+5. 세션 로그에 시작 시간 기록
+6. 사용자에게 현재 상황 안내
+
+## 세션 종료 시 (/end)
+
+1. 오늘 진행한 내용 요약
+2. 세션 로그 업데이트:
+ - 진행한 내용 정리
+ - 커밋 내역 추가 (`git log --oneline`로 확인)
+ - 미완료 작업을 "다음 세션에서 할 일"에 기록
+ - 세션 종료 시간 기록
+3. `.claude/state/progress.json` lastUpdated 갱신
+4. 커밋되지 않은 변경사항 확인 및 제안
+5. 사용자에게 요약 안내
+
+## 태스크 완료 시 (/done)
+
+1. `.claude/state/logs/task-[n].md` 작성
+2. `.claude/state/tasks.md` 업데이트 (체크 표시)
+3. `.claude/state/progress.json` 업데이트
+4. 세션 로그에도 완료 내용 기록
+5. 커밋 메시지 제안:
+
+Type: 내용
+
+- 세부 내용
+- 세부 내용
+
+6. 다음 태스크 안내
+
+## 힌트 사용 시 (/hint)
+
+1. `.claude/state/progress.json`의 hintsUsed 업데이트
+2. 세션 로그에 힌트 사용 기록
+
+## 커밋 시 (/commit)
+
+1. 세션 로그의 "커밋 내역"에 추가
+
+## 커밋 메시지 규칙
+
+- Type은 영어 대문자로 시작: Feat, Fix, Refactor, Style, Docs, Test, Chore
+- 내용은 한글로 작성
+- 세부 내용은 * 로 나열
+
+## 로그 파일 구조
+
+```
+.claude/state/logs/
+├── task-1.md # 완료된 태스크 로그
+├── task-2.md # 완료된 태스크 로그
+├── session-2025-12-08.md # 일일 세션 로그
+└── session-2025-12-09.md # 일일 세션 로그
+```
+
+## 금지사항
+
+- 코드 직접 작성 금지 (guide에게 위임)
+- 로그 없이 태스크 완료 처리 금지
+- 세션 로그 없이 세션 종료 금지
diff --git a/.claude/commands/check.md b/.claude/commands/check.md
new file mode 100644
index 0000000..9b2eb3b
--- /dev/null
+++ b/.claude/commands/check.md
@@ -0,0 +1,13 @@
+---
+description: 과제 구현을 검증합니다
+---
+
+현재 구현을 검증하고 피드백을 제공합니다.
+
+## 검증 항목
+
+- 요구사항 충족
+- 코드 동작 여부
+- 유의점 준수
+
+$ARGUMENTS
diff --git a/.claude/commands/commit.md b/.claude/commands/commit.md
new file mode 100644
index 0000000..08a2325
--- /dev/null
+++ b/.claude/commands/commit.md
@@ -0,0 +1,31 @@
+---
+description: 커밋 메시지를 생성합니다
+---
+
+현재까지의 변경사항을 기반으로 커밋 메시지를 생성합니다.
+
+## 커밋 메시지 형식
+
+```
+Type: 내용
+
+- 세부 내용
+- 세부 내용
+```
+
+## Type 종류
+
+- Feat: 새로운 기능
+- Fix: 버그 수정
+- Refactor: 리팩토링
+- Style: 스타일 변경
+- Docs: 문서 수정
+- Test: 테스트
+- Chore: 기타
+
+## 규칙
+
+- Type은 영어 대문자로 시작
+- 내용은 한글로 작성
+
+$ARGUMENTS
diff --git a/.claude/commands/done.md b/.claude/commands/done.md
new file mode 100644
index 0000000..4a61ada
--- /dev/null
+++ b/.claude/commands/done.md
@@ -0,0 +1,13 @@
+---
+description: 현재 태스크를 완료 처리합니다
+---
+
+현재 태스크를 완료하고 다음 작업을 수행합니다:
+
+1. **로그 작성**: `.claude/state/logs/task-[n].md`
+2. **태스크 목록 업데이트**: `.claude/state/tasks.md`
+3. **진행 상황 업데이트**: `.claude/state/progress.json`
+4. **커밋 메시지 제안**
+5. **다음 태스크 안내**
+
+$ARGUMENTS
diff --git a/.claude/commands/end.md b/.claude/commands/end.md
new file mode 100644
index 0000000..10e3171
--- /dev/null
+++ b/.claude/commands/end.md
@@ -0,0 +1,31 @@
+---
+description: 학습 세션을 종료합니다
+---
+
+학습 세션을 종료합니다.
+
+## 수행할 작업
+
+1. **오늘 진행 내용 요약**:
+ - 현재 태스크 진행 상황 파악
+ - 완료한 작업 목록 정리
+ - 미완료 작업 목록 정리
+
+2. **세션 로그 업데이트**: `.claude/state/logs/session-YYYY-MM-DD.md`
+ - 세션 종료 시간 기록
+ - 오늘 진행한 내용 정리
+ - 커밋 내역 추가 (git log로 확인)
+ - "다음 세션에서 할 일" 작성
+
+3. **progress.json 업데이트**:
+ - lastUpdated 시간 갱신
+
+4. **커밋 제안** (선택):
+ - 커밋되지 않은 변경사항이 있으면 커밋 제안
+
+5. **사용자에게 안내**:
+ - 오늘 진행 요약
+ - 다음에 이어서 할 내용
+ - 수고했다는 인사
+
+$ARGUMENTS
diff --git a/.claude/commands/hint.md b/.claude/commands/hint.md
new file mode 100644
index 0000000..b5af92c
--- /dev/null
+++ b/.claude/commands/hint.md
@@ -0,0 +1,14 @@
+---
+description: 현재 태스크에 대한 힌트를 요청합니다
+---
+
+현재 진행 중인 태스크에 대한 힌트를 제공합니다.
+
+## 힌트 레벨
+
+- 기본: Level 1 (방향성 힌트)
+- "더 자세히": Level 2
+- "더 구체적으로": Level 3
+- "거의 답": Level 4
+
+$ARGUMENTS
diff --git a/.claude/commands/setup.md b/.claude/commands/setup.md
new file mode 100644
index 0000000..288af36
--- /dev/null
+++ b/.claude/commands/setup.md
@@ -0,0 +1,7 @@
+---
+description: 학습 과제 초기 설정을 시작합니다 (최초 1회)
+---
+
+SETTING.md를 읽고 학습 과제 환경을 설정합니다.
+
+$ARGUMENTS
diff --git a/.claude/commands/start.md b/.claude/commands/start.md
new file mode 100644
index 0000000..2a91796
--- /dev/null
+++ b/.claude/commands/start.md
@@ -0,0 +1,28 @@
+---
+description: 학습 세션을 시작합니다
+---
+
+학습 세션을 시작합니다.
+
+## 수행할 작업
+
+1. **세션 로그 생성/확인**: `.claude/state/logs/session-YYYY-MM-DD.md`
+ - 오늘 날짜의 세션 로그가 없으면 새로 생성
+ - 이미 있으면 이어서 작성 (재시작 기록)
+
+2. **현재 상태 확인**:
+ - `.claude/state/progress.json` 읽기
+ - `.claude/state/tasks.md`에서 현재 태스크 확인
+ - 이전 세션 로그가 있다면 마지막 진행 상황 확인
+
+3. **세션 로그에 기록**:
+ - 세션 시작 시간
+ - 현재 진행 중인 태스크
+ - 이전 세션에서 남긴 "다음에 할 일" 확인
+
+4. **사용자에게 안내**:
+ - 현재 태스크 요약
+ - 이전 세션에서 미완료된 작업
+ - 오늘 할 일 제안
+
+$ARGUMENTS
diff --git a/.claude/docs/2-2-99.react-state-strategy.md b/.claude/docs/2-2-99.react-state-strategy.md
new file mode 100644
index 0000000..8714b0c
--- /dev/null
+++ b/.claude/docs/2-2-99.react-state-strategy.md
@@ -0,0 +1,1764 @@
+# 프론트엔드 상태관리 전략
+
+## 1. 상태관리란 무엇일까?
+
+### (1) 상태관리가 등장하게 된 배경
+
+**1) 서버 중심(SSR) → 클라이언트 중심(SPA)으로 변화**
+
+- 모바일 앱의 등장으로 웹에서도 앱과 유사한 경험 요구
+- SPA 방식 도입으로 클라이언트에 유지해야 할 상태가 급증
+- 페이지 새로고침 없이 다양한 UI를 처리하는 복잡성 증가
+
+```jsx
+// 복잡해진 클라이언트 상태들
+let user = {
+ /* 사용자 정보 */
+};
+let cart = [
+ /* 장바구니 아이템 */
+];
+let uiState = { isModalOpen: false, darkMode: true };
+let apiData = {
+ /* API 응답 데이터 */
+};
+```
+
+**2) 체계적인 상태관리 패턴과 라이브러리 등장**
+
+- 2013년: React의 단방향 데이터 흐름 소개
+- 2015년: Redux(Flux 패턴)의 등장으로 전역 상태관리 체계화
+- 2018년 이후: MobX, Zustand, Recoil, Jotai 등으로 발전
+
+**3) 상태관리 핵심 원칙 확립**
+
+- 단일 진실 공간(Single source of truth)
+- 불변성(Immutability)을 통한 예측 가능한 상태 변화
+- 명시적인 상태 업데이트 메커니즘
+- 클라이언트 상태와 서버 상태의 분리
+
+**AS-IS (상태 관리 도입 전)**
+
+```jsx
+// 전역 변수로 관리되는 상태
+let shoppingCart = [];
+let notifications = [];
+
+// 장바구니 기능
+function addToCart(product) {
+ shoppingCart.push(product);
+ localStorage.setItem("cart", JSON.stringify(shoppingCart));
+
+ // DOM 직접 조작
+ const cartCount = document.getElementById("cart-count");
+ cartCount.textContent = shoppingCart.length;
+
+ // 알림 추가
+ addNotification(`${product.name}이(가) 장바구니에 추가되었습니다`);
+}
+
+// 알림 기능
+function addNotification(message) {
+ notifications.push(message);
+
+ // DOM 직접 생성 및 조작
+ const notificationEl = document.createElement("div");
+ notificationEl.className = "notification";
+ notificationEl.textContent = message;
+
+ const container = document.getElementById("notification-container");
+ container.appendChild(notificationEl);
+
+ // 3초 후 알림 제거
+ setTimeout(() => {
+ container.removeChild(notificationEl);
+ }, 3000);
+}
+```
+
+**TO-BE (현재의 모습)**
+
+```jsx
+import React, { useState, useEffect } from "react";
+
+// 컴포넌트 내부에서 관리되는 상태
+function ShoppingApp() {
+ const [cart, setCart] = useState([]);
+ const [notifications, setNotifications] = useState([]);
+
+ // 장바구니 기능
+ const addToCart = (product) => {
+ setCart((prevCart) => [...prevCart, product]);
+ addNotification(`${product.name}이(가) 장바구니에 추가되었습니다`);
+ };
+
+ // 알림 기능
+ const addNotification = (message) => {
+ const newNotification = { id: Date.now(), message };
+ setNotifications((prev) => [...prev, newNotification]);
+
+ // 3초 후 알림 제거
+ setTimeout(() => {
+ setNotifications((prev) => prev.filter((item) => item.id !== newNotification.id));
+ }, 3000);
+ };
+
+ // localStorage 동기화
+ useEffect(() => {
+ localStorage.setItem("cart", JSON.stringify(cart));
+ }, [cart]);
+
+ return (
+
+ {/* 장바구니 아이콘 */}
+
장바구니 ({cart.length})
+
+ {/* 알림 컨테이너 */}
+
+ {notifications.map((notification) => (
+
+ {notification.message}
+
+ ))}
+
+
+ );
+}
+```
+
+### (2) 상태관리의 등장 후 달라진 애플리케이션의 모습
+
+**예측 가능한 데이터 흐름**
+
+- 상태 변경이 단방향으로 흐르며 추적 가능
+- 액션 → 리듀서 → 상태 업데이트 → UI 렌더링의 명확한 사이클
+
+**컴포넌트 간 결합도 감소**
+
+- 컴포넌트는 필요한 상태만 구독하고 렌더링에 집중
+- 상태 로직과 UI 로직의 명확한 분리
+
+**개발 도구와 디버깅 향상**
+
+- 상태 변화 추적 및 시간 여행 디버깅 가능
+- 상태 스냅샷을 통한 애플리케이션 동작 이해 용이
+
+**대규모 애플리케이션 구조화 가능**
+
+- 확장 가능한 패턴으로 복잡한 애플리케이션 관리
+- 팀 협업 시 일관된 데이터 접근 방식 제공
+
+### **Summary**
+
+- 스마트 폰의 등장으로 앱 개발이 시작 됨 → 앱의 개발 비용을 낮추기 위해 “웹뷰”를 사용 → 웹에서 앱 만큼의 사용성이 필요해짐 → 점점 클라이언트의 코드가 복잡해지고, 스마프폰의 작은 화면에 모든 내용을 담을 수 없음 → 성능도 챙기면서 유지보수도 쉬운 방법이 필요 → 상태를 기반으로 렌더링하는 시스템(선언형 방식)이 등장하고, 컴포넌트 단위 개발이 가능해짐
+ - 규모가 커질수록 상태관리에 대한 복잡도가 증가하고, 이를 정돈할 수 있는 기술과 방법론이 필요해짐 → Redux 같은 상태관리 라이브러리가 등장하고 점점 발전
+
+## 2. 상태관리를 위한 도구
+
+### (1) 컴포넌트 로컬 상태관리
+
+**React의 내장 상태관리 도구**
+
+**1) useState**
+
+- 간단한 상태 관리를 위한 가장 기본적인 Hook
+- 독립적인 상태 변수를 생성하고 관리
+
+```jsx
+function Counter() {
+ const [count, setCount] = useState(0);
+
+ return (
+
+
현재 카운트: {count}
+
+
+ );
+}
+```
+
+**2) useReducer**
+
+- 복잡한 상태 로직을 reducer 함수로 분리
+- 여러 하위 값을 포함하는 복잡한 상태 관리에 적합
+
+```jsx
+function counterReducer(state, action) {
+ switch (action.type) {
+ case "increment":
+ return { count: state.count + 1 };
+ case "decrement":
+ return { count: state.count - 1 };
+ default:
+ throw new Error("Unknown action");
+ }
+}
+
+function Counter() {
+ const [state, dispatch] = useReducer(counterReducer, { count: 0 });
+
+ return (
+
+
현재 카운트: {state.count}
+
+
+
+ );
+}
+```
+
+### (2) 컴포넌트 간 상태 공유
+
+**1) Props와 리프팅 스테이트 업(Lifting State Up)**
+
+- 부모 컴포넌트에서 상태를 관리하고 자식에게 전달
+- 간단한 구조에서 효과적이지만 props drilling 문제 발생 가능
+
+```jsx
+function Parent() {
+ const [count, setCount] = useState(0);
+
+ return (
+
+ );
+});
+```
+
+- 불변성보다 가변성 채택(mutable state)
+- 반응형 프로그래밍 패러다임 기반
+- 상태 변화를 자동으로 추적하고 UI 업데이트
+- Redux보다 적은 보일러플레이트 코드
+- 객체 지향 프로그래밍 스타일에 적합
+
+### (4) 서버 상태 관리 도구
+
+**1) TanStack Query**
+
+- 서버 데이터 가져오기, 캐싱, 동기화를 위한 라이브러리
+- 자동 리프레시, 데이터 무효화, 페이지네이션 지원
+
+```jsx
+import { useQuery, useMutation, queryClient } from "react-query";
+
+// 데이터 조회
+function Products() {
+ const { data, isLoading, error } = useQuery("products", fetchProducts);
+
+ if (isLoading) return
로딩 중...
;
+ if (error) return
에러: {error.message}
;
+
+ return (
+
+ {data.map((product) => (
+
{product.name}
+ ))}
+
+ );
+}
+
+// 데이터 변경
+function AddProduct() {
+ const mutation = useMutation(addProduct, {
+ onSuccess: () => {
+ queryClient.invalidateQueries("products");
+ },
+ });
+
+ return (
+
+ );
+}
+```
+
+**2) SWR (Stale-While-Revalidate)**
+
+- Vercel에서 개발한 데이터 가져오기 라이브러리
+- 캐시된 데이터를 먼저 보여주고 백그라운드에서 갱신
+
+```jsx
+import useSWR from "swr";
+
+function Profile() {
+ const { data, error, isLoading } = useSWR("/api/user", fetcher);
+
+ if (isLoading) return
로딩 중...
;
+ if (error) return
에러가 발생했습니다
;
+
+ return
안녕하세요, {data.name}님!
;
+}
+```
+
+이러한 상태관리 도구들은 각기 다른 사용 사례와 복잡성에 맞게 설계되었습니다. 어떤 도구를 선택할지는 애플리케이션의 규모, 팀의 선호도, 성능 요구사항 등에 따라 달라집니다.
+
+### Summary
+
+| **라이브러리** | **특징** | **장점** | **단점** | **성능 최적화 방법** |
+| ------------------------ | ------------------------------- | ----------------------- | -------- | -------------------- |
+| **Context API** | 컴포넌트 트리 전체 데이터 공유 | • props drilling 방지 |
+| • React 내장 기능 | • 소비자 컴포넌트 전체 리렌더링 |
+| • 깊은 중첩 시 성능 이슈 | • Context 분리 |
+| • memo 활용 |
+| **Redux** | 단일 스토어, Flux 패턴 | • 예측 가능한 상태 변화 |
+
+• 강력한 개발자 도구
+• 풍부한 미들웨어 | • 많은 보일러플레이트
+• 복잡한 설정 | • 선택적 구독
+• reselect
+• immer |
+| **Zustand** | 간결한 API, 훅 기반 | • 최소 보일러플레이트
+• 쉬운 통합 | • 제한된 개발자 도구
+• 대규모 앱에 제한적 | • 선택적 상태 구독
+• 상태 분리 |
+| **Jotai** | 원자 기반, 상향식 접근 | • 세분화된 리렌더링
+• 조합 가능한 API | • 복잡한 상태 관계 관리 어려움 | • 최소한의 atom 구독
+• atom 분리 |
+| **React Query** | 서버 데이터 특화 | • 자동 리프레시
+• 내장된 오류 처리
+• 페이지네이션 | • 클라이언트 상태에 오버헤드 | • 쿼리 키 설계
+• 캐시 무효화 정책 |
+
+## 3. 코드를 통해 살펴보기
+
+### (1) useState: Modal로 입력폼을 관리하는 사례
+
+```tsx
+export const PostsManagerPage = () => {
+ const { onOpen } = useToggleState()
+ const postAddModalProps = useAddPostModal();
+
+ return (
+
+
+
+ 게시물 관리자
+
+
+
+
+
+
+
+
+
+ )
+}
+```
+
+```tsx
+export const useAddPostModal = () => {
+ const { onClose } = useToggleState();
+ const [formData, setFormData] = useState({ title: "", body: "", userId: 1 });
+
+ const addPostMutation = useMutation({
+ ...postMutations.addMutation(),
+ onSuccess: () => {
+ setFormData({ title: "", body: "", userId: 1 });
+ queryClient.invalidateQueries({ queryKey: ["posts"] });
+
+ handleClose();
+ },
+ onError: (error) => {
+ console.error("게시물 추가 오류:", error);
+ },
+ });
+
+ const handleChange = ({ field, value }: { field: string; value: string | number }) => {
+ setFormData((prev) => ({ ...prev, [field]: value }));
+ };
+
+ const handleSubmit = () => {
+ addPostMutation.mutateAsync(formData);
+ };
+
+ const handleClose = () => {
+ setFormData({ title: "", body: "", userId: 1 });
+ onClose("addPost");
+ };
+
+ return {
+ formData,
+ onChange,
+ onSubmit,
+ pending: addPostMutation.isPending,
+ };
+};
+```
+
+```tsx
+interface PostAddModalProps {
+ formData: {
+ title: string;
+ body: string;
+ userId: number;
+ };
+ onChange: (data: { field: string; value: string | number }) => void;
+ onSubmit: () => void;
+ pending: boolean;
+}
+
+export const PostAddModal = ({ formData, onChange, onSubmit, pending }: PostAddModalProps) => {
+ const { isOpen, onClose } = useToggleState();
+ return (
+ onClose("addPost")} title='새 게시물 추가'>
+ onChange({ field, value })}
+ submitLabel={{ default: "게시물 추가", loading: "추가 중..." }}
+ onSubmit={onSubmit}
+ pending={pending}
+ />
+
+ );
+};
+```
+
+```mermaid
+sequenceDiagram
+ participant Hook as useAddPostModal
+ participant Page as PostsManagerPage
+ participant Modal as PostAddModal
+ participant Form as PostForm
+
+ Note over Hook: 초기 상태 설정 (formData, mutation)
+ Hook-->>Page: formData, handlers 제공
+
+ Note over Page: 사용자가 '게시물 추가' 버튼 클릭
+ Page->>Modal: formData, handlers 전달
+
+ Note over Modal: Modal 컴포넌트 렌더링
+ Modal->>Form: formData, onChange, onSubmit 전달
+
+ Note over Form: 사용자가 폼 입력
+ Form-->>Modal: onChange 이벤트
+ Modal-->>Hook: handleChange 호출
+ Hook-->>Hook: setFormData로 상태 업데이트
+
+ Note over Form: 사용자가 제출
+ Form-->>Modal: onSubmit 이벤트
+ Modal-->>Hook: handleSubmit 호출
+ Hook-->>Hook: addPostMutation 실행
+
+ Note over Hook: 성공 시
+ Hook-->>Hook: 1. formData 초기화
+ Hook-->>Hook: 2. posts 쿼리 무효화
+ Hook-->>Modal: 3. Modal 닫기
+```
+
+1. useAddPostModal이 로컬 상태(useState)로 값이 관리되고 있습니다.
+2. useAddPostModal에서 setState가 발생하면 PostsManagerPage이 렌더링 됩니다.
+3. PostsManagerPage의 경우 여러 개의 Modal과 게시물 목록을 포함하여 수백 개의 컴포넌트를 렌더링하고 있습니다.
+4. 결과적으로 PostManagerPage에서 렌더링이 발생 → 자식 컴포넌트에게 전파 되면서, 1회 입력당 60ms 이상의 렌더링 비용이 발생합니다.
+
+이를 개선하기 위해선 다음과 같은 과정이 필요합니다.
+
+1. PostForm 내부에 useState로 form의 상태를 정의하고, change 시점에 반영합니다.
+2. PostForm에서 submit을 하는 경우 PostManagerPage에 알립니다.
+
+```tsx
+// Page에서는 PostAddModal에 대한 관심사 제거
+export const PostsManagerPage = () => {
+ const { onOpen } = useToggleState()
+
+ return (
+
+
+
+ 게시물 관리자
+
+
+
+
+
+
+
+
+
+ )
+}
+```
+
+```tsx
+// AddPost에 대한 API 요청을 하나의 묶어서 관리
+export const useAddPost = () => {
+ const addPostMutation = useMutation({
+ ...postMutations.addMutation(),
+ onSuccess: () => {
+ queryClient.invalidateQueries({ queryKey: ["posts"] });
+ },
+ onError: (error) => {
+ console.error("게시물 추가 오류:", error);
+ },
+ });
+
+ return {
+ submit: addPostMutation.mutateAsync,
+ pending: addPostMutation.isPending,
+ };
+};
+
+// formData에 대한 관심사를 하나로 묶어서 관리
+export const useAddPostForm = () => {
+ const [formData, setFormData] = useState({ title: "", body: "", userId: 1 });
+
+ const changeFormData = ({ field, value }: { field: string; value: string | number }) => {
+ setFormData((prev) => ({ ...prev, [field]: value }));
+ };
+
+ return {
+ value: formData,
+ change: changeFormData,
+ };
+};
+```
+
+```tsx
+// PostAddModal이 자신의 상태를 스스로 책임지도록 함
+export const PostAddModal = () => {
+ const { isOpen, onClose } = useToggleState();
+
+ const formData = useAddPostForm();
+ const addPost = useAddPost();
+ const opened = isOpen("addPost");
+ const close = () => onClose("addPost");
+
+ const handleSubmit = () => {
+ addPost.submit(formData.value);
+ close();
+ };
+
+ return (
+
+ formData.change({ field, value })}
+ submitLabel={{
+ default: "게시물 추가",
+ loading: "추가 중...",
+ }}
+ onSubmit={handleSubmit}
+ pending={addPost.pending}
+ />
+
+ );
+};
+```
+
+AS-IS: input에 입력할 때 70ms 정도 시간 소요
+
+TO-BE: input에 입력할 때 **3ms 정도의 시간** 소요
+
+```mermaid
+sequenceDiagram
+ participant Page as PostsManagerPage
+ participant Modal as PostAddModal
+ participant Form as PostForm
+ participant AddPost as useAddPost
+ participant AddForm as useAddPostForm
+
+ Note over Modal: 초기화
+ Modal->>AddForm: useAddPostForm() 호출
+ AddForm-->>Modal: {value, change} 반환
+ Modal->>AddPost: useAddPost() 호출
+ AddPost-->>Modal: {submit, pending} 반환
+
+ Note over Page: 사용자가 '게시물 추가' 버튼 클릭
+ Page->>Modal: isOpen("addPost")
+
+ Note over Form: 사용자가 폼 입력
+ Form->>Modal: onChange 호출
+ Modal->>AddForm: change({field, value})
+ AddForm->>AddForm: setFormData 실행
+
+ Note over Form: 사용자가 제출 버튼 클릭
+ Form->>Modal: onSubmit 호출
+ Modal->>AddPost: submit(formData)
+ AddPost->>AddPost: mutation.mutateAsync 실행
+
+ Note over AddPost: 성공 시
+ AddPost->>AddPost: posts 쿼리 무효화
+ Modal->>Modal: close()
+```
+
+1. PostAddModal 내부로 formData의 state 옮겼습니다.
+2. PostAddModal 내부에서 상태 변화가 발생해도 PostsManagerPage까지 전파되지 않습니다.
+3. PostAddModal의 관심사를 PostAddModal 스스로 관리하도록 만들었습니다.
+
+### (2) Context/Provider: with Drag&Drop 기반의 시간표 관리 프로그램
+
+```jsx
+function App() {
+ return (
+
+
+
+
+
+
+
+ );
+}
+```
+
+```jsx
+export const useScheduleContext = () => {
+ const context = useContext(ScheduleContext);
+ if (context === undefined) {
+ throw new Error("useSchedule must be used within a ScheduleProvider");
+ }
+ return context;
+};
+
+export const ScheduleProvider = ({ children }) => {
+ const [schedulesMap, setSchedulesMap] = useState({});
+ const contextValue = { schedulesMap, setSchedulesMap };
+
+ return {children};
+};
+```
+
+```jsx
+export default function ScheduleDndProvider({ children }: PropsWithChildren) {
+ const { schedulesMap, setSchedulesMap } = useScheduleContext();
+ const sensors = useSensors(
+ useSensor(PointerSensor, {
+ activationConstraint: {
+ distance: 8,
+ },
+ })
+ );
+
+ const handleDragEnd = (event: any) => {
+ /* ...요약.... */
+ };
+
+ return (
+
+ {children}
+
+ );
+}
+```
+
+```jsx
+const ScheduleTable = ({ tableId, schedules, onScheduleTimeClick, onDeleteButtonClick }: Props) => {
+
+ ...
+
+ const dndContext = useDndContext();
+ const activeTableId = (() => {
+ const activeId = dndContext.active?.id;
+ return activeId ? String(activeId).split(":")[0] : null;
+ })();
+
+ return (
+
+
+
+ {schedules.map((schedule, index) => (
+
+ ))}
+
+ );
+};
+```
+
+```mermaid
+sequenceDiagram
+ participant App
+ participant ScheduleProvider
+ participant ScheduleDndProvider
+ participant ScheduleTables
+ participant ScheduleTable
+ participant DraggableSchedule
+
+ App->>ScheduleProvider: 1. Context 생성 (schedulesMap, setSchedulesMap)
+ ScheduleProvider->>ScheduleDndProvider: 2. DnD Context 초기화
+ ScheduleDndProvider->>ScheduleTables: 3. DnD 기능 제공
+ ScheduleTables->>ScheduleTable: 4. schedules 전달
+ ScheduleTable->>DraggableSchedule: 5. schedule, id 전달
+
+ DraggableSchedule->>ScheduleDndProvider: 6. Drag 이벤트 발생
+ ScheduleDndProvider->>ScheduleProvider: 7. handleDragEnd로 상태 업데이트
+ ScheduleProvider-->>ScheduleTables: 8. 변경된 상태 전파
+ ScheduleTables-->>ScheduleTable: 9. 변경된 schedules 전달
+ ScheduleTable-->>DraggableSchedule: 10. 변경된 위치 적용
+```
+
+1. 최상위 레이어에 Schedule 값을 관리하기 위한 Context를 정의했습니다.
+2. ScheduleTable에 렌더링된 시간표 개체를 드래그할 수 있습니다.
+3. ScheduleTable의 개체 하나를 드래그 하면 Drag/Drop 정보를 관리하는 Context에 접근하고, 개체를 움직시작하면 ScheduleContext와 DragContext에 각각 값이 업데이트 됩니다.
+4. ScheduleTable 한 개당 최소 24개의 컴포넌트 (네모 상자 묶음)를 가지고 있고, 전부 Context를 가져다 사용하고 있습니다.
+5. 드래그를 한 번 할 때 마다 모든 시간표 컴포넌트에 렌더링이 전파됩니다.
+6. 1회 드래그를 할 때 마다 200ms 이상의 렌더링 비용이 소모되고, 보통 드래그 기반의 인터랙션은 수십 회의 렌더링을 필요로 합니다.
+7. 결국 현재와 같은 시스텡믄 사용자가 어플리케이션을 사용하기 불가능한 수준의 상태가 됩니다.
+
+여기서 핵심은 Context의 관리방법입니다. Context의 값이 변경될 때, Context를 의존하는 모든 컴포넌트에 영향이 갑니다. 이를 최소화 하기 위해선 **Context를 작은 단위로 만들어서 관리**해야 합니다.
+
+먼저 drag가 다른 ScheduleTable로 전파되지 않도록 하는 방법입니다.
+
+```jsx
+function App() {
+ return (
+
+
+
+
+
+ );
+}
+```
+
+```jsx
+function ScheduleTables() {
+ const { schedulesMap, setSchedulesMap } = useScheduleContext();
+ return (
+
+ {Object.entries(schedulesMap).map(([tableId, schedules], index) => (
+
+
+
+
+
+
+ ))}
+
+ )
+}
+```
+
+ScheduleDndProvider를 최상위 레이어에 씌워놓는게 아니라, 각각의 ScheduleTable에 씌워서 관리하도록 합니다.
+
+ScheduleTable에서 Drag/Drop을 하더라도 다른 스케쥴로 전파되지 않도록 만들 수 있습니다.
+
+```mermaid
+sequenceDiagram
+ participant App
+ participant ScheduleProvider
+ participant ScheduleTables
+ participant ScheduleDndProvider
+ participant ScheduleTable
+ participant DraggableSchedule
+ participant DndContext
+
+ App->>ScheduleProvider: 1. Context 생성 (schedulesMap)
+ ScheduleProvider-->>ScheduleTables: 2. Context 제공
+ ScheduleTables->>+ScheduleDndProvider: 3. 각 테이블별 DnD 초기화
+ ScheduleDndProvider->>DndContext: 4. DnD Context 생성
+ ScheduleDndProvider-->>ScheduleTable: 5. DnD 기능 래핑
+ ScheduleTable->>DraggableSchedule: 6. schedule, id 전달
+
+ Note over DraggableSchedule: 드래그 시작
+ DraggableSchedule->>DndContext: 7. Drag 이벤트 발생
+ DndContext->>ScheduleDndProvider: 8. handleDragEnd 호출
+ ScheduleDndProvider->>ScheduleProvider: 9. setSchedulesMap으로 상태 갱신
+ ScheduleProvider-->>ScheduleTables: 10. 갱신된 상태 전파
+ ScheduleTables-->>ScheduleTable: 11. 갱신된 schedules 전달
+```
+
+이를 통해 알 수 있는 부분은, 인터랙션이 빈번하게 발생하는 환경에서 컨텍스트를 전역상태로 만들어서 관리하는 경우 컨텍스트의 변화가 전역에 전파되고 불필요한 렌더링이 많이 발생한다는 것입니다.
+
+그래서 컨텍스트는 작은 단위로 만들어서 관리해야 하고, 꼭 필요한 곳에서만 사용하도록 만들어야 불필요한 렌더링을 예방할 수 있습니다.
+
+```jsx
+function AppContextProvider({ children }) {
+ const [theme, setTheme] = useState("light");
+ const [items, setItems] = useState(generateItems(1000));
+ const [user, setUser] = useState(null);
+ const [notifications, setNotifications] = useState([]);
+
+ const toggleTheme = () => {
+ setTheme((prevTheme) => (prevTheme === "light" ? "dark" : "light"));
+ };
+
+ const addItems = () => {
+ setItems((prevItems) => [...prevItems, ...generateItems(1000, prevItems.length)]);
+ };
+
+ const login = (email: string) => {
+ setUser({ id: 1, name: "홍길동", email });
+ addNotification("성공적으로 로그인되었습니다", "success");
+ };
+
+ const logout = () => {
+ setUser(null);
+ addNotification("로그아웃되었습니다", "info");
+ };
+
+ const addNotification = (message, type) => {
+ const newNotification: Notification = {
+ id: Date.now(),
+ message,
+ type,
+ };
+ setNotifications((prev) => [...prev, newNotification]);
+ };
+
+ const removeNotification = (id) => {
+ setNotifications((prev) => prev.filter((notification) => notification.id !== id));
+ };
+
+ const contextValue = {
+ theme,
+ toggleTheme,
+ user,
+ login,
+ logout,
+ notifications,
+ addNotification,
+ removeNotification,
+ };
+
+ return {chidlren};
+}
+```
+
+- 테마, 사용자 데이터, 알림 등의 관심사가 뭉쳐져셔 관리되는 모습
+- 테마를 의존하는 컴포넌트 A가 있을 때, 사용자 데이터가 변경되어도 컴포넌트 A가 렌더링 됨
+
+```jsx
+function AppContextProvider({ children }) {
+ const [theme, setTheme] = useState("light");
+ const [user, setUser] = useState(null);
+ const [notifications, setNotifications] = useState([]);
+ const [items, setItems] = useState(() => generateItems(1000));
+
+ const addItems = useCallback(() => {
+ setItems((prevItems) => [
+ ...prevItems,
+ ...generateItems(1000, prevItems.length),
+ ]);
+ }, []);
+
+ const toggleTheme = useCallback(() => {
+ setTheme((prevTheme) => (prevTheme === "light" ? "dark" : "light"));
+ }, []);
+
+ const login = useCallback((email: string) => {
+ setUser({ id: 1, name: "홍길동", email });
+ addNotification("성공적으로 로그인되었습니다", "success");
+ }, []);
+
+ const logout = useCallback(() => {
+ setUser(null);
+ addNotification("로그아웃되었습니다", "info");
+ }, []);
+
+ const addNotification = useCallback(
+ (message: string, type: Notification["type"]) => {
+ const newNotification: Notification = {
+ id: Date.now(),
+ message,
+ type,
+ };
+ setNotifications((prev) => [...prev, newNotification]);
+ },
+ [],
+ );
+
+ const removeNotification = useCallback((id: number) => {
+ setNotifications((prev) =>
+ prev.filter((notification) => notification.id !== id),
+ );
+ }, []);
+
+ const themeContextValue = useMemo(
+ () => ({ theme, toggleTheme }),
+ [theme, toggleTheme],
+ );
+
+ const userContextValue = useMemo(
+ () => ({ user, login, logout }),
+ [user, login, logout],
+ );
+
+ const notificationContextValue = useMemo(
+ () => ({
+ notifications,
+ addNotification,
+ removeNotification,
+ }),
+ [notifications, addNotification, removeNotification],
+ );
+
+ return (
+
+
+
+ {children}
+
+
+
+ )
+}
+```
+
+- 관심사 단위로 컨텍스트를 분리하도록 하여 불필요한 렌더링을 방지하고, 코드에 대한 응집도를 높일 수 있습니다.
+
+```jsx
+function AppContextProvider({ children }) {
+ const themeContextValue = useThemeUseCase();
+ const userContextValue = useUserUseCase();
+ const notificationContextValue = useNotificationUseCase();
+
+ return (
+
+
+
+ {children}
+
+
+
+ );
+}
+
+const useUserContext = () => useContext(UserContext);
+const useThemeContext = () => useContext(ThemeContext);
+const useNotificationContext = () => useContext(NotificationContext);
+```
+
+- 분할된 컨텍스트 로직을 훅으로 묶어주는 모습
+
+### (3) Zustand: TodoList를 통해 알아보기
+
+이번에는 요구사항의 변화와 코드의 변화를 유심히 지켜봐주세요.
+
+**<할 일 관리 요구사항>**
+
+1. 사용자는 새로운 할 일을 추가할 수 있어야 함
+2. 사용자는 할 일을 완료 상태로 표시할 수 있어야 함
+3. 사용자는 할 일을 삭제할 수 있어야 함
+4. 사용자는 모든 할 일 목록을 볼 수 있어야 함
+
+```tsx
+export interface Todo {
+ id: string;
+ text: string;
+ completed: boolean;
+}
+
+interface TodoState {
+ todos: Todo[];
+ addTodo: (text: string) => void;
+ toggleTodo: (id: string) => void;
+ deleteTodo: (id: string) => void;
+}
+
+export const useTodoStore = create()(
+ persist(
+ (set) => ({
+ todos: [],
+ addTodo: (text) =>
+ set((state) => ({
+ todos: [...state.todos, { id: crypto.randomUUID(), text, completed: false }],
+ })),
+ toggleTodo: (id) =>
+ set((state) => ({
+ todos: state.todos.map((todo) =>
+ todo.id === id ? { ...todo, completed: !todo.completed } : todo
+ ),
+ })),
+ deleteTodo: (id) =>
+ set((state) => ({
+ todos: state.todos.filter((todo) => todo.id !== id),
+ })),
+ }),
+ {
+ name: "todo-storage",
+ }
+ )
+);
+```
+
+TodoStore 내부에서 todos 값과 todos를 변경시키는 함수가 응집되어있습니다.
+
+```tsx
+export function TodoForm() {
+ const [text, setText] = useState("");
+ const addTodo = useTodoStore((state) => state.addTodo);
+
+ const handleSubmit = (e: React.FormEvent) => {
+ e.preventDefault();
+ if (text.trim()) {
+ addTodo(text);
+ setText("");
+ }
+ };
+
+ return (
+
+ );
+}
+```
+
+- TodoForm에서는 addTodo를 가져와서 사용합니다.
+
+```tsx
+export function TodoList() {
+ const todos = useTodoStore((state) => state.todos);
+
+ if (todos.length === 0) {
+ return (
+
+ );
+}
+```
+
+redux, jotai 등 전역상태 라이브러리는 selector를 토대로, 가져오는 값이 변경될 때에만 렌더링이 발생하도록 합니다.
+
+selector를 사용하지 않을 경우, 상태 변이가 더 예민하게 반응하고 렌더링이 쉽게 전파됩니다.
+
+```jsx
+export function TodoList({ id }: TodoListProps) {
+ const { todosUserMap } = useTodoStore();
+ const { userName, todos } = todosUserMap[id]
+
+ return (...)
+}
+```
+
+```jsx
+export function TodoList({ id }: TodoListProps) {
+ const { userName, todos } = useTodoStore(state => state.todosUserMap[id])
+
+ return (...)
+}
+```
+
+AS-IS: todosUserMap이 변화할 때 마다 렌더링이 발생
+
+TO-BE: todosUserMap[id]가 변화할 때 렌더링이 발생
+
+그리고 `useStore` `useStoreWithEqualityFn` 등을 사용하는 방법도 있습니다.
+
+```jsx
+const getTodoIds = useTodoStore((state) => state.getTodoIds);
+const listIds = getTodoIds();
+```
+
+```jsx
+const listIds = useStore(useTodoStore, (state) => Object.keys(state.todosUserMap));
+```
+
+```jsx
+const listIds = useStoreWithEqualityFn(
+ useTodoStore,
+ (state) => Object.keys(state.todosUserMap),
+ (a, b) => JSON.stringify(a) === JSON.stringify(b)
+);
+```
+
+jotai, redux, zustand 를 같이 비교해보겠습니다.
+
+```jsx
+import { configureStore, createSlice } from "@reduxjs/toolkit";
+import { useSelector } from "react-redux";
+import { createSelector } from "reselect";
+
+// Redux 초기 상태 및 슬라이스 생성 (리듀서 없이)
+const todosSlice = createSlice({
+ name: "todos",
+ initialState: {
+ todosMap: todosUserMap,
+ },
+ reducers: {},
+});
+
+// 스토어 설정
+const store = configureStore({
+ reducer: {
+ todos: todosSlice.reducer,
+ },
+});
+
+// Redux 셀렉터 생성
+// 1. 기본 셀렉터 - 전체 데이터 맵 가져오기
+const selectTodosMap = (state) => state.todos.todosMap;
+
+// 2. 특정 사용자의 할 일 목록을 가져오는 셀렉터
+const selectUserTodoList = (state, userId) => {
+ const userListId = Object.values(state.todos.todosMap).find((list) => list.userId === userId)?.id;
+ return userListId ? state.todos.todosMap[userListId] : null;
+};
+
+// 3. 모든 사용자 이름 목록을 가져오는 셀렉터
+const selectUserNames = (state) => Object.values(state.todos.todosMap).map((list) => list.userName);
+
+// 4. 특정 사용자의 완료된 할 일 수를 가져오는 셀렉터
+const selectCompletedTodoCount = (state, userId) => {
+ const userListId = Object.values(state.todos.todosMap).find((list) => list.userId === userId)?.id;
+
+ if (!userListId) return 0;
+ return state.todos.todosMap[userListId].todos.filter((todo) => todo.completed).length;
+};
+
+// 5. 메모이제이션된 셀렉터 (Reselect 사용)
+const selectTotalTodoCount = createSelector([selectTodosMap], (todosMap) =>
+ Object.values(todosMap).reduce((total, list) => total + list.todos.length, 0)
+);
+
+// 6. 특정 리스트ID로 직접 데이터 가져오는 셀렉터
+const selectSpecificList = (state, listId) => state.todos.todosMap[listId];
+
+// Redux 셀렉터 사용 예시 (컴포넌트 없이)
+// useSelector 호출은 실제로는 컴포넌트 내부에서 이루어져야 하지만, 예시를 위해 표현
+const userId = "user2";
+const listId = listIds[1];
+
+const todoMap = useSelector(selectTodosMap);
+const userTodoList = useSelector((state) => selectUserTodoList(state, userId));
+const userNames = useSelector(selectUserNames);
+const completedCount = useSelector((state) => selectCompletedTodoCount(state, userId));
+const totalTodos = useSelector(selectTotalTodoCount);
+const specificList = useSelector((state) => selectSpecificList(state, listId));
+```
+
+```jsx
+import { atom, useAtomValue } from "jotai";
+import { selectAtom } from "jotai/utils";
+
+// 기본 아톰 생성
+const todosMapAtom = atom(todosUserMap);
+
+// 파생 아톰 및 셀렉터 생성
+// 1. 특정 사용자 할 일 목록을 가져오는 아톰
+const userTodoListAtom = (userId) =>
+ atom((get) => {
+ const todosMap = get(todosMapAtom);
+ const userListId = Object.values(todosMap).find((list) => list.userId === userId)?.id;
+ return userListId ? todosMap[userListId] : null;
+ });
+
+// 2. 모든 사용자 이름 목록을 가져오는 아톰
+const userNamesAtom = atom((get) => Object.values(get(todosMapAtom)).map((list) => list.userName));
+
+// 3. 특정 사용자의 완료된 할 일 수를 가져오는 아톰
+const completedTodoCountAtom = (userId) =>
+ atom((get) => {
+ const todosMap = get(todosMapAtom);
+ const userListId = Object.values(todosMap).find((list) => list.userId === userId)?.id;
+
+ if (!userListId) return 0;
+ return todosMap[userListId].todos.filter((todo) => todo.completed).length;
+ });
+
+// 4. 모든 사용자의 할 일 총 개수를 가져오는 아톰
+const totalTodoCountAtom = atom((get) =>
+ Object.values(get(todosMapAtom)).reduce((total, list) => total + list.todos.length, 0)
+);
+
+// 5. 특정 리스트ID로 직접 데이터 가져오는 아톰
+const specificListAtom = (listId) => atom((get) => get(todosMapAtom)[listId]);
+
+// 6. selectAtom 유틸리티를 사용한 셀렉터 예시
+const completedTodosAtom = selectAtom(todosMapAtom, (todosMap) => {
+ const result = {};
+ Object.entries(todosMap).forEach(([listId, list]) => {
+ result[listId] = list.todos.filter((todo) => todo.completed);
+ });
+ return result;
+});
+
+// Jotai 셀렉터 사용 예시 (컴포넌트 없이)
+// useAtomValue 호출은 실제로는 컴포넌트 내부에서 이루어져야 하지만, 예시를 위해 표현
+const userId = "user2";
+const listId = listIds[1];
+
+const todosMap = useAtomValue(todosMapAtom);
+const userTodoList = useAtomValue(userTodoListAtom(userId));
+const userNames = useAtomValue(userNamesAtom);
+const completedCount = useAtomValue(completedTodoCountAtom(userId));
+const totalTodos = useAtomValue(totalTodoCountAtom);
+const specificList = useAtomValue(specificListAtom(listId));
+const completedTodos = useAtomValue(completedTodosAtom);
+```
+
+### Summary
+
+**<추가로 고민해보면 좋은 지점>**
+
+- useReducer, zustand, redux 같은 것들을 사용하는 경우 react 렌더링 시스템에 대한 의존성을 낮출 수 있고, 이는 테스트를 효과적으로 관리할 수 있도록 해줍니다.
+- 상태관리에서 발생하는 대부분의 문제는 “결합도”가 높고 “응집도”가 낮은 모습과 관련 있습니다. 상태의 응집도를 높이면 자연스럽게 렌더링도 응집도 있게 관리할 수 있게 됩니다.
+- 응집도를 높이기 위해 custom hook을 적극적으로 사용하면 좋습니다. custom hook을 재활용을 목적으로 구성할 수도 있지만 관심사를 분리하고 응집도를 높이기 위해 custom hook을 활용할 수 있습니다.
+- 상태관리의 코드를 변경하지 않고 렌더링을 최적화 하려면 useMemo, useCallback, memo 등을 적극적으로 사용해야 합니다. 하지만 이는 불필요한 비용을 소모하게 만들 수 있으므로 가능하다면 상태관리 시스템을 잘 구축하여 불필요한 메모이제이션을 남발하지 않도록 해야합니다.
+
+****
+
+- 컨텍스트를 최적화 하려면 컨텍스트에서 관리하는 값을 작게 쪼개서 컨텍스트의 종류를 많이 만들어야 합니다.
+- 컨텍스트를 참조하는 컴포넌트를 최소화 해야합니다.
+- 상태관리 라이브러리는 정의는 큰 덩어리로 해도, 상태를 작은 단위로 가져와서 사용할 수 있는 장치를 제공합니다.
+- react에서 제공하는 zustand, redux, jotai 등은 useSyncExternalStore와 useSyncExternalStoreWithSelector를 기반으로 만들어졌습니다. 이를 이용하면 직접 상태관리 라이브러리를 만들 수도 있습니다.
+- context는 어플리케이션의 상태를 관리하기보단, 컴포넌트 그룹의 UI 상태를 관리하는 방식으로 쓰면 효과적입니다. (컴파운드 컴포넌트)
diff --git a/.claude/docs/4-2-1.Front-end Performance Optimization Catalog.md b/.claude/docs/4-2-1.Front-end Performance Optimization Catalog.md
new file mode 100644
index 0000000..261d8ac
--- /dev/null
+++ b/.claude/docs/4-2-1.Front-end Performance Optimization Catalog.md
@@ -0,0 +1,114 @@
+## 참고자료
+
+[프론트엔드에서 가장 중요한데 가장 안하는거](https://www.youtube.com/watch?v=_8deUuO0iJ8)
+
+[FE Guide](https://ui.toast.com/fe-guide/ko)
+
+## 1. 코드 레벨 최적화
+
+### 1.1 JavaScript 최적화
+
+- 코드 분할 및 레이지 로딩
+- 불필요한 렌더링 최소화 (React의 메모이제이션 등)
+- 이벤트 델리게이션
+- 디바운싱과 쓰로틀링
+- 메모리 누수 방지
+
+### 1.2 CSS 최적화
+
+- 크리티컬 CSS 인라인화
+- CSS 애니메이션 최적화 (transform, opacity 사용)
+- 불필요한 스타일 제거
+
+### 1.3 HTML 최적화
+
+- DOM 구조 최적화
+- 적절한 시맨틱 태그 사용
+
+### 1.4 리소스 최적화
+
+- 이미지, 폰트 등 미디어 자원 최적화
+- 스프라이트 이미지 사용
+
+### 1.5 렌더링 최적화
+
+- 가상 스크롤링 구현
+- 레이아웃 스래싱 방지
+- will-change 속성 사용
+
+### 1.6 프레임워크 특화 최적화
+
+- React, Vue, Angular 등 프레임워크별 최적화 기법 적용
+
+## 2. 인프라 레벨 최적화
+
+### 2.1 서버 최적화
+
+- 서버 사이드 렌더링 (SSR) 구현
+- 정적 사이트 생성 (SSG) 활용
+- API 응답 최적화
+
+### 2.2 네트워크 최적화
+
+- CDN 활용
+- HTTP/2, HTTP/3 활용
+- 압축 (Gzip, Brotli 등) 적용
+
+### 2.3 캐싱 전략
+
+- 브라우저 캐시 설정 최적화
+- 서비스 워커 구현
+- 메모리 캐시, 디스크 캐시 활용
+
+### 2.4 로드 밸런싱
+
+- 트래픽 분산을 통한 응답 시간 개선
+
+### 2.5 데이터베이스 최적화
+
+- 쿼리 최적화
+- 인덱싱 전략
+
+### 2.6 컨테이너화 및 마이크로서비스
+
+- Docker를 이용한 컨테이너화
+- 마이크로서비스 아키텍처 도입
+
+### 2.7 클라우드 서비스 활용
+
+- Auto-scaling 구현
+- 서버리스 아키텍처 활용
+
+## 3. 통합적 접근
+
+### 3.1 성능 모니터링 및 분석
+
+- 실시간 모니터링 도구 활용
+- 로그 분석 및 성능 메트릭 추적
+
+### 3.2 보안 최적화
+
+- HTTPS 적용
+- 컨텐츠 보안 정책 (CSP) 설정
+
+### 3.3 프로그레시브 웹 앱 (PWA)
+
+- 서비스 워커를 통한 오프라인 기능 구현
+- 푸시 알림 활용
+
+| 특성 | 코드 레벨 최적화 | 인프라 레벨 최적화 |
+| ------------ | ------------------------------------- | ---------------------------------- |
+| 주요 영역 | JavaScript, CSS, HTML, 리소스 관리 | 서버, 네트워크, 캐싱, 데이터베이스 |
+| 구현 주체 | 프론트엔드 개발자 | 백엔드 개발자, DevOps 엔지니어 |
+| 구현 난이도 | 중간 ~ 높음 | 높음 |
+| 적용 범위 | 특정 애플리케이션 | 전체 시스템 또는 여러 애플리케이션 |
+| 변경 주기 | 빈번함 | 상대적으로 덜 빈번함 |
+| 즉시성 | 빠른 적용 및 결과 확인 가능 | 적용 및 결과 확인에 시간 소요 |
+| 확장성 | 제한적 | 높음 |
+| 비용 효율성 | 대체로 저비용 | 초기 투자 비용이 높을 수 있음 |
+| 성능 영향 | 로컬 성능 향상에 직접적 | 전체 시스템 성능에 광범위한 영향 |
+| 유지보수 | 지속적인 코드 관리 필요 | 설정 후 상대적으로 안정적 |
+| 팀 협업 | 주로 프론트엔드 팀 내 | 여러 팀 간 협업 필요 |
+| 도구 및 기술 | 번들러, 프레임워크, 최적화 라이브러리 | 클라우드 서비스, CDN, 캐싱 솔루션 |
+| 주요 이점 | 세밀한 성능 튜닝 가능 | 대규모 성능 향상 및 안정성 개선 |
+| 제한 사항 | 브라우저/디바이스 제약 | 인프라 제약, 비용 |
diff --git a/.claude/docs/4-2-2.To measure performance.md b/.claude/docs/4-2-2.To measure performance.md
new file mode 100644
index 0000000..7034c8b
--- /dev/null
+++ b/.claude/docs/4-2-2.To measure performance.md
@@ -0,0 +1,198 @@
+# 참고자료
+
+[Bora Lee on LinkedIn: [프론트엔드 성능 모니터링(측정편) - 1] 현대 경영학의 아버지 피터 드러커는 “측정할 수 없으면 관리할 수 없고, 관리할 수…](https://www.linkedin.com/posts/learner-bora_프론트엔드-성능-모니터링측정편-1-현대-경영학의-아버지-피터-activity-7226187837338009600-FtBz?utm_source=share&utm_medium=member_desktop)
+
+[Bora Lee on LinkedIn: [프론트엔드 성능 모니터링(측정편) - 2] 앞서 소개한 PageSpeed Insights의 경우 여러 가지 장점이 있으나, 실제…](https://www.linkedin.com/posts/learner-bora_프론트엔드-성능-모니터링측정편-2-앞서-소개한-pagespeed-activity-7226655481723858944-CCBz?utm_source=share&utm_medium=member_desktop)
+
+[Core Web Vitals - Chrome Web Store](https://chromewebstore.google.com/detail/core-web-vitals/adeniimnihmbpgpbljmnohjpoolmgabj)
+
+[웹 바이탈 확장 프로그램을 사용하여 코어 웹 바이탈 문제 디버그 | Articles | web.dev](https://web.dev/articles/debug-cwvs-with-web-vitals-extension?hl=ko)
+
+[](https://developer.mozilla.org/en-US/docs/Web/API/Performance_API/Using_the_Performance_API)
+
+[Profiler – React](https://react.dev/reference/react/Profiler)
+
+[실적 패널: 웹사이트 실적 분석 | Chrome DevTools | Chrome for Developers](https://developer.chrome.com/docs/devtools/performance/overview?hl=ko)
+
+[성능 최적화](https://ui.toast.com/fe-guide/ko_PERFORMANCE)
+
+[](https://www.webpagetest.org/)
+
+[커스텀 측정항목 | Articles | web.dev](https://web.dev/articles/custom-metrics?hl=ko)
+
+# 1. 프론트엔드 성능의 종류와 측정 방법
+
+[사용자 중심 성능 측정항목 | Articles | web.dev](https://web.dev/articles/user-centric-performance-metrics?hl=ko)
+
+## (1) 측정할 수 있는 성능의 종류
+
+### 1) 로딩 성능
+
+- **First Contentful Paint (FCP):** 페이지의 첫 번째 콘텐츠(텍스트, 이미지 등)가 렌더링되는 시간을 측정합니다. 사용자가 페이지가 **로딩되었음을 인지할 수 있는 첫 시점**입니다.
+ [First Contentful Paint (FCP) | Articles | web.dev](https://web.dev/articles/fcp?hl=ko)
+- **Largest Contentful Paint (LCP):** 가장 큰 콘텐츠가 렌더링되는 시간으로, 사용자가 페이지가 **거의 로드되었다고 느끼는 시점**을 반영합니다.
+ [Largest Contentful Paint (LCP) | Articles | web.dev](https://web.dev/articles/lcp?hl=ko)
+- **Interaction to Next Paint(INP)**: INP는 사용자가 페이지와 함께 한 모든 **상호작용**의 지연 시간을 관찰하고 모든 상호작용 또는 거의 모든 상호작용이 아래에 있는 단일 값을 보고합니다.
+ [Interaction to Next Paint(다음 페인트와의 상호작용)(INP) | Articles | web.dev](https://web.dev/articles/inp?hl=ko)
+- **Time to Interactive (TTI):** 페이지가 완전히 상호작용 가능해지는 시점까지의 시간입니다. 모든 주요 리소스가 로드되고 이벤트 핸들러가 설정된 시점입니다.
+ [상호작용 시작 시간 (TTI) | Articles | web.dev](https://web.dev/articles/tti?hl=ko)
+- **Total Blocking Time (TBT):** 페이지 로딩 중 사용자가 인터랙션을 시도할 때 발생하는 지연 시간을 측정합니다.
+ [총 차단 시간 (TBT) | Articles | web.dev](https://web.dev/articles/tbt?hl=ko)
+- **Speed Index:** 페이지의 시각적 콘텐츠가 얼마나 빨리 완전히 로드되는지를 측정하는 지표입니다.
+ [속도 색인 | Lighthouse | Chrome for Developers](https://developer.chrome.com/docs/lighthouse/performance/speed-index?hl=ko)
+
+### 2) 렌더링 성능
+
+- **Frame Rate (FPS):** 애니메이션이나 스크롤 시의 부드러움을 나타내며, 초당 프레임 수를 측정합니다.
+- **Cumulative Layout Shift (CLS):** 페이지가 로드되는 동안 발생하는 예상치 못한 레이아웃 변경의 빈도를 측정합니다. 사용자가 예상치 못한 움직임을 경험하게 되는 경우가 줄어들도록 도와줍니다.
+ [Cumulative Layout Shift (CLS) | Articles | web.dev](https://web.dev/articles/cls?hl=ko)
+- **Long Tasks:** 메인 스레드에서 실행되는 긴 작업(50ms 이상)으로 인해 페이지 응답성이 저하되는 시간을 측정합니다.
+ [장기 작업 최적화 | Articles | web.dev](https://web.dev/articles/optimize-long-tasks?hl=ko)
+
+### 3) 자바스크립트 성능
+
+- **Execution Time:** 자바스크립트 코드가 실행되는 시간을 측정하여 성능을 분석합니다.
+- **Memory Usage:** 자바스크립트 코드가 사용하고 있는 메모리의 양을 측정합니다. 메모리 누수가 없는지, 효율적으로 메모리가 사용되고 있는지를 파악할 수 있습니다.
+ [할당 프로파일러 도구 사용 방법 | Chrome DevTools | Chrome for Developers](https://developer.chrome.com/docs/devtools/memory-problems/allocation-profiler?hl=ko)
+ [measureUserAgentSpecificMemory()로 웹페이지의 총 메모리 사용량을 모니터링합니다. | Articles | web.dev](https://web.dev/articles/monitor-total-page-memory-usage?hl=ko)
+ [JavaScript의 메모리 관리 - JavaScript | MDN](https://developer.mozilla.org/ko/docs/Web/JavaScript/Memory_management)
+ [당신이 모르는 자바스크립트의 메모리 누수의 비밀](https://ui.toast.com/weekly-pick/ko_20210611)
+- **Garbage Collection 빈도 및 지속 시간:** 가비지 컬렉터가 메모리를 정리하는 빈도와 그 과정에서의 시간 소모를 측정합니다.
+ [가비지 컬렉션](https://ko.javascript.info/garbage-collection)
+
+### 4) 네트워크 성능
+
+- **Resource Size:** 페이지에 필요한 리소스(이미지, CSS, JS 파일 등)의 크기를 측정합니다. 파일 크기가 클수록 로드 시간이 증가할 수 있습니다.
+- **Number of Requests:** 페이지 로딩 시 발생하는 네트워크 요청 수를 측정합니다. 요청 수가 많을수록 페이지 로드 속도가 느려질 수 있습니다.
+- **Time to First Byte (TTFB):** 사용자가 요청을 보낸 후, 서버에서 첫 번째 바이트를 받기까지 걸리는 시간을 측정합니다. 서버 응답 시간을 개선하는 데 중요한 지표입니다.
+
+## (2) 성능을 측정하는 도구
+
+### 1) 브라우저 내장 도구
+
+- **Chrome DevTools**
+ - **Performance 탭:** 전체 페이지 성능을 분석할 수 있으며, 로딩 성능, 자바스크립트 실행 시간, 렌더링 성능 등을 상세히 살펴볼 수 있습니다.
+ [실적 패널: 웹사이트 실적 분석 | Chrome DevTools | Chrome for Developers](https://developer.chrome.com/docs/devtools/performance/overview?hl=ko)
+ - **Network 탭:** 페이지의 모든 네트워크 요청을 분석하여 로딩 성능을 최적화할 수 있습니다.
+ [네트워크 패널: 네트워크 부하 및 리소스 분석 | Chrome DevTools | Chrome for Developers](https://developer.chrome.com/docs/devtools/network/overview?hl=ko)
+ - **Memory 탭:** 메모리 사용량과 가비지 컬렉션을 분석하여 자바스크립트 성능을 최적화할 수 있습니다.
+ [메모리 패널 개요 | Chrome DevTools | Chrome for Developers](https://developer.chrome.com/docs/devtools/memory?hl=ko)
+- **Lighthouse:** 웹 페이지의 성능, 접근성, SEO 등을 종합적으로 평가하는 도구입니다. 자동화된 보고서를 통해 성능을 개선할 수 있는 방법을 제시합니다.
+ [개요 | Lighthouse | Chrome for Developers](https://developer.chrome.com/docs/lighthouse/overview?hl=ko)
+
+### 2) 외부 성능 측정 도구
+
+- **WebPageTest:** 웹 페이지의 성능을 다양한 환경에서 테스트할 수 있는 도구로, 상세한 성능 분석 결과를 제공합니다.
+ [](https://www.webpagetest.org/)
+ [웹페이지 성능/속도측정 - www.webpagetest.org](https://test-myid.tistory.com/4)
+- **Google Search Console:** 사이트의 기본 성능 이외에 검색 성능등을 모니터링하고 최적화 하는데 사용하는 무료도구로, 웹사이트 소유자만 접근 가능합니다. 성능 이외에도 SEO 최적화를 위한 모니터링과 수단을 설명해줍니다.
+ [](https://search.google.com/search-console/welcome)
+- **GTmetrix:** 페이지 로딩 속도와 관련된 다양한 지표를 제공하며, 개선 사항을 제안해 줍니다.
+ [GTmetrix | Website Performance Testing and Monitoring](https://gtmetrix.com/)
+ [내 웹사이트 로딩 속도 측정 체크 방법1 - gtmetrix.com - 사이트 최적화 하기](https://blog.naver.com/myappkorea/222226836260)
+- **PageSpeed Insights:** Google에서 제공하는 도구로, 모바일과 데스크톱 환경에서의 페이지 성능을 평가합니다.
+ [PageSpeed Insights](https://pagespeed.web.dev/?hl=ko)
+ [맞춤형 풀스택 옵저버빌리티 및 보안 | Datadog](https://www.datadoghq.com/ko/dg/monitor/personalized-demo-request/?utm_source=google&utm_medium=paid-search&utm_campaign=dg-brand-apac-ko-brand&utm_keyword=datadog&utm_matchtype=p&igaag=158938765812&igaat=&igacm=20569180520&igacr=674510540631&igakw=datadog&igamt=p&igant=g&utm_campaignid=20569180520&utm_adgroupid=158938765812&gad_source=1)
+ [PageSpeed Insights 정보 | Google for Developers](https://developers.google.com/speed/docs/insights/v5/about?hl=ko)
+
+### 3) 성능 모니터링 서비스
+
+- **New Relic:** 애플리케이션 성능을 실시간으로 모니터링하고, 문제 발생 시 알림을 제공합니다. 서버 성능까지 포함하여 전체적인 성능 분석이 가능합니다.
+ [Monitor, Debug and Improve Your Entire Stack](https://newrelic.com/)
+ [여기어때 서비스 모니터링 : New Relic 이야기](https://techblog.gccompany.co.kr/여기어때-서비스-모니터링-new-relic-이야기-dad36583dec4)
+- **Datadog:** 애플리케이션, 서버, 데이터베이스 등 다양한 환경의 성능을 모니터링할 수 있으며, 통합된 대시보드를 제공합니다.
+ [Datadog 소개](https://velog.io/@kameals/system-monitoring-datadog-intro)
+- **Sentry:** 자바스크립트 오류 추적 및 성능 모니터링에 특화된 도구로, 버그와 성능 문제를 실시간으로 파악할 수 있습니다.
+ [Application Performance Monitoring & Error Tracking Software](https://sentry.io/welcome/)
+ [Sentry로 우아하게 프론트엔드 에러 추적하기 | 카카오페이 기술 블로그](https://tech.kakaopay.com/post/frontend-sentry-monitoring/)
+ [Sentry로 사내 에러 로그 수집 시스템 구축하기](https://engineering.linecorp.com/ko/blog/log-collection-system-sentry-on-premise)
+ [프론트엔드 에러 로그 시스템 Sentry 적용기](https://urbanbase.github.io/dev/2021/03/04/Sentry.html)
+- Azure Application Insights: Microsoft Azure 클라우드 플랫폼의 일부로, 웹 애플리케이션의 성능과 사용량을 모니터링하고 분석하는 도구입니다. 상대적으로 간단한 설정으로 설치 가능하고, Azure 인프라를 사용하는 경우 데이터 적재 등의 연동에 유리합니다.
+ [Application Insights 개요 - Azure Monitor](https://learn.microsoft.com/ko-kr/azure/azure-monitor/app/app-insights-overview)
+
+## (3) 코드를 통해서 직접 성능을 측정해보기
+
+### 1) Web API를 이용한 성능 측정
+
+```jsx
+// Performance API를 사용한 시간 측정
+const startTime = performance.now();
+
+// 측정하고자 하는 작업 수행
+heavyComputation();
+
+const endTime = performance.now();
+console.log(`작업 실행 시간: ${endTime - startTime} 밀리초`);
+
+// Navigation Timing API를 사용한 페이지 로드 시간 측정
+window.addEventListener("load", () => {
+ const navigationTiming = performance.getEntriesByType("navigation")[0];
+ console.log(
+ `페이지 로드 시간: ${navigationTiming.loadEventEnd - navigationTiming.navigationStart} 밀리초`
+ );
+});
+```
+
+```bash
+# 결과물 예시
+작업 실행 시간: 543.2 밀리초
+페이지 로드 시간: 1245.7 밀리초
+```
+
+### 2) Core Web Vitals 측정
+
+```jsx
+const observer = new PerformanceObserver((list) => {
+ for (const entry of list.getEntries()) {
+ if (entry.entryType === "largest-contentful-paint") {
+ console.log(`LCP: ${entry.startTime}`);
+ }
+ if (entry.entryType === "layout-shift") {
+ console.log(`CLS: ${entry.value}`);
+ }
+ if (entry.entryType === "first-input") {
+ console.log(`FID: ${entry.processingStart - entry.startTime}`);
+ }
+ }
+});
+
+observer.observe({ entryTypes: ["largest-contentful-paint", "layout-shift", "first-input"] });
+```
+
+```bash
+# 결과물 예시
+LCP: 2345.6
+CLS: 0.1
+FID: 95.3
+```
+
+### 3) React 성능 프로파일링
+
+```jsx
+import React, { Profiler } from "react";
+
+function onRenderCallback(
+ id,
+ phase,
+ actualDuration,
+ baseDuration,
+ startTime,
+ commitTime,
+ interactions
+) {
+ console.log(`컴포넌트 ${id}의 렌더링 시간: ${actualDuration}`);
+}
+
+function MyComponent() {
+ return (
+
+ {/* 컴포넌트 내용 */}
+
+ );
+}
+```
+
+```bash
+# 결과물 예시
+컴포넌트 MyComponent의 렌더링 시간: 78.5
+```
diff --git a/.claude/docs/4-2-3. Code Perspective Performance Optimization Case.md b/.claude/docs/4-2-3. Code Perspective Performance Optimization Case.md
new file mode 100644
index 0000000..f364881
--- /dev/null
+++ b/.claude/docs/4-2-3. Code Perspective Performance Optimization Case.md
@@ -0,0 +1,1405 @@
+## 코드 분할 및 레이지 로딩 최적화 사례
+
+### AS-IS
+
+대규모 싱글 페이지 애플리케이션(SPA)에서 모든 JavaScript 코드를 하나의 큰 번들로 제공하여 초기 로딩 시간이 매우 길었습니다.
+
+```jsx
+// App.js
+import React from "react";
+import { BrowserRouter as Router, Route, Switch } from "react-router-dom";
+import Home from "./components/Home";
+import About from "./components/About";
+import Contact from "./components/Contact";
+import LargeComponent1 from "./components/LargeComponent1";
+import LargeComponent2 from "./components/LargeComponent2";
+
+function App() {
+ return (
+
+
+
+
+
+
+
+
+
+ );
+}
+
+export default App;
+```
+
+### TO-BE
+
+코드 분할과 레이지 로딩을 적용하여 필요한 코드만 로드하도록 변경했습니다.
+
+- Why: 초기 로딩 시간을 줄이고 사용자 경험을 개선하기 위해
+- How:
+ ```jsx
+ // App.js
+ import React, { Suspense, lazy } from "react";
+ import { BrowserRouter as Router, Route, Switch } from "react-router-dom";
+
+ const Home = lazy(() => import("./components/Home"));
+ const About = lazy(() => import("./components/About"));
+ const Contact = lazy(() => import("./components/Contact"));
+ const LargeComponent1 = lazy(() => import("./components/LargeComponent1"));
+ const LargeComponent2 = lazy(() => import("./components/LargeComponent2"));
+
+ function App() {
+ return (
+
+ Loading...}>
+
+
+
+
+
+
+
+
+
+ );
+ }
+
+ export default App;
+ ```
+ 1. React.lazy()와 Suspense를 사용하여 컴포넌트 레벨에서 코드 분할 구현
+ 2. 웹팩의 dynamic import를 활용하여 라우트 기반 코드 분할 적용
+ 3. 중요하지 않은 리소스는 Intersection Observer API를 사용하여 레이지 로딩 구현
+- Result:
+ 1. 초기 로딩 시간 감소
+ 2. Time to Interactive(TTI) 개선
+ 3. 전체 페이지 중량 감소
+
+### 참고자료
+
+- React 공식 문서: Code-Splitting (https://reactjs.org/docs/code-splitting.html)
+- 웹팩 문서: Lazy Loading (https://webpack.js.org/guides/lazy-loading/)
+
+---
+
+## 웹 폰트 FOUT/FOIT 최적화 사례
+
+### AS-IS
+
+웹 폰트 로딩 중 FOUT(Flash of Unstyled Text) 또는 FOIT(Flash of Invisible Text) 현상으로 사용자 경험이 저하되었습니다.
+
+```jsx
+/* styles.css */
+@font-face {
+ font-family: 'CustomFont';
+ src: url('custom-font.woff2') format('woff2');
+}
+
+body {
+ font-family: 'CustomFont', sans-serif;
+}
+```
+
+### TO-BE
+
+폰트 로딩 전략을 최적화하여 FOUT/FOIT 현상을 최소화하고 일관된 사용자 경험을 제공했습니다.
+
+- Why: 텍스트 깜빡임 현상을 줄이고 일관된 타이포그래피를 제공하기 위해
+- How:
+ ```css
+ /* styles.css */
+ @font-face {
+ font-family: "CustomFont";
+ src: url("custom-font.woff2") format("woff2");
+
+ /*
+ font-display: swap을 사용하여 폰트가 로드되는 동안 시스템 폰트를 표시하고,
+ FontFace API를 사용하여 폰트 로딩을 프로그래매틱하게 제어합니다.
+ 이를 통해 텍스트가 보이지 않는 현상을 방지하고 일관된 사용자 경험을 제공할 수 있습니다.
+ */
+ font-display: swap;
+ }
+
+ body {
+ font-family: "CustomFont", Arial, sans-serif;
+ }
+ ```
+ ```jsx
+ // font-loader.js
+ if ("fonts" in document) {
+ const font = new FontFace("CustomFont", "url(custom-font.woff2)");
+ font.load().then(() => {
+ document.fonts.add(font);
+ document.body.classList.add("fonts-loaded");
+ });
+ }
+ ```
+ 1. font-display 속성 최적화 (swap, optional, fallback 등)
+ 2. FontFace API를 사용한 프로그래매틱 폰트 로딩
+ 3. 로컬 폰트와 유사한 폴백 폰트 지정
+ 4. FOFT(Flash of Faux Text) 기법 적용
+- Result:
+ 1. 사용자가 인지하는 폰트 변경 깜빡임 현상 감소
+ 2. 초기 컨텐츠 가시성 향상
+ 3. 일관된 타이포그래피로 인한 사용자 경험 개선
+
+### 참고자료
+
+- CSS-Tricks: FOUT, FOIT, FOFT (https://css-tricks.com/fout-foit-foft/)
+- web.dev: Avoid invisible text during font loading (https://web.dev/avoid-invisible-text/)
+
+---
+
+## 이벤트 위임(Event Delegation) 최적화 사례
+
+### AS-IS
+
+많은 수의 요소에 개별적으로 이벤트 리스너를 추가하여 메모리 사용량이 높고 성능이 저하되었습니다.
+
+```html
+
+
+
+
상품 1
+
가격: 10,000원
+
+
+
+
상품 2
+
가격: 20,000원
+
+
+
+
+```
+
+```jsx
+// 모든 "장바구니 담기" 버튼에 개별적으로 이벤트 리스너 추가
+const addToCartButtons = document.querySelectorAll(".add-to-cart");
+
+addToCartButtons.forEach((button) => {
+ button.addEventListener("click", function () {
+ const productId = this.closest(".product-card").dataset.productId;
+ addToCart(productId);
+ });
+});
+
+function addToCart(productId) {
+ console.log(`상품 ${productId}를 장바구니에 담았습니다.`);
+ // 실제 장바구니 추가 로직...
+}
+```
+
+### TO-BE
+
+이벤트 위임 패턴을 적용하여 이벤트 핸들링을 최적화했습니다.
+
+- Why: 메모리 사용량을 줄이고 동적으로 추가되는 요소들의 이벤트 처리를 효율적으로 하기 위해
+- How:
+ ```jsx
+ // 상품 목록 컨테이너에 단일 이벤트 리스너 추가
+ const productList = document.getElementById("product-list");
+
+ productList.addEventListener("click", function (event) {
+ if (event.target.classList.contains("add-to-cart")) {
+ const productId = event.target.closest(".product-card").dataset.productId;
+ addToCart(productId);
+ }
+ });
+
+ function addToCart(productId) {
+ console.log(`상품 ${productId}를 장바구니에 담았습니다.`);
+ // 실제 장바구니 추가 로직...
+ }
+
+ // 동적으로 새 상품 추가 (예시)
+ function addNewProduct(productId, name, price) {
+ const newCard = document.createElement("div");
+ newCard.className = "product-card";
+ newCard.dataset.productId = productId;
+ newCard.innerHTML = `
+
${name}
+
가격: ${price}원
+
+ `;
+ productList.appendChild(newCard);
+ }
+
+ // 새 상품 추가 예시
+ addNewProduct(101, "새 상품", 30000);
+ ```
+ 1. 공통 부모 요소에 단일 이벤트 리스너 추가
+ 2. event.target을 사용하여 실제 이벤트가 발생한 요소 식별
+ 3. 이벤트 처리 로직에 조건문 추가하여 특정 요소만 처리
+ 4. 필요한 경우 event.stopPropagation() 사용하여 이벤트 전파 제어
+- Result:
+ 1. 이벤트 리스너 수 감소
+ 2. 메모리 사용량 절감
+ 3. 동적으로 추가되는 요소들의 이벤트 처리 용이성 증가
+
+### 참고자료
+
+- MDN: Event delegation (https://developer.mozilla.org/en-US/docs/Learn/JavaScript/Building_blocks/Events#Event_delegation)
+- [JavaScript.info](http://javascript.info/): Event delegation (https://javascript.info/event-delegation)
+- 참고로 리액트는 이벤트를 다 이벤트 위임 방식으로 관리하고 있습니다.
+ [React에서는 이벤트를 어떻게 처리하고 있을까?](https://puki4416blog.netlify.app/how-to-react-event-handle/)
+
+---
+
+## 이미지 최적화 사례 Case 1
+
+### AS-IS
+
+대용량 이미지를 한 번에 로딩하여 초기 페이지 로드 시간이 길고 사용자 경험이 좋지 않았습니다.
+
+### TO-BE
+
+프로그레시브 이미지 로딩 기법을 적용하여 사용자 경험을 개선하고 로딩 성능을 최적화했습니다.
+
+- Why: 초기 페이지 로드 시간을 단축하고 사용자에게 시각적 피드백을 빠르게 제공하기 위해
+- How:
+ ```html
+
+
+
+
+
+ 프로그레시브 이미지 로딩
+
+
+
+
+
+
+
+
+
+
+ ```
+ 1. 저해상도 이미지를 먼저 로드하여 플레이스홀더로 사용
+ 2. 고해상도 이미지를 비동기적으로 로드
+ 3. CSS blur 효과를 사용하여 저해상도에서 고해상도로의 전환을 부드럽게 처리
+ 4. Intersection Observer API를 사용하여 뷰포트에 진입한 이미지만 로드
+- Result:
+ 1. 초기 페이지 로드 시간 감소
+ 2. 사용자의 인지된 로딩 속도 향상
+ 3. 데이터 사용량 최적화로 모바일 사용자 경험 개선
+
+### 참고자료
+
+- web.dev: Lazy loading images (https://web.dev/lazy-loading-images/)
+- CSS-Tricks: Progressive Image Loading (https://css-tricks.com/the-complete-guide-to-lazy-loading-images/)
+
+---
+
+## 이미지 최적화 사례 Case 2
+
+### AS-IS
+
+대용량 이미지 파일들을 최적화 없이 그대로 사용하여 페이지 로딩 속도가 느리고 모바일 사용자의 데이터 소비가 많았습니다.
+
+```jsx
+
+
+
+
+
+ Image Gallery
+
+
+
+
+
+
+
+
+
+
+
+
+
+```
+
+### TO-BE
+
+다양한 이미지 최적화 기법을 적용하여 이미지 로딩 속도와 품질을 개선했습니다.
+
+- Why: 페이지 로딩 속도를 개선하고 모바일 사용자의 데이터 사용량을 줄이기 위해
+- How:
+ ```html
+
+
+
+
+
+ Optimized Image Gallery
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ```
+ - 지연 로딩 (Lazy Loading): Intersection Observer API를 사용하여 뷰포트에 가까워질 때만 이미지를 로드
+ - 반응형 이미지: CSS `background-image`를 사용하여 이미지 크기를 유연하게 조절
+ - WebP 형식 지원: 브라우저가 지원하는 경우 WebP 형식의 이미지를 우선적으로 로드
+ - 플레이스홀더 사용: 이미지가 로드되기 전에 SVG 플레이스홀더를 표시하여 레이아웃 시프트를 방지
+ - 점진적 로딩: 저해상도 플레이스홀더에서 고해상도 이미지로 자연스럽게 전환
+- Result:
+ 1. 페이지 로드 시간 감소
+ 2. 이미지 관련 데이터 전송량 감소
+ 3. 모바일 사용자의 이탈률 감소
+
+### 참고자료
+
+- web.dev: Optimize your images (https://web.dev/fast/#optimize-your-images)
+- MDN: Responsive images (https://developer.mozilla.org/en-US/docs/Learn/HTML/Multimedia_and_embedding/Responsive_images)
+
+---
+
+## 로컬 스토리지를 활용한 상태 지속성 최적화 사례
+
+### AS-IS
+
+페이지 새로고침 시 모든 상태가 초기화되어 사용자 경험이 단절되고 불필요한 API 호출이 발생했습니다.
+
+```jsx
+let cart = [];
+
+function addToCart(productId) {
+ // API를 호출하여 상품 정보를 가져옴
+ fetch(`/api/products/${productId}`)
+ .then((response) => response.json())
+ .then((product) => {
+ cart.push(product);
+ updateCartUI();
+ });
+}
+
+function removeFromCart(productId) {
+ cart = cart.filter((item) => item.id !== productId);
+ updateCartUI();
+}
+
+function updateCartUI() {
+ const cartElement = document.getElementById("cart");
+ cartElement.innerHTML = cart
+ .map(
+ (item) => `
+
+ ${item.name} - $${item.price}
+
+
+ `
+ )
+ .join("");
+}
+
+// 페이지 로드 시 장바구니 UI 업데이트
+updateCartUI();
+```
+
+### TO-BE
+
+로컬 스토리지를 활용하여 중요 상태를 유지하고 필요한 데이터만 서버에서 갱신하도록 최적화했습니다.
+
+- Why: 페이지 새로고침 시에도 일관된 사용자 경험을 제공하고 불필요한 네트워크 요청을 줄이기 위해
+- How:
+ ```jsx
+ let cart = [];
+
+ function loadCart() {
+ const savedCart = localStorage.getItem("cart");
+ if (savedCart) {
+ cart = JSON.parse(savedCart);
+ updateCartUI();
+ }
+ }
+
+ function saveCart() {
+ localStorage.setItem("cart", JSON.stringify(cart));
+ }
+
+ function addToCart(productId) {
+ // 먼저 로컬 스토리지를 확인
+ const existingProduct = cart.find((item) => item.id === productId);
+ if (existingProduct) {
+ existingProduct.quantity += 1;
+ saveCart();
+ updateCartUI();
+ } else {
+ // API를 호출하여 새로운 상품 정보만 가져옴
+ fetch(`/api/products/${productId}`)
+ .then((response) => response.json())
+ .then((product) => {
+ cart.push({ ...product, quantity: 1 });
+ saveCart();
+ updateCartUI();
+ });
+ }
+ }
+
+ function removeFromCart(productId) {
+ cart = cart.filter((item) => item.id !== productId);
+ saveCart();
+ updateCartUI();
+ }
+
+ function updateCartUI() {
+ const cartElement = document.getElementById("cart");
+ cartElement.innerHTML = cart
+ .map(
+ (item) => `
+
+ ${item.name} - $${item.price} x ${item.quantity}
+
+
+ `
+ )
+ .join("");
+ }
+
+ // 페이지 로드 시 로컬 스토리지에서 장바구니 정보 로드
+ document.addEventListener("DOMContentLoaded", loadCart);
+
+ // 주기적으로 카트 데이터 서버와 동기화 (선택적)
+ function syncCartWithServer() {
+ fetch("/api/sync-cart", {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify(cart),
+ })
+ .then((response) => response.json())
+ .then((updatedCart) => {
+ cart = updatedCart;
+ saveCart();
+ updateCartUI();
+ });
+ }
+
+ // 예: 5분마다 서버와 동기화
+ setInterval(syncCartWithServer, 5 * 60 * 1000);
+
+ /*
+ 주의사항:
+ - 민감한 정보는 로컬 스토리지에 저장하지 않도록 주의해야 합니다.
+ - 브라우저의 로컬 스토리지 용량 제한(보통 5-10MB)을 고려해야 합니다.
+ - 여러 기기 간 동기화가 필요한 경우, 추가적인 서버 동기화 로직이 필요할 수 있습니다.
+ */
+ ```
+ 1. 중요 상태 데이터를 로컬 스토리지에 저장
+ 2. 페이지 로드 시 로컬 스토리지의 데이터를 먼저 확인하여 상태 초기화
+ 3. 백그라운드에서 필요한 데이터만 서버와 동기화
+ 4. 데이터 만료 시간 설정으로 오래된 데이터 자동 갱신
+- Result:
+ 1. 페이지 새로고침 후 초기 로딩 시간 단축
+ 2. 불필요한 API 호출 감소
+ 3. 오프라인 상태에서도 기본적인 기능 사용 가능
+
+### 참고자료
+
+- MDN: Web Storage API (https://developer.mozilla.org/en-US/docs/Web/API/Web_Storage_API)
+- web.dev: Storage for the web (https://web.dev/storage-for-the-web/)
+
+---
+
+## CSS 애니메이션 성능 최적화 사례
+
+### AS-IS
+
+JavaScript를 사용한 애니메이션으로 인해 메인 스레드에 부하가 걸리고 성능이 저하되었습니다.
+
+```html
+
+
+
+
+
+```
+
+### TO-BE
+
+CSS 애니메이션을 활용하여 GPU 가속을 적용하고 성능을 최적화했습니다.
+
+- Why: 부드러운 애니메이션을 제공하고 CPU 사용량을 줄여 전반적인 성능을 개선하기 위해
+- How:
+ ```html
+
+
+
+
+
+ ```
+- how
+ 1. GPU 가속: `transform` 속성을 사용하여 GPU 가속을 활용합니다. 이는 `left`와 `top`을 변경하는 것보다 훨씬 효율적입니다.
+ 2. 메인 스레드 부하 감소: 애니메이션이 CSS에 의해 처리되므로 JavaScript 실행으로 인한 메인 스레드 부하가 없습니다.
+ 3. 선언적 정의: 애니메이션의 시작과 끝 상태를 명확하게 정의하여 코드의 가독성이 향상됩니다.
+ 4. `will-change` 속성: 브라우저에게 변화할 속성을 미리 알려줌으로써 최적화를 유도합니다.
+- 추가적인 최적화 팁:
+ 1. 복잡한 애니메이션의 경우, `opacity`와 `transform` 속성만을 애니메이션화하는 것이 가장 효율적입니다.
+ 2. 애니메이션이 필요 없는 요소에는 `pointer-events: none`을 적용하여 불필요한 이벤트 처리를 방지할 수 있습니다.
+ 3. 3D 변형을 사용하면(`transform: translate3d(0,0,0)`) 강제로 GPU 가속을 활성화할 수 있습니다. 하지만 남용은 피해야 합니다.
+
+
+
+### 참고자료
+
+- MDN: CSS animations (https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Animations/Using_CSS_animations)
+- web.dev: Animations and performance (https://web.dev/animations-and-performance/)
+
+---
+
+## 메모이제이션을 통한 불필요한 렌더링 최소화 사례 (React)
+
+### AS-IS
+
+대규모 React 애플리케이션에서 불필요한 리렌더링이 빈번하게 발생하여 성능 저하의 원인이 되었습니다.
+
+```jsx
+// App.js
+import React, { useState } from "react";
+import ProductList from "./ProductList";
+import Cart from "./Cart";
+
+function App() {
+ const [products, setProducts] = useState([
+ { id: 1, name: "상품 1", price: 10000 },
+ { id: 2, name: "상품 2", price: 20000 },
+ // ... 더 많은 상품들
+ ]);
+ const [cart, setCart] = useState([]);
+
+ const addToCart = (productId) => {
+ const product = products.find((p) => p.id === productId);
+ setCart([...cart, product]);
+ };
+
+ return (
+
+ );
+ });
+
+ export default ProductItem;
+ ```
+ 1. `useCallback`을 사용하여 `addToCart` 함수를 메모이제이션합니다. 이로 인해 불필요한 리렌더링이 줄어듭니다.
+ 2. `ProductList` 컴포넌트에서 `useMemo`를 사용하여 상품 목록이 변경될 때만 자식 컴포넌트를 다시 렌더링합니다.
+ 3. `ProductItem` 컴포넌트를 `React.memo`로 감싸 props가 변경될 때만 리렌더링되도록 합니다.
+- Result:
+ 1. 컴포넌트 리렌더링 횟수 감소
+ 2. CPU 사용량 감소 (대신 메모리 사용량 증가)
+
+### 참고자료
+
+- React 공식 문서: Memoizing Components (https://reactjs.org/docs/react-api.html#reactmemo)
+- React 공식 문서: Hooks API Reference (https://reactjs.org/docs/hooks-reference.html)
+
+---
+
+## 상태 관리 최적화 사례 (React)
+
+### AS-IS
+
+대규모 React 애플리케이션에서 전역 상태 관리가 비효율적이어서 불필요한 리렌더링이 자주 발생했습니다.
+
+### TO-BE
+
+상태 관리 라이브러리와 최적화 기법을 적용하여 리렌더링을 최소화하고 애플리케이션 성능을 개선했습니다.
+
+- Why: 불필요한 리렌더링을 줄여 애플리케이션의 반응성을 높이고 성능을 개선하기 위해
+- How:
+ 1. Redux 대신 Recoil이나 Jotai 같은 경량 상태 관리 라이브러리 도입
+ 2. 상태를 더 작은 단위로 분리하여 관리 (Atomic 패턴 적용)
+ 3. useSelector 훅 사용 시 메모이제이션 적용
+ 4. 상태 변경 시 불변성을 지키며 업데이트하는 로직 구현
+- Result:
+ 1. 컴포넌트 리렌더링 횟수 50% 감소
+ 2. 상태 업데이트 시 반응 속도 40% 개선
+ 3. 메모리 사용량 20% 절감
+
+### 참고자료
+
+- Recoil 공식 문서: https://recoiljs.org/
+- Redux 공식 문서: https://redux.js.org/tutorials/essentials/part-6-performance-normalization
+
+---
+
+## 인터섹션 옵저버를 활용한 스크롤 성능 최적화 사례
+
+### AS-IS
+
+긴 스크롤 페이지에서 모든 요소를 한 번에 렌더링하여 초기 로딩이 느리고 스크롤 성능이 저하되었습니다.
+
+```jsx
+import React from "react";
+
+const LongScrollPage = () => {
+ const items = Array.from({ length: 1000 }, (_, index) => ({
+ id: index,
+ title: `Item ${index}`,
+ image: `https://via.placeholder.com/150?text=Image${index}`,
+ }));
+
+ return (
+
+ {items.map((item) => (
+
+
{item.title}
+
+
Some description for {item.title}
+
+ ))}
+
+ );
+};
+
+export default LongScrollPage;
+```
+
+### TO-BE
+
+Intersection Observer API를 활용하여 뷰포트에 있는 요소만 렌더링하도록 최적화했습니다.
+
+- Why: 초기 렌더링 속도를 높이고 스크롤 시 부드러운 사용자 경험을 제공하기 위해
+- How:
+ ```jsx
+ import React, { useState, useEffect, useRef } from "react";
+
+ // 범용 LazyLoad 컴포넌트
+ const LazyLoad = ({ children, placeholder }) => {
+ const [isVisible, setIsVisible] = useState(false);
+ const ref = useRef(null);
+
+ useEffect(() => {
+ const observer = new IntersectionObserver(
+ ([entry]) => {
+ if (entry.isIntersecting) {
+ setIsVisible(true);
+ observer.disconnect();
+ }
+ },
+ { rootMargin: "100px" }
+ );
+
+ if (ref.current) {
+ observer.observe(ref.current);
+ }
+
+ return () => {
+ if (ref.current) {
+ observer.unobserve(ref.current);
+ }
+ };
+ }, []);
+
+ return
+ }
+ >
+
+
+ ))}
+
+ );
+ };
+
+ export default LongScrollPage;
+ ```
+ 1. `IntersectionObserver`를 사용하여 각 아이템의 가시성을 추적합니다.
+ 2. 뷰포트 밖의 요소는 플레이스홀더로 대체됩니다.
+ 3. 요소가 뷰포트에 진입할 때 실제 콘텐츠로 교체됩니다.
+ 4. 이미지에 `loading="lazy"` 속성을 추가하여 브라우저의 네이티브 이미지 지연 로딩을 활용합니다.
+- Result:
+ 1. 초기 렌더링 속도가 크게 향상됩니다. 처음에는 뷰포트 내의 아이템만 렌더링됩니다.
+ 2. 메모리 사용량이 줄어듭니다. 실제 콘텐츠는 필요할 때만 렌더링됩니다.
+ 3. 스크롤 성능이 개선됩니다. 렌더링되는 실제 DOM 요소의 수가 줄어듭니다.
+ 4. 사용자 경험이 향상됩니다. 스크롤이 부드러워지고, 콘텐츠가 자연스럽게 로드됩니다.
+- 주의할 점:
+ - SEO를 고려해야 하는 경우, 서버 사이드 렌더링과 함께 사용하는 것이 좋습니다.
+ - 네트워크 요청이 많아질 수 있으므로, 적절한 캐싱 전략을 함께 사용하는 것이 좋습니다.
+
+### 참고자료
+
+- MDN: Intersection Observer API (https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API)
+- web.dev: Lazy loading images and video (https://web.dev/lazy-loading/)
+
+---
+
+## 가상 스크롤링(Virtual Scrolling) 구현 사례
+
+### AS-IS
+
+대량의 데이터를 리스트로 표시할 때 모든 항목을 한 번에 렌더링하여 초기 로딩 속도와 스크롤 성능이 매우 저하되었습니다.
+
+```jsx
+import React from "react";
+
+const LargeList = ({ items }) => {
+ return (
+
+ {items.map((item) => (
+
+
+
+
{item.name}
+
{item.email}
+
+
+ ))}
+
+ );
+};
+
+const App = () => {
+ // 10,000개의 아이템을 가정
+ const items = Array.from({ length: 10000 }, (_, index) => ({
+ id: index,
+ name: `User ${index}`,
+ email: `user${index}@example.com`,
+ avatar: `https://via.placeholder.com/50?text=${index}`,
+ }));
+
+ return ;
+};
+
+export default App;
+```
+
+### TO-BE
+
+가상 스크롤링 기법을 적용하여 화면에 보이는 항목만 렌더링하도록 최적화했습니다.
+
+- Why: 대량의 데이터를 효율적으로 표시하고 스크롤 성능을 개선하기 위해
+- How:
+ ```jsx
+ import React from "react";
+ import { FixedSizeList as List } from "react-window";
+ import AutoSizer from "react-virtualized-auto-sizer";
+
+ const Row = ({ index, style, data }) => {
+ const item = data[index];
+ return (
+
+ );
+ };
+
+ export default App;
+ ```
+ 1. `react-window` 라이브러리의 `FixedSizeList` 컴포넌트를 사용하여 가상 스크롤링을 구현합니다.
+ 2. `AutoSizer`를 사용하여 컨테이너의 크기에 따라 리스트 크기를 자동으로 조정합니다.
+ 3. `Row` 컴포넌트는 각 아이템을 렌더링하는 로직을 담당합니다. 이 컴포넌트는 필요할 때만 렌더링됩니다.
+ 4. `itemSize`를 80으로 설정하여 각 아이템의 높이를 지정합니다. 실제 사용 시에는 아이템의 실제 크기에 맞게 조정해야 합니다.
+ 5. 전체 리스트의 높이를 뷰포트 높이(`100vh`)로 설정하여 전체 화면에 맞추었습니다.
+
+result:
+
+- 메모리 사용량 감소: 화면에 보이는 항목만 렌더링하므로 메모리 사용량이 크게 줄어듭니다.
+- 초기 렌더링 시간 단축: 전체 10,000개 항목이 아닌 화면에 보이는 항목만 렌더링하므로 초기 로딩 속도가 매우 빨라집니다.
+- 스크롤 성능 향상: 스크롤 시 새로운 항목을 효율적으로 렌더링하여 부드러운 스크롤 경험을 제공합니다.
+
+### 참고자료
+
+- react-window 문서: https://react-window.vercel.app/
+- web.dev: Virtualize large lists with react-window (https://web.dev/virtualize-long-lists-react-window/)
+
+---
+
+## 프리페칭(Prefetching)을 통한 네비게이션 최적화 사례
+
+### AS-IS
+
+사용자가 링크를 클릭할 때마다 새로운 페이지 리소스를 로드하여 페이지 전환이 느렸습니다.
+
+```jsx
+// App.js
+import React from "react";
+import { BrowserRouter as Router, Route, Link } from "react-router-dom";
+import Home from "./components/Home";
+import About from "./components/About";
+import Contact from "./components/Contact";
+
+function App() {
+ return (
+
+
+
+
+
+
+
+
+
+ );
+}
+
+export default App;
+```
+
+### TO-BE
+
+프리페칭 기술을 도입하여 예상되는 사용자 행동에 따라 리소스를 미리 로드하도록 개선했습니다.
+
+- Why: 페이지 전환 속도를 개선하고 사용자 경험을 향상시키기 위해
+- How:
+ ```jsx
+ // App.js
+ import React, { useEffect } from "react";
+ import { BrowserRouter as Router, Route, Link } from "react-router-dom";
+ import { useInView } from "react-intersection-observer";
+
+ const Home = React.lazy(() => import("./components/Home"));
+ const About = React.lazy(() => import("./components/About"));
+ const Contact = React.lazy(() => import("./components/Contact"));
+
+ function PrefetchLink({ to, children }) {
+ const [ref, inView] = useInView({
+ triggerOnce: true,
+ rootMargin: "200px",
+ });
+
+ useEffect(() => {
+ if (inView) {
+ const componentMap = {
+ "/": () => import("./components/Home"),
+ "/about": () => import("./components/About"),
+ "/contact": () => import("./components/Contact"),
+ };
+
+ if (componentMap[to]) {
+ componentMap[to]();
+ }
+ }
+ }, [inView, to]);
+
+ return (
+
+ {children}
+
+ );
+ }
+
+ function App() {
+ useEffect(() => {
+ // About 페이지의 주요 리소스 프리페치
+ const resources = ["/static/js/about.chunk.js", "/static/css/about.chunk.css"];
+
+ resources.forEach((resource) => {
+ const link = document.createElement("link");
+ link.rel = "prefetch";
+ link.href = resource;
+ document.head.appendChild(link);
+ });
+ }, []);
+
+ return (
+
+
+
+
+ Loading...
}>
+
+
+
+
+
+
+ );
+ }
+
+ export default App;
+ ```
+ 1. 태그를 사용하여 중요 리소스 프리페치
+ 2. Intersection Observer를 활용해 뷰포트에 들어온 링크의 리소스 프리페치
+ 3. 사용자 행동 분석을 통해 자주 방문하는 페이지 예측 및 프리페치
+ 4. Service Worker를 사용하여 백그라운드에서 리소스 프리페치 및 캐싱
+
+ [Service Worker를 사용하여 백그라운드에서 프리페치](https://www.notion.so/Service-Worker-2cd2dc3ef514813f9f64cdbaa60ff0b3?pvs=21)
+- Result:
+ 1. 페이지 전환 시간 평균 감소
+ 2. 사용자의 체감 로딩 속도 크게 개선
+ 3. 바운스 레이트 감소
+- 주의할 점:
+ - 프리페치할 리소스의 정확한 경로를 알아야 합니다. 이는 보통 빌드 프로세스 후에 결정되므로, 동적으로 이 정보를 가져오는 방법을 고려해볼 수 있습니다.
+ - 너무 많은 리소스를 프리페치하면 초기 로딩 시 불필요한 네트워크 사용이 발생할 수 있으므로, 중요한 리소스만 선별적으로 프리페치해야 합니다.
+
+### 참고자료
+
+- web.dev: Preload critical assets to improve loading speed (https://web.dev/preload-critical-assets/)
+- Google Developers: Prefetching Resources to Improve Performance (https://developers.google.com/web/fundamentals/performance/resource-prioritization#prefetch)
+
+---
+
+## 번들 크기 최적화 사례
+
+### AS-IS
+
+대규모 SPA에서 단일 JavaScript 번들 파일의 크기가 매우 커서 초기 로딩 시간이 길었습니다.
+
+```jsx
+// webpack.config.js
+const path = require("path");
+
+module.exports = {
+ entry: "./src/index.js",
+ output: {
+ filename: "bundle.js",
+ path: path.resolve(__dirname, "dist"),
+ },
+};
+
+// src/index.js
+import React from "react";
+import ReactDOM from "react-dom";
+import { BrowserRouter as Router, Route, Switch } from "react-router-dom";
+import moment from "moment";
+import lodash from "lodash";
+import Home from "./components/Home";
+import About from "./components/About";
+import Contact from "./components/Contact";
+
+const App = () => (
+
+
+
+
+
+
+
+);
+
+ReactDOM.render(, document.getElementById("root"));
+
+// 불필요하게 전체 라이브러리를 import
+console.log(moment().format("MMMM Do YYYY, h:mm:ss a"));
+console.log(lodash.chunk(["a", "b", "c", "d"], 2));
+```
+
+### TO-BE
+
+번들 분석 및 최적화 기법을 적용하여 번들 크기를 줄이고 로딩 성능을 개선했습니다.
+
+- Why: 초기 페이지 로드 시간을 단축하고 사용자 경험을 개선하기 위해
+- How:
+ ```jsx
+ // webpack.config.js
+ const path = require("path");
+ const BundleAnalyzerPlugin = require("webpack-bundle-analyzer").BundleAnalyzerPlugin;
+ const TerserPlugin = require("terser-webpack-plugin");
+
+ module.exports = {
+ entry: "./src/index.js",
+ output: {
+ filename: "[name].[contenthash].js",
+ path: path.resolve(__dirname, "dist"),
+ },
+ optimization: {
+ minimizer: [new TerserPlugin()],
+ splitChunks: {
+ chunks: "all",
+ },
+ },
+ plugins: [new BundleAnalyzerPlugin()],
+ };
+
+ // src/index.js
+ import React, { lazy, Suspense } from "react";
+ import ReactDOM from "react-dom";
+ import { BrowserRouter as Router, Route, Switch } from "react-router-dom";
+ import { format } from "date-fns";
+
+ const Home = lazy(() => import("./components/Home"));
+ const About = lazy(() => import("./components/About"));
+ const Contact = lazy(() => import("./components/Contact"));
+
+ const App = () => (
+
+ Loading...}>
+
+
+
+
+
+
+
+ );
+
+ ReactDOM.render(, document.getElementById("root"));
+
+ // 필요한 기능만 import하여 사용
+ console.log(format(new Date(), "MMMM do yyyy, h:mm:ss a"));
+
+ // lodash 함수는 필요할 때 동적으로 import
+ if (needChunkFunction) {
+ import("lodash/chunk").then((module) => {
+ const chunk = module.default;
+ console.log(chunk(["a", "b", "c", "d"], 2));
+ });
+ }
+ ```
+ 1. webpack-bundle-analyzer를 사용하여 번들 크기를 분석합니다.
+ 2. Tree shaking이 자동으로 적용되어 사용하지 않는 코드가 제거됩니다.
+ 3. React.lazy와 Suspense를 사용하여 코드 분할을 구현했습니다.
+ 4. moment.js 대신 더 가벼운 date-fns를 사용하고, 필요한 함수만 import 합니다.
+ 5. lodash는 필요한 함수만 동적으로 import 하여 사용합니다.
+ 6. TerserPlugin을 사용하여 코드 압축 및 최적화를 수행합니다.
+ 7. contenthash를 사용하여 장기 캐싱을 가능하게 합니다.
+- Result:
+ 1. 메인 번들 크기 감소
+ 2. 초기 로딩 시간 단축
+ 3. Time to Interactive (TTI) 개선
+
+### 참고자료
+
+- webpack 공식 문서: Code Splitting (https://webpack.js.org/guides/code-splitting/)
+- web.dev: Reduce JavaScript payloads with code splitting (https://web.dev/reduce-javascript-payloads-with-code-splitting/)
+
+---
diff --git a/.claude/docs/4-2-4. React profiling and basic optimization.md b/.claude/docs/4-2-4. React profiling and basic optimization.md
new file mode 100644
index 0000000..616306a
--- /dev/null
+++ b/.claude/docs/4-2-4. React profiling and basic optimization.md
@@ -0,0 +1,1047 @@
+## (2) 최적화 되지 않은 예제 코드
+
+[front_3rd_chapter1-3/packages/example/src/common-sample/origin/App.tsx at main · hanghae-plus/front_3rd_chapter1-3](https://github.com/hanghae-plus/front_3rd_chapter1-3/blob/main/packages/example/src/common-sample/origin/App.tsx)
+
+```tsx
+import { PropsWithChildren, useState } from "react";
+
+type NewsCategory = "정치" | "경제" | "사회" | "문화";
+
+interface NewsItem {
+ id: number;
+ title: string;
+ category: NewsCategory;
+ likes: number;
+ content: string;
+}
+
+const NEWS_CATEGORIES = ["정치", "경제", "사회", "문화"] as const;
+
+const generateNewsData = (count: number): NewsItem[] => {
+ return Array.from({ length: count }, (_, index) => ({
+ id: index + 1,
+ title: `뉴스 제목 ${index + 1}`,
+ category: [...NEWS_CATEGORIES].sort(() => Math.random() - 0.5)[0],
+ likes: Math.floor(Math.random() * 100),
+ content: `이것은 뉴스 ${index + 1}의 내용입니다. 실제 내용은 더 길 것입니다.`,
+ }));
+};
+
+const newsItems = generateNewsData(50);
+
+const Header = ({ totalLikes }: { totalLikes: number }) => (
+
+