ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 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

     

    GitHub - jin-Pro/JavaScript-Component-CSR

    Contribute to jin-Pro/JavaScript-Component-CSR development by creating an account on GitHub.

    github.com


    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

     

    GitHub - jin-Pro/JavaScript-Component-CSR

    Contribute to jin-Pro/JavaScript-Component-CSR development by creating an account on GitHub.

    github.com

     

     

     


    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

     

    GitHub - jin-Pro/JavaScript-Component-CSR

    Contribute to jin-Pro/JavaScript-Component-CSR development by creating an account on GitHub.

    github.com

     

    [ 학습 출처 ] : https://junilhwang.github.io/TIL/Javascript/Design/Vanilla-JS-Store/#_1-%E1%84%8C%E1%85%AE%E1%86%BC%E1%84%8B%E1%85%A1%E1%86%BC-%E1%84%8C%E1%85%B5%E1%86%B8%E1%84%8C%E1%85%AE%E1%86%BC%E1%84%89%E1%85%B5%E1%86%A8-%E1%84%89%E1%85%A1%E1%86%BC%E1%84%90%E1%85%A2%E1%84%80%E1%85%AA%E1%86%AB%E1%84%85%E1%85%B5

    '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

    댓글

Designed by Tistory.