리덕스 공부 #2

min Mt·2020년 5월 7일
0
post-thumbnail

리덕스 공식 튜토리얼 정리

Introduction

리덕스 없이 순수 리액트로 프로젝트를 해보니, 스테이트 관리가 정말 힘들었다. 자식 컴포넌트에서 Inverse Data flow로 state를 바꾸는 것도 나중에 가니 헷갈리고, 자식 컴포넌트들에게 필요한 값들만 props로 전달하는 것도 귀찮고 헷갈리고 그랬다. 그래서 Redux를 한번 써보기 위해 기초부터 꼼꼼하게 공부하면서 정리하는 기록.

Actions

Actions are payloads of information that send data from your application to your store. They are the only source of information for the store. You send them to the store using store.dispatch()

리덕스의 2번째 원칙. State is read-only를 생각해보면, 스토어 안에 저장되어 있는 state들은 이 액션들을 통해서만 접근이 가능하다. React처럼 this.setState() 혹은 useState()를 통해 직접적으로 state에 접근하여 변경하는 것이 아니다.
액션은 다음과 같이 생겼다.

const ADD_TODO = 'ADD_TODO'
{
  type: ADD_TODO,
  text: 'Build my first Redux app'
}

액션도 plain JavaScript object 이다.
액션은 type property를 반드시 가지고 있어야 한다.
이 type은 액션이 어떠한 일을 수행하는지 알려준다. 함수명이랑 비슷한 것 같다.
또, type은 string constants 꼴을 보통 가진다. 이래야 사람이 액션이 어떠한 일을 하는지 직관적으로 판단하기 쉽다. string 특성상 오타를 유발하기 쉽기 때문에 const를 사용하는 것이라 생각한다.

액션에는 타입과 데이터가 들어간다. 데이터는 가능한한 최대한 줄이는게 좋다.

Action Creators

액션 크리에이터는 액션을 반환하는 함수이다.
액션은 JavaScript Object이다. 이 액션을 반환하는 함수를 만들어 사용하는 것이다.
예를 들어

{
  type: ADD_TODO,
  text: 'Build my first Redux app'
}

이 액션을 storedispatch하고싶은데 어떻게 해야 할까?

dispatch({
  type: ADD_TODO,
  text: 'Build my first Redux app',  
})

이렇게 하면 된다. 하지만, 데이터 즉 위의 예제의 경우 text property는 변수이기 때문에 저걸 함수로 만든다.

function addTodo(text) {
  return {
    type: ADD_TODO,
    text
  }
}

dispatch(addTodo('Build my first Redux app'))

이게 다다. 쉬운건데.. Store, Action, Action Creators, Dispatch 이런 단어들만 들으면 헷갈린다. 모국어가 영어인 사람들은 쉽게 이해하려나..

또 저 dispatch 를 매번 쓰기 귀찮은 경우 bound action creator를 만들어 사용하면 된다. 별거 아니다.

const boundAddTodo = text => dispatch(addTodo(text));

boundAddTodo('Build my first Redux app');

Reducers

액션을 배웠다. 이 액션 type에서 이 액션이 어떠한 일을 하는지는 알겠다. 그래서 정확히 어떻게 바뀌는지, 즉 코드를 작성하는 부분이 이 리듀서다. 리듀서는 Pure Function 이다.

(prevState, action) => nextState

위와 같이 이전 state값과 액션을 받아, 이 prevState를 액션에 따라 변형한 후 새로운 nextState를 반환한다.

Given the same arguments, it should calculate the next state and return it. No surprises. No side effects. No API calls. No mutations. Just a calculation.

예제를 봐보자

import { 
  SET_VISIBILITZY_FILTER, 
  VisibilityFilters 
} from './actions'

const initialState = {
  visibilityFilter: VisibilityFilters.SHOW_ALL,
  todos: []
}

function todoApp(state = initialState, action) {  
  switch(action.type){
    case SET_VISIBILTIY_FILTER:
      return Object.assign({}, state, {
        visibilityFilter: action.filter
      })
    default:
      return state;      
  }  
}

여기서 중요한 것은 인자로 받아온 state를 변형(mutate) 한 것이 아니라, 복사하여 사용했다는 것이다. Object.assign()의 첫번째 인자에 빈 Object를 넣어 새롭게 카피하는 것이다.
Spread Operator를 사용하는 것을 추천한다. spread operator를 사용하면 다음과 같이 쓸 수 있다.

function todoApp(state = initialState, action) {  
  switch(action.type){
    case SET_VISIBILTIY_FILTER:
      return {
        ...state, 
        visibilityFilter: action.filter
      }
    default:
      return state;      
  }  
}

어찌됐든, reducer를 통해 새로운 state가 반환되었다.

//previous State
{
  visibilityFilter: VisibilityFilters.SHOW_ALL,
  todos: []
}

//new State
{
  visibilityFilter: VisibilityFilters.RECEIVED_NEW_FILTER,
  todos: []
}

다시 한마디로 정리하자면 reducerPrevious State와 Action을 인자로 받아 New State를 반환한다.

이 ToDo App 에는 액션이 여러개다. 예제에 나온 다음 코드를 봐보자.

function todoApp(state = initialState, action) {
  switch (action.type) {
      //필터를 설정하는 action type
    case SET_VISIBILITY_FILTER:
      return {
        ...state,
        visibilityFilter: action.filter,
      }    
      //할 일을 추가하는 action type
    case ADD_TODO:
      return {
        ...state,
        todos: [
          ...state.todos,
          {
            text: action.text,
            completed: false
          }
        ]
      }    
      //완료or미완료를 토글 하는 action type
    case TOGGLE_TODO:
      return {
        ...state,
        todos: state.todos.map((todo, index) => {
          if (index === action.index) {
            return {
              ...todo,
              completed: !todo.completed
            }    
          }
          return todo
        })
      }           
      //action 이 없는 경우 state 변경 X
    default:
      return state
  }
}

총 액션은 3개이다.

여기서 이런 생각을 할 수 있다. 음? state가 하나인데, state가 규모가 커지면 어떻게 해야 하지? 매번 state전체를 다뤄야 하나?
필요한 state만 관리하기 위한 방법을 알아보자.

Reducer composition

예제를 봐보자

function todos(state = [], action) {
  switch (action.type) {
    case ADD_TODO:
      return [
        ...state,
        {
          text: action.text,
          completed: false
        }
      ]
    case TOGGLE_TODO:
      return state.map((todo, index) => {
        if (index === action.index) {
          return {
              ...todo,
              completed: !todo.completed
          }    
        }
        return todo
      })
    default:
      return state
  }
}

function todoApp(state = initialState, action) {
  switch (action.type) {
    case SET_VISIBILITY_FILTER:
      return {
        ...state,
        visibilityFilter: action.filter
      }     
    case ADD_TODO:
      return {
        ...state,
        todos: todos(state.todos, action)
      }    
    case TOGGLE_TODO:
      return {
        ...state,
        todos: todos(state.todos, action)
      }    
    default:
      return state
  }
}

todos와 관련된 state를 관리하는 todos()를 분리하여 만들었다. 이 함수는 stateaction을 인자로 받는 다는 점은 reducer와 같은 듯 하지만, 자세히 보면 이 state는 배열이다.
무슨 얘기냐면 전체 state의 일부분만 받아와서 일부분으로 받아온 그 state만 관리하는 reducer라는 것이다. 이렇게 하면 리듀서의 규모가 커지지 않게 각 역할별로 state를 나누어 리듀서를 만들수 있다.

Note that todos also accepts state—but state is an array! Now todoApp gives todos just a slice of the state to manage, and todos knows how to update just that slice. This is called reducer composition, and it's the fundamental pattern of building Redux apps.

여기서 나아가 filter관련된 state를 관리하는 리듀서를 따로 만들면

const { SHOW_ALL } = VisibilityFilters

function visibilityFilter(state = SHOW_ALL, action) {
  switch (action.type) {
    case SET_VISIBILITY_FILTER:
      return action.filter
    default:
      return state
  }
}

이고 이를 다시 todoApp에 적용해보면

function todoApp(state = {}, action) {
  switch (action.type) {
    case SET_VISIBILITY_FILTER:
      return {
        ...state,
        visibilityFilter: visibilityFilter(state.visibilityFilter, action)
      }     
    case ADD_TODO:
      return {
        ...state,
        todos: todos(state.todos, action)
      }    
    case TOGGLE_TODO:
      return {
        ...state,
        todos: todos(state.todos, action)
      }    
    default:
      return state
  }
}

initialState{}로 바뀐것을 생각해보자. initialState관리를 쪼개진 reducer에게 역할을 넘겼기 때문에, 이렇게 바뀐 것이다.

또 switch 문을 잘 보면, 굳이 switch 문이 필요하지 않다는 것을 알 수 있다.

function todoApp(state = {}, action) {
  return {
    visibilityFilter: visibilityFilter(state.visibilityFilter, action),
    todos: todos(state.todos, action)
  }  
}

멋지당..

Note that each of these reducers is managing its own part of the global state. The state parameter is different for every reducer, and corresponds to the part of the state it manages.

이것도 충분히 멋진데 return쪽이 지저분해 보였나 보다.
combineReducers()라는 기능이 있다.

import { combineReducers } from 'redux'

const todoApp = combineReducers({
  visibilityFilter,
  todos
})

export default todoApp

argument로 slice된 스테이트를 넣어야 되는데, 위처럼 하면 어떻게 되나? 라는 고민을 했는데 다음과 같은 원리다는 것을 문서에서 바로 설명해준다. 진짜 친절한 문서인 듯 하다.

const reducer = combineReducers({
  a: doSomethingWithA,
  b: processB,
  c: c
})

is equivalent to:

function reducer(state = {}, action) {
  return {
    a: doSomethingWithA(state.a, action),
    b: processB(state.b, action),
    c: c(state.c, action)
  }
}

All combineReducers() does is generate a function that calls your reducers with the slices of state selected according to their keys, and combines their results into a single object again. It's not magic.

여기도 나오는 "It's not magic"!!ㅋㅋ
한번 더 깨닫는건, 마법을 부려주는 라이브러리같은 건 없다.
이 말의 행간은 결국 어떤 원리인지 이해하는 것이 그 만큼 중요하다는 것 같다.

profile
안녕하세요!

0개의 댓글