Redux > subscribe()

업무 때문에 Redux 코드를 들여다 보다가 subscribe() 구성이 재미있게 되어서 정리를 해보았습니다. (TS는 아직 익숙하지 않아서 3.7.2 버전 기준입니다. )

이벤트 등록/해제

이벤트 저장소는 배열(listeners = [])입니다. 등록되는 listener들을 subscribe()로 등록을 하고 등록을 해제하는 함수를 리턴해 줌으로서 listener들을 개별적으로 관리하는 작업을 사용자 측이 관리하도록 처리를 넘겼습니다. 중복 등록 해제를 제거하기 위해서 isSubscribed 변수를 두어서 등록해제 하였는지를 체크하고 unsubscribe 함수의 지역 변수로 관리하는 형태를 취하고 있네요. (쉽게 설명하기 위해서 current/next listeners를 하나로 통합하였습니다.)

function createStore () {
  let listeners = []

  function subscribe (listener) {
    let isSubscribed = true
    listeners.push(listener)

    return function unsubscribe () {
      if (!isSubscribed) {
        return
      }
      isSubscribed = false

      const index = listeners.indexOf(listener)
      listeners.splice(index, 1)
    }
  }

listeners의 변조 방지

아마도, store의 이벤트를 청취하고 있는 listener 중 일부는 state가 변경된 경우 동적으로 청취를 멈추거나 추가하고 싶은 코드가 있었고요. 그런데 이 변경이 발생하는 도중에 dispatch()가 호출이 되면서 변경된 배열에 의한 이벤트 청취를 못하게 되는 경우가 발생하지 않았을까 싶은데요.

ensureCanMutateNextListeners()를 들여다보면 현재의 listeners들을 slice()함수로 새로운 배열로 생성을 합니다. 이미 nextListeners를 지정한 경우에는 최적화를 위해서 한 번만 생성하는 체크가 되어 있습니다.

  let currentListeners = []
  let nextListeners = currentListeners

  function ensureCanMutateNextListeners() {
    if (nextListeners === currentListeners) {
      nextListeners = currentListeners.slice()
    }
  }

이 함수는 listener가 추가/삭제되는 경우 먼저 호출이 되어 현재 dispatch()에서 처리되고 있는 listeners들에 영향을 미치지 않게 listener를 nextListeners에 추가/삭제하게 해줍니다.

  function subscribe (listener) {
    let isSubscribed = true

    ensureCanMutateNextListeners()
    nextListeners.push(listener)

    return function unsubscribe () {
      if (!isSubscribed) {
        return
      }
      isSubscribed = false

      ensureCanMutateNextListeners()
      const index = nextListeners.indexOf(listener)
      nextListeners.splice(index, 1)
    }
  }

dispatch()의 코드도 살짝 보면 dispatch() 호출 시에 nextListeners를 currentListeners로 최신 청취자들로 갱신을 한 다음에 지역 변수에 담아서 dispatch 호출시에 등록되어 있던 listener들에게 이벤트 변경이 되었음을 알려주게 됩니다.

  function dispatch (action) {
    const listeners = currentListeners = nextListeners
    for (let i = 0; i < listeners.length; i++) {
      const listener = listeners[i]
      listener()
    }
    return action
  }

출처:
https://github.com/reduxjs/redux/blob/v3.7.2/src/createStore.js#L101
– Vuex: https://github.com/vuejs/vuex/blob/329828dba41dec59a6880e69a32f9da36c62f3bd/src/store.js#L105