하위 태스크 1 ~ 4
DiaryReducer 정의
일기 목록을 관리하는 리듀서 함수/초기 상태 정의
액션 타입 상수 분리
액션 타입을 별도 파일로 관리
useReducer로 상태 관리
App에서 useReducer 로 상태 관리 전환
CRUD 액션 구현
추가/수정/삭제 액션과 UI 연결
diaryAction.js에 액션 타입을 정의한다.
const diaryAction = {
ADD: "ADD",
MODIFY: "MODIFY",
DELETE: "DELETE",
};
export default diaryAction;diaryReducer.js에 일기 목록 State를 관리하는 diaryReducer 함수를 정의한다.
import DIARY_ACTION from "./diaryAction";
export default function diaryReducer(diaries, action) {
switch (action.type) {
case DIARY_ACTION.ADD: {
return [...diaries, action.diary];
}
case DIARY_ACTION.MODIFY: {
return diaries.map((diary) => diary.id === action.diary.id ? action.diary : diary);
}
case DIARY_ACTION.DELETE: {
return diaries.filter((diary) => diary.id !== action.diaryId);
}
default: {
throw new Error("알 수 없는 액션입니다.");
}
}
}App.jsx에서 useReducer를 사용해 State를 만들고 컴포넌트 트리를 구성한다.
import { useReducer, useState } from "react";
import DiaryForm from "./DiaryForm";
import DiaryList from "./DiaryList";
import diaryReducer from "./diaryReducer";
import DIARY_ACTION from "./diaryAction";
const initialDiaries = [
{ id: 0, moodScore: 7, summary: "오늘은 나쁘지 않았어." },
{ id: 1, moodScore: 10, summary: "날씨가 화창해 기분이 좋았어." },
{ id: 2, moodScore: 3, summary: "어제 잠을 자지 못해서 피곤해." },
{ id: 3, moodScore: 5, summary: "점심 메뉴가 나쁘지 않았어." },
];
export default function App() {
const [diaries, dispatch] = useReducer(diaryReducer, initialDiaries);
function handleDiaryAdd(draft) {
dispatch({ type: DIARY_ACTION.ADD, diary: { id: diaries.length, ...draft } });
}
function handleDiaryModify(diary) {
dispatch({ type: DIARY_ACTION.MODIFY, diary });
}
function handleDiaryDelete(diaryId) {
dispatch({ type: DIARY_ACTION.DELETE, diaryId });
}
return (
<main>
<h1>일기</h1>
<DiaryForm onDiaryAdd={handleDiaryAdd} />
<DiaryList
diaries={diaries}
onDiaryModify={handleDiaryModify}
onDiaryDelete={handleDiaryDelete}
/>
</main>
);
}일기를 추가하기 위한 Form UI와 일기 목록을 조회하기 위한 UI를 만들기 위해 DiaryForm.jsx, DiaryList.jsx, DiaryItem.jsx를 작성한다.
import { useState } from "react";
export default function DiaryForm({ onDiaryAdd }) {
const [moodScore, setMoodScore] = useState(0);
const [summary, setSummary] = useState("");
return (
<form>
<fieldset>
<legend>오늘 하루를 기록하세요.</legend>
<label>
감정 점수
<input type="number" value={moodScore} onChange={(event) => {
setMoodScore(event.target.value);
}} />
</label>
<label>
한 줄 일기
<input type="text" value={summary} onChange={(event) => {
setSummary(event.target.value);
}} />
</label>
<button type="button" onClick={() => {
onDiaryAdd({ moodScore, summary });
setMoodScore(0);
setSummary("");
}}>
추가
</button>
</fieldset>
</form>
);
}import DiaryItem from "./DiaryItem";
export default function DiaryList({ diaries, onDiaryModify, onDiaryDelete }) {
return (
<ul>
{diaries.map((diary) =>
<DiaryItem
key={diary.id}
diary={diary}
onDiaryModify={onDiaryModify}
onDiaryDelete={onDiaryDelete}
/>
)}
</ul>
);
}import { useState } from "react";
export default function DiaryItem({ diary, onDiaryModify, onDiaryDelete }) {
const [summary, setSummary] = useState(diary.summary);
const [moodScore, setMoodScore] = useState(diary.moodScore);
const [modifying, setModifying] = useState(false);
return (
<li>
{
modifying ?
<>
<input
type="text"
value={summary}
onChange={(event) => { setSummary(event.target.value); }}
/>
<input
type="number"
value={moodScore}
onChange={(event) => { setMoodScore(event.target.value); }}
/>
<button type="button" onClick={() => {
onDiaryModify({ id: diary.id, summary, moodScore });
setModifying(!modifying);
}}>
저장
</button>
</>
:
<>
<span>한 줄 일기: "{diary.summary}", 감정 점수: {diary.moodScore}점</span>
<button type="button" onClick={() => { setModifying(!modifying); }}>수정</button>
<button type="button" onClick={() => { onDiaryDelete(diary.id); }}>삭제</button>
</>
}
</li>
);
}웹브라우저에서 확인한 결과는 다음과 같다.

하위 태스크 5 ~ 7
Context 생성 및 Provider 구성
상태/디스패치를 제공하는 Context/Provider 구성
Context 파일 분리
Context를 별도 파일로 분리해 관리
useContext로 상태 소비
하위 컴포넌트에서 전역 상태/함수 사용
Context를 사용하면 깊은 컴포넌트 트리에서 Props를 연달아 전달해야 하는 문제를 해결할 수 있다.
DiaryContext.js에 Context를 정의한다.
import { createContext } from "react";
export const DiaryStateContext = createContext([]);
export const DiaryDispatchContext = createContext(null);App.jsx에 DiaryContext.js에서 정의한 Context의 Provider를 배치한다. DiaryForm과 DiaryList의 Props를 제거한다.
import { useContext, useReducer, useState } from "react";
// ...
import { DiaryStateContext, DiaryDispatchContext } from "./DiaryContext";
// ...
export default function App() {
const [diaries, dispatch] = useReducer(diaryReducer, initialDiaries);
return (
<DiaryStateContext value={diaries}>
<DiaryDispatchContext value={dispatch}>
<main>
<h1>일기</h1>
<DiaryForm />
<DiaryList />
</main>
</DiaryDispatchContext>
</DiaryStateContext>
);
}DiaryForm 컴포넌트가 Context를 사용하도록 수정한다.
import { useContext, useState } from "react";
import { DiaryStateContext, DiaryDispatchContext } from "./DiaryContext";
// ...
export default function DiaryForm() {
const diaries = useContext(DiaryStateContext);
const dispatch = useContext(DiaryDispatchContext);
// ...
return (
// ...
<button type="button" onClick={() => {
dispatch({ type: DIARY_ACTION.ADD, diary: { id: diaries.length, moodScore, summary } });
setMoodScore(0);
setSummary("");
}}>
추가
</button>
// ...
);
}DiaryList 컴포넌트가 Context를 사용하도록 수정한다.
import { useContext } from "react";
import { DiaryStateContext, DiaryDispatchContext } from "./DiaryContext";
// ...
export default function DiaryList() {
const diaries = useContext(DiaryStateContext);
const dispatch = useContext(DiaryDispatchContext);
return (
<ul>
{diaries.map((diary) =>
<DiaryItem
key={diary.id}
diary={diary}
onDiaryModify={(diary) => { dispatch({ type: DIARY_ACTION.MODIFY, diary }); }}
onDiaryDelete={(diaryId) => { dispatch({ type: DIARY_ACTION.DELETE, diaryId }); }}
/>
)}
</ul>
);
}하위 태스크 8 ~ 12
React Router 설치
npm install react-router-dom
Router 기본 설정
BrowserRouter, Routes, Route 설정
페이지 컴포넌트 분리
리스트/작성/상세/수정 페이지 컴포넌트 분리
Link/useNavigate 적용
버튼/링크로 페이지 전환 구현
useParams로 ID 가져오기
URL 파라미터에서 ID 추출
React Router v7을 설치한다.
npm install react-routerDiaryProvider.jsx를 작성한다.
import { useContext, useReducer, useState } from "react";
import diaryReducer from "./diaryReducer";
import { DiaryStateContext, DiaryDispatchContext } from "./DiaryContext";
const initialDiaries = [
{ id: 0, moodScore: 7, summary: "오늘은 나쁘지 않았어." },
{ id: 1, moodScore: 10, summary: "날씨가 화창해 기분이 좋았어." },
{ id: 2, moodScore: 3, summary: "어제 잠을 자지 못해서 피곤해." },
{ id: 3, moodScore: 5, summary: "점심 메뉴가 나쁘지 않았어." },
];
export default function DiaryProvider({ children }) {
const [diaries, dispatch] = useReducer(diaryReducer, initialDiaries);
return (
<DiaryStateContext value={diaries}>
<DiaryDispatchContext value={dispatch}>
{children}
</DiaryDispatchContext>
</DiaryStateContext>
);
}애플리케이션 전체에 네비게이션 바를 추가하기 위해 DiaryLayout 컴포넌트를 구현한다.
import { Outlet, Link } from "react-router";
export default function DiaryLayout() {
return (
<>
<header>
<h1>일기장</h1>
</header>
<nav>
<ul>
<li><Link to="/">전체</Link></li>
<li><Link to="/add">추가</Link></li>
</ul>
</nav>
<Outlet />
</>
);
}일기의 상세 내용을 확인하기 위한 DiaryDetail 컴포넌트를 구현한다.
import { useContext } from "react";
import { useParams } from "react-router";
import { DiaryStateContext } from "./DiaryContext";
export default function DiaryDetail() {
const { id } = useParams();
const diaries = useContext(DiaryStateContext);
const diary = diaries.find((diary) => String(diary.id) === id);
if (diary === undefined) {
return <div>일기를 찾을 수 없습니다.</div>;
}
return (
<div>
<h2>{id}번 일기</h2>
<div>감정 점수: {diary.moodScore}</div>
<blockquote>{diary.summary}</blockquote>
</div>
);
}DiaryItem 컴포넌트에 상세 페이지로 이동하기 위한 ‘상세’ 링크를 추가한다.
// ...
import { Link } from "react-router";
export default function DiaryItem({ diary, onDiaryModify, onDiaryDelete }) {
// ...
<Link to={`/detail/${diary.id}`}>상세</Link>
// ...
}main.jsx에서 React Router의 컴포넌트를 사용해 라우팅을 구현한다.
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import { BrowserRouter, Routes, Route, Outlet } from "react-router";
import DiaryLayout from "./DiaryLayout";
import DiaryList from "./DiaryList";
import DiaryForm from "./DiaryForm";
import DiaryDetail from "./DiaryDetail";
import DiaryProvider from "./DiaryProvider";
createRoot(document.getElementById('root')).render(
<StrictMode>
<DiaryProvider>
<BrowserRouter>
<Routes>
<Route element={<DiaryLayout />}>
<Route path="/" element={<DiaryList />} />
<Route path="/add" element={<DiaryForm />} />
<Route path="/detail/:id" element={<DiaryDetail />} />
</Route>
</Routes>
</BrowserRouter>
</DiaryProvider>
</StrictMode>
);하위 태스크 13
React.memo/useMemo/useCallback 적용
렌더링 최적화 포인트 선정 및 적용
memo 함수는 DiaryItem 컴포넌트에 적용할 수 있다.
import { memo, useState } from "react";
// ...
const DiaryItem = memo(function DiaryItem(/* ... */) { /* ... */ });
export default DiaryItem;useMemo Hook은 DiaryDetail 컴포넌트에서 특정 일기를 찾는 작업을 캐시하기 위해 사용할 수 있다.
import { memo, useContext } from "react";
// ...
export default function DiaryDetail() {
// ...
const diary = useMemo(() => diaries.find((diary) => String(diary.id) === id), [diaries]);
// ...
}useCallback Hook은 DiaryList 컴포넌트에서 콜백 함수를 캐시하기 위해 사용할 수 있다.
import { useCallback, useContext } from "react";
// ...
export default function DiaryList() {
// ...
const handleDiaryModify = useCallback((diary) => {
dispatch({ type: DIARY_ACTION.MODIFY, diary });
}, [diaries]);
const handleDiaryDelete = useCallback((diaryId) => {
dispatch({ type: DIARY_ACTION.DELETE, diaryId });
}, [diaries]);
return (
<ul>
{diaries.map((diary) =>
<DiaryItem
key={diary.id}
diary={diary}
onDiaryModify={() => { handleDiaryModify(diary) }}
onDiaryDelete={() => { handleDiaryDelete(diary.id) }}
/>
)}
</ul>
);
}