본문 바로가기

React

Redux 기초 - (1)액션, 리듀서

액션



데이터를 변경하려면 어떤 행동을 해야하는지에 대한 정의가 필요하다. 


새 할 일의 추가를 나타내는 액션의 예시이다. 액션은 평범한 자바스크립트 객체다. 


const ADD_TODO = 'ADD_TODO'

{
  type: ADD_TODO,
  text: 'Build my first Redux app'
}
  • type - 어떤 형태의 액션이 실행될지 나타내는 속성. 앱이 충분히 커지게 되면 타입들을 별도의 파일로 분리
  • type 외에 액션 객체의 구조는 마음대로 짜면 된다. ( 물론 권장사항이 있다. - https://github.com/acdlite/flux-standard-action )

import { ADD_TODO, REMOVE_TODO } from '../actionTypes'



액션 생산자



액션을 만드는 함수다.


function addTodo(text) {

  return {
    type: ADD_TODO,
    text
  }
}


실제로 액션을 보내려면 결과값을 dispatch() 함수에 넘긴다. ( 자세한건 뒷 부분에서 확인해보자 )


dispatch(addTodo(text))

dispatch(completeTodo(index))



리듀서


 

 앞에서 살펴본 액션은 무언가 일어난다를 기술한 것이지, 상태가 어떻게 바뀌는지는 정의하지 않았다. 이 일을 리듀서가 한다.


 상태 설계하기


 Redux에서 애플리케이션의 모든 상태는 하나의 객체에 저장된다. 

 먼저, 할일 앱을 예시로 살펴보자.

    • 현재 선택된 필터 ( 다 보여줌, 완료만 보여줌 등 )
    • 할 일의 실제 목록

{

   visibilityFilter: 'SHOW_ALL',

   todos: [{

text: 'Consider using Redux',

completed: true, 

   }, { 

text: 'Keep all state in a single tree', 

completed: false 

   }]

}


 더 복잡한 앱에서는 각기 다른 개체들이 서로를 참조하게 만들어야 한다. 이를 위해서는 데이터베이스에서 배웠던 정규화 할 것을 권장한다. 모든 개체가 ID를 가지고, ID를 통해 다른 개체나 목록을 참조하도록 해야한다. 즉, 앱의 상태를 데이터베임스라고 생각하면 된다. https://github.com/paularmstrong/normalizr 를 참고하면 된다. 예를 들어 상태 안에 todosById: { id -> todo } 와  todos : array<id> 처럼 구현하는 것이 실제 앱에서는 더 적절하다. 단지 이것은 보여주기 용이다.


 액션 다루기


리듀서는 이전 상태와 액션을 받아서 다음 상태를 반환하는 순수 함수다. 즉, 이전의 상태(목록들)와 어떤 행동인지(할일더하기)를 넘겨주면 그걸 바탕으로 새로운 상태를 만들어준다. 


(previousState, action) => newState

 

 리듀선는 순수 함수이기 때문에 순수하게 유지하는 것(?)은 매우 중요하다. 리듀서에서 절대로 하지 말아야 할 것은

    • 인수들을 변경(mutate)하기
    • API 호출 / 라우팅 전환같은 사이드이펙트를 일으키기
    • Date.now()Math.random() 같이 순수하지 않은 함수를 호출하기.

 사이드 이펙트가 어떻게 일어나느지는 심화과정에서 확인해보자. 인수가 주어지면, 계산만 가능하다.


 먼저 초기 상태를 정의하자

import { VisibilityFilters } from './actions'

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

function todoApp(state, action) {
  if (typeof state === 'undefined') {
    return initialState
  }

  // 지금은 아무 액션도 다루지 않고
  // 주어진 상태를 그대로 반환합니다.
  return state 
}


이제  SET_VISIBILITY_FILTER 를 처리해보자. 우리가 해야 할일은  visibilityFilter 를 바꾸는 것 뿐이다.,

function todoApp(state = initialState, action) {
  switch (action.type) {
  case SET_VISIBILITY_FILTER:
    return Object.assign({}, state, {
      visibilityFilter: action.filter
    });
  default:
    return state
  }
}
  1. 우리는 state를 변경하지 않았다. Object.assign()을 통해 복사본을 만들었다. 
  2.  default 케이스에 대해 이전의 state 를 반환하였다. 알수 없는 액션에 대해서는 이전의 state 를 반환하는 것이 좋다.


더 많은 액션 다루기


우리의 리듀서가 할일을 더하는 ADD_TODO를 다룰 수 있도록 확장해보자.

function todoApp(state = initialState, action) {
  switch (action.type) {
  case SET_VISIBILITY_FILTER:
    return Object.assign({}, state, {
      visibilityFilter: action.filter
    });
  case ADD_TODO:
    return Object.assign({}, state, {
      todos: [...state.todos, {
        text: action.text,
        completed: false
      }]
    });    
  default:
    return state;
  }
}

앞에서와 마찬가지로 state나 그 필드들을 직접 쓰는 대신 새 객체를 반환하였다.


만약 이런 코드를 자주 작성해야 한다면 React update 같은 헬퍼를 사용하는 것이 좋다.

 switch (action.type) 
    {
      case types.AUTH_LOGIN:
        return update(state,{
          login :{
            status : { $set :'WAITING'}
          }
        });
...


리듀서 쪼개기


리듀서 내에서 너무 많은 액션을 분류하다 보니 코드가 번접스러워 졌다.

function todoApp(state = initialState, action) {
  switch (action.type) {
  case SET_VISIBILITY_FILTER:
    return Object.assign({}, state, {
      visibilityFilter: action.filter
    });
  case ADD_TODO:
    return Object.assign({}, state, {
      todos: [...state.todos, {
        text: action.text,
        completed: false
      }]
    });
  case COMPLETE_TODO:
    return Object.assign({}, state, {
      todos: [
        ...state.todos.slice(0, action.index),
        Object.assign({}, state.todos[action.index], {
          completed: true
        }),
        ...state.todos.slice(action.index + 1)
      ]
    });
  default:
    return state;
  }
}


코드를 살펴보면  todos visibilityFilter 가 독립적으로 수정된다. 즉 두가지 함수로 분리해보자.

function todos(state = [], action) {
  switch (action.type) {
  case ADD_TODO:
    return [...state, {
      text: action.text,
      completed: false
    }];
  case COMPLETE_TODO:
    return [
      ...state.slice(0, action.index),
      Object.assign({}, state[action.index], {
        completed: true
      }),
      ...state.slice(action.index + 1)
    ];
  default:
    return state;
  }
}

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

function todoApp(state = {}, action) {
  return {
    visibilityFilter: visibilityFilter(state.visibilityFilter, action),
    todos: todos(state.todos, action)
  };
}
    •  todoApp 은 관리할 상태의 조각만 넘기고,  todos 는 그 조각을 어떻게 수정할지 알고 있다. 
    • 이것을 리듀서 조합이라 부르고, Redux 앱을 만드는 기본 패턴이다.


각각의 리듀서는 전체 상태(state)에서 자기가 수정해야 할 부분만을 관리한다. 모든 리듀서의  state 매개변수는 서로 다르고, 자신이 관리하는 부분에 해당한다. 앱이 더 커지면, 리듀서를 별도의 파일로 분리해서 관리하는 것도 좋은 방법이다.


마지막으로 Redux는  combineReducers() 라는 유틸리티를 제공한다. 이를 이용하면 다음과 같이 재작성이 가능하다.

import { combineReducers } from 'redux';

const todoApp = combineReducers({
  visibilityFilter,
  todos
});

export default todoApp;

이는 아래와 같은 코드다.

export default function todoApp(state, action) {
  return {
    visibilityFilter: visibilityFilter(state.visibilityFilter, action),
    todos: todos(state.todos, action)
  };
}

ES6을 이해하는 사람들을 위한 한마디


combineReducers 는 객체를 기대하기 때문에, 모든 최상위 리듀서들을 각기 다른 파일에 놓고 export 한 다음 import * as reducers 를 이용해 

각각의 이름을 키로 가지는 객체를 얻을 수 있다.

import { combineReducers } from 'redux';
import * as reducers from './reducers';

const todoApp = combineReducers(reducers);
import { combineReducers } from 'redux';
import memo from './memo';
import authentication from './authentication';
const todoApp = combineReducers({ authentication, memo } );


Source Code - reducers.js


import { combineReducers } from 'redux';
import { ADD_TODO, COMPLETE_TODO, SET_VISIBILITY_FILTER, VisibilityFilters } from './actions';
const { SHOW_ALL } = VisibilityFilters;

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

function todos(state = [], action) {
  switch (action.type) {
  case ADD_TODO:
    return [...state, {
      text: action.text,
      completed: false
    }];
  case COMPLETE_TODO:
    return [
      ...state.slice(0, action.index),
      Object.assign({}, state[action.index], {
        completed: true
      }),
      ...state.slice(action.index + 1)
    ];
  default:
    return state;
  }
}

const todoApp = combineReducers({
  visibilityFilter,
  todos
});

export default todoApp;


'React' 카테고리의 다른 글

Redux 시작하기 기초 - (마지막) React와 함께 사용하기  (0) 2017.11.21
Redux 기초 - (2) 스토어(Store), 데이터 흐름  (0) 2017.11.21
#4. React 성능과 렌더링  (0) 2017.06.19
Tip  (0) 2017.05.23
#3. Redux  (0) 2017.05.23