Yestoday-3
๐จ ํ๋ก์ ํธ ์ผ์ง
์ ๋ณด ์ ๋ฌ๋ณด๋จ ํ๋ก์ ํธ๋ฅผ ์งํํ๋ฉฐ ๊ฒช์ ์ ๋ค, ๋๋ ์ ๋ค์ ๊ธฐ๋กํ ์ผ์ง
Project
: Yestoday(account book)
๊ธฐํ๋ถํฐ ์์ํด๋ณด๋ ํ๋ก์ ํธ!!! ์ด๋ฒ ํ๋ก์ ํธ๋ ๊ฐ๊ณ๋ถ ์น์ฑ์ ๋ง๋ค๊ธฐ๋ก ๊ฒฐ์ ํ๋ค.
๋ค๋ง, ์ด๋ฏธ ์ํ ์ดํ์ ํตํด ์ด๋์ ๋ ๊ฐ๊ณ๋ถ ์ญํ ์ ํ๋ ์์ธํ ๊ธฐ๋ฅ๋ค์ด ์ ๊ณต๋๊ธฐ์ ์๋ก์ด ๊ธฐํ์ ์๊ฐํด๋ณด์๋ค.
๊ฐ๋ตํ ์ปจ์
์ ๋ค์๊ณผ ๊ฐ๋ค.
- ์ ์ ๋ ์ค๋
๋์ ํ ์๋น ๊ธ์ก
์ ์ค์ ํ๋ค. ์๋น ์ ํด๋น ๊ธ์ก๊ณผ ๋ด์ฉ์ ์ ๋ ฅํ๋ค. - ๋ฉ์ธ ํ๋ฉด์๋
์ด์
์์ค๋
์ ๋ํ ์ ๋ณด๊ฐ ๋์จ๋ค. ์ด์
๋ฅผ ํด๋ฆญํ๋ฉด ์ต๊ทผ 1๋ ๊ฐ์ ๋ฐ์ดํฐ๋ฅผ ์บ๋ฆฐ๋ ๋ทฐ๋ก ๋ณผ ์ ์๋ค.
์ฒ์ ํผ๊ทธ๋ง๋ก ๊ฐ๋จํ๊ฒ ๋์์ธ์ ๋ง๋ค์ด๋ณด์๋ค.
๐ฆพ ํ๋ก์ ํธ ๋ชฉํ
- 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์ ๋ํด ์ ๋ฆฌํด๋ณด๋ คํ๋ค!