๐ŸŽพ ๊ธฐ์ˆ ์ฑ… ์Šคํ„ฐ๋””

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, '');

์ฐธ๊ณ