-
Vanilla Javascript로 상태관리 시스템 만들기Vanilla js 2021. 9. 18. 00:22
현대적인 프론트엔드 개발에서 중요한 것은 상태관리 라고 한다.
Vue나 React 같은 프레임워크의 주된 목적 중 하나가 상태를 기반으로 DOM을 렌더링 하는 것이기 때문이다.
상태관리 학습을 위한 기초적인 구현에서는 상태관리와 컴포넌트 사이의 depth가 낮기 때문에 프레임워크가 필요하지 않다.
하지만 규모가 큰 어플을 구현하게 된다면 복잡한 상태관리와 컴포넌트 사이의 관계가 깊어지기 때문에 상태관리 프레임워크가 중요하다.
Observer Pattern
중앙 집중식 저장소를 간단하게 Store 라고 표현해보자.
Store를 구현하기 위해 먼저 저장소(Store)와 컴포넌트(Component)의 관계를 잘 살펴봐야 한다.- Store는 여러개의 컴포넌트에서 사용될 수 있다.
- Store가 변경될 때, Store가 사용되고 있는 Component도 변경되어야 한다.// Store 생성 const store = new Store({ a : 10, b : 20, }); // 컴포넌트 생성 const component1 = new Component({ subscribe : [store] }); const component2 = new Component({ subscribe : [store] }); // 컴포넌트가 Store를 구독한다. component1.subscribe(store); component2.subscribe(store); // Store 의 상태 변경 store.setState({ a : 100, b : 200, }); // Store 상태 변경 알리기 store.notify();
위 코드는 component에 subscribe 메서드를 등록해주었다.
나는 component의 constructor 내부에 this.store.subscribe(this) 와 같은 방법으로도 진행 할 것 같다.
가장 기초적인 프레임워크의 상태관리의 근본이되는 로직인 것 같다.
(1) publish
class pub{ construtor(state){ this.observers = new Set(); this.state = state; Object.keys(state).forEach(key => Object.defineProperty(this,key,{ get : () => this.state[key] })); } setState(newState){ this.state = { ...this.state, ...newState}; this.notifyAll(); } register(subscriber){ this.observers.add(subscriber); } notifyAll(){ this.observers.forEach(fn => fn()); } }
state에 변화가 생기면( setState내부에서 ) 구독자들에게 알리는 것( notifyAll을 실행 ) 이 핵심 내용이다.
( 2 ) Subscriber
class sub{ constructor(fn){ this.fn = fn; } subscribe(pub){ pub.register(this.fn); } }
구독자는 pub의 상태가 변화될때 실행할 함수를 저장해주어야 한다.(fn)
그리고 발행기관을 구독한다.( 3 ) 적용
const store = new pub({ a : 10, b : 20, } const addCal = new sub(() => console.log(store.a + store.b)); const minusCal = new sub(() => console.log(store.a - store.b)); addCal.subscribe(store); minusCal.subscribe(store); store.notifyAll(); // 30 , -10 store.setState({ a : 100, b : 20}); // 120 , 80
Object.defineProperty
사실 이번 추석 연휴때 property를 공부하다가 2일 동안 손을 놨다.
생각보다 많은 양의 학습할 내용이 존재했기 때문이였는데, 그 시작 부분이 이 부분이였다.let a = 10; const state = {}; Object.defineProperty(state,'a',{ get() { console.log(`현재 a의 값은 ${a} 입니다.`) return a; }, set(value){ a = 100; console.log(`변경된 a의 값은 ${a}입니다.`) } }); console.log(`State.a = ${state.a}`); state.a = 100;
console.log(`State.a = ${state.a}`); 부분에서 state.a를 접근하여 get 메서드가 실행된다.
여기서 console.log(`현재 a의 값은 ${a} 입니다.`) 가 실행되고 return 값으로 a 를 반환하여
console.log(`State.a = 10`)을 출력하게 된다.
이후 state.a = 100; 코드가 실행되어
set(100) 메서드가 실행된다.Object.defineProperty(object, prop, descriptor)
- object 속성을 정의할 객체 (ex : state)
- prop 새로 정의하거나 수정하려는 속성의 이름 (ex : 'a' )
- descriptor 새로 정의하거나 수정하려는 속성을 기술하는 객체 ( ex : get() , set(value) )
[ 여러개 속성 값 관리하기 ]
const state = { a : 10, b : 20, }; const stateKeys = Object.keys(state); for ( const key of stateKeys) { let value = state[key]; Object.defineProperty(state,key, { get(){ console.log(`현재 state.${key}의 값은 ${value}입니다.`); return value; } set(newValue){ value = newValue; console.log(`변경된 state.${key}의 값은 ${value}입니다.`); } }) } console.log(`a+b = ${state.a + state.b}`); state.a = 100; state.b = 200;
console.log(`a+b = ${state.a + state.b}`); 로 인해
console.log(`현재 state.a의 값은 10 입니다.`);
console.log(`현재 state.b의 값은 20 입니다.`); 가 출력되고
return 값으로 각각 state.a , state.b를 넣어줌으로써,
console.log(a+b = 30)이 출력된다.state.a = 100으로 인해
set(100) 메서드가 실행되어
console.log(`변경된 state.a의 값은 100입니다.`); 가 출력되고
state.b = 200으로 인해
set(200) 메서드가 실행되어
console.log(`변경된 state.b의 값은 200입니다.`); 가 출력된다.[ console 을 observer로 바꾸기 ]
const state = { a : 10, b : 20, } const stateKeys = Object.keys(state); const observer = () => console.log(`a+b = ${state.a + state.b}`); for ( const key of stateKeys) { Object.defineProperty(state,key,{ value = state[key] get(){ return state[key] } set(newValue){ value = newValue; observer(); } }) } observer(); state.a = 100; state.b = 200;
[ 여러 개의 Observer 관리하기 ]
let currentObserver = null; const state = { a : 10, b : 20, }; const stateKeys = Object.keys(state); for( const key of stateKeys ) { let _value = state[key]; const observers = new Set(); Object.defineProperty(state,key,{ get(){ if ( currentObserver ) observers.add(currentObserver); return _value; } set(value){ _value = value; observers.forEach(observer => observer()); } }) } const addCal = () => { currentObserver = addCal; console.log(`a+b = ${state.a + state.b}`); } const minusCal = () => { currentObserver = minusCal; console.log(`a - b = ${state.a - state.b}`); } addCal(); state.a = 100; minusCal(); state.b = 200; state.a = 1; state.b = 2;
addCal()
console.log(`a+b = ${state.a + state.b}`); 실행으로
get() 메서드 실행
key = a => observers에 currentObserver( = addCal ) 저장
key = b => observers에 currentObserver( = addCal ) 저장
console.log(`a+b = 30`); 출력state.a = 100; 실행으로
set(100) 실행
observers에 저장된 observer ( = addCal ) 실행
console.log(`a+b = 120`);minusCal()
currentObserver = minusCal
console.log(`a - b = ${state.a - state.b}`); 로 인해서 get 메서드 실행
observers에 minusCal 등록
state.b = 200; 실행으로
set(200) 메서드 실행
observers에 저장된 observer( addCal , minusCal ) 실행[ 함수화 ]
재사용을 위하여 위에 작성한 코드를 함수화 하여야한다.
let currentObserver = null; const observe = fn => { currentObserver = fn; fn(); currentObserver = null; } const observable = obj => { Object.keys(obj).forEach(key => { let _value = obj[key] const observers = new Set(); Object.definePropert(obj,key,{ get(){ if(currentObserver) observers.add(currentObserver); return _value; } set(value){ _value = value; observers.forEach(fn => fn()); } }) }) return obj; }
const store = observable({a : 10, b : 20}); observe(() => console.log(`a = ${store.a}`); observe(() => console.log(`b = ${store.b}`); observe(() => console.log(`a + b = ${store.a + store.b}`); observe(() => console.log(`a - b = ${store.a - store.b}`); store.a = 100; store.b = 200;
observe(fn) 실행
currentObserver = fn저장
fn() 실행 도중 store.a로 get에 접근
observers ( set ) 에 currentObserver 저장store.a = 100; , store.b = 200; 으로
set(100), set(200) 실행
다시 fn() 실행https://github.com/jin-Pro/JavaScript-Component-CSR/tree/master/Store-Component/Basic
DOM 접근하기
Flux Pattern
Flux의 가장 큰 특징은 단방향 데이터 흐름이다.
데이터의 흐름은
- Dispatcher -> Store
- Store -> View
- View -> Action
- Action -> Dispatcher
즉, View에서 Action이 발생하면 Dispatcher는 Action에 따라 Store의 값을 변경시키고 Store를 구독하는 View는 render를 진행한다.
Vue는 조금 변형하여 다음과 같은 형태로 사용한다.
- actions, mutations, state 를 묶어서 store라고 보면 된다.
- state를 변화시킬 수 있는 것은 오직 mutations 이다.
- actions는 backend api를 가져온 다음에 mutations를 이용하여 데이터를 변경한다.
- state가 변경되면, state를 사용 중인 컴포넌트를 업데이트한다.
Vuex
[ 공식 문서 ]
const store = new Vuex.Store({ state : { count : 0 }, mutations : { increment (state) { state.count++ } } });
state가 있고, state를 변경시킬 수 있는 mutations 가 존재하는 것을 확인 할 수 있다.
store는 다음과 같이 사용된다.
store.commit('increment') console.log(store.state.count); // 1
https://github.com/jin-Pro/JavaScript-Component-CSR/tree/master/Store-Component/Vuex
Redux
[ 공식 문서 ]
import { createStore } from 'redux' function counter(state = 0, action){ switch(action.type){ case 'INCREMENT': return state + 1 case 'DECREMENT': return state -1 default : return state } } let store = createStore(counter) store.subscribe(() => console.log(store.getState())) store.dispatch({ type : 'INCREMENT' }) //1 store.dispatch({ type : 'INCREMENT' }) //2 store.dispatch({ type : 'DECREMENT' }) //1
createStore가 subscribe, dispatch , getState 등의 메소드를 가진 객체를 반환한다.
const createStore = (reducer) => { return { subscribe , dispatch , getState }; }
https://github.com/jin-Pro/JavaScript-Component-CSR/tree/master/Store-Component/Redux
'Vanilla js' 카테고리의 다른 글
innerHTML , innerText , appendChild (0) 2021.09.19 Observer 패턴 (0) 2021.09.18 MVC 패턴 (0) 2021.09.18 Vanilla Javascript로 가상돔(VirtualDOM) 만들기 (0) 2021.09.18 Vanilla Javascript로 웹 컴포넌트 만들기 (0) 2021.09.18