Archive

React.js Redux

|

React.js Redux


  1. Redux 개요

    • Redux는 애플리케이션 state를 관리하기 위한 오픈소스 Javascript 라이브러리이다
    • Redux는 Javascript App을 위한 예측가능한 state container이다
    • Redux는 React의 작동 원리인 state에 대한 변화를 쉽게 예측할 수 있도록 도와준다
    • React 뿐만 아니라 Angular, jQuery, Vanilla JS 등 다양한 곳에서 적용이 가능하다
    • 앱의 규모가 커질수록 local state를 props의 형태로 전달하기 위해서는 연결된 chain layer들을 모두 거쳐야 한다
    • 위와 같은 경우 Context API를 쓸 수도 있다. 다시 말해, Redux만이 유일한 수단은 아니다
    • Redux의 3 원칙
      1. Single source of truth : global state는 트리 구조에서 하나의 저장소에 저장된다
      2. State is read-only : state를 변경하는 유일한 방법은 action을 발생시키는 것이다
      3. Changes are made with pure functions : action에 의한 state tree 변환을 지정하려면 pure reducer를 작성해야 한다. 즉, 리듀서는 action과 state를 받아서 다음 state를 반환하는 순수 함수여야한다

    작동원리

    출처 : Udemy / React - The Complete Guide


  2. Redux 기본 사용법

    • npm install –save redux (redux 패키지)
    • npm install –save react-redux (react와 redux를 연결)
    • Provider는 리액트의 컴포넌트들이 Redux store에 접근할 수 있도록 연결해준다 (상위 컴포넌트에서 사용)
    • store는 애플리케이션의 state를 저장하는 객체이다
    import { createStore } from 'redux';
    import { Provider } from 'react-redux';
    import reducer from 'path/reducer';
       
    // store 생성, reducer를 넣어준다
    const store = createStore(reducer);
       
    ReactDOM.render(
        // 하위 컴포넌트를 Provider로 감싸고 store 연결
    	<Provider store={store}>
        	<App />
        </Provider>
        document.getElementById('root')
    );
    
    • reducer는 현재 state와 action을 취하고 새로운 state를 반환하는 순수 함수이다. state는 불변성을 유지해야한다. 즉, state의 직접 수정 없이 복사본을 만들어 수정해야한다. 그 이유는 redux의 변경 감지 알고리즘에 있는데, 자세한 것은 링크 참조.
    const initialState = {
        counter: 0,
    };
       
    // ES6 default parameter, state가 undefined일 경우 initialState로 초기화
    const reducer = (state = initialState, action) => {
        if(action.type === 'INCREMENT') {
            return {
                counter: state.counter + 1,
            }
        }
        return state;
    };
       
    export default reducer;
    
    • action은 일반 객체로 state의 변화가 필요할 때, action을 발생시킨다. type 필드를 필수적으로 가지고 있어야한다. 일반 객체이기 때문에 얼마든지 함수로 생성하여 사용할 수 있다.
    • dispatch는 store의 내장 메소드로 action을 파라미터로 받아 store의 reducer에 넘겨주는 역할을 한다.
    • connect() 메소드는 Provider 컴포넌트로 감싼 하위 컴포넌트들이 store에 접근하게 하는데 하위 컴포넌트에서 사용한다
      • mapStateToProps : connect 메소드의 첫번째 인자로 들어가는 함수, store의 state를 조회해서 props로 넣어준다. 파라미터로 state를 받아온다. null 값으로 자동 호출을 막을 수 있다.
      • mapDispatchToProps : connect 메소드의 두번째 인자로 action을 dispatch하는 함수를 만들어 props로 넣어준다, dispatch를 파라미터로 받는다.
      • 이름은 꼭 저게 아니어도 되지만 일반적으로 통용되는 것으로 맞춰주는게 좋다
    import React from 'react';
    import { connect } from 'react-redux';
       
    class Counter extends React.Component {
        redner() {
            return (
            	<button onClick={this.props.onIncrementCounter}>
                    {this.props.ctr}
                </button>
            );
        }
    }
       
    const mapStateToProps = state => {
        return {
            ctr: state.counter,
        };
    }
       
    const mapDispatchToProps = dispatch => {
        return {
            onIncrementCounter: () => dispatch({ type: 'INCREMENT'}),
        };
    }
       
    export default connect(mapStateToProps,mapDispatchToProps)(Counter);
    


  3. payload

    • 위 코드에서는 reducer의 action 실행문에 의해 state의 값이 1씩 증가하게 된다. 하지만, 저렇게 하드코딩 되어있는 부분을 사용자가 지정한 임의의 값이 증가되도록 하려면 어떻게 해야될까?
    • action은 일반 객체라고 했다. action을 dispatch할 때, 단순히 type 외의 다른 필드 값을 넘겨주고 reducer에서 받아서 사용하기만 하면 된다
    const mapDispatchToProps = dispatch => {
        return {
            onIncrementCounter: () => dispatch({ type: 'INCREMENT', value: 5}),
        };
    }
    
    const reducer = (state = initialState, action) => {
        if(action.type === 'INCREMENT') {
            return {
                counter: state.counter + action.value,
            }
        }
        return state;
    };
    


    • 저렇게 해도 작동은 잘 된다. 하지만, type 이외의 전달하고 싶은 값들은 되도록 payload 필드명을 이용하여 전달하는 것이 좋다. 그렇게 통일되어 있으니깐. createAction 메소드를 이용하여 action을 생성할 때도 두 번째 인자값으로 paylaod 값을 받도록 설정되어 있다.
  4. Updating State Immutably

    • Redux의 핵심 규칙 중 하나는, state는 read-only이기에 불변성(immutable)을 유지해야 한다고 했다.
    • 위의 코드들과 달리 state가 복수개 존재한다면 기존 state를 통으로 복사해 필요한 부분만 수정하면 될 것이다.
    • 여기서 ES6의 Object Spread Operator가 사용된다. Object.assign()으로 객체 복사를 해도 된다.
    const initialState = {
        counter: 0,
        result: [],
    };
       
    const reducer = (state = initialState, action) => {
        if(action.type === 'INCREMENT') {
            return {
                ...state, // 객체 복사
                counter: state.counter + action.value,
            }
        }
        if(action.type === 'STORE') {
            return {
                ...state, // 객체 복사
                results: state.results.concat({
                    // id는 임시 id
                    id: new Date(), value: state.counter }),
            }// 기존배열을 직접 수정하지 않기 위해 concat 사용
        }
        return state;
    };
       
    export default reducer;
    


  5. Constant Action Type

    • 위의 코드들은 action을 하드코딩해서 직접 써넣었다
    • 이것을 상수로 설정하여 오타 등 에러가 날 확률을 미연에 방지할 수 있다
    export const INCREMENT = 'INCREMENT';
    export const DECREMENT = 'DECREMENT';
       
    // 상수가 담긴 js 파일, 사용처에서 import하여 사용한다
    
    import * as actionTypes from 'path/actions';
       
    const reducer = (state = initialState, action) => {
        if(action.type === actionTypes.INCREMENT) { // 상수 사용
            return {
                ...state, // 객체 복사
                counter: state.counter + action.value,
            }
        }
    
    import * as actionTypes from 'path/actions';
       
    const mapDispatchToProps = dispatch => {
        return {
            onIncrementCounter: () => dispatch({ type: actionTypes.INCREMENT, value: 5}),
        };
    }
    


  6. Combining Multiple Reducers

    • 애플리케이션의 규모가 커짐에 따라 한 개의 리듀로서만 모든걸 다루기에는 한계가 있다
    • 리듀서를 쪼개서 하나의 리듀서처럼 사용할 수 있다
    • 이를 위해서는 redux 패키지의 combineReducers가 필요하다
    // store를 정의한 최상위 컴포넌트에서
    import { createStore, combineReducers } from 'redux'; 
    import counterReducer from 'path/counter';
    import resultReducer from 'path/result';
       
    const rootReducer = combineReducers({
        ctr: counterReducer,
        res: resultReducer,
    });
       
    const store = createStore(rootReducer);
    
    const mapStateToProps = (state) => {
      return {
        // state의 블럭 단위가 한 단계씩 늘어난다
        ctr: state.ctr.counter,
        storedResults: state.res.results,
      };
    };
    
    • reducer를 쪼개면 그에 따라 state도 나뉘게 된다. 자신에게 없는 state 값이 필요한 경우엔 어떻게 해야할까?
    • 리듀서는 함수 안에서 global state에 접근할 수 없기때문에 값을 받아와야한다
    • 하나의 방법으로 action을 이용하여 값을 전달할 수 있다
    <button onClick={() => this.props.onStoreResult(this.props.ctr)}></button>
       
    const mapDispatchToProps = (dispatch) => {
        return {
            onStoreResult: (result) => dispatch({ 
                type: actionTypes.STORE, result: result })
        }// action을 이용하여 state를 전달
    }
    
    const reducer = (state = initialState, action) => {
      if (action.type === actionType.STORE) {
        return {
          ...state,
          results: state.results.concat({
            id: new Date(),
            value: action.result,
          }),
        };
      }
    }
    





참고 자료


reactjs.org - 공식홈페이지

Udemy - React The Complete Guide

redux 공식 홈페이지

React에 Redux 적용하기

리덕스(Redux)의 리듀서(reducer)가 순수 함수여야만 하는 이유

Redux-React의 기본