Redux 내부? 구조?

Soozynn·2022년 1월 28일
0

Redux의 구조를 간략하게 다시 정리해보면,

<3가지 기본 원칙>

  • Sinlge source of truth (SSOT) -> 모든 데이터를 한 곳에서만 관리
  • Read-only state
  • Changes from pure functions -> ⛔️ 순수함수를 통해 변화시킬 수 있다

❗️ React를 사용하면서 props로 넘겨주어야 하는 값들이 많아지고, 최상단으로 state를 계속 끌어줘야할 때 큰 불편함을 느꼈었다. 이럴 때 redux를 사용하면 전역스토어에 상태 값을 저장하고 가져올 수 있기 때문에 조금 더 유연하게? 편리하게 state를 관리하고 사용할 수 있다.

리덕스를 사용하지 않았을 때,
상태를 계속 최상단으로 끌어주어야만 형제 컴포넌트끼리 상태를 공유할 수 있었음.


리덕스는 전역 스토어가 존재하기 때문에 어느 컴포넌트에서든 스토어를 통해 state를 저장/사용할 수 있다.

Redux의 흐름

store의 대략적인 코드 구조는 아래와 같다.

const initialState = {
  value: 0, // state가 저장되는 공간, 모든 데이터는 이 한 곳에 저장된다
}; // 처음에는 이 하나의 객체에 모든 데이터가 저장된다는 것이 뭔가 신기했었다 

function addReducer(state = initialState, action) {
  switch(action.type) {
    case 'INCREMENT': // 다음과 같은 액션이 실행 되어지면 해당 케이스가 실행되고, 이전 state 값을 통해 새로운 값을 반환한다
      return { ...state, value: state.value + 1 };
    case 'DECREASE':
      return { ...state, value: state.value - 1 };
    default:
      return state;
  }
}

export default addReducer;

리덕스는 단방향 데이터 흐름을 가지고 있고,
진행되는 방식은 아래와 같다.

  • 액션을 스토어에 전달하고,
  • 리듀서를 실행,
  • 새로운 State 데이터를 Store에 반영,
  • Store Subscriber에게 알림,
  • 새로운 정보와 함께 컴포넌트를 다시 리렌더링

Action?

액션은 어플리케이션 상태에 영향을 미치는 사건을 표현/묘사하는 객체이다.
Store에 존재하는 dispatch라는 메소드를 이용하여 액션을 실행시킬 수 있다.
( 리듀서에 해당 케이스(action type)를 실행시킬거야~ 하고 표현해주는 느낌이다. )

액션을 실행시키는 코드 예시)

const dispatch = useDispatch(); // dispatch에 대한 설명은 아래에 있다. 
// 간략하게 설명하자면, 리액트에도 훅이 있듯이 리덕스에도 훅이 존재하는데 useDispatch가 그 중 하나이다.


dispatch({
    type: 'INCREMENT', // "액션의 종류를 한번에 식별할 수 있는 문자열"
    payload: 5 //  "액션의 실행에 필요한 임의의 데이터"
});

// type은 reducer에서 실행 시킬 케이스를 적어준다.
// 전달 할 data가 있다면 payload로 전달하고,
// 전달 할 data가 없다면 type만 적어주면 된다.

Store?

스토어는 어플리케이션의 State를 보관하는 곳이다.
redux는 모든 state가 하나의 store에서 관리된다 -> SSOT(Sinlge source of truth)

위 코드에선 아래 부분에 해당된다.

const initialState = {
  value: 0, // state가 저장되는 공간, 모든 데이터는 이 한 곳에 저장된다
};

createStore(reducer)로 Redux store를 생성할 수 있다.

import { createStore } from 'redux'

function todos(state = [], action) {
  switch (action.type) {
    case 'ADD_TODO':
      return state.concat([ action.text ]);
    default:
      return state;
  }
}

let store = createStore(todos, [ 'Use Redux' ]);

store.dispatch({
  type: 'ADD_TODO',
  text: 'Read the docs'
});

<store의 주요 메서드>

  • getState()를 통해 현재 state를 가져올 수 있다.
  • dispatch(action 발생)
  • subscribe(listener)
  • replaceReducer(nextReducer)


Redux hooks 공식문서

<Redux hooks의 2가지>

  • useSelector, useDispatch
  • 스토어의 메서드 getState와 hooks의 useSelector 차이?

이전 테스트를 진행하면서 아무 생각없이 useSelector가 아닌 getState를 사용한 적이 있는데 이 차이도 정리해보고자 한다.

이 전에 리액트에서 리덕스를 사용하기 위해서는 connect() 로 컴포넌트를 감싸고 mapStateToPropsmapToDispatchToProps 를 작성해야 리덕스 스토어에 접근을 할 수 있었지만,

현재는 리덕스에서 hooks를 지원하게 되면서 이러한 작업 없이 간결하고 코드 재사용성에 유리하게 코드를 작성할 수 있게 되었다.

그 훅이 바로 useSelector()useDispatch()이다.

useSelector()

리덕스 스토어의 데이터를 추출할 수 있는 리덕스의 훅 중 하나이며
문법적으로는 useSelector(함수, === 비교 함수)
첫 번째 인자에는 Selector 함수가 들어가는데 리덕스 스토어의 전체 상태를 인자로 받는다.

import React from 'react'
import { useSelector } from 'react-redux'

export const CounterComponent = () => {
  const counter = useSelector((state) => state.counter);
  return <div>{counter}</div>
};

useDispatch()

리덕스 스토어로부터 전달 받은 dispatch 함수를 참조하는 값을 반환한다

const dispatch = useDispatch(); // useDispatch를 dispatch로 선언


dispatch({
    type: 'INCREMENT', // "액션의 종류를 한번에 식별할 수 있는 문자열"
    payload: 5 //  "액션의 실행에 필요한 임의의 데이터"
});

👉 또 store는 4가지의 메서드가 들어있는 객체인데 그 중 하나인 getState()와 redux hook의 useSelector()의 차이는,

-> 즉, getState()는 현재 상태를 반환하는 함수이며, 스토어의 리듀서가 마지막으로 반환한 값과 동일하다. store에 state가 변경되어도 자동으로 업데이트 되지 않는다.
반대로 useSelector()는 이전 결과와의 비교를 통해 결과가 같을 경우 다시 렌더링하지 않는다는 큰 장점을 가진 훅이다.


Reducer?

외부에서 들어온 액션은 스토어의 상태(state)와 함께 리듀서에 전달되고,
스토어는 리듀서로부터 새로운 상태를 받는다.
그래서 리듀서는 이전 상태와 액션을 받아 새로운 상태를 반환하는 순수함수로 정의한다.

-> 이전 상태와 Action을 합쳐, 새로운 state를 만듦.
-> 반환된 state는 store에 바로 반영됨.

즉, 액션(Dispatch 메소드를 이용하여 Action을 발생시킴) -> 리듀서(currentState) -> 새로운 상태 값(nextState) 반환

function addReducer(state = initialState, action) {
  switch(action.type) {
    case 'INCREMENT': // 다음과 같은 액션이 실행되어지면 해당 케이스가 실행되고, 새로운 값을 반환한다
      return { ...state, value: state.value + 1 };
    case 'DECREASE':
      return { ...state, value: state.value - 1 };
    default:
      return state;
  }
}
  
export default addReducer;




그렇다면, 아무 생각 없이 써왔던 Redux의 내부는 어떻게 이루어져 있을까?🤔

createStore?

말 그대로 앱의 상태 트리 전체를 보관하는 Redux 스토어를 만드는 것이다.
앱 내에는 단 하나의 스토어만 있어야 한다!

middleware?

Redux는 액션을 실행하면 바로바로 적용되는 동기적인 동작 밖에 못한다.
-> 따라서 바로바로 실행되기 때문에 특정시간, 특정 동작 이후에 액션을 실행할 수 없다.
-> Redux action 사이사이에 비동기 요청이 들어갈 수 있게 확장을 해줘야 한다.
-> 미들웨어를 사용
-> 대표적인 saga, thunk, observer 등으로 이를 확장해 줄 수 있다.

dispatching 과정에서 액션이 리듀서에 도달하기 전 애플리케이션의 로직이 끼어들 수 있는 틈을 만들어주는 것이다.

-> 즉, 외부에서 생성된 액션이 리듀서에 도달하기전에 먼저 수신하여 시스템에 알맞게 특정 작업들을 미리 처리할 수 있도록 해주는 것.
여기서 특정 작업이란, 액션을 검증하거나 필터링, 모니터링, 외부 API와의 연동, 비동기 처리 등을 추가적으로 수행하는 부분들을 말한다.

이러한 특정 작업을 통해서 더 강력한 dispatch를 만들 수 있다고 한다.

그림으로 보면 아래와 같다.

<미들웨어의 위치>

<미들웨어를 통해 강력한 dispatch 생성 과정>
이미지 출처
<2가지의 dispatch>

  • 미들웨어가 없는 기본 dispatch
  • 미들웨어로 인해 고차 함수가 된 dispatching 함수

그리고 이러한 고차 함수는 아래에 compose를 활용해 구현할 수 있따.

  • middelware의 구조

    store API인 {getState, dispatch}를 인자로 받고,
    next라는 체이닝 함수를 전달받는 새로운 wrapDispatch 함수를 반환한다.

-> 미들웨어는 3개의 중첩된 함수를 구현하고 있어야 한다.

예시는 아래와 같다..

  function middleware({getState, dispatch}}) {
  return function wrapDispatch(next) {
    return function dispatchToSomething(action) {
      // do something...
      return next(action);
    }
  }
}
  
  
// arrow-syntax로 더 간략하게 
const middleware = ({getState, dispatch}) => next => action => { // getState, dispatch 를 인자로 받고,
  // do something...
  return next(action);
}

미들웨어가 하나가 아니라면?
스토어에 여러 미들웨어가 중첩되어 있을 때 바로 다음 미들웨어의 dispatching 함수에 진입하기 위해 필요한 함수가 바로 위의 next 함수이고, 그림으로 표현하면 아래와 같다.

- applyMiddleware API?

createStore 자체를 감싸는 고차 함수다.
applyMiddleware와 middleware는 정확히 구분해야 하는데

applyMiddleware >>> middleware

applyMiddleware: 미들웨어 자체보다 더 강력한 확장 메커니즘이다.

  • compose 함수중첩?

    여러 스토어 인핸서를 적용하기 위해 사용?
    리덕스 공식페이지에선 아래와 같이 말하고 있다.
함수를 오른쪽에서 왼쪽으로 조합합니다.
이것은 함수형 프로그래밍 유틸리티로, Redux에는 편리함을 위해 포함되었습니다. 
여러 스토어 인핸서들을 순차적으로 적용하기 위해 사용할 수 있습니다.


인수
(arguments): 조합할 함수들입니다. 
각각의 함수는 하나의 인자를 받아야 합니다. 
함수의 반환값은 왼쪽에 있는 함수의 인수로 제공되는 식으로 연속됩니다. 
예외는 가장 오른쪽에 있는 인수로, 여러 개의 인자를 받을 수 있으며 조합된 함수의 시그니처는 이를 따릅니다.

반환
(Function): 오른쪽에서 왼쪽으로 조합된 최종 함수입니다.

아래 코드는 compose를 사용해 스토어를 applyMiddleware와 redux-devtools 패키지의 몇몇 개발툴로 강화하는 방법이다.
import { createStore, combineReducers, applyMiddleware, compose } from 'redux'
import thunk from 'redux-thunk'
import DevTools from './containers/DevTools'
import reducer from '../reducers/index'

const store = createStore(
  reducer,
  compose( // ✅
    applyMiddleware(thunk),
    DevTools.instrument()
  )
)

compose가 하는 일은 결국 깊이 중첩된 함수 변환을 길게 늘어진 코드 없이 작성하게 해주는 것이라고 한다.

reducer

처음 위에서 리덕스의 구조를 설명하였을 때에 reducer가 하나였지만, reducer가
여러 개가 된다면 어떻게 될까?
-> 여러 개의 reducer를 지정했다면 redux의 combineReducer로 reducer를 통합시킬 수 있다.

아직 더 정리중..

0개의 댓글