Counter와 Todo component를 만들자.
Counter.js
import React from ‘react‘;
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.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;
App.js에 각 컴포넌트 사용시 다음과 같이 나타난다.
import React from 'react';
import Counter from './components/Counter';
import Todos from './components/Todos';
const App = () => {
return (
<div>
<Counter number={0} />
<hr />
<Todos />
</div>
);
};
export default App;
Ducks 패턴(액션 타입, 액션 생성 함수, 리듀서 함수를 기능별로 파일 하나에 몰아서 다 작성하는 방식)을 사용하여 액션 타입, 액션 생성 함수, 리듀서를 작성한 코드를 ‘모듈’이라고 한다. 각각의 컴포넌트를 모듈화하자.
# modules 폴더!!!
counter.js
import {createAction, handleActions} from 'redux-actions';
// actionType = moduleName/actionName
const INCREASE = 'counter/INCREASE';
const DECREASE = 'counter/DECREASE';
// action create function
// createAction을 사용하면 매번 객체를 직접 만들 필요 없이 간단히 액션 생성 함수 선언
export const increase = createAction(INCREASE);
export const decrease = createAction(DECREASE);
// 초기상태
const initialState = {
number: 0
};
// 리듀서 함수
// handleActions(update함수, 초기상태)
const counter = handleActions(
{
[INCREASE]: (state, action) => ({number : state.number + 1}),
[DECREASE]: (state, action) => ({number : state.number - 1}),
},
initialState
)
export default counter;
todos.js
import {createAction, handleActions} from 'redux-actions';
import produce from 'immer';
// action type
const CHANGE_INPUT = 'todos/CHANGE_INPUT';
const INSERT = 'todos/INSERT';
const TOGGLE = 'todos/TOGGLE';
const REMOVE = 'todos/REMOVE';
// action create func
export const changeInput = createAction(CHANGE_INPUT, input => input);
// insert는 todo객체를 액션 객체에 넣어줘야하므로, text를 넣으면 todo객체가 반환된다
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(TOGGLE, id => id);
// 초기값
const initialState = {
input: '',
todos:[
{
id: 1,
text: '리덕스 기초 배우기',
done: true
},
{
id: 2,
text: '리액트와 리덕스 사용하기',
done: false
}
]
};
// reducer 함수
// 액션에 필요한 추가 데이터를 모두 payload라는 이름으로 사용하도록 구현
const todos = handleActions(
{
[CHANGE_INPUT]: (state, {playload: input}) => produce(state, draft => {draft.input = input}),
[INSERT]: (state, {playload: todo}) => produce(state, draft => {draft.todos.push(todo)}),
[TOGGLE]: (state, {playload: id}) => produce(state, draft => {
const todo = draft.todos.find(todo => todo.id === id);
todo.done = !todo.done;
}),
[REMOVE]: (state, {playload: id}) => produce(state, draft => {
const index = draft.todos.findIndex(todo => todo.id === id);
draft.todos.splice(index, 1);
})
},
initialState
)
export default todos;
- 객체의 구조가 복잡해지거나 객체로 이루어진 배열을 다룰 경우, immer를 사용하면 코드의 가독성 및 컴포넌트와 리덕스 연동에 편리하다.
store를 사용하기 위해서는 reducer를 하나만 사용해야하므로, index.js 파일에서 reducer를 합쳐 주자.
index.js
import { combineReducers } from "redux";
import counter from './counter';
import todos from './todos';
const rootReducer = combineReducers({
counter, todos,
})
export default rootReducer;
# src 폴더!!!
redux를 적용하기 위해 src/index.js에 store를 생성한다.
index.js
import ReactDOM from 'react-dom';
import {createStore} from 'redux';
import {Provider} from 'react-redux';
import { composeWithDevTools } from 'redux-devtools-extension';
import App from './App';
import rootReducer from './modules';
const store = createStore(rootReducer, composeWithDevTools());
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root'));
react component에서 store를 사용할 수 있도록 app component를 react-redux에서 제공하는 Provider component로 감싸준다.
이제 component에서 redux store에 접근하여 원하는 상태를 받아오고, action을 dispatch하자. redux store와 연동된 컴포넌트를 container component라고 한다.
# src / containers 폴더!!!
CounterContainer.js
import { useCallback } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import Counter from '../components/Counter';
import { increase, decrease } from '../modules/counter';
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;
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;
위 코드에서 액션의 종류가 많은데 어떤 값이 액션 생성 함수의 파라미터로 사용되어야 하는지 명시해주어야 하므로 번거롭다. 이를 개선하기 위해 useActions를 사용해보자. 해당 lib은 공식 문서에서 제공하고 있다. (https://react-redux.js.org/api/hooks#recipe-useactions)
# src / 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
);
}
이 hook은 액션 생성 함수를 액션을 dispatch하는 함수로 변환해준다. 액션 생성 함수를 사용하여 액션 객체를 만들고, 이를 store에 dispatch하는 작업을 해주는 함수를 자동으로 만들어준다.
useActions(액션 생성 함수로 이루어진 배열, deps 배열) : deps 배열 안에 있는 원소가 바뀌면 action을 dispatch하는 함수를 새로 만든다.
TodoContainer.js에 적용
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를 사용하면 해당 컨테이너 컴포넌트의 부모 컴포넌트가 리렌더링 될 때 해당 컨테이너 컴포넌트의 props가 바뀌지 않았다면 리렌더링이 자동으로 방지되어 성능이 최적화된다. 하지만, useSelector는 자동으로 최적화가 이루어지지 않으므로 React.memo를 컨테이너 컴포넌트에 사용해주어야 한다.
'Programming > React.js' 카테고리의 다른 글
13. Redux (0) | 2022.09.17 |
---|---|
12. ContextApi (0) | 2022.09.14 |
11. 라우터로 SPA 개발하기 (0) | 2022.09.08 |
10. immer사용하여 불변성 유지하기 (0) | 2022.08.31 |
9. 컴포넌트 성능 최적화 (0) | 2022.08.24 |