1. About Redux
Flux 패턴에 대해 알고 있다면 그것은 라이브러리나, 프레임워크가 아닌 디자인 패턴이다. 즉 Flux 아키텍쳐를 사용하기 위해서는 개발자들이 직접 이 아키텍쳐에 맞게 코드로 구현해야 한다는 뜻이다. 초창기에는 Flux 패턴을 각자의 방법대로 구현한 여러가지 라이브러리들이 생겼지만, 현재 Flux 패턴을 근간으로 하는 라이브러리의 표준은 Redux로 정립되었다.
Redux는
Flux, CQRS, Event Sourcing의 개념을 사용해서 만든 라이브러리로서 “JavaScript 앱을 위한 예측 가능한 상태 컨테이너" 를 핵심 가치로 삼고 있다.
- 모든 상태를 관리하는 컨테이너로서의 역할을 수행
- 애플리케이션 내의 구성요소들은 컨테이너에 접근해서 상태를 읽어올 수 있기에 자바스크립트 앱에서 전역 상태 관리를 수행하기 위해서 사용할 수 있다.
- Flux 패턴의 단방향성을 차용했기에 Redux 내에서 발생하는 상태의 변화는 모두 예측 가능
이런 특성으로 인해 프론트엔드에서 발생하는 복잡한 상태들의 변화를 관리하기에 적격으로 판단되었으며, Redux가 발표되고 난 후 한동안 프론트엔드의 상태관리 표준은 Redux로 자리잡았다.
2. Redux의 3가지 원칙
2-1. Single source of truth
Redux 내의 모든 전역 상태는 하나의 객체 안에 트리구조로 저장되고, 이 객체를 Store라 한다. 모든 상태(state)가 하나의 객체에 저장되기에 애플리케이션이 단순해지고, 예측하기 쉬워진다. 또한 하나의 객체의 변화만 추적하면 되기에 Undo, Redo 등의 기능을 구현하기도 쉬워진다.
{
visibilityFilter: 'SHOW_ALL', // state 1
todos: [ // state 2
{
text: 'Consider using Redux',
completed: true,
},
{
text: 'Keep all state in a single tree',
completed: false
}
]
}
2-2. State is read-only
- Redux의 State를 변화시키는 유일한 방법은 “Action 객체를 Dispatch를 통해서 전달하는 것”
- 그 외에 Store에 직접 접근해서 상태를 수정하는 등의 행위는 허용되지 않는다.
- state를 불변하게 다루고, 변화시킬 수 있는 방법을 제약함으로서 안정성과 예측 가능성을 증대
모든 변화는 Dispatch를 통해서 중앙화되고, 순서대로 수행되기에 여러곳에서 동시에 데이터를 수정하면서 발생하는 race condition 문제 등이 발생하지 않게 된다.
또한, Action을 통해서 변화의 의도를 표현한다. Action은 단순한 형태의 객체이기 때문에 이를 추적하거나, 로깅, 저장하는 등이 동작을 수행하기 용이하기에 디버깅을 손쉽게 할 수 있으며, 추후 테스트 코드를 작성하기도 용이하다.
const action = {
type: 'COMPLETE_TODO',
index: 1
}
store.dispatch(action)
// ------------------------
store.dispatch({
type: 'SET_VISIBILITY_FILTER',
filter: 'SHOW_COMPLETED'
})
2-3. Changes are made with pure function
Action 객체가 Dispatch를 통해 Store에 전달된 후 실질적으로 Action을 통해서 Store를 변경시키는 동작은 Reducer라 불리는 순수함수를 통해서 수행된다.
1) 순수함수(pure function) 란
- 동일한 Input을 받았을 경우 항상 동일한 Output을 내는것이 보장되어 있는 함수를 의미
- 순수함수가 되기 위해서는 함수 내에 사이드이펙트가 없어야 한다.
만약 사이드 이펙트가 있는 경우 그 함수는 해당 사이드 이펙트에 의해서 같은 Input이라도 다른 Output을 리턴할 수가 있다.
// pure function
function sum(x,y) {
return x + y;
}
sum(1,2) // 3
sum(1,2) // 3
sum(1,2) // 3
sum(1,2) // 3
sum(1,2) // 3
// non-pure function
function sum(x) {
return x + Math.random();
}
sum(1) // ?
sum(1) // ?
sum(1) // ?
sum(1) // ?
sum(1) // ?
2) Reducer 는
- 이전 state 값과, action 객체를 인자로 받아서 새로운 state를 리턴하는 순수 함수
- 즉, 기존의 state 객체를 수정하는 것이 아니라, 기존의 state 객체를 이용해서 새로운 state 객체를 만들어내는 식으로 동작한다는 점
Redux는 Store가 하나이기에 이를 관리하는 Reducer 또한 하나여야 한다. 하지만 각기 다른 관심사가 하나의 함수에 모두 들어가게 되면 유지보수에 좋지 않기에 애플리케이션이 커지면 여러개의 Reducer 함수(slice reducer)로 분리해서 코드를 작성한 다음 하나의 reducer(root reducer)로 통합하는 방식을 활용할 수 있다.
function visibilityFilter(state = 'SHOW_ALL', action) {
switch (action.type) {
case 'SET_VISIBILITY_FILTER':
return action.filter
default:
return state
}
}
function todos(state = [], action) {
switch (action.type) {
case 'ADD_TODO':
return [
...state,
{
text: action.text,
completed: false
}
]
case 'COMPLETE_TODO':
return state.map((todo, index) => {
if (index === action.index) {
return Object.assign({}, todo, {
completed: true
})
}
return todo
})
default:
return state
}
}
import { combineReducers, createStore } from 'redux'
// root reducer(visibilityFilter, todos)를 통합해서 rootReducer 생성
const reducer = combineReducers({ visibilityFilter, todos })
// root reducer를 통해서 store 생성
const store = createStore(reducer)
3. Redux의 구성요소 & 데이터 흐름
1) View
- 유저에게 보이는 UI를 의미하며, store의 state를 기반으로 그려진다.
2) Action
- type property 를 가지고 있는 자바스크립트 객체, 애플리케이션 내에서 어떤 일이 일어났는지를 묘사하는 객체로 생각할 수 있다.
- type property는 어떤 변화가 발생했는지 묘사하는 string 으로, 통상 domain/eventName 의 형태
- domain 파트는 이 이벤트가 어떤 카테고리에 속하는지 표시하기 위함
- eventName 은 어떤 일이 발생했는지를 표현하는 부분
Action 객체는 type property는 필수적으로 포함하고 있어야 하며, 그 외에 추가적으로 전달할 데이터가 있을 시 다른 property를 객체 안에 포함시킬 수 있다. 통상적으로 추가적인 정보를 전달하는 property의 이름은 payload 로 표현한다.
{
type: 'TODO/ADD_TODO',
payload:"Learn Redux"
}
{
type: 'TODO/SET_VISIBILITY_FILTER',
filter: 'SHOW_COMPLETED'
}
3) Action Creator
- Action Creator는 말그대로 Action을 생성하는 함수
- 매번 액션 객체를 손수 작성하는 것은 중복이며, 번거롭고, 실수할 여지가 많은 작업이기에 Action Creator를 통해서 생성하는 것이 권장
- export로 내보내서 컴포넌트에서 사용할수있도록 해주고 type값은 필수적으로 있어야한다.
export const addTodo = todo => {
return {
type: 'TODO/ADD_TODO',
payload:todo
}
}
4) Reducer
Reducer는 이전 state 값과, action 객체를 인자로 받아서 새로운 state를 리턴하는 순수 함수입니다. Reducer를 단순화 해서 표현하자면 (state, action) => newState 의 형태로 표현할 수 있습니다.
ReducerReducer는 아래의 원칙을 따라야 합니다.
- 새로운 state는 오로지 기존의 state와 action 객체를 통해서만 계산되어야 한다. 그 외의 요소들에 영향을 받아서는 안된다.
- Reducer는 기존의 state를 수정해서는 안된다, 기존의 state를 복사하고, 복사한 state에 변화를 발생시킨 후 return 하는식으로 동작해야 한다.
- Reducer 내부에서는 비동기 통신, 랜덤 값을 사용하는 것 등의 그 어떤 사이드 이펙트도 수행되서는 안된다.
Reducer 함수는 일반적으로 아래의 과정을 수행합니다.
- Reducer가 현재 전달받은 Action을 처리할 수 있는지 판단한다.
a. 처리할 수 있을 경우 state와 action을 통해서 새로운 state 객체를 만든 뒤 리턴한다. - 처리할 수 없는 Action인 경우에는 기존의 state를 그대로 리턴한다.
const INITIAL_STATE = { value: 0 }
function counterReducer(state = INITIAL_STATE, action) {
// Reducer가 현재 전달받은 Action을 처리할 수 있는지 판단한다.
if (action.type === 'counter/increment') {
// 처리할 수 있을 경우 state와 action을 통해서 새로운 state 객체를 만든 뒤 리턴한다.
return {
...state,
value: state.value + 1
}
}
// 처리할 수 없는 Action인 경우에는 기존의 state를 그대로 리턴한다.
return state
}
5) Store
- Store는 Redux의 모든 state를 관리하는 객체
- Redux에서 store는 createStore 함수에 reducer를 인자로 넣으면서 호출해서 만들 수 있다.
- store는 getState란 메소드를 가지고 있으며 이를 통해 현재의 state값을 가져올 수 있다.
import { countReducer } from 'redux'
// root reducer(visibilityFilter, todos)를 통합해서 rootReducer 생성
const reducer = combineReducers({ countReducer })
// root reducer를 통해서 store 생성
const store = createStore(reducer)
// getState 메서드를 통해 현재 state 획득
store.getState() // {value: 0}
6) Dispatch
- Dispatch는 Store객체에 포함되어있는 메소드
- 이 메소드를 통해서 Action 객체를 Store에 전달할 수 있다.
- Dispatch를 통해서 Action이 전달되면 Store는 Reducer를 통해서 새로운 state를 만든다.
store.dispatch({ type: 'counter/increment' })
store.getState() // {value: 1}
store.dispatch({ type: 'counter/increment' })
store.getState() // {value: 2}
7) Selector
- Selector는 store에서 특정한 state만 가져오기 위한 함수
- 단일 store에 모든 state를 담아두기에 애플리케이션이 커질수록 store는 비대해지고, View에서는 이중에서 필요한 state만 가져오는 동작을 계속해서 수행
- 이 동작을 매번 손수 반복하지 않기 위해서 selector 함수를 이용한다.
const selectCounterValue = state => state.value
const currentValue = selectCounterValue(store.getState())
console.log(currentValue) // 2
4. React-Redux
4-1. Redux ? React-Redux?
Redux는 “자바스크립트 앱을 위한 예측 가능한 상태 컨테이너" 다. 이 말은 곧 Redux는 React 전용이 아닌 모든 자바스크립트 앱에 활용할 수 있다는 의미다.
React 또한 자바스크립트이기에 Redux를 사용할 수 있다. React에서 Redux를 사용하게 되면
- 전역 상태 관리
- Props drilling
이 두 문제를 해결할 수 있게 된다.
Redux는 어떤 자바스크립트 앱이든 사용할 수 있도록 범용성을 갖추고 있다. 하지만 반대로 생각해보면 React에 최적화되지 않았다는 것을 의미한다. 이러한 이유로 인해 Redux를 React와 통합하기 위해서는 Redux의 store에 저장된 state를 React 컴포넌트에서 가져올 수 있게 해주고, redux state가 변경될 경우 react state와 마찬가지로 리렌더링 해주는 등의 복잡한 과정을 수행해줘야 한다.
React-Redux는 이처럼 Redux와 React를 통합하기 위해 필요한 복잡한 과정을 수행해주는 라이브러리입니다. React-Redux는 React에 Redux를 통합하기 위한 여러 함수와 컴포넌트들을 제공한다. 따라서, 통상 React에서 Redux를 사용하기 위해서는 Redux와 React-Redux를 함께 사용하는 것이 일반적입니다.
- Redux - 자바스크립트 앱을 위한 상태 컨테이너
- React-Redux - Redux를 React와 통합하기 위한 라이브러리
4-2. React-Redux 에서 제공하는 대표적인 기능
1) Provider
- React 컴포넌트들에게 Redux Store에 접근할 수 있는 기능을 제공해주는 컴포넌트
- 내부적으로 Context API를 활용
// 일부 코드 생략
import { Provider } from 'react-redux';
import store from './store/index';
import App from './App';
root.render(
<Provider store={store}>
<App />
</Provider>
);
2) useSelector
- 컴포넌트에서 Redux Store의 값을 가져올 수 있는 hook
- Redux의 Selector를 React Hook으로 표현한 형태
import { useSelector } from 'react-redux';
const Counter = () => {
const count = useSelector((state) => state.value);
return <h1>{count}</h1>;
};
3) useDispatch
- 컴포넌트에서 action을 store에 보내기 위한 dispatch 함수를 가져올 수 있는 hook
import { useSelector, useDispatch } from 'react-redux';
const Count = () => {
const count = useSelector((state) => state.counter);
const dispatch = useDispatch();
const increase = () => {
dispatch({ type: 'counter/increment' });
};
return (
<div>
<h1>{count}</h1>
<button onClick={increase}>increment</button>
</div>
);
};
export default Count;
'Library & Framework > Redux' 카테고리의 다른 글
Redux MiddleWare 깊이 있게 살펴보자! (0) | 2022.10.31 |
---|---|
Redux-Saga (0) | 2022.10.29 |
Redux Middleware / DevTools (0) | 2022.10.29 |