🤔 목표 
이 펫 프로젝트의 목표는 다음과 같다.
- 최종적으로 라이트 한 프로젝트에 사용할 수준으로 제작한다.
 - 컴포넌트 정의 기능을 간단하게 만들 수 있어야 한다. 
- 아키텍처 레벨의 코드는 난이도가 높을 가능성이 크기 때문이다.
 
 - 컴포넌트 사용을 쉽게 사용 가능한 형태로 제작해야 한다.
 - 지원 기능 
- 상태 변경 시, 다시 렌더링 되는 기능
 - 부모-자식 관계를 맺을 수 있는 기능
 - 공유상태를 사용할 수 있는 기능
 
 
📄 컴포넌트 사용법 
기본 컴포넌트 
js
import {component} from './core/component.js';
export const BasicComponent = component(({html}) => {
  const render = () => {
    return html(`<div>
      <h2>Basic Component</h2>
      Hello World!
    </div>`);
  };
  return render
});component로 컴포넌트를 선언한다.component인자에 함수를 전달하는 데, 해당 함수를createComponent로 명명한다.createComponent함수는 항상 함수를 반환해야 하는 데. 해당 함수는render로 명명한다.createComponent는 첫번째 인자에html가 전달된다.html은template을 인자로 받아, DOM을 반환한다.
컴포넌트 내부 스토어 사용 
js
import {events, query} from './core/helper/dom.js';
import {component} from './core/component.js';
export const CounterComponent = component(({store, html}) => {
  const state = store.useState({
    count: 0,
  });
  const actions = {
    upCount: () => {
      state.count.set(state.count.get() + 1);
    },
    downCount: () => {
      state.count.set(state.count.get() - 1);
    }
  };
  const render = () => {
    const dom = html(`<div>
      <h2>Counter Component</h2>
      <button type="text" class="up">Up</button>
      <button type="text" class="down">Down</button>
      <div>${state.count.get()}</div>
    </div>`);
    events(query(dom, '.up'), {
      click: actions.upCount
    });
    events(query(dom, '.down'), {
      click: actions.downCount
    });
    return dom;
  };
  return render
});createComponent는 첫번째 인자에html과 함께,store가 전달된다.store는 컴포넌트 내부 스토어다.store.useState로 컴포넌트 상태를 등록한다.- 등록된 상태는 
get으로 조회,set으로 수정 할 수 있다. set이 실행되면render로 다시 렌더링한다.
- 등록된 상태는 
 - DOM 이벤트는 DOM API로 등록한다.
 
리스트 렌더링 
js
import {events, query} from './core/helper/dom.js';
import {component} from './core/component.js';
export const ListComponent = component(({store, html}) => {
  const state = store.useState({
    inputText: '',
    todoList: []
  });
  const actions = {
    addItem: () => {
      if (!state.inputText.get()) {
        return;
      }
      state.todoList.set([
        ...state.todoList.get(),
        state.inputText.get()
      ])
      state.inputText.set('', false);
    },
    changeInput: (inputText) => {
      // 렌더링을 하고 싶지 않을 때, set 두번째 인자에 false처리
      state.inputText.set(inputText, false);
    }
  };
  const render = () => {
    const dom = html(`<div>
      <h2>List Rendering</h2>
      <input type="text">
      <button type="button">Add</button>
      <ol>
       ${state.todoList.get().map((item) => {
          return `<li>${item}</li>`
        }).join('')}
      </ol>
    </div>`)
    const input = query(dom, 'input');
    const button = query(dom, 'button');
    events(button, {
      click: () => {
        actions.addItem();
        input.value = '';
      }
    });
    events(input, {
      input: (event) => {
        actions.changeInput(event.target.value)
      }
    });
    return dom;
  };
  return render;
});- 리스트 렌더링은 Array API를 사용한다.
 set함수의 두번째 인자에false를 전달하면, 해당 상태를 전파 하지 않는다.
컨디션 렌더링 
js
import {events, query} from './core/helper/dom.js';
import {component} from './core/component.js';
export const ConditionComponent = component(({store, html}) => {
  const state = store.useState({
    toggle: false,
  });
  const actions = {
    toggle: () => {
      state.toggle.set(!state.toggle.get())
    }
  };
  const render = () => {
    const dom = html(`<div>
      <h2>Condition Rendering</h2>
      <button type="button">Toggle</button>
      ${state.toggle.get() ? '<div>Hello World</div>' : ''}
    </div>`);
    events(query(dom, 'button'), {
      click: actions.toggle
    });
    return dom;
  };
  return render
});- 컨디션 렌디링은 연산자를 사용한다.
 
부모-자식 관계 
js
import {events, query, replaceWith} from './core/helper/dom.js';
import {component} from './core/component.js';
export const ParentButton = component(({html, store}) => {
  const state = store.useState({
    count: 0
  });
  const actions = {
    upCount: () => {
      state.count.set(state.count.get() + 1)
    }
  };
  const render = () => {
    const dom = html(`<div>
      <h2>Parent-Child</h2>
      <div>${state.count.get()}</div>
      <child-button></child-button>
     </div>`);
    
    const props = {
      buttonName: 'Up Count'
    };
    const emit = {
      upCount: actions.upCount
    };
    replaceWith(
      query(dom, 'child-button'),
      ChildButton({props, emit})
    );
    return dom;
  };
  return render;
});
export const ChildButton = component(({html}, {props, emit}) => {
  const render = () => {
    const dom = html(`<div>
       <button type="button">${props.buttonName}</button>
     </div>`);
    events(query(dom, 'button'), {
      click: emit.upCount
    });
    return dom;
  };
  return render;
});- 자식 컴포넌트는 
replaceWith으로 DOM을 변경한다. - 자식 컴포넌트에 
{props, emit}을 전달할 수 있다.props는 자식에게 전달할 상태를 담는다.emit은 자식에게 전달 받을 함수를 담는다.
 - 자식 컴포넌트에 전달된 
{props, emit}은createComponent의 두번째 인자로 전달된다. props,emit자료구조는 강제성이 없으나, 인자 네이밍은 강제한다.
공유 상태 사용 
js
import {events, query, replaceWith} from './core/helper/dom.js';
import {createStore} from './core/store.js';
import {component} from './core/component.js';
const sharedStore = createStore();
const sharedState = sharedStore.useState({
  count: 0
});
export const CounterButton = component(({html, store}) => {
  store.share(sharedStore);
  const actions = {
    upCount: () => {
      sharedState.count.set(sharedState.count.get() + 1)
    }
  };
  const render = () => {
    const dom = html(`<div>
       <button type="button">Up Count</button>
     </div>`);
    events(query(dom, 'button'), {
      click: actions.upCount
    });
    return dom;
  };
  return render;
});
export const MainComponent = component(({html, store}) => {
  store.share(sharedStore);
  return () => {
    const dom = html(`<div>
      <h2>Shared State</h2>
      ${sharedState.count.get()}
      <counter-button1></counter-button1>
      <counter-button2></counter-button2>
    </div>`);
    replaceWith(
      query(dom, 'counter-button1'),
      CounterButton()
    );
    replaceWith(
      query(dom, 'counter-button2'),
      CounterButton()
    );
    return dom;
  }
});- 공유 상태를 사용하려면, 
store.share로 스토어를 등록한다. - 등록된 스토어를 바로 사용하지 않고, 공유 상태를 사용한다.
 
마운트 
js
import {append} from './core/helper/dom.js';
import {BasicComponent} from './BasicComponent.js';
import {CounterComponent} from './CounterComponent.js';
import {ListComponent} from './ListComponent.js';
import {ConditionComponent} from './ConditionComponent.js';
import {ParentButton} from './ParentChild.js';
import {MainComponent} from './SharedState.js';
const app = document.querySelector('#app');
append(app, BasicComponent());
append(app, CounterComponent());
append(app, ListComponent());
append(app, ConditionComponent());
append(app, ParentButton());
append(app, MainComponent());- 컴포넌트 함수의 반환값은 DOM임으로 
appendChild로 마운트한다. 
💻 데모 
ESM를 지원하는 브라우저에서만 동작함
📄 코어 코드 
헬퍼 
mapValues 
js
export const mapValues = (obj, f) => Object
  .entries(obj)
  .map(([k, v]) => ({[k]: f(v)}))
  .reduce((acc, obj) => Object.assign(acc, obj));- Object의 값을 변경해주는 map 함수다.
 
html 
js
export const html = (template) => {
  const body = document.createElement('body');
  body.innerHTML = template;
  if (body.childNodes.length > 1) {
    console.warn('Wrapper 태그 필요!')
  }
  return body.childNodes[0];
};- 템플릿을 DOM으로 변환하는 역할을 한다.
 
DOM 
js
export const append = (parentNode, childNode) => {
  parentNode.appendChild(childNode)
};
export const query = (parentNode, selector) => {
  return parentNode.querySelector(selector)
};
export const events = (node, options) => {
  Object
    .entries(options)
    .forEach(([eventName, listener]) => {
      node.addEventListener(eventName, listener)
    })
};
export const replaceWith = (oldNode, newNode) => {
  oldNode.replaceWith(newNode)
};- DOM API 헬퍼 역할을 한다.
 
observer 
js
export const createSubject = () => {
  const set = new Set();
  return {
    notify: (value) => {
      set.forEach(observer => observer(value))
    },
    subscribe: (observer) => {
      set.add(observer)
    },
    unsubscribe: (observer) => {
      set.remove(observer)
    }
  };
};- 옵져버 패턴을 구현한 함수다.
 
스토어 
js
import {createSubject} from './helper/observer.js';
import {mapValues} from './helper/map-values.js';
export const createStore = () => {
  const subject = createSubject();
  const useState = (state = {}) => {
    return mapValues(state, (value) => {
      const get = () => value;
      const set = (newValue, shouldNotify = true) => {
        value = newValue;
        shouldNotify && subject.notify()
      };
      return {get, set}
    });
  };
  const share = (store) => {
    store._subscribe(() => {
      subject.notify();
    })
  };
  return {
    useState,
    share,
    _subscribe: subject.subscribe,
    _unsubscribe: subject.unsubscribe,
  }
};createStore실행 시,subject를 생성한다.useState로 상태를 등록한다.share로 스토어의 상태변경전파를 공유한다._subscribe,_unsubscribe는 코어레벨에서 사용된다.
컴포넌트 
js
import {html} from './helper/html.js';
import {createStore} from './store.js';
export const component = (createComponent) => {
  return ({props, emit} = {props: null, emit: null}) => {
    const store = createStore();
    const render = createComponent({html, store}, {props, emit});
    const state = {
      dom: null
    };
    const observer = () => {
      const newDom = render();
      state.dom && state.dom.replaceWith(newDom);
      state.dom = newDom;
    };
    store._subscribe(observer);
    observer();
    return state.dom;
  };
};createComponent를 인자로 받는다.createComponent를 실행하여render를 반환 받는다.render를 최초에 실행 후 반환한다.store를 감시하며, 상태 전파을 받으면render를 재실행하고 DOM을 교체한다.