프론트엔드 개발자의 기록 공간

[우아한테크캠프] 가계부 프로젝트 본문

우아한테크캠프5기

[우아한테크캠프] 가계부 프로젝트

[리우] 2022. 9. 11. 14:27

다 끝나고서야 정리하는 3~4주차 3번째 프로젝트...

3번째 프로젝트는 2주간 "가계부 서비스" 프로젝트였다.

페이지는 크게 수입, 지출 리스트를 볼 수 있는 메인 페이지, 달력 차트로 내역을 볼 수 있는 페이지, 차트로 내역을 볼 수 있는 페이지를 개발하는 것이었다.

 

프로젝트 후기

FE 요구사항

  • 객체 활용 프로그래밍
    • Model 과 View 역할을 나누고 옵저버 패턴을 활용해서 적용한다.
  • history API 와 라우팅
    • 대 분류에 해당하는 메뉴에 대해서 history API 를 적용해서 page routing 을 시도한다.
  • Fetch API와 Promise 패턴
    • catch 를 활용한 에러처리를 한다.
    • 서버와 통신을 하는 동안 loading indicator 를 노출한다.
  • 차트 개발
    • Canvas, SVG 기반으로 구현한다.

 

BE 요구사항

  • Express를 이용해 프론트에 필요한 API를 개발한다.
  • AWS EC2를 이용해 배포를 진행한다.
  • VPC를 설정하고 서버와 DB를 분리한다.
  • 스크립트를 이용해서 자동 배포가 이루어지도록 구성해 본다.

 

환경 세팅

VanillaJS(클래스형), babel, webpack, node.js express, webpack-dev-middleware, aws 배포 환경으로 구성했다.

지난번 프로젝트 때 환경설정과 유사하게 진행했다.달라진 점은 프론트 웹팩 설정에서 개발 모드, 배포 모드 설정을 다르게 했다. 개발모드일 때만 소스트리를 추가하여 디버깅을 원활하게 진행했다. 그 외에는 2주차 프로젝트와 유사하게 구성했다.

 

개발 (레포주소)

좌) 메인페이지  우) 가계부 생성
좌) 가계부 수정 및 결제수단 CRUD  우) 달력 페이지 및 통계페이지

 

개발 이슈

전역 상태 관리

이번 프로젝트에서 처음으로 클래스형으로 VanillaJS를 구성했고, 디자인 패턴(옵저버)도 처음으로 구현해 봤다. 그러다 보니, 문법이나 활용법에 처음에 많이 어려웠고 미숙했다.

팀원과 회고를 하면서 문서화를 통해 템플릿 코드가 있으면 개발 시 수월할 것 같다는 의견을 나눴고 이를 문서화하여 참고하면서 개발을 진행했고, 해당 문제를 해결할 수 있었다.

 

옵저버 패턴

const store = {}

/**
 * 상태를 구독한다(리렌더링 함수를 옵저버로 등록.
 * @param {string} key 구독할 key
 * @param {function} observer 변화 감지 후 실행시킬 함수(리렌더링 함수)
 * @returns
 */
const subscribe = (key, observer) => store[key]._observers.add(observer)

/**
 * 해당 리렌더링 함수를 제거한다.
 * @param {string} key 구독할 key
 * @param {function} observer 리렌더링 함수
 */
const unsubscribe = (key, observer) => {
  store[key]._observers = [...store[key]._observers].filter((subscriber) => subscriber !== observer)
}

/**
 * 옵저버 함수를 실행한다.
 * @param {string} key 해당 key의 옵저버 함수를 실행시킨다.
 */
const notify = (key) => store[key]._observers.forEach((observer) => observer())

/**
 * store 객체에 전역 상태를 추가한다.
 * @param {{key, defaultValue}} key 전역 상태 key, defaultValue 전역 상태 밸류
 * @returns {string} 키를 반환함
 */
const initState = ({ key, defaultValue }) => {
  store[key] = {
    _state: defaultValue,
    _observers: new Set(),
  }
  return key
}

/**
 * 해당 key의 상태 값을 불러온다.
 * @param {string} key 전역 상태 key
 * @returns {any} store[key]._state 상태 value
 */
const getState = (key) => {
  return store[key]._state
}

/**
 * 해당 key의 상태를 수정하고, notify로 옵저버 함수(리렌더링 함수)를 실행시킨다.
 * @param {string} key 전역 상태 key
 */
const setState = (key) => (newState) => {
  store[key]._state = newState
  notify(key)
}

export { subscribe, unsubscribe, initState, getState, setState }

 

옵저버 패턴에 등록한 데이터를 구독하는 코드 예시

import { Component } from '../core/component.js'
import { getState, subscribe } from '../core/observer.js'
import { textState } from '../stores/textStore.js'

export default class Text extends Component {
  constructor(target) {
    super(target)

    subscribe(textState, this.render.bind(this))
  }

  template() {
    const text = getState(textState)
    return `

        <div>${text}</div>
        `
  }
}

 

옵저버 패턴에 등록한 데이터를 바꾸는 코드 예시

import { Component } from '../core/component.js'
import { setState } from '../core/observer.js'
import { textState } from '../stores/textStore.js'

export default class Input extends Component {
  constructor(target) {
    super(target)

    this.setText = setState(textState)
  }

  handleInputText(e) {
    const { value } = e.target

    this.setText(value)
  }

  setEvent() {
    this.$target.querySelector('#test').addEventListener('input', this.handleInputText.bind(this))
  }

  template() {
    return `
        <input id="test" type="text" />
        `
  }
}

 

 

웹 컴포넌트 활용

 

  • 웹 컴포넌트로 컴포넌트를 만들 경우에는 컴포넌트 자체가 엘리먼트(요소)이기 때문에, 다루기가 편리해짐
  • 코드를 유지 보수하고 관리할 때 매우 이롭다. 각 컴포넌트 별로 독립적으로 존재하기 때문에 컴포넌트 별로 작업이 필요한 경우 반복문을 돌면서 직접 이벤트가 발생한 DOM을 찾고, 해당 로직을 찾을 필요 없이 상위에서 props함수를 통해 상위로 해당 객체의 정보를 반환하여 상위에서 컨트롤하면 된다.
  • props 컴포넌트 자체를 넘겨줘서 사용할 수 있기 때문에 재사용성에 편리해짐

props로 컴포넌트 자체를 넘겨서 활용한 사례

    openModal(
      new UpdateTransactionModal({
        id,
        title,
        category,
        paymentId,
        payment,
        price,
        paymentDate,
      }),
    )
const openModal = (modalElement) => {
  const $modalWrapper = document.querySelector('#modal')
  $modalWrapper.appendChild(modalElement)
  disableBodyScroll()
}

const closeModal = (modalElement) => {
  modalElement.remove()
  enableBodyScroll()
}

 

네트워크 비용 개선

 

1. 컴포넌트 렌더링에 따른 지속적인 API 요청 문제

(상위) 결제내역바 컴포넌트 -> (하위) 결제 수단 리스트 컴포넌트 결제내역바에서 props 상태 변경에 따라 재렌더링 되면서 하위 컴포넌트도 렌더링 되는 과정에서 결제 수단 GET API를 지속적으로 요청하는 문제가 발생 (최소 5번 이상의 데이터 요청)

해결 방법 (상위에서 전역 데이터로 관리)

  1. 결제 수단 리스트 컴포넌트를 두 곳에서 사용한다.
  2. 사용하는 두 개의 컴포넌트라서 그 상위에서 데이터를 한번 요청 후, 옵저버에 등록
  3. 결제 수단 리스트 컴포넌트는 옵저버에 등록된 데이터를 받아와서 사용
  4. 결제 수단 Create, Delete 시 옵저버에서 상태를 변경해서 사용

 

2. 결재내역 데이터 변경 시 API 요청에 따른 깜빡임 문제

결제 내역 CRUD, 결제 수단 CRUD 시, API 요청을 보냅니다. API 요청을 보내고 변경 사항을 UI에 반영하기 위해서는 강제적으로 새로고침을 통해 네트워크 통신을 통해 새로 받아옵니다. 이 과정에서 새로고침에 따른 깜빡임 문제, 네트워크 통신 비용 문제가 있었습니다.

 

해결 방법 (낙관적 업데이트)

API 요청이 정상적으로 되었다면, 사용하고 있는 객체들의 정보에 바뀌어야 하는 부분에 임의로 데이터를 수정합니다. 그 후 렌더링을 통해 사용자의 UI에서는 바로 적용된 것처럼 보이게 됩니다.

 

3~4주차 전체 회고

2주간 진행된 만큼 많은 것을 배울 수 있었고 많은 것을 학습할 수 있었다. 또 실력 있는 동료를 만나 많은 자극이 되었다. 기술적으로 도움을 주지 못해 동료한테 많은 미안감이 들었다. 하지만 최종 회고에서 "편한 분위기를 이끌어줘서 일하기 편했고, 제시하는 의견들을 다 반영해 줘서 좋았다"라는 평가를 듣고 내가 이런 것에 강점이 있는 사람이구나 깨달을 수 있었다.

728x90
Comments