이번장에서는 리덕스를 사용하여 리액트 애플리케이션 상태를 관리하는 방법에 대해 알아보겠습니다. 소규모 프로젝트에서는 컴포넌트가 가진 state를 사용하는 것만으로도 충분하지만 프로젝트의 규모가 커짐에 따라 상태 관리가 복잡해질 수 있습니다.
리덕스를 사용하여 상태 관리를 하면 다음과 같은 장점이 있습니다.
1. 상태 업데이트에 관한 로직을 모듈로 따로 분리하여 컴포넌트 파일과 별개로 관리할 수 있어 코드를 유지보수 하는데 도움이 됩니다.
2. 여러 컴포넌트에서 동일한 상태를 공유해야 할 때 유용합니다.
3. 실제 업데이트가 필요한 컴포넌트만 리렌더링되도록 쉽게 최적화할 수 있다.
이번 실습의 흐름은 다음과 같습니다.
프로젝트 준비 → 프레젠테이셔널 컴포넌트 작성 → 리덕스 관련 코드 작성 → 컨테이너 컴포넌트 작성 → 더 편하게 사용하는 방법 알아보기 → connect 대신 Hooks 사용하기
리액트 프로젝트를 생성하고, 리덕스를 적용해 봅시다.
yarn create react-app react-redux-tutorial
cd react-redux-tutorial
yarn add redux react-redux
새 프로젝트를 생성하고, 생성한 프로젝트 디렉터리에 리덕스와 react-redux 라이브러리를 설치하세요.
리액트 프로젝트에서 리덕스를 사용할 때 가장 많이 사용하는 패턴은 프레젠테이셔널 컴포넌트와 컨테이너 컴포넌트를 분리하는 것입니다. 여기서 프레젠테이셔널 컴포넌트란 주로 상태 관리가 이루어지지 않고, 그저 props를 받아와 화면에 UI를 보여주기만 하는 컴포넌트를 말합니다. 이와 달리 컨테이너 컴포넌트는 리덕스와 연동되어 있는 컴포넌트로, 리덕스로부터 상태를 받아오기도 하고 리덕스 스토어에 액션을 디스패치하기도 합니다.
이러한 패턴은 리덕스 사용에 있어 필수는 아니지만, 코드의 재사용성도 높아지고, UI 작성에 더 집중할 수 있습니다.
이 장의 프로젝트에서는 이 패턴을 사용해 코드를 작성하겠습니다. 프레젠테이셔널 컴포넌트는 src 밑에 components 디렉터리를 만들어 그 안에서 만들겠습니다. 컨테이너 컴포넌트는 src 밑에 containers 디렉터리를 만들어 그 안에서 만들겠습니다.
이번 장에서는 할 일 목록 컴포넌트를 만들겠습니다. components 디렉터리에 Todos 컴포넌트를 작성하세요
// components/Todos.js
import React from 'react';
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;
파일 하나에 두 컴포넌트를 선언했습니다. 취향에 따라 Todos 컴포넌트와 TodoItem 컴포넌트를 분리해도 되고, 위처럼 파일 하나에 작성해도 무방합니다.
App 컴포넌트에서 렌더링을 하면 다음과 같은 화면이 나타납니다.
// App.js
import React from 'react';
import Todos from './components/Todos';
const App = () => {
return (
<div>
<Todos />
</div>
);
};
export default App;
이제 리덕스를 사용해 보도록 하겠습니다. 리덕스를 사용할 때는 액션 타입, 액션 생성 함수, 리듀서 코드를 작성해야 합니다. 이 코드들을 각각 다른 파일에 작성할 수도 있고, 기능별로 묶어 파일 하나에 작성할 수도 있습니다.
위 구조는 가장 일반적인 구조로 actions, constants, reducers라는 세 개의 디렉터리를 만들고 그 안에 기능별로 파일을 하나씩 만드는 방식입니다. 정리하는데에 편리하지만, 새 액션을 만들 때마다 세 종류의 파일을 모두 수정해야 하는 불편함도 있습니다.
위 구조는 액션타입, 액션 생성 함수, 리듀서 함수를 기능별로 파일 하나에 몰아서 다 작성하는 방식입니다. 이러한 방식을 Ducks 패턴이라고 부르며, 첫 번째 구조에 불편함을 느낀 개발자들이 주로 사용합니다. Ducks 패턴을 사용하여 작성한 코드를 ‘모듈’이라고 합니다.
두 방식 모두 사용가능 하지만, 이번 장에서는 Ducks 패턴을 사용하여 코드를 작성하겠습니다.
modules 디렉터리에 todos.js 파일을 생성하세요
// modules/todos.js
// 첫 번째
const CHANGE_INPUT = 'todos/CHANGE_INPUT';
const INSERT = 'todos/INSERT';
const TOGGLE = 'todos/TOGGLE';
const REMOVE = 'todos/REMOVE';
// 두 번째
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: 2,
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;
액션 타입 정의입니다. 이 단계는 가장 먼저 해야 하는 작업으로, 액션 타입은 대문자로 정의하고, 문자열 내용은 ‘모듈 이름/액션 이름’과 같은 형태로 작성합니다. 문자열 안에 모듈 이름을 넣음으로써, 액션의 이름이 중복되지 않도록 합니다.
액션 생성 함수를 만듭니다. 이 예제에서는 액션 생성 함수에서 파라미터가 필요합니다. 전달받은 파라미터는 액션 객체 안에 추가 필드로 들어가게 됩니다. 물론 파라미터가 필요 없는 액션 생성 함수도 있습니다. export 키워드를 사용하면서, 추후 이 함수를 다른 파일에서 불러와 사용할 수 있습니다. insert 함수는 액션 객체를 만들 때 파라미터 외에 사전에 선언된 id라는 값에도 의존합니다. 액션 함수가 호출될 때마다 id값에 1씩 더해줍니다. 이 id 값은 각 todo 객체가 들고 있을 고윳값입니다.
모듈의 초기 상태와 리듀서 함수를 작성합니다. 객체에 한 개 이상의 값이 들어가 불변성을 유지해주어야 합니다. spread 연산자와 배열 내장 함수를 사용하여 구현했습니다.
이때, 액션 생성 함수는 export로 내보냈고, 리듀서는 export default로 내보내주었습니다. 둘의 차이점은 무엇일까요?
내보낼 수 있는 개수 | 불러오는 방식 | |
export | 여러 개 내보낼 수 있음 | import { changeInput, toggle, remove } from './todos |
export default | 단 한개만 내보낼 수 있음 | import todos from './todos |
이번 프로젝트에서는 리듀서가 한 개이지만, 여러 개라면 어떨까요? 나중에 createStore 함수를 이용하여 스토어를 만들 때는 리듀서를 하나만 사용해야 합니다. 따라서 여러 리듀서 함수를 하나로 합쳐주어야 하는데, 이 작업을 리덕스에서 제공하는 combineRedeucers라는 유틸 함수를 사용해 쉽게 처리할 수 있습니다.
modules 디렉터리에 index.js를 만들어 아래와 같이 작성하세요.
// modules/index.js
import { combineReducers } from ‘redux‘;
import todos from ‘./todos‘;
const rootReducer = combineReducers({
todos,
});
export default rootReducer;
이렇게 작성하면 나중에 불러올 때 디렉터리 이름까지만 입력하여 불러올 수 있습니다
import rootReducer from ‘./modules‘;
이제 리액트 애플리케이션에 리덕스를 적용해 봅시다. 작업은 src 디렉터리의 index.js 에서 이루어집니다.
가장 먼저 스토어를 생성합니다
// src/index.js
import React from 'react';
import ReactDOM from 'react-dom/client';
import {createStore} from 'redux';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
import rootReducer from './modules';
const store = createStore(rootReducer);
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
);
reportWebVitals();
그 후 react-redux에서 제공하는 Provider 컴포넌트로 App 컴포넌트를 감싸줍니다. 이 컴포넌트를 사용할 때는 store를 props로 전달해야 합니다.
// src/index.js
import React from 'react';
import ReactDOM from 'react-dom/client';
import {createStore} from 'redux';
import { Provider } from 'react-redux';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
import rootReducer from './modules';
const store = createStore(rootReducer);
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<Provider store={store}>
<App />
</Provider>
);
reportWebVitals();
다 작성했다면 Redux DevTools를 설치합시다. Redux DevTools는 리덕스 개발자 도구로 크롬 확장 프로그램으로 설치할 수 있습니다.
크롭 웹 스토어에서 Redux DevTools를 검색해 설치해 주세요.
그 후 패키지를 설치하여 코드를 더욱 깔끔하게 만들어줍시다.
yarn add redux-devtools-extension
그리고 다음과 같이 적용해 주세요.
// src/index.js
import React from 'react';
import ReactDOM from 'react-dom/client';
import {createStore} from 'redux';
import { Provider } from 'react-redux';
import { composeWithDevTools } from 'redux-devtools-extension';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
import rootReducer from './modules';
const store = createStore(rootReducer, composeWithDevTools());
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<Provider store={store}>
<App />
</Provider>
);
reportWebVitals();
이제 브라우저에서 크롬 개발자 도구를 실행한 후 Redux 탭을 열어보세요. 리덕스 개발자 도구가 잘 나타났나요?
이제 컴포넌트에서 리덕스 스토어에 접근하여 원하는 상태를 받아오고, 또 액션도 디스패치 해줄 차례입니다. 아까 설명했듯, 리덕스 스토어와 연동된 컴포넌트를 컨테이너 컴포넌트라고 부릅니다.
컴포넌트를 리덕스와 연동하기 위해 react-redux에서 제공하는 connect 함수를 사용해야 합니다. connect 함수의 사용법은 아래와 같습니다.
connect(mapStateToProps, mapDispatchToProps)(연동할 컴포넌트)
여기서 mapStateToProps는 리덕스 스토어 안의 상태를 컴포넌트의 props로 넘겨주기 위한 함수이고, mapDispatchToProps는 액션 생성 함수를 컴포넌트의 props로 넘겨주기 위해 사용하는 함수입니다. 이렇게 connect 함수를 호출하고 나면 또 다른 함수를 반환합니다. 반환된 함수에 컴포넌트를 파라미터로 넣어주면 리덕스와 연동된 컴포넌트가 만들어집니다.
connect 함수를 사용할 때는 일반적으로 mapStateToProps와 mapDispatchToProps를 미리 선언해 놓고 사용합니다. 하지만 이 작성보다 더 편한 방법도 있습니다. 바로 mapDispatchToProps에 해당하는 파라미터를 함수 형태가 아닌 액션 생성 함수로 이루어진 객체 형태로 넣어주는 것입니다. 자세한 사용 방법은 아래 코드에서 확인해 보겠습니다.
src 디렉터리에 containers 디렉터리를 생성하고, 그 안에 Todos 컴포넌트를 위한 컨테이너 TodosContainer를 작성해 보겠습니다
// containers/TodosContainer.js
import React from 'react';
import { connect } from 'react-redux';
import { changeInput, insert, toggle, remove } from '../modules/todos';
import Todos from '../components/Todos';
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 컴포넌트에서 Todos컴포넌트를 TodosContainer 컴포넌트로 교체하세요
// App.js
import React from 'react';
import TodosContainer from './containers/TodosContainer';
const App = () => {
return (
<div>
<TodosContainer />
</div>
);
};
export default App;
그다음으로는 Todos 컴포넌트에서 받아 온 props를 사용하도록 구현하세요
// components/Todos.js
import React from 'react';
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-action과 immer 라이브러리를 활용하면 리덕스를 더 편하게 사용할 수 있습니다.
우선 redux-action 라이브러리를 설치해 주세요
yarn add redux-actions
immer 라이브러리를 설치해 주세요
yarn add immer
이제 modules 디렉터리의 todos.js를 수정해 보도록 하겠습니다.
// modules/todos.js
import { createAction, handleActions } from "redux-actions";
import produce from "immer";
const CHANGE_INPUT = 'todos/CHANGE_INPUT';
const INSERT = 'todos/INSERT';
const TOGGLE = 'todos/TOGGLE';
const REMOVE = 'todos/REMOVE';
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
}
]
};
const todos = handleActions(
{
[CHANGE_INPUT]: (state, { payload: input }) => ({ ...state, input }),
[INSERT]: (state, { payload: todo }) => ({
...state,
todos: state.todos.concat(todo),
}),
[TOGGLE]: (state, { payload: id }) => ({
...state,
todos: state.todos.map(todo =>
todo.id = = = id ? { ...todo, done: !todo.done } : todo,
),
}),
[REMOVE]: (state, { payload: id }) => ({
...state,
todos: state.todos.filter(todo => todo.id != = id),
}),
},
initialState,
);
export default todos;
createAction을 사용하면 매번 객체를 직접 만들 필요 없이 더욱 간단하게 액션 생성 함수를 선언할 수 있습니다.
이번 예제의 각 액션 생성 함수에서 파라미터가 필요합니다. createAction으로 액션을 만들면 액션에 필요한 추가 데이터는 payload라는 이름을 사용합니다. 액션 생성 함수에서 받아온 파라미터를 그대로 payload에 넣는 것이 아니라 변형을 주어서 넣고 싶다면 createAction의 두 번째 함수에 payload를 정의하는 함수를 따로 선언해 넣어주면 됩니다.
handleActions는 리듀서 함수를 더 간단하게 표현할 수 있습니다. 첫 번째 파라미터에는 각 액션에 대한 업데이트 함수를 넣어주고, 두 번째 파라미터에는 초기 상태를 넣어줍니다. 액션 생성 함수는 액션에 필요한 추가 데이터를 모두 payload라는 이름으로 사용하기 때문에, action.id, action.todo를 조회하는 대신, 모두 공통적으로 action.payload라는 값을 조회하도록 리듀서에서 구현해야 합니다. 추가 데이터값을 모두 action.payload로 사용하면 나중에 리듀서 코드를 다시 볼 때 헷갈리기 때문에 객체 비구조화 할당 문법으로 action값의 payload 이름을 새로 설정해 action.payload가 어떤 값을 의미하는지 쉽게 파악하도록 했습니다.
이제 todos 모듈에 immer을 적용해 봅시다.
// modules/todos.js
import { createAction, handleActions } from "redux-actions";
import produce from "immer";
const CHANGE_INPUT = 'todos/CHANGE_INPUT';
const INSERT = 'todos/INSERT';
const TOGGLE = 'todos/TOGGLE';
const REMOVE = 'todos/REMOVE';
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
}
]
};
const todos = handleActions(
{
[CHANGE_INPUT]: (state, {payload:input}) =>
produce(state, draft => {
draft.input = input;
}),
[INSERT]: (state, {payload:todo}) =>
produce(state, draft => {
draft.todos.push(todo);
}),
[TOGGLE]: (state, {payload:id}) =>
produce(state, draft => {
const todo = draft.todos.find(todo => todo.id === id);
todo.done = !todo.done;
}),
[REMOVE] : (state, {payload:id}) =>
produce(state, draft => {
const index = draft.todos.findIndex(todo => todo.id === id);
draft.todos.splice(index,1);
}),
},
initialState,
);
export default todos;
리듀서에서 상태를 업데이트할 때 불변성을 지켜야 하기 때문에 spread 연산자와 배열의 내장 함수를 활용했습니다. 그러나 모듈의 상태가 복잡할수록 불변성을 지키기 어려워집니다. 이럴 때 immer를 사용하면 훨씬 편안하게 관리할 수 있습니다.
하지만 immer를 사용하지 않는 편이 코드의 길이가 더 짧은 경우도 있기 때문에 모든 업데이트함수에 immer를 적용할 필요는 없습니다.
리덕스 스토어와 연동된 컨테이너 컴포넌트를 만들 때 connect 함수 대신 react-redux에서 제공하는 Hooks를 사용할 수 있습니다.
useSelector Hook을 사용하면 connect 함수를 사용하지 않고도 리덕스의 상태를 조회할 수 있습니다. 사용법은 다음과 같습니다.
const 결과 = useSelector(상태 선택 함수);
여기서 상태 선택 함수는 mapStateToProps와 형태가 똑같습니다.
useDispatch Hook은 컴포넌트 내부에서 스토어의 내장 함수 dispatch를 사용할 수 있게 해 줍니다. 컨테이너 컴포넌트에서 액션을 디스패치해야 한다면 이 Hook을 사용하면 됩니다. 사용법은 다음과 같습니다.
const dispatch = useDispatch();
dispatch({ type: ‘SMAPLE_ACTION’ });
useStore Hooks를 사용하면 컴포넌트 내부에서 리덕스 스토어 객체를 직접 사용할 수 있습니다. 사용법은 다음과 같습니다.
const Store = useStore();
store.dispatch({type:’SAMPLE_ACTION’});
stroe.getState();
useStore은 컴포넌트에서 정말 어쩌다 스토어에 직접 접근해야 하는 상황에만 사용해야 합니다.
이를 사용하는 상황은 흔치 않습니다.
이제 TodosContainer에 connect 대신 useSelector와 useDispatch Hooks를 사용하는 형태로 전환해 봅시다.
// containers/TodosContainer.js
import React, { useCallback } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { changeInput, insert, toggle, remove } from '../modules/todos';
import Todos from '../components/Todos';
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={onChangeInput}
onInsert={onInsert}
onToggle={onToggle}
onRemove={onRemove}
/>
);
};
export default TodosContainer;
이번에는 useSelector를 사용할 때 비구조화 할당 문법을 활용했습니다. 또 useDispatch를 사용할 때 각 액션을 디스패치하는 함수를 만들었는데, 위 코드의 경우 액션의 종류가 많아 어떤 값이 액션 생성 함수의 파라미터로 사용되는지 일일이 명시해주어야 하므로 조금 번거롭습니다.
useActions는 원래 react-redux에 내장된 상태로 릴리즈 될 계획이었으나 제외된 Hooks입니다. 대신 공식 문서에서 그대로 복사하여 사용할 수 있도록 제공하고 있습니다. 이 Hook을 사용하면 여러 개의 액션을 사용해야 하는 경우 코드를 훨씬 깔끔하게 정리하여 작성할 수 있습니다.
src디렉터리에 lib 디렉터리를 만들고 그 안에 useAction.js 파일을 다음과 같이 만들어 보세요
// lib/useActions.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은 액션 생성 함수를 액션을 디스패치하는 함수로 변환해 줍니다. 액션 생성 함수를 이용해 액션 객체를 만들고, 이를 스토어에 디스패치하는 작업을 해 주는 함수를 자동으로 만들어주는 것이죠
useActions는 두 가지 파라미터를 가집니다. 첫 번째 파라미터는 액션 생성 함수로 이루어진 배열입니다. 두 번째는 deps 배열이며, 이 배열 안에 들어있는 원소가 바뀌면 액션을 디스패치하는 함수로 새로 만들게 됩니다.
그럼 TodoContainers에서 useAction을 불러와 봅시다.
// container/TodoContainer.js
import React from 'react';
import { useSelector } from 'react-redux';
import { changeInput, insert, toggle, remove } from '../modules/todos';
import Todos from '../components/Todos';
import useActions from '../lib/useActions';
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 TodosContainer;
잘 작동되나요?
앞으로 컨테이너 컴포넌트를 만들 때 connect함수를 사용해도 좋고 useSelector와 useDispatch를 사용해도 좋습니다.
하지만 Hooks를 사용할 때 잘 알아두어야 할 차이점이 있습니다. connect 함수를 사용해 컨테이너 컴포넌트를 만들었을 경우, 해당 컨테이너 컴포넌트의 부모 컴포넌트가 리렌더링 될 때 해당 컨테이너 컴포넌트의 props가 바뀌지 않았다면 리렌더링이 자동으로 방지되어 성능이 최적화됩니다.
그러나 useSelector를 사용하여 리덕스 상태를 조회할 경우 최적화 작업이 자동으로 이루어지지 않음으로 React.memo를 사용해 성능 최적화를 해주어야 합니다.
이번 장에서는 리액트 프로젝트에서 리덕스를 적용하여 사용하는 방법을 배워보았습니다. 리덕스 이용 시 업데이트에 관련된 로직을 리액트 컴포넌트에서 완벽하게 분리시킬 수 있으므로 유지 보수성이 높은 코드를 작성해 낼 수 있습니다.
Quiz
1. ( A )란 주로 상태 관리가 이루어지지 않고, 그저 props를 받아 와서 화면에 UI를 보여 주기만 하는 컴포넌트이다. ( B )란 리덕스와 연동되어 있는 컴포넌트로, 리덕스로부터 상태를 받아오기도 하고 리덕스 스토어에 액션을 디스패치하기도 한다.
2. 액션 타입, 액션 생성 함수, 리듀서 함수를 기능별로 파일 하나에 몰아서 다 작성하는 방식을 ( C )라고 부른다.
3. export는 ( D ) 내보낼 수 있고, export default는 ( E ) 내보낼 수 있다.
4. 리눅스에서는 리듀서를 하나만 사용해야 하기 때문에 ( F )라는 유틸 함수를 사용하여 처리한다.
5. connect 함수는 ( G )와 같이 사용하며, 함수 호출 후 또 다른 함수를 반환한다.
6. ( H ) Hook을 사용하면 connect 함수를 사용하지 않고도 리덕스의 상태를 조회할 수 있고, ( I ) Hook을 사용하면 컴포넌트 내부에서 스토어 내장 함수 dispatch를 사용할 수 있게 한다.
7. connect 함수를 사용할 때는 ( J )가 자동으로 되지만, useSelector를 사용하면 ( J )가 자동으로 이루어지지 않아 ( K )를 컨테이너 컴포넌트에 사용해야 한다.
8. 아래 CounterContainer.js의 mapDispatchToProps에 해당하는 파라미터를 함수 형태가 아닌 액션 생성 함수로 이루어진 객체 형태로 넣으세요.
import React from 'react';
import { connect } from 'react-redux';
import Counter from '../components/Counter';
import { increase, decrease } 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);
9. 숫자 0에서 더하고 빼는 counter 모듈을 createActions와 handleActions 함수를 사용하여 간단하게 만드세요.
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;
정답
1. A) 프레젠테이셔널 컴포넌트 B) 컨테이너 컴포넌트
2. C) Ducks 패턴
3. D) 여러 개를 E) 단 한 개만
4. F) combineReducers
5. G) connect(mapStateToProps, mapDispatchToProps)(연동할 컴포넌트)
6. H) useSelector I) useDispatch
7. J) 최적화 작업 K) React.memo
8.
import React from 'react';
import { connect } from 'react-redux';
import Counter from '../components/Counter';
import { increase, decrease } from '../modules/counter';
const CounterContainer = ({ number, increase, decrease }) => {
return (
<Counter number={number} onIncrease={increase} onDecrease={decrease} />
);
};
export default connect(
state=>({
number: state.counter.number,
}),
{
increase,
decrease,
},
)(CounterContainer);
9.
import { createAction, handleActions } from 'redux-actions';
const INCREASE = 'counter/INCREASE';
const DECREASE = 'counter/DECREASE';
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;
Corner React1
Editor: 이조
[리액트 스타터1] 16장. 리덕스 라이브러리 이해하기 (0) | 2023.01.12 |
---|---|
[리액트 스타터1] 13장. 리액트 라우터로 SPA 개발하기 (0) | 2023.01.05 |
[리액트 스타터1] 12장. immer를 사용하여 더 쉽게 불변성 유지하기 (1) | 2022.12.29 |
[리액트 스타터1] 11장. 컴포넌트 성능 최적화 (0) | 2022.12.29 |
[리액트 스타터1] 10장 : 일정 관리 웹 애플리케이션 만들기 (0) | 2022.12.22 |