[TIL CS]옵저버패턴 FE에 녹이기 (feat.상태관리라이브러리 구현)

조민수·2022년 9월 20일
1

cs

목록 보기
4/5

2022-09-03
옵저버패턴에 대하여 공부후 실제 해당 패턴을 어떻게 프론트엔드상에서 쓸 수 있는지 한 번 정리 후 적용을 위해 남겨놓을려고 한다

🚩옵저버패턴?

위키피디아식 정의는 이와 같다

옵저버 패턴은 객체의 상태 변화를 관찰하는 관찰자들, 즉 옵저버들의 목록을 객체에 등록하여 상태 변화가 있을 때마다 메서드 등을 통해 객체가 직접 목록의 각 옵저버에게 통지하도록 하는 디자인 패턴

옵저버패턴을 쉽게 이해하기 위해서는 딱 두 가지의 해당요소만 기억하고 있으면 쉽다

Subject(즉 관찰이 되어지는 대상)

그리고 subject는 구독,구독해제,알려주기 기능을 갖고있으면 된다!

Observer(즉 관찰을 하는 대상)

이 뜻을 간단하게 풀어보면 우리가 관찰하는 Subject라는 대상에 변화가 생기게 되면 이 변화에 대한 알림을 이 Subject를 관찰하고 있는 모든 Obsever에게 그 변한 사실을 알려주는 것이다

이것에 대한 예시를 보면
바로 자바스크립트에서 사용하는 이벤트리스너를 들어보면 이해가 쉽다

이벤트리스너

이벤트리스너에서 우리는 addEventListener를 통하여 어떤 특정 이벤트가 발생하게 되면 콜백을 실행해주듯이 이벤트(Subject)를 구독하여 변화가 감지될 때마다(이벤트가발생)하면 옵저버(콜백)에게 알려줘서 실행되는 원리==>옵저버패턴

👨‍💻Prototype으로 구현하기

function Subject(){
  this.observers=[];
  //이 해당 리스트에서는 Subject를 구독하고 있는 Observer들의 정보가 담김
}

Subject.prototype={
  registerObserver:function(observer){
    this.observers.push(observer);
  },
  unregisterObserver:function(observer){
    this.observers=this.observers.filter(registerObserver=>registerObserver!==observer);
  },
  notifyObservers: function(){
    this.observers.forEach(observer=>observer.notify("변화감지"))
  }
}

const subj=new Subject();
//subject객체 생성

const observer1={notify:data=>console.log('first '+data)}
const ovserver2={notify:data=>console.log('second '+data)}
//옵저버객체들을 생성

subj.registerObserver(observer1);
subj.registerObserver(ovserver2);
//등록하기

이와 같이 Subject객체의 Prototype으로 따로 쓸수있는 메소드를 따로 빼줌

👨‍💻Class문법으로 구현


class Subject{
  constructor(){
    this.observers=[];
  //이 해당 리스트에서는 Subject를 구독하고 있는 Observer들의 정보가 담김
  }
  registerObserver(observer){
    this.observers.push(observer);
  }

  unregisterObserver(observer){
    this.observers=this.observers.filter(registerObserver=>registerObserver!==observer);
  }
  
  notifyObservers(){
    this.observers.forEach(observer=>observer.notify("변화감지!"))
  }
  
}


const subj=new Subject();
//subject객체 생성

const observer1={notify:data=>console.log('first '+data)}
const ovserver2={notify:data=>console.log('second '+data)}
//옵저버객체들을 생성

subj.registerObserver(observer1);
subj.registerObserver(ovserver2);
//등록하기

subj.notifyObservers();
//변화된 객체를 알려줌

🙋‍그럼 상태관리라이브러리 구현해보기

사실 우리가 흔히 쓰고 있는 Redux나 ContextAPI에서는 이미 상태변화에 구현원리를 기본적으로 옵저버패턴으로 구현이 된 것를 쓸 줄만 알지 기본원리를 모르는게 현실이다 따라서 한 번 옵저버패턴을 적용하며
직접 구현해보자

시나리오는 이렇다
1.상태를 저장해둘 store객체를 만든다
2.특정 action을 행하면 state값을 변경하게 하도록 한다
3.해당 store객체에서의 상태값이 변하면 그 state에 해당하는 render와 같은 이벤트를 발동시켜준다

사실 기본적인 flux의 패턴인
action->store->dispatcher->view의 형태의 꼴이다

우선 store를 만들어보자!
스토어에 필요한 핵심요소를 정리해보자!

  1. 상태들을 저장해줄 Store라는 변수
  2. 각 구독하고 있는 action들을 저장해줄 배열
  3. 옵저버패턴에서 한 것처럼 특정 action에 해당하는 상태가 바뀌면 리랜더링을 시켜줄 Subscribe함수
  4. 실제 구독되어진 action들에 대한 콜백이벤트(리랜더링)을 호출시켜주는 publish함수
  5. 실제 액션을 담아주기 위한 dispatch함수
  6. (추가적인)현재 store에 저장되어진 state를 반환해주는 getState함수

👨‍💻Store 구현하기

export const createStore=(initialState,reducer)=>{
    let state=initialState;
    const events={};
    //각각의 action에 맞는 이벤트들을 저장해주는 곳-옵저버패턴에 의하면 자기를 관찰하는 옵저버들의 리스트


    //특정 이벤트가 변경시 해당 콜백을 달라는 것을 등록하는것
    //옵저버패턴에서의 구독이랑 같은 느낌
    //상태관리 라이브러리에서는 상태변화를 감지하여 해당 상태를 쓰는애들에 대하여 재랜더링을 시켜주는 용도
    const subscribe=(actionType,evventCallBack)=>{
        if(!events[actionType]){
            events[actionType]=[];
            //아직정의되지 즉 구독되지 않았던 이벤트이면 초기화
        }
        events[actionType].push(evventCallBack);

    }

    //실제로 상태변경시 해당 함수를 불러 이미 저장되어있는 콜백행동들을 행동시킴
    const publish=(actionType)=>{
        if(!events[actionType]){
            return;
        }
        events[actionType].map(ev=>ev());
        //해당 액션이 갖고있던 콜백이벤트들을 진행시켜줌
    }

    const dispatch=(action)=>{
        state=reducer(state,action);
        //인자로 받은 reducer로 state를 받은 액션을 기준으로 state를 변경시켜줌
        publish(action.type);
        //해당 action에 대한 콜백이벤트를 발동-리액트 상에서는 state가 변경되었으니 리랜더링을 호출해주는 느낌

    }

    const getState=()=>state;
    //현재 스토어상에 저장되어있는 상태를 알려주는 함수

    return{
        getState,
        subscribe,
        dispatch
    }
    
}

👨‍💻reducer 구현하기

우선 리듀서를 구현하기 핵심 요소들을 정리해보자면

1.우리가 실제로 사용할 action들의 타입들을 정리
2.액션들을 만들어주기
3. reducer제작으로 각 액션타입별로 state를 바꿔주기

import { createStore } from "./createStore.js";

const initialState=[];
//기본적으로 여러가지 state를 관리하기 위한 배열을 디폴트로 줌

//action 타입
const ADD_TODO="add_todo";
const GET_TODOS="get_todos";

//action 만들기

export const add_todo=()=>({
    type:ADD_TODO,
    data
});

export const get_todos=()=>({
    type:GET_TODOS  
});

//reducer 만들기

const reducer=(state=initialState,action)=>{
    switch(action.type){
        case ADD_TODO:
            return [...state,action.data];
        case GET_TODOS:
            return state;
        default:
            return state;
    }
}

export const todoStore=createStore(initialState,reducer);
//만든 스토어를 호출하여 하나의 스토어를 생성해준다

🖥이제 스토어와 리듀서의 구현이 마쳤으니 사용해보자!

import {todoStore,add_todo,get_todos} from "./reducer.js";


console.log(todoStore.getState());
//처음에 스토어가 가지고있는 state
todoStore.subscribe(add_todo.type,()=>alert("나는 리랜더링중~"));
//등록해주기

todoStore.dispatch(add_todo("이거 추가해주셈"));
//action을 불러와서 시켜보기

console.log(todoStore.getState());
//dispatch후 가지고 있는 state


실제 해당상황은 마치 리덕스에서 우리가 썼던것처럼 똑같이 dispatch를 통하여
특정 action을 호출하는 상황과 같다는 것을 알 수 있다

단지 redux에서는 우리가 직접 해주지 않았던 subscribe에 대한 것들은 위에서 명시적으로 호출을 해주어 리랜더링을 등록해주는 것을 알 수 있다

단지 여기서만 리랜더링이라는 함수구현보다는 alert()로 콜백을 주었다

🙌리랜더링함수는 원래 따로 구현해주자!

(해당 내용까지 한 내용은 따로 웹컴포넌트 관련 공부 포스팅 때 올려보겠다)

😲결과

실제로 나는 action을 취해준것밖에 없지만 해당 콜백이 자동으로 실행되는 것을 알 수 있다 왜냐하면 subscribe를 통해 저장해둔 action인 add_todo가 dispatch에 의해
state가 변경되고 그에 따른 publish()가 호출된것이다

state 또한 상태업데이트가 잘 이루어진 것을 볼 수 있다


결론적으로 실제 우리가 프론트엔드에서 쓰고 있는 상태관리라이브러리인 redux나 contextAPI는 옵저버패턴으로 구현된 것을 알 수 있었다

한 번쯤 이런구현원리를 알고나서 쓰는건 정말 좋은 기회인 것 같다

profile
컬러감이 있는 프론트엔드개발자

0개의 댓글