create-react-app 으로 프로젝트 생성,
redux 와 react-redux 설치
$ create-react-app redux-counter
$ cd redux-counter
$ yarn add redux react-redux
파일 제거
디렉토리 생성
src/
actions/
components/
containers/
reducers/
utils/
별명: 멍청한 컴포넌트
별명: 똑똑한 컴포넌트
비어있는 App 컴포넌트 생성
src/containers/App.js
import React, { Component } from 'react';
class App extends Component {
render() {
return (
<div>
Counter
</div>
);
}
}
export default App;
index 에 반영하기
src/index.js
import React from 'react';
import ReactDOM from 'react-dom';
import App from './containers/App';
import './index.css';
ReactDOM.render(
<App />,
document.getElementById('root')
);
개발서버 실행
$ yarn start
전달받는 props
number,
color,
onIncrement,
onDecrement,
onSetColor
src/components/Counter.js
import React from 'react';
import PropTypes from 'prop-types';
import './Counter.css';
const Counter = ({number, color, onIncrement, onDecrement, onSetColor}) => {
return (
<div
className="Counter"
onClick={onIncrement}
onContextMenu={
(e) => {
e.preventDefault();
onDecrement();
}
}
onDoubleClick={onSetColor}
style={{backgroundColor: color}}>
{number}
</div>
);
};
Counter.propTypes = {
number: PropTypes.number,
color: PropTypes.string,
onIncrement: PropTypes.func,
onDecrement: PropTypes.func,
onSetColor: PropTypes.func
};
Counter.defaultProps = {
number: 0,
color: 'black',
onIncrement: () => console.warn('onIncrement not defined'),
onDecrement: () => console.warn('onDecrement not defined'),
onSetColor: () => console.warn('onSetColor not defined')
};
export default Counter;
src/components/Counter.css
.Counter {
/* 레이아웃 */
width: 10rem;
height: 10rem;
display: flex;
align-items: center;
justify-content: center;
margin: 1rem;
/* 색상 */
color: white;
/* 폰트 */
font-size: 3rem;
/* 기타 */
border-radius: 100%;
cursor: pointer;
user-select: none;
transition: background-color 0.75s;
}
src/containers/App.js
import React, { Component } from 'react';
import Counter from '../components/Counter';
class App extends Component {
render() {
return (
<div>
<Counter/>
</div>
);
}
}
export default App;
모든 액션 객체는 type 를 지니고 있다.
{
type: "INCREMENT"
}
{
type: "DECREMENT"
}
{
type: "SET_COLOR"
color: "black"
}
ActionTypes.js 에 타입 정의
src/acctions/ActionTypes.js
/*
Action 의 종류들을 선언합니다.
앞에 export 를 붙임으로서, 나중에 이것들을 불러올 때,
import * as types from './ActionTypes' 를 할 수 있어요.
*/
export const INCREMENT = 'INCREMENT';
export const DECREMENT = 'DECREMENT';
export const SET_COLOR = 'SET_COLOR';
= 액션 객체를 만드는 함수
src/actions/index.js
/*
action 객체를 만드는 액션 생성자들을 선언합니다. (action creators)
여기서 () => ({}) 은, function() { return { } } 와 동일한 의미입니다.
scope 이슈와 관계 없이 편의상 사용되었습니다.
*/
import * as types from './ActionTypes';
export const increment = () => ({
type: types.INCREMENT
});
export const decrement = () => ({
type: types.DECREMENT
});
// 다른 액션 생성자들과 달리, 파라미터를 갖고있습니다
export const setColor = (color) => ({
type: types.SET_COLOR,
color
});
리덕스의 3가지 원칙중..
"변화는 순수(Pure)해야한다"
RANDOMIZE_COLOR 같은걸 만들면, 실행될때마다 같은값을 반환하기때문에 순수하지 않다.
따라서, SET_COLOR
리듀서?
액션의 type 에 따라서 변화를 일으키는 함수
초기상태가 정의되어야함
리듀서 생성, 초기 상태 정의하기
src/reducers/index.js
import * as types from '../actions/ActionTypes';
// 초기 상태를 정의합니다
const initialState = {
color: 'black',
number: 0
};
리듀서 함수 정의하기
state 와 action 을 파라미터로 가지는 함수
switch 문을 통하여 action.type 에 따라 상태에 변화를 일으킴
* 상태를 직접 수정하지말고, 기존 state 값에 새 값을 덮어씌운 새 객체를 만들어야함
src/components/Counter.js
import * as types from '../actions/ActionTypes';
// 초기 상태를 정의합니다
const initialState = {
color: 'black',
number: 0
};
// 리듀서 함수를 정의합니다.
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
};
case types.SET_COLOR:
return {
...state,
color: action.color
};
default:
return state;
}
};
export default counter;
파라미터로는 리듀서를 넣는다
src/index.js
import React from 'react';
import ReactDOM from 'react-dom';
import App from './containers/App';
import './index.css';
// Redux 관련 불러오기
import { createStore } from 'redux'
import reducers from './reducers';
// 스토어 생성
const store = createStore(reducers);
ReactDOM.render(
<App />,
document.getElementById('root')
);
src/index.js
import React from 'react';
import ReactDOM from 'react-dom';
import App from './containers/App';
import './index.css';
// Redux 관련 불러오기
import { createStore } from 'redux'
import reducers from './reducers';
import { Provider } from 'react-redux';
// 스토어 생성
const store = createStore(reducers);
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
);
connect(
mapStateToProps,
mapDispatchToProps
)
실행하면 함수가 반환되는데,
이 함수에 컴포넌트를 파라미터로 넣어서 실행시켜주면
해당 컴포넌트에 지정된 props 를 전달해준다
connect(...)(MyComponent)
CounterContainer 만들기
src/containers/CounterContainer
import Counter from '../components/Counter';
import * as actions from '../actions';
import { connect } from 'react-redux';
// store 안의 state 값을 props 로 연결해줍니다.
const mapStateToProps = (state) => ({
color: state.color,
number: state.number
});
/*
액션 생성자를 사용하여 액션을 생성하고,
해당 액션을 dispatch 하는 함수를 만들은 후, 이를 props 로 연결해줍니다.
*/
const mapDispatchToProps = (dispatch) => ({
onIncrement: () => dispatch(actions.increment()),
onDecrement: () => dispatch(actions.decrement()),
onSetColor: () => {
const color = 'black'; // 임시
dispatch(actions.setColor(color));
}
});
// Counter 컴포넌트의 Container 컴포넌트
// Counter 컴포넌트를 어플리케이션의 데이터 레이어와 묶는 역할을 합니다.
const CounterContainer = connect(
mapStateToProps,
mapDispatchToProps
)(Counter);
export default CounterContainer;
랜덤색상 생성 함수 만들기
(open-color 기반)
src/utils/index.js
export function getRandomColor() {
const colors = [
'#495057',
'#f03e3e',
'#d6336c',
'#ae3ec9',
'#7048e8',
'#4263eb',
'#1c7cd6',
'#1098ad',
'#0ca678',
'#37b24d',
'#74b816',
'#f59f00',
'#f76707'
];
// 0 부터 12까지 랜덤 숫자
const random = Math.floor(Math.random() * 13);
// 랜덤 색상 반환
return colors[random];
}
mapDispatchToProps 수정
src/containers/CounterContainer.js
const mapDispatchToProps = (dispatch) => ({
onIncrement: () => dispatch(actions.increment()),
onDecrement: () => dispatch(actions.decrement()),
onSetColor: () => {
const color = getRandomColor();
dispatch(actions.setColor(color));
}
});
src/containers/App.js
import React, { Component } from 'react';
import CounterContainer from '../containers/CounterContainer';
class App extends Component {
render() {
return (
<div>
<CounterContainer/>
</div>
);
}
}
export default App;
리듀서들을 합치는건 combineReducers
리듀서 분리하기
src/reducers/color.js
import * as types from '../actions/ActionTypes';
const initialState = {
color: 'black'
};
const color = (state = initialState, action) => {
switch(action.type) {
case types.SET_COLOR:
return {
color: action.color
};
default:
return state;
}
}
export default color;
src/reducers/number.js
import * as types from '../actions/ActionTypes';
const initialState = {
number: 0
};
const number = (state = initialState, action) => {
switch(action.type) {
case types.INCREMENT:
return {
number: state.number + 1
};
case types.DECREMENT:
return {
number: state.number - 1
};
default:
return state;
}
}
export default number;
리듀서 합치기
src/reducers/index.js
import number from './number';
import color from './color';
import { combineReducers } from 'redux';
/*
서브 리듀서들을 하나로 합칩니다.
combineReducers 를 실행하고 나면, 나중에 store의 형태가 파라미터로 전달한 객체의 모양대로 만들어집니다.
지금의 경우:
{
numberData: {
number: 0
},
colorData: {
color: 'black'
}
}
로 만들어집니다.
*/
const reducers = combineReducers({
numberData: number,
colorData: color
});
export default reducers;
mapStateToProps 수정하기
src/containers/ContactContainer.js
// store 안의 state 값을 props 로 연결해줍니다.
const mapStateToProps = (state) => ({
color: state.colorData.color,
number: state.numberData.number
});
src/index.js
import React from 'react';
import ReactDOM from 'react-dom';
import App from './containers/App';
import './index.css';
// Redux 관련 불러오기
import { createStore } from 'redux'
import reducers from './reducers';
import { Provider } from 'react-redux';
// 스토어 생성
const store = createStore(reducers, window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__());
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
);
(카운터 추가 및 삭제)
src/actions/ActionTypes.js
/*
Action 의 종류들을 선언합니다.
앞에 export 를 붙임으로서, 나중에 이것들을 불러올 때,
import * as types from './ActionTypes' 를 할 수 있어요.
*/
export const CREATE = 'CREATE';
export const REMOVE = 'REMOVE';
export const INCREMENT = 'INCREMENT';
export const DECREMENT = 'DECREMENT';
export const SET_COLOR = 'SET_COLOR';
create, remove 생성
기존 생성자는 전체적으로
index 파라미터를 추가적으로 받도록 수정
(몇번째 카운터를 바꿀지 정의함)
src/actions/index.js
/*
action 객체를 만드는 액션 생성자들을 선언합니다. (action creators)
여기서 () => ({}) 은, function() { return { } } 와 동일한 의미입니다.
scope 이슈와 관계 없이 편의상 사용되었습니다.
*/
import * as types from './ActionTypes';
export const create = (color) => ({
type: types.CREATE,
color
});
export const remove = () => ({
type: types.REMOVE
});
export const increment = (index) => ({
type: types.INCREMENT,
index
});
export const decrement = (index) => ({
type: types.DECREMENT,
index
});
export const setColor = ({index, color}) => ({
type: types.SET_COLOR,
index,
color
});
초기상태 정의
카운터 배열, 각 객체마다 카운터의 정보를 지니고있다
src/reducers/index.js
import * as types from '../actions/ActionTypes';
// 초기 상태를 정의합니다.
const initialState = {
counters: [
{
color: 'black',
number: 0
}
]
}
... (spread 문법)과 .slice() 함수 사용
.push(), .pop() 등의 내장함수는 배열 자체를 바꿔버리기 때문에 사용 X
src/reducers/index.js
import * as types from '../actions/ActionTypes';
// 초기 상태를 정의합니다.
const initialState = {
counters: [
{
color: 'black',
number: 0
}
]
}
// 리듀서 함수를 정의합니다.
function counter(state = initialState, action) {
// 레퍼런스 생성
const { counters } = state;
switch(action.type) {
// 카운터를 새로 추가합니다
case types.CREATE:
return {
counters: [
...counters,
{
color: action.color,
number: 0
}
]
};
// slice 를 이용하여 맨 마지막 카운터를 제외시킵니다
case types.REMOVE:
return {
counters: counters.slice(0, counters.length - 1)
};
default:
return state;
}
};
export default counter
INCREMENT 부분
case types.INCREMENT:
return {
counters: [
...counters.slice(0, action.index), // 0 ~ action.index 사이의 아이템들을 잘라와서 이 자리에 넣는다
{
...counters[action.index], // 기존 값은 유지하면서
number: counters[action.index].number + 1 // number 값을 덮어쓴다
},
...counters.slice(action.index + 1, counters.length) // action.index + 1 ~ 마지막까지 잘라온
]
};
src/reducers/index.js
import * as types from '../actions/ActionTypes';
// 초기 상태를 정의합니다.
const initialState = {
counters: [
{
color: 'black',
number: 0
}
]
}
// 리듀서 함수를 정의합니다.
function counter(state = initialState, action) {
// 레퍼런스 생성
const { counters } = state;
switch(action.type) {
// 카운터를 새로 추가합니다
case types.CREATE:
return {
counters: [
...counters,
{
color: action.color,
number: 0
}
]
};
// slice 를 이용하여 맨 마지막 카운터를 제외시킵니다
case types.REMOVE:
return {
counters: counters.slice(0, counters.length - 1)
};
// action.index 번째 카운터의 number 에 1 을 더합니다.
case types.INCREMENT:
return {
counters: [
...counters.slice(0, action.index),
{
...counters[action.index],
number: counters[action.index].number + 1
},
...counters.slice(action.index + 1, counters.length)
]
};
// action.index 번째 카운터의 number 에 1 을 뺍니다
case types.DECREMENT:
return {
counters: [
...counters.slice(0, action.index),
{
...counters[action.index],
number: counters[action.index].number - 1
},
...counters.slice(action.index + 1, counters.length)
]
};
// action.index 번째 카운터의 색상을 변경합니다
case types.SET_COLOR:
return {
counters: [
...counters.slice(0, action.index),
{
...counters[action.index],
color: action.color
},
...counters.slice(action.index + 1, counters.length)
]
};
default:
return state;
}
};
export default counter;
Buttons: 카운터 생성/제거 담당
CounterList: 여러개의 카운터 렌더링
onCreate 와 onRemove 함수를 props 로 전달받는다
src/components/Buttons.js
import React from 'react';
import PropTypes from 'prop-types';
import './Buttons.css';
const Buttons = ({onCreate, onRemove}) => {
return (
<div className="Buttons">
<div className="btn add" onClick={onCreate}>
생성
</div>
<div className="btn remove" onClick={onRemove}>
제거
</div>
</div>
);
};
Buttons.propTypes = {
onCreate: PropTypes.func,
onRemove: PropTypes.func
};
Buttons.defaultProps = {
onCreate: () => console.warn('onCreate not defined'),
onRemove: () => console.warn('onRemove not defined')
};
export default Buttons;
src/components/Buttons.css
.Buttons {
display: flex;
}
.Buttons .btn {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
height: 3rem;
color: white;
font-size: 1.5rem;
cursor: pointer;
}
.Buttons .add {
background: #37b24d;
}
.Buttons .add:hover {
background: #40c057;
}
.Buttons .remove {
background: #f03e3e;
}
.Buttons .remove:hover {
background: #fa5252;
}
여러개의 카운터를 렌더링하는
CounterList 만들기
객체배열 counters,
카운터를 조작하는
onIncrement, onDecrement, onSetColor
함수를 props 로 받음
src/components/CounterList.js
import React from 'react';
import Counter from './Counter';
import PropTypes from 'prop-types';
import './CounterList.css';
const CounterList = ({counters, onIncrement, onDecrement, onSetColor}) => {
const counterList = counters.map(
(counter, i) => (
<Counter
key={i}
index={i}
{...counter}
onIncrement={onIncrement}
onDecrement={onDecrement}
onSetColor={onSetColor}
/>
)
);
return (
<div className="CounterList">
{counterList}
</div>
);
};
CounterList.propTypes = {
counters: PropTypes.arrayOf(PropTypes.shape({
color: PropTypes.string,
number: PropTypes.number
})),
onIncrement: PropTypes.func,
onDecrement: PropTypes.func,
onSetColor: PropTypes.func
};
CounterList.defaultProps = {
counters: [],
onIncrement: () => console.warn('onIncrement not defined'),
onDecrement: () => console.warn('onDecrement not defined'),
onSetColor: () => console.warn('onSetColor not defined')
}
export default CounterList;
src/components/CounterList.css
.CounterList {
margin-top: 2rem;
display: flex;
justify-content: center;
flex-wrap: wrap;
}
CounterList 에서 전달 한 index 를
각 이벤트가 실행 될 때 함수의 파라미터로 넣는다
src/components/Counter.js
import React from 'react';
import PropTypes from 'prop-types';
import './Counter.css';
const Counter = ({number, color, index, onIncrement, onDecrement, onSetColor}) => {
return (
<div
className="Counter"
onClick={() => onIncrement(index)}
onContextMenu={
(e) => {
e.preventDefault();
onDecrement(index);
}
}
onDoubleClick={() => onSetColor(index)}
style={{backgroundColor: color}}>
{number}
</div>
);
};
Counter.propTypes = {
index: PropTypes.number,
number: PropTypes.number,
color: PropTypes.string,
onIncrement: PropTypes.func,
onDecrement: PropTypes.func,
onSetColor: PropTypes.func
};
Counter.defaultProps = {
index: 0,
number: 0,
color: 'black',
onIncrement: () => console.warn('onIncrement not defined'),
onDecrement: () => console.warn('onDecrement not defined'),
onSetColor: () => console.warn('onSetColor not defined')
};
export default Counter;
CounterContainer 는 이제 필요없으니, 삭제
이번에 만들 컨테이너 컴포넌트:
Buttons 의 경우엔 따로 컨테이너를 만들지 않고, App 컴포넌트를 redux 에 연결시켜서
액션함수들을 Buttons 컴포넌트로 전달
CounterListContainer 만들기
src/containers/CounterListContainer.js
import CounterList from '../components/CounterList';
import * as actions from '../actions';
import { connect } from 'react-redux';
import { getRandomColor } from '../utils';
// store 안의 state 값을 props 로 연결해줍니다.
const mapStateToProps = (state) => ({
counters: state.counters
});
/*
액션 생성자를 사용하여 액션을 생성하고,
해당 액션을 dispatch 하는 함수를 만들은 후, 이를 props 로 연결해줍니다.
*/
const mapDispatchToProps = (dispatch) => ({
onIncrement: (index) => dispatch(actions.increment(index)),
onDecrement: (index) => dispatch(actions.decrement(index)),
onSetColor: (index) => {
const color = getRandomColor();
dispatch(actions.setColor({ index, color}));
}
})
// 데이터와 함수들이 props 로 붙은 컴포넌트 생성
const CounterListContainer = connect(
mapStateToProps,
mapDispatchToProps
)(CounterList);
export default CounterListContainer;
App 컴포넌트 수정하기
연결시킬 상태는 없음: mapStateToProps 를 null 로 설정
onCreate 와 onRemove 에 액션함수를 연결시켜준다
src/containers/App.js
import React, {Component} from 'react';
import Buttons from '../components/Buttons';
import CounterListContainer from './CounterListContainer';
import { connect } from 'react-redux';
import * as actions from '../actions';
import { getRandomColor } from '../utils';
class App extends Component {
render() {
const { onCreate, onRemove } = this.props;
return (
<div className="App">
<Buttons
onCreate={onCreate}
onRemove={onRemove}
/>
<CounterListContainer/>
</div>
);
}
}
// 액션함수 준비
const mapToDispatch = (dispatch) => ({
onCreate: () => dispatch(actions.create(getRandomColor())),
onRemove: () => dispatch(actions.remove())
});
// 리덕스에 연결을 시키고 내보낸다
export default connect(null, mapToDispatch)(App);
let a = 7;
let b = 7;
let object1 = { a: 1, b: 2 };
let object2 = { a: 1, b: 2 };
object1 === object2
// false
let object3 = object1
object1 === object3
// true
object3.c = 3;
object1 === object3
// true
object1
// Object { a: 1, b: 2, c: 3 }
let array1 = [0,1,2,3,4];
let array2 = array1;
array2.push(5);
array1 === array2
// true
리액트에서는 state 혹은 props 가
변할 때 리렌더링을 함
객체/배열을 직접적으로 수정하면,
레퍼런스가 가르키는곳은 같기때문에
똑같은 값으로 인식
새 객체 / 배열을 생성
let object1 = {
a: 1,
b: 2,
c: 3,
d: {
e: 4,
f: {
g: 5,
h: 6
}
}
};
// h값을 10으로 업데이트함
let object2 = {
...object,
d: {
...object.d,
f: {
...object.d.f,
h: 10
}
}
}
코드가 복잡해진다
불변성 유지가 필요 없다면
object1.d.f.h = 10; 으로 되는건데..
불변성 유지를 도와주는 도구
let object1 = Map({
a: 1,
b: 2,
c: 3,
d: Map({
e: 4,
f: Map({
g: 5,
h: 6
})
})
});
let object2 = object1.setIn(['d', 'f', 'h'], 10);
object1 === object2;
// false
심지어 더 빠름
여러층의 Map
var Map = Immutable.Map;
var data = Map({
a: 1,
b: 2,
c: Map({
d: 3,
e: 4,
f: 5
})
})
내부 객체도 Map 으로 감싸야 함
혹은 fromJS 사용
var fromJS = Immutable.fromJS;
var data = fromJS({
a: 1,
b: 2,
c: {
d: 3,
e: 4,
f: 5
}
})
Map 을 그대로 프린트 할 수는 없다
console.log(data.a);
// undefined
자바스크립트 객체로 변환하기
data.toJS(); // { a:1, b:2, c: { d: 3, e: 4 } }
특정 키 불러오기
data.get('a'); // 1
깊숙한 값 불러오기
data.getIn(['c', 'd']) // 3
값 설정하기
var newData = data.set('a', 4);
깊숙한 값 설정하기
var newData = data.setIn(['c', 'd'], 10);
값 여러개 설정하기
var newData = data.mergeIn(['c'], { d: 10, e: 10 });
var newData = data.setIn(['c', 'd'], 10);
.setIn(['c', 'e'], 10);
var newData = data.merge({ a: 10, b: 10 })
값을 여러개 설정 할 땐,
merge 보다, set 을 여러번 하는게 더 빠름
내부의 객체를 업데이트 할 땐..
var newData = data.set('c', Map({ d: 10, e: 10, f: 10 }))
배열 대신 사용되는 데이터 구조
배열과 동일하게 map, filter, sort, push, pop 함수등이 있다.
(차이점: 언제나 새로운 List 를 만들어서 반환함!)
리액트는 Immutable List 와 호환이 되기때문에,
map 해서 컴포넌트 렌더링도 가능!
생성하기
var List = Immutable.List;
var list = List([0,1,2,3,4]);
객체의 배열이라면 내부도 Immutable 구조를 사용한다
객체의 배열이라면 내부도 Immutable 구조를 사용한다
사용하는것이 편하다
필수는 아니지만, 사용하면
내부 내용도 get 과 set 사용가능
객체 배열
var List = Immutable.List;
var Map = Immutable.Map;
var fromJS = Immutable.fromJS;
var list = List([
Map({ value: 1 }),
Map({ value: 2 })
]);
// or
var list2 = fromJS([
{ value: 1 },
{ value: 2 }
])
일반 배열로 변환
console.log(list.toJS());
(내부에 Map 이 있으면 이 또한 변환)
값 읽어오기 .get(n)
list.get(0);
List 안의 Map 내부의 값 가져오기
list.getIn([0, 'value']);
아이템 수정하기
var newList = list.setIn([0, 'value'], 10);
내부의 값에 기반하여 수정 할 땐
var newList = list.update(
1,
item => item.set('value', item.get('value') * 5)
)
var newList = list.setIn([1, 'value'], list.getIn([1, 'value']) * 5);
상황에 따라 편리한 방식을 선택
아이템 추가하기
var newList = list.push(Map({value: 3}))
맨 앞에 넣고 싶다면,
var newList = list.unshift(Map({value: 0}))
아이템 제거
var newList = list.delete(1);
가장 마지막 아이템 제거
var newList = list.pop();
크기 가져오기
console.log(list.size);
비어있는지 확인
list.isEmpty();
더 많이 알고 싶다면..
Immutable 설치
$ yarn add immutable
src/reducers/index.js
import { Map, List } from 'immutable';
const initialState = Map({
counters: List([
Map({
color: 'black',
number: 0
})
])
})
초기값 설정
src/reducers/index.js
// 리듀서 함수를 정의합니다.
function counter(state = initialState, action) {
const counters = state.get('counters');
switch(action.type) {
// 카운터를 새로 추가합니다
case types.CREATE:
return state.set('counters', counters.push(Map({
color: action.color,
number: 0
})))
// slice 를 이용하여 맨 마지막 카운터를 제외시킵니다
case types.REMOVE:
return state.set('counters', counters.pop());
// action.index 번째 카운터의 number 에 1 을 더합니다.
case types.INCREMENT:
return state.set('counters', counters.update(
action.index,
(counter) => counter.set('number', counter.get('number') + 1))
);
// action.index 번째 카운터의 number 에 1 을 뺍니다
case types.DECREMENT:
return state.set('counters', counters.update(
action.index,
(counter) => counter.set('number', counter.get('number') - 1))
);
// action.index 번째 카운터의 색상을 변경합니다
case types.SET_COLOR:
return state.set('counters', counters.update(
action.index,
(counter) => counter.set('color', action.color))
);
default:
return state;
}
};
리듀서 함수 재작성
컴포넌트 수정
src/containers/CounterListContainer.js
// store 안의 state 값을 props 로 연결해줍니다.
const mapStateToProps = (state) => ({
counters: state.get('counters')
});
mapStateToProps 에서, state.counters 대신 state.get('counters')
src/components/CounterList.js
import React from 'react';
import Counter from './Counter';
import PropTypes from 'prop-types';
import { List } from 'immutable';
import './CounterList.css';
const CounterList = ({counters, onIncrement, onDecrement, onSetColor}) => {
const counterList = counters.map(
(counter, i) => (
<Counter
key={i}
index={i}
{...counter.toJS()}
onIncrement={onIncrement}
onDecrement={onDecrement}
onSetColor={onSetColor}
/>
)
);
return (
<div className="CounterList">
{counterList}
</div>
);
};
CounterList.propTypes = {
counters: PropTypes.instanceOf(List),
onIncrement: PropTypes.func,
onDecrement: PropTypes.func,
onSetColor: PropTypes.func
};
CounterList.defaultProps = {
counters: [],
onIncrement: () => console.warn('onIncrement not defined'),
onDecrement: () => console.warn('onDecrement not defined'),
onSetColor: () => console.warn('onSetColor not defined')
}
export default CounterList;
CounterList 를 매핑하는 과정에서 ...counters 대신 ...counters.toJS()
PropTypes 는 PropTypes.instanceOf(List)
잘 되는지 테스팅..
액션 하나 추가할때마다,
액션타입에.. 액션생성자에, 리듀서...
한 파일에 넣어버리자!
리듀서, 액션타입, 액션생성자를 한 파일에 넣고,
이를 '모듈' 이라고 부른다.
예시 모듈
// widgets.js
// Actions
const LOAD = 'my-app/widgets/LOAD';
const CREATE = 'my-app/widgets/CREATE';
const UPDATE = 'my-app/widgets/UPDATE';
const REMOVE = 'my-app/widgets/REMOVE';
// Reducer
export default function reducer(state = {}, action = {}) {
switch (action.type) {
// do reducer stuff
default: return state;
}
}
// Action Creators
export function loadWidgets() {
return { type: LOAD };
}
export function createWidget(widget) {
return { type: CREATE, widget };
}
export function updateWidget(widget) {
return { type: UPDATE, widget };
}
export function removeWidget(widget) {
return { type: REMOVE, widget };
}
규칙
npm-module-or-app/reducer/ACTION_TYPE 의 형태
(모듈을 만드는게 아니라면 맨 앞은 생략하여 reducer/ACTION_TYPE 도 ok
리듀서를 만들땐 export default 로 내보내기
액션 생성자는 export 로 내보내기
createAction 과 handleActions
매우 유용함!
createAction 을 통한
액션생성 자동화
export const increment = (index) => ({
type: types.INCREMENT,
index
});
export const decrement = (index) => ({
type: types.DECREMENT,
index
});
그저 파라미터를 넣어줄뿐인데, 굳이 함수를 하나하나 만들어야하나..?
자동화하면 어떨까?
createAction !
export const increment = createAction(types.INCREMENT);
export const decrement = createAction(types.DECREMENT);
index 는?
액션생성자는 최대 1개의 파라미터를 받는것으로 가정
그 파라미터를 payload 라고 부른다.
increment(3)
{
type: 'INCREMENT',
payload: 5
}
여러개를 전달해야된다면, 객체를 전달
export const setColor = createAction(types.SET_COLOR);
setColor({index: 5, color: '#fff'})
/* 결과:
{
type: 'SET_COLOR',
payload: {
index: 5,
color: '#fff'
}
}
*/
switch 문으로 만든 리듀서의 단점
scope 를 공유함
서로 다른 case 에서 let 이나 const 로
같은 이름의 변수 생성 불가능..
handleActions !
const reducer = handleActions({
INCREMENT: (state, action) => ({
counter: state.counter + action.payload
}),
DECREMENT: (state, action) => ({
counter: state.counter - action.payload
})
}, { counter: 0 });
첫번째 파라미터: 액션이름: 함수 로 이뤄진 객체
두번째 파라미터: 기본 상태
redux-actions 설치
$ yarn add redux-actions
모듈 작성
src/modules/index.js
import { createAction, handleActions } from 'redux-actions';
import { Map, List } from 'immutable';
// 액션 타입
const CREATE = 'counter/CREATE';
const REMOVE = 'counter/REMOVE';
const INCREMENT = 'counter/INCREMENT';
const DECREMENT = 'counter/DECREMENT';
const SET_COLOR = 'counter/SET_COLOR';
// 액션 생성자
export const create = createAction(CREATE); // color
export const remove = createAction(REMOVE);
export const increment = createAction(INCREMENT); // index
export const decrement = createAction(DECREMENT); // index
export const setColor = createAction(SET_COLOR); // { index, color }
의존 모듈 불러오기 / 액션타입 선언
createAction 으로 액션생성자 만들기
src/modules/index.js
// 초기 상태를 정의합니다
const initialState = Map({
counters: List([
Map({
color: 'black',
number: 0
})
])
});
export default handleActions({
[CREATE]: (state, action) => state,
[REMOVE]: (state, action) => state,
[INCREMENT]: (state, action) => state,
[DECREMENT]: (state, action) => state,
[SET_COLOR]: (state, action) => state,
}, initialState);
초기상태 정의
리듀서 틀 만들기
src/modules/index.js
export default handleActions({
[CREATE]: (state, action) => {
const counters = state.get('counters');
return state.set('counters', counters.push(
Map({
color: action.payload,
number: 0
})
))
},
[REMOVE]: (state, action) => {
const counters = state.get('counters');
return state.set('counters', counters.pop())
},
[INCREMENT]: (state, action) => {
const counters = state.get('counters');
return state.set('counters', counters.update(
action.payload,
(counter) => counter.set('number', counter.get('number') + 1))
);
},
[DECREMENT]: (state, action) => {
const counters = state.get('counters');
return state.set('counters', counters.update(
action.payload,
(counter) => counter.set('number', counter.get('number') - 1))
);
},
[SET_COLOR]: (state, action) => {
const counters = state.get('counters');
return state.set('counters', counters.update(
action.payload.index,
(counter) => counter.set('color', action.payload.color))
);
},
}, initialState);
handleActions 완성하기
actions/
reducers/
디렉토리 제거
src/index.js
import reducers from './modules';
reducers 대신 modules
src/containers/App.js
&
src/containers/CounterListContainer.js
import * as actions from '../modules';
actions 대신 modules