๐Ÿ”จ ํ”„๋กœ์ ํŠธ ์ผ์ง€

์ •๋ณด ์ „๋‹ฌ๋ณด๋‹จ ํ”„๋กœ์ ํŠธ๋ฅผ ์ง„ํ–‰ํ•˜๋ฉฐ ๊ฒช์€ ์ ๋“ค, ๋Š๋‚€ ์ ๋“ค์„ ๊ธฐ๋กํ•œ ์ผ์ง€

Project: Yestoday(account book)

๊ธฐํš๋ถ€ํ„ฐ ์‹œ์ž‘ํ•ด๋ณด๋Š” ํ”„๋กœ์ ํŠธ!!! ์ด๋ฒˆ ํ”„๋กœ์ ํŠธ๋Š” ๊ฐ€๊ณ„๋ถ€ ์›น์•ฑ์„ ๋งŒ๋“ค๊ธฐ๋กœ ๊ฒฐ์ •ํ–ˆ๋‹ค.
๋‹ค๋งŒ, ์ด๋ฏธ ์€ํ–‰ ์–ดํ”Œ์„ ํ†ตํ•ด ์–ด๋Š์ •๋„ ๊ฐ€๊ณ„๋ถ€ ์—ญํ• ์„ ํ•˜๋Š” ์ƒ์„ธํ•œ ๊ธฐ๋Šฅ๋“ค์ด ์ œ๊ณต๋˜๊ธฐ์— ์ƒˆ๋กœ์šด ๊ธฐํš์„ ์ƒ๊ฐํ•ด๋ณด์•˜๋‹ค.
๊ฐ„๋žตํ•œ ์ปจ์…‰์€ ๋‹ค์Œ๊ณผ ๊ฐ™๋‹ค.

  • ์œ ์ €๋Š” ์˜ค๋Š˜ ๋„์ „ํ•  ์†Œ๋น„ ๊ธˆ์•ก์„ ์„ค์ •ํ•œ๋‹ค. ์†Œ๋น„ ์‹œ ํ•ด๋‹น ๊ธˆ์•ก๊ณผ ๋‚ด์šฉ์„ ์ž…๋ ฅํ•œ๋‹ค.
  • ๋ฉ”์ธ ํ™”๋ฉด์—๋Š” ์–ด์ œ์™€ ์˜ค๋Š˜์— ๋Œ€ํ•œ ์ •๋ณด๊ฐ€ ๋‚˜์˜จ๋‹ค.
  • ์–ด์ œ๋ฅผ ํด๋ฆญํ•˜๋ฉด ์ตœ๊ทผ 1๋…„ ๊ฐ„์˜ ๋ฐ์ดํ„ฐ๋ฅผ ์บ˜๋ฆฐ๋” ๋ทฐ๋กœ ๋ณผ ์ˆ˜ ์žˆ๋‹ค.

์ฒ˜์Œ ํ”ผ๊ทธ๋งˆ๋กœ ๊ฐ„๋‹จํ•˜๊ฒŒ ๋””์ž์ธ์„ ๋งŒ๋“ค์–ด๋ณด์•˜๋‹ค.

yestoday

๐Ÿฆพ ํ”„๋กœ์ ํŠธ ๋ชฉํ‘œ

  • MVC ํŒจํ„ด ๋ฐ ์˜ต์ €๋ฒ„ ํŒจํ„ด
  • ๋ผ์šฐํ„ฐ ๊ตฌํ˜„
  • ๊ผญ๊ผญ๊ผญ ์™œ ์ด๋ ‡๊ฒŒ ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•ด์•ผํ•˜๊ณ  ์–ด๋–ป๊ฒŒ ํ”„๋กœ์ ํŠธ๋ฅผ ๊ตฌ์„ฑํ• ์ง€ ๋ฏธ๋ฆฌ ์ƒ๊ฐํ•˜๊ณ  ์ž‘์„ฑํ•˜๋„๋ก ํ•˜์ž
  • ๊ฐ€๋Šฅํ•˜๋ฉด ๋‹ค์–‘ํ•œ ๊ฐœ๋ฐœ ํ™˜๊ฒฝ์„ ์‹œ๋„ํ•ด๋ณด์ž
  • ์š•์‹ฌ ๋” ๋ถ€๋ ค์„œ Jest๋ฅผ ์ด์šฉํ•˜์—ฌ ํ…Œ์ŠคํŠธ ์ฝ”๋“œ๊นŒ์ง€ ์ž‘์„ฑํ•ด๋ณด๊ธฐ
  • ์š•์‹ฌ ๋ถ€๋ฆด ์ˆ˜ ์žˆ๋Š” ํ”„๋กœ์ ํŠธ๋ฅผ ๋งŒ๋“ค์–ด๋ณด์ž

๐Ÿค‘ ๋ฉ”์ธ ํŽ˜์ด์ง€ - Model, View

๊ฑฐ๋‘์ ˆ๋ฏธํ•˜๊ณ  ๊ฐ„๋‹จํ•˜๊ฒŒ ์ฝ”๋“œ๋กœ ๋ณด์ž!

main(home) - html
<div id="app-main">
  <header class="header">
    <div class="header__left">
      <p>์•ˆ๋…•ํ•˜์„ธ์š”,</p>
      <p>์ œ์ด๋“ ๋‹˜</p>
    </div>
    <div class="header__right">
      <p>๋„์ „ ๊ธˆ์•ก</p>
    </div>
  </header>
  <main class="main">
    <div class="main__header">
      <div>์˜ค๋Š˜</div>
    </div>
    <div class="main__body spend-list"></div>
  </main>
  <!-- footer๋Š” ์ถ”ํ›„ ๊ตฌํ˜„ ์˜ˆ์ •์ด๋ผ ํ•˜๋“œ ์ฝ”๋”ฉ -->
  <footer class="footer">
    <div>์–ด์ œ</div>
    <div class="yesterday-challenge-money">150,000 ์›</div>
    <div class="yesterday-total-spend-money">85,000 ์›</div>
  </footer>
</div>
  • scss๋Š” ์ƒ๋žต

๋ฉ”์ธ ํŽ˜์ด์ง€

๋จผ์ € html๊ณผ css๋ฅผ ํ†ตํ•ด ์ „์ฒด์ ์ธ ๊ตฌ์กฐ๋ฅผ ์žก๊ณ  model๊ณผ view๋ฅผ ์ด์šฉํ•˜์—ฌ ๋งŒ๋“ค์–ด์ค˜์•ผํ•  ์ปดํฌ๋„ŒํŠธ๋ฅผ ๋ถ„๋ฆฌํ•˜์˜€๋‹ค.(์œ„์˜ ์˜ˆ์‹œ ์ฝ”๋“œ์—์„œ ์ด๋ฏธ์ง€์™€ ๋‹ค๋ฅด๊ฒŒ ๋น ์ ธ ์žˆ๋Š” ๊ตฌ์กฐ๋“ค์ด ์ถ”ํ›„ model์˜ ์ƒํƒœ(๋ฐ์ดํ„ฐ)๋ฅผ ๋ฐ›์•„ view๋กœ ๊ตฌํ˜„ ์—์ •)

Model - challenge money
import Observable from '../interfaces/observable';

export default class ChallengeMoneyModel extends Observable {
  constructor() {
    super();
    this.money = 0;
  }
  getMoney() {
    return this.money;
  }
  setMoney(money) {
    this.money = money;
    this.notify(this.money);
  }
}

๋ญ”๊ฐ€ ์„ค๋ช…ํ•˜๊ธฐ ๋ฏผ๋งํ•  ์ •๋„๋กœ ๊ฐ„๋‹จํ•˜๋‹ค. ๋จผ์ € ์˜ต์ €๋ฒ„ ํŒจํ„ด์„ ์ ์šฉํ•˜์—ฌ model์ด ๊ตฌ๋…์ค‘์ธ view์—๊ฒŒ ์ƒํƒœ ๋ณ€๊ฒฝ์„ ์•Œ๋ ค์•ผํ•˜๋ฏ€๋กœ ๋ฏธ๋ฆฌ ๋งŒ๋“ค์–ด๋‘” observable ๋ชจ๋“ˆ์„ ์ƒ์†ํ•œ๋‹ค.
์ดˆ๊ธฐ ๊ธฐ๋ณธ ๊ธˆ์•ก์€ 0 ์›์ด์ด ๋˜๊ณ  ๊ฐ๊ฐ ๊ธˆ์•ก์„ ๊ฐ€์ ธ์˜ค๋Š” ๋ฉ”์„œ๋“œ getMoney()์™€ ๊ธˆ์•ก์„ ๋ณ€๊ฒฝํ•˜๋Š” ๋ฉ”์„œ๋“œ setMoney()๋ฅผ ์ž‘์„ฑํ•ด์ค€๋‹ค.

View - challenge money
export default class ChallengeMoneyView {
  constructor({ model }) {
    this.$appMain = document.querySelector('#app-main');
    this.$appChallengeInput = document.querySelector('#app-challenge-input');
    this.$target = document.createElement('div');
    this.$target.className = 'challenge-money';
    this.moneyModel = model;
    this.moneyModel.subscribe(this.render.bind(this)); //Model์— ๊ตฌ๋…
    this.render();
  }
  render() {
    const money = this.moneyModel.getMoney(); //Model์˜ ์ƒํƒœ๋ฅผ ๊ฐ€์ ธ์™€์„œ ๋ Œ๋”๋ง
    this.$target.innerHTML = `${money.toLocaleString()} ์›`;
    this.$target.addEventListener('click', this.hideShow.bind(this));
  }
  hideShow() {
    this.$appMain.style.display = 'none';
    this.$appChallengeInput.style.display = 'flex';
  }
}

๋ชจ๋ธ์˜ getMoney()๋ฅผ ํ†ตํ•ด ๋„์ „ ๊ธˆ์•ก์„ ๊ฐ€์ ธ์˜จ๋‹ค.
๋˜ํ•œ, ๋„์ „ ๊ธˆ์•ก์„ ํด๋ฆญ ์‹œ ๊ธˆ์•ก์„ ์ž…๋ ฅํ•˜๋Š” ํŽ˜์ด์ง€๋กœ ๊ฐˆ ์ˆ˜ ์žˆ๊ฒŒ hideShow() ๋ฉ”์„œ๋“œ๋ฅผ ํ†ตํ•ด ๊ฐ ํŽ˜์ด์ง€์˜ display๋ฅผ ๋ณ€๊ฒฝํ•ด์ค€๋‹ค.
์ด ๋ถ€๋ถ„์€ ์ถ”ํ›„ client routing์œผ๋กœ ํŽ˜์ด์ง€ ๋ณ„ path๋ฅผ ๋‹ค๋ฅด๊ฒŒ ๋ฆฌํŽ™ํ† ๋งํ•  ์˜ˆ์ •์ด๋‹ค.

Model - spend item
import Observable from '../interfaces/observable';

export default class SpendItemModel extends Observable {
  constructor() {
    super();
    this.items = [];
    this.id = 1;
  }
  getItems() {
    return this.items;
  }
  setItems({ name, price }) {
    this.items.push({ id: this.id, name: name, price: price });
    this.notify(this.items);
    this.id++;
  }
  removeItem({ id }) {
    this.items = this.items.filter((item) => item.id !== +id);
    this.notify(this.items);
  }
}

์ด ๋ถ€๋ถ„์ด ์žฌ๋ฏธ์žˆ์—ˆ๋‹ค. item์˜ ๊ฒฝ์šฐ ์œ„์˜ money์™€ ๋‹ค๋ฅด๊ฒŒ item์˜ name, price ๊ทธ๋ฆฌ๊ณ  ๊ฐ ์•„์ดํ…œ์˜ ๊ณ ์œ ํ•œ id๋ฅผ ๊ฐ์ฒด๋กœ ์ „๋‹ฌํ•˜๊ฒŒ๋” ์ฒ˜๋ฆฌํ•ด์ฃผ์—ˆ๋‹ค.(๊ทธ๋ž˜์•ผ ์ถ”ํ›„ view์—์„œ name๊ณผ price๋ฅผ ๊ฐ๊ฐ ๋ณด์—ฌ์ค„ ์ˆ˜ ์žˆ์œผ๋‹ˆ)
๋˜ํ•œ, id๋ฅผ ํ†ตํ•ด view๋กœ ๋งŒ๋“  ํ’ˆ๋ชฉ์„ ์‚ญ์ œํ•  ์ˆ˜ ์žˆ๋„๋ก removeItem() ๋ฉ”์„œ๋“œ๋ฅผ ์ž‘์„ฑํ•˜์˜€๋‹ค.

View - spend money
export default class ChallengeMoneyView {
  constructor({ model }) {
    this.model = model;
    this.$target = document.createElement('div');
    this.$target.className = 'spend-money';
    this.$appMain = document.querySelector('#app-main');
    this.$appItemInput = document.querySelector('#app-item-input');
    this.model.subscribe(this.render.bind(this));
    this.render();
  }
  render() {
    this.spendMoney = 0;
    const items = this.model.getItems();
    items.forEach((item) => (this.spendMoney += item.price));
    this.$target.innerHTML = `${this.spendMoney.toLocaleString()} ์›`;
    this.$target.addEventListener('click', this.hideShow.bind(this));
    this.$target.addEventListener('click', this.resetInput.bind(this));
  }
  hideShow() {
    this.$appMain.style.display = 'none';
    this.$appItemInput.style.display = 'flex';
  }
  resetInput() {
    const $itemInput = document.querySelectorAll('.main-item-input input');
    $itemInput.forEach((ele) => (ele.value = ''));
  }
}

์•ž์„œ challenge money view๋ฅผ ๊ตฌํ˜„ํ•˜๋Š” ๋ถ€๋ถ„๊ณผ ์ฐจ์ด๋ผ๋ฉด ์•„๋ž˜์™€ ๊ฐ™๋‹ค.

  • ๋ฐ›์•„์˜ค๋Š” ๋ฐ์ดํ„ฐ๊ฐ ๊ฐ์ฒด์— ๋Œ€ํ•œ ๋ฐฐ์—ด์ด๋ฏ€๋กœ ๋ฐฐ์—ด ๋‚ด ๊ฐ ๊ฐ์ฒด์— ๋Œ€ํ•œ view๋ฅผ ์œ„ํ•œ ๋กœ์ง์„ ์ž‘์„ฑํ•œ ์ 
  • ์•„์ดํ…œ์„ ์ถ”๊ฐ€ํ•  ๋•Œ, ์ž…๋ ฅํ•˜๋Š” ํŽ˜์ด์ง€์—์„œ ์ด๋ฆ„๊ณผ ๊ธˆ์•ก ์š”์†Œ์˜ ๊ฐ’์„ ์ดˆ๊ธฐํ™”ํ•ด์ฃผ๋Š” ๋ฉ”์„œ๋“œ resetInput()
View - spend item list
export default class SpendItemView {
  constructor({ model }) {
    this.model = model;
    this.model.subscribe(this.render.bind(this));
    this.$spendList = document.querySelector('.spend-list');
    this.$spendList.addEventListener('dblclick', (event) => {
      if (!event.target.classList.contains('spend-item')) return;
      this.model.removeItem({ id: event.target.classList[1].split('-')[2] });
    });
    this.render();
  }
  render() {
    this.$target = ``;
    const items = this.model.getItems();
    items.forEach(
      (item) =>
        (this.$target += `
        <div class="spend-item spend-item-${item.id}">
          <div class="item-name item-name-${item.id}">${item.name}
          </div>
          <div class="item-money item-money-${item.id}">${item.price.toLocaleString()} ์›
          </div>
        </div>`),
    );
    this.$spendList.innerHTML = this.$target;
  }
}

์—ฌ๊ธฐ๋„ ๊ตฌํ˜„ํ•˜๋Š”๋ฐ ๊ณ ๋ฏผ์ด ๋งŽ์•˜์ง€๋งŒ ์žฌ๋ฏธ์žˆ์—ˆ๋‹ค!
๋ฐ”๋กœ ์œ„์˜ spend money์™€ ๊ฐ™์ด, ์•„์ดํ…œ ๋ฐ์ดํ„ฐ๋ฅผ ์ด์šฉํ•˜๋ฏ€๋กœ SpendItem.model ๋ชจ๋“ˆ์„ ๊ตฌ๋…ํ•œ๋‹ค.
์ด ๋ทฐ์—์„œ๋Š” ๊ฐ๊ฐ์˜ ํ’ˆ๋ชฉ์ด ๋ณด์ด๊ณ  ๊ทธ ํ’ˆ๋ชฉ์„ ๋”๋ธ” ํด๋ฆญ ์‹œ ๋ชจ๋ธ์—์„œ ์‚ญ์ œํ•˜๋Š” ๋กœ์ง๋„ gi๊ตฌํ˜„ํ•˜์˜€๋‹ค.(์ด ๋ถ€๋ถ„์„ controller์—์„œ ๊ตฌํ˜„ํ•˜๋Š” ๊ฒŒ ๋” ๋‚˜์„ ๊ฑฐ ๊ฐ™๋‹ค๋Š” ์ƒ๊ฐ๋„ ๋“ ๋‹ค.)

๊ฐ’ ์ž…๋ ฅ ํ›„ ๋ฉ”์ธํŽ˜์ด์ง€

๐Ÿคข ์—ฌ๊ธฐ๊นŒ์ง€ ํšŒ๊ณ 

ํ›„์•„โ€ฆ ๋ฐ”๋‹๋ผ ์ž๋ฐ”์Šคํฌ๋ฆฝํŠธ๋กœ ๊ตฌํ˜„ํ•˜๋Š” ๊ฒŒ ์‰ฝ์ง€ ์•Š๋‹ค๊ณ  ์ƒ๊ฐ์€ ํ–ˆ์ง€๋งŒ, ์ด๋ฒˆ ํ”„๋กœ์ ํŠธ๋Š” ํŠนํžˆ๋‚˜ ๊ฐœ๋…์„ ์ฝ”๋“œ๋กœ ์˜ฎ๊ธฐ๋Š” ๊ฒŒ ์ž˜ ์™€๋‹ฟ์ง€ ์•Š์•„์„œ ๋ฉ˜ํƒˆ์ด ์ชผ๋ฉ” ํž˜๋“ค์—ˆ๋‹ค.
์œ„์˜ ์ฝ”๋“œ๋„ ํŒจํ„ด์„ ์ œ๋Œ€๋กœ ๋”ฐ๋ฅธ ๊ฒƒ๋„ ์•„๋‹ˆ๊ณ , ์˜์กด์„ฑ์„ ์ตœ์†Œํ™”ํ•œ ๊ฒƒ๋„ ์•„๋‹ˆ์ง€๋งŒ ์ฝ”๋“œ๋ฅผ ์“ฐ๊ณ  ๋ณด์ด๋Š” view๋ฅผ ํ† ๋Œ€๋กœ ์ˆ˜์ •ํ•˜๊ณ  ์ˆ˜์ •ํ•˜๋‹ค๋ณด๋‹ˆ ์ •๋ง ๋Œ€๋žต์ ์œผ๋กœ๋‚˜๋งˆ ๋ชจ๋ธ์—์„œ ๋ฐ์ดํ„ฐ๋ฅผ ์–ด๋–ป๊ฒŒ ๊ด€๋ฆฌํ•˜๊ณ  ๊ฐ ๊ตฌ๋… ํ•จ์ˆ˜(์ •ํ™•ํžˆ๋Š” view๋“ค์˜ render() ๋ฉ”์„œ๋“œ)์—๊ฒŒ ์–ด๋–ป๊ฒŒ ์ƒํƒœ ๋ณ€๊ฒฝ์„ ์•Œ๋ ค์ฃผ๋Š”์ง€ ๊ทธ ํ๋ฆ„์„ ์•Œ ๊ฒƒ ๊ฐ™๋‹ค.

๋‹ค์Œ์—” ์ด์ œ ๊ฐ๊ฐ์˜ ํ•ญ๋ชฉ์„ ์ž…๋ ฅํ•˜๋Š” model๊ณผ view์— ๋Œ€ํ•ด ์ •๋ฆฌํ•ด๋ณด๋ คํ•œ๋‹ค!