[React.JS] 강좌 10-2편 Redux: 예제를 통해 사용해보기


liste

이번 포스트는 React.js 에서 Redux를 실제로 이용해보는 강좌입니다. Redux에 대한 이해가 부족하신분은 이전 강좌를 참조해주세요.

# 프로젝트 시작하기

강좌 2.1편을 참조하여 React.js 프로젝트를 생성하세요. (NPM 을 통하여 환경설정을 하시길 바랍니다)

이 프로젝트에선 webpack.config.js 에서 entry 를 index.js 로 설정하겠습니다.

module.exports = {
    entry: './src/index.js',

    output: {
        path: __dirname,
        filename: 'app.js'
    },
....

# 복습하기

강좌 10-1편 에서 Redux에 대한 배경지식을 공부했었죠? 한번 복습해봅시다.

  • store: React.js 프로젝트에서 사용하는 모든 동적 데이터들을 담아두는 곳 입니다.
  • action: 어떤 변화가 일어나야 할 지 나타내는 객체입니다.
  • reducer: action 객체를 받았을 때, 데이터를 어떻게 바꿀지 처리할지 정의하는 객체입니다.

# 의존 모듈 설치

React.js 에서 Redux 를 사용 할 때, 두가지의 의존 모듈이 사용됩니다.

  1. redux
  2. react-redux: React.js 프로젝트에서 Redux 를 더 편하게 사용 할 수 있게 해줍니다.

설치해봅시다:

   
npm install --save redux react-redux

(이 포스트에서는 react-redux@^4.4.5, redux@^3.5.1 이 사용 되었습니다.)


# react-redux 를 사용하지 않고 만들어보기

3

▲ 우리가 앞으로 구현 할 프로젝트입니다.

예제를 통하여 react-redux 를 사용하지 않고 redux 를 사용하는 방법을 알아봅시다.
(이 과정은 배우지 않아도 무방하나, redux 에 대한 이해를 돋구기 위해서 공부해봅시다)

# index.js – 의존 모듈 불러오기

import React from 'react';
import ReactDOM from 'react-dom';
import { createStore } from 'redux';

redux를 사용하기 위해선 createStore 객체를 불러와야합니다.

 

# index.js – action 작성하기

/*
 * Action
 */
const INCREMENT = "INCREMENT";

function increase(diff) {
    return {
        type: INCREMENT,
        addBy: diff
    };
}

어떤 변화가 일어나야 할 지 알려주는 객체인 action 을 작성하였습니다.

action을 작성 할 땐, 첫번째 필드는 type 으로서 필수적인 필드이며, action 의 형태를 정의해줍니다.

그 다음으로는 개발자가 마음대로 추가 할 수 있습니다. 필요없으면 생략해도 되는 부분입니다.

저희는 한번 클릭 될 때, 값이 얼마나 더해질 지 정할 수 있도록 addBy 를 추가하였습니다.

 

# index.js – reducer 작성하기

/*
 * Reducer
 */
const initialState = {
    value: 0
};

const counterReducer = (state = initialState, action) => {
    switch(action.type) {
        case INCREMENT:
            return Object.assign({}, state, {
                value: state.value + action.addBy
            });
        default:
            return state;
    }
}

reducer 를 만들때는, 우선 데이터의 초기 상태를 정의하고 arrow function 을 통하여 reducer를 만듭니다.

8번줄에서는 ES6 의 default parameter 를 사용하였습니다. 해당 parameter 가 undefined 일 때는 값을 initialState 로 설정하는 것 입니다.

주의 할 점

우리는 state 를 변경시키지 않습니다. 단, Object.assign() 메소드를 통하여 state를 복사 한후, 복사된 객체를 수정하여 반환합니다.

첫번째 argument 는 꼭 비어있는 객체이어야 합니다.

# index.js – store 생성하기

/*
 * Store
 */
const store = createStore(counterReducer);

store를 만들때는 createStore() 메소드를 사용하며 reducer가 인수로 사용됩니다.

 

# index.js – App 컴포넌트 작성하기

class App extends React.Component {
    constructor(props) {
        super(props);
        this.onClick = this.onClick.bind(this);
    }

    render() {

        let centerStyle = {
            position: 'fixed',
            top: '50%',
            left: '50%',
            transform: 'translate(-50%, -50%)',
            WebkitUserSelect: 'none',
            MozUserSelect: 'none',
            MsUserSelect:'none',
            userSelect: 'none',
            cursor: 'pointer'
        };

        return (
            <div
                onClick={ this.onClick }
                style={ centerStyle }
            >

                <h1> {this.props.store.getState().value} </h1>
            </div>
        )
    }

    onClick() {
        this.props.store.dispatch(increase(1));
    }
}

이 컴포넌트는 렌더링 될 때, store 를 props로 전달받습니다.

27번줄 과 33번줄에 주의하세요.  

  • store.getState() :  현재 스토어에있는 데이터를 반환합니다.
  • store.dispatch(ACTION) : 상태값을 수정 할 때 사용되는 메소드입니다. 인수로는 action 이 전달됩니다. 위 컴포넌트에서는 사전에 만들어둔 increase 함수가 action 객체를 반환합니다.

 

# index.js – 렌더링하기

const render = () => {

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

store.subscribe(render);
render();

LINE 5: store 를 App 컴포넌트의 props 로 전달해주었습니다.

LINE 10: store.subscribe(LISTENER)dispatch 메소드가 실행되면 리스너 함수가 실행됩니다. 즉, 데이터에 변동이 있을때마다 리렌더링하도록 설정합니다.

첫번째 예제를 완성하였습니다. 전체코드는 GitHub 에서 확인 하실 수 있습니다. Redux 가 사용되는 과정을 얼추 이해하셨나요? 근데 이런 방식으로 코딩을 하면 컴포넌트 갯수가 여러개가 되었을때, child 에게 store 를 계속해서 전달해주어야하니 좀 번거로워집니다. 더 편한 코드 유지보수를 위하여, react-redux 모듈을 사용하면 한켠 편해집니다. 어디한번 배워봅시다.


# react-redux 사용하기

기존에 사용했던 파일은 이제 비워주세요. (코딩한것이 아깝다면 나중을 위해 .bak 파일을 만들어두셔도 되구요)

이제 새로운 프로젝트를 작성 할 것입니다.

Animation

▲ 우리가 앞으로 구현 할 프로젝트입니다

# 디렉터리 구조

src
├── actions
│        └── index.js
├── components
│        ├── App.js
│        ├── Buttons.js
│        ├── Counter.js
│        └── Option.js
├── index.js
└── reducers
    └── index.js

강좌를 따라하면서 파일을 만들어도 되지만 사전에 만들어 놓는게 편합니다.

mkdir actions components reducers && touch actions/index.js components/App.js components/Buttons.js components/Counter.js components/Option.js reducers/index.js

 

이번 프로젝트를 작성하는 순서는 첫번째 예제 프로젝트와 비슷합니다.

action -> reducer -> store -> components 순으로 프로젝트를 작성하겠습니다.

단 차이점이 있다면? 각각 다른 파일에 분리하여 작성하는 것 입니다.

 

# actions/index.js – action 작성하기

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
    };
}
  • INCREMENT: 카운터의 값을 올린다
  • DECREMENT: 카운터의 값을 낮춘다
  • SET_DIFF: 버튼이 눌릴 때 더하거나 뺄 값을 설정한다

혹시 “export” 가 익숙하지 않으신가요? Mozilla ES6 참고자료를 참조하세요

# reducers/index.js – reducer 작성하기

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 extra = (state = { value: 'this_is_extra_reducer' }, action) => {
    switch(action.type) {
        default:
            return state;
    }
}

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

export default counterApp;
  • LINE 2: combineReducers 는 여러개의 reducer를 한개로 합칠 때 사용 되는 redux 내장 메소드 입니다.
  • LINE 36 – 39: combineReducers 를 사용 할 땐 이렇게 사용합니다.

이 예제에서는 딱히 여러개의 reducer 를 사용 할 필요가 없었으므로, 예제로만 작성 해 보았습니다.

위의 reducer 를 사용하여 store를 만들게 되면, store 의 state 구조는 다음과 같이 생성됩니다:

{
    counter: { value: 0, diff: 1 }
    extra: { value: 'this_is_extra_reducer' }
}

reducer를 여러개로 분리하여 작성 할 땐, 서로 직접적인 관계가 없어야 합니다.

예를 들어,INCREMENT 와 DECREMENT 부분에서, diff 값을 사용해야 하므로, SET_DIFF를 다른 reducer에 작성하지 않았죠.

LINE 36 – 39 코드는 다음과 코드와 동일합니다 :

const counterApp = ( state = { }, action ) => {
    return {
        counter: counter(state.counter, action),
        extra: extra(state.extra, action)
    }
}

combineReducers 를 사용 할 때, 각 reducer에 다른 key를 주고싶다면 다음과 같이 작성하면 됩니다.

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

# components/Counter.js – Counter 컴포넌트 작성하기

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

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

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

Counter = connect(mapStateToProps)(Counter);

export default Counter;

# connect API (자세히..)

connect([mapStateToProps], [mapDispatchToProps], [mergeProps], [options])

connect는 react-redux 의 내장 API 입니다. 이 함수는 React Component 를 Redux Store에  ‘연결’ 해줍니다.

이 함수의 리턴값은 특정 컴포넌트 클래스의 props 를 store의 데이터에 연결시켜주는 또 다른 함수를 리턴합니다.

리턴된 함수에 컴포넌트를 인수로 넣어 실행하면, 기존 컴포넌트를 수정하는게 아니라 새로운 컴포넌트를 return 합니다.

인수:

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

mapDispatchToProps(dispatch, [ownProps]): (Function or Object)  컴포넌트의 특정 함수형 props 를 실행 했을 때, 개발자가 지정한 action을 dispatch 하도록 설정합니다. ownProps의 용도는 위 인수와 동일합니다.

mapDispatchToProps 를 객체형으로 전달하는 방법 및 기타 인수들은 매뉴얼을 참조해주세요.

 

  • LINE 2: connect 를 react-redux 에서 불러옵니다.
  • LINE 12 – 16: mapStateToProps 는 이런식으로 arrow functions 를 사용해서 작성합니다.
  • LINE 18: 위에서 만든 mapStateToProps 를 사용하여 컴포넌트를 store에 연결시킵니다.

# components/Buttons.js – Buttons 컴포넌트 작성하기

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

class Buttons extends React.Component {
    render() {
        return (
            <div>
                <button type="button"
                        onClick={ this.props.onIncrement }>
                        +
                </button>
                <button type="button"
                        onClick={ this.props.onDecrement }>
                        -
                </button>
            </div>
        )
    }
}


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

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

export default Buttons;
  • LINE 3: increment, decrement action을 불러옵니다.
  • LINE 30여기선 mapStateToProps 가 필요없으므로 undefined 를 전달하여 생략해줍니다.

# components/Option.js – Option 컴포넌트 작성하기

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

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

        this.state = {
            diff: '1'
        }


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

    render() {
        return (
            <div>
                <input type="text" value={ this.state.diff } onChange={this.onChangeDiff}></input>
            </div>
        );
    }

    onChangeDiff(e) {

        if(isNaN(e.target.value))
            return;

        this.setState({ diff: e.target.value });

        if(e.target.value=='') {
            this.setState({ diff: '0' });
        }

        this.props.onUpdateDiff(parseInt(e.target.value));

    }
}

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

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

export default Option;
  • LINE 3: setDiff action을 불러옵니다.
  • LINE 20컴포넌트 내부의 input 값의 경우 자체 state 를 사용하도록합니다.
  • LINE 25input 값을 수정 할 때 실행됩니다. 숫자만 적을 수 있도록 코드를 작성하였습니다. 값이 수정될 떄 마다 mapDispatchToProps 에서 매핑된 onUpdateDiff 를 통하여 새로운 값을 dispatch 합니다.

# components/App.js – App 컴포넌트 작성하기

import React from 'react';
import Counter from './Counter';
import Buttons from './Buttons';
import Option from './Option';

class App extends React.Component {
    render(){
        return (
            <div style={ {textAlign: 'center'} }>
                <Counter/>
                <Option/>
                <Buttons/>
            </div>
        );
    }

}

export default App;

# index.js –  index 작성하기

import React from 'react';
import ReactDOM from 'react-dom';
import { createStore } from 'redux';
import { Provider  } from 'react-redux';
import App from './components/App';
import counterApp from './reducers';

const store = createStore(counterApp);
const appElement = document.getElementById('app');



ReactDOM.render(
    <Provider store = {store}>
        <App />
    </Provider>,
    appElement
);

렌더링 될 때 Redux 컴포넌트인 <Provider> 에 store 를 설정해주면 그 하위 컴포넌트들에 따로 parent-child 구조로 전달해주지 않아도 connect 될 때 store에 접근 할 수 있게 해줍니다.

이렇게 저희 예제 프로젝트가 완성되었습니다.


# 마치면서..

redux 를 사용하는것은 이해만 한다면 의외로 간단합니다.프로젝트에 사용된 코드는 GitHub 에서 모두 열람 가능합니다.

다음 강좌에서는 webpack-development 서버에만 의존하지 않고 서버 API 구현을 위하여 Express.js 서버와 함께 사용하는 방법에 대하여 알아보겠습니다.

liste

References

  1. Redux Docs
  2. Egghead.io – React Counter Example
  • 혹시나해서 와봤는데 2편이 나왔군요!!!!
    감사하게 잘 읽었습니다~^^ 회사 프로젝트에서 react, redux, webpack으로 환경구성하여 막무가내로 영어자료, 영어강좌들 찾아가면서 꾸역꾸역 만들고있는데…한글 자료가 너무 너무 반갑네요…ㅠㅠ
    다시 한 번 감사하고 잘 읽었습니다~!!!!

  • hanwool kim

    블로그를 잘보고 있는 초보 개발자입니다.
    블로그를 보던 중 이해가 안가는 부분이 있어 질문 드립니다.

    Counter = connect(mapStateToProps)(Counter);

    connect([mapStateToProps], [mapDispatchToProps], [mergeProps], [options])
    connect API의 경우, 위와 같이 내 가지 argument를 받아 컴포넌트가 생성되는 것으로 알고 있습니다.
    그렇다면 바로 뒤에 오는 (Counter)의 의미가 무엇인가요?

    class Counter의 constructor argument인건지 아니면 다른 의미가 있는지 궁금합니다~

    • Redux의 Connect.js – https://github.com/reactjs/react-redux/blob/master/src/components/connect.js 를 확인해보시면:

      connect() 함수에 return function wrapWithConnect(WrappedComponent) { … } 가 있습니다.
      즉, 이 함수의 리턴값은 함수라는거죠. 그리고 리턴된 함수에서 데이터를 연결시켜주는 작업을 하구요.

      쉽게 이해하자면, connect() 함수가 리턴하는 또 다른 함수에 argument를 Counter 클래스를 넣어줌으로서 Counter 클래스를 변형시키는것이라고 이해하시면 되겠습니다.

      꾸준히 읽어주셔서 감사합니다 😀

  • 이성필

    좋은 글 정말 감사합니다. 이해하려고 몇번을 위아래로 왔다갔다 했는지…
    덕분에 잘 배우고 갑니다!!

    • 안녕하세요~ 혹시 강좌의 글 순서가 난해했나요?

      • 이성필

        아 redux를 이해하는데 조금 걸렸어요ㅎㅎ

  • Kendrick B. Jung

    너무 잘봤습니다. 글에서 고생하신 흔적이 느껴지네요
    Button.js쪽 아래 Option.js 인데 Buttons.js로 표시된부분 수정 필요할 것 같습니다.
    다시한번 너무 잘읽었습니다 감사합니다

    • 잘 읽어주셔서 감사합니다 🙂

  • :D

    LINE 36 – 39 코드는 다음과 코드와 동일합니다 :
    이부분 예제코드 익명함수에 화살표가 빠져있어요

  • 김광섭

    우와.. 정말 잘 봤습니다.
    저같은 초심자가 볼때는 이제까지 본 글들 중에 최고네요..
    이해도 되면서 술술 읽히네요.
    좋은 글 계속 기대할게요 ㅎㅎ
    감사합니다.

  • Seongkuk Park

    오탈자 입니다. transofrm -> transform 으로 바꿔주세요~
    강의 잘 보고 있습니다! 감사합니다 :^)

  • 조재민

    안녕하세요 강의 잘 보고 있습니다.
    질문이 하나 있는데 reducer마다 key를 다르게 주는 방법을 추가로 설명해주셨는데, key를 다르게 해야할 경우가 있는건가요? 이해가 잘 안되서 질문드립니다.

    • 편의상 이유 말고는 없습니다

  • Yoon Jae Park

    Ie 호환은 안되는건가요?? 크롬에서는 잘되는데 익스플로어에서는 안돌아가네요 Object.assign 때문에 일단 에러인데 검색해보면 redux가 ie 지원 안한다는거 같아서 질문드려요

  • Stewart Arthur

    강의내용을 너무 재미있게 읽었어요
    정말로 성의있는 강의를 올려주셔서 감사의 인사를 전합니다.
    좋은강의 계속올려주시면 합니다.

    • 재밌게 읽어주셔서 감사합니다 🙂

  • dali high

    잘봤습니다 ㅎㅎ

  • 정말 정말 많은 공부가 되었습니다.
    개념들이 정확하지 않았는데 리덕스부터 접하고 나니 확실히 더 개념이 잘 잡힌 것 같습니다.
    정말 정말 감사합니다 ^^

  • 오현석

    잘 배워갑니다