본문 바로가기
웹 프로젝트/👨‍👨‍👧‍👧소셜 가계부

[React/Node.js/Express/MongoDB] 소셜 가계부 프로젝트 구현 일지: 프론트엔드 초안 작성하기(라우터 설정, 가계부 월별 이동하기 구현)

by 청량리 물냉면 2024. 1. 16.
반응형

기초공사

새로 기획한 프로젝트 내용에 필요하지 않은 코드를 모두 지우고 폴더 구조를 수정하고, 라우트도 새로 작성했다.

 

--- SPA Pages

  • `/`: 대시보드
  • `/authenticate`: 회원가입, 로그인 폼
  • `/calendar`: 캘린더
  • `/transactions`: 유저 입출금 기록
  • `/challenge`: 챌린지
  • `/community`: 커뮤니티
  • `/community/new`: 게시글을 추가하는 페이지로 연결
  • `/community/:cid`: 게시글을 조회 및 수정하는 페이지로 연결
  • `/profile`: 환경설정 및 개인 정보 페이지
import React, { useState } from "react";
import { BrowserRouter as Router, Routes, Route } from "react-router-dom";
...

function AppRouter() {
  const [userInfo, setuserInfo] = useState({
    authenticated: false,
    currentUser: "임시",
    loading: false,
  });

  if (userInfo.loading) {
    return <LoadingIndicator />;
  }

  return (
    <div>
      <Router>
        <Sidebar userInfo={userInfo} />
        <Routes>
          <Route exact path="/" element={<Dashboard userInfo={userInfo} />} />
          <Route path="/authenticate" element={<Auth userInfo={userInfo} />} />
          <Route path="/calendar" element={<Calendar />} />
          <Route path="/transactions" element={<Transactions />} />
          <Route path="/challenge" element={<Challenge userInfo={userInfo.currentUser} />} />
          <Route path="/community" element={<Community userInfo={userInfo.currentUser} />} />
          <Route path="/community/new" element={<Community userInfo={userInfo.currentUser} />} />
          <Route path="/community/:cid" element={<Community userInfo={userInfo.currentUser} />} />
          <Route path="/profile" element={<Profile userInfo={userInfo.currentUser} />} />
        </Routes>
      </Router>
    </div>
  );
}

export default AppRouter;

 

아직 수정해야 할 부분이 존재하지만, 만들어 가며 수정할 예정이다.

 

 

프론트엔드 초안 작성 시작: 가계부

가계부 앱이기 때문에 가장 우선해서 작업해야 할 부분이 바로 가계부 컴포넌트이다.

현재 구성은 이렇다.

가장 상단에 한 달 예산을 정할 수 있는 예산 컴포넌트가 있고, 아래로 한 달간의 수입과 지출 내역을 보여주는 거래내역 분석 컴포넌트가 존재한다.

현재 분석 컴포넌트는 하드코딩되어 있다. 입출금 내역을 통해 총수입 / 지출 금액을 계산하는 로직을 작성하는 것은 어렵지 않겠지만, 당시에도 계산 로직까지 프론트에서 구현하게 되면 보안 문제뿐만 아니라 프론트 코드가 무거워질 우려가 있다고 생각하여 작업하지 않았었다. 이번에도 계산 로직은 추후 백엔드 코드에 작성할 예정이다.

 

두 컴포넌트 하단에는 입출금 내역을 보여주는 컴포넌트가 존재하며, 당시에는 월별로 내역을 보여주는 기능을 구현할 수 없었기 때문에 모든 거래일의 내역을 하나의 페이지에서 보여주고 있는 모습이다.

내역 수정과 조회에 필요한 내역은 모두 모달을 이용하여 하도록 구현했고 모달 내부는 UI적 요소와 필요없는 필드 제거 정도만 진행해도 될 것 같다.

모달을 이용한 내역 수정 및 조회 기능

 

위에서 소개한 가계부 구성을 기반으로, 가계부 부분에서는 아래 기능을 추가적으로 구현한다.

  • 가계부 내용을 월별로 보여주는 기능을 추가
  • 수입 지출 내역에서 수입 / 지출 내용만 따로 모아볼 수 있도록 하는 기능 (select box)
  • 내역을 저장하는 테이블의 UI 일부 수정

그 외에도 추가할 부분이나 수정할 부분이 생기면 해당 부분 구현해 볼 예정이다.

 

 

가계부 월별 이동 구현

1. 월별 이동 헤더 구현

Transactions.jsx

import React, { useEffect, useState } from "react";
...

export default function Transactions() {
  const [curDate, setCurDate] = useState(new Date());
  const [data, setData] = useState([]);

  const dateText = `${curDate.getFullYear()}년 ${curDate.getMonth() + 1}월`;

  const increaseMonth = () => {
    setCurDate(new Date(curDate.getFullYear(), curDate.getMonth() + 1, curDate.getDate()));
  };

  const decreaseMonth = () => {
    setCurDate(new Date(curDate.getFullYear(), curDate.getMonth() - 1, curDate.getDate()));
  };

  useEffect(() => {
    if (transactionList.length >= 1) {
      const firstDay = new Date(curDate.getFullYear(), curDate.getMonth(), 1).getTime();
      const lastDay = new Date(curDate.getFullYear(), curDate.getMonth() + 1, 0, 23, 59, 59).getTime();
      setData(transactionList.filter((item) => firstDay <= item.date && item.date <= lastDay));
    }
  }, [transactionList, curDate]);

  return (
    <Section>
      <div className="container">
        <Header
          text={dateText}
          leftChild={<Button text="◀" onClick={decreaseMonth} color="grey" />}
          rightChild={<Button text="▶" onClick={increaseMonth} color="grey" />}
        />
        <div className="transaction">
          <div className="analytics">
            <AccountBookAnalytics />
          </div>
          <div className="list">
            <TransactionList data={data} />
          </div>
        </div>
      </div>
    </Section>
  );
}

일기 프로젝트를 진행할 때도 동일한 로직으로 코드를 작성했다.

코드를 하나씩 뜯어보면 다음과 같다.

  const [curDate, setCurDate] = useState(new Date());
  const [data, setData] = useState([]);
  • `curDate`에 `new Date()`를 이용해서 현재 시간 객체를 저장하였다. 추후 이렇게 저장된 시간 객체를 이용해 이번 한 달간의 일기 데이터만을 filter로 추려 화면에 렌더링 하도록 코드를 작성한다.
  • `data`는 각 달마다 추려진 가계부 데이터를 저장할 state이다.
const firstDay = new Date(curDate.getFullYear(), curDate.getMonth(), 1).getTime();

이번 달의 첫째날을 밀리초 단위로 구하는 코드이다.

만약 오늘이 2023년 3월 23일이었다면, `firstDay`에는 2023년 3월 1일이 밀리초 단위의 타임스탬프 형식으로 저장된다. `getTime()`이 주어진 일시와 1970년 1월 1일 00시 00분 00초 사이의 간격(밀리초 단위)인 타임스탬프를 반환하는 함수이기 때문이다.

const lastDay = new Date(curDate.getFullYear(), curDate.getMonth() + 1, 0, 23, 59, 59).getTime();

`lastDay`에는 이번 달 마지막 날의 타임스탬프가 저장된다.

`curDate.getMonth() + 1`은 현재 월에 1을 더한 다음 달을 의미하며 `0`은 0일로, 현재 월의 마지막 날을 얻어온다는 의미이다.

해당 내용은 Date 객체의 자동고침 기능을 이용한 것으로 내가 참고한 Date() 문서에서는 아래와 같이 설명하고 있다.

let date = new Date(2016, 0, 2); // 2016년 1월 2일

date.setDate(1); // 1일로 변경합니다.
alert( date ); // 01 Jan 2016

date.setDate(0); // 일의 최솟값은 1이므로 0을 입력하면 전 달의 마지막 날을 설정한 것과 같은 효과를 봅니다.
alert( date ); // 31 Dec 2015

 

따라서 위와 같이 코드를 작성하면 이번 달의 마지막 날을 구할 수 있으며, 시간을 23시 59분 59초로 설정하여 그 날의 끝까지 포함하도록 하였다. 즉 위의 코드는 해당 월의 마지막 순간까지의 데이터를 포함하기 위한 코드이다.

예를 들어, 현재 월이 31일까지 있다면 new Date(curDate.getFullYear(), curDate.getMonth() + 1, 0, 23, 59, 59)는 그 달의 31일 23시 59분 59초를 반환한다.

 

setData(transactionList.filter((item) => firstDay <= item.date && item.date <= lastDay));

data에 이번 달 안의 일기를 추려 저장한다.

 

  useEffect(() => {
    ...
  }, [transactionList, curDate]);

입출금 내역이 바뀌거나(수정, 삭제 발생) 오늘 날짜가 바뀐 경우 useEffect 내부 코드를 실행하도록 하였다.

이렇게 만들어진 `data`는 `TransactionList` 컴포넌트로 전달되어 화면에 렌더링된다.

헤더 양옆의 버튼을 누르면 조회하고자 하는 month의 가계부 기록을 확인할 수 있다.

스타일링까지 완료한 뒤의 헤더

 

 

2. TransactionList 컴포넌트에서 data 전달받아 렌더링

import React, { useState, useRef, useEffect } from "react";
...

export default function TransactionList({ data }) {
  const [transactionData, setTransactionData] = useState(data);
  ...

  return (
    <Section>
      {console.log(transactionData)}
      <div className="title">
        <h2>입출금 내역</h2>
        ...
      </div>
      <div className="history">
        <table class="table">
          <tbody>
            {transactionData.map((item) => (
              <tr key={item.id}>
                <td>{`${new Date(item.date).getDate()}일 (${day[new Date(item.date).getDay()]})`}</td>
                <td>{item.type}</td>
                <td>{item.category}</td>
                <td>{item.content}</td>
                <td>{item.amount.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",")}</td>
                <td>
                  <FaPen onClick={() => handleEdit(item.id)} />
                </td>
                <td>
                  <FaTrashAlt onClick={() => handleRemove(item.id)} />
                </td>
              </tr>
            ))}
          </tbody>
        </table>
        ...
      </div>
    </Section>
  );
}

에러 발생

Uncaught Error: Objects are not valid as a React child (found: object with keys {id, date, category, description, amount, type, memo}). If you meant to render a collection of children, use an array instead.

 

오류를 구글링 해보니 Transaction.jsx(부모 컴포넌트)에서 TransactionList.jsx(자식 컴포넌트)로 data를 넘겨줄 때 오류가 발생하여 `TransactionList.jsx`에서 정상적인 데이터를 받아오지 못하는 것으로 확인이 되었다. 실제 `{console.log(data)}`, ` {console.log(TransactionData)}`를 각 컴포넌트에 찍어본 결과, `TransactionData`에는 빈 배열이 들어와 있는 것을 확인했다.

이유를 찾아보다가, 부모 컴포넌트의 `data` 값이 유동적으로 변하는 값이라는 사실을 간과했다는 것을 깨달았다. 

export default function TransactionList({ data }) {
  const [transactionData, setTransactionData] = useState(data);

이렇게 `data`를 초기값으로 저장해 둔 코드가 문제가 되었다.

`data`는 유동적으로 값이 변하는 데이터이기 때문에, 저렇게 초기화해두면 부모 컴포넌트에서 `data` 값이 바뀌는 것을 추적할 수가 없다.

따라서 부모 컴포넌트의 `data`가 바뀔 때마다 `transactionData`도 최신 값으로 변할 수 있도록 useEffect를 사용하여 에러를 해결했다.

 

  useEffect(() => {
    setTransactionData(data);
  }, [data]);

이렇게 하면 부모 컴포넌트에서 `data`가 변경될 때마다 useEffect가 실행되어 `transactionData`를 항상 최신 값으로 업데이트할 수 있게 된다.

 

 

TransactionList.jsx

import React, { useState, useRef, useEffect } from "react";
...

const day = ["일", "월", "화", "수", "목", "금", "토"];

export default function TransactionList({ data }) {
  const [transactionData, setTransactionData] = useState([]);
  ...

  useEffect(() => {
    setTransactionData(data);
  }, [data]);

  ...

  return (
    <Section>
      {console.log(transactionData)}
      <div className="title">
        <h2>입출금 내역</h2>
        ...
      </div>
      <div className="history">
        <table class="table">
          <tbody>
            {transactionData.map((item) => (
              <tr key={item.id}>
                <td>{`${new Date(item.date).getDate()}일 (${day[new Date(item.date).getDay()]})`}</td>
                <td>{item.type}</td>
                <td>{item.category}</td>
                <td>{item.content}</td>
                <td>{item.amount.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",")}</td>
                <td>
                  <FaPen onClick={() => handleEdit(item.id)} />
                </td>
                <td>
                  <FaTrashAlt onClick={() => handleRemove(item.id)} />
                </td>
              </tr>
            ))}
          </tbody>
        </table>
        ...
      </div>
    </Section>
  );
}

저장한 더미 데이터가 잘 뜨는 걸 볼 수 있다.

TimeStamp 부분은 따로 처리를 해주어야 한다. 

 

3. new Date() 객체의 getDate(), getDay

TransactionList.jsx

const day = ["일", "월", "화", "수", "목", "금", "토"];

...

      <div className="history">
        <table class="table">
          <tbody>
            {transactionData.map((item) => (
              <tr key={item.id}>
                <td>{`${new Date(item.date).getDate()}일 (${day[new Date(item.date).getDay()]})`}</td>
                <td>{item.type}</td>
                <td>{item.category}</td>
                <td>{item.content}</td>
                <td>{item.amount.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",")}</td>
                <td>
                  <FaPen onClick={() => handleEdit(item.id)} />
                </td>
                <td>
                  <FaTrashAlt onClick={() => handleRemove(item.id)} />
                </td>
              </tr>
            ))}
          </tbody>
        </table>
 <td>{`${new Date(item.date).getDate()}일 (${day[new Date(item.date).getDay()]})`}</td>

타임스탬프 값을 `Date()`를 이용해 Date 객체로 만든 뒤 날짜와 요일을 가져오도록 하였다.

1월 가계부 데이터
3월 가계부 데이터

 


참고

 

Date()

 

Date 객체와 날짜

 

ko.javascript.info

 

반응형