Redux를 이용하여 Todo 로직 만들기

라이브러리

todos 모듈 생성

액션 타입, 액션 생성 함수, 리듀서를 묶은 todos 모듈을 정의한다.

// modules/todos.js
import {createAction, handleActions} from 'redux-actions'

// 액션 타입을 정의한다.
const CHANGE_INPUT = "todos/CHANGE_INPUT"
const INSERT = "todos/INSERT"
const TOGGLE = "todos/TOGGLE"
const REMOVE = "todos/REMOVE"

let id = ;

// 액션 생성함수. 컨테이너 컴포넌트에서 connect를 위해 export 한다.
// createAction(actonType, payloadCreator)
export const changeInput = createAction(CHANGE_INPUT, input => input)
export const toggle = createAction(TOGGLE, id=>id)
export const remove = createAction(REMOVE, id=>id)
export const insert = createAction(INSERT, text=> ({
    id: ++id,
    text,
    done: false
})

/*
    createAction으로 생성한 액션 생성함수는 다음과 같이 액션을 반환한다.
    insert("Something")
    {
        type: "todos/INSERT",
        payload: {
            id: 2,
            text: "Something",
            done: false
        }
    }

    createAction없이 액션생성함수를 작성하면 다음과 같다.
    export const insert = (text) => ({
        type: "todo/INSERT",
        payload: {
            id: ++id,
            text,
            done: false
        }
    })
*/

// initialState 정의
const initialState = {
    input: '',
    todos: []
}

// 리듀서 작성
// redux-actions의 handleActions를 이용한다.
// handleActions(reducerMap, initialState)
const todos = handleActions({
    [CHANGE_INPUT] : (state, action) => ({
        ...state,
        input: action.payload
    }),
    [INSERT] : (state, action) => ({
        ...state,
        todos: state.todos.concat(action.payload)
    }),
    [TOGGLE] : (state, action) => ({
        ...state,
        todos: state.todos.map(todo => todo.id === action.payload ? {...todo, done: !todo.done} : todo)
    }),
    [REMOVE] : (state, action) => ({
        ...state,
        todos: state.todos.filter(todo => todo.id !== action.payload)
    })
}, initialState)

/*
    handleActions안에 action.payload가 동일하게 나타나서 payload의 내용을 예측하기 힘들다.
    객체 구조 분해 할당(destructuring assignment)을 이용하여 다음과 같이 작성할 수 있다.

    [INSERT]: (state, {payload: todo}) => ({
        ...state,
        todos: state.todos.concat(todo)
    })

    다음과 같이 작성하면 payload의 내용을 예측하기 쉬워진다.

    handleActions 없이 리듀서를 작성하면 다음과 같다.

    const todos = (state = initialState, action) => {
        switch(action.type) {
            case CHANGE_INPUT:
                return {
                    ...state,
                    input: action.payload
                }
            case INSERT:
                return {
                    ...state,
                    todos: state.todos.concat(action.payload)
                }
            //...
        }
    }
*/


export default todos

루트리듀서 작성

전체 앱에 리듀서가 하나라면 방금 작성한 리듀서를 store에 주입하면 된다.

하지만 리듀서가 늘어나면 루트리듀서로 합쳐서 store에 주입해야 한다.

// modules/index.js

import {combineReducers} from 'redux'
import {todos} from './todo'

const rootReducer = combineReducers({
    todos
})

export default rootReducer
/*
    {
        todos: {...}
    }
*/

store 생성

작성한 리듀서를 바탕으로 store를 생성한다.

react-reduxProvider를 통해서 컴포넌트에서 store에 접근 가능하도록 한다.

// index.js
// 다른 import 생략
import {createStore} from 'redux'
import {Provider} from 'react-redux'
import rootReducer from './module'

const store = createStore(rootReducer)

// Provider는 컴포넌트에서 store에 접근할 수 있게한다.
ReactDOM.render( 
    <Provider store={store}>
        <App />
    </Provider>,
document.getElementById('root')
)

Container 컴포넌트 작성

Container 컴포넌트는 리덕스의 store와 연결하여 데이터를 가져와 view에 전달하는 역할을 한다.

store와 연결하기 위해 react-redux의 connect함수를 이용한다.

connect()는 store에서 데이터를 가져와 컴포넌트의 props에 전달한다.

// container/TodosContainer.js
import {insert, toggle, remove, changeInput} from '../modules/todo'

const TodosContainer = ({todos, insert, remove, toggle, changeInput, input}) => {

    return (
        <Todos 
            todos={todos} 
            onInsert={insert} 
            onRemove={remove} 
            onToggle={toggle} 
            onChangeInput={changeInput} 
            input={input} 
        />
    )
}
// state 데이터를 props에 전달한다.
const mapStateToProps = (state) => ({
    input: state.todos.input,
    todos: state.todos.todos
})
/*
    객체 구조분해 할당을 이용하여 다음과 같이 작성할 수 있다.
    const mapStateToProps = ({todos}) => ({
        input: todos.input,
        todos: todos.todos
    })
*/

// 액션생성함수를 props에 전달한다.
const mapDispatchToProps = {
    insert,
    toggle,
    remove,
    changeInput
}

/*
    connect()는 인자에 dispatch를 전달하기 때문에 다음과 같이 작성할 수 있다.
    
    const mapDispatchToProps = (dispatch) => ({
        insert: (text) => {dispatch(insert(text))},
        toggle: (id) => {dispatch(toggle(id))},
        remove: (id) => {dispatch(remove(id))},
        changeInput: (input) => {dispatch(changeInput(input))},
    })

    액션생성함수를 객체 담아 전달하게 되면 위의 과정을 생략할 수 있다.

    'mapStateToProps', 'mapDispatchToProps'라는 이름은 정해져있는 것이 아니기 때문에 다음과 같이 작성할 수 있다.

    connect(
        (state) => ({
            input: state.todos.input,
            todos: state.todos.todos
        }),
        {
            insert,
            toggle,
            remove,
            changeInput
        }
    )(TodosContainer)
*/

export default connect(mapStateToProps,mapDispatchToProps)(TodosContainer)

UI 작성

컨테이너 컴포넌트를 작성하고 나면 redux를 이용한 로직작성은 완료하였다.

가져온 데이터를 rendering하는 컴포넌트만 있으면 완성이다.