개념 미리 정리하기
< 액션 >
상태에 어떠한 변화가 필요하면 액션(action)이란 것이 발생합니다. 이는 하나의 객체로 표현된다.
그리고 액션 객체는 type필드를 반드시 가지고 있어야 합니다.
{
type: 'ADD_TODO',
data: {
id: 1,
text: '리덕스 배우기'
}
}
< 액션 생성 함수 >
function addTodo(data) {
return {
type: 'ADD_TODO',
data
}
}
액션 생성 함수는 액션 객체를 만들어 주는 함수입니다.
< 리듀서 >
리듀서는 변화를 일으키는 함수입니다. 리듀서가 현재 상태와 전달받은 액션 객체를 파라미터로 받아 옵니다. 그리고 두 값을 참고하여 새로운 상태를 만들어서 반환해 줍니다.
const initialState = {
counter: 1;
};
function reducer(state = initialState, action) {
switch((action.type) {
case INCREMENT:
return {
counter: state.counter + 1;
};
default:
return state;
}
}
< 스토어 >
프로젝트에 리덕스를 적용하기 위해 스토어를 만듭니다. 한 개의 프로젝트는 단 하나의 스토어만 가질 수 있습니다. 스토어 안에는 현재 애플리케이션 상태와 리듀서가 들어가 있으며, 그 외에도 몇 가지 중요한 내장 함수를 가집니다.
< 디스패치 >
디스패치는 스토어의 내장함수 중 하나입니다. 디스패티는 '액션을 발생시키는 것'이라고 이해하면 됩니다. dispatch(action)과 같은 형태로 액션 객체를 파라미터로 넣어서 호출합니다.
이 함수가 호출되면 스토어는 리듀서 함수를 실행시켜서 새로운 상태를 만들어 줍니다.
< 구독 >
구독도 스토어의 내장 함수 중 하나입니다. subscribe 함수 안에 리스너 함수를 파라미터로 넣어서 호출해 주면, 이 리스너 함수가 액션이 디스패치되어 상태가 업데이트 될 때마다 호출됩니다.
리덕스의 세 가지 규칙
< 단일 스토어 >
하나의 애플리케이션 안에는 하나의 스토어가 들어 있습니다.
< 읽기 전용 상태 >
리덕스 상태는 읽기 전용입니다. 기존에 리액트에서 setState를 사용하여 state를 업데이터 할 떄도 객체나 배열을 업데이터하는 과정에서 불변성을 지켜 주기 위해 spread연산자를 사용하거나 immer와 같은 불변성 관리 라이브러리를 사용했습니다. 리덕스도 상태를 업데이트 할 때는 기존의 객체는 건드리지 않고 새로운 객체를 생성해 주어야 합니다.
< 리듀서는 순수한 함수 >
- 리듀서 함수는 이전 상태와 액션 객체를 파라미터로 받습니다.
- 파라미터 외의 값에는 의존하면 안됩니다.
- 이전 상태는 절대로 건드리지 않고, 변화를 준 새로운 상태 객체를 만들어서 반환합니다.
- 똑같은 파라미터로 호출된 리듀서 함수는 언제나 똑같은 결과 값을 반환해야 합니다.
리덕스를 사용하여 리액트 애플리케이션 상태 관리하기
< UI 준비하기 >
리액트 프로젝트에서 리덕스를 사용할 때 가장 많이 사용하는 패턴은 프레젠테이셔널 컴포넌트와 컨테이너 컴포넌트를 분리하는 것입니다. 여기서 프레젠테이셔널 컴포넌트란 주로 상태 관리가 이루어지지 않고, 그저 props를 받아 와서 화면에 UI를 보여 주기만 하는 컴포넌트를 말합니다. 이와 달리 컨테이너 컴포넌트는 리덕스와 연동이 되어 있는 컴포넌트로, 리덕스로부터 상태를 받아오기도 하고 리덕스 스토어에 액션을 디스패치 하기도 합니다.
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;
import React from "react";
const Counter = ({ number, onInccrease, onDecrease }) => {
return (
<div>
<h1>{number}</h1>
<div>
<button onClick={onInccrease}>+1</button>
<button onClick={onDecrease}>-1</button>
</div>
</div>
);
};
export default Counter;
import React from "react";
const TodoItem = ({ todo, onToggle, onRemove }) => {
return (
<div>
<input type="checkbox" />
<span>예제 텍스트</span>
<button>삭제</button>
</div>
);
};
const Todos = () => {
const onSubmit = (e) => {
e.preventDefault();
};
return (
<div>
<form onSubmit={onSubmit}>
<input />
<button type="submit">등록</button>
</form>
<div>
<TodoItem />
<TodoItem />
<TodoItem />
<TodoItem />
<TodoItem />
<TodoItem />
</div>
</div>
);
};
export default Todos;
< 리덕스 관련 코드 작성하기 >
* Ducks방식을 사용합니다.
- counter module
// src/modules/counter.js
// 앞에 모듈 이름을 붙여 주는 것은 관례에고 익션 이름을 대문자로 하는 것 또한 관례입니다.
// 액션 타입 정의
const INCREASE = "counter/INCREASE";
const DECREASE = "counter/DECREASE";
// 액션 생성 함수 만들기
// 추후 다른 파일에서도 사용하기 위해 export해준다.
export const increase = () => ({ type: INCREASE });
export const decrease = () => ({ type: DECREASE });
// 초기 상태 => number = 0
const initialState = {
number: 0,
};
const 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 module
// src/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,
},
],
};
const todos = (state = initialState, action) => {
switch (action.type) {
case CHANGE_INPUT:
return {
...state,
input: action.input,
};
case INSERT:
return {
...state,
todos: [...todos, action.todos],
};
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;
2. 루트 리듀서 만들기
// src/modules/index.js
import { combineReducers } from "redux";
import counter from "./counter";
import todos from "./todos";
const rootReducer = combineReducers({
counter,
todos,
});
export default rootReducer;
< 리액트 애플리케이션에 리덕스 적용하기 >
1. 스토어 만들기
// src/index.js
import React from "react";
import ReactDOM from "react-dom";
import App from "./App";
import { createStore } from "redux";
import rootReducer from "./modules";
import { Provider } from "react-redux";
import { composeWithDevTools } from "redux-devtools-extension";
const store = createStore(rootReducer, composeWithDevTools());
ReactDOM.render(
<Provider store={store}>
<React.StrictMode>
<App />
</React.StrictMode>
</Provider>,
document.getElementById("root")
);
리액트 컴포넌트에서 스토어를 사용할 수 있도록 App컴포넌트를 react-redux에서 제공하는 Provider컴포넌트로 감싸 줍니다.
또한 Redux DevTools를 사용해 주기 위해 'redux-devtools-extension'을 사용해 줍니다.
< 컨테이너 컴포넌트 만들기 >
1. CounterContainer
// src/containers/CounterContainer.js
import React from "react";
import Counter from "../components/Counter";
import { connect } from "react-redux";
const CounterContainer = ({ number, increase, decrease }) => {
return <Counter number={number} onIncrease={increase} onDecrease={decrease} />;
};
const mapStateToProps = (state) => ({
number: state.counter.number,
});
const mapDispatchToProps = (dispatch) => ({
increase: () => {
console.log("increase");
},
decrease: () => {
console.log("decrease");
},
});
export default connect(mapStateToProps, mapDispatchToProps)(CounterContainer);
위 컴포넌트를 리덕스와 연동하기 위해 react-redux에서 제공하는 connect함수를 사용해야 합니다.
이 함수는 connect(mapStateToProps, mapDispatchToProps)(연동할 컴포넌트)
mapStateToProps => 리덕스 스토어 안의 상태를 컴포넌트의 props로 넘겨주기 위해 사용
mapDispatchToProps => 액션 생성 함수를 컴포넌트의 props로 넘겨주기 위해 사용
// src/containers/CounterContainer.js
import React from "react";
import Counter from "../components/Counter";
import { connect } from "react-redux";
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);
또한 action생성함수를 받아오고 dispatch를 해주는 것으로 increase와 decrease를 바꾸어 줍니다. 또한 우리는 컴포넌트에서 액션을 디스패치하기 위해 각 액션 생성 함수를 호출하고 dispatch로 감싸는 작업이 조금 번거로울 수 있기 때문에, 리덕스에서 제공하는 bindActionCreator 유틸 함수를 사용하면 간편해집니다.
// src/containers/CounterContainer.js
import React from "react";
import Counter from "../components/Counter";
import { connect } from "react-redux";
import { increase, decrease } from "../modules/counter";
import { bindActionCreators } from "redux";
const CounterContainer = ({ number, increase, decrease }) => {
return <Counter number={number} onIncrease={increase} onDecrease={decrease} />;
};
export default connect(
({ counter }) => ({ number: counter.number }),
(dispatch) =>
bindActionCreators(
{
increase,
decrease,
},
dispatch
)
)(CounterContainer);
이와같이 편리하게 쓸 수도 있습니다.
하지만 더 편한 방법이 있는데 바로 mapDispatchToProps에 해당하는 파라미터를 함수 형태가 아닌 액션 생성 함수로 이루어진 객체 형태로 넣어 주는 것입니다.
// src/containers/CounterContainer.js
import React from "react";
import Counter from "../components/Counter";
import { connect } from "react-redux";
import { increase, decrease } from "../modules/counter";
import { bindActionCreators } from "redux";
const CounterContainer = ({ number, increase, decrease }) => {
return <Counter number={number} onIncrease={increase} onDecrease={decrease} />;
};
export default connect(({ counter }) => ({ number: counter.number }), {
increase,
decrease,
})(CounterContainer);
위와같이 connect의 두번째 파라미터를 아예 객체 형태로 넣어 주면 내부적으로 bindActionCreator작업을 대신해줍니다.
2. TodosContainer
// src/containers/TodosContainer.js
import React from "react";
import { connect } from "react-redux";
import Todos from "../components/Todos";
import { changeInput, insert, toggle, remove } from "../modules/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 }) => ({
input: todos.input,
todos: todos.todos,
}),
{
changeInput,
insert,
toggle,
remove,
}
)(TodosContainer);
// src/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;
이와 같이 변경하였더니 정상 작동 하는 것을 볼 수 있습니다.
'React > ReactJs' 카테고리의 다른 글
React - Redux MiddleWare >> default + redux-thunk (0) | 2022.02.22 |
---|---|
React - redux ( util_Version ) (0) | 2022.02.21 |
React - Context API (0) | 2022.02.21 |
styled-component의 동작방식 (0) | 2022.02.21 |
SPA기반 SSR구현하기 - 3 (0) | 2022.02.20 |