티스토리 뷰

React.JS

[React.JS] Redux 예제로 살펴보기

버미노트 2017. 5. 4. 12:51

Redux

아래 그림과 같이 입력한 숫자를 더하는 counter을 redux를 사용하지 않고, redux를 사용하여, redux + react-redux를 사용하여 구현 한 3가지 코드를 비교하여 설명하겠습니다. create-react-app로 생성된 프로젝트로 진행 합니다. ([React.JS] CodePen, create-react-app으로 React.JS 개발하기 참고)

Counter
Counter

1. redux를 사용하지 않은 예제

Redux나 그 외의 모듈을 사용하지 않고, 순수 React만 사용하여 구현한 예제입니다.

컴포넌트 구조

컴포넌트 구조와, state 흐름은 아래 그림과 같습니다.

컴포넌트 구조
컴포넌트 구조

  • Counter 컴포넌트는 계산된 값을 화면에 나타내는 컴포넌트 입니다.
  • Option 컴포넌트는 계산할 값을 입력받는 컴포넌트 입니다.
  • Button 컴포넌트는 입력받은 값을 더할 지 뺄지 선택할 수 있게 하는 버튼을 나타내는 컴포넌트입니다.

App 컴포넌트는 위 3개의 컴포넌트의 공통된 부모 컴포넌트 입니다. 공통의 부모가 되기 때문에 Counter에 표시 되는 값과 계산될 값, 2개를 모두 state로 가지고 있어야 합니다. 또한 Button 컴포넌트에서 버튼이 클릭 될 때 마다, 발생한 이벤트를 전달 받아 Counter에 표시되는 값을 계산하여 state에 저장합니다.

코드 구조

src/components/Counter.js

import React, { Component } from 'react';

class Counter extends Component {
    render() {
        return (
            <div>
                <h1>Value : {this.props.value}</h1>
            </div>
        );
    }
}

export default Counter;
  • 7번 줄, 전달 받은 value를 출력합니다.

src/components/Option.js

import React, { Component } from 'react';

class Option extends Component {
    constructor(props) {
        super(props);
        this.onChange = this.onChange.bind(this);
    }

    onChange(event) {
        this.props.onChange(event.target.value);
    }

    render() {
        return (
            <div>
                <input value={this.props.diff} onChange={this.onChange} />
            </div>
        );
    }
}

export default Option;
  • 9~11번 줄, 사용자가 값 입력시, 입력 값을 부모 컴포넌트로 전달해 줍니다.
  • 16번 줄, 부모 컴포넌트로 부터 전달 받은 diff 값을 출력합니다. 또한 이벤트 핸들러를 등록합니다.

src/components/Button.js

import React, { Component } from 'react';

class Button extends Component {
    constructor(props) {
        super(props);

        this.onIncrement = this.onIncrement.bind(this);
        this.onDecrement = this.onDecrement.bind(this);
    }

    onIncrement(event) {
        this.props.onIncrement();
    }

    onDecrement(event) {
        this.props.onDecrement();
    }

    render() {
        return (
            <div>
                <button onClick={this.onIncrement}>+</button>
                <button onClick={this.onDecrement}>-</button>
            </div>
        );
    }
}

export default Button;
  • 11~13번 줄, + 버튼을 클릭 할 경우 실행 되는 이벤트 핸들러입니다. 부모 컴포넌트의 onIncrement 함수를 실행시킵니다.
  • 15~17번 줄, - 버튼을 클릭 할 경우 실행 되는 이벤트 핸들러입니다. 부모 컴포넌트의 onDecrement 함수를 실행시킵니다.
  • 22~23번 줄, 이벤트 핸들러를 등록합니다.

App.js

import React, { Component } from 'react';
import logo from './logo.svg';
import './App.css';

import Counter from './components/Counter';
import Option from './components/Option';
import Button from './components/Button';

class App extends Component {
  constructor(props) {
    super(props);

    this.state = {
      value: 0,
      diff: 1
    };
    this.onChange = this.onChange.bind(this);
    this.onIncrement = this.onIncrement.bind(this);
    this.onDecrement = this.onDecrement.bind(this);
  }

  onChange(diff) {
    this.setState({
      diff: diff
    });
  }

  onIncrement() {
    this.setState(prevState => ({
      value: prevState.value + Number(this.state.diff)
    }));
  }

  onDecrement() {
    this.setState(prevState => ({
      value: prevState.value - Number(this.state.diff)
    }));
  }

  render() {
    return (
      <div>
        <Counter value={this.state.value} />
        <Option diff={this.state.diff} onChange={this.onChange} />
        <Button onIncrement={this.onIncrement} onDecrement={this.onDecrement} />
      </div>
    );
  }
}

export default App;
  • 5~7번 줄, 사용할 컴포넌트를 import 합니다.
  • 22~26번 줄, Option에서 실행되는 이벤트 핸들러 입니다. 사용자의 입력 값을 state.diff에 저장합니다.
  • 28~32번 줄, Button에서 실행되는 이벤트 핸들러 입니다. state.diff와 이전의 state, 즉 prevState.value를 합친 값을 state.value에 저장합니다.
  • 34~38번 줄, Button에서 실행되는 이벤트 핸들러 입니다. state.diff에 preState.value를 뺀 값을 state.value에 저장합니다.
  • 43번 줄, Counter 컴포넌트를 불러옵니다. state.value를 출력 할 수 있도록 value attribute를 선언해야 합니다.
  • 44번 줄, Option 컴포넌트를 불러옵니다. state.diff를 보여주기 위해 diff attrubute를 선언하고, 변화된 값을 state에 저장하기 위해 onChange 이벤트 핸들러를 등록합니다.
  • 45번 줄, Button 컴포넌트를 불러옵니다. Button 컴포넌트에서 실행되는 이벤트 핸들러를 핸들링 하기 위해 onIncrement와 onDecrement 이벤트 핸들러를 등록합니다.

코드 확인

위의 코드는 https://github.com/beomy/hello-react-redux_v1 에서 확인 할 수 있습니다.

2. redux를 사용한 예제

Redux를 사용하여 위와 동일한 예제 입니다. redux 모듈을 설치해야 합니다.

npm install --save redux

컴포넌트 구조

store을 사용한 컴포넌트의 구조는 아래 그림과 같습니다.

컴포넌트 구조
컴포넌트 구조

Redux를 사용하지 않는 예제와 컴포넌트 구성은 동일합니다. 단지 Redux를 사용하기 때문에, store를 사용하게 됩니다.

dispatch가 호출되어 state가 업데이트 되고, subscribe가 호출 되면, 리스터가 등록되어 데이터에 변동이 있을 때 마다 리렌더링 할 수 있도록 설정합니다.

코드 구조

src/actions/index.js

export const INCREMENT = 'INCREMENT';
export const DECREMENT = 'DECREMENT';
export const SET_DIFF = 'SET_DIFF';

export function increment() {
    return {
        type: INCREMENT
    };
}

export function decrement() {
    return {
        type: DECREMENT
    };
}

export function setDiff(value) {
    return {
        type: SET_DIFF,
        diff: value
    };
}
  • 1~3번 줄, action의 type을 정의하여 export 합니다.
  • 5~9번 줄, increment action을 정의합니다.
  • 11~15번 줄, decrement action을 정의합니다.
  • 17~22번 줄, seDiff action을 정의합니다. 어떤 값을 계산(더할 것인지 혹은 뺄 것인지) 해 줄 것인지를 diff에 저장됩니다. 나중에는 reducer에 의해 store에 저장됩니다.

먼저, action입니다. action은 어떤 변화가 일어나야 할지 알려주는 객체 입니다.

action을 작성 할 때, 첫번째 필드 type은 필수적으로 포함되야 합니다. type은 action이 무엇을 해야 하는지.. ID와 같은 개념..? 으로 사용됩니다.

그 이후의 필드는 개발자가 임의로 추가할 수 있습니다. increment와 decrement에는 type만 있지만, setDiff에는 diff 라는 필드가 추가되어 있는 것을 확인 할 수 있습니다. 나중에 설명 할 reducer에서 diff를 store에 저장하게 됩니다.

src/reducers/index.js

import { INCREMENT, DECREMENT, SET_DIFF } from '../actions';
import { combineReducers } from 'redux';

const counterInitialState = {
    value: 0,
    diff: 1
};

const counter = (state = counterInitialState, action) => {
    switch(action.type) {
        case INCREMENT:
            return Object.assign({}, state, {
                value: state.value + state.diff
            });
        case DECREMENT:
            return Object.assign({}, state, {
                value: state.value - state.diff
            });
        case SET_DIFF:
            return Object.assign({}, state, {
                diff: action.diff
            });
        default:
            return state;
    }
}

const counterApp = combineReducers({
    counter
});

export default counterApp;
  • 1번 줄, action에서 정의한 action의 type 들을 import 합니다.
  • 2번 줄, combineReducers 를 import 합니다. combineReducers는 reducer가 여러개 있다면, 하나로 합쳐주는 메소드입니다.
  • 4~7번 줄, state의 초기값을 정의 합니다.
  • 9~26번 줄, counter의 reducer입니다.
  • 9번 줄, default parameter을 이용하여 state가 undefined로 넘어 올 경우 초기 state를 설정해 줍니다. ([자바스크립트] ES6(ECMA Script 6) - 기본 매개변수(Default parameter) 참고)
  • 12, 16, 20번 줄, state를 변경시키지 않고, Object.assign 메소드를 통해 state를 복사하여, 복사한 객체를 수정하여 리턴합니다. Redux에서 state는 읽기 전용이여야 합니다. (Redux의 3가지 원칙 참고)
  • 11~24번 줄, action.type에 따라 reducer가 동작하는 부분입니다.
  • 28번 줄, 작성한 reducer을 하나로 합쳐줍니다.
  • 32번 줄, reducer을 export 합니다.

combineRecucers 메소드는 여러개의 reducer을 하나로 합쳐주는 메소드입니다. 현재 예시는 하나의 reducer만 필요하기 때문에 위와 같이 작성 되었지만, 여러개의 reducer가 있다면, 아래와 같이 작성 하시면 됩니다.

const counterApp = combineReducers({
    counter,
    etc
});

reducer에 다른 key를 주고 싶다면 아래와 같이 사용하면 됩니다.

const counterApp = combineReducers({
    a: counter,
    b: etc
});

src/components/Counter.js

import React, { Component } from 'react';

class Counter extends Component {
    render() {
        return (
            <div>
                <h1>Value : {this.props.store.getState().counter.value}</h1>
            </div>
        );
    }
}

export default Counter;
  • 7번 줄, store.getState()로 store에 저장된 state를 가져와, counter.value를 출력합니다.

store의 state 구조는 아래와 같습니다.

{
    counter: { value: 0, diff: 1 }
}

필드명을 가지고, 그 필드명 하위로 state가 구성되어 있는 것을 확인 할 수 있습니다. 필드명은 reducer에서 combineReducers 메소드에서 정의한 키와 동일 합니다. 별도의 key를 설정하지 않았다면 reducer의 이름과 동일한 필드명을 가지게 됩니다.

src/components/Option.js

import React, { Component } from 'react';
import { setDiff } from '../actions';

class Option extends Component {
    constructor(props) {
        super(props);

        this.onChange = this.onChange.bind(this);
    }

    onChange(event) {
        this.props.store.dispatch(setDiff(parseInt(event.target.value)));
    }

    render() {
        return (
            <div>
                <input value={this.props.store.getState().counter.diff} onChange={this.onChange} />
            </div>
        );
    }
}

export default Option;
  • 2번 줄, setDiff action을 import 합니다.
  • 11~13번 줄, 사용자가 값을 입력할 경우 실행되는 이벤트 핸들러입니다. 이벤트 핸들러는 dispatch로 setDiff라는 action을 보내게 됩니다. setDiff의 인자는 사용자의 입력값이 됩니다. dispatch는 reducer로 action을 전달하고 reducer는 store에 state를 저장합니다.
  • 18번 줄, store.getState()로 state를 가져와, counter.diff를 출력 합니다. 그리고 사용자가 값을 입력 했을 때 호출되는 이벤트 핸들러를 등록합니다.

scr/components/Button.js

import React, { Component } from 'react';
import { increment, decrement } from '../actions';

class Button extends Component {
    constructor(props) {
        super(props);

        this.onIncrement = this.onIncrement.bind(this);
        this.onDecrement = this.onDecrement.bind(this);
    }

    onIncrement(event) {
        this.props.store.dispatch(increment())
    }

    onDecrement(event) {
        this.props.store.dispatch(decrement())
    }

    render() {
        return (
            <div>
                <button onClick={this.onIncrement}>+</button>
                <button onClick={this.onDecrement}>-</button>
            </div>
        );
    }
}

export default Button;
  • 2번 줄, increment와 decrement라는 action을 import 합니다. (정확히는 action 객체를 리턴하는 함수..)
  • 12~14번 줄, + 버튼을 클릭 했을 때, 실행되는 이벤트 핸들러입니다. 이 이벤트 핸들러는 increment라는 action을 dispatch 합니다.
  • 16~18번 줄, - 버튼을 클릭 했을 때, 실행되는 이벤트 핸들러입니다. 이 이벤트 핸들러는 decrement라는 action을 dispatch 합니다.
  • 23~24번 줄, 이벤트 핸들러를 등록합니다.

App.js

import React, { Component } from 'react';
import logo from './logo.svg';
import './App.css';

import Counter from './components/Counter';
import Option from './components/Option';
import Button from './components/Button';

class App extends Component {
  render() {
    return (
      <div>
        <Counter store={this.props.store} />
        <Option store={this.props.store} />
        <Button store={this.props.store} />
      </div>
    );
  }
}

export default App;
  • 5~7번 줄, 보여줄 컴포넌트를 import 합니다.
  • 13~15번 줄, 각각의 컴포넌트는 모두 store을 사용하기 때문에 store을 전달해 줍니다.

13~15번 줄이 react-redux를 사용하지 않을 때 생기는 불편함 중에 하나 입니다. store을 사용하는 모든 child에게 store을 계속해서 전달해 주어야 합니다. 지금은 간단해 보여도, 컴포넌트의 갯수가 여러개가 된다면 햇갈리고, 번거로운 작업이 될 수 있습니다.

index.js

import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import './index.css';

import { createStore } from 'redux';
import counterApp from './reducers';

const store = createStore(counterApp);

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

store.subscribe(render);
render();
  • 6번 줄, createStore을 import 합니다.
  • 7번 줄, reducer인 counterApp을 import 합니다.
  • 9번 줄, store을 만드는 방법입니다. createStore 메소드를 이용하면 되는데, createStore의 인자로 reducer을 전달해 줘야 합니다.
  • 18번 줄, store.subscribe(LISTENER) 형태입니다. dispatch 메소드가 실행되면 (Button 컴포넌트 또는 Option 컴포넌트에서 dispatch 메소드가 실행 되면) LISTENER 함수가 실행 됩니다. 그렇기 때문에, 데이터가 변경 될 때 마다 다시 랜더링하게 됩니다.

코드 확인

위의 코드는 https://github.com/beomy/hello-react-redux_v2 에서 확인 할 수 있습니다.

3. redux + react-redux를 사용한 예제

Redux와 react-redux를 함께 사용한 예제입니다. react-redux 모듈이 먼저 설치되어야 합니다.

npm install --save redux react-redux

컴포넌트 구조

react_redux라는 모듈만 사용할 뿐이기 때문에, 컴포넌트 구조는 Redux를 사용한 예제와 동일합니다.

코드 구조

src/actions/index.js와 src/reducers/index/js

action과 reducer는Redux를 사용한 예제와 동일 합니다.

src/components/Counter.js

code 1 (Redux를 사용한 예제)

import React, { Component } from 'react';

class Counter extends Component {
    render() {
        return (
            <div>
                <h1>Value : {this.props.store.getState().counter.value}</h1>
            </div>
        );
    }
}

export default Counter;

code 2 (Redux + react-redux를 사용한 예제)

import React, { Component } from 'react';
import { connect } from 'react-redux';

class Counter extends Component {
    render() {
        return (
            <div>
                <h1>Value : {this.props.value}</h1>
            </div>
        );
    }
}

let mapStateToProps = (state) => {
    return {
        value: state.counter.value
    }
}

Counter = connect(mapStateToProps)(Counter);

export default Counter;
  • code 2의 2번 줄, react-redux의 connect 를 import 합니다.
  • code 2의 8번 줄, code 1의 7번 줄이 code 2의 8번 줄과 같이 바뀝니다. store을 통해 state를 가져올 필요 없이 props.value로 store 값을 가져 올 수 있게 됩니다. 이것이 가능한 이유는 밑에서 설명드릴 connect 메소드 덕분입니다.
  • code 2의 14~20번 줄, store의 state를 props로 매핑 해주는 부분입니다.

참고 - connect([mapStateToProps], [mapDispatchToProps], [mergeProps], [options])

connect는 react-redux의 API 입니다. 이 함수는 컴포넌트를 store에 연결해 줍니다.

connect 함수는 특정 컴포넌트 클래스의 props를 store에 연결시켜주는 함수를 리턴합니다. 리턴된 함수에 컴포넌트를 인수로 넣어주면 기존 컴포넌트가 수정되는 것이 아니라 새로운 컴포는터를 리턴합니다. (code 2의 20번 줄)

참고 - connect의 parameter

mapStateToProps(state, [ownProps]) : store의 state를 컴포넌트의 props에 매핑 시켜줍니다. ownProps 인자가 명시될 경우, 이를 통해 함수 내부에서 컴포넌트의 props 값에 접근 할 수 있습니다. 즉, store.state를 props로 접근 할 수 있도록 합니다.

mapDispatchToPRops(dispatch, [ownProps]) : 컴포넌트의 함수형 props를 실행 했을 때, 개발자가 지정한 action을 dispatch 하도록 설정합니다. ownProps 는 동일 합니다. 즉, props._function_을 통해 action을 dispatch 할 수 있도록 합니다.

src/components/Option.js

code 1 (Redux를 사용한 예제)

import React, { Component } from 'react';
import { setDiff } from '../actions';

class Option extends Component {
    constructor(props) {
        super(props);

        this.onChange = this.onChange.bind(this);
    }

    onChange(event) {
        this.props.store.dispatch(setDiff(parseInt(event.target.value)));
    }

    render() {
        return (
            <div>
                <input value={this.props.store.getState().counter.diff} onChange={this.onChange} />
            </div>
        );
    }
}

export default Option;

code 2 (Redux + react-redux를 사용한 예제)

import React, { Component } from 'react';
import { connect } from 'react-redux';
import { setDiff } from '../actions';

class Option extends Component {
    constructor(props) {
        super(props);

        this.onChange = this.onChange.bind(this);
    }

    onChange(event) {
        this.props.onUpdateDiff(parseInt(event.target.value));
    }

    render() {
        return (
            <div>
                <input value={this.props.diff} onChange={this.onChange} />
            </div>
        );
    }
}

let mapStateToProps = (state) => {
    return {
        diff: state.counter.diff
    }
}

let mapDispatchToProps = (dispatch) =>{
    return {
        onUpdateDiff: (value) => dispatch(setDiff(value))
    };
}

Option = connect(mapStateToProps, mapDispatchToProps)(Option);

export default Option;
  • code 2의 2번 줄, react-redux의 connect를 import 합니다.
  • code 2의 12~14번 줄, 사용자가 값을 입력할 경우 실행되는 이벤트 핸들러입니다. 이벤트 핸들러는 props.onUpdateDiff 함수를 실행시킵니다. onUpdateDiff 함수는 code 2의 33번 줄에서 정의됩니다.
  • code 2의 19번 줄, props.diff의 값을 출력합니다. diff는 code 2의 27번 줄에서 정의됩니다.
  • code 2의 25~29번 줄, store.state를 prop로 매핑하는 코드입니다.
  • code 2의 31~35번 줄, props.onUpdateDiff를 실행 할 경우 dispatch 할 action을 정의하는 코드입니다.
  • code 2의 37번 줄, mapStateToProps와 mapDispatchToProps에서 작성한 내용을 적용하는 connect 메소드를 호출합니다.

src/components/Button.js

code 1 (Redux를 사용한 예제)

import React, { Component } from 'react';
import { increment, decrement } from '../actions';

class Button extends Component {
    constructor(props) {
        super(props);

        this.onIncrement = this.onIncrement.bind(this);
        this.onDecrement = this.onDecrement.bind(this);
    }

    onIncrement(event) {
        this.props.store.dispatch(increment())
    }

    onDecrement(event) {
        this.props.store.dispatch(decrement())
    }

    render() {
        return (
            <div>
                <button onClick={this.onIncrement}>+</button>
                <button onClick={this.onDecrement}>-</button>
            </div>
        );
    }
}

export default Button;

code 2 (Redux + react-redux를 사용한 예제)

import React, { Component } from 'react';
import { connect } from 'react-redux';
import { increment, decrement } from '../actions';

class Button extends Component {
    render() {
        return (
            <div>
                <button onClick={this.props.onIncrement}>+</button>
                <button onClick={this.props.onDecrement}>-</button>
            </div>
        );
    }
}

let mapDispatchToProps = (dispatch) => {
    return {
        onIncrement: () => dispatch(increment()),
        onDecrement: () => dispatch(decrement())
    }
}

Button = connect(undefined, mapDispatchToProps)(Button);

export default Button;
  • code 2의 2번 줄, react-redux의 connect를 import 합니다.
  • code 2의 9번 줄, + 버튼 클릭시 props.onIncrement를 실행 합니다. onIncrement는 code 2의 18번 줄에서 정의합니다.
  • code 2의 10번 줄, - 버튼 클릭시 props.onDecrement를 실행합니다. onDecrement는 code 2의 19번 줄에서 정의 합니다.
  • code 2의 18번 줄, props.onIncrement를 실행 할 경우 increment action을 dispatch 합니다.
  • code 2의 19번 줄, props.onDecrement를 실행 할 경우 decrement action을 dispatch 합니다.
  • code 2의 23번 줄, props를 store의 state에 매칭 시켜주는 connect 함수를 실행 합니다.

App.js

code 1 (Redux를 사용한 예제)

import React, { Component } from 'react';
import logo from './logo.svg';
import './App.css';

import Counter from './components/Counter';
import Option from './components/Option';
import Button from './components/Button';

class App extends Component {
  render() {
    return (
      <div>
        <Counter store={this.props.store} />
        <Option store={this.props.store} />
        <Button store={this.props.store} />
      </div>
    );
  }
}

export default App;

code 2 (Redux + react-redux를 사용한 예제)

import React, { Component } from 'react';
import logo from './logo.svg';
import './App.css';

import Counter from './components/Counter';
import Option from './components/Option';
import Button from './components/Button';

class App extends Component {
  render() {
    return (
      <div>
        <Counter />
        <Option />
        <Button />
      </div>
    );
  }
}

export default App;
  • code 2의 13~15번 줄, code 1에서 각각의 컴포넌트에 넘겨주던 store를 넘겨주지 않아도 됩니다.

index.js

code 1 (Redux를 사용한 예제)

import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import './index.css';

import { createStore } from 'redux';
import counterApp from './reducers';

const store = createStore(counterApp);

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

store.subscribe(render);
render();

code 2 (Redux + react-redux를 사용한 예제)

import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import './index.css';

import { createStore } from 'redux';
import { Provider  } from 'react-redux';
import counterApp from './reducers';

const store = createStore(counterApp);

ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById('root')
);
  • code 2의 7번 줄, react-redux의 Provider 컴포넌트를 import 합니다.
  • code 2의 13~15번 줄, Provider 컴포넌트 안에 App 컴포넌트를 둡니다. 그리고 Provider 컴포넌트에만 store을 지정해 주면 됩니다.

코드 확인

위의 코드는 https://github.com/beomy/hello-react-redux_v3 에서 확인 할 수 있습니다.

참고

댓글
공지사항
최근에 올라온 글