๋ณธ๋ฌธ ๋ฐ”๋กœ๊ฐ€๊ธฐ
JavaScript/Vanila

[Vanila js] ํƒญ ๊ตฌํ˜„ํ•˜๊ธฐ

by ๐Ÿณ Laboon 2024. 8. 1.

Result

tab ๊ธฐ๋Šฅ


View

export default class TabView extends View {

    constructor() {
        super(qs('#tab-view'));
     
        this.template = new Template();
    }
    
    show() {}
    bindEvents() {}
    
}

 

์ •์  ํŽ˜์ด์ง€์˜ tab-view id๋ฅผ ๊ฐ€์ง„ ํƒœ๊ทธ์— ์ž‘์—…์„ ์ˆ˜ํ–‰ํ•œ๋‹ค.

Result์™€ ๊ฐ™์ด ํƒญ์„ ๋ณด์—ฌ์ค„ ๊ธฐ๋Šฅ๊ณผ, ํƒญ์„ ๋ˆ„๋ฅด๋Š” ํ–‰์œ„๋ฅผ ํ•˜๋Š” ๊ธฐ๋Šฅ์ด ํ•„์š”


Controller

export default class Controller {
  constructor(store, {searchFormView, searchResultView, tabView}) {
    this.store = store;
    
    this.searchFormView = searchFormView;
    this.searchResultView = searchResultView;
    this.tabView = tabView;

    this.subscribeViewEvents();
    this.render();
  }
}

 

Controller์—๋Š” View์™€ Model์˜ ์ œ์–ด๋ฅผ ์œ„ํ•ด ๋‹น์—ฐํžˆ tabView๊ฐ€ ์ถ”๊ฐ€ ๋˜์–ด์•ผํ•œ๋‹ค.

์ถ”๊ฐ€์ ์œผ๋กœ View์—์„œ ๊ตฌํ˜„ํ•œ tab์„ ๋ณด์—ฌ์ฃผ๋Š” ๊ธฐ๋Šฅ๊ณผ ํด๋ฆญ๊ณผ ๊ฐ™์€ ์ด๋ฒคํŠธ๋“ค์„ ์ฒ˜๋ฆฌํ•˜๋Š” ์ž‘์—…์„ ๊ตฌํ˜„ํ•ด์•ผํ•œ๋‹ค.

  subscribeViewEvents() {
    this.tabView
    .on('@select', (event) => this.select(event.detail.target));
  }

์˜ˆ๋ฅผ ๋“ค์–ด, tabView์—์„œ select๋ผ๋Š” ์ด๋ฒคํŠธ๊ฐ€ ๋ฐœ์ƒํ–ˆ์„ ๋•Œ, chaining ๋œ on method๋กœ ์ด๋ฒคํŠธ๋ฅผ ์บ์น˜ํ•˜๊ณ  ์ฒ˜๋ฆฌํ•œ๋‹ค.


Model

export default class Store {
  constructor(storage) {
    if (!storage) throw "no storage";

    this.storage = storage;
    this.searchResult = [];
    this.searchKeyword = "";
    this.selectedTab = TabType.KEYWORD;
  }
}

 

Store Model ์—์„œ๋Š” ์–ด๋–ค ํ‚ค์›Œ๋“œ๋ฅผ ์„ ํƒํ–ˆ๋Š”์ง€์— ๋Œ€ํ•œ ์ •๋ณด๋ฅผ ๋‹ด๊ธฐ ์œ„ํ•œ selectedTab ๋ณ€์ˆ˜๊ฐ€ ์ถ”๊ฐ€๋œ๋‹ค.

๋งจ ์ฒ˜์Œ, ๋ Œ๋”๋ง ํ•  ๋•Œ default๋กœ ์ถ”์ฒœ๊ฒ€์ƒ‰์–ด๋ฅผ ์„ ํƒํ•œ๋‹ค.


Quest

Q. ๊ฐ ํƒญ์„ ํด๋ฆญํ•˜๋ฉด ํƒญ ์•„๋ž˜ ๋‚ด์šฉ์ด ๋ณ€๊ฒฝ๋˜๋„๋ก ํ•œ๋‹ค.

์šฐ์„ , ๊ฐ•์˜์˜ ์ฑ•ํ„ฐ ์ƒ ํƒญ ์•„๋ž˜ ๋‚ด์šฉ์€ tab-content๋กœ ๋‹ค๋ฅธ ์ฑ…์ž„์„ ๊ฐ€์ง€๊ณ  ์žˆ์œผ๋ฏ€๋กœ ์ž ์‹œ ๋น„์›Œ๋‘”๋‹ค.

    bindEvents() {
        on(this.element, "click", event => this.handleClick(event));
    }

    handleClick(event) {
        const { target } = event;
        
        this.emit("@select", { target });
    }

๋‚˜๋Š” TabView์—์„œ ํด๋ฆญ ์ด๋ฒคํŠธ๊ฐ€ ๋ฐœ์ƒํ•˜๋ฉด ํด๋ฆญํ•œ ์ฃผ์ฒด๋ฅผ @select ์ด๋ฒคํŠธ๋กœ ๋ฐœํ–‰ํ–ˆ๋‹ค.

 

๊ทธ๋ฆฌ๊ณ  ์•ž์„œ Controller์—์„œ ์ž‘์„ฑํ•œ, subscribeEvents ๋ฉ”์†Œ๋“œ์—์„œ tabView๋กœ ๋ถ€ํ„ฐ @select ์ด๋ฒคํŠธ๊ฐ€ ๋ฐœ์ƒํ•˜๋ฉด ์ฒ˜๋ฆฌํ•œ๋‹ค.

Controller ์ž…์žฅ์—์„œ, ์–ด๋–ค ํƒœ๊ทธ๋ฅผ ํด๋ฆญํ–ˆ๋Š”์ง€ ๋ช…ํ™•ํžˆ ์•Œ ์ˆ˜ ์žˆ๋‹ค. (TabView์—์„œ ์•Œ๋ ค์คŒ)

  select(target) {
    const tab = target.dataset.tab;

    this.store.select(tab);
    this.tabView.show(this.store.selectedTab);
  }

 

๊ทธ๋ฆฌ๊ณ  target์˜ tab ์ •๋ณด๋ฅผ ๊ฐ€์ ธ์™€์„œ store์˜ selectedTab ๋ณ€์ˆ˜๋กœ ์ง€์ •ํ•œ๋‹ค.

๊ทธ๋ฆฌ๊ณ  tabView๋ฅผ ๋‹ค์‹œ ๋ Œ๋”๋ง ํ•œ๋‹ค.


๊ฐ•์‚ฌ๋‹˜๊ป˜์„œ๋Š” utility(helper.js)์˜ delegate๋ฅผ ์‚ฌ์šฉํ•˜๊ณ  ์žˆ๋‹ค.

export function delegate(target, eventName, selector, handler) {
  const emitEvent = (event) => {
    const potentialElements = qsAll(selector, target);

    for (const potentialElement of potentialElements) {
      if (potentialElement === event.target) {
        return handler.call(event.target, event);
      }
    }
  };

  on(target, eventName, emitEvent);
}

1. ์ƒ์œ„ tag์˜ ๋ชจ๋“  ํ•˜์œ„ ์š”์†Œ๋ฅผ ๋‹ค ๊ฐ€์ ธ์˜จ๋‹ค.

2. ์ƒ์œ„ tag์˜ ๋ชจ๋“  ํ•˜์œ„ ์š”์†Œ ์ค‘ ๋ธŒ๋ผ์šฐ์ €์—์„œ event๊ฐ€ ๋ฐœ์ƒํ•œ ์š”์†Œ์™€ ๊ฐ™์€ ๊ฒƒ์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค.

3. ์ƒ์œ„ tag์˜ ์ด๋ฒคํŠธ๋กœ ์ถ”๊ฐ€ํ•œ๋‹ค.

 

๋‚ด ์ฝ”๋“œ์˜ ๋ฌธ์ œ์ ์€ li ํƒœ๊ทธ ์™ธ์— ๋‹ค๋ฅธ ํƒœ๊ทธ๊ฐ€ ์ถ”๊ฐ€๋˜์—ˆ์„ ๋•Œ๋„ ์ด๋ฒคํŠธ๊ฐ€ ๋ฐœํ–‰๋œ๋‹ค๋Š” ์ ์ด๋‹ค.

์œ„์— ์ž‘์„ฑํ•œ ์ฝ”๋“œ์—์„œ bindEvents๋งŒ delegate๋กœ ์ˆ˜์ •ํ•˜๋ฉด ๋œ๋‹ค.

handleClick(event) {
    const tab = event.target.dataset.tab;
    this.emit("@tabchange", { tab });
}
 
this.tabView
.on('@tabchange', (event) => {this.select(event.detail.tab);});
    
select(tab) {
    this.store.select(tab);
    this.tabView.show(this.store.selectedTab);
}

๊ทธ๋ฆฌ๊ณ  ์ถ”๊ฐ€๋กœ ๋ช…ํ™•ํžˆ ํ•ด์ฃผ์—ˆ๋‹ค. 

TabView์—์„œ click event๊ฐ€ tab change๋งŒ ์กด์žฌํ•˜์ง€ ์•Š๊ณ , ์ถ”๊ฐ€์ ์ธ event๊ฐ€ ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ๋‹ค.

-> event ๋ฐœํ–‰ ์‹œ ๋ช…ํ™•ํ•œ ์ด๋ฒคํŠธ ๋ช…๊ณผ ํ•„์š”ํ•œ ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์ง€๊ณ  ๋ฐœํ–‰ํ•œ๋‹ค.


Tip

class Template {
    getTabList() {
        return `
            <ul class="tabs">
                ${Object.values(TabType)
                .map(tabType => ({ tabType, tabLabel: TabLabel[tabType] }))
                .map(this._getTab)
                .join("")}
            </ul>
        `;
    }

    _getTab({tabType, tabLabel}) {
        return `
            <li data-tab=${tabType}>
                ${tabLabel}
            </li>
        `;
    }
}

 

๋ Œ๋”๋ง์„ ์œ„ํ•œ Template ํด๋ž˜์Šค์—์„œ _getTap method๋กœ map ํ•จ์ˆ˜๋ฅผ ๋Œ๋ฆฌ๋Š” ๊ณผ์ •์—์„œ $[tabLabel] ๋ฐฑํ‹ฑ ๋ถ€๋ถ„์ด ๊ณ„์†ํ•ด์„œ undefined๋กœ ์ถœ๋ ฅ๋๋‹ค.

์ด์œ ๋Š” js์—์„œ ์ค‘๊ด„ํ˜ธ, {}๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ํŒŒ๋ผ๋ฏธํ„ฐ ์ „๋‹ฌ์ด ์•„๋‹ˆ๊ณ  mapping์ด ๋˜๋Š” ๊ฒƒ์ด๋‹ค. ์ฆ‰, getTabList() ๋ฉ”์†Œ๋“œ์—์„œ ์ฒซ๋ฒˆ ์งธ map ๋ฉ”์†Œ๋“œ์—์„œ { tabType, tabLabel } ๋กœ ๋ฐ”๊พธ์—ˆ๊ณ , ๊ทธ ๋‹ค์Œ map ๋ฉ”์†Œ๋“œ์—์„œ _getTab์œผ๋กœ ๋ณ€ํ™˜ํ•˜๋Š” ์ค‘ tabLabel์ด tabLable๋กœ ์˜คํƒ€๊ฐ€ ๋ฐœ์ƒํ•ด์„œ ๋งคํ•‘์ด ๋˜์ง€ ์•Š๋Š” ๋ฌธ์ œ์˜€๋‹ค.

 

์ด ํŒ์€ handleClick ๋ฉ”์†Œ๋“œ์™€ ๊ฐ™์ด ์ž‘์„ฑํ•  ๋•Œ๋„ ์‹ ๊ฒฝ์จ์•ผํ•˜๋Š” ๋ถ€๋ถ„์ด๋‹ค.