원티드 프리온보딩 챌린지 1주차 선택과제

HR.lee·2022년 8월 14일
0

원티드

목록 보기
4/9

1. Redux 레포지토리에서 코드 분석하고 직접 scratch 작성해보기

  • 진행중!

1) Redux의 컨셉을 학습한다.
레포에 가서 코드를 뜯어다 분석하기

https://github.com/reduxjs/redux/blob/master/src/createStore.ts

그치만 원문만 보고 이해하는 것은 무리였다... 번역본은 밑에.

2) createStore의 최소 구현체를 직접 작성해본다.
- creactStore란?
- 1) createStorereducer를 인자로 받아 store를 리턴하는 함수다
- 2) storesubscribe(), dispatch(), getState()를 메서드로 가진 객체다
- 3) reducer는 createStore의 내부 상태인 state와, action 객체를 인자로 받아 action type에 따라 로직을 처리한 후 새로운 state를 리턴하는 함수다.

일단 검색을 해봤다.

https://yeoulcoding.me/145
https://wonit.tistory.com/344

  1. 리듀서 만들기
  • state와 action을 받아서 action type에 따라 로직을 처리한 후 새로운 state를 리턴하는 함수

State

  • state : 상태이다. 상태트리라고 적어두는 것 같다. 외부에서 직접적으로는 변경이 불가능하고 createStore를 통해 getState라는 메서드를 사용해야만 접근이 가능하다.

  • 이때 getState는 클로저이다.

  • 이렇게 복잡한 방식을 쓰는 이유는 불변성(immutability)을 지키기 위해서.

  • 단순히 객체리터럴로 상태를 덮어쓰게 되면 수많은 컴포넌트에서 상태를 관리하는 것이 힘들고 어느 시점에 어떤 상태를 바꾸었는지를 감지하기 위해 많은 자원을 소모하게 된다.

  • 그래서 리덕스는 ... <- spread 연산자를 사용해서 기존 state 객체를 얕은 복사하여 새로운 참조를 리턴한다.

const updatedState = {...data, newData}
  • 이렇게 업데이트하면 참조가 바뀌었을때 상태변경을 감지할 수 있다.

Action

  • 이제 변경을 하려면 action이 필요하다.

  • action : action은 키다. 문자열, 전부 대문자로, 그리고 상수로 지정해두는 것이 규칙이다. 키에 오타가 나면 안되기 때문에 만든 규칙이다.

dispatch

  • action type에 따라 dispatch로 state를 바꿀 수 있다.

subscribe

  • 스토어 내부에서 dispatch가 실행되어 state가 변경되는 것을 감지해주는 메서드다.
// 스토어 생성
export function createStore(reducer) {

const state = {}
const listeners = []
const getState = () => ({...state})

const dispatch = (action) => {
	state = reducer(state, action);
  	listeners.forEach((listener) => listener());
}

const subscribe = (listener) => {
 listeners.push(listener);
  return () => {
    listeners =  listeners.filter((l) => l !== listener);
  }
}

return {getState, dispatch, subscribe}
}
// 실제 사용
import { createStore } from "./redux"

const CHANGE = "CHANGE"
let initState = {name : world, level : 1}

function reducer(state = initState, action) {
	if (action.type === "CHANGE") {
    return { ...state, state.level : state.level + 1)
    };
  return state;
}

function actionCreator(type) {
	return {type: type}
}

const store = createStore(reducer)


function levelUp() {
	store.dispatch(actionCreator(CHANGE))
}

levelUp()

번역문 정리


import $$observable from './utils/symbol-observable'

import {
  Store,
  PreloadedState,
  StoreEnhancer,
  Dispatch,
  Observer,
  ExtendState
} from './types/store'
import { Action } from './types/actions'
import { Reducer } from './types/reducers'
import ActionTypes from './utils/actionTypes'
import isPlainObject from './utils/isPlainObject'
import { kindOf } from './utils/kindOf'

/**
 * Creates a Redux store that holds the state tree.
 상태트리(state tree)를 보유하는 리덕스 저장소를 만듭니다.
 * The only way to change the data in the store is to call `dispatch()` on it.
 스토어의 데이터를 변경하는 유일한 방법은 dispatch()를 호출하는 것입니다.
 *
 * There should only be a single store in your app. To specify how different
 한 앱에는 한 스토어만 있어야합니다.
 * parts of the state tree respond to actions, you may combine several reducers
 여러 상태트리가 작업에 응답하도록 하려면 리듀서 여러개를 결합해야 할수 있습니다.
 * into a single reducer function by using `combineReducers`.
 combineReducers를 이용하면 리듀서를 하나로 합칠수 있습니다.
 *
 * @param reducer A function that returns the next state tree, given the current state tree and the action to handle
 reducer: 지금 상태트리와 주어진 액션을 사용해서 주어진 다음 상태트리를 반환하는 함수
 
 * @param preloadedState The initial state. You may optionally specify it to hydrate the state from the server in universal apps, or to restore a previously serialized user session.
 preloadedState : initial state라고도 합니다. 선택적으로 앱 서버에서 상태를 가져오거나 이전 세션을 복원하도록 지정할 수 있습니다.
 
 * If you use `combineReducers` to produce the root reducer function, this must be an object with the same shape as `combineReducers` keys.
 combineReducers로 루트리듀서를 만들려고 한다면 반드시 객체 형태에 combineReducers 키를 가지도록 하세요!
 *
 * @param enhancer The store enhancer. You may optionally specify it to enhance the store with third-party capabilities such as middleware, time travel, persistence, etc. The only store enhancer that ships with Redux is `applyMiddleware()`.
 enhancer : 인핸서. 스토어인핸서. 미들웨어, 지속성, 시간여행...? 등으로 스토어 기능을 향상시키기 위해 선택적으로 지정할수 있습니다.
 리덕스에 스토어 인핸서는 딱 하나 입니다. applyMiddleware().
 *
 * @returns A Redux store that lets you read the state, dispatch actions and subscribe to changes.
 데이터 변경을 위해 스테이트, 디스패치, 액션, 구독 기능을 제공하는 리덕스 스토어를 리턴합니다.
 */

export default function createStore<
  S,
  A extends Action,
  Ext = {},
  StateExt = never
>(
  reducer: Reducer<S, A>, 
  enhancer?: StoreEnhancer<Ext, StateExt> 
): Store<ExtendState<S, StateExt>, A, StateExt, Ext> & Ext

export default function createStore<
  S,
  A extends Action,
  Ext = {},
  StateExt = never
>(
  reducer: Reducer<S, A>,
  preloadedState?: PreloadedState<S>, <-- 달라진점: init state
  enhancer?: StoreEnhancer<Ext, StateExt>
): Store<ExtendState<S, StateExt>, A, StateExt, Ext> & Ext

export default function createStore<
  S,
  A extends Action,
  Ext = {},
  StateExt = never
>(
  reducer: Reducer<S, A>,
  preloadedState?: PreloadedState<S> | StoreEnhancer<Ext, StateExt>, <-- 달라진점: init state
  enhancer?: StoreEnhancer<Ext, StateExt>
): Store<ExtendState<S, StateExt>, A, StateExt, Ext> & Ext {
  if (
    (typeof preloadedState === 'function' && typeof enhancer === 'function') ||
    (typeof enhancer === 'function' && typeof arguments[3] === 'function')
  ) {
    throw new Error(
      'It looks like you are passing several store enhancers to '
      +
        'createStore(). This is not supported. Instead, compose them ' +
        'together to a single function. See https://redux.js.org/tutorials/fundamentals/part-4-store#creating-a-store-with-enhancers for an example.'
      /// 크리에이트 스토어를 할땐 모든 애들을 하나로 합쳐서 single function으로 내보내야 합니다.
    )
  }

  if (typeof preloadedState === 'function' && typeof enhancer === 'undefined') {
    enhancer = preloadedState as StoreEnhancer<Ext, StateExt>
    preloadedState = undefined
  }

  if (typeof enhancer !== 'undefined') {
    if (typeof enhancer !== 'function') {
      throw new Error(
        `Expected the enhancer to be a function. Instead, received: '${kindOf(
          enhancer
        )}'`
        /// 인핸서의 타입은 반드시 함수여야 합니다.
      )
    }

    return enhancer(createStore)(
      reducer,
      preloadedState as PreloadedState<S>
    ) as Store<ExtendState<S, StateExt>, A, StateExt, Ext> & Ext
  }

  if (typeof reducer !== 'function') {
    throw new Error(
      `Expected the root reducer to be a function. Instead, received: '${kindOf(
        reducer
      )}'`
      /// 리듀서의 타입도 반드시 함수여야 합니다.
    )
  }

  let currentReducer = reducer
  let currentState = preloadedState as S
  let currentListeners: (() => void)[] | null = []
  let nextListeners = currentListeners
  let isDispatching = false

  /**
   * This makes a shallow copy of currentListeners so we can use
   * nextListeners as a temporary list while dispatching.
   현재 리스너 객체를 얕은복사해서 다음 리스너를 디스패치하는 동안 임시로 사용할 수 있습니다.
   *
   * This prevents any bugs around consumers calling
   * subscribe/unsubscribe in the middle of a dispatch.
   디스패치가 구독/구독취소 될때 그 중간 어딘가에서의 요청에 의한 버그를 막아줍니다.
   */
  function ensureCanMutateNextListeners() {
    if (nextListeners === currentListeners) {
      nextListeners = currentListeners.slice()
    }
  }

  /**
   * Reads the state tree managed by the store.
   스토어에 의해 관리되고 있는 상태트리를 읽어오고.
   *
   * @returns The current state tree of your application.
   현재 앱이 가지고 있는 상태트리를 리턴합니다.
   */
  function getState(): S {
    if (isDispatching) {
      throw new Error(
        'You may not call store.getState() while the reducer is executing. ' +
          'The reducer has already received the state as an argument. ' +
          'Pass it down from the top reducer instead of reading it from the store.'
      )
      // 리듀서가 실행되는 동안 store.getState()를 호출할 수 없습니다. 리듀서가 이미 상태를 인자로 받았습니다. 스토어에서 바로 읽으려고 하지 말고 리듀서에 내려보내 전달하도록 하세요.
    }

    return currentState as S
  }

  /**
   * Adds a change listener. It will be called any time an action is dispatched, and some part of the state tree may potentially have changed. You may then call `getState()` to read the current state tree inside the callback.
   리스너를 추가하는 기능입니다. 액션이 디스패치될때마다 호출되며 상태트리의 일부가 변경되었거나 변경되었을 수 있습니다. 이걸 한 다음 getState()를 해서 현재 상태 트리를 콜백함수 안에서 읽을 수 있습니다.
   *
   * You may call `dispatch()` from a change listener, with the following
   이제 리스너를 이용해 디스패치 할 수 있습니다. 아래의 지시를 따르세요.
   * caveats:
   * 주의사항
   
   * 1. The subscriptions are snapshotted just before every `dispatch()` call.
   모든 구독은 디스패치 콜 직전에 스냅샷됩니다.
   * If you subscribe or unsubscribe while the listeners are being invoked, this will not have any effect on the `dispatch()` that is currently in progress.
   리스너가 호출되는 동안 구독/ 구독취소 하면 현재 진행중인 디스패치에는 아무 영향도 미치지 않습니다.
   * However, the next `dispatch()` call, whether nested or not, will use a more recent snapshot of the subscription list.
   그러나 다음 디스패치는 중첩여부에 관계없이 구독 리스트의 최신 스냅샷을 사용하게 됩니다.
   *
   * 2. The listener should not expect to see all state changes, as the state might have been updated multiple times during a nested `dispatch()` before the listener is called. It is, however, guaranteed that all subscribers registered before the `dispatch()` started will be called with the latest state by the time it exits.
   리스너가 호출되기 전에 디스패치가 중첩되어 여러번 업데이트 되었을 수 있으므로 리스너가 상태변경을 완벽하게 볼수 있다고 생각해서는 안됩니다.
하지만 디스패치가 콜되기 전에 생성된 모든 구독은 당시의 최신 상태입니다.
   *
   * @param listener A callback to be invoked on every dispatch.
   리스너 : 모든 디스패치에서 호출되는 콜백함수
   * @returns A function to remove this change listener.
   리턴값 : 리스너를 제거하는 함수
   */
  function subscribe(listener: () => void) {
    if (typeof listener !== 'function') {
      throw new Error(
        `Expected the listener to be a function. Instead, received: '${kindOf(
          listener
        )}'`
      )
    }

    if (isDispatching) {
      throw new Error(
        'You may not call store.subscribe() while the reducer is executing. ' +
        // 리듀서가 실행되는 동안 구독을 호출할수는 없습니다.
          'If you would like to be notified after the store has been updated, subscribe from a ' +
        // 스토어 업데이트가 끝난 후 구독을 원한다면
          'component and invoke store.getState() in the callback to access the latest state. ' +
        // 콜백에 store.getState()를 전달해서 최신 스테이트를 받을 수 있습니다.
          'See https://redux.js.org/api/store#subscribelistener for more details.'
      )
    }

    let isSubscribed = true

    ensureCanMutateNextListeners()
    nextListeners.push(listener)

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

      if (isDispatching) {
        throw new Error(
          'You may not unsubscribe from a store listener while the reducer is executing. ' +
          // 리듀서가 실행되는 동안 리스너의 구독을 취소할 수 없습니다.
            'See https://redux.js.org/api/store#subscribelistener for more details.'
        )
      }

      isSubscribed = false

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

  /**
   * Dispatches an action. It is the only way to trigger a state change.
   액션을 디스패치합니다. 스테이트를 변경할 수 있는 유일한 방법입니다.
   *
   * The `reducer` function, used to create the store, will be called with the current state tree and the given `action`. Its return value will be considered the **next** state of the tree, and the change listeners will be notified.
   리듀서 함수는 스토어를 생성하기 위해 현재 상태트리와 주어진 액션으로 호출됩니다. 리턴값은 상태트리의 다음 상태로 간주되며 리스너에 알림이 전달됩니다.
   *
   * The base implementation only supports plain object actions. If you want to dispatch a Promise, an Observable, a thunk, or something else, you need to wrap your store creating function into the corresponding middleware. For example, see the documentation for the `redux-thunk` package. Even the middleware will eventually dispatch plain object actions using this method.
   - 기본 리덕스는 일반 객체 작업만 지원합니다.
   - 프로미스, 옵저블, thunk 등 다른객체를 전달하고 싶다면 크리에이트 스토어를 해당 미들웨어에 래핑해야합니다. redux-thunk 패키지 같은걸 참고해보세요. 미들웨어도 결국 이 로직을 써서 일반객체를 디스패치하긴 합니다.
   *
   * @param action A plain object representing “what changed”. It is a good idea to keep actions serializable so you can record and replay user sessions, or use the time travelling `redux-devtools`. An action must have a `type` property which may not be `undefined`. It is a good idea to use string constants for action types.
   액션 : 변경된 사항을 나타내는 일반 객체, 문자열로 상수를 만들어 사용하는 것이 좋습니다. 사용자세션을 기록하고 재생할수 있도록 유지하는 애입니다.
   리덕스 데브툴을 같이 사용하면 좋습니다.
   액션은 undefined로 정의될수 없는 속성이어야 합니다.
   
   *
   * @returns For convenience, the same action object you dispatched.
   디스패치한 것과 같은 액션을 리턴합니다.
   *
   * Note that, if you use a custom middleware, it may wrap `dispatch()` to return something else (for example, a Promise you can await).
   메모, 커스텀 미들웨어를 사용하면 당신의 디스패치를 다른 무언가로 감싸서 리턴할 수 있습니다. 프로미스 객체라던지...
   */
  function dispatch(action: A) {
    if (!isPlainObject(action)) {
      throw new Error(
        `Actions must be plain objects. Instead, the actual type was: '${kindOf(
          action
        )}'. You may need to add middleware to your store setup to handle dispatching other values, such as 'redux-thunk' to handle dispatching functions. See https://redux.js.org/tutorials/fundamentals/part-4-store#middleware and https://redux.js.org/tutorials/fundamentals/part-6-async-logic#using-the-redux-thunk-middleware for examples.`
      )
      
      // 액션은 일반객체여야합니다! 그렇지만 당신이 준 타입은 이거! 
      // 스토어에 미들웨어를 설치하거나 다른 값을 보내주세요.
      // redux-thunk 추천합니다.
    }

    if (typeof action.type === 'undefined') {
      throw new Error(
        'Actions may not have an undefined "type" property. You may have misspelled an action type string constant.'
        // 액션은 undefined "type"을 가져서는 안됩니다. 문자열 상수를 입력하다가 오타가 난 것 같습니다. 
      )
    }

    if (isDispatching) {
      throw new Error('Reducers may not dispatch actions.')
      // 리듀서가 액션을 디스패치하지 하면 안됩니다.
    }

    try {
      isDispatching = true
      currentState = currentReducer(currentState, action)
    } finally {
      isDispatching = false
    }

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

    return action
  }

  /**
   * Replaces the reducer currently used by the store to calculate the state.
   현재 스토어의 상태를 계산하기 위해 리듀서를 교체합니다.
   *
   * You might need this if your app implements code splitting and you want to load some of the reducers dynamically. You might also need this if you implement a hot reloading mechanism for Redux.
   앱이 코드스플리팅을 구현하고 일부 리듀서를 동적으로 로드하려는 경우 이 항목이 필요할 수 있습니다. 리덕스에 대한 핫 리로딩 메커니즘을 구현하려는 경우에도 필요할 수 있습니다.
   *
   * @param nextReducer The reducer for the store to use instead.
    nextReducer: 지금 리듀서 대신 쓸 리듀서
   * @returns The same store instance with a new reducer in place.
   */
    //리턴값 : 새 리듀서가 설치된 동일한 스토어 인스턴스
  function replaceReducer<NewState, NewActions extends A>(
    nextReducer: Reducer<NewState, NewActions>
  ): Store<ExtendState<NewState, StateExt>, NewActions, StateExt, Ext> & Ext {
    if (typeof nextReducer !== 'function') {
      throw new Error(
        `Expected the nextReducer to be a function. Instead, received: '${kindOf(
          nextReducer
        )}`
      )
      // 넥스트리듀서의 타입은 함수여야합니다.
    }

    // TODO: do this more elegantly
    // 더 이쁘게 할수 있는 방법
    ;(currentReducer as unknown as Reducer<NewState, NewActions>) = nextReducer

    // This action has a similar effect to ActionTypes.INIT.
    // Any reducers that existed in both the new and old rootReducer will receive the previous state. This effectively populates the new state tree with any relevant data from the old one.
    // 이 액션은 ActionTypes.INIT.과 유사한 효과입니다.
    // 새 루트리듀서와 이전 루트리듀서 모두 이전 상태를 전달받습니다. 이렇게 하면 새 상태트리를 이전 상태트리의 데이터로 효과적으로 채울수 있습니다.
    dispatch({ type: ActionTypes.REPLACE } as A)
    // change the type of the store by casting it to the new store
    // 스토어 액션타입을 새로운 스토어껄로 바꿔주기
    return store as unknown as Store<
      ExtendState<NewState, StateExt>,
      NewActions,
      StateExt,
      Ext
    > &
      Ext
  }

  /**
   * Interoperability point for observable/reactive libraries.
   관찰가능한, 반응형 라이브라리에 대한 접근가능지점
   * @returns A minimal observable of state changes.
   리턴값 : 최소한의 관찰가능한 상태변화를 리턴합니다.
   * For more information, see the observable proposal:
   * https://github.com/tc39/proposal-observable
   */
  function observable() {
    const outerSubscribe = subscribe
    return {
      /**
       * The minimal observable subscription method.
       관찰이 가능한 최소 구독 단위
       * @param observer Any object that can be used as an observer.
       observer: 관찰자로 사용할수 있는 아무 객체
       * The observer object should have a `next` method.
      observer는 next 메서드를 꼭 가지고 있어야 합니다.
       * @returns An object with an `unsubscribe` method that can
       * be used to unsubscribe the observable from the store, and prevent further emission of values from the observable.
       스토어에서 관찰가능한 애들을 구독취소하고 더아상 값이 나오는 것을 방지하기 위해 사용됩니다.
       */
      subscribe(observer: unknown) {
        if (typeof observer !== 'object' || observer === null) {
          throw new TypeError(
            `Expected the observer to be an object. Instead, received: '${kindOf(
              observer
            )}'`
          )
          // 옵저버는 객체여야합니다.
        }

        function observeState() {
          const observerAsObserver = observer as Observer<S>
          if (observerAsObserver.next) {
            observerAsObserver.next(getState())
          }
        }

        observeState()
        const unsubscribe = outerSubscribe(observeState)
        return { unsubscribe }
      },

      [$$observable]() {
        return this
      }
    }
  }

  // When a store is created, an "INIT" action is dispatched so that every reducer returns their initial state. This effectively populates the initial state tree.
    // 스토어가 생성되면 모든 리듀서가 초기상태를 반환할 수 있게 INIT초기상태 액션이 구독됩니다. 이러면 initial state tree가 효과적으로 채워집니다.
  dispatch({ type: ActionTypes.INIT } as A)

  const store = {
    dispatch: dispatch as Dispatch<A>,
    subscribe,
    getState,
    replaceReducer,
    [$$observable]: observable
  } as unknown as Store<ExtendState<S, StateExt>, A, StateExt, Ext> & Ext
  return store
}
profile
It's an adventure time!

0개의 댓글