Web Component 상태관리 만들기 (Vanillajs)

Web Component를 통해서 Vanillajs 기반 웹서비스를 만드는 방법을 배웠다.

2021.08.16 - [Web] - 기존 web site를 components 로 다시 만들기 (No State management)

이제 Statement를 통해서 Component를 관리 하는 방법을 알아보고자 한다.

본 내용은 위의 링크 내용의 연장이다.

관련 내용을 더 이야기 하기 전에 MVVM이란 무었인지 꼭 이해하길 바란다.

2021.08.20 - [Web] - MVC, MVP, MVVM 패턴의 이해

가장 유명한 Statement 관리 library는 redux이다. 

redux를 통해 어떤식으로 Statement가 관리 되는지 알아보자.

https://redux.js.org/tutorials/fundamentals/part-2-concepts-data-flow

  • UI에서 Event가 Dispatch 되면 Action을 발생시킨다.
  • Action은 해당 Action과 연결된 reducer를 작동시킨다.
  • Reducer는 State를 update 시킨다.
  • update된 State는 연결된 UI를 rendering 시킨다.

이게 redux의 전체적인 data flow이다.

이 data flow를 아래 참고했던 url의 내용과 비교해보자.

https://css-tricks.com/build-a-state-management-system-with-vanilla-javascript/

큰 작동의 원리는 이름을 제외하고는 동일하다고 볼 수 있다.

그럼 위의 참조 링크내용을 바탕으로 Web component에서 Vanilla로 State management를 만들어 보도록하자.

 

 

Mediator

Publish/Subscribe 라고 하는 이름으로 많이 알려진 패턴이다.

어떤 채널에 다수개의 Subscriber가 있을 때, 해당 채널로 하나의 Publisher가 Data를 발행하면 Subscriber들은 해당 Data를 무조건 받는 방식이다.

Subscriber는 Publisher를 알 수 없고, Subscriber는 Publisher를 알 수 없다.

Observer 패턴에서는 Publisher를 Observable, Subscriber를 Observer라고 부른다.

상기 내용을 간단히 만든 코드가 아래와 같다.

export default class Mediator {
    constructor(events = {}){
        this.events = events;
    }

    subscribe(key, callback){
        if(!this.events.hasOwnProperty(key)){
            this.events[key] = new Array();
        }

        this.events[key].push(callback);
    }

    publish(key, params = {}){
        if(!this.events.hasOwnProperty(key)) return [];

        return this.events[key].map(callback => {
            callback(params);
        });
    }
}

위의 코드에서 events는 channel 명이라고 생각하면 된다. subscribe는 events에 특정키에 Publish가 발생하면 수신을 받을 Subscriber 의 명시적 행위이다.

코드와 그림사이의 이름이 달라서 이해하기 어려울 수 있기때문에, 그림을 코드에 맞추어 보았다.

  • subscribe에서 key명으로 callback을 등록하고
  • publish에서 key명으로 callback을 call한다

 

Store

이제 Store를 만들어 보자. Store는 많은 일을 하게 된다.

앞서서는 각 Component에서 State 또는 Data에 대한 수정 대응 행위를 했다고 하면 이제부터는 Store에서만 해당 행위가 가능해야 한다.

  • Action을 수신 받을 수 있어야 한다. 
  • 받은 Action에 따라서 실제 Action을 실행 할 수 있어야 한다.
  • Action이 완료된 이후 Data의 수정을 요청(Commit) 받을 수 있어야 한다.
  • 요청 받은 Commit에 따라서 Mutation을 실행 할 수 있어야 한다.
  • Mutation으로 인해 수정된 State를 관련된 Component에게 통보하여야 한다.

다소 복잡한데 이를 그림으로 보면 아래와 같다.

위의 그림은 Component가 각각 ADD_ITEM, DELETE_ITEM Action을 CALL하고 Strore에서 이에 관련있는 Action을 찾아 Dispatch하고 Action은 Business Logic을 수행하고 Mutation에 Commit함으로써 State를 최종적으로 Update하게 된다.

주의할점은 State의 Update는 반듯이 Mutation이 완료된이 후 Store를 통해서 이루어 진다는 것이다.

이를 관리하기 위해서 Store의 상태를 관리해야 한다.

위의 붉은 색을 보면 Action이 이러나기 전의 store상태는 resting, Action중은 action, Mutation은 mutation그리고 Commit은 store가 mutation인 상태에서만 가능하게 만들면 된다.

자이제 필요한 내용들을 같이 개발해 보자.

import PubSub from "./pubsub.js";

export default class Store {
    constructor(params){
        this.actions = {};
        this.mutations = {};
        this.state = {};
        this.status = 'resting';
        this.events = new PubSub();
    }
};

Store에 필요한 것은 

  • actions : Action을 담을 공간
  • mutations : Mutation을 담을 공간
  • state : 본 서비스 전체에서 사용될 모든 값
  • status : Store의 현재 상태
  • events : Subcriber를 한 components들에게 state가 업데이트 되었을 때 Event를 공지하는 역할

이 필요하다.

import PubSub from "./pubsub.js";

export default class Store {
    constructor(params){
        this.actions = {};
        this.mutations = {};
        this.state = {};
        this.status = 'resting';
        this.events = new PubSub();

        if(params.hasOwnProperty('actions')){
            this.actions = params.actions;
        }
        if(params.hasOwnProperty('mutations')){
            this.mutations = params.mutations;
        }

        const that = this;

        this.state = new Proxy(
            (params.state || {}), {
                set : function(state, key, value){
                    state[key] = value;
                    console.log(`stateChange: ${key}: ${value}`);
                    that.events.publish('stateChange',that.state);
                    that.status = 'resting';
                    return true;
                }
            }
        );
    }

};

actions와 mutations는 

        if(params.hasOwnProperty('actions')){
            this.actions = params.actions;
        }
        if(params.hasOwnProperty('mutations')){
            this.mutations = params.mutations;
        }

바로 입력해 주면 된다.

어려운 부분은 state에 대한 부분이다.

        this.state = new Proxy(
            (params.state || {}), {
                set : function(state, key, value){
                    state[key] = value;
                    console.log(`stateChange: ${key}: ${value}`);
                    that.events.publish('stateChange',that.state);
                    that.status = 'resting';
                    return true;
                }
            }
        );

여기서 Proxy를 사용했는데, 

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy

기본적인 기능은 java의 refection에 있는 dynamic proxy와 비슷한 목적으로 갖고 있다.

우리가 일반적인 개발을 할때, this.state에 어떤값을 assign하거나 get을 할 때 바로 "="을 사용하던가, 그냥 "this.state.items" 이런식으로 retrive를 하면 된다. 그런데 그사이에 끼어들어서 무언가를 더 하고 싶을때 Proxy라고 하는 기능이 활용된다.

Java의 Spring에서는 Point cut이라는 개념과 비슷하다. 

위의 코드를 다시 보자면 이런 의미다.

  • this.state에 어떤 값을 assign하면 state의 값을 update하고
  • stateChange라고 하는 Log를 콘솔에 찍고,
  • events에 stateChange라는 event를 publish하고 store의 상태를 resting으로 변환하라.

앞서서 Store에서 만들어야 하는 기능 중 하나가 위의 내용으로 해결이 되었다.

  • Action을 수신 받을 수 있어야 한다. 
  • 받은 Action에 따라서 실제 Action을 실행 할 수 있어야 한다.
  • Action이 완료된 이후 Data의 수정을 요청(Commit) 받을 수 있어야 한다.
  • 요청 받은 Commit에 따라서 Mutation을 실행 할 수 있어야 한다.
  • Mutation으로 인해 수정된 State를 관련된 Component에게 통보하여야 한다.

이제 수신받는 부분과 Action을 실행하는 부분을 보자.

import PubSub from "./pubsub.js";

export default class Store {
    constructor(params){
        this.actions = {};
        this.mutations = {};
        this.state = {};
        this.status = 'resting';
        this.events = new PubSub();

        if(params.hasOwnProperty('actions')){
            this.actions = params.actions;
        }
        if(params.hasOwnProperty('mutations')){
            this.mutations = params.mutations;
        }

        const that = this;

        this.state = new Proxy(
            (params.state || {}), {
                set : function(state, key, value){
                    state[key] = value;
                    console.log(`stateChange: ${key}: ${value}`);
                    that.events.publish('stateChange',that.state);
                    that.status = 'resting';
                    return true;
                }
            }
        );
    }

    dispatch(actionKey, payload){
        if(typeof this.actions[actionKey] !== 'function'){
            console.error(`Action ${actionKey} does not exist`);
            return false;
        }

        this.status = 'action';
        this.actions[actionKey](this,payload);
        return true;
    }

};

 

dispatch이다. component는 어떤 Action을 요청할 것이지를 action key로 요청을 하고 해당 action key에 관련 data를 같이 넘겨주면 action을 실행 하는 형태이다. 물론 action을 실행하면서 status를 action으로 변경하는 것을 잊으면 안된다.

그럼 action은 어떻게 생겼을까?

export default {
    "ADD_ITEM" : function(context, params){
        context.commit("addItem", params);
    },
    "DELETE_ITEM" : function(context, params){
        context.commit("deleteItem", params);
    }
}

일반적으로 Action은 대문자로 표현된다. 각 Action은 독립적으로 어떤 행위를 하고 최종적으로 this.state르 업데이트해달라고 요청하게 된다. 이 요청을 우리는 commit이라고 부른다.

위에서 보자면 하나의 action에 하나의 commit이 발생하는데, 이건 경우에 따라서 1:n의 commit이 발생 할 수 있다.

import Mediator from "./Mediator.js";

export default class Store {
    constructor(state = {}, actions, mutations){
        let that = this;
        this.actions = actions;
        this.mutations = mutations;
        this.events = new Mediator();
        this.status = 'resting'; // 'resting' 'action' 'mutation'
        this.state = new Proxy(state,{
                get: function(target,prop,recever){
                    return target[prop]?? null;
                },
                set: function(state, key, value){
                    state[key] = value;
                    //TODO : 해당 state 에 연결된 화면 render필요
                    that.events.publish('stateChange',state);
                    return true;
                }
            }
        );
    }

    dispatch(actionKey, prams){ //Component -> Action Dispatch
        if(!this.actions.hasOwnProperty(actionKey)){
            console.error(`ActionKey ${actionKey} does not exist`);
            return false;
        }
        this.status = 'action';
        return this.actions[actionKey](this, prams);
    }

    commit(mutationKey, prams){ //Action -> mutation commit -> proxy trigger
        if(!this.mutations.hasOwnProperty(mutationKey)){
            console.error(`MutationKey ${mutationKey} does not exist`);
            return false;
        }
        this.status = 'mutation';
        let newState = this.mutations[mutationKey](this.state, prams); //action은 state를 return 해야한다.

        Object.assign(this.state, newState);
        return true;
    }

}

Store의 마지막 코드인 commit이다. 

Action과 비슷한 코드이다. 다른점이 있다면 실행된 결과값을 this.state로 반환시키고 이것으로 Object.assign함으로써 위의 Proxy 모듈이 작동 되도록 만든것이 다르다면 다르다.

그럼 mutations는 어떻게 생긴 코드일까?

export default {
    addItem : function(state,item){
        state.items.push(item);
        return state;
    }, 
    deleteItem : function(state,index){
        state.items.splice(index,1);
        return state;
    }
}

 

state값에 items를 push하거나 splice하는 방식으로 state의 값을 변화 시킨다.

여기서 의문이 하나 생긴다. Proxy는 어떠한 set이나 get이 발생할 경우 모두 catch할 수있다. 그리고 Proxy Object도 Object임으로 Memory Address를 arguments에 Passing 할 것이다.
그래서 Object.assign이 없다고 해도 mutations내에서 state의 형태를 변화 시키면서 Proxy의 set이 발생할 것이라고 예상했지만 그렇지 않다.
정확한 사유는 모르겠지만 this Scope를 잃는 순간 Proxy는 더이상 작동이 안되는 것 같다.
나중에 사유를 알게 되면 더 작성해 보겠다.

 

위의 내용까지 해서 Store에 대한 작동은 모두 알아 보았다. 이제 component에 해당 행위를 이식하기만 하면 된다.

  • Action을 수신 받을 수 있어야 한다. 
  • 받은 Action에 따라서 실제 Action을 실행 할 수 있어야 한다.
  • Action이 완료된 이후 Data의 수정을 요청(Commit) 받을 수 있어야 한다.
  • 요청 받은 Commit에 따라서 Mutation을 실행 할 수 있어야 한다.
  • Mutation으로 인해 수정된 State를 관련된 Component에게 통보하여야 한다.

 

Store를 다 만들었다. 이제 Store에 들어가야할 state와 actions 그리고 mutations의 Instance를 만들자.

import Store from "./Store.js"
import mutations from "../props/mutations.js"
import actions from "../props/actions.js"
import state from "../props/state.js";

export default new Store(state, actions, mutations);

이 인스턴스는 본 서비스 전반에서 사용되는 서비스가 될 것이다.

Parameters로 Passing되지 않고 Singletone으로 단하나의 Store로 작동 된다는 것을 꼭 이해해야 한다.

 

App Input 수정

앞서 만들어 놓은 AppInput 파일을 리뷰해 보자.

export default class AppInput extends HTMLElement {
    constructor(){
        super();
        this.items = new Array();

        this.render();
    }

    render(){
        this.innerHTML = this.getTemplate();
    }

    getTemplate(){
        return `
            <h2 class="app__heading">What you've done</h2>
            <div class="js-items" aria-live="polite" aria-label="A list of items you have done"></div>
            <form class="[ new-item ] [ boilerform ] [ js-form ]">
                <div class="boilerform">
                    <!-- Form styles from the https://boilerform.design boilerplate -->
                    <label for="new-item-field" class="[ new-item__label ] [ c-label ]">Add a new item</label>
                    <input type="text" class="[ new-item__details ] [ c-input-field ]" id="new-item-field" autocomplete="off" />
                    <button class="[ c-button ] [ new-item__button ]">Save</button>
                </div>
            </form>
        `;
    }

    connectedCallback(){
        let that = this;
        this.querySelector('form').addEventListener('submit',(e)=> {
            e.preventDefault();
            that.addItem();
        });
    }
    
    addItem(){
        let item = this.querySelector('#new-item-field').value.trim();
        this.items.push(item);
        this.dispatchItems();
        this.renderList();
    }

    renderList(){
        let that = this;
        this.querySelector('.js-items').innerHTML = `
            <ul>
                ${this.items.map((item,idx)=> `<li> ${item} <button class="rm" aria-label="Delete this item">×</button></li>`).join('')}
            </ul>
        `;

        this.querySelectorAll('.rm').forEach((btn,idx)=>{
            btn.addEventListener('click',(e)=>{
                that.items.splice(idx, 1);
                that.renderList();
                that.dispatchItems();
            });
        });
    }

    dispatchItems(){
        this.dispatchEvent(new CustomEvent('changed',{
            detail : {data : this.items.length},
            bubbles : true
        }));
    }

    disconnectedCallback(){

    }

    static get observedAttributes(){
        return [];
    }

    attributeChangedCallback(name, oldValue, newValue){

    }
};

customElements.get('app-input')?? customElements.define('app-input',AppInput);

여기서 주요하게 사용되는 기능은 2가지이다.

  • addItem : 새로운 Todo 를 추가한다.
    addItem(){
        let item = this.querySelector('#new-item-field').value.trim();
        this.items.push(item);
        this.dispatchItems();
        this.renderList();
    }
  • deleteItem : click이 된 Todo를 삭제한다.
        this.querySelectorAll('.rm').forEach((btn,idx)=>{
            btn.addEventListener('click',(e)=>{
                that.items.splice(idx, 1);
                that.renderList();
                that.dispatchItems();
            });
        });

 

이 두가지를 Store를 이용해서 기능 변경을 하도록 하겠다.

 

Add Item & Delete Item Action 연결

addItem에 대한 Action과 그 값을 commit하는 Mutation에 대해서 알아보자.

//actions.js
export default {
    "ADD_ITEM" : function(context, params){
        context.commit("addItem", params);
    },
    "DELETE_ITEM" : function(context, params){
        context.commit("deleteItem", params);
    }
}

actions.js에 위와 같이 2가지 Action을 정의하자.

두개 모두 별다른 business 로직이 필요로 하진 않는다.

이제 Item의 Add와 Item의 삭제를 작동시키는 부분을 만들자.

//mutations.js
export default {
    addItem : function(state,item){
        state.items.push(item);
        return state;
    }, 
    deleteItem : function(state,index){
        state.items.splice(index,1);
        return state;
    }
}

 mutations.js이다. 앞서 action에서부터 넘어온 item 정보를 받아서 기존 state에 추가하거나. 삭제하는 역할을 한다.

위의 행위를 기반으로 기존 코드를 변경해 보자.

// 기존 소스 AppInput.js    
    
    connectedCallback(){
        let that = this;
        this.querySelector('form').addEventListener('submit',(e)=> {
            e.preventDefault();
            that.addItem();
        });
    }
    
    addItem(){
        let item = this.querySelector('#new-item-field').value.trim();
        this.items.push(item);
        this.dispatchItems();
        this.renderList();
    }

기존 소스에서 addItem하는 부분을 Store의 Action을 Call하는 것으로 변경해보자.

//수정된 소스
	connectedCallback(){
        let that = this;
        this.querySelector('form').addEventListener('submit',(e)=> {
            e.preventDefault();
            that.addItemWithStore(); //for state management
        });
    }
    
    addItemWithStore(){
        let item = this.querySelector('#new-item-field').value.trim();   
        StoreInstance.dispatch("ADD_ITEM", item);
    }

기존과 달라진 점은 기존에는 Items의 Data를 AppInput내에서 직접 관리했다고 하면, Store를 이용하면 더 이상 Items를 직접 관리할 수 없다.

이점이 가능 큰 차이점이다.

그래서 위의 수정사항도 보면 new-item-field에서 item의 data만 갖어올뿐이지 state.items를 직접 control 하지 못한다.

그럼 state는 어떻게 생겼을까?

아래는 state.js이다.

export default {
    items : ['my first','status management'],
}

관리하고자 하는 Data나 state는 모두 이 값을 initial value로 작동한다.

비록 값이 없다고 하더라도, 나중에 유지보수가 안될 수 있음으로 여기에 모든 key를 등록해 주자.

 

Delete code도 마저 수정해 주자.

//기존 AppInput.js
    renderList(){
        let that = this;
        this.querySelector('.js-items').innerHTML = `
            <ul>
                ${this.items.map((item,idx)=> `<li> ${item} <button class="rm" aria-label="Delete this item">×</button></li>`).join('')}
            </ul>
        `;

        this.querySelectorAll('.rm').forEach((btn,idx)=>{
            btn.addEventListener('click',(e)=>{
                that.items.splice(idx, 1);
                that.renderList();
                that.dispatchItems();
            });
        });
    }

    dispatchItems(){
        this.dispatchEvent(new CustomEvent('changed',{
            detail : {data : this.items.length},
            bubbles : true
        }));
    }

기존 AppInput.js를 보면 Redering을 완료한 이후 rendering된 button에 click event를 통해서 어떤 Todo가 삭제 되어야 하는지 알아내는 방식이었다.

위를 보면 자신이 갖고 있는 items에서 splice를 통해서 직접 Todo를 삭제 하는 모습을 볼 수 있다.

                that.items.splice(idx, 1);

이 부분을 모두 Store의 Action으로 형태를 변경해 주자.

//변경된 코드

    renderList(){
        let that = this;
        const list = `
            <ul>
                ${this.store.state.items.map((item,idx)=> `<li> ${item} <button class="rm" aria-label="Delete this item">×</button></li>`).join('')}
            </ul>
        `;

        this.querySelector('.js-items').innerHTML = list;

        this.querySelectorAll('.rm').forEach((btn,idx)=>{
            btn.addEventListener('click',(e)=>{
                that.dispatchItemsWithStore(idx);
            });
        });
    }

    dispatchItemsWithStore(idx){
        this.store.dispatch("DELETE_ITEM",idx);
    }

store에 dispatch를 통해서 Action을 요청한 모습을 확인 할 수있다.

이제 Action이 발생하고 나면 나타날 작동 순서를 생각해 보자.

AppInput component에서 dispatch를 발생시키면 순서에 따라서 action과 commit 그리고 events가 발생하게 된다.

events가 발생하면 어떤 현상이 일어나야 할까?

바로 해당 events를 Observe하고 있는 Components들은 모두 해당 state의 변화에 따라서 Rendering이 일어나야 한다.

 

Component에 Event 등록하기

이제 state의 값이 변경되면 Redering이 일어날 수 있도록 각 Component를 store의 event에 등록하자.

Channel명은 'stateChange'로 일괄로 통일 시키자.

서비스가 커지면 특정 state의 수정상태만 monitoring해서 처리하도록 고도화 되어야 하지만, 

이정도까지만 개발 했으면 된거 아닐까? 이제부터는 그냥 Redux 쓰는 걸로 하자.... ㅎㅎㅎ

AppStatus.js event 등록

import StoreInstance from "../statem/StoreInstance.js";

export default class AppStatus extends HTMLElement {
    constructor(){
        super();

        this.store = StoreInstance;
        this.store.events.subscribe('stateChange',this.render.bind(this));

        this.render();
    }

    render(){
        this.innerHTML = this.getTemplate();
    }

    getTemplate(){
        return `
            <aside class="app__status">
            <p role="status" class="visually-hidden">You have done <span class="js-status">1 thing</span> today!</p>
            <div class="[ app__decor ] [ js-count ]" aria-hidden="true">
                <small>You've done</small>
                <span>${this.store.state.items.length}</span>
                <small>things today 😢</small>
            </div>
            </aside>
        `;
    }

    connectedCallback(){

    }

    disconnectedCallback(){

    }

    static get observedAttributes(){
        // return ['item-count'];
    }

    attributeChangedCallback(name, oldValue, newValue){
        // if(name = 'item-count'){
        //     this.count = newValue;
        //     this.render();
        // }

    }
};

customElements.get('app-status')?? customElements.define('app-status',AppStatus);

위코드에서 기존과 바뀐점은

    constructor(){
        super();

        this.store = StoreInstance;
        this.store.events.subscribe('stateChange',this.render.bind(this));

우선 store의 stateChange를 subscribe 했다. 이제 해당 이벤트가 오면 render Method가 불리워 질것이다.

    getTemplate(){
        return `
            <aside class="app__status">
            <p role="status" class="visually-hidden">You have done <span class="js-status">1 thing</span> today!</p>
            <div class="[ app__decor ] [ js-count ]" aria-hidden="true">
                <small>You've done</small>
                <span>${this.store.state.items.length}</span>
                <small>things today 😢</small>
            </div>
            </aside>
        `;

Render Method는 getTemplate를 Call하게 되는데

기존과 다른점은 this.store.state.itmes를 직접 접근 하고 있다는 것이다.

    static get observedAttributes(){
        // return ['item-count'];
    }

    attributeChangedCallback(name, oldValue, newValue){
        // if(name = 'item-count'){
        //     this.count = newValue;
        //     this.render();
        // }

    }

앞서 item count를 갖고 오기위해 attribute를 oberve했던 부분은 삭제해준다.

 

AppInput.js event 등록하기

import StoreInstance from "../statem/StoreInstance.js";
export default class AppInput extends HTMLElement {
    constructor(){
        super();
        this.store = StoreInstance;
        this.store.events.subscribe('stateChange',this.renderList.bind(this));

        this.render();
        this.renderList();
    }

    render(){

        this.innerHTML = this.getTemplate();
    }

    getTemplate(){
        return `
            <h2 class="app__heading">What you've done</h2>
            <div class="js-items" aria-live="polite" aria-label="A list of items you have done">
            </div>
            <form class="[ new-item ] [ boilerform ] [ js-form ]">
                <div class="boilerform">
                    <!-- Form styles from the https://boilerform.design boilerplate -->
                    <label for="new-item-field" class="[ new-item__label ] [ c-label ]">Add a new item</label>
                    <input type="text" class="[ new-item__details ] [ c-input-field ]" id="new-item-field" autocomplete="off" />
                    <button class="[ c-button ] [ new-item__button ]">Save</button>
                </div>
            </form>
        `;
    }

    connectedCallback(){
        let that = this;
        this.querySelector('form').addEventListener('submit',(e)=> {
            e.preventDefault();
            that.addItemWithStore(); //for state management
        });
    }
    
    addItemWithStore(){
        let item = this.querySelector('#new-item-field').value.trim();   
        StoreInstance.dispatch("ADD_ITEM", item);
    }

    renderList(){
        let that = this;
        const list = `
            <ul>
                ${this.store.state.items.map((item,idx)=> `<li> ${item} <button class="rm" aria-label="Delete this item">×</button></li>`).join('')}
            </ul>
        `;

        this.querySelector('.js-items').innerHTML = list;

        this.querySelectorAll('.rm').forEach((btn,idx)=>{
            btn.addEventListener('click',(e)=>{
                that.dispatchItemsWithStore(idx);
            });
        });
    }

    dispatchItemsWithStore(idx){
        this.store.dispatch("DELETE_ITEM",idx);
    }

    disconnectedCallback(){

    }

    static get observedAttributes(){
        return [];
    }

    attributeChangedCallback(name, oldValue, newValue){

    }
};

customElements.get('app-input')?? customElements.define('app-input',AppInput);

기존과 달리지는 부분은 

this.store의 events에 역시 subscribe하는 부분이다.

앞서 AppState.js와 다른 부분은 render를 callback으로 처리하지 않고 renderList를 별도로 만들어 줬다.

그 이유는 form부분까지 redering이 불필요하게 일어나기 때문이다.

이점을 꼭 생각해 주자.

    constructor(){
        super();
        this.store = StoreInstance;
        this.store.events.subscribe('stateChange',this.renderList.bind(this));

renderList에서는 this.store.state.itmes를 역시 직접 연동해서 Data를 갖어온다.

    renderList(){
        let that = this;
        const list = `
            <ul>
                ${this.store.state.items.map((item,idx)=> `<li> ${item} <button class="rm" aria-label="Delete this item">×</button></li>`).join('')}
            </ul>
        `;

        this.querySelector('.js-items').innerHTML = list;

        this.querySelectorAll('.rm').forEach((btn,idx)=>{
            btn.addEventListener('click',(e)=>{
                that.dispatchItemsWithStore(idx);
            });
        });
    }

 

무언가 장황하게 

  • 일반 서비스 -> Web Component화 -> StateManagement 기능 추가

까지 완료 하였다.

나름 쉽게 작성해보려고 노력했지만, 내 능력이 부족해서 쉽게 설명이 잘 안된거 같다.

하지만 이를 통해 꼭 기억해야 할 것은 아래 남겨둔다.

 

  • Component에서 Data를 관리하지 않는다. Store를 통해서 Data(state)를 주입받는다.
  • Action은 component가 하지 않는다. 모든 Action은 Store에 위임한다.
  • Action은 State자체를 수정하지 않는다. 수정하기 전에 필요한 행위를 할 뿐이다.
  • Mutation을 통해 State의 값을 수정한다.
    • 그러나 Mutation이 최종적인 Commit은 아니다.
  • Store를 통해서 state는 최종적으로 update된다.
  • update된 state는 Mediator를 통해서 Subscribe하고 있는 Component들에게 변화 되었음을 알려준다.
  • state의 변화를 받은 Component는 즉시 redering을 다시 처리한다.
    • 단, 부분영역 rendering이 필요할 경우 주의 하자

 

 

마지막으로 MutationObserver 등과 같은 Web Component기능에 대해서는 시간날때 더 추가해서 작성하겠다.

branch 이름이 꼬였지만 위의 코드들이 있는 위치는 아래와 같다.

https://github.com/theyoung/webcomponents/tree/webComOnly/statementforwebcomponent

누군가에게는 도움이 되길...

728x90
반응형