[React] 가계부 프로젝트 | My Pocket

승연·2022년 8월 6일
80

Toy Projects

목록 보기
1/1
post-thumbnail

My Pocket


리액트 공부를 시작한 지 이주 정도 된 초심자로써
리액트 기초 학습 & 헷갈리는 개념들(state, props 등)에 익숙해지기를 목표로 가계부 프로젝트를 진행했습니다.

styled-components, Redux 라이브러리 등은 이용하지 않았고
정말 정말 초심자 입장에서 리액트에 적응하기 위해 진행한 프로젝트였기 때문에 아래와 같이 목표를 잡고 진행했습니다.

  • 직접 컴포넌트 구조 생각해보기
  • 컴포넌트들을 결합해서 UI 구현하기
  • state를 이용해서 동적인 화면 만들기
  • 사용자 이벤트 발생 시 state 업데이트 하기
  • props를 이용해 부모 → 자식 컴포넌트로 데이터 전달하기
  • state 끌어올리기를 이용해 자식 → 부모 컴포넌트로 데이터 전달하기


🎨 디자인 시안


디자인 시안은 Figma로 만들었습니다.



🌲 컴포넌트 구조


컴포넌트 구조는 React 파일 구조 문서를 참고해서 기능에 따라 분류하는 방식으로 접근했습니다.

기능을 구현하는 과정에서 아주 많은 수정을 거쳐 최종적으로 완성한 구조는 아래와 같습니다. (아직 부족한 점이 많을 것 같습니다.)

1. 컴포넌트 트리


2. 디자인 시안을 기반으로 한 컴포넌트 구조


3. 컴포넌트 설명

  • Chart : 차트 컴포넌트
    • ChartBar : 개별 차트 바
  • DateLabel : 날짜를 yyyy-mm-dd 형태로 출력하는 컴포넌트
  • Filter : 연도 별 필터 컴포넌트
  • Item : 개별 아이템 컴포넌트
  • NewItemContainer : 내역 추가 관련 컴포넌트
    • NewItem : 내역 추가 영역
    • NewItemForm : 내역 추가 폼
  • PocketContainer : 자산 현황, 연간 내역, 월 별 지출 차트 관련 컴포넌트
    • PocketStatus : 자산 현황
    • PocketList : 연간 내역
    • PocketItems : 개별 내역
    • PocketChart : 월 별 지출 차트


✅ 구현 기능


1. 필터링 기능

연도 기준으로 자산 현황, 연간 내역, 월 별 지출 차트를 필터링하는 기능입니다.

연도가 변경되면 부모 컴포넌트로 변경된 연도를 전달하고,
부모 컴포넌트에서는 전달받은 연도를 기준으로 필터링을 한 후, 필터링 된 item 목록을 자식 컴포넌트들에게 props로 전달하도록 했습니다.


2. 자산 현황 관리

사용자가 선택한 연도의 자산, 수입, 지출 합계를 계산하고 출력하는 기능입니다.

사용자가 선택한 연도 기준으로 필터링 된 item 목록을 부모 컴포넌트로부터 전달받아 합계를 구하고 화면에 출력했습니다.

3. 연간 내역 관리

내역을 추가/삭제할 수 있고 연도 별로 내역을 출력하는 기능입니다.

추가

추가한 내역의 연도에 맞게 필터가 자동 설정되도록 구현하기 위해
item이 추가된 상황일 때 가장 최신 id를 가진 item을 가져와서 그 item의 연도에 맞게 필터를 설정하도록 했습니다. (ex: 2020년 내역을 추가한 경우 필터가 2020년으로 자동 설정)


삭제

삭제 버튼 클릭 이벤트가 발생한 item의 id를 부모 컴포넌트로 전달했고,
부모 컴포넌트에서는 해당 id를 가진 item을 삭제하도록 했습니다.

출력

연간 내역을 출력하는 기능입니다.

출력 시 최근 날짜 기준으로 상단부터 위치하도록,
만약 날짜가 같다면 최근에 추가한 순서대로 상단에 위치하도록 했습니다.

4. 월 별 지출 차트

연도 별로 1월부터 12월까지의 지출 금액을 차트로 보여주는 기능입니다.

지출 금액에 따라 차트 바 높이를 설정하기 위해
(각 달의 지출 금액 / 1월부터 12월까지의 지출 금액 중 최대 값) * 100을 계산했습니다.
그리고 계산한 값에 %를 붙여 css height 속성 값으로 설정했습니다.

차트 바에 마우스 포인터를 갖다대면 지출 금액을 확인할 수 있도록 구현했습니다.

추가적으로 웹 접근성을 고려하여 연간 지출 금액을 aria-label 속성 값으로도 확인할 수 있도록 하였습니다.


+) 로컬스토리지 저장

최초 목표 기능은 아니었지만 기능 구현을 끝내고 나니 욕심이 생겨 로컬스토리지 저장 기능도 추가 구현했습니다. 😃


🔫 트러블슈팅


왜 setState만 하면 무한루프에 빠지는거야?

- 오류 발생 상황

import React, { useState } from "react";
import { addComma } from "../../utils/numberUtils";
import "./PocketStatus.css";

const PocketStatus = (props) => {
    const [totalBalance, setTotalBalance] = useState(0);
    const [totalIncome, setTotalIncome] = useState(0);
    const [totalExpense, setTotalExpense] = useState(0);

    let total = {balance: 0, income: 0, expense: 0};

    if (props.filteredItems.length > 0) {
        // 자산, 수입, 지출 합계 계산
        props.filteredItems.forEach(item => {
            if (item.amountType === "income") {
                total.balance += +item.amount;
                total.income += +item.amount;
            } else {
                total.balance -= +item.amount;
                total.expense += +item.amount;
            }
        });

    setTotalBalance(total.balance);
    setTotalIncome(total.income);
    setTotalExpense(total.expense);

    return (
        // JSX 코드 생략
    );
};

export default PocketStatus;
};

export default AssetStatus;

자산, 수입, 지출 합계를 계산한 후 setState()를 사용해서 업데이트를 하려고 하면 Uncaught Error: Too many re-renders. React limits the number of renders to prevent an infinite loop. 에러가 발생했습니다.

console.log()로 찍어보면 멀쩡하게 잘 찍혔는데, state를 업데이트 하려고만 하면 무한루프에 빠져버렸습니다.

- 오류 이유 찾기

스택오버플로우의 Why does setState cause my React app go into infinite loop? 글을 보고 추측한 오류 발생 이유는 아래와 같습니다.

화면 렌더링 시 props.filteredItems 값 변경
→ setTotalBalance(), setTotalIncome(), setTotalExpense()를 이용하여 상태를 재설정
→ 상태를 재설정했기 때문에 재렌더링
→ 렌더링이 됐으니까 props.filteredItems 값 변경
→ setTotalBalance(), setTotalIncome(), setTotalExpense()을 이용하여 상태를 재설정
→ ... 이런 과정으로 무한 반복


- 오류 해결하기

스택오버플로우의 React UseState hook causing infinite loop 글을 참고하여 props 변경 사항이 있는 경우에만 상태를 업데이트 하는 방식으로 코드를 수정했습니다.

import React, { useEffect, useState } from "react";
import { addComma } from "../../utils/numberUtils";
import "./PocketStatus.css";

const PocketStatus = (props) => {
    const [totalBalance, setTotalBalance] = useState(0);
    const [totalIncome, setTotalIncome] = useState(0);
    const [totalExpense, setTotalExpense] = useState(0);
    const twoDigitYear = props.filterBaseYear.slice(-2);

    useEffect(() => {
        let total = {balance: 0, income: 0, expense: 0};

        if (props.filteredItems.length > 0) {
            // 자산, 수입, 지출 합계 계산
            props.filteredItems.forEach(item => {
                if (item.amountType === "income") {
                    total.balance += +item.amount;
                    total.income += +item.amount;
                } else {
                    total.balance -= +item.amount;
                    total.expense += +item.amount;
                }
            });
        }

        setTotalBalance(total.balance);
        setTotalIncome(total.income);
        setTotalExpense(total.expense);
    }, [props.filteredItems]);

    return (
        // JSX 코드 생략
    );
};

export default PocketStatus;

useEffect를 이용해서 props.filteredItems이 변할 때만 상태를 업데이트 하도록 하니 쓸데없는 재렌더링이 일어나지 않아 무한 루프 발생 문제가 해결되었습니다.



🌈 느낀 점


생각을 먼저 한 후 기능을 구현하자

이번 프로젝트를 진행하면서 컴포넌트 구조를 제대로 생각하지 않고 바로 기능 구현에 들어갔는데요, 이런 점 때문에 시간을 많이 허비했습니다..


첫 번째 삽질

(내역 입력, 자산 현황, 연간 내역, 월 별 지출 차트 이렇게 4개로만 분리하면 되겠넹~~~ 이라고 생각하며 구현을 시작한 내 모습)


실제로 구현을 하다보니 차트, 아이템, 날짜 출력 Label 같은 경우는 재사용이 가능한 컴포넌트라고 판단되어서 따로 분리하고..
연간 내역 컴포넌트 안에서도 내역 리스트로 더 분리할 수 있었고..

물론 구현을 하면서 분리할 수 있는 부분이 보이면 추가 분리하는 게 맞다고는 하지만,
대략적으로 큰 그림을 그리고 시작하지 않고 바로 구현에 들어가서 컴포넌트를 분리했다가 다시 합쳤다가 또 분리했다가 하는.. 굳이 하지 않아도 될 시간 낭비(삽질)를 아주 많이 했습니다.

다음 프로젝트를 진행할 때에는 이번처럼 바로 코드를 짜기보다는
대략적으로 큰 그림을 그린 후 기능 구현을 해야겠다는 교훈을 얻었습니다.


두 번째 삽질 (개인적으로 아주 끔찍했던 기억입니다..💩)

8월 4일 기능 구현을 마무리하고 친구에게 Repository를 공유했는데, 아래와 같은 피드백을 받았습니다.

React에서 이미지, font등을 저장하는 폴더 명을 주로 assets으로 사용한다는 걸 모른 채로 내역 추가 컴포넌트명을 NewAsset, 자산 관련 컴포넌트명을 Asset이라고 지었었고.. (아래 이미지 참조)

이로 인해 asset이 들어가는 컴포넌트명, 함수명, 변수명을 전부 변경하느라 정말 힘들었습니다. (discard를 엄청 했습니다😂)

조금이라도 더 찾아보고 더 생각해봤다면 이런 일은 없었을텐데.. 이 날의 삽질을 바탕으로 앞으론 이러지 말자고 다짐하게 됐습니다.



👇 프로젝트 보러가기


profile
✈️ https://sypear.tistory.com/

10개의 댓글

comment-user-thumbnail
2022년 8월 12일

잘 만드셨네요...

1개의 답글
comment-user-thumbnail
2022년 8월 14일

멋있네요 ㅎㅎ 잘봤습니다.

1개의 답글
comment-user-thumbnail
2022년 8월 14일

혹시, Figma는 어떤 방식으로 프로젝트에 활용하셨나요?
한 번도 활용해 본 적이 없어서 궁금하네요!

1개의 답글
comment-user-thumbnail
2023년 6월 5일

안녕하세요! 너무 멋진 프로젝트네요! 저도 향후에 사이드프로젝트 데모페이지를 만들어보고싶은데 어떻게 구현하셨나요? 배포하신건가요? 코린이라 잘 몰라서 여쭤봅니다!ㅜㅜ 좋은하루되세요!

1개의 답글