IT

리덕스의 구조 및 장단점

단점이없어지고싶은개발자 2022. 1. 30. 00:29
반응형

리덕스 탄생배경

  • Sinngle Page Application이 탄생하게 되면서 더 많은 상태를 관리하게 되었다.
  • 항상 변화하는 state를 관리하기 어려운 문제가 발생함으로 이벤트가 수 없이 발생하면서 데이터를 바꾸는 변화를 감지하는게 어려웠다. 언제, 왜, 어떻게 상태를 제어할 수 없게 되면서 앱에서 어떤일이 발생하는지 더 이상 이해할 수 없었다.
  • mutation - asynchronicity 변화와 비동기는 사람이 추론해내기 어려운 두 가지 개념을 섞어서 사용한다는데 온다. 이 두 가지를 멘토스와 콜라라고 한다. 이 둘을 나눠서 보면 훌륭하지만 함께 두면 엉망이 되어버린다. React와 같은 라이브러리는 이런 문제를 해결하기 위해서 view 단체에서 비동기와 직접적인 DOM접근을 막는다. 하지만 데이터의 상태를 관리하는 것은 개발자에게 맡겨져 있는데, 이 때 리덕스가 등장한다.

리덕스의 세 가지 철학

진실은 하나의 근원으로부터

애플리케이션의 모든 상태는 하나의 저장소 안에 하나의 객체 트리 구조로 저장된다.

이를 통해 범용적인 애플리케이션(universal application, 하나의 코드 베이스로 다양한 환경에서 실행 가능한 코드)을 만들기 쉽게 만들 수 있다. 서버로부터 가져온 상태는 시리얼라이즈되거나(serialized) 수화되어 전달되며 클라이언트에서 추가적인 코딩 없이도 사용할 수 있다. 또한 하나의 상태 트리만을 가지고 있기 때문에 디버깅에도 용이할 것이다. 빠른 개발 사이클을 위해 개발중인 애플리케이션의 상태를 저장해놓을 수도 있다. 하나의 상태 트리만을 가지고 있기 때문에 이전에는 구현하기 어려웠던 실행취소, 다시 실행을 쉽게 구현할 수 있다

상태는 읽기 전용이다

상태를 변화시키는 유일한 방법은 무슨 일이 벌어지는지를 묘사하는 액션 객체를 전달하는 방법뿐이다.

즉, 리덕스의 스토어에 저장된 상태는 읽을 수만 있고, 상태를 변경시키기 위해서는 액션을 발행해야 한다. 데이터의 단방향 흐름이다. 데이터는 오직 애션을 통해서만 변경되고, 리덕스 스토어의 상태는 읽기 전용이다. 직접적인 데이터 변화는 허용하지 않는다.

이를 통해서mutation(화면의 변화) 또는 asynchronicity(네트워크 콜백함수)가 직접적인 상태를 변화시키는 것을 막는다. 대신에, 상태를 변화시킬 것이라는 의도만을 표현하도록 한다. 모든 변화들이 스토어로 집중화 되어있고, 오직 한 가지의 엄격한 순서(단방향)에 의해 일어나기 때문에, 알아차리기 어려운 미묘한 데이터 변화의 순서 경쟁이 없다.

순수한 Redux코드를 보면 아래와 같다. 스토어에 액션을 발행시키는 행위다. 액션은 아래와 같이 그저 객체일 뿐이다. 그 객체는 type을 가지고 데이터를 변화시키기 위한 재료 (paylaod)를 가진다

store.dispatch({
  type: 'COMPLERTE_TODO', //todo list의 항목을 추가하는 액션
  type: 'I ho to Home..', //추가할 텍스트(재료)
})

store.dispatch({
  type: 'COMPLETED_TODO' //todo list 한 항목의 완성표시 (액션이름)
  index: 1 //index가 1번인 todo list
})

3. 변화는 순수 함수로 작성되어야 한다

액션에 의해 상태 트리가 어떻게 변하는지를 지정하기 위해 프로그래머는 순수 리듀서를 작성해야한다

변화는 순수한 함수들로 만들어 진다. 앞서서 액션 객체에 의해 변화를 하는데 그 변화를 어떻게 만들어 내는것은 Reducer함수다.

Reducer함수는 순수함수로써 이전의 상태와 액션을 인자로 받고, 변화된 상태를 리턴한다. 기억해야 할것은, 순수함수란 점이다. 쉽게 말해 오로지 결과만을 바꾸는 함수를 의미한다. 따라서 이전의 상태를 변화시키는 것 대신에 새로운 state를 생성해낸다.

function todos(state = [], action) { 
//todos라는 상태의 초기값은 빈 배열이고, reducer함수는 항상 액션객체를 인자로 받는다.
  switch (action.type) {//switch-case문으로 action의 타입에 따라 다른 상태를 리턴하도록한다.
    case 'ADD_TODO':
	  return [
        ...state, //이전의 배열을 변화시키면 안되기에 spread로 복사
        {
        ...text: action.text, //새롭게 들어온 text를 배열에 넣는다.
          completed: false,
        }
      ]
    case 'COMPELTED_TODO':
      return state.map((todo, index) => {
        if (index === action.index) { //action객체에 index로 들어온 것과 비교해서 같을 때에만
          return {
            ...todos,
            completed: true,
          }
        }
        
      return todo;
    });
  default:
    return state
  }
}

import { combineReducer, createStore } from 'redux'
const reducer = combineReducers({ todos });
//ES6+ 모던 자바스크립트에서는 key와 value 가 같으면 생략해서 쓸 수 있다. 즉 이코드는 todos라는
//배열에 todos Reducer 함수를 맴핑해 놓은 것이다.
const store = createStore(reducer); 
//reducer로 스토어를 만든다. 스토어는 상태들이 저장되어있는 객체형태

기본 개념

  1. 액션 : 애플리케이션의 상태를 어떻게 변경시킬지 추상화한 표현이다. 단순 객체로 type 프로퍼티를 꼭 가지고 있어야 한다
  2. 리듀서 : 애플리케이션의 다음 상태를 반환하는 함수이다. 이전 상태와 액션을 받아 처리하고 다음 상태를 반환한다.
  3. 스토어 : 애플리케이션의 상태를 저장하고 읽을 수 있게 하며 액션을 보내거나 상태의 변화를 감지할 수 있도록 API를 제공하는 객체이다.

Redux는 애플리케이션의 상태 관리를 스토어라는 개념으로 추상화하고, 상태 트리를 내부적으로 관리한다. 애플리케이션은 변화를 표현하는 액션얼 스토어에 전달하고 스토어는 리듀서를 통해 상태 트리를 형상화하여 변경한다. 그리고 스토어는 다시 애플리케이션에게 상태 트리의 변경을 알린다. 애플리케이션은 상태 트리의 변경을 인지하고 이에 따른 UI 변경이나 다른 서비스 로직을 수행한다.

<aside> 💡 리덕스의 함수들은 어떻게 이뤄져있을까?

</aside>

CreateStore(reducer, [preloadedState], [enhancer])

앱의 전체 상태 트리를 보유하는 저장소를 만든다. 앱에는 하나의 스토어만 있어야 한다.

인자

  1. reducer(함수): 현재 상태 트리와 처리할 작업이 주어지면 다음 상태트리를 반환하는 축소 함수
  2. preloadedState : 초기상태. 유니버설 앱의 서버나 이전의 직렬화된 사용자 세션에서 상태를 채우기 위해 선택적으로 지정할 수 있다. 만약 combineReducers로 리듀서를 만들었다면, 이 인수는 전달했던 것과 같은 키구조를 가지는 평법한 객체여야한다. 그렇지 않으면 리듀서가 이해할 수 있는 어떤것도 사용할 수 있다.
  3. enhancer : 타사 기능으로 저장소를 향상시키기 위해 선택적으로 지정할 수 있다. Redux와 함께 제공되는 유일한 저장소 향상 플로그램은 applyMiddleware다.

반환

Store : 앱의 전체 상태를 가지고 있는 객체. 이 객체의 상태를 바꾸는 유일한 방법은 액션을 보내는 것이다. UI를 업데이트 하기 위해 상태를 구독 할 수 있다.

import { createStore } from 'redux'

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

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

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

console.log(store.getState())
// [ 'Use Redux', 'Read the docs' ]
  • 둘 이상에 저장소를 만들지 말고, combineReducers를 이용해 단일 reducer을 만든다
  • Redux 상태는 일반적으로 일반 js 객체 및 배열이다
//- 상태가 일반 객체인 경우 변경하지말고,
Object.assign({}, state, newData)
//변경할 수 없는 업데이트는 일반적으로 객체 확산 연산자 
return { ...state, ...newData }
//를 사용하여 각 데이터 복사본을 만들어야 한다
  • 저장소가 생성되면 Redux는 reducer에 보내진 더미데이터가 저장소에 초기 상태로 채운다.리듀서가 undefined를 인수로 받았을 대 최초 상태를 반환해야한다라는 걸 기억하자

Store

애플리케이션의 전체 상태트리를 보유한다. 내부 상태를 변경하는 유일한 방법은 해당 상태에 대한 Action을 보내는 방법이다.

  • 액션?액션은 상태를 변화시키려는 의도를 표현하는 평법한 객체다. 액션은 저장소에 데이터를 넣는 유일한 방법이다. UI이벤트에서 왔든, 네트워크 콜백에서 왔든, 웹소켓과 같은 다른 소스에서 왔든 모든 데이터는 액션으로 보내진다.
  • 액션은 어떤 형태의 액션이 행해질지 표시하는 type필드를 갖는다.
  • type Action = Object

내부적으로 리듀서와 애플리케이션의 상태, 이벤트 리스너, 현재 디스패팅 여부를 나타내는 값(isDispatching: boolean)을 관리한다. 외부적으로는 dispatch, subscribe, getState, replaceReducer API를 노출한다

Redux는 다음과 같은 단방향 흐름을 갖고 있다.

Dispatch(action) ⇒ Reducer(currentState, action) ⇒ nextState ⇒ currentState ← nextState ⇒ Invoke listenrs

Store 메소드

getState()

애플리케이션의 현재 상태 트리를 반환한다. 저장소의 리듀서가 마지막으로 반환한 값과 동일하다

반환 : 애플리케이션의 현재 상태 트리

dispatch(action)

액션을 보낸다. 상태변경을 일으키기 위한 유일한 방법이다.

저장소의 리듀서 함수는 getState()의 현재 결과와 주어진 액션과 함께 동기적으로 호출된다. 반환된 값이 다음 상태가 되어 이제부터 getState()에서 반환될 것이고, 상태 변경 리스너들은 즉시 알람은 받는다.

dispatch가 된 상태라면 에러를 발생시킨다.

subscribe(listener)

변경사항에 대한 리스너를 추가한다. 리스너는 액션이 보내져서 상태 트리의 일부가 변경될 수 있을 때마다 호출된다. 콜백 안에서 현재 상태 트리를 읽으면 getState()를 호출하면 된다.

유의사항

  1. 리스너는 사용자 액션에 응답하기 위해서나 특정 조건 하에서만 dispatch()를 호출해야 한다. 기술적으로는 아무런 조건 없이 호출할 수 있지만 호출하면 리스너를 작동시키는 것이기에 무한루프에 빠진다
  2. 구독자들은 매번 dispatch()가 호출되기 전 시점의 것들이 사용된다. 만약 리스넝 안에서 구독이나 구독 취소를 하더라도 지금 진행중인 dispatch()에는 영향을 미치지 않을 것이다. 하지만 중첩 여부와 관계 없이, 다음 dispatch()호출에서는 더 최근의 구독 목록이 사용될 것이다.
  3. 리스너가 호출되기 전에 중첩된 dispatch()들이 상태를 여러번 업데이트 할 수 있기 때문에, 리스너가 모든 상태 변화를 받아볼 것이라고 생각하면 안된다. dispatch()가 시작하기 전에 등록된 모든 구독다들이 해당 시점의 최신 상태를 받아볼 것이 보장된다.

전달인자

listener : 액션이 보내져서 상태트리가 바뀌게 될 때마다 호출할 콜백. 현재 상태 트리를 읽기 위해 콜백 내에서 getState()를 호출할 수 있다.

replaceReducer(nextReducer)

현재 저장소에서 상태를 계산하기 위해 사용중인 리듀서를 교체한다

이것은 고급 API로 코드 분할이나 동적으로 리듀서를 불러오고 싶을 때 사용할 수 있다. Redux에서 핫 리로딩을 구현하기 위해서도 사용할 수 있다.

combineReducer(reducers)

서로 다른 리듀싱 함수들을 값으로 가지는 객체를 받아서 createStore에 넘길 수 있는 하나의 리듀싱 함수로 바꿔준다. 생성된 리듀서는 내부의 모든 리듀서들을 호출하고 결과를 모아서 하나의 상태 객체로 바꿔준다. 상태 객체의 현태는 reducers로 전달된 키들을 따른다.

{
  reducer1: ...
  reducer2: ...
}

상태의 키 이름은 전달되는 객체의 키 이름을 다르게 함으로써 제어할 수 있다.

예를 들어 상태 모양이 { todos, counter } 가 되기 위해선

combineReducers({ todos: myTodosReducer, counter: myCounterReducer })

인자

reducers : 하나로 합쳐질 각각의 리듀실 함수들을 값으로 가지는 객체다.

반환

reducers 객체 안의 모든 리듀서들을 실행해서 하나의 상태 객체를 만드는 리듀서다.

applyMiddleware(...middleware)

dispatch메서드를 실용적으로 감쌀 수 있게 해준다.

예를 들어 redux-thunk는 액션 생산자가 디스패치 함수를 통해 제어를 할 수 있다. 액션 생산자는 dispatch를 인수로 받아 비동기적으로 호출할 수 있다.

...middleware(arguments) : 미들웨어 API를 따르는 함수. 각각의 미들웨어는 Store의 dispatch, getState함수를 명명된 인수로 받아서, 함수를 반환. 이 함수는 미들웨어의 디스패치 함수에서 next로 주어져서, 다른 인수와 함께 , 아니면 다른 시점에, 아니면 전혀 호출되지 않을 수도 있는 next(action)을 호출하는 action의 함수여야 한다. 체인의 마지막 미들웨어는 next인자로 원래 저장소의 dispatch를 받아 체인을 마무리한다.

({ getState, dispatch }) => next => action
import { createStore, applyMiddleware } from 'redux'
import todos from './reducers'

function logger({ getState }) {
  return next => action => {
    console.log('will dispatch', action)

    // Call the next dispatch method in the middleware chain.
    let returnValue = next(action)

    console.log('state after dispatch', getState())

    // This will likely be the action itself, unless
    // a middleware further in chain changed it.
    return returnValue
  }
}

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

store.dispatch({
  type: 'ADD_TODO',
  text: 'Understand the middleware'
})
// (These lines will be logged by the middleware:)
// will dispatch: { type: 'ADD_TODO', text: 'Understand the middleware' }
// state after dispatch: [ 'Use Redux', 'Understand the middleware' ]

bindActionCreators(actionCreators, dispatch)

값이 액션 생산자인 객체를 받아서, 같은 키를 가지지만 각각의 생산자들을 dispatch로 감싸서 바로 호출 가능하게 만든 객체로 바꾼다.

보통은 Store인스턴스에서 바로 dispatch를 호출하면 된다. Redux를 React와 함께 사용한다면, react-redux가 dispatch함수를 함께 제공하므로 바로 호출할 수 있다. 유일한 사용처는 Redux를 상관하지 않는 컴포넌트로 액션 생산자를 넘기짐나 dispatch나 Redux저장소는 넘기고 싶지 않을 때이다.

인자

actionCreators : 액션 생상자 또는 값으로 액션 생산자들을 가지는 객체

dispatch : Store인스턴스에서 가져온 dispatch함수

compose(...functions)

함수를 오른쪽에서 왼쪽으로 조합한다

인자

...functions : 조합할 함수들이다. 각각의 함수는 하나의 인자를 받아야 한다.

반환

오른쪽에서 왼쪽으로 조합된 최종함수

<aside> 💡 리덕스의 장점, 단점 그리고 언제 사용해야할까?

</aside>

장점

  • 단방향 모델링, action을 dispatch 할때마다 기록(history)이 남아서 에러를 찾기 쉽다. 타임머신 기능을 사용할 수 있다.
  • 상태의 중앙화 : 스토어라는 이름의 전역 자바스크립트 변수를 통해 상태를 한 곳에서 관리하는데, 이를 중앙화라 한다. 전역 상태를 관리할 때 굉장히 효과적이다.
  • Redux는 상태를 읽기 전용으로 취급, 이전 상태로 돌아가기 위해서는 그저 이전 상태를 현재 상태에 덮어쓰기만 하면 된다.

단점

  • 아주 작은 기능이여도 리덕스로 구현하는 순간 몇 개의 파일(액션등을 미리 만들어놔야 함)들을 필수로 만들어야 하며 코드량이 늘어난다. 또한 보일러 플레이트 코드(액션 타입, 액션 생성함수, 리듀서)를 많이 준비해야 한다.
    • 보일러플레이트 코드 : 컴퓨터 프로그래밍에서 보일러플레이트 코드는 최소한의 변경으로 여러곳에서 재사용되며, 반복적으로 비슷한 형태를 띄는 코드를 말한다.
  • 타임머신 기능을 사용하려면 불변성 개념을 지켜야 사용할 수 있으므로 매번 state라는 객체를 만들어줘야 한다.
  • Redux는 상태를 읽기 전용으로 취급할 뿐, 실제 읽기 전용으로 만들어주지는 않는다. 때문에 상태를 실수로 직접 변경하지 않도록 항상 주의해야 한다.
  • 다른 것 다 필요 없고 상태 관리를 중앙화하는 것만 있어도 된다면 ContextAPI를 사용

이러한 단점들을 해결하기 위해 나온 것이 리덕스 툴킷이다

리덕스 툴킷이란?

  • reduxjs/toolkit
    1. 리덕스 스토어를 구성하는 것은 너무 복잡하다
    2. 리덕스가 유용해지려면 많은 패키지들을 추가로 설치해야 한다
    3. 리덕스는 너무 많은 보일러플레이트 코드를 요구한다
    이 세가지를 해결한 것이 리덕스 툴킷이라는 라이브러리로 리덕스를 훨씬 쉽고 간편하게 사용할 수 있다.크게 7가지가 있다.
    1. configureStore() : createStore함수와 비슷한 함수로 간단화된 구성 옵션과 기본 구성을 제공한다. slice reducer을 자동으로 합치고, 미들웨어를 추가할 수 있고, redux-thunk를 기본적으로 제공한다. 또한 redux devtools Extension사용이 가능하다
    2. createReducer() : 리듀서 함수를 switch 구문으로 쓰기보다는, 리듀서 함수를 계속쓰는 lookup table방식을 쓸 수 있게 해주고, immer라이브러리가 내장되어 있어서 mutative한 코드를 작성할 수 있도록 해준다. (ex state.todos[3].completed = true
    3. createAction() : 주어진 액션 타입 문자열로 액션 크리에이터 함수를 생성해준다. 함수 자체에 toString()이 정의되어 있어서 constant 타입 대신 사용이 가능하다
    4. createSlice() : reducer 함수, slice 이름, 초깃값을 넣을 수 있고 action creator 와 action type을 가진 slice reducer을 자동으로 생성해준다.
    5. createAsyncThunk : redux-thunk의 대체재
    6. createEntityAdapter : 스토어에서 정규화된 데이터를 관리하기 위해 재사용 가능한 리듀서 및 selector 집합을 생성한다.
    7. createSelector : reselect라이브러리의 유틸 기능과 똑같음
    장점
    • action type이나 action creator을 따로 생성해주지 않아도 된다.
    • 미들웨어 추가가 편리하다
    • immer가 내장되어 있어 mutable객체를 사용해도 된다
    • redux thunk가 내장되어 있어 비동기를 지원한다
    • 타입스크립트 지원이 잘된다

리덕스 언제 사용할까?

상태를 관리함에 있어서 복잡성이 높지 않다면 사용하지 않아도 괜찮다. React만으로도 충분히 단방향 데이터 흐름을 구현할 수 있다. 꼭 필요하지 않은 상태에서 Redux를 사용한다면 불필요한 라이브러리만 하나 더 import해서 번들 사이즈만 증가시킬 뿐이다.

만약! 과도한 pro drilling(prop 전달이 매우 많은 경우)가 생기거나 디버깅이 어렵다면 Redux를 사용하자

You Might Not Need Redux

 

You Might Not Need Redux

People often choose Redux before they need it. “What if our app doesn’t scale without it?” Later, developers frown at the indirection Redux…

medium.com

 

 

참고사이트

Redux의 탄생동기와 철학 이해하기

리덕스 공부해보기 (2) - 리덕스의 탄생, 핵심 개념 그리고 3가지 원칙.

Flux로의 카툰 안내서

리덕스(Redux)는 왜 쓰는 건데⁉

Redux Toolkit (리덕스 툴킷)은 정말 천덕꾸러기일까?

[리액트] 리덕스의 장점, 단점

반응형