하나의 상태 저장소와 이를 변경하는 Dispatcher를 통해 단방향 순환 구조를 갖는 것.
npm install --save redux
혹은 cdn을 통해 삽입할 수 있다.
따라서 리덕스는 아래와 같이 작동하게 된다.
//reducer 함수를 선언한다.
function reducer(state,action){
//state 초기화
if(state === undefined){
return{
max_id:2,
mode:'create',
selected_id:1,
contents:[
{id:1, title:"HTML", desc:'HTML is ...'},
{id:2, title:"CSS", desc:"CSS is ..."}
]
}
}
var newState={};
if(action.type === 'SELECT'){
newState = Object.assign({},state,{selected_id:action.id, mode:'read'} );
}
else if(action.type === 'CREATE'){
..
}
else if(action.type === 'DELETE'){
...
}
else if(action.type === 'CHANGE_MODE') {
...
}
return newState;
}
//Redux.createStore(reducer)를 통해 새로운 store를 만든다.
var store = Redux.createStore(reducer);
function article() {
//store에 저장된 state를 가져온다.
var state = store.getState();
if (state.mode === "create") {
//html에서 submit 이벤트가 발생하면
//store.dispatch에 action객체를 주입한다
document.querySelector("#content").innerHTML = `
<article>
<form>
<p>
<input type="text" name="title" placeholder="title">
</p>
<p>
<textarea name="desc" placeholder="description"></textarea>
</p>
<p>
<input type="submit">
</p>
</form>
</article>
`;
}
}
// dispatch가 리듀서 함수를 호출하여 store의 state를 변경하게 된다.
// store.subscribe를 통해 store가 변경될 때마다 글의 내용과 목차의 DOM를 변경한다.
store.subscribe(article);
store.subscribe(TOC);
<!-- https://opentutorials.org/module/4078 -->
<!-- https://velog.io/@annie1004619/Redux-생활코딩 -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/redux/4.0.5/redux.min.js"></script>
</head>
<body>
<div id="subject"></div>
<div id="toc"></div>
<div id="control"></div>
<div id="content"></div>
<script>
// 제목
function subject() {
document.querySelector("#subject").innerHTML = `
<header>
<h1>WEB</h1>
Hello, WEB!
</header>
`;
}
// 목차
function TOC() {
var state = store.getState();
var i = 0;
var liTags = "";
while (i < state.contents.length) {
liTags += `
<li>
<atoken interpolation">${state.contents[i].id}}
store.dispatch(action);
" href="${state.contents[i].id}">
${state.contents[i].title}
</a>
</li>`;
i += 1;
}
document.querySelector("#toc").innerHTML = `
<nav>
<ol>
${liTags}
</ol>
</nav>
`;
}
// 생성, 삭제 버튼
function control() {
document.querySelector("#control").innerHTML = `
<ul>
<li><a href="/create">create</a></li>
<li><input type="button" value="delete"></li>
</ul>
`;
}
//컨텐츠의 내용
function article() {
var state = store.getState();
if (state.mode === "create") {
document.querySelector("#content").innerHTML = `
<article>
<form>
<p>
<input type="text" name="title" placeholder="title">
</p>
<p>
<textarea name="desc" placeholder="description"></textarea>
</p>
<p>
<input type="submit">
</p>
</form>
</article>
`;
} else if (state.mode === "read") {
var i = 0;
var aTitle, aDesc;
while (i < state.contents.length) {
if (state.contents[i].id === state.selected_id) {
aTitle = state.contents[i].title;
aDesc = state.contents[i].desc;
break;
}
i = i + 1;
}
document.querySelector("#content").innerHTML = `
<article>
<h2>${aTitle}</h2>
${aDesc}
</article>
`;
} else if (state.mode === "welcome") {
document.querySelector("#content").innerHTML = `
<article>
<h2>welcome</h2>
hello redux
</article>
`;
}
}
// --------리덕스--------
function reducer(state, action) {
if (state === undefined) {
return {
max_id: 2,
mode: "create",
selected_id: 1,
contents: [
{ id: 1, title: "HTML", desc: "HTML is ..." },
{ id: 2, title: "CSS", desc: "CSS is ..." },
],
};
}
var newState = {};
if (action.type === "SELECT") {
newState = Object.assign({}, state, {
selected_id: action.id,
mode: "read",
});
} else if (action.type === "CREATE") {
var newMaxId = state.max_id + 1;
var newContents = state.contents.concat();
newContents.push({
id: newMaxId,
title: action.title,
desc: action.desc,
});
var newState = Object.assign({}, state, {
max_id: newMaxId,
contents: newContents,
selected_id: newMaxId,
mode: "read",
});
} else if (action.type === "DELETE") {
var newContents = [];
var i = 0;
while (i < state.contents.length) {
if (state.selected_id !== state.contents[i].id) {
newContents.push(state.contents[i]);
}
i += 1;
}
newState = Object.assign({}, state, {
contents: newContents,
mode: "welcome",
});
} else if (action.type === "CHANGE_MODE") {
newState = Object.assign({}, state, {
mode: action.mode,
});
}
console.log(action, state, newState);
return newState;
}
var store = Redux.createStore(reducer);
store.subscribe(article);
store.subscribe(TOC);
subject();
TOC();
control();
article();
</script>
</body>
</html>
컨테이너 컴포넌트에서 프레젠테이셔널 컴포넌트로 프롭스로 전달한다.
설치 yarn add redux react-redux
actions/constants/reducers
action함수와 reducer함수를 관리하는 폴더, actionsType을 관리하는 폴더, 총 세 개의 폴더를 만드는 방식.
Ducks 패턴
modules 폴더 안에 action, reducer, actionsType을 하나의 파일로 관리.
이하는 ducks의 예시이다.
const 액션 = '모듈명/액션명'
으로 타입을 생성한다. 모듈명을 함께 적음으로서 액션명의 중복을 방지할 수 있다.// modules/counter.js
const INCREASE = "counter/INCREASE";
const DECREASE = "counter/DECREASE";
// modules/counter.js
const INCREASE = "counter/INCREASE";
const DECREASE = "counter/DECREASE";
export const increase = () => ({ type: INCREASE });
export const decrease = () => ({ type: DECREASE });
// modules/counter.js
const INCREASE = "counter/INCREASE";
const DECREASE = "counter/DECREASE";
export const increase = () => ({ type: INCREASE });
export const decrease = () => ({ type: DECREASE });
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;
import counter from "./counter";
import { increase, decrease } from "./counter";
// 한꺼번에 불러오고 싶을 때
import counter, { increase, decrease } from "./counter";
위의 내용을 참조하여 todo 모듈을 생성해보자.
props를 받아 해당 props가 액션타입에 들어감을 확인할 것.
// modules/todos.js
const CHANGE_INPUT = "todos/CHANGE_INPUT"; // 인풋 값을 변경함
const INSERT = "todos/INSERT"; // 새로운 todo를 등록함
const TOGGLE = "todos/TOGGLE"; // todo를 체크/체크 해제함
const REMOVE = "todos/REMOVE"; // todo를 제거함
export const changeInput = (input) => ({
type: CHANGE_INPUT,
input,
});
let id = 3; // insert가 호출될 때마다 1씩 더해집니다.
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,
});
// modules/todos.js
...
const initialState = {
input: “,
todos: [
{
id: 1,
text: '리덕스 기초 배우기',
done: true
},
{
id: 2,
text: '리액트와 리덕스 사용하기',
done: false
}
]
};
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;
// modules/index.js
import { combineReducers } from "redux";
import counter from "./counter";
import todos from "./todos";
const rootReducer = combineReducers({
counter,
todos,
});
export default rootReducer;
import rootReducer from './modules';
// src/index.js
import { createStore } from 'redux';
...
const store = createStore(rootReducer);
// index.js
import React from "react";
import ReactDOM from "react-dom";
import { createStore } from "redux";
import { Provider } from "react-redux";
import "./index.css";
import App from "./App";
import * as serviceWorker from "./serviceWorker";
import rootReducer from "./modules";
const store = createStore(rootReducer);
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById("root")
);
serviceWorker.unregister();
const store = createStore(
rootReducer, /* preloadedState, */
window._ _REDUX_DEVTOOLSEXTENSION && window. _REDUX_DEVTOOLSEXTENSION _()
);
yarn add redux-devtools-extension
import { composeWithDevTools } from "redux-devtools-extension";
const store = createStore(rootReducer, composeWithDevTools());
스토어에 접근하여 상태를 받아오고, 액션도 디스패치하는 컨테이너 컴포넌트를 만들자.
// containers/CounterContainers.js
import React from "react";
import Counter from "../components/Counter";
const CounterContainer = () => {
return <Counter />;
};
export default CounterContainer;
connect(mapStateToProps, mapDispatchToProps)(연동할 컴포넌트)
의 구조를 갖는다.import React from "react";
import { connect } from "react-redux";
import Counter from "../components/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: () => {
console.log("increase");
},
decrease: () => {
console.log("decrease");
},
});
export default connect(mapStateToProps, mapDispatchToProps)(CounterContainer);
CounterContainer로 선언한 컴포넌트에
mapStateToProps와 mapDispatchToProps를 넘겨주는 모습.
mapStateToProps에는 sotre의 counter리듀서로 생성된 state 중 number를 객체로 넘겨주고 있으며,
mapDispatchToProps는 increase라는 함수와 decrease라는 함수를 하나의 객체로 넘겨주고 있다.
해당 값들은 비구조화 할당으로 CounterContainer 컴포넌트의 props로 받아진다.
// App.js
App.js;
import React from "react";
import Todos from "./components/Todos";
import CounterContainer from "./containers/CounterContainer";
const App = () => {
return (
<div>
<CounterContainer />
<hr />
<Todos />
</div>
);
};
export default App;
Counter 컴포넌트를 CounterContainer로 대체하였다.
CounterContainer에서 mapStateToProps, mapDispatchToProps를 통해 state와 dispatch를 받은 후,
Counter 컴포넌트에 props로 넘겨주게 된다.
컨테이너의 state와 함수 부분을 아래와 같이 수정한다.
const mapStateToProps = (state) => ({
number: state.counter.number,
});
const mapDispatchToProps = (dispatch) => ({
increase: () => {
dispatch(increase());
},
decrease: () => {
dispatch(decrease());
},
});
export default connect(mapStateToProps, mapDispatchToProps)(CounterContainer);
위와 같이 미리 mapStateToProps와 mapDispatchToProps를 선언하지 않고, connect 함수 내부에 직접 익명함수로 선언할 수도 있다.
export default connect(
(state) => ({
number: state.counter.number,
}),
(dispatch) => ({
increase: () => dispatch(increase()),
// 위 코드는 다음과 완전히 동일하게 작동함 increase: () => { return dispatch(increase()) }
decrease: () => dispatch(decrease()),
})
)(CounterContainer);
bindActionCreators 유틸 함수를 이용하여 여러개의 액션 함수를 dispatch할 수도 있다.
import { bindActionCreators } from 'redux';
...
export default connect(
state => ({
number: state.counter.number,
}),
dispatch =>
bindActionCreators(
{
increase,
decrease,
},
dispatch,
),
)(CounterContainer);
익명함수의 state, dispatch는 아래와 같이 비구조화 할당을 사용하여 가독성을 높일 수 있다.
export default connect(
// 비구조화 할당을 통해 todos를 분리하여
// state.todos.input 대신 todos.input을 사용
({ todos }) => ({
input: todos.input,
todos: todos.todos,
}),
{
changeInput,
insert,
toggle,
remove,
}
)(TodosContainer);
디스패치와 리듀서를 더욱 쉽게 작성하게 해주는 라이브러리.
createAction으로 액션을 만들고, handleActions(업데이트 함수, 초기 상태)으로 액션을 관리한다.
yarn add redux-actions
// 기존 코드
// modules/counter.js
const INCREASE = "counter/INCREASE";
const DECREASE = "counter/DECREASE";
export const increase = () => ({ type: INCREASE });
export const decrease = () => ({ type: DECREASE });
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;
}
}
// 새로운 코드
import { createAction, handleActions } from "redux-actions";
const INCREASE = "counter/INCREASE";
const DECREASE = "counter/DECREASE";
export const increase = createAction(INCREASE);
export const decrease = createAction(DECREASE);
const initialState = {
number: 0,
};
const counter = handleActions(
{
[INCREASE]: (state, action) => ({ number: state.number + 1 }),
[DECREASE]: (state, action) => ({ number: state.number - 1 }),
},
initialState
);
export default counter;
yarn add immer
import produce from 'immer';
produce(변경할 값, 변경함수)
const state = {
number: 1,
dontChangeMe: 2,
};
//불변성을 지키지 않는 방식
state.number += 1;
//기존 방식
const nextState = { ...state, number: (state.number += 1) };
//immer
const nextState = produce(state, (draft) => {
draft.number += 1;
});
connect 대신 hooks를 사용할 수 있다.
const dispatch = useDispatch()
dispatch({type :...})
import React 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();
return (
<Counter
number={number}
onIncrease={() => dispatch(increase())}
onDecrease={() => dispatch(decrease())}
/>
);
};
export default CounterContainer;
디스패치 함수에 useCallback을 이용하여 아래와 같이 최적화
import React, { useCallback } from 'react';
...
const onIncrease = useCallback(() => dispatch(increase()), [dispatch]);
const onDecrease = useCallback(() => dispatch(decrease()), [dispatch]);
return (
<Counter number={number} onIncrease={onIncrease} onDecrease={onDecrease} />
);
const store = useStore();
store.dispatch({ type: "SAMPLE_ACTION " });
store.getState();
// @/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
);
}
import useActions from "../lib/useActions";
// 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]);
const [onChangeInput, onInsert, onToggle, onRemove] = useActions(
[changeInput, insert, toggle, remove],
[]
);
액션 배열에 있는 액션들을 dispatch하며 새로운 배열을 만든다.
의존자 배열을 빈 배열로 초기화하여 최적화할 수 있다.
connect함수는 컨테이너 컴포넌트의 props가 바뀌지 않았다면 리렌더링이 방지되어 성능이 최적화된다.
반면 useSelector 훅은 부모컴포넌트가 리렌더링될 때에 최적화가 되지 않고 계속해서 리렌더링이 이루어 진다.
React.memo를 통해 리렌더링을 방지할 수 있다.
import React from 'react';
import { useSelector } from 'react-redux';
...
export default React.memo(TodosContainer);
yarn add redux react-redux redux-actions
미들웨어란 액션과 리듀서 사이의 중간자라 볼 수 있다.
액션에서의 처리 결과에 따라 미들웨어에서 작업한 후, 리듀서를 호출하거나, 액션을 취소하는 등의 작업을 진행한다.
미들웨어는 기본적으로 함수를 반환하는 함수이다.
store.dispatch({액션객체}는 middleware = store => next => action => {액션내용}
으로 치환되는데,
이때 next()는 다음 미들웨어로 액션을 넘겨주거나, 다음 미들웨어가 없는 경우 store.dispatch(액션)을 실행시킨다.
// @/lib/loggerMiddleware.js
const loggerMiddleware = (store) => (next) => (action) => {
console.group(action && action.type); // 액션 타입으로 log를 그룹화함
console.log("이전 상태", store.getState());
console.log("액션", action);
next(action); // 다음 미들웨어 혹은 리듀서에게 전달
console.log("다음 상태", store.getState()); // 업데이트된 상태
console.groupEnd(); // 그룹 끝
};
export default loggerMiddleware;
// 아래와 같은 구조임
/*
const loggerMiddleware = function loggerMiddleware(store) {
return function (next) {
return function (action) {
// 미들웨어 기본 구조
};
};
};
*/
위의 미들웨어는 액션의 타입과 상태를 하나의 그룹으로 묶어서 console에 출력한다.
만들어진 미들웨어는 index.js의 스토어에 아래와 같이 적용한다.
import loggerMiddleware from ‘./lib/loggerMiddleware‘;
const store = createStore(rootReducer, applyMiddleware(loggerMiddleware));
yarn add redux-logger
// import loggerMiddleware from ‘./lib/loggerMiddleware‘;
import { createLogger } from ‘redux-logger‘;
const logger = createLogger();
const store = createStore(rootReducer, applyMiddleware(logger));
redux-logger는 액션의 타입과 디스패치된 시간, 스토어의 상태 등을 알록달록 이쁘게 출력해주는 미들웨어다.
thunk는 특정 작업을 나중으로 미루기 위해 함수 형태로 감싼 단위를 의미한다. 즉 비동기 처리에서 넘겨주는 콜백함수가 thunk에 포함된다.
redux-thunk를 이용하면 함수를 디스패치할 수 있게된다.
yarn add redux-thunk
import { createLogger } from ‘redux-logger‘;
import ReduxThunk from ‘redux-thunk‘;
const logger = createLogger();
const store = createStore(rootReducer, applyMiddleware(logger, ReduxThunk));
위와 같이 store에 ReduxThunk를 등록하면 함수를 디스패치 할 수 있다.
//기본 방식
export const increase = () => ({ type: INCREASE });
export const decrease = () => ({ type: DECREASE });
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;
}
}
//redux-action을 사용한 방식
const INCREASE = "counter/INCREASE";
const DECREASE = "counter/DECREASE";
export const increase = createAction(INCREASE);
export const decrease = createAction(DECREASE);
const initialState = {
number: 0,
};
const counter = handleActions(
{
[INCREASE]: (state, action) => ({ number: state.number + 1 }),
[DECREASE]: (state, action) => ({ number: state.number - 1 }),
},
initialState
);
// redux-thunk를 사용한 방식
import { createAction, handleActions } from ‘redux-actions‘;
const INCREASE = ‘counter/INCREASE‘;
const DECREASE = ‘counter/DECREASE‘;
export const increase = createAction(INCREASE);
export const decrease = createAction(DECREASE);
export const increaseAsync = () => dispatch => {
setTimeout(() => {
dispatch(increase());
}, 1000);
};
export const decreaseAsync = () => dispatch => {
setTimeout(() => {
dispatch(decrease());
}, 1000);
};
const initialState = 0; // 상태는 꼭 객체일 필요가 없습니다. 숫자도 작동해요.
const counter = handleActions(
{
[INCREASE]: state => state + 1,
[DECREASE]: state => state - 1
},
initialState
);
export default counter;
// container/CounterContainer.js
//redux-action을 사용한 방식
import React from "react";
import { connect } from "react-redux";
import Counter from "../components/Counter";
const CounterContainer = ({ number, increase, decrease }) => {
return (
<Counter number={number} onIncrease={increase} onDecrease={decrease} />
);
};
export default connect(
(state) => ({
number: state.counter.number,
}),
(dispatch) => ({
increase: () => dispatch(increase()),
// 위 코드는 다음과 완전히 동일하게 작동함 increase: () => { return dispatch(increase()) }
decrease: () => dispatch(decrease()),
})
)(CounterContainer);
//redux-thunk를 이용한 방식. state에는 숫자를, mapDispatchToProps에는 함수들을 넣어주었다.
const CounterContainer = ({ number, increaseAsync, decreaseAsync }) => {
return (
<Counter
number={number}
onIncrease={increaseAsync}
onDecrease={decreaseAsync}
/>
);
};
export default connect(
state => ({
number: state.counter
}),
{
increaseAsync,
decreaseAsync
}
)(CounterContainer);
대부분의 비동기 처리는 redux-thunk에서도 가능하지만,
redux-saga는 다음의 상황에서 유리하다.
• 기존 요청을 취소 처리해야 할 때(불필요한 중복 요청 방지)
• 특정 액션이 발생했을 때 다른 액션을 발생시키거나, API 요청 등 리덕스와 관계없는 코드를 실행할 때
• 웹소켓을 사용할 때
• API 요청 실패 시 재요청해야 할 때
ES6에 추가된 사양이다.
함수 내에 yield를 만나면 함수의 흐름을 중단하며,
함수를 호출할 때마다 다음 yield를 호출한다.
function* foo() {
var index = 0;
while (index <= 2)
// when index reaches 3,
// yield's done will be true
// and its value will be undefined;
yield index++;
}
var iterator = foo();
console.log(iterator.next()); // { value: 0, done: false }
console.log(iterator.next()); // { value: 1, done: false }
console.log(iterator.next()); // { value: 2, done: false }
console.log(iterator.next()); // { value: undefined, done: true }
next 함수에 value값을 넘겨서 yield로 멈춘 곳에 값을 추가해줄 수 있다.
function* sumGenerator() {
console.log("sumGenerator가 만들어졌습니다.");
let a = yield;
let b = yield;
yield a + b;
}
const sum = sumGenerator();
sum.next();
// sumGenerator가 만들어졌습니다.
// {value: undefined, done: false}
sum.next(1);
// {value: undefined, done: false}
sum.next(2);
// {value: 3, done: false}
sum.next();
// {value: undefined, done: true}
redux-saga는 const action = yield;
제너레이터.next({ type: 'TEST' });
와 같이 next 함수에 type 값을 포함한 객체를 넘겨주며 작동한다.
(리덕스 사가 작성중)