TIL 61 | Redux

hyounglee·2020년 10월 2일
1

React

목록 보기
20/33
post-thumbnail

아마 이게 제일 이해하기 쉬울걸요? React + Redux 플로우의 이해
(Youtube) Redux For Beginners | React Redux Tutorial

Redux의 특징

1. Single Source of Truth
하나의 Store를 사용한다.
2. State is Read-only
어플리케이션에서 state를 직접 변경할 수 없다. 무조건 action이 dispatch되어야 한다.
3. Changes are made with pure Functions
action 객체를 처리하는 함수를 reducer라고 부른다. reducer는 정보를 받아서 상태를 어떻게 업데이트 할 지 정의한다. reducer는 '순수 함수'로 작성되어야 한다. 즉 비동기적인 처리를 하면 안된다. 즉, 네트워크 및 데이터베이스 접근X, 인수 변경X. 같은 인수로 실행된 함수는 언제나 같은 결과를 반환. 순수하지 않은 API는 사용할 수 없다. ( Date.now(), Math.random() 등 )

Redux의 형태

import { createStore } from 'redux';

// STORE -> GLOBALIZED STATE

// ACTION INCREMENT
const increment = () => {
  return {
    type: 'INCREMENT'
  }
}

// ACTION DECREMENT
const decrement = () => {
  return {
    type: 'DECREMENT'
  }
}

// REDUCER
const counter = (state = 0, action) => {
  switch (action.type) {
    case 'INCREMENT':
      return state + 1;
    case 'DECREMENT':
      return state - 1;
  }
};

let store = createStore(counter);

// Display it in the console
store.subscribe(() => console.log(store.getState()));

// DISPATCH
store.dispatch(increment());
store.dispatch(decrement());

Redux Flow

store

상태는 기본적으로 전부 여기서 집중 관리 된다. 커다란 JSON의 결정체 정도의 이미지이다. 규모가 클 경우에는 상태를 카테고리별로 분류하는 경우가 일반적이다.

{
    // 세션과 관련된 것
    session: {
        loggedIn: true,
        user: {
            id: "114514",
            screenName: "@mpyw",
        },
    },// 표시중인 타임라인에 관련된 것
    timeline: {
        type: "home",
        statuses: [
            {id: 1, screenName: "@mpyw", text: "hello"},
            {id: 2, screenName: "@mpyw", text: "bye"},
        ],
    },// 알림과 관련된 것
    notification: [],
}

Action 및 Action Creator

Store 및 Store에 존재하는 State는 아주 신성한 것이다. React 컴포넌트 같은 하등한 것이 직접 접근하면 안된다. 직접 접근하기 위해서는 Action 이라는 의식을 거쳐야 하는데, 이벤트 드리븐과 같은 개념이다.

  1. Store에 대해 뭔가 하고 싶은 경우엔 Action을 발행한다.
  2. Store의 문지기가 Action의 발생을 감지하면, State가 경신된다.

Action은 기본적으로 아래와 같은 포맷을 가지고 있는 오브젝트이다.

{
    type: "액션의 종류를 한번에 식별할 수 있는 문자열 혹은 심볼",
    payload: "액션의 실행에 필요한 임의의 데이터",
}

예를 들어 카운터의 값을 2배 늘리는 경우, 아래와 같은 오브젝트가 될 것이다. 머릿 부분에 @@myapp/ 라고 Prefix를 붙인 것은 다른 사람이 쓴 코드와의 충돌을 피하기 위함이다.

{
    type: "@@myapp/ADD_VALUE",
    payload: 2,
}

그런데 하나하나 이런 오브젝트를 만드는 것을 수작업으로 하는 것도 정말 괴로운 일이다. 또 매번 Action 명을 문자열로 쓰는 것도 불편하다. 그래서 이걸 편하게 하기 위해 상수와 함수를 준비하는 것이 일반적이다. 외부 파일이 참고할수도 있으니 제대로 export 해두어야 한다.

export const ADD_VALUE = '@@myapp/ADD_VALUE';
export const addValue = amount => ({type: ADD_VALUE, payload: amount});

Reducer

Store의 문지기 역할이라고 생각하면 된다. 함수형 프로그래밍에서 Reducer라는 용어는 합성곱을 의미하지만, Redux에 한해서는 아래와 같이 이전 상태와 Action을 합쳐, 새로운 state를 만드는 조작을 말한다.

import { ADD_VALUE } from './actions';export default (state = {value: 0}, action) => {
    switch (action.type) {
        case ADD_VALUE:
            return { ...state, value: state.value + action.payload };
        default:
            return state;
    }
}
  • 초기 상태는 Reducer의 디폴트 인수에서 정의된다.
  • 상태가 변할 때 전해진 state는 그 자체의 값으로 대체되는 것이 아니라, 새로운 것이 합성되는 것처럼 쓰여진다.

반환된 state는 store에 바로 반영 되어 아래와 같이 변화한다.

{
    value: 2,
}

트위터에서처럼 대규모 개발에 Reducer를 미세하게 분할하는 경우 Redux에서 제공하는 combineReducers함수를 이용하여 아래와 같이 쓴다.

import { combineReducers } from 'redux';const sessionReducer = (state = {loggedIn: false, user: null}, payload) => {
    /* 省略 */
};
const timelineReducer = (state = {type: "home", statuses: []}, payload) => {
    /* 省略 */
};
const notificationReducer = (state = [], payload) => {
    /* 省略 */
};export default combineReducers({
    session: sessionReducer,
    timeline: timelineReducer,
    notification: notificationReducer,
})

이렇게 하면, Reducer분할에 쓰인 Key가 그대로 State분할에도 쓰인다. 또한 실제로 각각의 reducer의 정의 자체도 다른 파일로 나누는 것이 일반적이다.

순수한 Component와 연결된 Component

React의 컴포넌트 자체는 리덕스의 흐름에 타는 것이 불가능하다. 흐름에 타기 우해서는 ReactRedux에 의해 제공되는 connect라고 불리는 함수를 이용하여 아래와 같이 쓴다. 함수판과 클래스판 각각 쓴다.

>>>>>>>> 함수판
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { addValue } from './actions';const Counter = ({ value, dispatchAddValue }) => (
    <div>
        Value: {value}
        <a href="#" onClick={e => dispatchAddValue(1)}>+1</a>
        <a href="#" onClick={e => dispatchAddValue(2)}>+2</a>
    </div>
);export default connect(
    state => ({ value: state.value }),
    dispatch => ({ dispatchAddValue: amount => dispatch(addValue(amount)) })
)(Counter)
>>>>>>>>> 클래스판
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { addValue } from './actions';class Counter extends Component {
    render() {
        const { value, dispatchAddValue } = this.props;
        return (
            <div>
                Value: {value}
                <a href="#" onClick={e => dispatchAddValue(1)}>+1</a>
                <a href="#" onClick={e => dispatchAddValue(2)}>+2</a>
            </div>
        );
    }
}export default connect(
    state => ({ value: state.value }),
    dispatch => ({ dispatchAddValue: amount => dispatch(addValue(amount)) })
)(Counter)

먼저 컴포넌트가 store로부터 무언가 정보를 받는 경우, 그걸 props 를 통해 받는다. props는 immutable하다. 다시 말해, 상대가 변경될 때마다 새로운 컴포넌트가 다시 만들어진다는 의미이다. 이것을 염두에 둔 후에 connect를 실행하고 있는 주변 코드를 보자.

  1. store가 가진 state를 어떻게 props에 엮을지 정한다. (이 동작을 정의하는 함수는 mapStateToProps라고 불린다.)
  2. Reducer에 action을 알리는 함수 dispatch를 어떻게 props에 엮을지 정한다. (이 동작을 정의하는 함수는 mapDispatchToProps라고 불린다.)
  3. 위에 두가지가 적용된 props를 받을 컴포넌트를 정한다.
  4. Store와 Reducer를 연결시킬 수 있도록 만들어진 컴포넌트가 반환값이 된다.

connect(mapStateToProps, mapDispatchToProps)(Component)라고 쓰인걸 보면 좀 독특하다고 생각할 수 있겠지만, 결국 최종적인 반환값은 4번과 같다.

mapStateToProps

인수로 전달된 state는 전체를 의미한다는 것에 주의해야 한다. 카운터의 예를 다시 보면,

{
    value: 2,
}

<Counter value={2} />

로 들어가길 바라며 state => ({ value: state.value })라고 썼다. 이번 경우에는 다른 프로퍼티가 없기 때문에 state => state 라고 써도 동작에는 무리가 없겠지만, 기본적으로 필요한 것만 선별하여 props로 엮는다가 원칙이라고 생각하자.

mapDispatchToProps

Action Creator에서 action을 만든다고 해도, 그것으론 아무런 일도 일어나지 않는다. Reducer를 향해 통지를 할 수 있게 만들기 위해서는 만든 action을 dispatch라는 함수에 넘겨줘야 한다. 이렇게 하면 모든 Reducer가 실행된다. Reducer에 switch문으로 분기를 나눈 것은 바로 이 때문이다. Reducer는 관계없는 action을 무시하고, 자기에게 주어진 actioin 만을 처리하도록 되어있어야만 한다.

또 Component 쪽에 하나하나 수동으로 dispatch 하는 처리를 하지 않아도 되도록, 여기서 action의 생성부터 dispatch의 실행까지 한번에 이뤄질 수 있도록 함수를 정의하여 props에 넘겨주도록 한다는 멋진 존재 의의도 엿볼 수 있다.

bindActionCreators

하지만 무려 mapDispatchToProps를 이용하여 위와 같은 코드를 짜는 것에서도 도망칠 수 있다. bindActionCreators라는 함수를 제공하기 때문이다. 이걸 쓰면 아래와 같은 생략이 가능하다.

import React, { Component } from 'react';
import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';
import { addValue } from './actions';
​
​
const Counter = ({ value, addValue }) => (
    <div>
        Value: {value}
        <a href="#" onClick={e => addValue(1)}>+1</a>
        <a href="#" onClick={e => addValue(2)}>+2</a>
    </div>
);export default connect(
    state => ({ value: state.value }),
    dispatch => bindActionCreators({ addValue }, dispatch)
)(Counter)

현재는 bindActionCreators의 실행도 생략할 수 있게끔 되었다.

import React, { Component } from 'react';
import { connect } from 'react-redux';
import { addValue } from './actions';
​
​
const Counter = ({ value, addValue }) => (
    <div>
        Value: {value}
        <a href="#" onClick={e => addValue(1)}>+1</a>
        <a href="#" onClick={e => addValue(2)}>+2</a>
    </div>
);export default connect(
    state => ({ value: state.value }),
    { addValue }
)(Counter)

Container

지금까지는 '연결된 Component'라고 불렀지만, 상황에 따라서 'Container'라고 불려야 하는 Component도 나온다. 수많은 Component가 리스트 형식으로 모여있는 가운데 각 요소의 Component를 각각 연결하면 수습이 안되므로, 대표적인 자식 요소를 안고 있는 하나의 부모 Component가 connect 는 경우이다.

<UsersList>
    <User />
    <User />
    <User />
    <User />
</UsersList>

이 대표로서 connect될 부모 Component를 Container라고 부른다. Container는 가독성을 높이기 위해, Component와는 디렉토리를 따로 나누는 경우가 많다.

발전

Redux Saga의 도입

실은 React+Redux만으로는 아직 불편한 경우가 많다. Reducer 안에 부작용이 생길 처리를 써선 안 된다 라는 원칙이 있기 때문이다.

  • 같은 입력에 대해 확률적으로 다른 결과가 나오는 처리
  • 지연처리
  • HTTP 리퀘스트 처리

이런 것들은 기본적으로 Reducer 안에서 쓸 수가 없다. 그럼 대체 어디서 써야할까?

  • Component 안
  • Action Creator 안
  • mapDispatchToProps 안

“또 안티패턴이니? 그럼 넌 Saga야 ^^”

지금까지 connect 된 Component로부터 action이 dispatch되면 그 Reducer를 향한다, 고만 한정지어 설명했습니다. 여기서 새로운 방법을 제시하는 것이 Saga다. Saga는 제너레이터 함수이기 때문에, 비동기처리를 간단히 다룰 수 있다.

  1. yield take(ACTION_TYPE)으로 지정한 action의 발생을 감시한다
  2. 가져온 action을 구워먹고 삶아먹고 마음대로 할 수 있다
  3. yield put(action)의 결과를 다른 action으로 내보낼 수 있다

기본적으론 이런 것들이 가능하다. 내보낸 action은 Reducer를 향하게도 할 수 있고 자기 자신의 Saga에게 다시 올 수도 있고 자기 외의 다른 Saga에 보낼 수 있을지도 모른다.

profile
(~˘▾˘)~♫❝ 쉽게만 살아가면 재미없어 빙고 .ᐟ ❞•*¨*•.¸¸♪

0개의 댓글