본문 바로가기

FE/React & TS

Redux를 활용한 투두리스트 구현

리액트 애플리케이션: 컴포넌트 간의 데이터 props로 전달

컴포넌트 여기저기에서 필요한 데이터 있을 시 주로 최상위 컴포넌트인 App의 state에 넣어서 관리 

리덕스(Redux): 전역 상태 관리 작업을 처리하는 상태 관리 라이브러리 

 

프레젠테이셔널 컴포넌트 / 컨테이너 컴포넌트 분리 형식이 가장 많이 사용되는 패턴

프레젠테이셔널 컴포넌트: 상태 관리가 이루어지지 않고 그저 props를 받아와서 화면에 UI를 보여주기만 함

컨테이너 컴포넌트: 리덕스와 연동되어 있음, 리덕스로부터 상태를 받아 오기도 하고 리덕스 스토어에 액션을 디스패치 하기도 함. 

 

리덕스 사용할 때: 액션 타입, 액션 생성 함수, 리듀서 코드 작성 필요

가장 일반적인 구조: actions / constants / reducers 디렉터리 => 그 안에 기능별로 파일 하나씩 만듦 

but! 위 세가지를 기능별로 파일 하나에 몰아서 작성하는 방식: Ducks 패턴 

 

1. 액션 타입 정의

'모듈 이름/액션 이름'

문자열 안에 모듈 이름을 넣음 -> 추후 프로젝트가 커졌을 때 액션 이름 충돌하지 않게 해줌 

const INCREASE = 'counter/INCREASE';
const DECREASE = 'counter/DECREASE'

 

2. 액션 생성 함수 

export const increase = () => ({type: INCREASE});
export const decrease = () => ({type:DECREASE});

 

3. 초기 상태 및 리듀서 함수 생성

const initialState = {number: 0};
function counter(state = initialState, action){
    switch (action.type){
        case INCREASE:
            return{
                number: state.number + 1
            };
        case DECREASE:
            return{
                number: state.number -1
            };
        default: return state;
    }
}

counter.js 

const INCREASE = 'counter/INCREASE';
const DECREASE = 'counter/DECREASE'

export const increase = () => ({type: INCREASE});
export const decrease = () => ({type:DECREASE});

const initialState = {number: 0};
function counter(state = initialState, action){
    switch (action.type){
        case INCREASE:
            return{
                number: state.number + 1
            };
        case DECREASE:
            return{
                number: state.number -1
            };
        default: return state;
    }
}
export default counter;

 

todos.js - 액션 타입 정의, 액션 생성 함수 

*id 값은 todo 개체가 들고있게 될 고유값이므로 잊지말자. 

const CHANGE_INPUT = 'todos/CHANGE_INPUT'; //인풋값 변경
const INSERT = 'todos/INSERT'; //새로운 todo 등록
const TOGGLE = 'todos/TOGGLE'; //todo 체크 및 체크 해제
const REMOVE = 'todos/REMOVE'; //todo 제거

export const changeInput = input => ({
    type: CHANGE_INPUT,
    input
});
let id = 3; //insert 호출될 때마다 1씩 증가

export const insert = text => ({
    type: INSERT,
    todo:{
        id: id++, //todo 항목마다 id 붙여줘야 toggle이나 remove와 같은 작업 처리 가능
        //즉 id 값은 각 todo 개체가 들고 있게 될 고유값. 
        text,
        done: false
    }
});

export const toggle = id => ({
    type: TOGGLE,
    id
})

export const remove = id => ({
    type: REMOVE,
    id
})

 

리듀서 함수 

function todos(state = initialize, action){
    switch (action.type){
        case CHANGE_INPUT:
            return{
                ...state,
                input: action.input
            };
        case INSERT:
            return{
                ...state,
                todos: state.todos.concat(action.todo)
            };
        case TOGGLE:
            return{
                ...state,
                todos: state.todos.map(todo =>
                    todo.id === action.id ? {...todo, done: !todo.done} : todo)
            };
        case REMOVE:
            return{
                ...state,
                todos: state.todos.filter(todo =>
                    todo.id !== action.id)
            };
    }
}

 

루트리듀서 - index.js

//루트 리듀서
import {combineReducers} from "redux";
import counter from "./counter";
import todos from './todos';

const rootReducer = combineReducers({
    counter, todos
});
export default rootReducer;

 

리덕스는 리액트 애플리케이션의 src/index.js 에서 적용 

// import React from 'react';
import ReactDOM from 'react-dom/client';
import { Provider } from 'react-redux';
import { createStore } from 'redux';
import App from './App';
import rootReducer from '../modules';

const store = createStore(rootReducer);
ReactDOM.render(
    <Provider store={store}>
        <App />
    </Provider>,
    document.getElementById('root'),
);

 

redux-devtools-extension을 사용한 redux store 생성

 

src/index.js 

// import React from 'react';
import ReactDOM from 'react-dom/client';
import { Provider } from 'react-redux';
import { createStore } from 'redux';
import { composeWithDevTools} from "redux-devtools-extension";
import App from './App';
import rootReducer from '../modules';

// const store = createStore(rootReducer);
// const store = createStore(
//     rootReducer, /*preloadedState*/
//     window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__()
// )

const store = createStore(rootReducer, composeWithDevTools());
ReactDOM.render(
    <Provider store={store}>
        <App />
    </Provider>,
    document.getElementById('root'),
);

 

* The slice reducer for key "todos" returned undefined during initialization.

해당 에러 발생

 

해결방안

 

todos.js

default:
    return {
        ...state
    }

default 상태에서도 state 관련 값 넘겨줘야 함. 

 

 

Container Component: 컴포넌트에서 리덕스 스토어에 접근하여 원하는 상태 받아오고, 액션도 디스패치.

 

containers/CounterContainer.js

import Counter from '../components/Counter';

const CounterContainer = () => {
    return <Counter/>;
};

export default CounterContainer;

 

위 컴포넌트를 리덕스와 연동하는 방법: connect 함수 사용 

how? connect(mapStateToProps, mapDispatchToProps)(연동할 컴포넌트) 

mapStateToProps: 리덕스 스토어 안의 상태를 컴포넌트의 props로 넘겨줌

mapDispatchToProps: 액션 생성 함수를 컴포넌트의 props로 넘겨줌 

 

same as,

const makeContainer = connect(mapStateToProps, mapDispatchToProps)

makeContainer(target component)

 

containers/CounterContainer.js

import {connect} from 'react-redux';
import Counter from '../components/Counter';

const CounterContainer = ({number, increase, decrease}) => {
    return <Counter number={number} onIncrease={increase} onDecrease={decrease}/>;
};

const mapStateToProps = state => ({
    number: state.counter.number, //상태의 카운터의 number
});

const mapDispatchToProps = dispatch => ({
    increase: () => {
        console.log('increase');
    },
    decrease: () => {
        console.log('decrease');
    },
});

export default connect(
    mapStateToProps,
    mapDispatchToProps,
)(CounterContainer);

mapStateToProps와 mapDispatchToProps에서 반환하는 객체 내부의 값 -> 컴포넌트의 props로 전달

mapStateToProps: state를 파라미터로 받아옴 -> 현재 스토어가 지니고 있는 상태 가리킴

mapDispatchToProps: store의 내장 함수 dispatch를 파라미터로 받아옴 -> console.log로 진행상태 확인

 

성공적

 

then, console.log 대신 액션 생성함수 불러오기 

액션 객체 생성 후 dispatch 

import {increase, decrease} from '../modules/counter'; //counter함수에서 increase, decrease 함수 불러옴.
const mapDispatchToProps = dispatch => ({
    increase: () => {
        // console.log('increase');
        dispatch(increase());
    },
    decrease: () => {
        // console.log('decrease');
        dispatch(decrease());
    },
});

숫자가 바뀌는 것 확인O 

 

redux dev 도구

 

+만약 액션 생성 함수의 개수가 많아진다면?

컴포넌트에서 액션을 디스패치하기 위해 액션 생성 함수를 호출하고 dispatch로 감싸는 작업이 더욱 번거로울 것. 

 

how to solve?: bindActionCreators

import {bindActionCreators} from "redux";
export default connect(
    state => ({
        number: state.counter.number,
    }),
    bindActionCreators(
        {increase, decrease}, dispatch,
    ),
)(CounterContainer);

 

also,

export default connect(
    state => ({
        number: state.counter.number,
    }),
    {increase, decrease},
)(CounterContainer);

connect 함수가 내부적으로 bindActionCreators 작업 대신해줌. 

 

 

CounterContainer.js

import {bindActionCreators} from "redux";
import {connect} from 'react-redux';
import Counter from '../components/Counter';
import {increase, decrease} from '../modules/counter'; //counter함수에서 increase, decrease 함수 불러옴.

const CounterContainer = ({number, increase, decrease}) => {
    return <Counter number={number} onIncrease={increase} onDecrease={decrease}/>;
};

export default connect(
    state => ({
        number: state.counter.number,
    }),
    {increase, decrease},
)(CounterContainer);

 

TodosContainer for Todos component

Todos 컴포넌트에서 받아온 props를 사용

 

TodosContainer.js

import {bindActionCreators} from "redux";
import {connect} from 'react-redux';
import todos from '../components/Todo';
import {increase, decrease} from '../modules/counter'; //counter 함수에서 increase, decrease 함수 불러옴.

const TodosContainer = ({input, todo, changeInput, insert, toggle, remove}) => {
    return (
        <Todos
            input={input}
            todos={todos}
            onChangeInput={changeInput}
            onInsert={insert}
            onToggle={toggle}
            onRemove={remove} />
    );
};

export default connect(
    //비구조화 할당을 통해 todos 분리
    //state.todos.input 대신 todos.input 사용
    ({ todos }) => ({
        input: todos.input,
        todos: todos.todos,
    }),
    {
        changeInput,
        insert,
        toggle,
        remove,
    },
)(TodosContainer);

 

redux에 잘 저장된 모습