이번 포스트는 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 를 사용 할 때, 두가지의 의존 모듈이 사용됩니다.
- redux
- react-redux: React.js 프로젝트에서 Redux 를 더 편하게 사용 할 수 있게 해줍니다.
설치해봅시다:
npm install --save redux react-redux
(이 포스트에서는 react-redux@^4.4.5, redux@^3.5.1 이 사용 되었습니다.)
# react-redux 를 사용하지 않고 만들어보기
▲ 우리가 앞으로 구현 할 프로젝트입니다.
예제를 통하여 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 파일을 만들어두셔도 되구요)
이제 새로운 프로젝트를 작성 할 것입니다.
▲ 우리가 앞으로 구현 할 프로젝트입니다
# 디렉터리 구조
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 25: input 값을 수정 할 때 실행됩니다. 숫자만 적을 수 있도록 코드를 작성하였습니다. 값이 수정될 떄 마다 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 서버와 함께 사용하는 방법에 대하여 알아보겠습니다.
References