(Vanilla JS๋ก ์น ์ปดํฌ๋ํธ ๋ง๋ค๊ธฐ by ํฉ์ค์ผ๋) 1ํธ
๐พ ๊ธฐ์ ์ฑ ์คํฐ๋
23๋
1์๋ถํฐ ํ๋ ์ค์ธ ๊ต์ก์์, ๋ป์ด ๋ง๋ ๋๋ฃ๋ค๊ณผ ํจ๊ป ์งํํ๊ฒ ๋ ์คํฐ๋
์์ผ๋ก๋ ๊พธ์คํ ๊ธฐ์ ์์ ์ ์ฝ๊ณ ํจ๊ป ๋ฐ์ ํ๋ ์๊ฐ์ด ๋์์ผ๋ฉด ์ข๊ฒ ๋ค!
๋ค์ด๊ฐ๊ธฐ์ ์์
์ด ๋ด์ฉ์ ๊ฐ๋ฐ์ ํฉ์ค์ผ - Vanilla Javascript๋ก ์น ์ปดํฌ๋ํธ ๋ง๋ค๊ธฐ 1ํธ์ ๊ณต๋ถํ๋ฉฐ ์์ฑํ ๊ธ์ ๋๋ค. ๋๋ถ๋ถ์ ๋ด์ฉ์ ํฉ์ค์ผ๋์ ๋ธ๋ก๊ทธ๋ฅผ ์ฐธ๊ณ ํ์๊ณ ๋ช๊ฐ์ ๊ฐ๋ ๋ด์ฉ ์ ๋๋ง ์ถ๊ฐ ํน์ ๋ด์ฉ ์์ฝ์ด ๋์ด์์ต๋๋ค. ์์ธํ ์ฌํญ์ ํฉ์ค์ผ๋์ ๋ธ๋ก๊ทธ๋ฅผ ์ฐธ๊ณ ํด์ฃผ์ธ์!(์ ๋ง ๋๋ฌด ์ข์ ๊ธ์ด์์~!)
Vanilla Javascript๋ก ์น ์ปดํฌ๋ํธ ๋ง๋ค๊ธฐ - 1
1. ์ปดํฌ๋ํธ์ ์ํ๊ด๋ฆฌ
ํฌ๋ก์ค ๋ธ๋ผ์ฐ์ง
- ์น ์ ์ ์์ ๋ชจ๋ ๋ธ๋ผ์ฐ์ ์์ ๊นจ์ง์ง ์๊ณ ์๋ํ ๋๋ก ์ฌ๋ฐ๋ฅด๊ฒ ๋์ค๋ ์์ (ํธํ์ฑ ํด๊ฒฐ)
- ํ์ค ์น ๊ธฐ์ ์ ์ฑ์ฉํ์ฌ ๋ค๋ฅธ ๊ธฐ์ข ํน์ ํ๋ซํผ์ ๋ฐ๋ผ ๋ฌ๋ฆฌ ๊ตฌํ๋๋ ๊ธฐ์ ์ ๋น์ทํ๊ฒ ๋ง๋ฆ๊ณผ ๋์์ ์ด๋ ํ์ชฝ์ผ๋ก ์ต์ ํ ๋์ด ์น์ฐ์น์ง ์๋๋ก ๊ณตํต ์์๋ฅผ ์ฌ์ฉํ์ฌ ์นํ์ด์ง๋ฅผ ์ ์ํ๋ ๊ธฐ
- ๋จ์ํ๊ฒ ๋งํ์๋ฉด ์ปดํจํฐ, ์ค๋งํธํฐ ๋ฑ๋ฑ ์ด๋ค ๊ธฐ๊ธฐ๋ก ์ ์ํด๋ ์ฌ์ฉ์๊ฐ ๋์ผํ ๊ฒฝํ์ ํ ์ ์๊ฒ๋ ํด์ฃผ๋ ๊ฒ์ด๋ค.
GraphQL(Graph Query Language; gql)
- sql๊ณผ ๋ง์ฐฌ๊ฐ์ง๋ก ์ฟผ๋ฆฌ ์ธ์ด
- sql์
๋ฐ์ดํฐ๋ฒ ์ด์ค
์ ์ ์ฅ๋ ๋ฐ์ดํฐ๋ฅผ ํจ์จ์ ์ผ๋ก ๊ฐ์ ธ์ค๋ ๊ฒ์ด ๋ชฉ์ . ์ฃผ๋ก ๋ฐฑ์๋ ์์คํ ์์ ์์ฑ - gql์
์น ํด๋ผ์ด์ธํธ
๊ฐ ๋ฐ์ดํฐ๋ฅผ ์๋ฒ๋ก๋ถํฐ ํจ์จ์ ์ผ๋ก ๊ฐ์ ธ์ค๋ ๊ฒ์ด ์ฃผ๋ ๋ชฉ์ . ์ฃผ๋ก ํด๋ผ์ด์ธํธ ์์คํ ์์ ์์ฑ
ํด๋ผ์ด์ธํธ ๋ ๋๋ง์ ํ๋ฆ
๋ธ๋ผ์ฐ์ ์ JS๊ฐ ๋ฐ์ ํ๋ ๊ณผ์ ์์ ์์ ๋ธ๋ผ์ฐ์ (ํด๋ผ์ด์ธํธ)๋จ์์ ๋ ๋๋ง
์ ํ๊ณ , ์๋ฒ์์๋ REST API ๋๋ GraphQL ๊ฐ์ด ๋ธ๋ผ์ฐ์ ๋ ๋๋ง์ ํ์ํ ๋ฐ์ดํฐ๋ฅผ ์ ๊ณตํ๋ ํํ
๋ก ๋ณํํ์๋ค.
์ฆ, ์ง์ ์ ์ผ๋ก DOM์ ์กฐ์ํ๋ ํ์๊ฐ ๊ธ๊ฒฉํ๊ฒ ๊ฐ์ํ๋ค. ์ํ(state)
๋ฅผ ๊ธฐ์ค์ผ๋ก DOM
์ ๋ ๋๋งํ๋ ํํ๋ก ๋ฐ์ ํ๋ค.
๋ค๋ฅด๊ฒ ์๊ฐํ๋ฉด DOM์ด ๋ณํ๋ ๊ฒฝ์ฐ๊ฐ State์ ์ข
์๋์๊ณ ์ด ๋ง์ ์ฆ, State๊ฐ ๋ณํ์ง ์์ ๊ฒฝ์ฐ DOM์ด ๋ณํ๋ฉด ์๋๋ค๋ ์๋ฏธ์ด๋ค.
SSR๊ณผ CSR
SSR(Server-Side-Rendering)
- ์๋ฒ์์ HTML์ ๋ง๋ค์ด์ ํด๋ผ์ด์ธํธ์ ๋๊ฒจ์ค๋ค. ๋ง ๊ทธ๋๋ก ์๋ฒ์ชฝ์์ ๋ ๋๋ง์ ํ๋ค.
- ๊ทธ๋ ๊ธฐ ๋๋ฌธ์ ํด๋ผ์ด์ธํธ์์๋ ๋ฐ์ดํฐ๋ฅผ ๊น์ ๋จ๊ณ๊น์ง ๊ด๋ฆฌํ๊ณ ๋ค๋ฃฐ ํ์๊ฐ ์์๋ค.
CSR(Client-Side-Rendering)
- JS์ ๋ฐ์ ์ ๋ฐ๋ผ ํด๋ผ์ด์ธํธ ๋จ์์ ๋ชจ๋ ๋ ๋๋ง์ ์ฒ๋ฆฌํ๋ ค๋ ์๋๊ฐ ์๊ฒผ๋ค.(React, Vue, Angular ๋ฑ)
- ํด๋ผ์ด์ธํธ ๋จ์์ ๋ ๋๋ง์ ํ๊ธฐ ์ํด, ๋ ๋๋ง์ ํ์ํ ๋ฐ์ดํฐ(์ํ)๋ฅผ ์ธ๋ฐํ๊ฒ ๊ด๋ฆฌํด์ผํ ํ์๊ฐ ์๊ฒผ๋ค.
- ๊ทธ๋์ Redux์ ๊ฐ์ ์ํ๊ด๋ฆฌ ๋ผ์ด๋ธ๋ฌ๋ฆฌ(ํ๋ ์์ํฌ)๊ฐ ์๊ฒจ๋ฌ๋ค.
์ปดํฌ๋ํธ
- Angular๊ฐ CSR์ ์์์ด์๋ค๋ฉด React๋ ์ปดํฌ๋ํธ ๊ธฐ๋ฐ ๊ฐ๋ฐ์ ์์!
- ํ ์์ ์ ์น ์ดํ๋ฆฌ์ผ์ด์ ์ ๋๋ถ๋ถ ์ปดํฌ๋ํธ ๋จ์๋ก ์ค๊ณ๋๊ณ ๊ฐ๋ฐ๋๋ค.
- ๋ํ ์ปดํฌ๋ํธ๋ง๋ค ์ปดํฌ๋ํธ๋ฅผ ๋ ๋๋งํ ๋ ํ์ํ ์ํ๋ฅผ ๊ด๋ฆฌํ๋ค.
Proxy
ํน์Observer Pattern
๋ฑ์ ์ด์ฉํ์ฌ ๊ตฌํํ๋ค.
2. state - setState - render
2-1. ๊ตฌํํด๋ณด๊ธฐ
setState
๋ฅผ ํตํด์ state
๋ฅผ ๊ธฐ๋ฐ์ผ๋ก render
๋ฅผ ํด์ฃผ๋ ์ฝ๋๋ฅผ ๋ง๋ค์ด๋ณด๊ธฐ
<div id="app"></div>
<script>
const $app = document.querySelector('#app');
let state = {
items: ['item1', 'item2', 'item3', 'item4']
}
const render = () => {
const { items } = state;
$app.innerHTML = `
<ul>
${items.map(item => `<li>${item}</li>`).join('')}
</ul>
<button id="append">์ถ๊ฐ</button>
`;
document.querySelector('#append').addEventListener('click', () => {
setState({ items: [ ...items, `item${items.length + 1}` ] })
})
}
const setState = (newState) => {
state = { ...state, ...newState };
render();
}
render();
</script>
state
๊ฐ ๋ณ๊ฒฝ๋๋ฉดrender
๋ฅผ ์คํํ๋ค.state
๋setState
๋ก๋ง ๋ณ๊ฒฝํด์ผ ํ๋ค.
์์ 2๊ฐ์ง๊ฐ ์ ๋ง ์ ๋ง ํต์ฌ ๋ด์ฉ์ด๋ผ๊ณ ์๊ฐํ๋ค.
2-2. ์ถ์ํ
class ๋ฌธ๋ฒ์ผ๋ก ์ข๋ ์ถ์ํํด๋ณด์
<div id="app"></div>
<script>
class Component {
$target;
state;
constructor ($target) {
this.$target = $target;
this.setup();
this.render();
}
setup () {};
template () { return ''; }
render () {
this.$target.innerHTML = this.template();
this.setEvent();
}
setEvent () {}
setState (newState) {
this.state = { ...this.state, ...newState };
this.render();
}
}
class App extends Component {
setup () {
this.state = { items: ['item1', 'item2'] };
}
template () {
const { items } = this.state;
return `
<ul>
${items.map(item => `<li>${item}</li>`).join('')}
</ul>
<button>์ถ๊ฐ</button>
`
}
setEvent () {
this.$target.querySelector('button').addEventListener('click', () => {
const { items } = this.state;
this.setState({ items: [ ...items, `item${items.length + 1}` ] });
});
}
}
new App(document.querySelector('#app'));
</script>
class๋ฅผ ์ฌ์ฉํ์ฌ ์กฐ๊ธ ๋ ์ ์ฐํ๊ณ ๊ทธ๋ด๋ฏํ(?) ์ปดํฌ๋ํธ๊ฐ ์์ฑ๋์๋ค.
2-3. ๋ชจ๋ํ
์์ ํ์ผ๋ค์ ์๋์ ๊ฐ์ ๊ตฌ์กฐ๋ก ๋๋ ์ ์๋ค.
.
โโโ index.html
โโโ src
โโโ app.js # ES Module์ entry file
โโโ components # Component ์ญํ ์ํ๋ ๊ฒ๋ค
โ โโโ Items.js
โโโ core # ๊ตฌํ์ ํ์ํ ์ฝ์ด๋ค
โโโ Component.js
src/core/Component.js
- ์๋์ ๊ฐ์ด Component ์ญํ ์ ํ๋ class๋ฅผ ์์ฑํ๋ค.
export default class Component {
$target;
state;
constructor ($target) {
this.$target = $target;
this.setup();
this.render();
}
setup () {};
template () { return ''; }
render () {
this.$target.innerHTML = this.template();
this.setEvent();
}
setEvent () {}
setState (newState) {
this.state = { ...this.state, ...newState };
this.render();
}
}
src/components/Items.js
- ์์ Component class๋ฅผ ์์ํ์ฌ ์ข๋ ๊ตฌ์ฒด์ ์ธ class๋ฅผ ๊ตฌํํ๋ค.
import Component from "../core/Component.js";
export default class Items extends Component {
setup () {
this.state = { items: ['item1', 'item2'] };
}
template () {
const { items } = this.state;
return `
<ul>
${items.map(item => `<li>${item}</li>`).join('')}
</ul>
<button>์ถ๊ฐ</button>
`
}
setEvent () {
this.$target.querySelector('button').addEventListener('click', () => {
const { items } = this.state;
this.setState({ items: [ ...items, `item${items.length + 1}` ] });
});
}
}
index.html
- html์๋ ์๋์ ๊ฐ์ด entry ์ญํ (app ํน์ root)์ ํ๋ ํ๊ทธ ํ๋๋ง์ด ์กด์ฌํ๋ค.
<!doctype html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<title>Simple Component 2</title>
</head>
<body>
<div id="app"></div>
<script src="./src/app.js" type="module"></script>
</body>
</html>
src/app.js
- app ํ๊ทธ๋ฅผ target์ผ๋ก ํ์ฌ ๊ตฌํ๋ Items๋ฅผ ๋ถ์ธ๋ค.
import Items from "./components/Items.js";
class App {
constructor() {
const $app = document.querySelector('#app');
new Items($app);
}
}
new App();
3. ์ด๋ฒคํธ ์ฒ๋ฆฌ
3-1. ๋ถํธํจ ๊ฐ์ง
์์ ์ฝ๋๋ฅผ ๋ณด๋ฉด render ์๋ง๋ค ์๋ก์ด html์ ํ ๋นํจ์ผ๋ก ๋ค์ event๋ฅผ ๋ฑ๋กํด์ฃผ์ด์ผ ํ๋ค. ๋ํ ๋ฐ๋ณต์ ์ธ ์์์ ๋ํด์ ์ด๋ฒคํธ๋ฅผ ๋ฑ๋กํด์ฃผ์ด์ผํ ๋๋ ๋ ๋ถํธํ๋ค. ์๋ฅผ ๋ค์ด ๊ฐ๊ฐ์ ์์ดํ ์ ์ญ์ ํ๋ ๊ธฐ๋ฅ์ ์ถ๊ฐํ๋ค๊ณ ํ๋ฉด ์๋์ ๊ฐ๋ค.
import Component from "../core/Component.js";
export default class Items extends Component {
setup () {
this.state = { items: ['item1', 'item2'] };
}
template () {
const { items } = this.state;
return `
<ul>
${items.map((item, key) => `
<li>
${item}
<button class="deleteBtn" data-index="${key}">์ญ์ </button>
</li>
`).join('')}
</ul>
<button class="addBtn">์ถ๊ฐ</button>
`
}
setEvent () {
this.$target.querySelector('.addBtn').addEventListener('click', () => {
const { items } = this.state;
this.setState({ items: [ ...items, `item${items.length + 1}` ] });
});
// ์๋์ ๊ฐ์ด ๋ชจ๋ deleteBtn์ ๋ํด์ ๋ค์ ๋ณต์กํ ํธ๋ค๋ฌ๋ฅผ ์ ๋ฌํด์ผํ๋ค.
this.$target.querySelectorAll('.deleteBtn').forEach(deleteBtn =>
deleteBtn.addEventListener('click', ({ target }) => {
const items = [ ...this.state.items ];
items.splice(target.dataset.index, 1);
this.setState({ items });
}))
}
}
3-2. ์ด๋ฒคํธ ๋ฒ๋ธ๋ง
export default class Items extends Component {
setup () {/* ์๋ต */}
template () { /* ์๋ต */}
setEvent () {
// ๋ชจ๋ ์ด๋ฒคํธ๋ฅผ this.$target์ ๋ฑ๋กํ์ฌ ์ฌ์ฉํ๋ฉด ๋๋ค.
// ์ฌ๊ธฐ์ { target } ์์ target์ e.target์ด ๋๋ค.
this.$target.addEventListener('click', ({ target }) => {
const items = [ ...this.state.items ];
if (target.classList.contains('addBtn')) {
this.setState({ items: [ ...items, `item${items.length + 1}` ] });
}
if (target.classList.contains('deleteBtn')) {
items.splice(target.dataset.index, 1);
this.setState({ items });
}
});
}
}
์์ ๊ฐ์ด ์ด๋ฒคํธ ๋ฒ๋ธ๋ง์ ์ด์ฉํ๊ธฐ ์ํด $target(์ปดํฌ๋ํธ๊ฐ ๋ถ๊ฒ ๋๋ element)์ ์ด๋ฒคํธ๋ฅผ ๋ฑ๋กํ๊ธฐ ๋๋ฌธ์ ์ด์ render๋ง๋ค ๊ตณ์ด setEvent๋ฅผ ํธ์ถํ ํ์๊ฐ ์์ด์ง๋ค.
export default class Component {
$target;
state;
constructor ($target) {
this.$target = $target;
this.setup();
+ this.setEvent(); // constructor์์ ํ ๋ฒ๋ง ์คํํ๋ค.
this.render();
}
setup () {};
template () { return ''; }
render () {
this.$target.innerHTML = this.template();
- this.setEvent(); // render ๋๋ง๋ค ์ด๋ฒคํธ๋ฅผ ๋ถ์ผ ํ์๊ฐ ์์ด์ก๋ค.
}
setEvent () {}
setState (newState) {
this.state = { ...this.state, ...newState };
this.render();
}
}
3-3. ์ด๋ฒคํธ ๋ฒ๋ธ๋ง ์ถ์ํ
export default class Component {
$target;
state;
constructor ($target) { /* ์๋ต */ }
setup () { /* ์๋ต */ }
template () { /* ์๋ต */ }
render () { /* ์๋ต */ }
setEvent () { /* ์๋ต */ }
setState (newState) { /* ์๋ต */ }
addEvent (eventType, selector, callback) {
const children = [...this.$target.querySelectorAll(selector)];
this.$target.addEventListener(eventType, event => {
if (!event.target.closest(selector)) return false;
callback(event);
})
}
}
export default class Items extends Component {
setup () { /* ์๋ต */ }
template () {/* ์๋ต */ }
setEvent () {
this.addEvent('click', '.addBtn', ({ target }) => {
const { items } = this.state;
this.setState({ items: [ ...items, `item${items.length + 1}` ] });
});
this.addEvent('click', '.deleteBtn', ({ target }) => {
const items = [ ...this.state.items ];
items.splice(target.dataset.index, 1);
this.setState({ items });
});
}
}
4. ์ปดํฌ๋ํธ ๋ถํ ํ๊ธฐ
4-1. ๊ธฐ๋ฅ ์ถ๊ฐ
ํ์ฌ์ ์ฝ๋๊น์ง๋ ์ปดํฌ๋ํธ๋ฅผ ๋ถ๋ฆฌํ ์ด์ ๊ฐ ์๊ธฐ์, toggle
, filter
๋ฑ์ ๊ธฐ๋ฅ์ ์ถ๊ฐ
export default class Items extends Component {
get filteredItems () {
const { isFilter, items } = this.state;
return items.filter(({ active }) => (isFilter === 1 && active) ||
(isFilter === 2 && !active) ||
isFilter === 0);
}
setup() {
this.state = {
isFilter: 0,
items: [
{
seq: 1,
contents: 'item1',
active: false,
},
{
seq: 2,
contents: 'item2',
active: true,
}
]
};
}
template() {
return `
<header>
<input type="text" class="appender" placeholder="์์ดํ
๋ด์ฉ ์
๋ ฅ" />
</header>
<main>
<ul>
${this.filteredItems.map(({contents, active, seq}) => `
<li data-seq="${seq}">
${contents}
<button class="toggleBtn" style="color: ${active ? '#09F' : '#F09'}">
${active ? 'ํ์ฑ' : '๋นํ์ฑ'}
</button>
<button class="deleteBtn">์ญ์ </button>
</li>
`).join('')}
</ul>
</main>
<footer>
<button class="filterBtn" data-is-filter="0">์ ์ฒด ๋ณด๊ธฐ</button>
<button class="filterBtn" data-is-filter="1">ํ์ฑ ๋ณด๊ธฐ</button>
<button class="filterBtn" data-is-filter="2">๋นํ์ฑ ๋ณด๊ธฐ</button>
</footer>
`
}
setEvent() {
this.addEvent('keyup', '.appender', ({ key, target }) => {
if (key !== 'Enter') return;
const {items} = this.state;
const seq = Math.max(0, ...items.map(v => v.seq)) + 1;
const contents = target.value;
const active = false;
this.setState({
items: [
...items,
{seq, contents, active}
]
});
});
this.addEvent('click', '.deleteBtn', ({target}) => {
const items = [ ...this.state.items ];;
const seq = Number(target.closest('[data-seq]').dataset.seq);
items.splice(items.findIndex(v => v.seq === seq), 1);
this.setState({items});
});
this.addEvent('click', '.toggleBtn', ({target}) => {
const items = [ ...this.state.items ];
const seq = Number(target.closest('[data-seq]').dataset.seq);
const index = items.findIndex(v => v.seq === seq);
items[index].active = !items[index].active;
this.setState({items});
});
this.addEvent('click', '.filterBtn', ({ target }) => {
this.setState({ isFilter: Number(target.dataset.isFilter) });
});
}
}
ํฌ๊ฒ ๋ณด๋ฉด ์ด๊ฒ๋ ์ปดํฌ๋ํธ๋ผ๊ณ ๋ณผ ์ ์์ง๋ง, ์ปดํฌ๋ํธ๋ฅผ ๋๋๋ ๊ธฐ๋ณธ์ ์ธ ์ด์ ์ธ ์ฌํ์ฉ
์ด ์ฌ์ค์ ์ด๋ ต๋ค.
ํ๋์ ์ปดํฌ๋ํธ๊ฐ ์ต๋ํ ์์ ์ผ์ ํ๋๋ก ํด์ผ ์ถํ์ ์ฌํ์ฉํ๊ธฐ ์ข๋ค.
4-2. ํด๋ ๊ตฌ์กฐ
ํ ๋ฒ ๋ ๋ถ๋ฆฌํ์ฌ ์ข๋ ์ธ๋ฐํ๊ฒ ๊ตฌ์กฐ๋ฅผ ๋๋ ๋ณด์
.
โโโ index.html
โโโ src
โโโ App.js # main์์ App ์ปดํฌ๋ํธ๋ฅผ ๋ง์ดํธํ๋ค.
โโโ main.js # js์ entry ํฌ์ธํธ
โโโ components
โ โโโ ItemAppender.js
โ โโโ ItemFilter.js
โ โโโ Items.js
โโโ core
โโโ Component.js
- App Component ์ถ๊ฐ
- entry point๋ฅผ app.js์์ main.js๋ก ๋ณ๊ฒฝ
Items
์์ItemAppender
,ItemFilter
์ ๋ถ๋ฆฌ
4-3. Component Core ๋ณ๊ฒฝ(props์ mounted ์ถ๊ฐ)
export default class Component {
$target;
props;
state;
constructor ($target, props) {
this.$target = $target;
this.props = props; // props ํ ๋น
this.setup();
this.setEvent();
this.render();
}
setup () {};
mounted () {};
template () { return ''; }
render () {
this.$target.innerHTML = this.template();
this.mounted(); // render ํ์ mounted๊ฐ ์คํ ๋๋ค.
}
setEvent () {}
setState (newState) { /* ์๋ต */ }
addEvent (eventType, selector, callback) { /* ์๋ต */ }
}
render
์ดํ์ ์ด๋ค ํจ์๋ค์ ์คํํ๊ธฐ ์ํดmounted()
๋ฉ์๋๋ฅผ ์ถ๊ฐํ๋ค.props
๋ ๋ถ๋ชจ ์ปดํฌ๋ํธ๊ฐ ์์ ์ปดํฌ๋ํธ์๊ฒ ์ํ ํน์ ๋ฉ์๋๋ฅผ ๋๊ฒจ์ฃผ๊ธฐ ์ํจ์ด๋ค.
๋ฆฌ์กํธ์ ์ปดํฌ๋ํธ๋
์๋ช ์ฃผ๊ธฐ
๋ฅผ ๊ฐ๋๋ค. ๋จ์ํ๊ฒ์์ฑ -> ์ ๋ฐ์ดํธ -> ์ ๊ฑฐ
์ ์ถ์ ์ด๊ฒ ๋๋ ๊ฒ์ด๋ค.
์ด ๋, ์์ฑ ๋จ๊ณ๊ฐmounting
๋จ๊ณ์ด๋ค. ์ด ๋จ๊ณ์์๋ Component ํจ์๊ฐ ์คํ๋๊ณ ๊ฒฐ๊ณผ๋ฌผ๋ก ๋์จ Element๊ฐ ๊ฐ์ DOM์ ์ฝ์ ๋๊ณ ์ค์ DOM์ ์ ๋ฐ์ดํธํ๊ธฐ๊น์ง์ ๊ณผ์ ์ด ์ผ์ด๋๋ค.
4-4. Entry Point ๋ณ๊ฒฝ
<!doctype html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<title>Simple Component 8</title>
</head>
<body>
<h1>Example #8</h1>
<div id="app"></div>
-<script src="src/app.js" type="module"></script>
+<script src="src/main.js" type="module"></script>
</body>
</html>
import App from './App.js';
new App(document.querySelector('#app'));
์์งํ ์ด ๊ธ์์ ์์ ๊ฐ์ด entry point๋ฅผ ๋ณ๊ฒฝํ ๋ช ํํ ์ด์ ๋ ๋ชจ๋ฅด๊ฒ ๋ค.
์ผ๋จ ์ด๋ ๊ฒ App๋ ํ๋์ ์ปดํฌ๋ํธ๋ก ๊ตฌ๋ถํด์ฃผ๋ฉด ์ฌํ์ฉ์ฑ์ด ๋์์ง๋ค๋ ์ , ์ถํ์ App ์ปดํฌ๋ํธ ์ธ๋ถ์์ ์ด๋ค ์์ ์ ํ ๋ ์ถ๊ฐํ๊ธฐ ํธํ๋ค๋ ์ ์ ๋..?!
์๋ฅผ ๋ค์ด ๋ฆฌ์กํธ์ ๊ฒฝ์ฐ๋ ์๋์ ๊ฐ์ด ์ฒ๋ฆฌ๊ฐ ๋์ด์์ผ๋ ๋ง์ด๋ค.
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<React.StrictMode> // ์ด๋ ๊ฒ strict mode๋ฅผ ๊ฑธ์ด์ฃผ๊ณ ์ถ์ ๋
<App />
</React.StrictMode>
);
4-5. ์ปดํฌ๋ํธ ๋ถํ
๊ธฐ์กด Items์ ์กด์ฌํ๋ ๋ก์ง์ App์ผ๋ก ๋๊ธฐ๊ณ App์์ ์ฌ๋ฌ ๋ฉ์๋๋ฅผ ๊ด๋ฆฌํ๋ฉฐ ํ์ ์ปดํฌ๋ํธ๋ค(Items, ItemAppender, ItemFilter)์๊ฒ props๋ก ๋๊ฒจ์ค๋ค.
import Component from "./core/Component.js";
import Items from "./components/Items.js";
import ItemAppender from "./components/ItemAppender.js";
import ItemFilter from "./components/ItemFilter.js";
export default class App extends Component {
setup () {
this.state = {
isFilter: 0,
items: [
{
seq: 1,
contents: 'item1',
active: false,
},
{
seq: 2,
contents: 'item2',
active: true,
}
]
};
}
template () {
return `
<header data-component="item-appender"></header>
<main data-component="items"></main>
<footer data-component="item-filter"></footer>
`;
}
// mounted์์ ์์ ์ปดํฌ๋ํธ๋ฅผ ๋ง์ดํธ ํด์ค์ผ ํ๋ค.
mounted () {
const { filteredItems, addItem, deleteItem, toggleItem, filterItem } = this;
const $itemAppender = this.$target.querySelector('[data-component="item-appender"]');
const $items = this.$target.querySelector('[data-component="items"]');
const $itemFilter = this.$target.querySelector('[data-component="item-filter"]');
// ํ๋์ ๊ฐ์ฒด์์ ์ฌ์ฉํ๋ ๋ฉ์๋๋ฅผ ๋๊ฒจ์ค bind๋ฅผ ์ฌ์ฉํ์ฌ this๋ฅผ ๋ณ๊ฒฝํ๊ฑฐ๋,
// ๋ค์๊ณผ ๊ฐ์ด ์๋ก์ด ํจ์๋ฅผ ๋ง๋ค์ด์ค์ผ ํ๋ค.
// ex) { addItem: contents => addItem(contents) }
new ItemAppender($itemAppender, {
addItem: addItem.bind(this)
});
new Items($items, {
filteredItems,
deleteItem: deleteItem.bind(this),
toggleItem: toggleItem.bind(this),
});
new ItemFilter($itemFilter, {
filterItem: filterItem.bind(this)
});
}
get filteredItems () {
const { isFilter, items } = this.state;
return items.filter(({ active }) => (isFilter === 1 && active) ||
(isFilter === 2 && !active) ||
isFilter === 0);
}
addItem (contents) {
const {items} = this.state;
const seq = Math.max(0, ...items.map(v => v.seq)) + 1;
const active = false;
this.setState({
items: [
...items,
{seq, contents, active}
]
});
}
deleteItem (seq) {
const items = [ ...this.state.items ];;
items.splice(items.findIndex(v => v.seq === seq), 1);
this.setState({items});
}
toggleItem (seq) {
const items = [ ...this.state.items ];
const index = items.findIndex(v => v.seq === seq);
items[index].active = !items[index].active;
this.setState({items});
}
filterItem (isFilter) {
this.setState({ isFilter });
}
}
import Component from "../core/Component.js";
export default class ItemAppender extends Component {
template() {
return `<input type="text" class="appender" placeholder="์์ดํ
๋ด์ฉ ์
๋ ฅ" />`;
}
setEvent() {
const { addItem } = this.props; // ์ด๋ ๊ฒ props๋ฅผ ํตํด ์ด๋ฒคํธ ํธ๋ค๋ฌ๋ฅผ ์ ๋ฌํ๋ค.
this.addEvent('keyup', '.appender', ({ key, target }) => {
if (key !== 'Enter') return;
addItem(target.value);
});
}
}
import Component from "../core/Component.js";
export default class Items extends Component {
template() {
const { filteredItems } = this.props;
return `
<ul>
${filteredItems.map(({contents, active, seq}) => `
<li data-seq="${seq}">
${contents}
<button class="toggleBtn" style="color: ${active ? '#09F' : '#F09'}">
${active ? 'ํ์ฑ' : '๋นํ์ฑ'}
</button>
<button class="deleteBtn">์ญ์ </button>
</li>
`).join('')}
</ul>
`
}
setEvent() {
const { deleteItem, toggleItem } = this.props;
this.addEvent('click', '.deleteBtn', ({target}) => {
deleteItem(Number(target.closest('[data-seq]').dataset.seq));
});
this.addEvent('click', '.toggleBtn', ({target}) => {
toggleItem(Number(target.closest('[data-seq]').dataset.seq));
});
}
}
import Component from "../core/Component.js";
export default class ItemFilter extends Component {
template() {
return `
<button class="filterBtn" data-is-filter="0">์ ์ฒด ๋ณด๊ธฐ</button>
<button class="filterBtn" data-is-filter="1">ํ์ฑ ๋ณด๊ธฐ</button>
<button class="filterBtn" data-is-filter="2">๋นํ์ฑ ๋ณด๊ธฐ</button>
`
}
setEvent() {
const { filterItem } = this.props;
this.addEvent('click', '.filterBtn', ({ target }) => {
filterItem(Number(target.dataset.isFilter));
});
}
}
์คํฐ๋ ์ดํ
Template Method Pattern(ํ ํ๋ฆฟ ๋ฉ์๋ ํจํด)
- ๊ฐ์ฒด์งํฅ ํ๋ก๊ทธ๋๋ฐ์ ๋์์ธ ํจํด ์ค ํ๋
- ์๊ณ ๋ฆฌ์ฆ์ ๊ตฌ์กฐ๋ฅผ ๋ฉ์๋์ ์ ์ํ๊ณ , ํ์ ํด๋์ค์์ ์๊ณ ๋ฆฌ์ฆ ๊ตฌ์กฐ์ ๋ณ๊ฒฝ์์ด ํด๋น ์๊ณ ๋ฆฌ์ฆ์ ์ฌ์ ์ํ์ฌ ์ฌ์ฉํ๋ ํจํด์ด๋ค.
- ์๊ณ ๋ฆฌ์ฆ์ด ๋จ๊ณ๋ณ๋ก ๋๋์ด์ง๋ ๊ฒฝ์ฐ ํน์ ๊ฐ์ ์ญํ ์ ํ๋ ๋ฉ์๋์ง๋ง ์ฌ๋ฌ ๊ณณ์์ ๋ค๋ฅธ ํํ๋ก ์ฌ์ฉ์ด ํ์ํ ๊ฒฝ์ฐ ์ ์ฉํ ํจํด์ด๋ค.
- ์์์ ํตํด์ ์ํผํด๋์ค์ ๊ธฐ๋ฅ์ ํ์ฅํ ๋ ์ฌ์ฉํ๋ ๋ํ์ ์ธ ๋ฐฉ๋ฒ์ด๋ค. ๋ณํ์ง ์๋ ๊ธฐ๋ฅ์ ์ํผ ํด๋์ค์ ๋ง๋ค์ด๋๊ณ ์์ฃผ ๋ณ๊ฒฝ๋๋ฉฐ ํ์ฅํ ๊ธฐ๋ฅ์ ์๋ธ ํด๋์ค์์ ๊ตฌํํ๋ค.
class SuperClass {
constructor() {
// ์ปจ์คํธ๋ญํฐ
}
superLog() {
console.log('์ํผํด๋์ค์์ ์ ์ํ ๋ฉ์๋');
this.subLog();
}
subLog() {
console.log('์๋ธํด๋์ค์์ ๋ณ๊ฒฝํ๋ฉฐ ์ฌ์ฉํ ๋ฉ์๋')
}
}
class SubClass extends SuperClass {
constructor() {
// ์ปจ์คํธ๋ญํฐ
super();
}
subLog() {
console.log('์๋ธํด๋์ค์์ ์ง๊ธ ๋ณ๊ฒฝํ ๋ฉ์๋')
}
}
const sub = new SubClass();
sub.superLog();
/* log
์ํผํด๋์ค์์ ์ ์ํ ๋ฉ์๋
์๋ธํด๋์ค์์ ์ง๊ธ ๋ณ๊ฒฝํ ๋ฉ์๋
*/
์์ ๊ฐ์ด SubClass
๋ก ์์ฑ๋ sub
๊ฐ์ฒด๊ฐ superLog
๋ฅผ ํธ์ถํ ๊ฒฝ์ฐ, SubClass์์ ์ค๋ฒ๋ผ์ด๋ฉ๋ subLog
๊ฐ ํธ์ถ๋๋ ๊ฒ์ ๋ณผ ์ ์๋ค.
reduce๋ ์ธ์ ๋ ์ข์๊น?(feat. map, join)
์คํฐ๋์๋ง๋ค ํธ์ํ(?) ๊ณ ์ฐจํจ์๊ฐ ๋ฌ๋๋ค. ์๋๋ ์ด์ผ๊ธฐ๋ฅผ ๋๋๋ฉฐ ๋์จ ์์
const names = ['jayden', 'den', 'zoey', 'lily', 'bakha'];
const literalWithReduce = names.reduce((acc, cur) => {
return acc + `<li>${cur}</li>`;
}, '');
const literalWithMapJoin = names.map((name) => `<li>${name}</li>`).join('');
์์์ ๊ฒฝ์ฐ 2๊ฐ์ง ๋ชจ๋ names ๋ฐฐ์ด์ ์์๋ค์ ๋ฐ์์ li ํ๊ทธ ํํ๋ก ๋ง๋ ํ ๋ฌธ์์ด์ ํฉ์น literal์ ๋ฐํํ๋ค. ์ ๋ต์ ์๊ฒ ์ง๋ง reduce๋ฅผ ํ์ฉํ๋ ๊ฒฝ์ฐ, ๊ณ ์ฐจํจ์ ํ๋๋ก ๋ฌธ์์ด์ ์ถ๊ฐํ๊ณ ๊ฐ ๋ฐฐ์ด์ ์์๋ฅผ ํฉ์น ์ ์๋ค๋ ์ ์์ ์์ฃผ ์ฝ๊ฐ์ ์ฑ๋ฅ ์ฐ์๊ฐ ์์ ๊ฒ ๊ฐ๋ค. ๋ฐ๋ฉด map, join์ names๋ผ๋ ๋ฐฐ์ด์ 2๋ฒ ์ํํ๊ธฐ๋ ํ์ง๋ง, ํจ์ํ ํ๋ก๊ทธ๋๋ฐ์์ ์งํฅํ๋ ์ ์ธํ์ ๋๋์ ์ ๋ง ์ ๋ณด์ฌ์ค๋ค๊ณ ์๊ฐํ๋ค. (๋๊ฐ๋ด๋ ๋ฐฐ์ด์ mapping์ ํ๊ณ join์ ํตํด ๋ฐฐ์ด์ ๊ฐ ์์๋ฅผ ํฉ์ณ์ฃผ๊ณ ์์ผ๋๊น)
๋์ ๊ฐ์ธ์ ์ธ ๊ฒฐ๋ก ์ ๋ค์ฑ๋กญ๊ฒ ํ์ฉ๊ฐ๋ฅํ๊ณ ์ฑ๋ฅ์์ ์ฐ์๋ฅผ ์ ํ ์ ์๋ reduce๋ฅผ ์ฌ์ฉํ๋, ๊ทธ reduce์ ์ ๋ฌํ๋ callback์ ๋ฐ๋ก ๋ถ๋ฆฌํ์ฌ ์ข๋ ๋ช ํํ ์ด๋ฆ์ ์ง์ด์ฃผ๋ ๊ฒ ์ข๋ค๋ ๊ฒ์ด๋ค.
const names = ['jayden', 'den', 'zoey', 'lily', 'bakha'];
const getListTags = (acc, cur) => acc + `<li>${cur}</li>`;
const literalWithReduce = names.reduce(getListTags, '');