노션클론 리팩토링(2) - 옵저버패턴을 리덕스로, npm 오류

김영현·2023년 11월 5일
0

npm WARN config global --global, --local are deprecated. Use --location=global instead.

별거아닌 오류같은데 오래된 명령어가 저장되어있나보다.
노트북이아닌 PC에서 개발작업을 한지 오래되서 그런듯?
윈도우니까 powerShell열고
npm install --global --production npm-windows-upgrade입력하니 되었다.


왜 리덕스인가?

전역상태를 어떻게 관리해야할까 고민이 많았다.
웹 프론트엔드 디자인패턴 변천사에도 나왔듯, 컴포넌트의 깊이가 깊어질수록 props drilling은 심해졌다.
전달해야하는 상태도 각양 각색. 보기 힘든게 사실이었다.
물론 내가 진행하고있는 수준에선 전혀 문제될 것이 없었지만...
재밌어 보였음!이게 진짜 이유고.
나중에 당연히 상태 관리라이브러리를 쓰게 될텐데, 왜 쓰는지 어떤 방식으로 쓰는지 몹시 궁금할 것 같아서 이렇게 시간이 널널할때 확인하려한다.


옵저버패턴

class Observer {
  constructor() {
    this.subscribers = new Set();
  }
  subscribe(observerCallback) {
    this.subscribers.add(observerCallback);
  }
  unsubscribe(observerCallback) {
    this.subscribers.delete(observerCallback);
  }
  notify(data) {
    this.subscribers.forEach((subscriber) => subscriber(data));
  }
}
export default Object.freeze(new Observer());

조촐한 옵저버 패턴.
중복방지용 set으로 구독자(객체)들의 콜백을 받아와 알람이 발생되면 단지 콜백을 실행시켜준다.
이를 기반으로 리덕스의 FLUX패턴을 만들어서 사용해보고싶다.

참고로 FLUX패턴은 단방향 흐름이다.
Action을 Dispatch하여 Reducer에서 View로 보낸다.


리덕스

리덕스는 단 하나의 스토어를 사용하여 여러개의 리듀서를 관리한다.
여기서 리듀서를 주목하자. 이놈이 핵심이다.
리듀서초기 상태를 가지며 액션에따라 변화된 상태를 리턴한다(기존 상태가 아니다.)
따라서 리듀서가 결국 각 상태를 갖고있고 상태변화를 진행하는함수를 가지고있다고 보면 된다.
=> 상태 변화를 일으키는 녀석은 결국액션 함수다. 헷갈리지 말자. 또한 액션 함수리듀서에 전달해주는녀석이 디스패치다.

공식문서에 나와있는 예제를 보면서 천천히 따라가보자.

import { createStore } from 'redux'

/**
 * 이것이 (state, action) => state 형태의 순수 함수인 리듀서입니다.
 * 리듀서는 액션이 어떻게 상태를 다음 상태로 변경하는지 서술합니다.
 *
 * 상태의 모양은 당신 마음대로입니다: 기본형(primitive)일수도, 배열일수도, 객체일수도,
 * 심지어 Immutable.js 자료구조일수도 있습니다.  오직 중요한 점은 상태 객체를 변경해서는 안되며,
 * 상태가 바뀐다면 새로운 객체를 반환해야 한다는 것입니다.
 *
 * 이 예제에서 우리는 `switch` 구문과 문자열을 썼지만,
 * 여러분의 프로젝트에 맞게
 * (함수 맵 같은) 다른 컨벤션을 따르셔도 좋습니다.
 */
function counter(state = 0, action) {
  switch (action.type) {
    case 'INCREMENT':
      return state + 1
    case 'DECREMENT':
      return state - 1
    default:
      return state
  }
}

// 앱의 상태를 보관하는 Redux 저장소를 만듭니다.
// API로는 { subscribe, dispatch, getState }가 있습니다.
let store = createStore(counter)

// subscribe()를 이용해 상태 변화에 따라 UI가 변경되게 할 수 있습니다.
// 보통은 subscribe()를 직접 사용하기보다는 뷰 바인딩 라이브러리(예를 들어 React Redux)를 사용합니다.
// 하지만 현재 상태를 localStorage에 영속적으로 저장할 때도 편리합니다.

store.subscribe(() => console.log(store.getState())))

// 내부 상태를 변경하는 유일한 방법은 액션을 보내는 것뿐입니다.
// 액션은 직렬화할수도, 로깅할수도, 저장할수도 있으며 나중에 재실행할수도 있습니다.
store.dispatch({ type: 'INCREMENT' })
// 1
store.dispatch({ type: 'INCREMENT' })
// 2
store.dispatch({ type: 'DECREMENT' })
// 1

이름만 듣고 처음엔 createStore가 상태를 만들줄 알았지만, 리듀서가 먼저 선언하고있다.
또한 Store가 갖고있는 API는 총 3가지다.

  • subscribe : 옵저버패턴의 구독기능과 똑같다.
  • dispatch : 액션 함수리듀서에 전달하는 기능을 가진다.
  • getState : 현재 상태를 준다. 아마 getter로 값만 가져오는 기능을 가졌을 것이다.

이들부터 하나씩 구현해보도록하겠다.
먼저 subscribe부분이다. 이를 구현하기위해 옵저버패턴의 구독기능을 활용할 것이다.
다만 싱글톤 패턴으로 활용하던 옵저버 클래스를 없앨 필요가 있어보인다.

export default Object.freeze(new Observer());
=> export default Observer;

이렇게 되면 자연스레 옵저버 객체를 사용하던 부분들에 오류가 나게된다.
따라서 잠깐 임시로 옵저버 객체main.js에 인스턴스화 하여서 사용하도록 처리했고.
다시 리듀서의 subscribe로 넘어가보자.

  const observer = Object.freeze(new Observer());
  const state = reducer();
  const subscribe = () => {
    observer.subscribe();
  };
  return { subscribe, dispatch, getState };

이렇게 작성하던 와중, 의문이 듬.
리듀서가 반환해준 state가 바뀌었을때 알림이 가야한다.
하지만 나의 조촐한 옵저버패턴의 내부엔 그런 기능이 없었다. 추가해보자.
추가하면서 아예 역할을 나누어보자.

  • Observable : 관찰 가능한 객체를 의미한다.
  • Observer : 관찰가능한 객체를 관찰하는 객체. 말장난같지만 그냥 구독자다.

구현해야겠지?

Observable, Observer

일단, 관찰 가능한 객체가 가져야하는 기능을 열거해보자.

  1. 구독자들을 저장해놓아야 함.
  2. 자신을 구독할 수 있는 기능.
  3. 구독을 해제할 수 있는 기능.
  4. 알림 기능
  5. 자신의 상태에 변화가 생겼을때 구독자들에게 알려주는 기능

당장 4번까지는 그대로다. 생각보다 수월하겠구나?
대신 subscribers를 분리할 필요가 있어보인다. Observer역할도 따로 빼서 운용할 것이니까.
=> 역할을 나누어서 관리하면 유지보수가 편하고 수정할때 덜 애먹는다. 경험상...

//Observable.js
import getDeepCopy from "../getDeepCopy.js";

export default class Observable {
  #observers = new Set();
  //리액트에서도 자주 발생하는 깊은복사 문제가 생길수도 있으니, 자체 제작한 깊은복사 함수를 사용했다.
  constructor(state) {
    this.state = getDeepCopy(state);
  }
  addSubscriber(observerCallback) {
    this.#observers.add(observerCallback);
  }
  unsubscribe(observerCallback) {
    this.#observers.delete(observerCallback);
  }
  notify() {
    this.#observers.forEach((subscriber) => subscriber());
  }
  changedState(nextState) {
    this.state = { ...getDeepCopy(this.state), ...getDeepCopy(nextState) };
    this.notify;
  }
}
//Observer.js
export default class Observer {
  constructor(callback) {
    this.callback = callback;
  }
  subscribe(observable) {
    observable.addSubscriber(this.callback);
  }
}

이렇게 둘을 분리한뒤, 다시생각해보니 reducersubscirbe는 결국 구독하면서 함수를 전달한다..분리할 필요가 없었다. 일단 Observer는 봉인해두겠다.

createStore 완

import Observable from "../observer/Observable.js";

export const createStore = (reducer) => {
  const observable = Object.freeze(new Observable());
  const state = reducer();
  observable(state);
  const subscribe = (callback) => {
    observable.subscribe(callback);
  };
  return { subscribe, dispatch, getState };
};

따란~! subscirbe가 간단하게 완성되었다.
이번엔 dispatch, getState를 구현해보자. dispatch는 액션함수를 리듀서에 넘겨주는 고차함수다.
또한 리덕스의 상태는 직접참조가 불가능해야하기에 getState를 사용하면 얼려진 객체를 반환하게 하였다.(일단 이렇게만...나중에 재귀적으로 얼려보자)

import Observable from "../observer/Observable.js";

export const createStore = (reducer) => {
  const observable = Object.freeze(new Observable());
  const state = reducer();
  observable(state);
  const subscribe = (callback) => observable.subscribe(callback);
  const dispatch = (action) => reducer(state, action);
  const getState = () => Object.freeze(state);
  return { subscribe, dispatch, getState };
};

이렇게 간단하게 만들어보았다.

combineReducers

사실 리덕스를 사용할때 쯤이면 리듀서는 하나가 아니다.
따라서 여럿 리듀서를 하나의 리듀서로 묶는 combineReducers가 필요하다.

공식문서에 따르면 이렇다.
undefined로 주어지면 초기상태를 반환해야하는 이유는아마 이럴 것 같다.
combineReducers리듀서들을 묶어서 하나의 리듀서로 합칠때, undefined를 넣어서 초기상태를 얻으려는 목적 아닐까?

또한, 액션함수가 겹칠까봐 모듈화(상수화)시킨다는 건 분명 이런 구조일것이다.

import getDeepCopy from "../getDeepCopy.js";

export const combineReducers = (reducers) => {
  //리듀서들은 객체에 하나씩 담겨 온다.
  const reducerKeys = Object.keys(reducers);
  //state들의 key는 리듀서 이름으로 된다.
  const combinatedReducer = (state = {}, action) => {
    const nextState = {};
    reducerKeys.forEach((reducerKey) => {
      const reducer = reducers[reducerKey];
      const prevStateForKey = state[reducerKey];
      const nextStateForKey = reducer(prevStateForKey, action);
      nextState[reducerKey] = nextStateForKey;
    });
    return nextState;
  };
  return combinatedReducer;
};

간단하게 combineReducers를 구현할수 있었다. 실제로 제대로 작동하는지는 내일이나 화요일에 테스트 해봐야될것 같다...!


느낀점

바퀴의 재발명은 금지하라지만, 필요에 의해 라이브러리나 프레임워크처럼 만들며 공식문서도 참고하고 내부구조를 살펴보는건 생각보다 더 유익하다!
차차 개선해 나가며 나만의 리덕스를 프로젝트에 적용시켜봐야겠다!
이맛에 리팩토링 하는구먼?

profile
모르는 것을 모른다고 하기

0개의 댓글