티스토리 뷰

이번에는 리덕스를 사용하여 리액트 App의 상태를 관리하는 방법을 알아보려고 한다.

 

리액트 App에서 리덕스를 사용하면, 상태 업데이트에 관한 로직을 모듈로 따로 분리하여 컴포넌트 파일과 별개로 관리할 수  있으므로 코드를 유지보수 하는데 큰 도움이 된다. 

 

또한, 여러 컴포넌트에서 동일한 상태를 공유해야 할 때 매우 유용하며, 실제 업데이트가 필요한 컴포넌트만 리렌더링되도록 쉽게 최적화할 수도 있다. 

 

리액트 프로젝트에서 리덕스를 사용하기 위해서는 

npm install react-redux
npm install redux

를 사용하여 라이브러리를 설치하고 사용한다.

 

리액트 프로젝트에서 리덕스를 사용할 때 가장 많이 사용하는 패턴은 프레젠테이셔널 컴포넌트와 컨테이너 컴포넌트를 분리하는 것이다. 여기서 프레젠테이셔널 컴포넌트란 주로 상태 관리가 이뤄지지 않고, 그저 props를 받아 와서 화면에 UI를 보여 주기만 하는 컴포넌트를 말한다.

 

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

 

이를 그림으로 나타내면 아래와 같다.

 

이러한 패터는 리덕스를 사용하는 데 필수 사항은 아니지만, 이 패턴을 사용하면 코드의 재사용성이 높아지고, 관심사의 분리가 이뤄져 UI를 작성할 때 좀 더 집중할 수 있다.

 

바로 한번 예제와 함께 알아보자.


우선, 프로젝트 구조는 UI에 관련된 프레젠테이셔널 컴포넌트를 components 디렉토리에,  리덕스와 연동된 컨테이너 컴포넌트는 containers 컴포넌트에 작성한다.

 

 

우선 프레젠테이셔널 컴포넌트를 아래와 같이 구성하자.


카운터 컴포넌트

 

◎ components/Counter.js

const Counter = ({number, onIncrease, onDecrease}) => {
    return (
        <div>
            <h1>{number}</h1>
            <div>
                <button onClick={onIncrease}>+1</button>
                <button onClick={onDecrease}>-1</button>
            </div>
        </div>
    );
};

export default Counter;

 


Todo List 컴포넌트

 

◎ components/Todos.js

const TodoItem = ({todo, onToggle, onRemove}) => {
    return (
        <div>
            <input type="checkbox" />
            <span>예제 텍스트</span>
            <button>삭제</button>
        </div>
    );
};

const Todos = ({
    input,
    todos,
    onChangeInput,
    onInsert,
    onToggle,
    onRemove
}) => {
    const onSubmit = e => {
        e.preventDefault();
    };

    return (
        <div>
            <form onSubmit={onSubmit}>
                <input />
                <button type="submit">등록</button>
            </form>
            <div>
                <TodoItem/>
                <TodoItem/>
                <TodoItem/>
                <TodoItem/>
                <TodoItem/>
            </div>
        </div>
    );
};

export default Todos;

 

◎ App.js

import Counter from "./components/Counter";
import Todos from "./components/Todos";

function App() {
  return (
    <div>
        <Counter number={0}/>
        <hr/>
        <Todos/>
    </div>
  );
};

export default App;


리덕스 관련 코드 작성

이제 프로젝트에서 리덕스를 사용해보자. 

 

리덕스는 액션 타입, 액션 생성 함수, 리듀서 코드를 작성해야 하는데, 이 코드들은 각각 다른 파일에 작성하는 방법도 있고, 기능별로 묶어서 하나에 파일에 작성하는 방법도 있다. 어떤 방법을 선택할 지는 개발자의 취향에 따라 선택하면 된다.

 

여기서는 기능별로 하나의 파일에 작성하겠다. (이를 Ducks 패턴이라고 한다.)

 

Ducks 패턴을 사용하여 액션 타입, 액션 생성 함수, 리듀서를 작성한 코드를 모듈이라고 한다.


counter 모듈 작성

◎ modules/counter.js

/*
*  액션 타입 정의
*  문자열 안에 모듈 이름을 함께 넣음으로써, 나중에 프로젝트가 커졌을 때 액션의 이름이 충돌되지 않게 해준다.
* */
const INCREASE = 'counter/INCREASE';
const DECREASE = 'counter/DECREASE';

/*
*  액션 생성 함수 만들기
*  export 키워드를 사용해야 이 함수를 다른 파일에서 불러와 사용할 수 있다.
* */
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 모듈 만들기

◎ modules/todos.js

// 액션 타입 정의
const CHANGE_INPUT = 'todos/CHANGE_INPUT'; // 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;

export const insert = text => ({
    type: INSERT,
    todo: {
        id: id++,
        text,
        done: false
    }
});

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

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

// 초기 상태
const initialState = {
    input: '',
    todos: [
        {
            id: 1,
            text: '리덕스 기초 배우기',
            done: true
        },
        {
            id: 1,
            text: '리액트와 리덕스 사용하기',
            done: false
        }
    ]
};

// 리듀서 함수
function todos(state = initialState, 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)
            };
        default:
            return state;
    }
}

export default todos;

루트 리듀서 

리듀서를 counter, todos에서 2개를 만들었지만, 나중에 createStore 함수를 사용하여 스토어를 만들 때는 리듀서를 하나만 사용해야 한다.

 

그렇기 때문에 기존에 만들었던 리듀서를 하나로 합쳐줘야 한다. 이 작업은 리덕스에서 제공하는 combineReducers라는 유틸 함수를 사용하면 쉽게 처리할 수 있다.

 

◎modules/index.js

import {combineReducers} from 'redux';
import counter from './counter';
import todos from './todos';

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

export default rootReducer;

또한 모듈 파일의 이름을 index.js로 설정해주면 나중에 디렉토리 이름까지만 입력해서 불러올 수도 있다.


리액트 어플리케이션에 리덕스 적용

이제 리액프 프로젝트에 리덕스를 적용해보자. 스토어를 만들고 리액트에 리덕스를 적용하는 작업은 scr/index.js 에서 이뤄진다.

 

1. 스토어 만들기

 

◎index.js

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

const store = createStore(rootReducer);

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
    <App />
);

 

2. Provider 컴포넌트를 사용하여 프로젝트에 리덕스 적용하기

 

리액트 컴포넌트에서 스토어를 사용할 수 있도록 App 컴포넌트를 react-redux에서 제공하는 Provider 컴포넌트로 감싸준다. 이 컴포넌트를 사용할 때는 store를 props로 전달해야 한다.

 

◎index.js

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

const store = createStore(rootReducer);

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

 

3. Redux DevTools 사용

 

필수는 아니지만, 리덕스 개발자 도구를 설치하고 크롬, 개발환경에 적용하면 개발 / 디버깅이 훨씬 수월해진다.

 

크롬 플러그인은 여기에서 받을 수 있다.

 

이제 터미널에서 라이브러리를 설치해주고, 아래와 같이 적용해주자.

npm install redux-devtools-extension

 

◎index.js

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

const store = createStore(rootReducer, devToolsEnhancer());

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

 

적용하고 나면 아래와 같이 redux 개발자 도구를 사용할 수 있다.


컨테이너 컴포넌트 만들기

이제 컴포넌트에서 리덕스 스토어에 접근하여 원하는 상태를 받아 오고, 또 액션도 디스패치할 컨테이너 컴포넌트를 만들어보자.

 

컨테이너 컴포넌트를 리덕스와 연동하려면 아래와 같이 connect 함수를 사용해야 한다.

 

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

mapStateToProps는 리덕스 스토어 안의 상태를 컴포넌트의 props로 넘겨주기 위해 설정하는 함수이고, mapDispatchToProps는 액션 생성 함수를 컴포넌트의 props로 넘겨주기 위해 사용하는 함수이다.

 

connect함수를 호출하면 또 다른 함수를 반환하는데, 반환된 함수에 컴포넌트를 파라미터로 넣어 주면 리덕스와 연동된 컴포넌트가 만들어진다.

 

1. CounterContainer 

 

◎container/CounterContainer.js

import Counter from '../components/Counter';
import {connect} from "react-redux";
import {decrease, increase} from "../modules/counter";

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

const mapStateToProps = state => ({
    number: state.counter.number
});

const mapDispatchToProps = dispatch => ({
    increase: () => {
        dispatch(increase());
    },
    decrease: () => {
        dispatch(decrease());
    }
});

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

이렇게 각 상태와 액션 함수를 모두 연결한 컨테이너 컴포넌트를 App 컴포넌트에서 호출하기만 하면 된다.

 

◎App.js

import Todos from "./components/Todos";
import CounterContainer from "./containers/CounterContainer";

function App() {
  return (
    <div>
        <CounterContainer/>
        <hr/>
        <Todos/>
    </div>
  );
};

export default App;

이제 카운터가 제대로 동작하는 걸 확인할 수 있다.

 

컴포넌트에서 액션을 디스패치하기 위해 각 액션 생성 함수를 호출하고 dispatch로 감싸는 작업이 조금 번거로울 수도 있다. 특히 액션 생성 함수의 개수가 많아진다면 더더욱 그럴 것이다.

 

이와 같은 경우에는 리덕스에서 제공하는 bindActionCreators 유틸 함수를 사용하면 간편하다.

 

◎container/CounterContainer.js

import Counter from '../components/Counter';
import {connect} from "react-redux";
import {decrease, increase} from "../modules/counter";
import {bindActionCreators} from "redux";

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

const mapStateToProps = state => ({
    number: state.counter.number
});

const mapDispatchToProps = dispatch => {
        bindActionCreators(
            {
                increase,
                decrease
            },
            dispatch,
        )
}
;

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

 

혹은 그냥 mapDispatchToProps에 해당하는 파라미터를 함수 형태가 아닌 액션 생섬 함수로 이뤄진 객체 형태로 바로 넣어도 된다.

 

◎container/CounterContainer.js

import Counter from '../components/Counter';
import {connect} from "react-redux";
import {decrease, increase} from "../modules/counter";
import {bindActionCreators} from "redux";

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


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

 

2. TodosContainer

 

마찬가지로 TodosContainer도 아래와 같이 구현해주자.

 

◎container/TodosContainer.js

import Todos from "../components/todos";
import {changeInput, insert, remove, toggle} from "../modules/todos";
import {connect} from 'react-redux';

const TodosContainer = ({
    input,
    todos,
    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);

 

◎App.js

import CounterContainer from "./containers/CounterContainer";
import TodosContainer from "./containers/TodosContainer";

function App() {
  return (
    <div>
        <CounterContainer/>
        <hr/>
        <TodosContainer/>
    </div>
  );
};

export default App;

 

이제 todos 프레젠테이셔널 컴포넌트도 샘플 데이터가 아닌 컨테이너에서 받은 상태를 사용하도록 수정하자

 

◎components/Todos.js

const TodoItem = ({todo, onToggle, onRemove}) => {
    return (
        <div>
            <input
                type="checkbox"
                onClick={() => onToggle(todo.id)}
                checked={todo.done}
                readOnly={true}
            />
            <span style={{textDecoration: todo.done ? 'line-through' : 'none'}}>
                {todo.text}
            </span>
            <button onClick={() => onRemove(todo.id)}>삭제</button>
        </div>
    );
};

const Todos = ({
    input,
    todos,
    onChangeInput,
    onInsert,
    onToggle,
    onRemove
}) => {
    const onSubmit = e => {
        e.preventDefault();
        onInsert(input);
        onChangeInput('');
    };

    const onChange = e => onChangeInput(e.target.value);

    return (
        <div>
            <form onSubmit={onSubmit}>
                <input value={input} onChange={onChange}/>
                <button type="submit">등록</button>
            </form>
            <div>
                {todos.map(todo => (
                    <TodoItem
                        todo={todo}
                        key={todo.id}
                        onToggle={onToggle}
                        onRemove={onRemove}
                    />
                ))}
            </div>
        </div>
    );
};

export default Todos;

이렇게 해주면, 이제 구현하려고 했던 기능이 모두 동작하는 걸 확인할 수 있다.


리덕스 더 편하게 사용하기

액션 생성 함수, 리듀서를 작성할 때 redux-actions 라는 라이브러리와 이전에 배웠던 immer 라이브러리를 사용하면 훨씬 편하게 사용할 수 있다.

 

redux-actions

redux-actions를 사용하면 액션 생성 함수를 더 짧은 코드로 작성할 수 있다. 그리고 리듀서를 작성할 때도 switch/case 문이 아닌 handleActions 라는 함수를 사용하여 각 액션마다 업데이트 함수를 설정하는 형식으로 작성할 수 있다.

npm install redux-actions

 

라이브러리를 설치하고, counter 모듈에 적용해보자.

 

◎ modules/counter.js

import {createAction, handleActions} from 'redux-actions';

/*
*  액션 타입 정의
*  문자열 안에 모듈 이름을 함께 넣음으로써, 나중에 프로젝트가 커졌을 때 액션의 이름이 충돌되지 않게 해준다.
* */
const INCREASE = 'counter/INCREASE';
const DECREASE = 'counter/DECREASE';

/*
*  액션 생성 함수 만들기
*  createAction을 사용한 액션 생성 함수 생성
* */
export const increase = createAction(INCREASE);
export const decrease = createAction(DECREASE);

// 초기 상태
const initialState = {
    number: 0
};

// 리듀서 함수
const counter = handleActions(
    {
        [INCREASE]: (state, action) => ({number: state.number + 1}),
        [DECREASE]: (state, action) => ({number: state.number - 1})
    },
    initialState
);

export default counter;

거슬렸던 switch/case 문이 빠지니까 코드가 훨씬 간결해지고 가독성도 좋아졌다!

 

동일하게 todos 모듈에도 적용해보자.

 

이때, createAction으로 마찬가지로 액션 생성 함수를 생성해야 하는데, todos 모듈의 액션 생성 함수는 모두 파라미터가 필요하다. createAction으로 액션을 만들면 액션에 필요한 추가 데이터는 payload라는 이름을 사용한다. 

 

코드로 확인해보자.

 

◎ modules/todos.js

import {createAction, handleActions} from 'redux-actions';

// 액션 타입 정의
const CHANGE_INPUT = 'todos/CHANGE_INPUT'; // input 값을 변경함
const INSERT = 'todos/INSERT'; // 새로운 todo를 등록함
const TOGGLE = 'todos/TOGGLE'; // todo를 체크 / 체크 해제함
const REMOVE = 'todos/REMOVE'; // todo를 제거함

/*
*  액션 생성 함수
*  파라미터를 전달받아 액션 객체 안에 추가 필드로 넣어준다.
* */
export const changeInput = createAction(CHANGE_INPUT, input => input);

let id = 3;

export const insert = createAction(INSERT, text => ({
    id: id++,
    text,
    done:false
}));

export const toggle = createAction(TOGGLE, id => id);

export const remove = createAction(REMOVE, id => id);

// 초기 상태
const initialState = {
    input: '',
    todos: [
        {
            id: 1,
            text: '리덕스 기초 배우기',
            done: true
        },
        {
            id: 2,
            text: '리액트와 리덕스 사용하기',
            done: false
        }
    ]
};

/*
*  리듀서 함수
*  createAction으로 만든 액션 생성 함수는 파라미터로 받아 온 값을 객체 안에 넣을 때
*  지정한 이름으로 넣는 것이 아니라, action.id, action.todo와 같이 모두 action.payload 값을 조회하여
*  업데이트하도록 구현해 줘야 한다.
* */
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
)

export default todos;

모든 추가 데이터 값을 action.payload로 사용하기 때문에 나중에 리듀서 코드를 다시 볼 때 헷갈릴 수가 있다. 

 

객체 비구조화 문법으로 아래와 같이 payload 이름을 새로 설정해줄 수 있다.

[CHANGE_INPUT]: (state, {payload: input}) => ({
    ...state,
    input: input
}),

immer

리듀서에서 상태를 업데이트할 때는 불변성을 지켜야 하기 때문에 위에서는 spread 연산자(...)와 배열의 내장 함수를 사용했다.

 

이는 마찬가지로 지난 번에 배운 immer를 사용하여 객체의 구조가 복잡해질 때를 대비할 수 있다.

import produce from 'immer';

[CHANGE_INPUT]: (state, {payload: input}) =>
    produce(state, draft => {
        draft.input = input;
    }),

Hooks를 사용하여 컨테이너 컴포넌트 만들기

리덕스 스토어와 연동된 컨테이너 컴포넌트를 만들 때 connect 함수를 사용하는 대신 react-redux에서 제공하는 Hooks를 사용할 수 있다.

 

1. useSelector로 상태 조회하기

useSelector Hook을 사용하면 connect 함수를 사용하지 않고도 리덕스의 상태를 조회할 수 있다. 

const 결과 = useSelector(상태 선택 함수);

여기서 상태 선택 함수는 mapStateToProps와 형태가 동일하다.

 

◎container/CounterContainer.js

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

const CounterContainer = () => {
    const number = useSelector(state => state.counter.number);
    return <Counter number={number}/>;
};


export default CounterContainer;

 

2. useDispatch를 사용하여 액션 디스패치하기

컨테이너 컴포넌트에서 액션을 디스패치하기 위해서는 이 Hook을 사용하면 된다.

 

◎container/CounterContainer.js

import Counter from '../components/Counter';
import {useDispatch, useSelector} from "react-redux";
import {decrease, increase} from "../modules/counter";

const CounterContainer = () => {
    const number = useSelector(state => state.counter.number);
    const dispatch = useDispatch();
    return <Counter
        number={number}
        onIncrease={() => dispatch(increase())}
        onDecrease={() => dispatch(decrease())}
    />;
};


export default CounterContainer;

또한 성능을 최적화하기 위해 useDispatch 함수는 useCallback으로 감싸주는 것이 좋다.

 

◎container/CounterContainer.js

import Counter from '../components/Counter';
import {useDispatch, useSelector} from "react-redux";
import {decrease, increase} from "../modules/counter";
import {useCallback} from "react";

const CounterContainer = () => {
    const number = useSelector(state => state.counter.number);
    const dispatch = useDispatch();
    const onIncrease= useCallback(() => dispatch(increase()), [dispatch]);
    const onDecrease= useCallback(() => dispatch(decrease()), [dispatch]);
    
    return <Counter
        number={number}
        onIncrease={onIncrease}
        onDecrease={onDecrease}
    />;
};

export default CounterContainer;

동일하게 useSelector와 useDispatch를 사용하여 todos 컴포넌트도 전환해보자.

 

◎container/TodosContainer.js

import Todos from "../components/Todos";
import {changeInput, insert, remove, toggle} from "../modules/todos";
import {useCallback} from "react";
import {useDispatch, useSelector} from "react-redux";

const TodosContainer = () => {
    const {input, todos} = useSelector(({todos}) => ({
        input: todos.input,
        todos: todos.todos
    }));
    
    const dispatch = useDispatch();
    const onChangeInput = useCallback(input => dispatch(changeInput(input)), [dispatch]);
    const onInsert = useCallback(text => dispatch(insert(text)), [dispatch]);
    const onToggle = useCallback(id => dispatch(toggle(id)), [dispatch]);
    const onRemove = useCallback(id => dispatch(remove(id)), [dispatch]);
    
    return (
        <Todos
            input={input}
            todos={todos}
            onChangeInput={changeInput}
            onInsert={insert}
            onToggle={toggle}
            onRemove={remove}
        />
    );
};

export default default TodosContainer;

useActions 유틸 Hook을 만들어서 사용하기

useActions는 원래 react-redux에 내장된 상태로 릴리즈될 계획이었으나 제외됐다. 여기에서 그대로 복사하여 사용할 수 있도록 제공하니 참고하자.

 

이 Hook을 사용하면, 여러 개의 액션을 사용해야 하는 경우 코드를 훨씬 깔끔하게 정리하여 작성할 수 있다.

 

◎lib/useAction.js

import {bindActionCreators} from "redux";
import {useDispatch} from "react-redux";
import {useMemo} from "react";

export default function useActions(actions, deps) {
    const dispatch = useDispatch();
    return useMemo(
        () => {
            if(Array.isArray(actions)) {
                return actions.map(a => bindActionCreators(a, dispatch));
            }
            return bindActionCreators(actions, dispatch);
        },
        deps ? [dispatch, ...deps] : deps
    );
}

useActions Hook은 액션 생성 함수를 액션을 디스패치하는 함수로 변환해준다.

 

액션 생성 함수를 사용하여 액션 객체를 만들고, 이를 스토어에 디스패치하는 작업을 해주는 함수를 만들어 주는 것이다.

 

◎container/TodosContainer.js

import Todos from "../components/Todos";
import {changeInput, insert, remove, toggle} from "../modules/todos";
import {useCallback} from "react";
import {useDispatch, useSelector} from "react-redux";
import useActions from "../lib/useAction";
import react from "hoist-non-react-statics/dist/hoist-non-react-statics";

const TodosContainer = () => {
    const {input, todos} = useSelector(({todos}) => ({
        input: todos.input,
        todos: todos.todos
    }));

    const [onChangeInput, onInsert, onToggle, onRemove] = useActions(
        [changeInput, insert, toggle, remove], []
    );

    return (
        <Todos
            input={input}
            todos={todos}
            onChangeInput={onChangeInput}
            onInsert={onInsert}
            onToggle={onToggle}
            onRemove={onRemove}
        />
    );
};

export default react.Memo(TodosContainer);

 

단, 이렇게 connect 함수 대신 Hooks를 사용했을 때는 컨테이너의 부모 컴포넌트가 리렌더링될 때 해당 컨테이너 컴포넌트의 props가 바뀌지 않았다면 리렌더링이 자동으로 방지되는 성능 최적화를 지원하지 않으니, 필요한 경우에는 아래와 같이 해당 컨테이너를 react.Memo로 감싸줘야 한다.

export default react.Memo(TodosContainer);

생각보다... 내용도 굉장히 많고 알아둬야 할 것도 많지만, 리덕스를 사용하면 업데이트에 관련된 로직을 리액트 컴포넌트에서 완벽하게 분리시킬 수 있으므로 유지 보수성이 높은 코드를 작성할 수 있다.

 

반드시 복습하여 잘 알아두고 넘어가자.

 

 

 

끝!

Comments