하위 태스크 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.jsxDiaryContext.js에서 정의한 Context의 Provider를 배치한다. DiaryFormDiaryList의 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-router

DiaryProvider.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>
	);
}