오늘은 Mac 기본 메모 앱을 참고하여 React와 Redux를 사용한 메모 앱을 구현했습니다. 주요 기능은 다음과 같습니다
- 메모의 CRUD 기능: 메모 추가(Create), 삭제(Delete), 수정(Update), 선택(Read) 기능
- 메모 항목 시간 순 정렬: 메모 리스트를 Date()객체를 활용해서 최신 순으로 정렬
- 메모 리스트 제목과 메모 내용 연동: 메모를 수정하면 사이드바에 메모 리스트의 메모 제목이 같이 수정
주요 구현 내용
Redux를 사용한 상태 관리
- memo.reducer.js에서 메모와 관련된 상태를 관리하는 리듀서를 작성했습니다.
- 메모의 추가, 삭제, 수정, 선택 기능을 위한 액션과 액션 크리에이터를 정의했습니다.
- 로컬 스토리지를 사용하여 메모 데이터를 저장하고 불러왔습니다.
Router 설정:
- createBrowserRouter를 사용하여 라우팅을 설정했습니다.
- 공통 레이아웃을 적용하여 각 페이지가 동일한 레이아웃을 가지도록 구성했습니다.
메모 목록과 메모 내용 컴포넌트:
- MemoSide 컴포넌트에서 메모 목록을 표시하고, 새로운 메모를 추가하거나 선택된 메모를 삭제하는 기능을 구현했습니다.
- MemoArticle 컴포넌트에서 선택된 메모의 내용을 편집할 수 있도록 했습니다.
스타일링:
- styled-components를 사용하여 컴포넌트의 스타일을 정의했습니다.
- 레이아웃 컴포넌트를 사용하여 전체적인 앱의 레이아웃을 설정했습니다.
코드 구조
- Redux Reducer: memo.reducer.js
- Redux Store: store.js
- Main App: App.jsx, main.jsx
- Routing: router.js
- Layout Component: Layout.jsx
- Memo Components: MemoSide.jsx, MemoArticle.jsx
- Styling: Layout.styled.js, MemoSide.styled.js, MemoArticle.styled.js
주요 코드
Redux Reducer (memo.reducer.js)
import { createSelector } from "@reduxjs/toolkit";
// Action Types
const ADD_MEMO = "memo/ADD_MEMO";
const DELETE_MEMO = "memo/DELETE_MEMO";
const EDIT_MEMO = "memo/EDIT_MEMO";
const SELECT_MEMO = "memo/SELECT_MEMO";
// Action Creators
export const addMemo = () => ({
type: ADD_MEMO,
payload: { id: Date.now(), content: "" },
});
export const deleteMemo = (id) => ({
type: DELETE_MEMO,
payload: id,
});
export const editMemo = (id, content) => ({
type: EDIT_MEMO,
payload: { id, content },
});
export const selectMemo = (id) => ({
type: SELECT_MEMO,
payload: id,
});
const selectMemos = (state) => state.memo.memos;
const selectSelectedMemoId = (state) => state.memo.selectedMemoId;
export const selectMemosAndSelectedMemoId = createSelector(
[selectMemos, selectSelectedMemoId],
(memos, selectedMemoId) => ({ memos, selectedMemoId })
);
const initialMemoId = Date.now();
const localData = JSON.parse(localStorage.getItem("memo")) || {
memos: [],
selectedMemoId: null,
};
const initialState = {
memos: localData.memos.length
? localData.memos
: [
{
id: initialMemoId,
content: "",
},
],
selectedMemoId: localData.memos.length
? localData.memos[0].id
: initialMemoId,
};
const memoReducer = (prevState = initialState, action) => {
switch (action.type) {
case ADD_MEMO: {
const newMemo = action.payload;
const updatedMemos = [newMemo, ...prevState.memos];
localStorage.setItem(
"memo",
JSON.stringify({ memos: updatedMemos, selectedMemoId: newMemo.id })
);
return {
...prevState,
memos: updatedMemos,
selectedMemoId: newMemo.id,
};
}
case DELETE_MEMO: {
if (prevState.memos.length <= 1) {
alert("하나 이상의 메모는 남겨두어야 합니다.");
return prevState;
}
const memosAfterDeletion = prevState.memos.filter(
(memo) => memo.id !== action.payload
);
localStorage.setItem(
"memo",
JSON.stringify({
memos: memosAfterDeletion,
selectedMemoId: memosAfterDeletion[0].id,
})
);
return {
...prevState,
memos: memosAfterDeletion,
selectedMemoId: memosAfterDeletion[0].id,
};
}
case EDIT_MEMO: {
const updatedMemos = prevState.memos.map((memo) =>
memo.id === action.payload.id
? { ...memo, content: action.payload.content }
: memo
);
localStorage.setItem(
"memo",
JSON.stringify({
memos: updatedMemos,
selectedMemoId: prevState.selectedMemoId,
})
);
return {
...prevState,
memos: updatedMemos,
};
}
case SELECT_MEMO: {
return {
...prevState,
selectedMemoId: action.payload,
};
}
default:
return prevState;
}
};
export default memoReducer;
Router 설정 (router.js)
import { createBrowserRouter } from "react-router-dom";
import Layout from "../components/Layout";
import MainPage from "../pages/MainPage";
const router = createBrowserRouter([
{
path: "/",
element: <Layout />,
children: [
{
path: "/",
element: <MainPage />,
},
{
path: "/memo/:memoId",
element: <MainPage />,
},
],
},
]);
export default router;
Memo Components (MemoSide.jsx, MemoSide.jsx)
// MemoSide.jsx
import { useDispatch, useSelector } from "react-redux";
import { Link } from "react-router-dom";
import {
addMemo,
deleteMemo,
selectMemo,
selectMemosAndSelectedMemoId,
} from "../../../redux/reducers/memo.reducer";
import { NowTime } from "../NowTime/NowTime";
import {
MemoAside,
MemoItem,
MemoList,
MemoSideHeader,
} from "./MemoSide.styled";
function MemoSide() {
console.log("MemoSide 리랜더링");
const dispatch = useDispatch();
const { memos, selectedMemoId } = useSelector(selectMemosAndSelectedMemoId);
const handleSelectMemo = (id) => {
dispatch(selectMemo(id));
};
const handleAddMemo = () => {
dispatch(addMemo());
};
const handleDeleteMemo = () => {
if (memos.length <= 1) {
alert("하나 이상의 메모는 남겨두어야 합니다.");
return;
}
dispatch(deleteMemo(selectedMemoId));
};
return (
<MemoAside>
<MemoSideHeader>
<button onClick={handleAddMemo}>새 메모 작성하기</button>
<button onClick={handleDeleteMemo}>삭제</button>
</MemoSideHeader>
<MemoList>
{memos.map((memo) => (
<Link key={memo.id} to={`/memo/${memo.id}`}>
<MemoItem
key={memo.id}
onClick={() => handleSelectMemo(memo.id)}
$isSelected={memo.id === selectedMemoId}
>
<h6>{memo.content || "새로운 메모"}</h6>
<time>{NowTime("time", memo.id)}</time>
</MemoItem>
</Link>
))}
</MemoList>
</MemoAside>
);
}
export default MemoSide;
// MemoArticle.jsx
import { useDispatch, useSelector } from "react-redux";
import {
editMemo,
selectMemosAndSelectedMemoId,
} from "../../../redux/reducers/memo.reducer";
import { NowTime } from "../NowTime/NowTime";
import { CurrentTime, StrMemoArticle } from "./MemoArticle.styled";
function MemoArticle() {
const dispatch = useDispatch();
const { memos, selectedMemoId } = useSelector(selectMemosAndSelectedMemoId);
const memo = memos.find((memo) => memo.id === selectedMemoId) || {};
const handleContentChange = (e) => {
dispatch(editMemo(memo.id, e.target.value));
};
return (
<StrMemoArticle>
<CurrentTime>{NowTime("datetime")}</CurrentTime>
<textarea
value={memo.content}
name="memo"
id={memo.id}
onChange={handleContentChange}
></textarea>
</StrMemoArticle>
);
}
export default MemoArticle;
마무리
오늘 구현한 메모 앱을 Redux와 React Router를 통해 구현 해봤습니다. Redux reducer에서 로컬 스토리지를 활용하여 CRUD 기능을 구현하고 조건부 렌더링 styled-components를 사용한 스타일링 기법을 연습 했습니다.
'TIL' 카테고리의 다른 글
TIL - Form Input - useRef vs useState (0) | 2024.05.26 |
---|---|
React - Debounce 적용하기 (0) | 2024.05.24 |
TIL - 리액트로 가계부 만들기 - 3 (0) | 2024.05.23 |
Redux - 비동기 처리 createAsyncThunk (0) | 2024.05.23 |
TIL - 리액트로 가계부 만들기 - 2 (0) | 2024.05.22 |