REDUX_1

SooYoung Kim·2020년 3월 30일
0
post-thumbnail

P.S. 여기 나오는 설명들은 redux의 documentation과 기타 자료들을 조합한 것이다.

Actions

What is "Actions"?

(payload라는 )이 용어는 운송업에서 비롯하였는데, 지급(pay)해야 하는 적화물(load)을 의미한다. 예를 들어, 유조선 트럭이 20톤의 기름을 운반한다면 트럭의 총 무게는 차체, 운전자 등의 무게 때문에 그것보다 더 될 것이다. 이 모든 무게를 운송하는데 비용이 들지만, 고객은 오직 기름의 무게만을 지급(pay)하게 된다. 그래서 'pay-load'란 말이 나온 것이다.

Action(액션)이란 app에서 store로 데이터를 보내는, 즉 정보들의 payloads이다. 위에서 설명한 위키백과에 나온 payload라는 용어의 유래를 참고한다면 "정보들의 핵심 정보"를 일컫는 정도가 될 것이다.

{
    type: "ADD_TODO",
    text: 'This part is action.payload!'
}

위의 간단한 객체가 바로 action이다. redux를 쓰다보면 action.type 혹은action.payload 라는 코드를 볼 수 있다. 위에서 보이는 하나의 객체는 곧 하나의 action이고, "ADD_TODO"는 action.type, "This part is action.payload!"는 action.payload가 된다. type은 어떤 행위를 하는 것인지 알려주는 것이고, 전달하고자 하는 정보 그 자체는 text가 되는 것이다.(payload의 이름은 가변적이다. 꼭 text라고 쓰지 않아도 된다. 정보의 성격을 반영하여 이름을 붙인다.)

type은 보통 string const 타입이다. 위처럼 바로 string을 써줘도 되지만 const ADD_TODO = "ADD_TODO"라고 따로 변수를 만들어주는 것이 일반적이다.(아마 관리의 용이성을 위해?)

Action Creators

간단히 말하면 action을 만들기 쉽게 하기 위하여 action을 만드는 함수를 만든 것이다.

const addTodo = (text) => {
    return {
        type: ADD_TODO,
        text
    };
}

//addTodo 호출
addTodo("this is first task");
//result : {type: ADD_TODO, text:"this is first task"}

이후에 나오겠지만 action -> reducer -> store change 이런 순서로 변화가 일어난다. 그러나 위와 같이 action을 만드는 것만으로는 reducer에게 "야, store에 있는 값 바꿔"라고 요청할 수 없다. 그걸 하기 위해서는 dispatch라는 함수 안에 인자로 action을 넘겨 주어야 한다. dispatch는 네이버 영어사전의 정의에 따르면 "(특히 특별한 목적을 위해) 보내다[파견하다]"라는 의미이다. action을 reducer로 보내주는 함수라는 것이다.

dispatch(addTodo(text));

모든 action을 일일이 저렇게 dispatch 함수화 시켜야 한다면 귀찮을 것이다. bindActionCreators()라는 함수는 여러 action creator들을 모아 dispatch 함수화 시켜주는 역할을 한다. 이후에 다시 등장할 때 자세히 소개하기로 한다.

Reducers

What is Reducers?

Reducer는 store에 보내진 action에 대한 반응으로서 어떻게 app의 state이 바뀌어야 하는지를 정의한다. action은 무슨 일이 일어났는지에 대한 정보를 담고 있을 뿐 app의 state가 어떻게 바뀌어야 한다는 것을 나타내고 있지는 않다. app의 state가 어떻게 바뀌어야 할지 나타내는 것은 reducer가 한다. reducer는 기존 state과 새로 일어난 action을 조합하여 새로운 state를 만드는 것을 말한다.

Designing the State Shape

redux에서 app의 state는 하나의 단일한 객체로 저장된다. reducer코드를 작성하기 전에 저장될 객체의 구조를 먼저 정하는 것이 좋다. 아래의 코드는 redux documentation에서 샘플로 보여주는 todo리스트의 state 구조이다. 크게 todo 리스트가 보여지는 방식, todo 리스트 그 자체로 나눠져 있다.

{
  visibilityFilter: 'SHOW_ALL',
  todos: [
    {
      text: 'Consider using Redux',
      completed: true
    },
    {
      text: 'Keep all state in a single tree',
      completed: false
    }
  ]
}

구조를 짤 때는 최대한 간단하게, 양파 껍질처럼 여러 층을 가지지 않게 짤 것을 권유하고 있다. 여러가지 요소를 사용할 때는 고유한 id를 만들어서 사용하라고 한다.

Handling Actions

reducer는 기본적으로 두 가지의 인자(이전의 state, action)를 받아 새로운 상태를 return하는 "순수한 함수"이다. redux documentation에서는 "순수"하다는 것을 강조한다. 여기서 순수하다는 의미는 reducer함수는 아래 세가지 규칙을 지킨다는 것을 의미한다.

  1. 인자로 받은 것을 변형시키지 않는다.

    1. API를 사용하거나 routing 변경을 하지 않는다.
    2. Date.now()혹은 Math.random()과 같은 순수하지 않은(?) 함수를 사용하지 않는다.

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.

똑같은 인자가 주어질 때, reducer함수는 다음 state를 연산해서 return해야 한다. 예상할 수 없는, 또는 때마다 결과가 달라지는 것은 안 된다. 똑같은 인자가 주어지면 매번 똑같은 결과를 내야 한다는 점에서 "순수"한 함수라고 부르는 것 같다.

아래는 간단한 reducer함수의 예시이다.

import { VisibilityFilters } from './actions'; // 정의된 action들을 불러온다.

const initialState = {						   // 처음에 세팅될 state를 선언한다.
    visibilityFilter: VisibilityFilters.SHOW_ALL,
    todos:[]
};

const todoApp = (state, action) {			   // reducer함수 정의
    if(typeof(state) === 'undefined') {        // 맨처음 reducer호출 때는 state가 undefined로,
        return initialState;				   // 이 때 초기화 해준다.
    }
    //나중에 여기에다 action별 처리를 해준다.
    return state;
}
/*
ES6 문법을 쓴다면 todoApp 함수를 더 짧게 쓸 수도 있다.
const todoApp = (state = initialState, action) => {
	......
	return state;
}
*/

이제 reducer함수에서 action별 처리를 해보자. 가장 기본적인 방법은 switch문을 이용하는 것이다.

const todoApp = (state = initialState, action) {			    
    switch (action.type) {
        case '조건1':
            return ('action.payload를 반영한 새로운 state 객체');
        case '조건2':
            return ('action.payload를 반영한 새로운 state 객체');
        default:
            return state;
    }
}

위 코드에서 action.payload를 반영한 새로운 state 객체를 만들어내는 함수를 각각 만들어 따로 관리할 수 있다. 또한 combineReducers라는 함수를 사용해 더 간편하게 코드를 작성할 수도 있다.

import { combineReducers } from 'redux';

const doSomethingWithA = (state = {}, action) => {
    switch (action.type) {
        case '조건1':
            return ('조건에 맞는 객체');
        case '조건2':
            return ('조건에 맞는 객체');
        default:
            return state;
    }
}

const doSomethingWithB = (state = {}, action) => {
    switch (action.type) {
        case '조건1':
            return ('조건에 맞는 객체');
        case '조건2':
            return ('조건에 맞는 객체');
        default:
            return state;
    }
}

const todoApp = combineReducers({
    doSomethingWithA,
    doSomethingWithB
});

/* 위의 todoApp은 아래와 같다.
const todoApp = (state = {}, action) {
    return {
        doSomethingWithA : doSomethingWithA(state.doSomethingWithA, action),
        doSomethingWithB : doSomethingWithB(state.doSomethingWithB, action)
    };
}
*/

combineReducers를 사용할 때 염두해 두어야 할 점은 인자로 주는 객체의 key값이 곧 value로 넘겨지는 함수의 첫번째 인자인 state를 특정한다는 것이다.

const todoApp = combineReducers({
    a: doSomethingWithA,
    b: doSomethingWithB
});

/* 위의 todoApp은 아래와 같다.
const todoApp = (state = {}, action) {
    return {
        doSomethingWithA : doSomethingWithA(state.a, action),
        doSomethingWithB : doSomethingWithB(state.b, action)
    };
}
*/

Store

store는 action과 reducer를 하나로 묶어준다. store는 다음의 특징이 있다.

  • app의 state를 담고 있다.
  • store.getState()으로 state에 접근할 수 있다.
  • store.dispatch(action)으로 state을 업데이트 할 수 있다.
  • store.subscribe(listener)로 리스너를 등록할 수 있다.
  • store.subscribe를 하면 리스너 등록과 동시에 unsubscribe 할 수 있는 함수가 반환된다. 그걸 호출하면 리스너가 해제된다.

How to create Store

redux 모듈에는 createStore라고 store를 만들어 주는 기능이 있다. 기억해야 할 것은 하나의 app에서는 store가 하나이므로 최상단에서 한번만 store를 만들어주면 된다. 만드는 방법은 아래와 같다.

import { createStore } from 'redux'; // redux에서 createStore를 불러온다.
import todoApp from './reducers';    // reducer를 불러온다.
const store = createStore(todoApp);  // createStore에 reducer를 인자로 넘겨 store를 만든다.

Dispatching Actions

store에 있는 state를 바꾸는 방법은 store.dispatch(action)을 호출하는 것이다. redux documentation에 나온 아래 방법을 참고해서 테스트 해보자.

// 수행할 action들을 가져온다.
import {
  addTodo,
  toggleTodo,
  setVisibilityFilter,
  VisibilityFilters
} from './actions'

// 맨 처음의 state를 콘솔로 체크한다.
console.log(store.getState())

// state이 바뀔 때마다 콘솔로 찍는다.
// subscribe()함수는 본인을 해제 할 수 있는 unsubscribe함수를 리턴한다는 걸 기억하자.
const unsubscribe = store.subscribe(() => console.log(store.getState()))

// dispatch에 action을 넘겨주어 state의 상태를 바꾼다.
store.dispatch(addTodo('Learn about actions'))
store.dispatch(addTodo('Learn about reducers'))
store.dispatch(addTodo('Learn about store'))
store.dispatch(toggleTodo(0))
store.dispatch(toggleTodo(1))
store.dispatch(setVisibilityFilter(VisibilityFilters.SHOW_COMPLETED))

// 리스너를 해제한다.
unsubscribe()

Data flow

redux의 구조는 엄격한 일방향 데이터 흐름을 중심으로 돌아간다. 즉, app 안의 모든 데이터는 같은 lifecycle 패턴을 가진다는 것이다. 이는 app을 좀 더 예상 가능하고, 이해하기 쉽게 만든다고 공식문서는 설명한다.

아래 4가지 단계는 redux의 데이터 lifecycle을 말한다.

  1. store.dispatch(action)을 호출한다.
  2. store는 생성될 때 넘겨 받았던 reducer 함수를 호출한다. 이 때 store는 현 시점의 state와 dispatch로 받은 action을 reducer에 인자로 넘겨준다.
  3. store에 의해 호출된 root reducer는 여러 자식 reducer들을 조합하여 단일한 state를 도출한다.
  4. redux store는 root reducer에서 반환받은 new state를 저장한다. 새 state가 저장되면 store.subscribe(listener)가 작동한다.
profile
Enjoy my coding

0개의 댓글