├── .gitignore ├── 01-pubsub └── index.html ├── 02-object-define-property ├── 01-example.html ├── 02-multiple-key.html ├── 03-multiple-key-refactor.html ├── 04-multiple-observer.html └── 05-functionalized.html ├── 03-with-component ├── 01-just-implement │ ├── index.html │ └── src │ │ ├── core │ │ └── observer.js │ │ └── main.js ├── 02-component │ ├── index.html │ └── src │ │ ├── App.js │ │ ├── core │ │ ├── Component.js │ │ └── observer.js │ │ └── main.js └── 03-store │ ├── index.html │ └── src │ ├── App.js │ ├── core │ ├── Component.js │ └── observer.js │ ├── main.js │ └── store.js ├── 04-simple-vuex ├── index.html └── src │ ├── App.js │ ├── core │ ├── Component.js │ ├── Store.js │ └── observer.js │ ├── main.js │ └── store.js ├── 05-simple-redux ├── index.html └── src │ ├── App.js │ ├── core │ ├── Component.js │ ├── Store.js │ └── observer.js │ ├── main.js │ └── store.js ├── 06-optimization ├── index.html └── src │ ├── App.js │ ├── core │ ├── Component.js │ ├── Store.js │ └── observer.js │ ├── main.js │ └── store.js ├── 07-proxy ├── index.html └── src │ ├── App.js │ ├── core │ ├── Component.js │ ├── Store.js │ └── observer.js │ ├── main.js │ └── store.js └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .DS_Store 3 | -------------------------------------------------------------------------------- /01-pubsub/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 7 | 8 | Document 9 | 10 | 11 |
12 | 개발자도구에서 콘솔로그를 확인해주세요 13 |
14 | 71 | 72 | -------------------------------------------------------------------------------- /02-object-define-property/01-example.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 7 | 8 | Document 9 | 10 | 11 |
12 | 개발자도구에서 콘솔 로그를 확인해주세요. 13 |
14 | 31 | 32 | -------------------------------------------------------------------------------- /02-object-define-property/02-multiple-key.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 7 | 8 | Document 9 | 10 | 11 |
12 | 개발자도구에서 콘솔 로그를 확인해주세요. 13 |
14 | 41 | 42 | -------------------------------------------------------------------------------- /02-object-define-property/03-multiple-key-refactor.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 7 | 8 | Document 9 | 10 | 11 |
12 | 개발자도구에서 콘솔 로그를 확인해주세요. 13 |
14 | 41 | 42 | -------------------------------------------------------------------------------- /02-object-define-property/04-multiple-observer.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 7 | 8 | Document 9 | 10 | 11 |
12 | 개발자도구에서 콘솔 로그를 확인해주세요. 13 |
14 | 58 | 59 | -------------------------------------------------------------------------------- /02-object-define-property/05-functionalized.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 7 | 8 | Document 9 | 10 | 11 |
12 | 개발자도구에서 콘솔 로그를 확인해주세요. 13 |
14 | 53 | 54 | -------------------------------------------------------------------------------- /03-with-component/01-just-implement/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Store를 적용해보자 6 | 7 | 8 |
9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /03-with-component/01-just-implement/src/core/observer.js: -------------------------------------------------------------------------------- 1 | let currentObserver = null; 2 | 3 | export const observe = fn => { 4 | currentObserver = fn; 5 | fn(); 6 | currentObserver = null; 7 | } 8 | 9 | export const observable = obj => { 10 | Object.keys(obj).forEach(key => { 11 | let _value = obj[key]; 12 | const observers = new Set(); 13 | 14 | Object.defineProperty(obj, key, { 15 | get () { 16 | if (currentObserver) observers.add(currentObserver); 17 | return _value; 18 | }, 19 | 20 | set (value) { 21 | _value = value; 22 | observers.forEach(fn => fn()); 23 | } 24 | }) 25 | }) 26 | return obj; 27 | } 28 | -------------------------------------------------------------------------------- /03-with-component/01-just-implement/src/main.js: -------------------------------------------------------------------------------- 1 | import { observable, observe } from "./core/observer.js"; 2 | 3 | const state = observable({ 4 | a: 10, 5 | b: 20, 6 | }); 7 | 8 | const $app = document.querySelector('#app'); 9 | 10 | const render = () => { 11 | $app.innerHTML = ` 12 |

a + b = ${state.a + state.b}

13 | 14 | 15 | `; 16 | 17 | $app.querySelector('#stateA').addEventListener('change', ({ target }) => { 18 | state.a = Number(target.value); 19 | }) 20 | 21 | $app.querySelector('#stateB').addEventListener('change', ({ target }) => { 22 | state.b = Number(target.value); 23 | }) 24 | } 25 | 26 | observe(render); -------------------------------------------------------------------------------- /03-with-component/02-component/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Store를 적용해보자 6 | 7 | 8 |
9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /03-with-component/02-component/src/App.js: -------------------------------------------------------------------------------- 1 | import {Component} from "./core/Component.js"; 2 | 3 | export class App extends Component { 4 | initState () { 5 | return { 6 | a: 10, 7 | b: 20, 8 | } 9 | } 10 | 11 | template () { 12 | const { a, b } = this.state; 13 | return ` 14 | 15 | 16 |

a + b = ${a + b}

17 | `; 18 | } 19 | 20 | setEvent () { 21 | const { $el, state } = this; 22 | 23 | $el.querySelector('#stateA').addEventListener('change', ({ target }) => { 24 | state.a = Number(target.value); 25 | }) 26 | 27 | $el.querySelector('#stateB').addEventListener('change', ({ target }) => { 28 | state.b = Number(target.value); 29 | }) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /03-with-component/02-component/src/core/Component.js: -------------------------------------------------------------------------------- 1 | import { observable, observe } from './observer.js'; 2 | 3 | export class Component { 4 | state; props; $el; 5 | 6 | constructor ($el, props) { 7 | this.$el = $el; 8 | this.props = props; 9 | this.setup(); 10 | } 11 | 12 | setup() { 13 | this.state = observable(this.initState()); 14 | observe(() => { 15 | this.render(); 16 | this.setEvent(); 17 | this.mounted(); 18 | }); 19 | } 20 | 21 | initState() { return {} } 22 | template () { return ''; } 23 | render () { this.$el.innerHTML = this.template(); } 24 | setEvent () {} 25 | mounted () {} 26 | } 27 | -------------------------------------------------------------------------------- /03-with-component/02-component/src/core/observer.js: -------------------------------------------------------------------------------- 1 | let currentObserver = null; 2 | 3 | export const observe = fn => { 4 | currentObserver = fn; 5 | fn(); 6 | currentObserver = null; 7 | } 8 | 9 | export const observable = obj => { 10 | Object.keys(obj).forEach(key => { 11 | let _value = obj[key]; 12 | const observers = new Set(); 13 | 14 | Object.defineProperty(obj, key, { 15 | get () { 16 | if (currentObserver) observers.add(currentObserver); 17 | return _value; 18 | }, 19 | 20 | set (value) { 21 | _value = value; 22 | observers.forEach(fn => fn()); 23 | } 24 | }) 25 | }) 26 | return obj; 27 | } 28 | -------------------------------------------------------------------------------- /03-with-component/02-component/src/main.js: -------------------------------------------------------------------------------- 1 | import { App } from "./App.js"; 2 | 3 | new App(document.querySelector('#app')); 4 | -------------------------------------------------------------------------------- /03-with-component/03-store/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Store를 적용해보자 6 | 7 | 8 |
9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /03-with-component/03-store/src/App.js: -------------------------------------------------------------------------------- 1 | import { Component } from "./core/Component.js"; 2 | import { store } from './store.js'; 3 | 4 | const InputA = () => ` 5 | 6 | `; 7 | 8 | const InputB = () => ` 9 | 10 | ` 11 | 12 | const Calculator = () => ` 13 |

a + b = ${store.state.a + store.state.b}

14 | ` 15 | 16 | export class App extends Component { 17 | template () { 18 | return ` 19 | ${InputA()} 20 | ${InputB()} 21 | ${Calculator()} 22 | `; 23 | } 24 | 25 | setEvent () { 26 | const { $el} = this; 27 | 28 | $el.querySelector('#stateA').addEventListener('change', ({ target }) => { 29 | store.setState({ a: Number(target.value) }); 30 | }) 31 | 32 | $el.querySelector('#stateB').addEventListener('change', ({ target }) => { 33 | store.setState({ b: Number(target.value) }); 34 | }) 35 | } 36 | } -------------------------------------------------------------------------------- /03-with-component/03-store/src/core/Component.js: -------------------------------------------------------------------------------- 1 | import { observable, observe } from './observer.js'; 2 | 3 | export class Component { 4 | state; props; $el; 5 | 6 | constructor ($el, props) { 7 | this.$el = $el; 8 | this.props = props; 9 | this.setup(); 10 | } 11 | 12 | setup() { 13 | this.state = observable(this.initState()); 14 | observe(() => { 15 | this.render(); 16 | this.setEvent(); 17 | this.mounted(); 18 | }); 19 | } 20 | 21 | initState() { return {} } 22 | template () { return ''; } 23 | render () { this.$el.innerHTML = this.template(); } 24 | setEvent () {} 25 | mounted () {} 26 | } 27 | -------------------------------------------------------------------------------- /03-with-component/03-store/src/core/observer.js: -------------------------------------------------------------------------------- 1 | let currentObserver = null; 2 | 3 | export const observe = fn => { 4 | currentObserver = fn; 5 | fn(); 6 | currentObserver = null; 7 | } 8 | 9 | export const observable = obj => { 10 | Object.keys(obj).forEach(key => { 11 | let _value = obj[key]; 12 | const observers = new Set(); 13 | 14 | Object.defineProperty(obj, key, { 15 | get () { 16 | if (currentObserver) observers.add(currentObserver); 17 | return _value; 18 | }, 19 | 20 | set (value) { 21 | _value = value; 22 | observers.forEach(fn => fn()); 23 | } 24 | }) 25 | }) 26 | return obj; 27 | } 28 | -------------------------------------------------------------------------------- /03-with-component/03-store/src/main.js: -------------------------------------------------------------------------------- 1 | import { App } from "./App.js"; 2 | 3 | new App(document.querySelector('#app')); 4 | -------------------------------------------------------------------------------- /03-with-component/03-store/src/store.js: -------------------------------------------------------------------------------- 1 | import { observable } from './core/observer.js' 2 | 3 | export const store = { 4 | state: observable({ 5 | a: 10, 6 | b: 20, 7 | }), 8 | 9 | setState (newState) { 10 | for (const [key, value] of Object.entries(newState)) { 11 | if (!this.state[key]) continue; 12 | this.state[key] = value; 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /04-simple-vuex/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Store를 적용해보자 6 | 7 | 8 |
9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /04-simple-vuex/src/App.js: -------------------------------------------------------------------------------- 1 | import { Component } from "./core/Component.js"; 2 | import { store } from './store.js'; 3 | 4 | const InputA = () => ``; 5 | const InputB = () => ``; 6 | const Calculator = () => `

a + b = ${store.state.a + store.state.b}

`; 7 | 8 | export class App extends Component { 9 | template () { 10 | return ` 11 | ${InputA()} 12 | ${InputB()} 13 | ${Calculator()} 14 | `; 15 | } 16 | 17 | setEvent () { 18 | const { $el} = this; 19 | 20 | $el.querySelector('#stateA').addEventListener('change', ({ target }) => { 21 | // commit을 통해서 값을 변경시킨다. 22 | store.commit('SET_A', Number(target.value)); 23 | }) 24 | 25 | $el.querySelector('#stateB').addEventListener('change', ({ target }) => { 26 | // commit을 통해서 값을 변경시킨다. 27 | store.commit('SET_B', Number(target.value)); 28 | }) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /04-simple-vuex/src/core/Component.js: -------------------------------------------------------------------------------- 1 | import { observable, observe } from './observer.js'; 2 | 3 | export class Component { 4 | state; props; $el; 5 | 6 | constructor ($el, props) { 7 | this.$el = $el; 8 | this.props = props; 9 | this.setup(); 10 | } 11 | 12 | setup() { 13 | this.state = observable(this.initState()); 14 | observe(() => { 15 | this.render(); 16 | this.setEvent(); 17 | this.mounted(); 18 | }); 19 | } 20 | 21 | initState() { return {} } 22 | template () { return ''; } 23 | render () { this.$el.innerHTML = this.template(); } 24 | setEvent () {} 25 | mounted () {} 26 | } 27 | -------------------------------------------------------------------------------- /04-simple-vuex/src/core/Store.js: -------------------------------------------------------------------------------- 1 | import { observable } from './observer.js'; 2 | 3 | export class Store { 4 | 5 | #state; #mutations; #actions; 6 | state = {}; 7 | 8 | constructor ({ state, mutations, actions }) { 9 | this.#state = observable(state); 10 | this.#mutations = mutations; 11 | this.#actions = actions; 12 | 13 | // state를 직접적으로 수정하지 못하도록 다음과 같이 정의한다. 14 | Object.keys(state).forEach(key => { 15 | Object.defineProperty( 16 | this.state, 17 | key, 18 | { get: () => this.#state[key] }, 19 | ) 20 | }) 21 | } 22 | 23 | commit (action, payload) { 24 | this.#mutations[action](this.#state, payload); 25 | } 26 | 27 | dispatch (action, payload) { 28 | return this.#actions[action]({ 29 | state: this.#state, 30 | commit: this.commit.bind(this), 31 | dispatch: this.dispatch.bind(this), 32 | }, payload); 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /04-simple-vuex/src/core/observer.js: -------------------------------------------------------------------------------- 1 | let currentObserver = null; 2 | 3 | export const observe = fn => { 4 | currentObserver = fn; 5 | fn(); 6 | currentObserver = null; 7 | } 8 | 9 | export const observable = obj => { 10 | Object.keys(obj).forEach(key => { 11 | let _value = obj[key]; 12 | const observers = new Set(); 13 | 14 | Object.defineProperty(obj, key, { 15 | get () { 16 | if (currentObserver) observers.add(currentObserver); 17 | return _value; 18 | }, 19 | 20 | set (value) { 21 | _value = value; 22 | observers.forEach(fn => fn()); 23 | } 24 | }) 25 | }) 26 | return obj; 27 | } 28 | -------------------------------------------------------------------------------- /04-simple-vuex/src/main.js: -------------------------------------------------------------------------------- 1 | import { App } from "./App.js"; 2 | 3 | new App(document.querySelector('#app')); 4 | -------------------------------------------------------------------------------- /04-simple-vuex/src/store.js: -------------------------------------------------------------------------------- 1 | import { Store } from './core/Store.js'; 2 | 3 | export const store = new Store({ 4 | state: { 5 | a: 10, 6 | b: 20, 7 | }, 8 | 9 | // state의 값은 오직 mutations를 통해서 변경할 수 있다. 10 | mutations: { 11 | SET_A (state, payload) { 12 | state.a = payload; 13 | }, 14 | 15 | SET_B (state, payload) { 16 | state.b = payload; 17 | } 18 | }, 19 | 20 | // actions도 있으면 좋겠지만.. 딱히 지금 쓸만한 API가 없다. 21 | }); 22 | -------------------------------------------------------------------------------- /05-simple-redux/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Store를 적용해보자 6 | 7 | 8 |
9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /05-simple-redux/src/App.js: -------------------------------------------------------------------------------- 1 | import { Component } from "./core/Component.js"; 2 | import {setA, setB, store} from './store.js'; 3 | 4 | const InputA = () => ``; 5 | const InputB = () => ``; 6 | const Calculator = () => `

a + b = ${store.getState().a + store.getState().b}

`; 7 | 8 | export class App extends Component { 9 | template () { 10 | return ` 11 | ${InputA()} 12 | ${InputB()} 13 | ${Calculator()} 14 | `; 15 | } 16 | 17 | setEvent () { 18 | const { $el} = this; 19 | 20 | $el.querySelector('#stateA').addEventListener('change', ({ target }) => { 21 | // commit을 통해서 값을 변경시킨다. 22 | store.dispatch(setA(Number(target.value))); 23 | }) 24 | 25 | $el.querySelector('#stateB').addEventListener('change', ({ target }) => { 26 | // commit을 통해서 값을 변경시킨다. 27 | store.dispatch(setB(Number(target.value))); 28 | }) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /05-simple-redux/src/core/Component.js: -------------------------------------------------------------------------------- 1 | import { observable, observe } from './observer.js'; 2 | 3 | export class Component { 4 | state; props; $el; 5 | 6 | constructor ($el, props) { 7 | this.$el = $el; 8 | this.props = props; 9 | this.setup(); 10 | } 11 | 12 | setup() { 13 | this.state = observable(this.initState()); 14 | observe(() => { 15 | this.render(); 16 | this.setEvent(); 17 | this.mounted(); 18 | }); 19 | } 20 | 21 | initState() { return {} } 22 | template () { return ''; } 23 | render () { this.$el.innerHTML = this.template(); } 24 | setEvent () {} 25 | mounted () {} 26 | } 27 | -------------------------------------------------------------------------------- /05-simple-redux/src/core/Store.js: -------------------------------------------------------------------------------- 1 | import { observable } from './observer.js'; 2 | 3 | export const createStore = (reducer) => { 4 | 5 | // reducer가 실행될 때 반환하는 객체(state)를 observable로 만들어야 한다. 6 | const state = observable(reducer()); 7 | 8 | // getState가 실제 state를 반환하는 것이 아니라 frozenState를 반환하도록 만들어야 한다. 9 | const frozenState = {}; 10 | Object.keys(state).forEach(key => { 11 | Object.defineProperty(frozenState, key, { 12 | get: () => state[key], // get만 정의하여 set을 하지 못하도록 만드는 것이다. 13 | }) 14 | }); 15 | 16 | // dispatch로만 state의 값을 변경할 수 있다. 17 | const dispatch = (action) => { 18 | const newState = reducer(state, action); 19 | 20 | for (const [key, value] of Object.entries(newState)) { 21 | // state의 key가 아닐 경우 변경을 생략한다. 22 | if (!state[key]) continue; 23 | state[key] = value; 24 | } 25 | } 26 | 27 | const getState = () => frozenState; 28 | 29 | // subscribe는 observe로 대체한다. 30 | return { getState, dispatch }; 31 | 32 | } 33 | -------------------------------------------------------------------------------- /05-simple-redux/src/core/observer.js: -------------------------------------------------------------------------------- 1 | let currentObserver = null; 2 | 3 | export const observe = fn => { 4 | currentObserver = fn; 5 | fn(); 6 | currentObserver = null; 7 | } 8 | 9 | export const observable = obj => { 10 | Object.keys(obj).forEach(key => { 11 | let _value = obj[key]; 12 | const observers = new Set(); 13 | 14 | Object.defineProperty(obj, key, { 15 | get () { 16 | if (currentObserver) observers.add(currentObserver); 17 | return _value; 18 | }, 19 | 20 | set (value) { 21 | _value = value; 22 | observers.forEach(fn => fn()); 23 | } 24 | }) 25 | }) 26 | return obj; 27 | } 28 | -------------------------------------------------------------------------------- /05-simple-redux/src/main.js: -------------------------------------------------------------------------------- 1 | import { App } from "./App.js"; 2 | 3 | new App(document.querySelector('#app')); 4 | -------------------------------------------------------------------------------- /05-simple-redux/src/store.js: -------------------------------------------------------------------------------- 1 | import {createStore} from './core/Store.js'; 2 | 3 | const initState = { 4 | a: 10, 5 | b: 20, 6 | }; 7 | 8 | export const SET_A = 'SET_A'; 9 | export const SET_B = 'SET_B'; 10 | 11 | export const store = createStore((state = initState, action = {}) => { 12 | switch (action.type) { 13 | case 'SET_A' : 14 | return { ...state, a: action.payload } 15 | case 'SET_B' : 16 | return { ...state, b: action.payload } 17 | default: 18 | return state; 19 | } 20 | }); 21 | 22 | export const setA = (payload) => ({ type: SET_A, payload }); 23 | export const setB = (payload) => ({ type: SET_B, payload }); -------------------------------------------------------------------------------- /06-optimization/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Store를 적용해보자 6 | 7 | 8 |
9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /06-optimization/src/App.js: -------------------------------------------------------------------------------- 1 | import { Component } from "./core/Component.js"; 2 | import {setA, setB, store} from './store.js'; 3 | 4 | const InputA = () => ``; 5 | const InputB = () => ``; 6 | const Calculator = () => `

a + b = ${store.getState().a + store.getState().b}

`; 7 | 8 | export class App extends Component { 9 | template () { 10 | return ` 11 | ${InputA()} 12 | ${InputB()} 13 | ${Calculator()} 14 | `; 15 | } 16 | 17 | setEvent () { 18 | const { $el} = this; 19 | 20 | $el.querySelector('#stateA').addEventListener('change', ({ target }) => { 21 | // commit을 통해서 값을 변경시킨다. 22 | store.dispatch(setA(Number(target.value))); 23 | }) 24 | 25 | $el.querySelector('#stateB').addEventListener('change', ({ target }) => { 26 | // commit을 통해서 값을 변경시킨다. 27 | store.dispatch(setB(Number(target.value))); 28 | }) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /06-optimization/src/core/Component.js: -------------------------------------------------------------------------------- 1 | import { observable, observe } from './observer.js'; 2 | 3 | 4 | export class Component { 5 | state; props; $el; 6 | 7 | constructor ($el, props) { 8 | this.$el = $el; 9 | this.props = props; 10 | this.setup(); 11 | } 12 | 13 | setup() { 14 | this.state = observable(this.initState()); 15 | observe(() => { 16 | this.render(); 17 | this.setEvent(); 18 | this.mounted(); 19 | }); 20 | } 21 | 22 | initState() { return {} } 23 | template () { return ''; } 24 | render () { this.$el.innerHTML = this.template(); } 25 | setEvent () {} 26 | mounted () {} 27 | } 28 | -------------------------------------------------------------------------------- /06-optimization/src/core/Store.js: -------------------------------------------------------------------------------- 1 | import { observable } from './observer.js'; 2 | 3 | export const createStore = (reducer) => { 4 | 5 | // reducer가 실행될 때 반환하는 객체(state)를 observable로 만들어야 한다. 6 | const state = observable(reducer()); 7 | 8 | // getState가 실제 state를 반환하는 것이 아니라 frozenState를 반환하도록 만들어야 한다. 9 | const frozenState = {}; 10 | Object.keys(state).forEach(key => { 11 | Object.defineProperty(frozenState, key, { 12 | get: () => state[key], // get만 정의하여 set을 하지 못하도록 만드는 것이다. 13 | }) 14 | }); 15 | 16 | // dispatch로만 state의 값을 변경할 수 있다. 17 | const dispatch = (action) => { 18 | const newState = reducer(state, action); 19 | 20 | for (const [key, value] of Object.entries(newState)) { 21 | // state의 key가 아닐 경우 변경을 생략한다. 22 | if (!state[key]) continue; 23 | state[key] = value; 24 | } 25 | } 26 | 27 | const getState = () => frozenState; 28 | 29 | // subscribe는 observe로 대체한다. 30 | return { getState, dispatch }; 31 | 32 | } 33 | -------------------------------------------------------------------------------- /06-optimization/src/core/observer.js: -------------------------------------------------------------------------------- 1 | let currentObserver = null; 2 | 3 | const debounceFrame = (callback) => { 4 | let currentCallback = -1; 5 | return callback => { 6 | cancelAnimationFrame(currentCallback); // 현재 등록된 callback이 있을 경우 취소한다. 7 | currentCallback = requestAnimationFrame(callback); // 1프레임 뒤에 실행되도록 한다. 8 | } 9 | }; 10 | 11 | export const observe = fn => { 12 | currentObserver = debounceFrame(fn); 13 | fn(); 14 | currentObserver = null; 15 | } 16 | 17 | export const observable = obj => { 18 | Object.keys(obj).forEach(key => { 19 | let _value = obj[key]; 20 | const observers = new Set(); 21 | 22 | Object.defineProperty(obj, key, { 23 | get () { 24 | if (currentObserver) observers.add(currentObserver); 25 | return _value; 26 | }, 27 | 28 | set (value) { 29 | if (_value === value) return; 30 | if (JSON.stringify(_value) === JSON.stringify(value)) return; 31 | _value = value; 32 | observers.forEach(fn => fn()); 33 | } 34 | }) 35 | }) 36 | return obj; 37 | } 38 | -------------------------------------------------------------------------------- /06-optimization/src/main.js: -------------------------------------------------------------------------------- 1 | import { App } from "./App.js"; 2 | 3 | new App(document.querySelector('#app')); 4 | -------------------------------------------------------------------------------- /06-optimization/src/store.js: -------------------------------------------------------------------------------- 1 | import {createStore} from './core/Store.js'; 2 | 3 | const initState = { 4 | a: 10, 5 | b: 20, 6 | }; 7 | 8 | export const SET_A = 'SET_A'; 9 | export const SET_B = 'SET_B'; 10 | 11 | export const store = createStore((state = initState, action = {}) => { 12 | switch (action.type) { 13 | case 'SET_A' : 14 | return { ...state, a: action.payload } 15 | case 'SET_B' : 16 | return { ...state, b: action.payload } 17 | default: 18 | return state; 19 | } 20 | }); 21 | 22 | export const setA = (payload) => ({ type: SET_A, payload }); 23 | export const setB = (payload) => ({ type: SET_B, payload }); -------------------------------------------------------------------------------- /07-proxy/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Store를 적용해보자 6 | 7 | 8 |
9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /07-proxy/src/App.js: -------------------------------------------------------------------------------- 1 | import { Component } from "./core/Component.js"; 2 | import {setA, setB, store} from './store.js'; 3 | 4 | const InputA = () => ``; 5 | const InputB = () => ``; 6 | const Calculator = () => `

a + b = ${store.getState().a + store.getState().b}

`; 7 | 8 | export class App extends Component { 9 | template () { 10 | return ` 11 | ${InputA()} 12 | ${InputB()} 13 | ${Calculator()} 14 | `; 15 | } 16 | 17 | setEvent () { 18 | const { $el} = this; 19 | 20 | $el.querySelector('#stateA').addEventListener('change', ({ target }) => { 21 | // commit을 통해서 값을 변경시킨다. 22 | store.dispatch(setA(Number(target.value))); 23 | }) 24 | 25 | $el.querySelector('#stateB').addEventListener('change', ({ target }) => { 26 | // commit을 통해서 값을 변경시킨다. 27 | store.dispatch(setB(Number(target.value))); 28 | }) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /07-proxy/src/core/Component.js: -------------------------------------------------------------------------------- 1 | import { observable, observe } from './observer.js'; 2 | 3 | 4 | export class Component { 5 | state; props; $el; 6 | 7 | constructor ($el, props) { 8 | this.$el = $el; 9 | this.props = props; 10 | this.setup(); 11 | } 12 | 13 | setup() { 14 | this.state = observable(this.initState()); 15 | observe(() => { 16 | this.render(); 17 | this.setEvent(); 18 | this.mounted(); 19 | }); 20 | } 21 | 22 | initState() { return {} } 23 | template () { return ''; } 24 | render () { this.$el.innerHTML = this.template(); } 25 | setEvent () {} 26 | mounted () {} 27 | } 28 | -------------------------------------------------------------------------------- /07-proxy/src/core/Store.js: -------------------------------------------------------------------------------- 1 | import { observable } from './observer.js'; 2 | 3 | export const createStore = (reducer) => { 4 | 5 | // reducer가 실행될 때 반환하는 객체(state)를 observable로 만들어야 한다. 6 | const state = observable(reducer()); 7 | 8 | // getState가 실제 state를 반환하는 것이 아니라 frozenState를 반환하도록 만들어야 한다. 9 | const frozenState = {}; 10 | Object.keys(state).forEach(key => { 11 | Object.defineProperty(frozenState, key, { 12 | get: () => state[key], // get만 정의하여 set을 하지 못하도록 만드는 것이다. 13 | }) 14 | }); 15 | 16 | // dispatch로만 state의 값을 변경할 수 있다. 17 | const dispatch = (action) => { 18 | const newState = reducer(state, action); 19 | 20 | for (const [key, value] of Object.entries(newState)) { 21 | // state의 key가 아닐 경우 변경을 생략한다. 22 | if (!state[key]) continue; 23 | state[key] = value; 24 | } 25 | } 26 | 27 | const getState = () => frozenState; 28 | 29 | // subscribe는 observe로 대체한다. 30 | return { getState, dispatch }; 31 | 32 | } 33 | -------------------------------------------------------------------------------- /07-proxy/src/core/observer.js: -------------------------------------------------------------------------------- 1 | let currentObserver = null; 2 | 3 | const debounceFrame = (callback) => { 4 | let currentCallback = -1; 5 | return () => { 6 | cancelAnimationFrame(currentCallback); // 현재 등록된 callback이 있을 경우 취소한다. 7 | currentCallback = requestAnimationFrame(callback); // 1프레임 뒤에 실행되도록 한다. 8 | } 9 | }; 10 | 11 | export const observe = fn => { 12 | currentObserver = debounceFrame(fn); 13 | fn(); 14 | currentObserver = null; 15 | } 16 | 17 | export const observable = obj => { 18 | 19 | const observerMap = Object.keys(obj).reduce((map, key) => { 20 | map[key] = new Set(); 21 | return map; 22 | }, {}); 23 | 24 | return new Proxy(obj, { 25 | get: (target, name) => { 26 | if (currentObserver) observerMap[name].add(currentObserver) 27 | return target[name]; 28 | }, 29 | set: (target, name, value) => { 30 | if (target[name] === value) return true; 31 | if (JSON.stringify(target[name]) === JSON.stringify(value)) return true; 32 | target[name] = value; 33 | observerMap[name].forEach(fn => fn()); 34 | return true; 35 | }, 36 | }); 37 | 38 | } 39 | -------------------------------------------------------------------------------- /07-proxy/src/main.js: -------------------------------------------------------------------------------- 1 | import { App } from "./App.js"; 2 | 3 | new App(document.querySelector('#app')); 4 | -------------------------------------------------------------------------------- /07-proxy/src/store.js: -------------------------------------------------------------------------------- 1 | import {createStore} from './core/Store.js'; 2 | 3 | const initState = { 4 | a: 10, 5 | b: 20, 6 | }; 7 | 8 | export const SET_A = 'SET_A'; 9 | export const SET_B = 'SET_B'; 10 | 11 | export const store = createStore((state = initState, action = {}) => { 12 | switch (action.type) { 13 | case 'SET_A' : 14 | return { ...state, a: action.payload } 15 | case 'SET_B' : 16 | return { ...state, b: action.payload } 17 | default: 18 | return state; 19 | } 20 | }); 21 | 22 | export const setA = (payload) => ({ type: SET_A, payload }); 23 | export const setB = (payload) => ({ type: SET_B, payload }); -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Vanilla Javascript로 상태관리 시스템 만들기 2 | 3 | 이 저장소는 [Vanilla Javascript로 상태관리 시스템 만들기](https://junilhwang.github.io/TIL/Javascript/Design/Vanilla-JS-Store/) 아티클에서 사용되는 코드를 모아놓는 용도로 사용되었습니다. 4 | --------------------------------------------------------------------------------