함께 성장하는 프로독학러

8-2. Counter 어플리케이션 만들기 - 1) reducer, store, react-redux 본문

Programming/react.js

8-2. Counter 어플리케이션 만들기 - 1) reducer, store, react-redux

프로독학러 2018. 4. 27. 14:26

안녕하세요, 프로독학러입니다.


이번 포스팅에서는 저번 포스팅에 이어 Counter 어플리케이션을 만들어보도록 하겠습니다.


*velopert 님의 Youtube 강의를 정리한 내용이라고 보시면 될 것 같습니다. (5강)

<Contact application - velopert>


리덕스를 이용한 상태관리 프로젝트의 순서는 다음과 같다고 했습니다.


1. 액션 타입 생성 (action 은 객체이며, type 속성을 반드시 가지고 있어야 한다.)

2. 액션 생성 함수 정의 (액션 생성 함수를 통하여 action 객체를 빠르게 만들 수 있도록 도와줌)

3. 실질적으로 변화를 일으키는 함수인 리듀서 정의 (인자로는 prevState와 action이 들어옴, 들어오는 action 객체의 타입에 따라 어떤 변화를 일으킬지 정의)

4. 스토어 생성 (3에서 만든 리듀서를 포함하여 createStore를 통해 생성)

5. 스토어의 변화가 생길때 실행될 리스너 함수 정의

6. 클릭 등 이벤트에 dispatch 를 통해 액션 전달


저번 포스팅에서 1~2 번의 과정을 진행했으므로 이번 포스팅에서는 3~6까지의 과정을 진행하도록 하겠습니다.


먼저 reducer 에 관련된 작업을 해 보도록 하겠습니다.


reducer 는 스토어 안에 위치하며, 변화에 관련된 작업을 하는 '함수' 입니다.

이 reducer 는 스토어의 상태와 액션 객체를 인자로 받아 변화를 일으키며, 함수의 리턴값은 새로운 상태 입니다.

reducer 함수는 다음의 세 가지 조건을 만족하는 '순수한 함수' 여야 합니다.


  1. reducer 함수 내에서 비동기 작업을 수행하면 안 된다.
  2. reducer 함수로 들어온 인수를 변경해서는 안된다.
  3. reducer 함수로 들어온 인수가 같다면 결과는 항상 동일해야한다.

위의 reducer 가 순순한 함수여야 한다는 것을 기억해 주세요.


우리는 두개의 파일로 나누어 reducer 함수를 작성할 것입니다. 

하나는 숫자를 올리고 내리는 작업을 하는 counter 리듀서, 다른 하나는 배경색깔을 변경하는 ui 리듀서 입니다.


1) counter.js


src 폴더의 하위에 reducer 들을 위한 폴더 'reducers' 를 생성합니다.

그리고 reducers 폴더의 하위에 counter.js 파일을 생성합니다.

파일의 내용은 다음과 같습니다.


(./src/reducers/counter.js)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import * as types from '../actions/ActionTypes';
 
const initialState = {
  number : 0
};
 
export default function counter(state = initialState, action) {
  switch (action.type) {
    case types.INCREMENT:
      return {
        ...state,
        number : state.number + 1
      };
    case types.DECREMENT:
      return {
        ...state,
        number : state.number - 1
      };
    default:
      return state;
  }
}
cs


코드의 첫 번째 줄에서 액션타입 정보를 담고 있는 ActionTypes 파일을 types 객체로 불러왔습니다.


세 번째 줄에서는 스토어의 초기 상태값을 지정했습니다.


7번째 줄에서 reducer 함수를 정의 했습니다. 파라메터로는 state 와 action 이 들어왔습니다. 

이 때 state 는 default parameter 를 설정하여 state 값을 찾지 못하면 3번째 줄에서 설정한 초기 상태값이 들어오도록 했습니다.

*ES6 - default parameter 에 익숙하지 않으신 분들은 아래의 링크를 참조해 주세요.

<ES6 - default parameter>

함수의 내용은 action 객체의 type 이 INCREMENT 이면 스테이트의 number 값을 1 올려주고, DECREMENT 이면 1 내려주고, INCREMENT, DECREMENT 둘 다 아닐 경우(default)에는 원래의 상태를 리턴하는 내용입니다.

*리턴되는 새로운 상태 객체에 전개 연산자가 사용되었습니다. ES6 - spread operator 에 익숙하지 않으신 분들은 아래의 링크를 참조해 주세요.

<ES6 - spread operator>

*action 객체의 type 을 검사할 때 switch 문을 사용했습니다. switch 문에 익숙하지 않으신 분들은 아래의 링크를 참조해 주세요.

<javascript - switch>


2) ui.js


두 번째로 만들 reducer 는 배경색을 변경하는 reducer 입니다.

reducers 폴더 하위에 ui.js 파일을 생성합니다.

코드의 내용은 다음과 같습니다.


(./src/reducers/ui.js)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import * as types from '../actions/ActionTypes';
 
const initialState = {
  color : [255255255]
};
 
export default function ui(state = initialState, action) {
  if (action.type === types.SET_COLOR) {
    return {
      color : action.color
    };
  } else {
    return state;
  }
}
cs


첫 번째 줄에서 액션타입을 불러왔습니다.


세 번째 줄에서 스토어의 초기 상태값을 지정했습니다.

color 키의 값으로 길이가 3인 배열을 지정했습니다. 

배열의 각 원소들은 CSS 에서 background 로 지정할 rgb 값을 의미합니다.


7번째 줄에서 reducer 함수를 정의했습니다. 

인자로 state 와 action 을 받고, action 객체의 type 에 따라 다르게 동작합니다.

만약 action 객체의 type 이 SET_COLOR 라면 스토어의 상태로 사용할 새로운 객체를 만듭니다.

새로운 객체의 color 값은 action 객체의 color 값으로 설정합니다.

(action 객체의 컬러값은 생성자 함수가 실행될 때 들어오는 인자입니다.)


3) index.js


reducers 폴더의 index.js 는 reducers 폴더 자체를 의미합니다.

reducer 가 여러개라면 reducer 를 하나로 묶어주는 작업이 필요합니다.

리덕스의 스토어를 생성할 때 reducer 를 인수로 주고 생성해야하는데, 인자로는 하나의 값만 들어올 수 있기 때문입니다. 

(아래에서 자세히 살펴볼 예정.)

reducers 폴더 하위에 index.js 파일을 생성합니다.

코드는 다음과 같습니다.


(./src/reducers/index.js)

1
2
3
4
5
6
7
8
9
import { combineReducers } from 'redux';
import counter from './counter';
import ui from './ui';
 
const reducers = combineReducers({
  counter, ui
});
 
export default reducers;
cs


첫 번째 줄에서 여러개의 reducer 를 하나로 묶는 redux 의 메소드인 combineReducers 를 불러왔습니다.


두 번째 줄과 세 번째 줄에서는 이전에 만든 counter 리듀서와 ui 리듀서를 불러왔습니다.


5번째 줄에서 combineReducers 를 통해 리듀서들을 하나로 묶었습니다. 

combineReducers 의 인자로는 객체가 들어오며, 객체 안에 묶을 대상들이 들어갑니다.

combineReducers 메소드를 통해 묶어진 리듀서들을 reducers 라는 상수에 할당했습니다.


9번째 줄에서 묶어진 리듀서(reducers)를 export 했습니다.


여기까지 스토어의 상태에 변화를 일으키는 함수인 reducer 들을 정의했습니다.


다음으로는 store 에 관련된 작업을 해보도록 하겠습니다.


store 는 컴포넌트 외부에 존재하며, 어플리케이션의 상태를 지니고 있습니다.


1) store 생성


store 는 createStore 메소드를 통해 생성합니다.

src 폴더의 index.js 파일을 아래와 같이 수정합니다.


(./src/index.js)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import React from 'react';
import ReactDOM from 'react-dom';
import { createStore } from 'redux';
 
import Counter from './components/Counter';
import reducers from './reducers';
 
 
const store = createStore(reducers);
 
ReactDOM.render(
  <Counter />,
  document.getElementById('root')
);
 
module.hot.accept();
cs


코드의 세 번째 줄에서 스토어를 생성하는 createStore 메소드를 불러왔습니다.


6번째 줄에서 reducers/index.js 파일을 불러왔습니다.

이는 리듀서를 하나로 묶은 파일입니다.


9번째 줄에서 createStore 메소드를 통해 스토어를 생성했습니다.

스토어 안에는 reducer 가 포함돼 있어야 하므로 인자로 reducers 를 주었습니다.

메소드를 통해 만들어진 store 를 store 상수에 할당했습니다.


store 에는 네 가지의 메소드가 존재합니다. 


  1. dispatch(action) - action 객체를 reducer 로 전달하는 메소드
  2. getState() - 스토어의 현재 상태 (state) 를 알아내는 메소드
  3. subscribe(listener) - 상태가 바뀔 때 마다 실행할 콜백함수 (listener) 를 등록하는 메소드
  4. replaceReducer(nextReducer) - hot 리로딩, 코드 분할을 위한 메소드 (다루지 않을 예정)

위의 메소드들을 사용하는 것은 store 를 컴포넌트와 연결시킨 뒤에 하도록 하겠습니다.


2) react 에서 redux 를 사용하기 위한 설정 - provider


위의 과정까지를 통하여 action, reducer, store 를 생성했습니다. 

react 에서 redux 를 사용하려면, 가장 부모가 되는 루트 컴포넌트를 렌더링 할 때 Provider 컴포넌르도 루트 컴포넌트를 감싸 주어야 합니다.


(./src/index.js)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import { createStore } from 'redux';
 
import Counter from './components/Counter';
import reducers from './reducers';
 
 
const store = createStore(reducers);
 
ReactDOM.render(
  <Provider store={store}>
    <Counter />
  </Provider>,
  document.getElementById('root')
);
 
module.hot.accept();
cs


코드의 세 번째 줄에서 react 에서 redux 를 사용하기 위해 react-redux 로 부터 Provider 컴포넌트를 불러왔습니다.


12번째 줄에서 ReactDOM 을 통하여 리액트 컴포넌트를 렌더링 할 때, 13~15번째 줄과 같이 Provider 컴포넌트로 감싸 주었습니다.

Provider 컴포넌트에 store 속성을 줬는데, 이때 우리가 생성한 store 를 넣어주면 redux 를 사용할 수 있습니다.


마지막으로 redux store 를 컴포넌트에서 활용해 보도록 하겠습니다.


1) connect 메소드를 이용해 Counter 컴포넌트와 store 연결


우리의 루트 컴포넌트인 Counter  컴포넌트는 자체의 state 를 가지고 있지 않은 상태입니다.

대신 store 를 통해 store 의 state 를 상위 컴포넌트로부터 전달받은 props 처럼 사용할 것입니다. (Counter 컴포넌트의 입장에서)

그를 위해 필요한 것은 react-redux 의 connect 메소드 입니다.

connect 메소드는 컴포넌트를 redux 에 연결하는 새로운 함수를 반환합니다.

그리고 반환된 함수를 통해 컴포넌트와 redux store 를 연결합니다. (connect 함수는 함수를 반환, 반환된 함수로 연결)


connect([options])(컴포넌트)


위의 표현식과 같은 모습으로 사용됩니다. 연결하는 함수를 만들 때는 인자로 options 가 들어갑니다.

options 는 다음과 같습니다.


  1. mapStateToProps - 리덕스 스테이트를 모든 컴포넌트가 전달받는 props 로 매핑 (함수 - state 를 인자로 받음)
  2. mapDispatchToProps - 디스패치를 모든 컴포넌트가 전달받는 props 로 매핑 (함수 - dispatch 를 인자로 받음)

mapStateToProps 와 mapDispatchToProps 에서 리턴하는 객체의 키(key)가 props 명이 됩니다.


**어떤 컴포넌트에서 store 의 state 나 dispatch 에 접근하고 싶다면 원하는 컴포넌트를 connect 해야만 사용할 수 있다.

(connect 를 통해 store 와 연결된 컴포넌트만이 store 의 state 나 dispatch 에 props 로 접근할 수 있다.)


루트 컴포넌트인 Counter 컴포넌트에서 이를 적용해 봅시다.


(./src/components/Counter.js)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
import React, { Component } from 'react';
import { connect } from 'react-redux';
 
import Value from './Value';
import Control from './Control';
 
 
import * as actions from '../actions';
 
 
class Counter extends Component {
 
    _setRandomColor = () => {
        const color = [
            Math.floor((Math.random()*55+ 200),
            Math.floor((Math.random()*55+ 200),
            Math.floor((Math.random()*55+ 200)
        ];
 
        this.props.handleSetColor(color);
    }
 
    render() {
 
        const color = this.props.color;
        const style = {
            background: `rgb(${color[0]}, ${color[1]}, ${color[2]})`
        };
 
        return (
            <div style={style}>
                <Value number={this.props.number} />
                <Control
                    onPlus={this.props.handleIncrement}
                    onSubtract={this.props.handleDecrement}
                    onRandomizeColor={this._setRandomColor}
                />
            </div>
        );
    }
}
 
const mapStateToProps = (state) => {
    return {
        number: state.counter.number,
        color: state.ui.color
    };
};
 
const mapDispatchToProps = (dispatch) => {
    //return bindActionCreators(actions, dispatch);
    return {
        handleIncrement: () => { dispatch(actions.increment()); },
        handleDecrement: () => { dispatch(actions.decrement()); },
        handleSetColor: (color) => { dispatch(actions.setColor(color)); }
    };
};
 
 
export default connect(mapStateToProps, mapDispatchToProps)(Counter);
cs


코드의 두 번째 줄에서 connect 메소드를 불러왔습니다.


8번째 줄에서 이전 포스팅에서 만들었던 액션 생성자들을 actions 객체로 불러왔습니다.


코드의 43번째 줄에서 mapStateToProps 함수를 정의 했습니다. 

인자로는 state(redux-store의) 를 받고, 객체를 리턴합니다. 

이 객체는 Counter 컴포넌트(현재 컴포넌트)에서 props 를 통해 접근할 수 있는 객체입니다.

props 의 number 값을 store 의 state.counter.number 로 지정하고, 

color 값을 store 의 state.ui.color 로 지정했습니다.


50번째 줄에서는 mapDispatchToProps 함수를 정의했습니다.

이를 통해 store 의 dispatch 메소드를 Counter 컴포넌트(현재 컴포넌트)에서 props 처럼 사용할 수 있게 됩니다.

(각 디스패치의 인자로 액션 객체가 들어가야함 -> 액션생성자를 통하여 액션객체를 생성)

(각 메소드의 이름이 props 명으로 사용됨)


코드의 마지막 줄(60번째 줄)에서 connect 메소드를 이용해 Counter 컴포넌트와 store 를 연결했습니다.


*이를 통해 마치 아래의 객체가 Counter 컴포넌트의 props 로 전달된 것 처럼 작동합니다.

1
2
3
4
5
6
7
{
    number: state.counter.number,
    color: state.ui.color
    handleIncrement: () => { dispatch(actions.increment()); },
    handleDecrement: () => { dispatch(actions.decrement()); },
    handleSetColor: (color) => { dispatch(actions.setColor(color)); }
}
cs


코드의 13번째 줄에서는 rgb 값을 랜덤으로 설정하는 setRandomColor 함수를 정의 했습니다.

200~255 사이의 rgb 값을 랜덤으로 설정해 color 라는 상수에 할당하고, color 를 55번째에서 정의한 handleSetColor 의 인자로 주어 실행했습니다.

*랜덤하게 rgb 값을 설정하는 코드는 reducer 가 아닌 컴포넌트 안에서 실행되야 합니다.

(리듀서는 순수한 함수, 같은 인자를 넣으면 매번 같은결과가 나와야 함.)


25번째 줄에서 상수 color 에 스토어를 통해 전달받은 props.color 를 할당했습니다.

26번째 줄에서 style 을 CSS 문법에 따라 정의했습니다.

(rgb 의 값은 각각 배열의 원소를 주었습니다.)

*여기서 ES6 - template literals 를 사용했습니다. 이에 익숙하지 않으신 분은 아래의 링크를 참조해 주세요

<ES6 - template literals>


31번째 줄에서 배경에 스타일을 지정했습니다.


32번째 줄과 33번째 줄에서 각각 Value 컴포넌트와 Control 컴포넌트에 프롭스를 전달했습니다.

(각각 컴포넌트에서 바로 사용하려면 각각의 컴포넌트에서 해당 컴포넌트와 스토어를 connect 해 줘야 한다.)

(그렇게 하지 않는 이유는 default props 와 props validation 의 이유도 있고, 코드의 중복을 피하기 위해서도 있다.)


2) Value 컴포넌트에서 Counter 컴포넌트로 전달받은 store 의 state.counter.number 를 렌더링


Value 컴포넌트는 Counter 컴포넌트로 부터 number 프롭스로 스토어의 state 값을 전달 받았습니다.

이를 Value 컴포넌트 안에서 렌더링 하는 코드입니다.


(./src/components/Value.js)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import React, { Component } from 'react';
import PropTypes from 'prop-types';
 
const propTypes = {
  number:PropTypes.number
};
const defaultProps = {
  number:-1
};
 
class Value extends Component {
    state = {
 
    }
    render() {
        return (
            <div>
              <h1>{this.props.number}</h1>
            </div>
        );
    }
}
 
Value.propTypes = propTypes;
Value.defaultProps = defaultProps;
 
export default Value;
cs


코드의 4, 7번째 줄에서 각각 props 를 검증, 기본값 설정 하는 객체를 만들었습니다.

(24, 25번째 줄에서 해당객체를 사용)


그리고 18번째 줄에서 전달받은 store 의 state.counter.number 를 h1 태그를 이용해 렌더링 했습니다.


3) Control  컴포넌트에서 버튼 이벤트를 통해 dispatch 실행 

(dispatch 를 통해 action 객체를 스토어의 리듀서로 전달)


Control 컴포넌트는 Counter 컴포넌트를 통해 onPlus, onSubtract, onRandomizeColor 를 전달받았습니다.

이들은 함수로, dispatch 를 통해 스토어에 상태변경을 요청하는 store 의 메소드 입니다.


(./src/components/Control.js)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
import React, { Component } from 'react';
import PropTypes from 'prop-types';
 
const propTypes = {
  onPlus : PropTypes.func,
  onSubtract : PropTypes.func,
  onRandomizeColor : PropTypes.func
};
 
function createWarning(funcName) {
  return () => console.warn(`${funcName} is not defined`);
}
 
const defaultProps = {
  onPlus : createWarning('onPlus'),
  onSubtract : createWarning('onSub'),
  onRandomizeColor : createWarning('onRandomColor'),
};
 
class Control extends Component {
    state = {
 
    }
    render() {
        return (
            <div>
              <button onClick={this.props.onPlus}>+</button>
              <button onClick={this.props.onSubtract}>-</button>
              <button onClick={this.props.onRandomizeColor}>Randomize Color</button>
            </div>
        );
    }
}
 
Control.propTypes = propTypes;
Control.defaultProps = defaultProps;
 
export default Control;
cs


코드의 4, 36 번째 줄에서 프롭스의 타입을 검증하고, 14, 37 번째 줄에서 기본 프롭스 값을 설정합니다.


10번째 줄의 함수는 콘솔에 경고창을 띄우는 코드를 자동으로 생성하는 함수입니다. (15-17번째 줄에서 사용됩니다.)


27-29번째 줄의 버튼들에 onClick 이벤트로 프롭스로 받은 함수를 지정해 줍니다.


코드가 동작하는 방식은 버튼을 누르면 dispatch 가 action 객체와 함께 실행되고, store 안의 reducer 가 전달받은 action 객체의 type 에 따라 상태를 변경하고, 변경된 값이 화면에 표시되는 방식입니다.


아래의 reducer 를 사용하는 순서 중


1. 액션 타입 생성 (action 은 객체이며, type 속성을 반드시 가지고 있어야 한다.)

2. 액션 생성 함수 정의 (액션 생성 함수를 통하여 action 객체를 빠르게 만들 수 있도록 도와줌)

3. 실질적으로 변화를 일으키는 함수인 리듀서 정의 (인자로는 prevState와 action이 들어옴, 들어오는 action 객체의 타입에 따라 어떤 변화를 일으킬지 정의)

4. 스토어 생성 (3에서 만든 리듀서를 포함하여 createStore를 통해 생성)

5. 스토어의 변화가 생길때 실행될 리스너 함수 정의

6. 클릭 등 이벤트에 dispatch 를 통해 액션 전달


3, 4, 6 번을 완료했습니다.

(스토어를 구독하는 5번 과정은 생략했습니다.)


여기까지의 코드 작성된 코드들은 다음과 같습니다.


(./src/actions/ActionTypes.js)

1
2
3
4
 export const INCREMENT = "INCREMENT";
 export const DECREMENT = "DECREMENT";
 export const SET_COLOR = "SET_COLOR";
 
cs


(./src/actions/index.js)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 액션 생성자
import * as types from './ActionTypes';
 
export function increment() {
  return {
    type : types.INCREMENT
  };
}
 
export function decrement() {
  return {
    type : types.DECREMENT
  };
}
 
export function setColor(color) {
  return {
    type : types.SET_COLOR,
    color
  };
}
 
cs


(./src/reducers/counter.js)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import * as types from '../actions/ActionTypes';
 
const initialState = {
  number : 0
};
 
export default function counter(state = initialState, action) {
  switch (action.type) {
    case types.INCREMENT:
      return {
        ...state,
        number : state.number + 1
      };
    case types.DECREMENT:
      return {
        ...state,
        number : state.number - 1
      };
    default:
      return state;
  }
}
 
cs


(./src/reducers/ui.js)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import * as types from '../actions/ActionTypes';
 
const initialState = {
  color : [255255255]
};
 
export default function ui(state = initialState, action) {
  if (action.type === types.SET_COLOR) {
    return {
      color : action.color
    };
  } else {
    return state;
  }
}
 
cs


(./src/reducers/index.js)

1
2
3
4
5
6
7
8
9
import { combineReducers } from 'redux';
import counter from './counter';
import ui from './ui';
 
const reducers = combineReducers({
  counter, ui
});
 
export default reducers;
cs


(./src.index.js)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import { createStore } from 'redux';
 
import Counter from './components/Counter';
import reducers from './reducers'
 
 
const store = createStore(reducers);
 
ReactDOM.render(
  <Provider store={store}>
    <Counter />
  </Provider>,
  document.getElementById('root')
);
 
module.hot.accept();
 
cs


(./src/components/Counter.js)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
import React, { Component } from 'react';
import { connect } from 'react-redux';
// import { connect, bindActionCreators } from 'react-redux';
 
import Value from './Value';
import Control from './Control';
 
 
import * as actions from '../actions';
 
 
class Counter extends Component {
 
    _setRandomColor = () => {
        const color = [
            Math.floor((Math.random()*55+ 200),
            Math.floor((Math.random()*55+ 200),
            Math.floor((Math.random()*55+ 200)
        ];
 
        this.props.handleSetColor(color);
    }
 
    render() {
 
        const color = this.props.color;
        const style = {
            background: `rgb(${color[0]}, ${color[1]}, ${color[2]})`
        };
 
        return (
            <div style={style}>
                <Value number={this.props.number} />
                <Control
                    onPlus={this.props.handleIncrement}
                    onSubtract={this.props.handleDecrement}
                    onRandomizeColor={this._setRandomColor}
                />
            </div>
        );
    }
}
 
const mapStateToProps = (state) => {
    return {
        number: state.counter.number,
        color: state.ui.color
    };
};
 
const mapDispatchToProps = (dispatch) => {
    //return bindActionCreators(actions, dispatch);
    return {
        handleIncrement: () => { dispatch(actions.increment()); },
        handleDecrement: () => { dispatch(actions.decrement()); },
        handleSetColor: (color) => { dispatch(actions.setColor(color)); }
    };
};
 
 
export default connect(mapStateToProps, mapDispatchToProps)(Counter);
 
cs


(./src/components/Value.js)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import React, { Component } from 'react';
import PropTypes from 'prop-types';
 
const propTypes = {
  number:PropTypes.number
};
const defaultProps = {
  number:-1
};
 
class Value extends Component {
    state = {
 
    }
    render() {
        return (
            <div>
              <h1>{this.props.number}</h1>
            </div>
        );
    }
}
 
Value.propTypes = propTypes;
Value.defaultProps = defaultProps;
 
export default Value;
 
cs


(./src/components/Control.js)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
import React, { Component } from 'react';
import PropTypes from 'prop-types';
 
const propTypes = {
  onPlus : PropTypes.func,
  onSubtract : PropTypes.func,
  onRandomizeColor : PropTypes.func
};
 
function createWarning(funcName) {
  return () => console.warn(`${funcName} is not defined`);
}
 
const defaultProps = {
  onPlus : createWarning('onPlus'),
  onSubtract : createWarning('onSub'),
  onRandomizeColor : createWarning('onRandomColor'),
};
 
class Control extends Component {
    state = {
 
    }
    render() {
        return (
            <div>
              <button onClick={this.props.onPlus}>+</button>
              <button onClick={this.props.onSubtract}>-</button>
              <button onClick={this.props.onRandomizeColor}>Randomize Color</button>
            </div>
        );
    }
}
 
Control.propTypes = propTypes;
Control.defaultProps = defaultProps;
 
export default Control;
 
cs


지금까지 리덕스의 개념을 활용한 Counter 어플리케이션을 만들어 보았습니다.

마지막으로 리덕스를 사용하는 과정에 대해서 정리해 보도록 하겠습니다.


1. 액션 타입 생성 (action 은 객체이며, type 속성을 반드시 가지고 있어야 한다.)

2. 액션 생성 함수 정의 (액션 생성 함수를 통하여 action 객체를 빠르게 만들 수 있도록 도와줌)

3. 실질적으로 변화를 일으키는 함수인 리듀서 정의 (인자로는 prevState와 action이 들어옴, 들어오는 action 객체의 타입에 따라 어떤 변화를 일으킬지 정의)

4. 스토어 생성 (3에서 만든 리듀서를 포함하여 createStore를 통해 생성 - reactDOM 을 통해 리액트 컴포넌트를 렌더링 하는 곳에서 생성)

5. store 의 state 와 dispatch 를 사용할 컴포넌트를 connect 를 이용해 스토어와 컴포넌트를 연결

6 스토어의 변화가 생길때 실행될 리스너 함수 정의

7. 클릭 등 이벤트에 dispatch 를 통해 액션 전달


리덕스에 대해 이해가 잘 되셨길 바라겠습니다.

리덕스는 상태관리를 외부에서 하는 것이다! 라는 것이 리덕스의 핵심 개념입니다.

감사합니다.


**참고 자료 (항상 감사드립니다)

https://velopert.com/reactjs-tutorials


*다녀가셨다는 표시는 공감으로 부탁드릴게요! (로그인 하지 않으셔도 공감은 가능합니다 ㅎㅎ)

Comments