코드스피츠 86 객체지향 자바스크립트 - 2회차 part3 (step 31)

KHW·2021년 3월 19일
0

js-study

목록 보기
18/39
post-custom-banner

MVVM 적용해보기

  1. ViewModel
  2. binder
  3. scanner
  4. client

1. ViewModel

const ViewModel = class {
  static #private = Symbol();	// #을 통해 private로 외부에서 접근불가

  static get(data) {
    return new ViewModel(this.#private, data);	//this는 클래스를 나타냄
  }

  styles = {};
  attributes = {};
  properties = {};
  events = {};

  constructor(checker, data) {
    if (checker != ViewModel.#private) throw 'useViewModel.get()!';
    Object.entries(data).forEach(([k, v]) => {
      switch (k) {
        case'styles':
          this.styles = v;
          break;
        case'attributes':
          this.attributes = v;
          break;
        case'properties':
          this.properties = v;
          break;
        case'events':
          this.events = v;
          break;
        default:
          this[k] = v;
      }
    }); 
    Object.seal(this);
  }
};

2. Binder

const BinderItem = class {
  el;
  viewmodel;

  constructor(el, viewmodel, _0 = type(el, HTMLElement), _1 = type(viewmodel, 'string')) {
    this.el = el;
    this.viewmodel = viewmodel;
    Object.freeze(this);  // 불변객체화
  }
};
const Binder = class {
  #items = new Set;

  add(v, _ = type(v, BinderItem)) {
    this.#items.add(v);
  }

  render(viewmodel, _ = type(viewmodel, ViewModel)) {
    this.#items.forEach(item => {
      const vm = type(viewmodel[item.viewmodel], ViewModel), el = item.el;
      Object.entries(vm.styles).forEach(([k, v]) => el.style[k] = v);
      Object.entries(vm.attributes).forEach(([k, v]) => el.setAttribute(k, v));
      Object.entries(vm.properties).forEach(([k, v]) => el[k] = v);
      Object.entries(vm.events).forEach(([k, v]) => el['on' + k] = e => v.call(el, e, viewmodel)); // this 를 el 로 바인딩
    });
  }
};

해당 Binder에서 필요한 기능을 추가하거나 처리하는 기능을 수행한다. (제어역전)


3. Scanner

const Scanner = class {
  scan(el, _ = type(el, HTMLElement)) {
    const binder = newBinder;
    this.checkItem(binder, el);

    const stack = [el.firstElementChild];
    let target;   // 임시변수
    while (target = stack.pop()) {
      this.checkItem(binder, target);
      if (target.firstElementChild) stack.push(target.firstElementChild);
      if (target.nextElementSibling) stack.push(target.nextElementSibling);
    }
    return binder;
  }

  checkItem(binder, el) {
    const vm = el.getAttribute('data-viewmodel');
    if (vm) binder.add(new BinderItem(el, vm));
  }
};

4. Client

const viewmodel = ViewModel.get({
  wrapper: ViewModel.get({styles: {width: '50%', background: '#ffa', cursor: 'pointer'}}),
  title: ViewModel.get({properties: {innerHTML: 'Title'}}),
  contents: ViewModel.get({properties: {innerHTML: 'Contents'}}),
});

const scanner = new Scanner;
const binder = scanner.scan(document.querySelector('#target'));
binder.render(viewmodel);

Client 변형

const viewmodel = ViewModel.get({
  isStop: false,
  changeContents() {
    this.wrapper.styles.background = `rgb(${parseInt(Math.random() * 150) + 100},${...},${...})`;
    this.contents.properties.innerHTML = Math.random().toString(16).replace('.', '');
  }, wrapper: ViewModel.get({
    styles: {width: '50%', background: '#ffa', cursor: 'pointer'}, events: {
      click(e, vm) {
        vm.isStop = true;
      },
    },
  }),
  // ...
});

const f = _ => {
  viewmodel.changeContents();
  binder.render(viewmodel);
  if (!viewmodel.isStop) requestAnimationFrame(f);
};
requestAnimationFrame(f);

전체코드

<!DOCTYPE html>
<html lang="en">
<head>
	<meta charset="UTF-8">
	<title>MVVM</title>
</head>
<body>
<section id="target" data-viewmodel="wrapper">
  <h2 data-viewmodel="title"></h2>
  <section data-viewmodel="contents"></section>
</section>
<script>

const type = (target, type) => {
  if (typeof type == "string") {
    if (typeof target != type) throw `invalid type ${target} : ${type}`
  } else if (!(target instanceof type)) {
    throw `invalid type ${target} : ${type}`
  }
  return target;
}

const ViewModel = class {
  static #private = Symbol()
  static get (data) {
    return new ViewModel(this.#private, data)
  } 
  styles = {}; attributes = {}; properties = {}; events = {};
  constructor(checker, data) {
    if (checker != ViewModel.#private) throw 'use ViewModel.get()!'
    Object.entries(data).forEach(([k, v]) => {
      switch (k) {
        case 'styles': this.styles = v; break;
        case 'attributes': this.attributes = v; break;
        case 'properties': this.properties = v; break;
        case 'events': this.events = v; break;
        default: this[k] = v;
      }
    });
    Object.seal(this); // Value를 바꿀 순 있지만 Key를 추가할 순 없다.
  }
}

const BinderItem = class {
  el; viewmodel;
  constructor (el, viewmodel, _0 = type(el, HTMLElement), _1 = type(viewmodel, 'string')) {
    this.el = el
    this.viewmodel = viewmodel
    Object.freeze(this)
  }
}

const Binder = class {
  #items = new Set()
  add (v, _ = type(v, BinderItem)) { this.#items.add(v) }
  render (viewmodel, _ = type(viewmodel, ViewModel)) {
    this.#items.forEach(item => {
      const vm = type(viewmodel[item.viewmodel], ViewModel), el = item.el
      Object.entries(vm.styles).forEach(([k, v]) => el.style[k] = v)
      Object.entries(vm.attributes).forEach(([k, v]) => el.attribute[k] = v)
      Object.entries(vm.properties).forEach(([k, v]) => el[k] = v)
      Object.entries(vm.events).forEach(([k, v]) => el[`on${k}`] = e => v.call(el, e, viewmodel))
    })
  }
}

const Scanner = class {
  scan (el, _ = type(el, HTMLElement)) {
    const binder = new Binder();
    this.checkItem(binder, el)
    const stack = [el.firstElementChild]

    // HTML 전체에 대한 순회
    let target
    while (target = stack.pop()) {
      this.checkItem(binder, target)
      if (target.firstElementChild) stack.push(target.firstElementChild)
      if (target.nextElementSibling) stack.push(target.nextElementSibling)
    }
    return binder;
  }
  checkItem (binder, el) {
    const vm = el.getAttribute('data-viewmodel')
    if (vm) binder.add(new BinderItem(el, vm))
  }
}

const scanner = new Scanner()
const binder = scanner.scan(document.querySelector('#target'))

const getRandom = () => parseInt(Math.random() * 150) + 100
const viewmodel2 = ViewModel.get({
  isStop: false,
  changeContents () {
    this.wrapper.styles.background = `rgb(${getRandom()},${getRandom()},${getRandom()})`
    this.contents.properties.innerHTML = Math.random().toString(16).replace('.', '')
    binder.render(viewmodel2)
    // viewmodel을 갱신하면, binder가 viewmodel을 view에 rendering 한다.
    // 즉, '인메모리 객체'만 수정하면 된다.
  },
  wrapper: ViewModel.get({
    styles: { width: '50%', background: '#fff', cursor: 'pointer' },
    events: { click(e, vm) { vm.isStop = true } }
  }),
  title: ViewModel.get({
    properties: { innerHTML: 'Title' }
  }),
  contents: ViewModel.get({
    properties: { innerHTML: 'Contents' }
  })
})
const f = () => {
  viewmodel2.changeContents()
  binder.render(viewmodel2)
  if (!viewmodel2.isStop) requestAnimationFrame(f)
}
requestAnimationFrame(f)
</script>
</body>
</html>

간단히는 우리는 필요한 viewmodel만 수정함으로써 해당 style, events, properties와 관련한 내용들이 알아서 추가되고 핸들러가 등록되고 style이 적용되는 것을 알 수 있다. (해당 역할들을 Binder에서 처리하는 제어역전이 일어나기때문에)


정리

  1. new Scanner를 통한 scanner 인스턴스 생성
  2. scanner.scan 메소드 실행
  3. 해당 메소드에서 binder 생성 및 binder.add 실행 후 연결된 BinerItem을 적용시켜 data-viewmodel 속성이 있는지 확인 후 binder를 리턴한 const binder에 저장
  4. viewmodel.get 실행 후 return new ViewModel()을 통해 인스턴스 생성
  5. 해당 인스턴스 생성에 필요한 Data값을 적용 시켜서 constructor안에 있는 switch문을 통해 해당 객체key에 따른 value값 적용 후 viewmodel2 생성
  6. requestAnimationFrame(f)를 통해 해당 멈춤이 있기 전까지 계속해서 viewmodel2의 내용을 바꾸고 해당 값을 binder.render를 통해 적용시킨다.

해당 Binder는 #items로 이루어져있는데 해당 배열 내용들은 각각 BinderItem이 들어있다. (첫번째 BinderItem의 내용과 Binder의 0번째 배열의 BinderItem의 속성내용은 같다. )

느낀점

겨우 이해는 되는거 같지만 이런 것을 코드로 작성하라고 하면 정말 어렵고 현재는 못할거같다. => 결국 많이하면 알게되겠지 ... 열심히하자


출처

코드스피츠

200413 방법 다시 정리

  1. HTMLElement를 포함한 내용을 Scanner를 통해 가져온다.
  2. Scanner.scan을 통해 binder 생성
  3. checkItem을 실행 => scan함수 안의 while문을 통해 태그안의 태그도 조사하며 checkItem 실행
  4. checkitem을 통해 data-viewmodel속성을 포함한다면 binder의 내용에 BinderItem 형태로 추가
  5. return binder를 한다. (BinderItem의 형태의 태그를 binder값에 생성)
  6. requestAnimationFrame 실행
  7. viewModel2.changeContents 실행
  8. binder.render() 실행
  9. viewModel2가 순수하게 계속 변한다.
  10. viewModel2가 브라우저에서 클릭하면 멈추어 끝낸다.

이를 통해 HTMLElement가 scanner의 영향을 받아 알아내고 이를 binder와 연관시키고 이를 viewModel에서 필요에따라 바꾸어 처리한다.

profile
나의 하루를 가능한 기억하고 즐기고 후회하지말자
post-custom-banner

0개의 댓글