[Project] 감정 일기장
01
프로젝트 소개
이 프로젝트는 "한입 크기로 잘라먹는 리액트" 강의의 마지막 실습 프로젝트입니다.
React와 JavaScript를 활용하여 감정 일기장을 제작하였으며, 단순한 일기가 아니라 감정을 함께 기록할 수 있는 특별한 일기장입니다.
이번 프로젝트에서는 외부 폰트 적용법, 이미지 최적화 방법, 다양한 페이지 구성, 자주 사용하는 요소의 컴포넌트화, 그리고 실제 웹 배포 과정까지 실습했습니다.
1. 폴더 구조
컴포넌트를 분리하여 구조를 깔끔하게 유지하였고,
스타일(CSS) 파일은 styles 폴더에서 관리한 후 global.css에서 한 번에 export하여 사용했습니다.
\---src
| App.jsx
| index.css
| main.jsx
+---assets
| emotion1.png
| ...
+---components
| Header.jsx
| ...
+---hooks
| ...
+---pages
| Home.jsx
| ...
+---styles
| | global.css
| \---components
| header.css
| ...
\---util
...
2. 페이지 라우팅
프로젝트는 크게 네 개의 페이지로 구성되었으며,
Edit과 Diary 페이지는 id 값을 받아 개별 페이지를 렌더링 하도록 설정했습니다.
<Routes>
<Route path="/" element={<Home />} />
<Route path="new" element={<New />} />
<Route path="/diary/:id" element={<Diary />} />
<Route path="/edit/:id" element={<Edit />} />
</Routes>
이렇게 설정함으로써 일기 데이터를 개별적으로 조회하고 수정할 수 있도록 구조화했습니다.
3. 폰트,이미지 설정
폰트 적용
폰트는 public 폴더에 저장하여 관리하였습니다.
이렇게 하면 빌드 시 폰트가 상대 경로로 지정되어 별도의 import 없이 직접 접근 가능합니다.
@font-face {
font-family: "NanumPenScript";
src: url("/NanumPenScript-Regular.ttf");
}
* {
padding: 0;
margin: 0;
box-sizing: border-box;
font-family: "NanumPenScript";
}
이미지 최적화
이미지는 src/assets 폴더에 보관하여 관리했습니다.
왜 public이 아니라 src/assets 폴더를 사용했을까?
- src 내부에 두면 import를 활용하여 경로를 자동으로 관리할 수 있기 때문입니다.
- 이미지 파일이 많아지면 최적화를 통해 불필요한 로딩 시간을 줄일 수 있습니다.
02
개발 과정 - 메인 기능
이 프로젝트에서는 일기 생성(Create), 수정(Update), 삭제(Delete), 정렬(Sort) 기능을 구현했습니다.
초기에는 useState를 이용하여 각 컴포넌트에서 개별적으로 상태를 관리했지만, 상태 관리의 복잡성이 증가하면서 코드가 길어지고 중복이 많아지는 문제가 발생했습니다.
이를 해결하기 위해 useReducer를 활용한 상태 관리와 Context API를 이용한 전역 상태 공유를 도입했습니다.
1. useReducer를 이용한 상태 관리
초기에는 상태를 useState로 관리했지만, 일기 데이터가 많아지고 CRUD 기능이 추가되면서 상태 변경 로직이 복잡해지는 문제가 있었습니다.
useReducer를 도입하면 상태 변경 로직을 하나의 함수(reducer)에서 관리할 수 있어 코드의 가독성이 좋아졌습니다.
✅ 기존 방식 - useState 사용
const [diaryList, setDiaryList] = useState([]);
const onCreate = (date, emotionId, content) => {
const newDiary = { id: Date.now(), date, emotionId, content };
setDiaryList([...diaryList, newDiary]);
};
❌ 문제점
- 상태 변경 로직이 여러 개의 함수에 분산되어 있어 유지보수가 어려움
- setDiaryList를 직접 호출하기 때문에 불필요한 상태 업데이트가 발생할 가능성이 있음
✅ 개선된 방식 - useReducer 적용
useReducer를 사용하면 상태 변경 로직을 하나의 reducer 함수로 통합할 수 있습니다.
const reducer = (state, action) => {
switch (action.type) {
case "CREATE":
return [...state, action.data];
case "UPDATE":
return state.map((item) =>
item.id === action.data.id ? action.data : item
);
case "DELETE":
return state.filter((item) => item.id !== action.targetId);
case "SORT":
return state.slice().sort((a, b) =>
action.sortType === "latest" ? b.date - a.date : a.date - b.date
);
default:
return state;
}
};
const [state, dispatch] = useReducer(reducer, []);
📌 개선된 점
- 상태 변경 로직을 한 곳에 모아서 관리 가능 → 가독성 증가
- dispatch를 사용하여 불필요한 상태 업데이트를 줄이고 최적화 가능
- 기능이 추가될 때도 reducer 내부에 case만 추가하면 됨 → 유지보수 용이
2. Context API를 이용한 전역 상태 공유
useReducer를 적용한 후, 상태를 여러 페이지에서 공유하려면 props로 데이터를 전달해야 하는 문제가 있었습니다.
이 과정에서 props drilling(깊이 있는 props 전달) 문제가 발생하여 가독성이 떨어졌습니다.
이 문제를 해결하기 위해 Context API를 사용하여 전역적으로 데이터를 관리하도록 개선했습니다.
✅ 기존 방식 (props로 직접 전달)
return(
<>
<Routes>
<Route path="/" element={<Home data={data} onDelete={onDelete} onSort={onSort}/>} />
<Route path="new" element={<New onCreate={onCreate}/>} />
<Route path="/diary/:id" element={<Diary data={data}/>} />
<Route path="/edit/:id" element={<Edit data={data} onUpdate={onUpdate} onDelete={onDelete}/>} />
</Routes>
</>
)
❌ 문제점
- data, onCreate, onUpdate, onDelete 등을 여러 개의 컴포넌트에 전달해야 함
- 컴포넌트가 많아질수록 props를 계속 추가해야 해서 유지보수가 어려워짐
✅ 개선된 방식 - Context API 적용
Context API를 사용하여 data와 dispatch 함수를 전역으로 관리하도록 변경했습니다.
export const DiaryStateContext = createContext();
export const DiaryDispatchContext = createContext();
function App() {
return (
<>
<DiaryStateContext.Provider value={data}>
<DiaryDispatchContext.Provider
value={{ onCreate, onUpdate, onDelete, onSort }}
>
<Routes>
<Route path="/" element={<Home />} />
<Route path="new" element={<New />} />
<Route path="/diary/:id" element={<Diary />} />
<Route path="/edit/:id" element={<Edit />} />
</Routes>
</DiaryDispatchContext.Provider>
</DiaryStateContext.Provider>
</>
);
}
📌 개선된 점
- useContext를 사용하여 필요한 곳에서만 데이터를 가져와서 사용 가능
- props drilling 문제 해결 → 코드가 간결해지고 유지보수가 쉬워짐
- dispatch를 어디서든 호출할 수 있어 더 유연한 상태 관리 가능
03
개발 과정 - UI
초기에는 페이지를 제작할 때 버튼, 헤더 같은 공통 컴포넌트만 분리하고, 나머지 UI와 기능을 각 페이지에서 모두 처리하는 방식으로 구현했습니다.
이 방식은 처음에는 간단해 보였지만, 프로젝트가 커지면서 코드 길이가 길어지고 가독성이 떨어지는 문제가 발생했습니다.
✅ 기존 방식 - 모든 UI와 기능을 페이지에 직접 작성
초기에는 Home, New, Edit, Diary 페이지마다 필요한 기능과 UI를 직접 구현했습니다.
예를 들어, Header와 Editor 같은 요소를 각 페이지에서 직접 사용하고, 필요한 기능도 개별적으로 처리했습니다.
❌ 문제점
- 코드가 너무 길어짐 → 기능이 많아질수록 가독성이 떨어짐
- 같은 UI 요소가 반복됨 → Header, Editor 같은 요소를 매번 사용해야 함
- 유지보수가 어려움 → 같은 요소를 수정할 때 여러 파일을 수정해야 하는 번거로움
✅ 개선된 방식 - 기능과 UI를 컴포넌트로 분리
이 문제를 해결하기 위해 공통 UI 요소와 주요 기능을 컴포넌트로 분리하고,
페이지에서는 필요한 기능을 가져와서 조합하는 방식으로 개선했습니다.
📌 개선된 점
- UI 요소를 개별 컴포넌트로 분리하여 재사용성을 높임
//Home.jsx
return (
<div className="home">
<Header />
<MenuBar />
<DiaryList />
</div>
);
//New.jsx
return (
<div className="new">
<Header />
<Editor />
</div>
);
//Edit.jsx
return (
<div>
<Header />
<Editor />
</div>
);
//Diary.jsx
return (
<div className="diary">
<Header />
<Viewer />
</div>
);
04
리팩토링
각 컴포넌트에서 중복으로 사용되는 함수들을 util 함수와 커스텀 훅으로 분리하여 관리했습니다.
이 과정에서 이미지 처리, 날짜 변환, 데이터 불러오기 기능을 최적화하여 코드의 가독성과 유지보수성을 향상시켰습니다.
1. 이미지 import 최적화
✅ 기존 방식 - 각 파일에서 개별적으로 import
기존에는 각 파일에서 직접 이미지를 import하여 사용해야 했기 때문에, 코드가 중복되고 관리가 어려웠습니다.
❌ 문제점
- 모든 파일에서 개별적으로 import 해야 함 → 코드가 중복됨
- 이미지가 추가될 경우 → 여러 파일을 수정해야 함
✅ 개선된 방식 - 이미지 관리 유틸 함수 사용
getEmotionImage 함수를 호출하기만 하면 자동으로 이미지를 불러올 수 있습니다.
import emotion1 from "../assets/emotion1.png";
import emotion2 from "../assets/emotion2.png";
import emotion3 from "../assets/emotion3.png";
import emotion4 from "../assets/emotion4.png";
import emotion5 from "../assets/emotion5.png";
function getEmotionImage(id) {
switch (id) {
case 1:
return emotion1;
case 2:
return emotion2;
case 3:
return emotion3;
case 4:
return emotion4;
case 5:
return emotion5;
default:
return null;
}
}
export default getEmotionImage;
📌 개선된 점
- 중복 import 제거 → 모든 파일에서 개별적으로 import 할 필요 없음
- 유지보수 용이 → 새로운 이미지 추가 시 한 곳만 수정하면 됨
- 코드 가독성 향상
2. 날짜 변환 기능 최적화
감정 일기장에서는 날짜 데이터를 타임스탬프(숫자) 형태로 저장하고 있습니다.
하지만, 화면에서 날짜를 출력할 때는 YYYY-MM-DD 형식으로 변환해야 했기 때문에,
이전에는 각 컴포넌트에서 변환 로직을 중복 작성하는 문제가 있었습니다.
✅ 기존 방식 - 개별적으로 날짜 변환
❌ 문제점
- 각 파일에서 변환 로직을 직접 구현해야 함
- 날짜 포맷을 바꾸려면 모든 파일을 수정해야 함
✅ 개선된 방식 - getStringedDate 유틸 함수 사용
이제 getStringedDate를 호출하면 통일된 날짜 형식(YYYY-MM-DD)으로 변환 가능합니다.
export const getStringedDate = (targetDate) => {
if (!(targetDate instanceof Date)) {
targetDate = new Date(targetDate);
}
let year = targetDate.getFullYear();
let month = targetDate.getMonth() + 1;
let date = targetDate.getDate();
if (month < 10) {
month = `0${month}`;
}
if (date < 10) {
date = `0${date}`;
}
return `${year}-${month}-${date}`;
};
📌 개선된 점
- 중복 코드 제거 → 날짜 변환 로직을 한 곳에서 관리 가능
- 일관된 포맷 유지 → 모든 날짜가 같은 형식으로 출력됨
- 유지보수 용이 → 날짜 포맷을 변경해야 할 경우, 유틸 함수만 수정하면 됨
3. DATA 불러오기 기능 최적화 (커스텀 훅 사용)
Edit.jsx와 Diary.jsx에서는 특정 ID에 해당하는 일기 데이터를 불러와야 하는 기능이 필요합니다.
하지만, 두 페이지에서 동일한 코드가 사용되면서 중복이 발생하는 문제가 있었습니다.
✅ 기존 방식 - 페이지마다 개별적으로 데이터 조회
const data = useContext(DiaryStateContext);
const currentDiary = data.find((item) => String(item.id) === String(id));
if (!currentDiary) {
alert("존재하지 않는 일기입니다!");
navigate("/", { replace: true });
}
❌ 문제점
- 같은 로직을 여러 페이지에서 중복 작성해야 함
- 데이터가 변경될 때마다 모든 페이지를 수정해야 함
✅ 개선된 방식 - useDiary 커스텀 훅 사용
이제 useDiary 훅을 만들어 필요한 페이지에서 간편하게 호출할 수 있도록 변경했습니다.
import { useContext, useEffect, useState } from "react";
import { DiaryStateContext } from "../App";
import { useNavigate } from "react-router-dom";
const useDiary = (id) => {
const data = useContext(DiaryStateContext);
const [currentDiaryItem, setCurrentDiaryItem] = useState();
const navigate = useNavigate();
useEffect(() => {
if (!data || data.length === 0) {
return; // 데이터가 비어 있으면 실행하지 않음
}
const currentDiaryItem = data.find(
(item) => String(item.id) === String(id)
);
if (!currentDiaryItem) {
window.alert("존재하지 않는 일기입니다!");
navigate("/", { replace: true });
}
setCurrentDiaryItem(currentDiaryItem);
}, [id]);
return currentDiaryItem;
};
export default useDiary;
useDiary를 호출하면 중복 코드 없이 데이터를 가져올 수 있음
const currentDiary = useDiary(params.id);
📌 개선된 점
- 중복 코드 제거 → Edit, Diary 페이지에서 동일한 로직을 따로 작성할 필요 없음
- 가독성 향상 → 데이터 조회 로직이 간결해짐
- 유지보수 용이 → 데이터 조회 방식이 변경될 경우 useDiary만 수정하면 됨
유틸함수와 커스텀으로 분리하는 기준:
- 여러 곳에서 동일한 로직이 사용되는 경우 → 유틸 함수로 분리
- 컴포넌트 내부에서 비슷한 기능이 반복될 경우 → 커스텀 훅으로 변환
- 상태나 컨텍스트와 관련된 기능 → 커스텀 훅이 적절함
05
배포
이 프로젝트는 Vercel을 이용해 배포했으며, 데이터는 로컬 스토리지(LocalStorage)에 저장하는 방식으로 동작합니다.
즉, 사용자가 작성한 일기는 브라우저의 로컬 저장소에 보관되며, 페이지를 새로고침해도 유지됩니다.
하지만 다른 기기에서는 동일한 데이터를 볼 수 없으며, 브라우저 데이터를 삭제하면 초기화됩니다.
배포 과정에 대한 내용은 아래 포스팅에서 확인할 수 있습니다.
👉 https://dev-watnu.tistory.com/77
Vercel 배포, 누르기 전에 딱 5분만 확인!
React 프로젝트를 배포하기 전에 반드시 체크해야 할 사항들이 있습니다.이 과정을 건너뛰면 배포 후에 제목이 이상하게 나오거나, 썸네일이 안 뜨거나, 파비콘이 적용되지 않는 문제가 생길 수
dev-watnu.tistory.com
06
마무리 - 최종 회고 및 개선할 점
처음엔 무작정 코드를 짜고 기능부터 구현했는데, 리팩토링을 하면서 불필요하게 복잡했던 코드들이 하나씩 정리되는 과정이 흥미로습니다
특히 useState만 쓰다가 useContext와 useReducer로 상태 관리를 바꾸면서 코드가 훨씬 깔끔해지고 관리하기 편해졌습니다.
컴포넌트도 그냥 막 쪼개기만 하면 되는 게 아니라, 적절한 단위로 나누는 게 중요하다는 걸 깨달았다.
CSS도 처음엔 컴포넌트별로 따로 작성했는데, 같은 스타일을 계속 반복해서 쓰다 보니 전역 스타일을 활용하는 게 더 효율적일 수도 있겠다는 생각이 들었다.
아직 부족한 점도 많지만, 이번 프로젝트를 하면서 더 좋은 코드 구조를 고민하는 습관을 들이게 된 것 같다.
앞으로는 무조건 익숙한 방식만 쓰기보다, 더 나은 방법이 없는지 먼저 고민해보는 개발자가 되어야겠다!