[우아한테크코스 4기] 끝나지 않은 프리코스 (리팩토링)

rat8397·2021년 12월 16일
1

배달의민족

목록 보기
4/6
post-thumbnail

1,2 주차는 리팩토링 과정을 따로 포스팅 하지 않았으나, 3주차에는 기억해야하고싶은 정보도 많고, 개선해보고 싶은 부분을 공유하고 싶기도 해서 포스팅을 진행합니다

컨트롤러의 역할을 생각해보자

가장 아쉬웠던 부분이 이 컨트롤러의 역할을 제대로 이해하지 못해, 함수가 '하나의 일'만 하지 못하도록 했다는 점이었다. 컨트롤러의 역할을 생각해보자.

컨트롤러는

  • 모델과 뷰를 업데이트할 데이터를 만들어낼거고,

  • 모델을 업데이트할거고,

  • 업데이트된 모델로 뷰를 그리도록 할거다.

이 세개의 기능은 이벤트 핸들러에서 수행하도록 구현한다면, 컨트롤러가 갖는 메소드는 크게 네가지가 된다.

다음은 Product Add 하는 기능을 구현한 컨트롤러의 메소드들이다.

  1. Event Handler
  /** 상품을 만들어 모델을 업데이트하고 뷰를 업데이트한다. */
  onSubmitProductAddForm(e) {
    e.preventDefault();
    try {
      // 1. 데이터를 만든다.
      const newProduct = this.makeNewProduct();
      // 2. 모델을 업데이트한다.
      this.mutateModelByNewProduct(newProduct);
      // 3. 뷰를 업데이트한다.
      this.renderViewByNewProductList();
    } catch (error) {
      alert(error);
    }
  }
  1. 데이터를 만든다.

예외처리가 없어 간단한 모습이다. 다음은 새로운 상품 데이터를 만들어내는 컨트롤러의 메소드이다.

  makeNewProduct() {
    const { name, price, quantity } = this.$inputsModel.getProductInformationInputs();

    return {
      id: generateRandomProductId(),
      name,
      price: Number(price),
      quantity: Number(quantity),
    };
  }
  1. 새로운 데이터를 통해 모델을 업데이트한다.
  mutateModelByNewProduct(newProduct) {
    this.$productModel.setNewProductAtLast(newProduct);
  }
  1. 새로운 모델로 뷰를 그린다.
  renderViewByNewProductList() {
    const productList = this.$productModel.getProductList();
    this.$view.renderWithNewProductList(productList);
  }

이벤트 핸들러가 메모리에 쌓인다 ?

현재 코드는 탭이 변경될 때 마다 메인 템플릿을 innerHTML로 삽입하여 돔을 만들어낸다. 이 경우 이전 화면이 날라가기 때문에, 달아두었던 이벤트 핸들러도 제 기능을 하지 못하게 된다.

이 때 걱정되는 것은 이벤트 핸들러가 메모리에 쌓이지 않을까 ? 이다.

내가 뷰를 위와 같은 방식으로 계속 사용해보고자 하기에, 메모리 누수에 대한 걱정을 해결할 수 있는 좋은 방법은 unmounthandler들을 remove 해버리는 것이다.

unmount 되는 것의 기준은 tab의 정보가 변경되었을 때 이므로 이를 핸들링해보자.

controller가 변경되면, 이전 controller에서 할당된 핸들러를 지워버리자

다음은 master controller 컴포넌트에서 main-section을 마운트 시키는 함수이다.

mountMainSection() {
    const tab = this.$model.getGlobalModel().getTab();
  // 이전 컨트롤러의 핸들러들을 지워버린다.
    this.$controller.removeEventHandler();
  
    if (tab === TAB.productAddMenu) {
      this.renderProductAddView();
      this.bindProductAddController();
    }
    if (tab === TAB.vendingMachineManageMenu) {
      this.renderVendingMachineView();
      this.bindVendingMachineManageController();
    }
    if (tab === TAB.productPurchaseMenu) {
      this.renderProductPurchaseView();
      this.bindProductPurchaseController();
    }
  }
// subController 마다 있는 removeEventHandler
  removeEventHandler() {
    $(DOM.PRODUCT_ADD_FORM).removeEventListener('input', this.onInputProductAddForm);
    $(DOM.PRODUCT_ADD_FORM).removeEventListener('submit', this.onSubmitProductAddForm);
  }

View가 기본적으로 갖는 메소드와 멤버

리팩토링 한 지점
기존 메소드 이름을 더 명확히 혹은 조금 간소화

리팩토링이 더 필요한 지점
메소드, 생성자에서 전달받는 인자를 객체 형태로 받는것이 좋아보인다.

 mount(productInformationInputs, productList) {
    this.$app.innerHTML = this.generateTemplate(
      productInformationInputs,
      productList
    );
  }

위 경우 혹시 인자의 순서가 바뀌거나 해버리면, 곤란해진다.
member

  • this.$app : 해당 뷰가 템플릿을 만들어 붙일 가장 최상위 컴포넌트이다. 뷰의 마스터 컴포넌트는 #app이며, 서브 컴포넌트 들은 #main-section 이다. 모두 $app 멤버가 기억하는 DOM Element에 아래에 렌더링을 진행한다.

method

  • mount() : 뷰가 탭에 의해 마운트되면 실행되는 메소드이다. 아래 탭을 클릭하여 mainSection에 렌더링 될 뷰를 조정할 수 있다.
  mount(productInformationInputs, productList) {
    this.$app.innerHTML = this.generateTemplate(
      productInformationInputs,
      productList
    );
  }
  • generateTemplate() : 마운트될 때 템플릿을 만들어내는 함수이다.
 generateTemplate(productInformationInputs, productList) {
    return `
      ${this.createProductAddFormSectionTemplate(productInformationInputs)}
      ${this.createProductListSectionTemplate(productList)}
      `;
  }

Coin의 모듈화

기존의 Coin은 {500 : 0, 100 : 0, 50 : 0, 10 : 0}의 객체 형태를 가지고 계산을 도와주는 라이브러리 형태였다. 자신만의 데이터 멤버를 가지고, 메소드로 이를 관리하는 형태의 모듈이 아니었다. 유틸 라이브러리 느낌 ?

Coin 모듈을 다음과 같이 coinsData라는 객체 데이터 멤버를 가지고, 메소드로 이를 관리하도록 만들어 보았다.

  1. 인자를 받아 Coin 객체를 만들 수 있다.

class Coin {
  constructor(coinsData) {
    // 인자로 전달되는 값은 5000 형태의 넘버(지폐) 일수도 있고, 동전 객체의 형태일 수도 있다. {500:0,100:0...}
    this.initializeCoinsData(coinsData);
  }
  
}
  1. 키 값은 500, 100, 50, 10으로 고정되어 있으므로, 이를 강제하고 숫자값인 경우와 객체 값인 경우를 분리하여 초기화를 진행한다.
// 생성자가 호출되면, 그 다음 이 함수가 실행되어 $coinsData를 만들어낸다.
initializeCoinsData(coinsData) {
    if (typeof coinsData === 'number') {
      this.$coinsData = Coin.generateRandomCoinsData(coinsData);
      return;
    }
    this.$coinsData = coinsData;
  }
  • 지폐 형태로 받는 다면, 랜덤 동전들로 구성된 동전 데이터를 만든다.

  • 동전 형태라면 정상적인 경우이므로 멤버 변수에 저장한다.

  1. 각 메소드들로 이 coinsData를 관리한다.
  • getChargeAmount : 총 합을 구할 수 있다.

  • getCoinsData : 객체 형태의 동전 데이터만 받아온다.

  • addCoins : Coin 객체를 받아 동전 데이터 들을 더해, 객체의 동전 데이터 값을 변경한다.

...

이 전보다 훨씬 모듈이 되었으며, 캡슐화를 진행하여 Coin 객체의 값을 멤버로 갖는 VendingMachine도 직접 접근하지 않도록 구현하였다.

localStorage 저장을 편하게 하기 위한 데이터 모델의 변경

이전에는 로컬스토리지에 저장하는 코드가 모든 setter 메소드에 존재하였다.

기본적인 모델 클래스를 만들어 상속관계를 활용한다. 부모 클래스에서만 직접 데이터를 반환, 변경 및 로컬 스토리지에 set 하도록 한다.

import store from '../../lib/store.js';

class Model {
  constructor({ previousData, modelName }) {
    // 데이터는 기본적으로 객체형태이다.
    this.$data = {};
    // 모델의 이름을 저장하여 로컬스토리지의 키로 활용한다.
    this.$modelName = modelName;
    // $data를 초기화한다. 이 때 로컬스토리지에 이미 데이터가 있다면 이로 초기화해야하니 인자로 넣어준다.
    this.initializeData(previousData);
  }

  /* 데이터 구조가 복잡한 경우 오버라이드 가능하다 */
  initializeData(previousData) {
    
    // 이전 값이 있으면 이로, 아니면 기본값을 저장한다.
    this.$data = previousData ? previousData : this.generateDefaultValue();
    // store에 저장한다. 
    this.setDataInStore();
  }

  /* 모델별로 오버라이드 해야한다. 필수적인 메소드 */
  generateDefaultValue() {
    return {};
  }

  /* 데이터에 직접 접근하는 것은 이 메소드 뿐이다. 모두 이 메소드를 이용한다. */
  getDataByKey(key) {
    return this.$data[key];
  }
  /* 데이터에 직접 변경을 가하는 것은 이 메소드 뿐이다. 모두 이 메소드를 이용한다. */
  setDataByKey(key, value) {
    this.$data[key] = value;
    this.setDataInStore();
  }

  /* 로컬스토리지에 현재 데이터를 모델이름을 키로 저장한다. 데이터가 변경될 때마다 실행하도록 코딩한다. */
  setDataInStore() {
    store.setLocalStorage(this.$modelName, this.$data);
  }
}
export default Model;

옵저버 패턴이 필요하다

현재는 데이터의 변경을 가하고, 뷰를 업데이트 하는 절차적인 코드를 짜고 있다. 이 데이터가 변경이 가해지면 자연스럽게 뷰를 업데이트 하도록 구현하고 싶다. 옵저버 패턴에 대해 공부해보자

MVC 패턴의 장점을 느끼다

간단히 느껴본거니 비웃지말자

내가 설계한 MVC 구조에 의하면, 뷰는 컨트롤러에 의해 전달받는 값으로만 뷰를 그려낸다. 따라서 모델 부분을 내맘대로 재 설계, 리팩토링하여도, 뷰의 코드는 수정할 게 없다.

  1. 컨트롤러에서 뷰에 데이터를 뿌려주는 부분,

  2. 컨트롤러에서 모델에 변경을 가하는 부분

이 두 파트만 코드 수정을 해주면 되므로, 유지보수에 걸리는 시간이 줄어들었고, 어디를 변경하면 좋을지 한눈에 파악할 수 있어 가독성도 좋아지게 되었달까 ?

profile
Frontend Ninja

0개의 댓글