React & Redux

오늘의 실습

1. 카운터 만들기

2. 멀티 카운터 만들기

3. Immutable.js 익히기

4. Ducks 구조와

react-actions 익히기

5장. 주소록에 Redux 끼얹기

1. 카운터 만들기

1-1

작업환경 설정

create-react-app 으로 프로젝트 생성,

redux 와 react-redux 설치

$ create-react-app redux-counter
$ cd redux-counter
$ yarn add redux react-redux

파일 제거

  • App.css
  • App.js
  • App.test.js
  • logo.svg

디렉토리 생성

src/

    actions/

    ​components/

    containers/

    reducers/

    utils/

Presentational & Container

컴포넌트

1-2

프리젠테이셔널 컴포넌트

  • DOM 과 style 을 가지고 있다.
  • 내부에 프리젠테이셔널 / 컨테이너 컴포넌트들을 가지고 있을 수 있다.
  • store 에 직접적인 연결이 없다
  • props 로만 데이터를 가져온다
  • state 를 가지고있지 않다 (있는경우 UI 관련)
  • 주로 함수형, state가 필요하거나 LifeCycle 이 필요해질때 클래스형

별명: 멍청한 컴포넌트

컨테이너 컴포넌트

  • DOM 엘리먼트가 직접적으로 사용되지 않음, 있다면 감싸는 용도로만
  • 스타일을 갖고있지 않음
  • 프리젠테이셔널 컴포넌트와 컨테이너 컴포넌트들을 관리
  • 리덕스와 연동이 되어있음

별명: 똑똑한 컴포넌트

장점

  • UI 와 데이터 관리의 분리
  • 이해하기 쉬운 프로젝트 구조
  • 높아지는 컴포넌트 재사용률

컨테이너 컴포넌트 대상?

  • 페이지
  • 리스트
  • 헤더
  • 사이드바
  • 내부의 컴포넌트 때문에 props 가
    여러 컴포넌트를 거쳐야 하는 경우

오해

컨테이너 컴포넌트라고해서 무조건 내부에 여러개의 컴포넌트가 있을 필요가 없다.

어떤걸 컨테이너로 할 지, 그리고 구조를 따를지는 자유; 무조건 따라야 할 규칙이 아니다.

그저 유용한 팁!

기본적인 틀 만들기

1-3

비어있는 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

Counter 컴포넌트 만들기

1-4

전달받는 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;

Actions 만들기

1-5

ActionTypes 준비하기

모든 액션 객체는 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

리듀서 만들기

1-6

리듀서?

액션의 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;

store 만들기

1-7

store 를 만들땐,
createStore 를 사용

파라미터로는 리듀서를 넣는다

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')
);

Provider 컴포넌트를 사용하여
리액트 앱에 store 연동하기

1-8

Provider

리액트 앱에 store 를 연동시켜줌.

컴포넌트처럼, 렌더링
store 를 props 로 전달

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')
);

CounterContainer
컴포넌트 만들기

1-9

컨테이너 컴포넌트를 연결 할 땐, connect

상태와, 액션함수들을 연결시켜준다음에, 컴포넌트의 props 로 전달해준다

두개의 함수 준비

mapStateToProps
mapDispatchToProps

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;

서브 리듀서 만들기

1-10

여러개의 리듀서

리듀서들을 합치는건 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
});

2. 멀티카운터 만들기

Redux 개발자 도구 사용하기

2-1

확장 프로그램 설치

개발자도구를 위한 설정하기

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')
);

Actions 고치기

2-2

추가 할 Action

CREATE & REMOVE

(카운터 추가 및 삭제)

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
});

Reducers 고치기

2-3

1장에서 만든 리듀서와 작동방식이 다르므로,
기존의 리듀서 (color.js, number.js) 제거

index.js 새로 작성

초기상태 정의

카운터 배열, 각 객체마다 카운터의 정보를 지니고있다

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;

프리젠테이셔널 컴포넌트 만들기

2-4

Buttons: 카운터 생성/제거 담당

CounterList: 여러개의 카운터 렌더링

Buttons

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;
}

Counter 컴포넌트 수정하기

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;

컨테이너 컴포넌트 만들기

2-5

CounterContainer 는 이제 필요없으니, 삭제

이번에 만들 컨테이너 컴포넌트:

CounterListContainer
 

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);

3. Immutable.js

객체의 불변성

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; 으로 되는건데..

Immutable.JS!

불변성 유지를 도와주는 도구

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

3-1

객체 대신 사용되는 구조

JSBin 에서 실습

상단 Add Library 를 눌러서 Immutable.js 추가

여러층의 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 }))

List

3-2

배열 대신 사용되는 데이터 구조

 

배열과 동일하게 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();

더 많이 알고 싶다면..

리덕스에서 사용하기

3-1

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)

잘 되는지 테스팅..

4. Ducks 와

redux-actions

액션 하나 추가할때마다,

액션타입에.. 액션생성자에, 리듀서...

 

3가지 파일이나 수정해야한다니 귀찮아!

한 파일에 넣어버리자!

Ducks 구조의 소개

4-1

리듀서, 액션타입, 액션생성자를 한 파일에 넣고,

이를 '모듈' 이라고 부른다.

예시 모듈
// 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 로 내보내기

redux-actions 를 통한

더 쉬운 액션관리

4-2

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 });

첫번째 파라미터: 액션이름: 함수 로 이뤄진 객체

두번째 파라미터: 기본 상태

Ducks & react-actions
적용하기

4-3

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);

초기상태 정의

리듀서 틀 만들기

ActionType 이 "CREATE" 가 아닌

"counter/CREATE" 이런식의 접두사가 있는 문자열이기에,

[CREATE] 라고 설정해야함

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

5. 주소록에

Redux 끼얹기

숙제

deck

By Minjun Kim

deck

  • 1,987