목표
- alert창, confirm창 모달창으로 변경
- 메인 페이지 그래프 컴포넌트 추가
- 가계부 항목 정렬순으로 표시
주요 학습 내용
- 모달창 상태 Redux 활용해서 저장소에서 관리
- styled-components 활용해서 총 지출 그래프 구현
- useState와 switch 문 활용해서 항목 정렬
1. 모달창 상태 Redux 활용해서 저장소에서 관리
src
└── assets
└── components
└── Modal
├── AlertModal.jsx
├── ConfirmModal.jsx
└── Modal.styled.js
Alert 창과 Confirm 창을 모달 창으로 구현하고, styled-components를 사용하여 스타일링했습니다.
// src/redux/slices/modalSlice.js 파일 생성
import { createSlice } from "@reduxjs/toolkit";
const initialState = {
isConfirmModalOpen: false,
isAlertModalOpen: false,
alertMessage: "",
};
const modalSlice = createSlice({
name: "modal",
initialState,
reducers: {
openConfirmModal: (state) => {
state.isConfirmModalOpen = true;
},
closeConfirmModal: (state) => {
state.isConfirmModalOpen = false;
},
openAlertModal: (state, action) => {
state.isAlertModalOpen = true;
state.alertMessage = action.payload;
},
closeAlertModal: (state) => {
state.isAlertModalOpen = false;
state.alertMessage = "";
},
},
});
export const {
openConfirmModal,
closeConfirmModal,
openAlertModal,
closeAlertModal,
} = modalSlice.actions;
export default modalSlice.reducer;
// store에 모달 reducer 추가
import { configureStore } from "@reduxjs/toolkit";
import fetchedDataReducer from "../slices/fetchedDataSlice";
import modalReducer from "../slices/modalSlice";
const store = configureStore({
reducer: {
fetchedData: fetchedDataReducer,
modal: modalReducer,
},
});
export default store;
기존에는 모달 기능이 필요한 컴포넌트에서 useState를 사용해서 모달창을 구현했지만, 모달이 필요한 컴포넌트가 하나의 페이지가 아니여서 Redux를 활용하여 중앙에서 전역 상태를 관리하도록 변경했습니다.
2. styled-components 활용해서 총 지출 그래프 구현
// 홈페이지 컴포넌트에 그래프를 표시 할 ExpenseSummaryByMonth 컴포넌트 추가
function Homepage() {
const [filterMonth, setFilterMonth] = useState("");
return (
<StrMain className="main-container">
<StrSection>
<ExpenseForm />
</StrSection>
<StrSection>
<MonthlyExpenses setFilterMonth={setFilterMonth} />
</StrSection>
<StrSection>
<ExpenseSummaryByMonth filterMonth={filterMonth} />
</StrSection>
<StrSection>
<ExpenseListByMonth filterMonth={filterMonth} />
</StrSection>
</StrMain>
);
}
// ExpenseSummaryByMonth.jsx
import { useEffect, useState } from "react";
import { useSelector } from "react-redux";
import {
CategoryDiv,
CategoryWrapDiv,
GraphBar,
TotalAmount,
TotalAmountGraph,
} from "./ExpenseSummaryByMonth.styled";
function ExpenseSummaryByMonth({ filterMonth }) {
const [totalMonthlyExpense, setTotalMonthlyExpense] = useState(0);
const [categoryExpenses, setCategoryExpenses] = useState({});
const fetchedData = useSelector((state) => state.fetchedData);
useEffect(() => {
const filtered = fetchedData.filter((item) => {
const month = new Date(item.date).getMonth();
return month === filterMonth;
});
const initialCategoryExpenses = {
여행: 0,
미용: 0,
도서: 0,
식비: 0,
기타: 0,
};
const updatedCategoryExpenses = filtered.reduce(
(acc, currentItem) => {
const category = currentItem.item;
const amount = currentItem.amount;
const categoryName = ["여행", "미용", "도서", "식비"].includes(category)
? category
: "기타";
acc[categoryName] += amount;
return acc;
},
{ ...initialCategoryExpenses }
);
setTotalMonthlyExpense(
formattedAmount(
Object.values(updatedCategoryExpenses).reduce(
(acc, current) => acc + current,
0
)
)
);
setCategoryExpenses(updatedCategoryExpenses);
}, [fetchedData, filterMonth]);
const formattedAmount = (amount) =>
new Intl.NumberFormat("ko-KR").format(amount);
const calculateCategoryPercentage = (categoryExpense) => {
const totalExpense = Object.values(categoryExpenses).reduce(
(acc, current) => acc + current,
0
);
if (totalExpense === 0) return "0.00%";
const percentage = (categoryExpense / totalExpense) * 100;
return percentage.toFixed(2) + "%";
};
return (
<>
<TotalAmount>
{filterMonth + 1}월 총 지출: {totalMonthlyExpense} 원
</TotalAmount>
<TotalAmountGraph>
{Object.entries(categoryExpenses).map(([category, expense]) => (
<GraphBar
key={category}
$category={category}
style={{ width: calculateCategoryPercentage(expense) }}
/>
))}
</TotalAmountGraph>
<CategoryWrapDiv>
{Object.entries(categoryExpenses).map(([category, expense]) => {
if (expense > 0) {
return (
<CategoryDiv key={category} $category={category}>
<div></div>
{category}: {formattedAmount(expense)}원 (
{calculateCategoryPercentage(expense)})
</CategoryDiv>
);
}
return null;
})}
</CategoryWrapDiv>
</>
);
}
export default ExpenseSummaryByMonth;
import styled from "styled-components";
const TotalAmount = styled.div`
text-align: center;
font-weight: bold;
font-size: 26px;
margin-bottom: 20px;
`;
const TotalAmountGraph = styled.div`
display: flex;
align-items: center;
margin-bottom: 20px;
height: 40px;
background-color: rgb(233, 236, 239);
border-radius: 8px;
overflow: hidden;
`;
const GraphBar = styled.div`
height: 100%;
background-color: ${({ $category }) => getCategoryColor($category)};
`;
const getCategoryColor = ($category) => {
switch ($category) {
case "여행":
return "rgb(0, 123, 255)";
case "미용":
return "rgb(40, 167, 69)";
case "도서":
return "rgb(220, 53, 69)";
case "식비":
return "rgb(255, 193, 7)";
case "기타":
return "rgb(23, 162, 184)";
default:
return;
}
};
const CategoryWrapDiv = styled.div`
display: flex;
justify-content: center;
gap: 20px;
flex-wrap: wrap;
margin-bottom: 10px;
`;
const CategoryDiv = styled.div`
display: flex;
align-items: center;
font-size: 14px;
color: rgb(85, 85, 85);
div {
width: 20px;
height: 10px;
background-color: ${({ $category }) => getCategoryColor($category)};
margin-right: 8px;
}
`;
export {
CategoryDiv,
CategoryWrapDiv,
GraphBar,
TotalAmount,
TotalAmountGraph,
};
ExpenseSummaryByMonth 컴포넌트는 filterMonth를 기준으로 useSelector를 사용해 fetchedData를 받아와 가공한 데이터를 화면에 표시합니다. 의존성 배열에 fetchedData와 filterMonth를 포함시켜, 데이터 변경 시 리렌더링 되도록 했습니다. styled-components를 활용해 카테고리별로 스타일을 조건부로 할당했습니다.
3. 가계부 항목 정렬해서 표시하기
import { useEffect, useState } from "react";
import { useDispatch, useSelector } from "react-redux";
import { Link } from "react-router-dom";
import { loadFetchedData } from "@redux/slices/fetchedDataSlice";
import {
ArrowIcon,
NoExpenseDiv,
Select,
SelectWrapDiv,
StrDateItemWrapDiv,
StrDiv,
StrItemWrapDiv,
} from "./ExpenseListByMonth.styled";
function ExpenseListByMonth({ filterMonth }) {
const dispatch = useDispatch();
const fetchedData = useSelector((state) => state.fetchedData);
const [filteredData, setFilteredData] = useState([]);
const [sortOption, setSortOption] = useState("amount");
useEffect(() => {
dispatch(loadFetchedData());
}, [dispatch]);
useEffect(() => {
const filtered = fetchedData.filter((item) => {
const month = new Date(item.date).getMonth();
return month === filterMonth;
});
switch (sortOption) {
case "amount":
filtered.sort((a, b) => b.amount - a.amount); // 가격 순
break;
case "category":
filtered.sort((a, b) => a.item.localeCompare(b.item)); // 항목 순
break;
case "date":
filtered.sort((a, b) => new Date(a.date) - new Date(b.date)); // 최신 순
break;
default:
break;
}
setFilteredData(filtered);
}, [fetchedData, filterMonth, sortOption]);
const formattedAmount = (amount) =>
new Intl.NumberFormat("ko-KR").format(amount);
return (
<StrDiv>
<SelectWrapDiv>
<Select
value={sortOption}
onChange={(e) => setSortOption(e.target.value)}
>
<option value="amount">가격 순</option>
<option value="category">항목 순</option>
<option value="date">최신 순</option>
</Select>
<ArrowIcon>▼</ArrowIcon>
</SelectWrapDiv>
{filteredData.length === 0 ? (
<NoExpenseDiv>지출이 없습니다.</NoExpenseDiv>
) : (
filteredData.map((expense) => (
<Link key={expense.id} to={`/expenses/${expense.id}`}>
<StrItemWrapDiv>
<StrDateItemWrapDiv>
<span>{expense.date}</span>
<span>
{expense.item} - {expense.description}
</span>
</StrDateItemWrapDiv>
<span>{formattedAmount(expense.amount)}원</span>
</StrItemWrapDiv>
</Link>
))
)}
</StrDiv>
);
}
export default ExpenseListByMonth;
switch 문과 select 태그로 드롭다운 메뉴를 만들어 정렬 기능을 구현했습니다. useState 훅을 연결하여 상태가 변할 때마다 정렬이 바로 적용되도록 했습니다.
결론
- 전역 상태 관리를 통해 props drilling 문제를 해결하고, 모달 창의 확장성과 유지 보수성을 높였습니다.
- switch 문과 useState를 사용하여 다양한 정렬 옵션을 제공했습니다.
- styled-components를 활용하여 카테고리별 그래프의 색상을 조건부로 설정했습니다.
오늘은 모달 창을 구현하고, 모달 창의 상태를 Redux를 활용하여 중앙 저장소에서 관리했으며, styled-components를 활용하여 카테고리별로 조건부 스타일링을 적용했습니다. 또한 useState와 select 태그를 활용하여 정렬 기능을 구현했고 이번 학습을 통해 상태 관리의 중요성과 styled-components를 활용한 조건부 스타일링의 유용성을 체감할 수 있었습니다.
'TIL' 카테고리의 다른 글
React - Debounce 적용하기 (0) | 2024.05.24 |
---|---|
TIL - React와 Redux를 사용한 메모 앱 구현 (0) | 2024.05.24 |
Redux - 비동기 처리 createAsyncThunk (0) | 2024.05.23 |
TIL - 리액트로 가계부 만들기 - 2 (0) | 2024.05.22 |
TIL - 리액트로 가계부 만들기 - 1 (0) | 2024.05.21 |