끄적끄적

[REACT] Redux란? 본문

Front-end/React.js

[REACT] Redux란?

mashko 2021. 5. 5. 16:46
반응형

Redux에 대해 정리해보려고 합니다.
React를 공부하다보면 필연적으로 접해야하고 접하게 되는 Redux는 앞서 Vue에서 Store 상태관리 라이브러리를 소개했던 Vuex와 마찬가지로 React의 상태관리 라이브러리입니다.
하지만 Vue와는 달리 React에서는 상태관리 라이브러리가 굉장히 많습니다. Redux기반의 redux-saga,redux-thunk,redux-observable,mobx 기타 등등등...
사용하는 라이브러리에 따라 추구하는 방향은 같지만 개발방법이 조금씩 달라지게 되죠. 그래서 React 프레임워크를 쓰는 회사들마다 전부 다른 상태관리 라이브러리를 쓰고 있습니다. 성향에 맞게 또는 특성에 따라 골라쓰면 될꺼같긴합니다.

Redux
앞서 적었듯이 Redux는 Flux개념을 바탕으로한 React에서 현재 가장 많이 사용되는 State관리 라이브러리입니다.
React는 기본적으로 컴포넌트 내부에서 자신의 State를 갖습니다. 그 State를 기반으로 컴포넌트를 변경하고 컨트롤하게 되는데 프로젝트의 규모가 커지게되며 여러 단위의 컴포넌트끼리의 State를 참조하게 될 필요성이 있고 다수의 컴포넌트간의 상속이 발생하다보니 문제점이 생기게 되죠.

위와같이 어디부터 어디까지 서로의 State를 참조하고 있는지 또한 참조의 깊이가 깊어지면 구현에 문제가 생기며 관리가 되지 않게 되어 아키텍처는 엉망이 되고 뒤죽박죽 엉키게 됩니다. 이러한 문제를 해결하고자 나온 것이 Flux 패턴으로 이 Flux패턴을 기반으로 Redux를 이용한 상태관리를 하게 됩니다. 위의 그림과 같이 중앙상태관리를 통해 각 컴포넌트간에 state를 공급해주게되고 각각의 state를 참조하기에도 depth와 상관없이 쉽게 참조 할 수 있게 됩니다.

예제

// components/storeExample/counter.js
import React from 'react';
import PropTypes from 'prop-types';

import { Button } from 'antd';


const defaultProps = {
    number: -1,
    onPlus: createWarning('onPlus'),
    onSubtract: createWarning('onSubtract'),
};

const propTypes = {
    number: PropTypes.number,
    onPlus: PropTypes.func,
    onSubtract: PropTypes.func,
};



function createWarning(funcName) {
    return () => console.warn(funcName + ' is not defined');
}


const Counter = (props) => {
    return (
        <div>
            <h1>{ props.number }</h1>
            <Button type="primary" onClick={ props.onPlus }>+</Button>
            <Button type="primary" onClick={ props.onSubtract }>-</Button>
        </div>
    );
}

Counter.propTypes = propTypes;
Counter.defaultProps = defaultProps;

export default Counter;
// container/storeExampleContainer/counterContainer.js
import React, { Component } from 'react';
import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';

// UI Components
import Counter from 'components/StoreExample/Counter';

// Actions
import * as counterActions from 'stores/modules/example/counter.module';

class CounterContainer extends Component {

    constructor(props) {
        super(props);
    }

    render() {
        const { CounterActions, number, color } = this.props;

        return (
            <div style={style}>
                <Counter
                    number={number}
                    onPlus={CounterActions.increment}
                    onSubtract={CounterActions.decrement}
                />
            </div>
        );
    }
}


const mapStateToProps = (state) => {
    return {
        number: state.counter.number,
    };
};

const mapDispatchToProps = (dispatch) => {
    return {
        CounterActions: bindActionCreators(counterActions, dispatch)
    };
};

export default connect(
    mapStateToProps,
    mapDispatchToProps
)(CounterContainer);
// stores/modules/example/counter.module.js
import { Record } from 'immutable';
import { createAction, handleActions } from 'redux-actions';

const INCREMENT = 'counter/INCREMENT';
const DECREMENT = 'counter/DECREMENT';

export const increment = createAction(INCREMENT);
export const decrement = createAction(DECREMENT);

// 초기 상태를 정의합니다
const initialState = Record({
    color: 'black',
    number: 0
})();

/* 
    리듀서 함수를 정의합니다. 리듀서는 state 와 action 을 파라미터로 받습니다.
    state 가 undefined 일때 (스토어가 생성될때) state 의 기본값을 initialState 로 사용합니다.
    action.type 에 따라 다른 작업을 하고, 새 상태를 만들어서 반환합니다.
    이 때 주의 할 점은 state 를 직접 수정하면 안되고,
    기존 상태 값에 원하는 값을 덮어쓴 새로운 객체를 만들어서 반환해야합니다.
*/
const counter = handleActions({
    [INCREMENT]: (state, action) => {
        return state.set('number', state.get('number') + 1);
    },
    [DECREMENT]: (state, action) => {
        return state.set('number', state.get('number') - 1);
    },
}, initialState);


export default counter;
// stores/index.js
import {
    createStore,
    applyMiddleware
} from 'redux';
import modules, { rootSaga } from './modules';
import { createLogger } from 'redux-logger';

const logger = createLogger();

const configure = () => {
  const store = createStore(modules, applyMiddleware(logger));
  store.close = () => store.dispatch(END);

  return store;
}

export default configure;
// index.js
import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import { BrowserRouter as Router } from 'react-router-dom';
import { matchPath } from 'react-router';
import _ from 'lodash';
import { AppContainer } from 'react-hot-loader';
// settings
import stores from './stores';
import registerServiceWorker from './registerServiceWorker';
// ant design css
import 'antd/dist/antd.css';
// routes
import { Routes } from 'routes';
// sass
import './assets/sass/main.scss';

// components
import App from './pages/App'

// store setting
let store = stores;

const render = async (Component) => {
    ReactDOM.render(
        <Provider store={store}>
            <AppContainer>
                <Router>
                    <App />
                </Router>
            </AppContainer>
        </Provider>,
        document.getElementById('root')
    );
}

render(App);


if (module.hot) {
    module.hot.accept('./pages/App', () => { render(App) })
}

registerServiceWorker();

결과

참고자료

반응형
Comments