React 감정 일기장 만들기 5편 - 전체 앱 연결 및 데이터 관리
드디어 마지막 편입니다! 1-4편에서 프로젝트 설정부터 컴포넌트 분리까지 배웠는데요, 이제 모든 걸 하나로 연결해서 완벽하게 작동하는 감정일기장을 완성할 차례예요!
지금까지는 각 부품을 만들었다면, 이번에는 그 부품들을 조립해서 실제로 일기를 작성하고, 수정하고, 삭제할 수 있는 진짜 앱을 만들어볼게요.
[!NOTE] 이 튜토리얼 시리즈는 “한입 크기로 잘라 먹는 리액트” (이정환 저) 책의 내용을 바탕으로 작성되었습니다. 더 깊이 있는 학습을 원하시면 해당 도서를 참고하세요!
이번 편에서 배울 내용
- ✅ App.js에서 전체 일기 데이터 관리하기
- ✅ onCreate, onUpdate, onDelete 함수 구현
- ✅ Props로 데이터와 함수 전달하기
- ✅ New 페이지에서 일기 작성 완성
- ✅ Diary 페이지에서 일기 상세보기
- ✅ Edit 페이지에서 일기 수정
- ✅ Home 페이지에서 일기 목록 보여주기
- ✅ 완성된 앱 최종 확인
전체 데이터 흐름 이해하기
먼저 전체 앱의 데이터 흐름을 다이어그램으로 볼게요:
graph TD
A["App.js<br/>(일기 배열 State 관리)"] --> B["Home 페이지<br/>(일기 목록)"]
A --> C["New 페이지<br/>(일기 작성)"]
A --> D["Diary 페이지<br/>(일기 상세)"]
A --> E["Edit 페이지<br/>(일기 수정)"]
B --> F["DiaryList<br/>(목록 렌더링)"]
C --> G["Editor<br/>(작성 폼)"]
D --> H["일기 내용 표시"]
E --> G
G --> I["onCreate()<br/>onUpdate()"]
I --> A
style A fill:#fff4e1
style G fill:#e1ffe1
style I fill:#ffe1e1
핵심 개념:
- 📦 App.js가 모든 일기 데이터를 가지고 있어요 (단일 진실 공급원)
- ⬇️ Props로 데이터와 함수를 자식에게 전달
- ⬆️ 자식이 함수를 호출하면 App.js의 State가 변경됨
- 🔄 State가 바뀌면 모든 페이지가 자동으로 업데이트
App.js - 중앙 데이터 관리소
App.js를 완전히 새로 작성할게요. 이게 가장 중요한 파일이에요!
App.js 완성 코드
import { useState, useRef } from 'react';
import { Routes, Route } from 'react-router-dom';
import Home from './page/Home';
import New from './page/New';
import Diary from './page/Diary';
import Edit from './page/Edit';
import './App.css';
// 테스트용 더미 데이터
const mockData = [
{
id: 1,
date: new Date('2024-12-01').getTime(),
emotionId: 1,
content: '오늘은 정말 좋은 일이 있었어요! 친구와 즐거운 시간을 보냈어요.'
},
{
id: 2,
date: new Date('2024-12-05').getTime(),
emotionId: 3,
content: '그냥 평범한 하루였어요. 특별한 일은 없었네요.'
},
{
id: 3,
date: new Date('2024-12-07').getTime(),
emotionId: 5,
content: '오늘은 정말 안 좋은 일이 있었어요. 기분이 많이 안 좋아요.'
}
];
function App() {
// 일기 데이터 State
const [data, setData] = useState(mockData);
// ID 생성용 (새 일기마다 증가)
const idRef = useRef(4);
// CREATE - 새 일기 추가
const onCreate = (date, emotionId, content) => {
const newItem = {
id: idRef.current,
date: new Date(date).getTime(), // 문자열을 timestamp로
emotionId,
content
};
setData([newItem, ...data]); // 최신 일기를 맨 앞에
idRef.current += 1;
};
// UPDATE - 일기 수정
const onUpdate = (targetId, date, emotionId, content) => {
setData(
data.map((item) =>
item.id === targetId
? {
...item,
date: new Date(date).getTime(),
emotionId,
content
}
: item
)
);
};
// DELETE - 일기 삭제
const onDelete = (targetId) => {
setData(data.filter((item) => item.id !== targetId));
};
return (
<div className="App">
<Routes>
<Route path="/" element={<Home data={data} />} />
<Route path="/new" element={<New onCreate={onCreate} />} />
<Route path="/diary/:id" element={<Diary data={data} onDelete={onDelete} />} />
<Route path="/edit/:id" element={<Edit data={data} onUpdate={onUpdate} />} />
</Routes>
</div>
);
}
export default App;
코드 상세 설명
1. State와 Ref
const [data, setData] = useState(mockData);
const idRef = useRef(4);
data: 모든 일기를 담은 배열idRef: 새 일기의 ID를 생성 (useState가 아닌 useRef를 쓰는 이유는 변경 시 리렌더링이 필요 없어서)
2. onCreate 함수
const onCreate = (date, emotionId, content) => {
const newItem = {
id: idRef.current,
date: new Date(date).getTime(), // timestamp로 저장
emotionId,
content
};
setData([newItem, ...data]); // 배열 앞에 추가
idRef.current += 1; // ID 증가
};
- 새 일기 객체를 만들어서 배열 맨 앞에 추가
- 날짜는 timestamp(숫자)로 저장해야 정렬이 쉬워요
3. onUpdate 함수
const onUpdate = (targetId, date, emotionId, content) => {
setData(
data.map((item) =>
item.id === targetId
? { ...item, date: new Date(date).getTime(), emotionId, content }
: item
)
);
};
map()으로 배열을 순회하면서id가 일치하는 항목만 새 내용으로 교체- 나머지는 그대로 유지
4. onDelete 함수
const onDelete = (targetId) => {
setData(data.filter((item) => item.id !== targetId));
};
filter()로 해당 ID를 제외한 나머지만 남김
5. Props 전달
<Route path="/" element={<Home data={data} />} />
<Route path="/new" element={<New onCreate={onCreate} />} />
<Route path="/diary/:id" element={<Diary data={data} onDelete={onDelete} />} />
<Route path="/edit/:id" element={<Edit data={data} onUpdate={onUpdate} />} />
각 페이지에 필요한 데이터와 함수를 Props로 전달!
New.js - 일기 작성 페이지
New 페이지에서 Editor를 사용해서 일기를 작성할 수 있게 만들어볼게요.
New.js 완성 코드
import { useNavigate } from 'react-router-dom';
import Editor from '../components/Editor';
import Header from '../components/Header';
import Button from '../components/Button';
const New = ({ onCreate }) => {
const navigate = useNavigate();
// 뒤로가기
const handleGoBack = () => {
navigate(-1);
};
// 작성 완료
const handleSubmit = (data) => {
const { date, emotionId, content } = data;
onCreate(date, emotionId, content);
alert('일기가 저장되었습니다!');
navigate('/', { replace: true }); // 홈으로 이동
};
return (
<div>
<Header
title="새 일기 쓰기"
leftChild={<Button text="< 뒤로가기" onClick={handleGoBack} />}
/>
<Editor onSubmit={handleSubmit} />
</div>
);
};
export default New;
코드 설명
const handleSubmit = (data) => {
const { date, emotionId, content } = data;
onCreate(date, emotionId, content); // App.js의 함수 호출
alert('일기가 저장되었습니다!');
navigate('/', { replace: true }); // 홈으로 이동
};
- Editor에서 받은 데이터를 App.js로 전달
replace: true로 이동하면 뒤로가기 했을 때 New 페이지로 안 돌아감
Diary.js - 일기 상세보기
이제 일기를 클릭했을 때 보여줄 상세 페이지를 만들어봅시다.
Diary.js 완성 코드
import { useNavigate, useParams } from 'react-router-dom';
import { useEffect, useState } from 'react';
import Header from '../components/Header';
import Button from '../components/Button';
// 감정 목록 (EmotionItem과 동일)
const emotionList = [
{ id: 1, name: '완전 좋음', emoji: '😊' },
{ id: 2, name: '좋음', emoji: '😄' },
{ id: 3, name: '그럭저럭', emoji: '😐' },
{ id: 4, name: '나쁨', emoji: '😞' },
{ id: 5, name: '끔찍함', emoji: '😢' }
];
const Diary = ({ data, onDelete }) => {
const { id } = useParams();
const navigate = useNavigate();
const [currentDiary, setCurrentDiary] = useState(null);
useEffect(() => {
// URL의 id로 일기 찾기
const diary = data.find((item) => item.id === parseInt(id));
if (!diary) {
alert('존재하지 않는 일기입니다.');
navigate('/', { replace: true });
return;
}
setCurrentDiary(diary);
}, [id, data, navigate]);
if (!currentDiary) {
return <div>로딩 중...</div>;
}
// 날짜 포맷팅
const getFormattedDate = (timestamp) => {
const date = new Date(timestamp);
return `${date.getFullYear()}년 ${date.getMonth() + 1}월 ${date.getDate()}일`;
};
// 감정 정보 가져오기
const emotionItem = emotionList.find(
(item) => item.id === currentDiary.emotionId
);
// 삭제 처리
const handleDelete = () => {
if (window.confirm('정말 삭제하시겠습니까?')) {
onDelete(currentDiary.id);
navigate('/', { replace: true });
}
};
// 수정 페이지로 이동
const handleEdit = () => {
navigate(`/edit/${currentDiary.id}`);
};
return (
<div>
<Header
title={getFormattedDate(currentDiary.date)}
leftChild={<Button text="< 뒤로가기" onClick={() => navigate(-1)} />}
rightChild={<Button text="수정하기" onClick={handleEdit} />}
/>
<div style={{ padding: '20px' }}>
{/* 감정 표시 */}
<div style={{
textAlign: 'center',
padding: '40px',
backgroundColor: '#f8f8f8',
borderRadius: '10px',
marginBottom: '30px'
}}>
<div style={{ fontSize: '80px', marginBottom: '10px' }}>
{emotionItem.emoji}
</div>
<div style={{ fontSize: '24px', fontWeight: 'bold' }}>
오늘의 감정: {emotionItem.name}
</div>
</div>
{/* 일기 내용 */}
<div style={{
padding: '30px',
backgroundColor: '#fff',
border: '1px solid #e2e2e2',
borderRadius: '10px',
minHeight: '300px',
fontSize: '18px',
lineHeight: '1.8',
whiteSpace: 'pre-wrap'
}}>
{currentDiary.content}
</div>
{/* 삭제 버튼 */}
<div style={{ marginTop: '30px', textAlign: 'center' }}>
<Button text="삭제하기" type="negative" onClick={handleDelete} />
</div>
</div>
</div>
);
};
export default Diary;
핵심 포인트
1. useEffect로 일기 찾기
useEffect(() => {
const diary = data.find((item) => item.id === parseInt(id));
if (!diary) {
alert('존재하지 않는 일기입니다.');
navigate('/', { replace: true });
return;
}
setCurrentDiary(diary);
}, [id, data, navigate]);
- URL의
:id로 해당 일기를 찾음 - 없으면 alert 후 홈으로 이동
data나id가 바뀔 때마다 다시 실행
2. 날짜 포맷팅
const getFormattedDate = (timestamp) => {
const date = new Date(timestamp);
return `${date.getFullYear()}년 ${date.getMonth() + 1}월 ${date.getDate()}일`;
};
timestamp를 “2024년 12월 7일” 형식으로 변환
Edit.js - 일기 수정 페이지
수정 페이지는 New와 비슷하지만, 기존 데이터를 불러와서 Editor에 전달해야 해요.
Edit.js 완성 코드
import { useNavigate, useParams } from 'react-router-dom';
import { useEffect, useState } from 'react';
import Editor from '../components/Editor';
import Header from '../components/Header';
import Button from '../components/Button';
const Edit = ({ data, onUpdate }) => {
const { id } = useParams();
const navigate = useNavigate();
const [currentDiary, setCurrentDiary] = useState(null);
useEffect(() => {
const diary = data.find((item) => item.id === parseInt(id));
if (!diary) {
alert('존재하지 않는 일기입니다.');
navigate('/', { replace: true });
return;
}
setCurrentDiary(diary);
}, [id, data, navigate]);
const handleGoBack = () => {
navigate(-1);
};
const handleSubmit = (updatedData) => {
if (window.confirm('정말 수정하시겠습니까?')) {
const { date, emotionId, content } = updatedData;
onUpdate(currentDiary.id, date, emotionId, content);
alert('수정이 완료되었습니다!');
navigate(`/diary/${currentDiary.id}`, { replace: true });
}
};
if (!currentDiary) {
return <div>로딩 중...</div>;
}
return (
<div>
<Header
title="일기 수정하기"
leftChild={<Button text="< 뒤로가기" onClick={handleGoBack} />}
/>
<Editor onSubmit={handleSubmit} initData={currentDiary} />
</div>
);
};
export default Edit;
핵심 차이점
<Editor onSubmit={handleSubmit} initData={currentDiary} />
initDataProps로 기존 일기 데이터를 전달!- Editor가 이 데이터로 초기값을 설정해야 함
Editor.js 수정 필요
Editor 컴포넌트가 initData를 받을 수 있도록 수정해야 해요:
const Editor = ({ onSubmit, initData }) => {
const [state, setState] = useState({
date: initData?.date
? new Date(initData.date).toISOString().split('T')[0]
: new Date().toISOString().split('T')[0],
emotionId: initData?.emotionId || 3,
content: initData?.content || ''
});
// ... 나머지 코드 동일
};
initData가 있으면 그 값을 사용- 없으면 (New 페이지) 기본값 사용
Home.js - 일기 목록 페이지
마지막으로 홈 페이지에서 일기 목록을 보여줄게요.
Home.js 완성 코드
import { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import Header from '../components/Header';
import Button from '../components/Button';
const Home = ({ data }) => {
const navigate = useNavigate();
const [sortType, setSortType] = useState('latest');
// 정렬된 데이터
const getSortedData = () => {
return data.toSorted((a, b) => {
if (sortType === 'oldest') {
return a.date - b.date; // 오래된 순
} else {
return b.date - a.date; // 최신 순
}
});
};
const sortedData = getSortedData();
// 날짜 포맷팅
const getFormattedDate = (timestamp) => {
const date = new Date(timestamp);
return `${date.getFullYear()}.${String(date.getMonth() + 1).padStart(2, '0')}.${String(date.getDate()).padStart(2, '0')}`;
};
return (
<div>
<Header
title="감정 일기장"
rightChild={<Button text="새 일기 쓰기" type="positive" onClick={() => navigate('/new')} />}
/>
{/* 정렬 옵션 */}
<div style={{ marginBottom: '20px' }}>
<select
value={sortType}
onChange={(e) => setSortType(e.target.value)}
style={{
padding: '10px',
fontSize: '16px',
border: '1px solid #ccc',
borderRadius: '5px'
}}
>
<option value="latest">최신순</option>
<option value="oldest">오래된순</option>
</select>
</div>
{/* 일기 목록 */}
<div>
{sortedData.length === 0 ? (
<div style={{
textAlign: 'center',
padding: '60px',
color: '#999',
fontSize: '18px'
}}>
아직 작성된 일기가 없습니다.<br/>
새 일기를 작성해보세요!
</div>
) : (
sortedData.map((item) => (
<div
key={item.id}
onClick={() => navigate(`/diary/${item.id}`)}
style={{
padding: '20px',
marginBottom: '15px',
backgroundColor: '#f8f8f8',
borderRadius: '10px',
cursor: 'pointer',
border: '2px solid transparent',
transition: 'all 0.2s'
}}
onMouseEnter={(e) => {
e.currentTarget.style.borderColor = '#64c964';
e.currentTarget.style.backgroundColor = '#fff';
}}
onMouseLeave={(e) => {
e.currentTarget.style.borderColor = 'transparent';
e.currentTarget.style.backgroundColor = '#f8f8f8';
}}
>
<div style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center'
}}>
<div style={{
fontSize: '40px',
marginRight: '20px'
}}>
{item.emotionId === 1 && '😊'}
{item.emotionId === 2 && '😄'}
{item.emotionId === 3 && '😐'}
{item.emotionId === 4 && '😞'}
{item.emotionId === 5 && '😢'}
</div>
<div style={{ flex: 1 }}>
<div style={{
fontSize: '18px',
fontWeight: 'bold',
marginBottom: '8px'
}}>
{getFormattedDate(item.date)}
</div>
<div style={{
fontSize: '16px',
color: '#666',
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap'
}}>
{item.content.slice(0, 50)}
{item.content.length > 50 && '...'}
</div>
</div>
<Button text="상세보기 >" onClick={(e) => {
e.stopPropagation();
navigate(`/diary/${item.id}`);
}} />
</div>
</div>
))
)}
</div>
</div>
);
};
export default Home;
핵심 기능
1. 정렬
const getSortedData = () => {
return data.toSorted((a, b) => {
if (sortType === 'oldest') {
return a.date - b.date;
} else {
return b.date - a.date;
}
});
};
toSorted()는 원본 배열을 변경하지 않고 새 배열 반환- timestamp 숫자 비교로 정렬
2. 빈 목록 처리
{sortedData.length === 0 ? (
<div>아직 작성된 일기가 없습니다.</div>
) : (
// 일기 목록
)}
3. 내용 미리보기
{item.content.slice(0, 50)}
{item.content.length > 50 && '...'}
처음 50자만 보여주고 더 길면 ”…” 추가
최종 확인하기
모든 코드를 작성했으면 이제 테스트해볼 차례예요!
✅ 전체 파일 체크리스트
src/
├── App.js ✓ 전체 데이터 관리
├── components/
│ ├── Button.js ✓ (4편에서 작성)
│ ├── Button.css ✓
│ ├── Header.js ✓ (4편에서 작성)
│ ├── Editor.js ✓ (initData 추가)
│ ├── Editor.css ✓
│ ├── EmotionItem.js ✓ (4편에서 작성)
│ └── EmotionItem.css ✓
└── page/
├── Home.js ✓ 일기 목록
├── New.js ✓ 일기 작성
├── Diary.js ✓ 일기 상세
└── Edit.js ✓ 일기 수정
✅ 기능 테스트
1. 일기 작성 (CREATE)
- ”/” 접속 → “새 일기 쓰기” 버튼 클릭
- “/new” 페이지에서 날짜, 감정, 내용 입력
- “작성 완료” 클릭
- 홈으로 이동하고 새 일기가 목록에 표시됨
2. 일기 목록 (READ)
- 홈에서 일기 목록 확인
- 정렬 옵션 (최신순/오래된순) 변경 시 순서 바뀜
- 일기 카드 클릭 시 상세 페이지로 이동
3. 일기 상세보기 (READ)
- “/diary/1” 접속 시 해당 일기 내용 표시
- 감정 이모지와 날짜 정확히 표시
- “뒤로가기” 버튼 동작
- “수정하기” 버튼으로 Edit 페이지 이동
4. 일기 수정 (UPDATE)
- “/edit/1” 접속 시 기존 내용이 Editor에 나타남
- 내용 수정 후 “작성 완료” 클릭
- Diary 페이지로 이동하고 변경사항 반영됨
5. 일기 삭제 (DELETE)
- Diary 페이지에서 “삭제하기” 클릭
- Confirm 창에서 “확인”
- 홈으로 이동하고 해당 일기가 목록에서 사라짐
✅ React DevTools로 확인
Chrome에서 React DevTools를 열고:
- App 컴포넌트의
dataState 확인 - 일기 작성/수정/삭제 시 실시간으로 변화 관찰
- Props가 제대로 전달되는지 확인
전체 데이터 흐름 정리
마지막으로 전체 흐름을 한 번 더 정리하면:
sequenceDiagram
participant 사용자
participant Home
participant App
participant New
participant Editor
사용자->>Home: "새 일기 쓰기" 클릭
Home->>New: navigate('/new')
사용자->>Editor: 일기 작성
Editor->>New: onSubmit(data)
New->>App: onCreate(date, emotionId, content)
App->>App: setData([newItem, ...data])
App->>Home: data Props 업데이트
Home->>사용자: 새 일기 목록에 표시
핵심 패턴:
- 자식이 버튼을 클릭 → 함수 실행
- 그 함수는 사실 부모에서 받은 것
- 부모의 State가 변경
- 모든 자식이 새 Props를 받고 리렌더링
자주 묻는 질문
Q1. 새로고침하면 데이터가 사라지는데요?
맞아요! 지금은 State에만 저장하니까 새로고침하면 초기값(mockData)으로 돌아가요.
해결 방법:
- localStorage: 브라우저에 저장 (간단함)
- Firebase/Supabase: 클라우드 DB (실시간 동기화)
- 백엔드 API: 직접 서버 구축
// localStorage 예시
useEffect(() => {
localStorage.setItem('diaryData', JSON.stringify(data));
}, [data]);
const [data, setData] = useState(() => {
const saved = localStorage.getItem('diaryData');
return saved ? JSON.parse(saved) : mockData;
});
Q2. Props를 계속 전달하는 게 불편한데요?
지금은 Props Drilling이라고 해서 깊이 전달해야 해요.
더 나은 방법:
- Context API: React 내장 전역 State
- Redux: 큰 프로젝트용 상태 관리
- Zustand: 가볍고 쉬운 상태 관리
이건 중급 주제니까 나중에 배워보세요!
Q3. 날짜를 timestamp로 저장하는 이유는?
// ✅ timestamp (숫자)
{ date: 1701907200000 } // 정렬, 비교가 쉬움
// ❌ 문자열
{ date: "2024-12-07" } // 정렬 시 문자열 비교 필요
숫자가 정렬하기 훨씬 쉽고, 다양한 포맷으로 변환도 편해요!
Q4. useRef vs useState 차이가 뭔가요?
// useState - 값이 바뀌면 리렌더링 발생
const [count, setCount] = useState(0);
// useRef - 값이 바뀌어도 리렌더링 X
const countRef = useRef(0);
ID는 화면에 안 보이니까 useRef로 관리하면 불필요한 리렌더링을 막을 수 있어요!
정리하며
🎉 축하합니다! 드디어 완벽하게 작동하는 감정일기장을 완성했어요!
전체 시리즈 복습:
| 편 | 주제 | 핵심 개념 |
|---|---|---|
| 1편 | 프로젝트 시작 | CRA, index.html→index.js→App.js |
| 2편 | 페이지 라우팅 | React Router, Link, useParams |
| 3편 | State 관리 | useState, 이벤트 핸들러, 불변성 |
| 4편 | 컴포넌트 분리 | Props, 재사용성, 컴포넌트 합성 |
| 5편 | 앱 통합 | CRUD, Props Drilling, 데이터 흐름 |
배운 것들:
- ✅ React의 핵심 개념 (컴포넌트, State, Props)
- ✅ 라우팅과 페이지 이동
- ✅ CRUD 기능 구현
- ✅ 부모-자식 간 데이터 통신
- ✅ 이벤트 처리와 사용자 인터랙션
다음 단계 추천
이제 기본은 마스터했으니, 이런 기능들을 추가해보세요:
입문자:
- 📱 localStorage로 데이터 저장
- 🎨 CSS로 더 예쁘게 꾸미기
- 🔍 일기 검색 기능
중급자:
- 🌐 Context API로 리팩토링
- 📊 월별 감정 통계
- 🔐 로그인/회원가입
고급자:
- ☁️ Firebase 연동
- 📸 사진 업로드
- 🌙 다크모드
여러분 정말 수고하셨어요! 💪
처음엔 어려웠을 텐데 5편까지 완주하신 여러분, 정말 대단해요! 이제 React의 기초는 완벽하게 이해하셨을 거예요.
계속 연습하고 새로운 기능을 추가하면서 실력을 키워가세요. 코딩의 진짜 재미는 직접 만들면서 느끼는 것이니까요!
이제 자신만의 프로젝트를 시작해보세요. 화이팅! 🚀
시리즈 네비게이션
- 1편: React 프로젝트 시작하기 - 기본 구조 이해
- 2편: React Router로 페이지 만들기
- 3편: State로 데이터 관리하기
- 4편: 재사용 가능한 컴포넌트 만들기
- 5편: 전체 앱 연결 및 데이터 관리 ← 현재 글 (완결)