리액트 프로젝트에서 리덕스를 사용할 때 가장 많이 사용하는 패턴은 주로 화면에 UI를 보여주는 프레젠테이셔널 컴포넌트와 리덕스와 연동하여 state를 받아오거나 리덕스 스토어에 액션을 디스패치하는 역할을 하는 컨테이너 컴포넌트로 분리하는 것이다.
실습할 내용
Counter 컴포넌트에는 number 변수와 onIncrease, onDecrease 함수가 props로 전달된다.
Todos 컴포넌트의 TodoItem에는 todo 객체와 onToggle, onRemove 함수가 props로 전달된다. Todos에는 인풋에 입력되는 텍스트인 input, todos 객체, onChangeInput, onInsert, onToggle, onRemove 함수가 props로 전달된다.
17. 리덕스를 사용하여 리액트 애플리케이션 상태 관리하기
리덕스 코드를 작성할때는 action, constants, reducers 라는 세 개의 디렉토리를 만들고 기능별로 파일을 관리하는 방식과 액션 타입, 액션 생성 함수, 리듀서 함수를 기능별로 파일 하나에 몰아서 작성하는 Ducks 패턴이 있다.
Ducks 패턴을 사용하여 액션 타입, 액션 생성 함수, 리듀서를 작성한 코드는 '모듈'이라고 한다.
기본적인 리덕스 코드는 앞서 리덕스 키워드를 공부하며 생성해둔 상태이다.
1. counter 모듈과 todos 모듈을 작성한다. (리액트를 다루는 기술 17.3.1 , 17.3.2)
// modules/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;
2. 루트 리듀서를 만든다. (17.3.3)
// modules/index.js
import { combineReducers } from 'redux';
import counter from './counter';
import todos from './todos';
const rootReducer = combineReducers({
counter,
todos,
});
export default rootReducer;
하나의 애플리케이션에서는 하나의 스토어를 사용하는 것이 권장된다. 따라서 기존에 만들었던 리듀서들을 combineReducers 라는 유틸함수를 사용하여 하나로 합쳐준다.
3. 스토어 만들고 Provider 컴포넌트를 사용하여 프로젝트에 리덕스 적용하기 + Redux DevTools 적용하기(17.4)
// src/index.js
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import { createStore } from 'redux';
import { Provider } from 'react-redux';
import rootReducer from './modules';
import { composeWithDevTools } from 'redux-devtools-extension';
const store = createStore(rootReducer,composeWithDevTools());
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root'),
);
크롬 확장 프로그램인 Redux DevTools 를 설치하고 redux-devtools-extension 라이브러리도 설치해야 한다.
4. 컨테이너 컴포넌트 만들기 ( 컴포넌트와 리덕스를 연동하기 위해 react-redux의 connect 함수 사용)
// 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 mapStateProps = (state) => ({
number: state.counter.number,
});
const mapDispatchToProps = (dispatch) => ({
// 임시함수
increase: () => {
console.log('increase');
},
decrease: () => {
console.log('decrease');
},
});
export default connect(mapStateProps, mapDispatchToProps)(CounterContainer);
mapStateToProps : 스토어 안의 상태를 컴포넌트의 props 로 넘겨주기 위해 설정하는 함수
mapDispatchToProps : 액션 생성 함수를 컴포넌트의 prosp 로 넘겨주기 위해 사용하는 함수
그리고 App.js 에서 Counter를 CounterContainer 로 교체하면 +1, -1 를 클릭할때마다 increase, decrease 콘솔이 출력되는 것을 확인할 수 있다.
connect 함수와 bindActionCreators 유틸함수를 사용하면 더 간편하게 사용할 수도 있다.(17.5.1)
// containers/TodosContainer.js
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.js 에서 Todos 컴포넌트 역시 TodosContainer로 교체
요약
modules 폴더의 각 파일에 action, action creator function, reducer function 코드를 작성하고
components 폴더의 각 파일에 화면에 보여질 컴포넌트 코드를 작성한다. ⇒ 프레젠테이셔널 컴포넌트
containers 폴더의 컨테이너 컴포넌트에서 module import 한 후에 프레젠테이셔널 컴포넌트 props로 전달
리덕스 더 편하게 사용하기
redux-action 라이브러리 와 immer 라이브러리를 활용하면 리덕스를 좀 더 편하게 사용할 수 있습니다.
모듈 상태가 복잡해질수록 불변성을 지키기 까다로워지는데 immer 라이브러리는 이런 경우에 활용하기 좋다. 다만, 상태값들을 하나의 객체 안에 묶어서 넣는 것이 가독성을 높이는데 유리하며, 추후 컴포넌트에 리덕스를 연동할 때도 더 편리하기 때문에 redux-action 라이브러리 활용방법까지만 알아볼 것이다. (객체의 구조가 복잡해지거나 객체로 이루어진 배열을 다뤄야할 경우 immer 라이브러리를 공부할 것)
// todos 모듈
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;
위에서 작성했던 todos 모듈을 redux-action 라이브러리와 비구조화 할당을 사용하여 가독성 있게 개선한 코드이다.
💡 본 포스팅은 리액트를 다루는 기술 17장 리덕스를 사용하여 리액트 애플리케이션 상태 관리하기 를 참고하였습니다.
'React' 카테고리의 다른 글
granularity of error boundaries ⎯ fault tolerance by. Brandon Dail (0) | 2024.06.04 |
---|---|
React | 리덕스 키워드 정리 (0) | 2022.03.18 |
React | 구글 리캡챠(reCaptcha) v2 사용하기 (0) | 2022.02.25 |
React | 카카오 맵 API 사용하여 지도 띄우기 (0) | 2022.02.23 |
React | do not nest ternary expression (Nextjs , typescript, airbnb style guide) (0) | 2022.02.10 |