- 리덕스를 사용할 때는 액션 타입, 액션 생성 함수, 리듀서 코드를 작성해야 함
- 이 코드를 각각 다른 파일에 작성해도되고 기능별로 묶어서 파일 하나에 작성해도 됨
- actions, constants, reducers라는 세 개의 디렉터리를 만드는 방식
- 편리하지만 새로운 액션을 만들 때 세 종류의 파일을 모두 수정해야 하여 불편함
- 리덕스 공식 문서에서 사용되는 가장 일반적인 방식
Ducks 패턴
- 액션 타입, 액션 생성함수, 리듀서 함수를 기능별로 파일 하나에 몰아서 다 작성하는 방식
- Ducks 패턴을 사용해 액션 타입, 액션 생성 함수, 리듀서를 작성한 코드를 '모듈' 이라고 한다
[counter 모듈 ( modules/counter.js]
1. 액션 타입을 정의
- 액션 타입은 대문자로 정의
- 문자열 내용은 '모듈 이름/액션 이름'과 같은 형태로 작성
- 문자열 안에 모듈 이름을 넣음으로써 나중에 프로젝트가 커졌을 때 액션의 이름이 충돌되지 않게 해줌
const INCREASE = 'counter/INCREASE';
const DECREASE = 'counter/DECREASE';
2. 액션 생성 함수 만들기
주의해야 할 점 : 앞 부분에 export라는 키워드가 들어가므로써 추후 이 함수를 다른 파일에서 사용 가능
export const increase = () => ({type : INCREASE});
export const decrease = () => ({type : DECREASE});
3. 초기 상태 및 리듀서 함수 만들기
- 초기 상태에는 number 값을 설정해줌
- 리듀서 함수에는 현재 상태를 참조하여 새로운 객체를 생성해서 반환하는 코드를 작성
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;
}
}
전체 코드
// 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;
}
}
export default counter;
export : 여러개를 내보낼 수 있음
export default : 단 한개만 내보낼 수 있음
→ 불러오는 방식도 다름
import counter from './counter'; import {increase, decrease} from './counter'; // 한꺼번에 불러오고 싶을 때 import counter, {increase, decrease} from './counter';
[counter 모듈 ( modules/counter.js]
- createAction을 사용해 매번 객체를 만들어 줄 필요 없이 간단하게 액션 생성 함수를 선언
- handleActions을 사용해 리듀서 함수를 가독성 높게 작성 가능
import {createAction, handleActions} from 'redux-actions';
// 1. 액션 타입 정의
const CHANGE_INPUT = "todos/CHANGE_INPUT"; // 인풋 값을 변경함
const INSERT = 'todos/INSERT'; // 새로운 todo를 등록함
const TOGGLE = 'todos/TOGGLE'; // todo를 체크/체크 해제함
const REMOVE = 'todos/REMOVE'; // todo를 제거함
// 2. 액션 생성 함수 만들기
// 액션 생성 함수에서 파라미터가 필요함
// 전달 받은 파라미터는 액션 객체 안에 추가 필드로 들어가게 됨
export const changeInput = createAction(CHANGE_INPUT, input => input);
// 액션 객체를 만들 때 파라미터 외에 사전에 이미 선언되어 있는 id값에 의존함
// 이 액션 생성 함수(insert 함수)는 호출될 때마다 id값에 1씩 더해줌
// 이 id값은 각 todo 객체가 들고 있게 될 고유값
let id = 3;
export const insert = createAction(INSERT, text => ({
id: id++,
text,
done: false,
}));
// export const insert = text => ({
// type : INSERT,
// todo : {
// id : id++,
// text,
// done: false
// }
// });
export const toggle = createAction(TOGGLE, id => id);
export const remove = createAction(REMOVE, id => id);
// export const toggle = id => ({
// type : TOGGLE,
// id
// });
// export const remove = id => ({
// type : REMOVE,
// id
// });
// 3. 초기 상태 및 리듀서 함수 만들기
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,
);
// 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;
4. 루트 리듀서 만들기
- 여러개 만든 리듀서를 combineReducer라는 유틸 함수를 사용해 하나로 합쳐줌
- createStore함수를 사용해 스토어를 만들 때는 리듀서를 하나만 사용해야 하기 때문
// modules/index.js
import { combineReducers } from 'redux';
import counter from './counter';
import todos from './todos';
const rootReducer = combineReducers({
counter,
todos,
});
export default rootReducer;
[리액트 애플리케이션에 리덕스 적용]
1. 스토어 만들기
2. Provider 컴포넌트를 사용해 프로젝트에 리덕스 적용시킴
- 리액트 컴포넌트에서 스토어를 사용할 수 있도록 Provider 컴포넌트로 감싸 줌
- 이 컴포넌트를 사용할 때는 store를 props로 전달해줘야함
// src/index.js
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
import './index.css';
import { BrowserRouter } from "react-router-dom";
import { createStore } from 'redux';
import rootReducer from './modules';
import {Provider} from 'react-redux';
import { composeWithDevTools } from 'redux-devtools-extension'; // 리덕스 개발자 도구
const store = createStore(rootReducer, composeWithDevTools()); // 스토어를 만듭니다.
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<React.StrictMode>
<BrowserRouter>
<Provider store={store}>
<App />
</Provider>
</BrowserRouter>
</React.StrictMode>
);
[컨테이너 컴포넌트 만들기]
- 리덕스 스토어에 접근해 원하는 상태를 받아 오고, 액션도 디스패치 해줘야함
- 리덕스 스토어와 연동된 컴포넌트를 컨테이너 컴포넌트라고 부름
// containers/CounterContainer.js
import {useCallback} from 'react';
import { connect } from 'react-redux';
import Counter from '../components/Counter';
import {increase, decrease} from '../modules/counter';
import {useSelector, useDispatch} from 'react-redux';
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;
<!--
const mapStateToProps = state => ({
number: state.counter.number,
});
const mapDispatchToProps = dispatch => ({
// 액션 생성 함수를 불러와 액션 객체를 만들고 디스패치해줌
increase: () => {
dispatch(increase());
},
decrease: () => {
dispatch(decrease());
},
});
두번째 파라미터를 아예 객체 형태로 넣어 주면 connect함수가 내부적으로 bindActionCreators 작업을 대신 해줌
export default connect(
state => ({
number: state.counter.number,
}),
{
increase,
decrease,
},
)(CounterContainer);
mapDispatchToProps에 해당하는 파라미터를 함수 형태가 아닌 액션 생성 함수로 이루어진 객체 형태로 넣어줌
-->
컴포넌트를 리덕스와 연동하려면 connect 함수를 사용해야 함
const makeContainer = connect(mapStateToProps, mapDispatchToProps) makeContainer(타깃 컴포넌트)
- mapStateToProps : 리덕스 스토어 안의 상태를 컴포넌트의 props로 넘겨주기 위해 설정하는 함수
- mapDispatchToProps : 액션 생성 함수를 컴포넌트의 props로 넘겨주기 위해 사용하는 함수
→ connect 함수를 호출하고 나면 또 다른 함수를 반환
→ 반환된 함수에 컴포넌트를 파라미터로 넣어 주면 리덕스와 연동된 컴포넌트가 만들어짐
[ TodosContainer 만들기]
// containers/TodosContainer.js
import { connect } from 'react-redux';
import { changeInput, insert, toggle, remove } from '../modules/todos';
import Todos from '../components/Todo';
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);
Hooks를 사용해 컨테이너 컴포넌트 만들기
1. useSelector로 상태 조회useSelector Hook을 사용하면 connect 함수를 사용하지 않고도 리덕스의 상태를 조회할 수 있음
const 결과 = useSelector(상태 선택 함수);
2. useDispatch를 사용해 액션 디스패치하기
컴포넌트 내부에서 스토어의 내장 함수 dispatch를 사용할 수 있게 해줌
→ 컨테이너 컴포넌트에서 액션을 디스패치해야한다면 이 Hook을 사용하면 됨
→ 컴포넌트 성능을 최적화해야 하는 상황이 온다면 useCallback으로 액션을 디스패치하는 함수를 감싸주는게 좋음
// containers/CounterContainer.js
import {useCallback} from 'react';
import { connect } from 'react-redux';
import Counter from '../components/Counter';
import {increase, decrease} from '../modules/counter';
import {useSelector, useDispatch} from 'react-redux';
const CounterContainer = () => {
//1. connect 함수 대신 useSelector를 사용해 counter.number값을 조회함으로써 counter에게 props를 넘겨줌
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}
/>
);
};
connect 함수와 리덕스 관련 Hook(useSelector, useDispatch)의 주요 차이점
- connect 함수를 사용해 컨테이너 컴포넌트를 만들었을 경우, 해당 컨테이너 컴포넌트의 부모 컴포넌트가 리렌더링될 때 해당 컨테이너 컴포넌트의 props가 바뀌지 않았다면 리렌더링이 자동으로 방지되어 성능이 최적화됨
- 반면 useSelector를 사용해 리덕스 상태를 조회했을 때는 이 최적화 작업이 자동으로 이루어지지 않으므로, 성능 최적화를 위해서는 React.memo를 컨테이너 컴포넌트에 사용해주어야 한다.
- 이번 예시(TodosContainer)에서는 부모 컴포넌트인 App 이 리렌더링되는 일이 없으므로 불필요한 최적화임
총 코드
https://github.com/yeeun426/recordMe_web
GitHub - yeeun426/recordMe_web
Contribute to yeeun426/recordMe_web development by creating an account on GitHub.
github.com
'React' 카테고리의 다른 글
[React] 코드 스플리팅 (1) | 2023.05.01 |
---|---|
[React] 리덕스 미들웨어를 통한 비동기 작업 관리 (0) | 2023.04.14 |
[React] 리덕스 라이브러리 (0) | 2023.04.05 |
[React] Context API (0) | 2023.04.05 |
[React] 라우팅 (0) | 2023.03.30 |