리덕스
사용률이 가장 높은 상태관리 라이브러리
ContextAPI + useReducer을 사용한 개발흐름과 유사하다.
Context와의 차이점
1. 미들웨어
비동기 작업을 더욱 체계적으로 관리가 가능하다.
2. 유용한 함수, Hooks
라이브러리 내 함수들과 Hook을 지원받을 수 있다.
ex) connect,useSelector,useDispatch ...
3. 기본적인 최적화가 이미 되어있다.
4. 하나의 커다란 상태
모든 글로벌상태를 큰곳에 넣어서 사용
5. DevTools
여러 도구가 있다.
6. 이미 사용중인 프로젝트가 많음
리덕스의 사용시기?
프로젝트가 클수록, 비동기 작업을 할수록, 리덕스가 편할수록 리덕스를 사용하면 된다.
사용 키워드들
액션
상태변화가 필요하게 될때 사용
{
type : "TOGGLE",
data : {
id : 0.
}
}
type은 필수로 들어가야 한다!
액션 생성함수
액션을 생성해주는 함수
export function addTodo(data) {
return {
type: "ADD_TODO",
data
};
}
// 화살표 함수로도 만들 수 있음
export const changeInput = text => ({
type: "CHANGE_INPUT",
text
});
리듀서(Reducer)
변화를 일으키는 함수
루트리듀서와 서브리듀서로 나누어져 있다.
function reducer(state, action) {
// 상태 업데이트 로직 (스위치 케이스문)
return alteredState;
}
스토어(Store)
리덕스에서는 한 애플리케이션당 하나의 스토어를 만들게 됩다. 스토어 안에는, 현재의 앱 상태와, 리듀서가 들어가있고, 추가적으로 몇가지 내장 함수들이 있다
디스패치(dispatch)
액션을 리듀서로 전달한다
구독(subscribe)
액션이 디스패치될때마다 설정된 함수가 실행
리덕스의 3가지 규칙
1. 하나의 애플리케이션엔 하나의 스토어가 존재 (여러개의 스토어 X)
2. 상태는 읽기전용 (불변성을 지키자)
3. 변화를 일으키는 함수 Reducer는 순수한 함수여야 함.
> 이전 상태는 변경하지 않고 새로운 상태객체를 만들어 봔한해야함
> 같은 인풋이 들어간다면 같은 아웃이 나와야함.
그럼이제 리덕스를 활용하여 간단한 예제를 구현해 보자.
우선 리액트 프로젝트를 하나 만들고
yarn add redux
리덕스 라이브러리를 설치해 주자
//exercise.js
import {createStore} from 'redux';
const initialState = {
counter : 0,
text : '',
list : []
};
const INCREASE = 'INCREASE';
const DECREASE = 'DECREASE';
const CHANGE_TEXT = 'CHANGE_TEXT';
const ADD_TO_LIST = 'ADD_TO_LIST';
const increase = () => ({
type : INCREASE,
});
const decrease = () => ({
type : DECREASE,
});
const changeText = (text) => ({
type : CHANGE_TEXT,
text
});
const addToList = (item) => ({
type : ADD_TO_LIST,
item
});
function reducer(state = initialState,action) { //state초기상태를 넣어주어야 한다.
switch ( action.type ) {
case INCREASE:
return {
...state,
counter : state.counter + 1
};
case DECREASE:
return {
...state,
counter : state.counter -1
}
case CHANGE_TEXT:
return {
...state,
text : action.text
}
case ADD_TO_LIST:
return {
...state,
list : state.list.concat(action.item)
}
default :
return state;
}
}
const store = createStore(reducer);
console.log(store.getState())
const listener = () => {
const state = store.getState();
console.log(state);
};
const unsubscribe = store.subscribe(listener); //구독해제
store.dispatch(increase());
store.dispatch(decrease());
store.dispatch(changeText('나는정민규'));
store.dispatch(addToList({id : 1, text : 'mingyu'}));
window.store = store; //콘솔창에서 볼 수 있게
위가 리덕스의 작동방식은 간략히 설명해준 예시이다.
리듀서를 사용해서 기능들을 정의해준 후에 dispatch가 실행될 때마다 해당하는 함수가 실행되는것을 볼 수 있다.
처음 코드로 실행한것과, 콘솔창에서 입력한 값이 잘 출력되는것을 확인할 수 있다.
리덕스 모듈 만들기
액션타입, 액션 생성함수, 리듀서가 모두 들어가있는 자바스크립트 파일
Ducks패턴
한 파일에 액션타입,액션 생성함수, 리듀서를 모두 작성하는 방식
src파일 안에 아래와 같은 파일구조를 만들어보자
//counter.js
const SET_DIFF = 'counter/SET_DIFF'; //액션
const INCREASE = 'counter/INCREASE';
const DECREASE = 'counter/DECREASE';
export const setDiff = diff => ({ type : SET_DIFF, diff}); //액션생성함수
export const increase = () => ({ type : INCREASE });
export const decrease = () => ({ type : DECREASE });
const initialState ={ //초기상태
number : 0,
diff : 1
};
export default function counter(state = initialState, action) { //리듀서
switch ( action.type ) {
case SET_DIFF:
return {
...state,
diff : action.diff
};
case INCREASE:
return {
...state,
number : state.number + state.diff
};
case DECREASE:
return {
...state,
number : state.number - state.diff
};
default:
return state
}
}
//todos.js
const ADD_TODO = 'todos/ADD_TODO';
const TOGGLE_TODO = 'todos/TOGGLE_TODO';
let nextId = 1;
export const addTodo = (text) => ({
type : ADD_TODO,
todo : {
id : nextId ++,
text
}
});
export const toggleTodo = id => ({
type : TOGGLE_TODO,
id
});
const initialState = [
];
export default function todos(state = initialState,action) {
switch (action.type) {
case ADD_TODO:
return state.concat(action.todo);
case TOGGLE_TODO:
return state.map(
todo => todo.id === action.id
? { ...todo, done : !todo.done}
: todo
)
default:
return state;
}
}
리덕스 두개를 만들어준 후에, index.js안에서 합쳐볼 것이다.
//index.js
import { combineReducers } from "redux";
import counter from "./counter";
import todos from "./todos";
const rootReducer = combineReducers({
counter,
todos
});
export default rootReducer;
리덕스를 합쳐서 쓰기 위해서는
yarn add react-redux
해당 라이브러리가 필요하다.
그 이후, src파일내가아닌 root파일의 index.js에서 Provider,createStore,rootReducer를 불러와준 후에 기존 <App>을 Provider로 감싸주면 적용이 된다.
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
import './exercise';
import { Provider } from 'react-redux';
import { createStore } from 'redux';
import rootReducer from './modules';
const store = createStore(rootReducer);
console.log(store.getState());
ReactDOM.render(
<React.StrictMode>
<Provider store={store}>
<App />
</Provider>
</React.StrictMode>,
document.getElementById('root')
);
// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();
콘솔창을 보면 초기 값들이 잘 들어있음을 확인할 수 있다.
간단한 카운터 예제
컴포넌트를 위처럼 두개로 나누어서 작성할 수 있다.
//Counter.js
import React from 'react';
function Counter( { number, diff, onIncrease, onDecrease, onSetDiff }) {
const onChange = e => {
onSetDiff(parseInt(e.target.value,10));
};
return (
<div>
<h1>{number}</h1>
<div>
<input type = 'number' value = {diff} onChange={onChange} />
<button onClick = {onIncrease}>+</button>
<button onClick = {onDecrease}>-</button>
</div>
</div>
);
}
export default Counter;
컴포넌트 파일안에서는 단순히 props를 받아와서 화면에 표시해주는 역할에 충실한다.
//CounterContainer.js
import React from 'react';
import Counter from '../components/Counter';
import {useSelector, useDispatch} from 'react-redux';
import {increase, decrease, setDiff} from '../modules/counter'
function CounterContainer() {
const {number, diff} = useSelector(state => ({ //리덕스의 현재 상태를 조회
number : state.counter.number,
diff : state.counter.diff
}))
const dispatch = useDispatch();
const onIncrease = () => dispatch(increase());
const onDecrease = () => dispatch(decrease());
const onSetDiff = diff => dispatch(setDiff(diff));
return (
<div>
<Counter
number={number}
diff={diff}
onIncrease={onIncrease}
onDecrease={onDecrease}
onSetDiff={onSetDiff}
/>
</div>
);
}
export default CounterContainer;
컨테이너에서는 현재 상태를 조회해서 그 기능들을 Counter로 넘겨주는 역할을 한다.
import React from "react";
import CounterContainer from "./containers/CounterContainer";
function App() {
return (
<CounterContainer/>
);
}
export default App;
이제 App.js에서 위 함수를 호출하기만 하면 간단한 카운터 예제가 구현이 된다.
리덕스 개발자 도구 적용하기
현재 상태를 개발자 도구에서 확인할 수 있고, 상태 변화나 상태를 뒤로 돌리는 등의 기능이 가능하다.
https://chrome.google.com/webstore/detail/redux-devtools/lmhkpmbekcpmknklioeibfkpmmfibljd/related
위 페이지에서 모듈추가를 해준 다음에
yarn add redux-devtools-extension
라이브러리를 추가해 준다.
import { composeWithDevTools} from 'redux-devtools-extension';
const store = createStore(rootReducer, composeWithDevTools()); //리덕스 개발자 도구 적용
index.js에서 위처럼 도구를 적용시키면
개발자 도구에서 Redux관련 칸이 생긴것을 알 수 있다.
그러면 저번에 만들어봤던 TodoList예제를 리덕스를 이용해서 만들어보자.
//Todos.js
import React, {useState} from 'react';
function TodoItem({todo, onToggle}) {
return(
<li
style={{
textDecoration : todo.done ? 'line-through' : 'none'
}}
onClick={() => onToggle(todo.id)}
>
{todo.text}
</li>
)
}
function TodoList({todos, onToggle}) {
return (
<ul>
{
todos.map(todo => <TodoItem
key = {todo.id}
todo ={todo}
onToggle={onToggle}
/>
)
}
</ul>
)
}
function Todos({todos, onCreate, onToggle}) {
const [text, setText] = useState('');
const onChange = e => setText(e.target.value);
const onSubmit = e => {
e.preventDefault(); //새로고침 방지
onCreate(text);
setText('');
}
return (
<div>
<form onSubmit={onSubmit}>
<input value = {text} onChange={onChange} placeholder='할일을 입력하세요..'/>
<button type = "submit">등록</button>
</form>
<TodoList
todos = {todos}
onToggle={onToggle}
/>
</div>
)
}
export default Todos;
크게 각 아이템을 개별관리하는 Todoitem과 할일들의 리스트를 출력해주는 TodoList, 그리고 상태관리와 등록기능을 담당하는 Todos컴포넌트로 구성되어 있다.
그 다음에 카운터 예제에서처럼 Container부분을 작성해준다.
//TodosContainer.js
import {React, useCallback} from 'react';
import Todos from '../components/Todos';
import {useSelector, useDispatch } from 'react-redux';
import { addTodo, toggleTodo } from '../modules/todos';
function TodosContainer() {
const todos = useSelector(state => state.todos);
const dispatch = useDispatch();
const onCreate = useCallback(text => dispatch(addTodo(text)),[dispatch]); //
const onToggle = useCallback(id => dispatch(toggleTodo(id)),[dispatch]);
return <Todos
todos = {todos}
onCreate = {onCreate}
onToggle = {onToggle}
/>
}
export default TodosContainer;
이전에 module을 만들때 만들어두었던 것을 활용한다.
import React from "react";
import CounterContainer from "./containers/CounterContainer";
import TodosContainer from "./containers/TodosContainer";
function App() {
return (
<div>
<CounterContainer/>
<hr />
<TodosContainer />
</div>
);
}
export default App;
다음 App.js에서 불러오기만 하면 된다.
카운터 예제 아래에 잘 구현이 된 것을 알 수 있다.
컴포넌트 최적화
이전에 배운 React.memo를 사용하면 불필요한 랜더링을 막아줄 수 있다.
const TodoItem = React.memo(function ({todo, onToggle}) {
return(
<li
style={{
textDecoration : todo.done ? 'line-through' : 'none'
}}
onClick={() => onToggle(todo.id)}
>
{todo.text}
</li>
)
})
TodoItem과 List를 memo로 감싸주면 된다.
개발자도구의 Profiler를 보면 불필요한 랜더링이 이루어지지 않았음을 확인할 수 있다.
useSelector 최적화
const {number, diff} = useSelector(state => ({ //리덕스의 현재 상태를 조회
number : state.counter.number,
diff : state.counter.diff
}))
현재 Count 예제는 위처럼 새객체를 계속 만들어내기 때문에 Counter말고 아래의 Todo를 건드려도 리랜더링이 되고있다.
두가지방법이 있다.
첫번째 방법은 useSelector을 여러번 사용하는것이다.
const number = useSelector(state => state.counter.number);
const diff = useSelector(state => state.counter.diff);
두번째 방법은 shallowEqual을 이용해서 비교할 수 있다.
const {number, diff} = useSelector(state => ({ //리덕스의 현재 상태를 조회
number : state.counter.number,
diff : state.counter.diff
}), shallowEqual )
//(left,right) => {
// return left.diff === right.diff && left.number === right.number; //shallowEqual
shallowEqual은 객체 내를 비교해주는 함수이다.
connect Hoc
사실 쓸일이 별로 없긴 하지만, 구형 컴포넌트나 이런저런 경우로 사용할 때가 있을 수 있다.
Props를 통해 리덕스의 상태 또는 액션을 디스패치하는 함수를 받아온다.
HOC???
재사용되는값, 함수를 Props로 받아올 수 있게 해주는 옛날 패턴
그럼 위에 했던 예제들을 connect로 바꾸어 보자.
//CounterContainer.js
import React from 'react';
import Counter from '../components/Counter';
import {connect} from 'react-redux';
import {increase, decrease, setDiff} from '../modules/counter'
import {bindActionCreators} from 'redux';
function CounterContainer({
number,
diff,
increase,
decrease,
setDiff
}) {
return (
<div>
<Counter
number={number}
diff={diff}
onIncrease={increase}
onDecrease={decrease}
onSetDiff={setDiff}
/>
</div>
);
}
const mapStateToProps = (state) => ({
number : state.counter.number,
diff : state.counter.diff,
})
//첫번째 방법
// const mapDispatchToProps = dispatch => ({
// Increase : () => dispatch(increase()),
// Decrease : () => dispatch(decrease()),
// SetDiff : (diff) => dispatch(setDiff(diff))
// })
//두번째 방법
// const mapDispatchToProps = dispatch => bindActionCreators({
// increase,
// decrease,
// setDiff
// }, dispatch) //bindActionCreators사용
const mapDispatchToProps = { //함수가 아닌 객체면 bindActionCreators가 자동으로 진행
increase,
decrease,
setDiff,
}
export default connect(mapStateToProps, mapDispatchToProps)(CounterContainer);
바꾸는 법은 간단하다. connect를 불러오고 Props로 값들을 불러와서 넣어주면 된다. 이때 mapDispatchToProps를 보면 알 수 있듯이 일일이 번거롭게 쓰지않아도 객체로 만들어주면 반복되는 코드를 많이 줄일 수 있다.
Todos도 바꾸어보자
//TodosContainer.js
import {React, useCallback} from 'react';
import {connect} from 'react-redux';
import Todos from '../components/Todos';
import { addTodo, toggleTodo } from '../modules/todos';
function TodosContainer({ todos, addTodo, toggleTodo }) {
const onCreate = useCallback(text => addTodo(text),[addTodo]);
const onToggle = useCallback(id => toggleTodo(id),[toggleTodo]);
return <Todos
todos = {todos}
onCreate = {onCreate}
onToggle = {onToggle}
/>
}
const mapStateToProps = state => ({ todos : state.todos});
const mapDispatchToProps = {
addTodo,
toggleTodo
};
export default connect(
mapStateToProps,
mapDispatchToProps,
)(TodosContainer);
'FrontEnd > React' 카테고리의 다른 글
23_리액트 리덕스 미들웨어(2) (0) | 2022.01.05 |
---|---|
22_리덕스 미들웨어 (0) | 2022.01.05 |
20_리액트_라우터 (0) | 2021.12.30 |
19_리액트 API연동 (0) | 2021.12.29 |
18_styled-components (0) | 2021.12.27 |