컴포넌트가 많을수록 복잡해지는 상태(State)를 관리하는 라이브러리.
원래 프론트엔드 생태계에서는 MVC 패턴이 대세였음.
MVC 패턴은 위와 같이 데이터 양방향의 흐름을 가짐. 그러나 이러한 패턴은 데이터 변화가 양쪽에서 일어나기 때문에 오히려 복잡성을 증가시킴. 이러한 점을 보완하여 나온 것이 바로 Flux!
Flux는 데이터가 한쪽 방향으로 흘러가는 단방향 데이터 흐름을 가진다. 일정 장소를 통해서만 데이터를 변화시킬 수 있기 때문에 코드 파악에 더 용이하다는 장점이 있으며, React에서는 이러한 패턴을 지향하고 있음.
Redux는 Flux를 기반으로 만들었기 때문에 기본적인 패턴이 동일함.
상태값은 반드시 Store에서 관리되며, Reducer를 통해서만 새로운 State를 받을 수 있는 구조로 이루어짐.
# npm으로 설치
npm install react-redux
# yarn으로 설치
yarn add react-redux
# CRA로 설치
npx create-react-app [app이름] --template redux
app에서 스토어로 전달하는 데이터 묶음. 유일한 정보 처리원으로 생각하면 쉬움.
액션 함수를 만들어서 액션 객체 반환해서 사용해도 됨.
// 액션 타입 지정 상수
const INCREMENT = 'INCREMENT';
const DECREMENT = 'DECREMENT';
// 액션 생성 함수
const increment = (diff) => ({
type: INCREMENT,
diff: diff
});
const decrement = (diff) => ({
type: DECREMENT,
diff: diff
});
이렇게 만들어진 액션 객체는 dispatch()
에 넘겨서 사용함.
dispatch(increment(1));
dispatch(decrement(1));
// # 액션 바인드 함수 사용
const handlePlus = number => dispatch(increment(number));
const handleMinus = number => dispatch(decrement(number));
handlePlus(1);
handleMinus(1);
상태값을 직접 변화시키는 함수. 이전 상태값, 액션을 받아 다음 상태값을 반환함.
이름이 reducer인 이유는 실제로 reducer 함수를
Array.reduce()
로 넘기기 때문이라함.
리듀서에 넘길 state는 처음에 undefined
를 호출하기 때문에 초깃값을 지정해서 사용함.
// 초깃값 설정 - 리듀서
const initialState = {
number: 0
};
// 리듀서 함수
function counter(state = initialState, action){
switch(action.type){
case INCREMENT:
return { number: state.number + action.diff};
case DECREMENT:
return { number: state.number - action.diff};
default:
return state;
}
}
현재의 앱 상태, 리듀서, 여러 내장 함수들을 가지고 있는 컨테이너 같은 것임. createStore
함수 사용. 파라미터로 리듀서 함수를 전달.
const { createStore } = Redux;
// import {createStore} from 'redux';
const store = createStore(counter);
스토어 내장함수. 구독함수는 함수 형태로 파라미터값을 받아옴.
const unsubscribe = store.subscribe(() => {
// getState = 현재 스토어 상태 반환하기.
console.log(store.getState());
})
스토어의 내장함수. 액션을 직접 발생시키는 애임. 액션을 파라미터로 전달해서 호출시키면 스토어가 리듀서 함수를 실행시키고, 새로운 상태값을 만들어줌.
store.dispatch(increment(1));
store.dispatch(decrement(5));
store.dispatch(increment(10));
modules라는 디렉토리 안에 counter / todo 모듈 파일을 생성하기.
modules
└ counter.js
└ todo.js
modules/counter.js :
// counter 모듈
// 1. 액션 타입 정의하기
// 액션 이름 중복 방지를 위해서 앞에 'counter/'안에 속해있다는 접두사를 붙임
const SET_DIFF = 'counter/SET_DIFF';
const INCREASE = 'counter/INCREASE';
const DECREASE = 'counter/DECREASE';
// 2. 액션 함수 생성
export const setDIFF = diff => ({
type: SET_DIFF,
diff
});
export const increase = () => ({
type: INCREASE
});
export const decrease = () => ({
type: DECREASE
});
// 3. 초기 상태를 정의
const initialState = {
number: 0,
diff: 1
}
// 4. 리듀서 선언
// 리듀서는 export default 로 반드시 내보내줘야함.
// 모듈을 가져다 써야하기 때문임
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;
}
}
modules/todo.js :
// 1. 액션 타입 정의
const ADD_TODO = 'todos/ADD_TODO';
const TOGGLE_TODO = 'todos/TOGGLE_TODO';
// todo 리스트의 고유 idx 설정
let idx = 1;
// 2. 액션 함수 생성
export const addTodo = text => ({
type: ADD_TODO,
todo: {
idx: idx++,
text
}
});
export const toggleTodo = idx => ({
type: TOGGLE_TODO,
idx
});
// 3. 초깃값 정의
// idx값과 text, 완료여부 객체를 저장
const initialState = [];
// 4. 리듀서 내보내기
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.idx === action.idx ? { ...todo, done: !todo.done} : todo);
default:
return state;
}
}
생성한 리덕스 모듈은 두개. 따라서 리듀서 또한 두개가 내보내짐. 한 프로젝트 내 리듀서가 여러개일 경우, combinReducers
라는 함수를 통해 하나의 리듀서로 통합한다. 이걸 Root Reducer라고 함!
modules/index.js :
import {combineReducers} from 'redux';
import counter from './counter';
import todo from './todo';
const rootReducer = combineReducers({
counter,
todo
});
export default rootReducer;
index.js
에서 스토어를 생성해서 루트 리듀서를 넣어줌.
index.js :
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import * as serviceWorker from './serviceWorker';
// 스토어 생성 함수 불러오기
import {createStore} from 'redux';
// 루트 리듀서 불러오기
import rootReducer from './module';
ReactDOM.render(<App />, document.getElementById('root'));
const store = createStore(rootReducer);
serviceWorker.unregister();
react-redux
라이브러리를 설치하여 리액트 내에서 리덕스를 사용해보자.
index.js에서 Provider 라는 컴포넌트를 불러와서 App
컴포넌트를 감싸 props로 store
를 넣어줌.
index.js :
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import * as serviceWorker from './serviceWorker';
import {createStore} from 'redux';
import rootReducer from './module';
// Provider 컴포넌트 불러오기
import {Provider} from 'react-redux';
const store = createStore(rootReducer);
// App 컴포넌트를 Provider 컴포넌트로 감싼 후,
// store 를 props로 넘겨준다.
// 이렇게 하면 전역에서 store를 공유하며 어디서든 접근이 가능함.
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
);
serviceWorker.unregister();
props로 필요한 값을 받아만 와서 UI를 구현하는 컴포넌트. component 디렉토리 내, Counter.js 파일을 생성하여 컴포넌트를 만들어준다.
component/Counter.js :
import React from 'react';
// Counter 컴포넌트는 only 읽기 전용, UI 관련 컴포넌트
// 스토어에 직접 접근 X / props를 통해 값을 받기만 한다.
// 전역에서 store에 접근할 수 있음.
function Counter({number, diff, onIncrease, onDecrease, onSetDiff
}){
const onChange = e => {
// e.target.value는 문자열이므로,
// int 값으로 변환해준다.
onSetDiff(parseInt(e.target.value, 10));
};
return (
<div>
<h1>{number}</h1>
<div>
<input type="number" value={diff} min="1" onChange={onChange} />
<button onClick={onIncrease}>+</button>
<button onClick={onDecrease}>-</button>
</div>
</div>
)
};
export default Counter;
스토어의 상태 조회 or 액션을 디스패치할 수 있는 컴포넌트를 의미함. 다른 프리젠테이셔널 컴포넌트를 불러와서 사용함.
CounterContainer.js :
import React from 'react';
// useSelector = 스토어 상태를 조회하는 리액트 hook
// = getState()
import { useSelector, userDispatch } from 'react-redux';
import Counter from '../components/Counter';
import { increase, decrease, setDiff} from '../module/counter';
function CounterContainer(){
const {number, diff} = useSelector(state => ({
number: state.counter.number,
diff: state.counter.diff
}));
// useDispatch, 스토어의 dispatch를 함수에서 사용하게 하는 hook임.
const dispatch = userDispatch();
// 각 액션들을 디스패치하는 함수 생성
// 액션 함수를 생성하는 함수를 파라미터값으로 넣어줌
const onIncrease = () => dispatch(increase());
const onDecrease = () => dispatch(decrease());
const onSetDiff = () => dispatch(setDiff(diff));
// 프리젠테이셔널 컴포넌트를 불러와 컴포넌트 안에 props값을 세팅
return (
<Counter
number={number}
diff={diff}
onIncrease={onIncrease}
onDecrease={onDecrease}
onSetDiff={onSetDiff}
/>
);
}
export default CounterContainer;
컨테이너 컴포넌트는 App 컴포넌트에서 불러와 렌더링 하면 됨.
App.js :
import React from 'react';
import CounterContainer from './containers/CounterContainer';
function App() {
return (
<div>
<CounterContainer />
</div>
);
}
export default App;