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 counterINCREMENT 부분
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.jsimport 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
// falselet object3 = object1object1 === object3
// trueobject3.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 immutablesrc/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