리듀서(Reducer)란?
→ 리듀서는 이전 상태와 동작을 받아 새 상태를 리턴한다.
→ 리듀서는 반드시 순수 함수여야 한다. 이를테면 데이터베이스 호출이나 HTTP 호출 등 외부의 데이터 구조를 변형하는 호출은 허용되지 않는다.
→ 리듀서는 항상 현재 상태를 '읽기 전용'으로 다룬다. 기존 상태를 변경하지는 않지만 새 상태를 리턴은 할 수 있다.
리듀서가 포함하고 있는 세 가지.
- 할 일을 정의하는 Action(인수는 옵션)
- 애플리케이션의 모든 데이터를 저장하는 state
- state와 Action을 받아 새 상태를 리턴하는 Reducer
첫 리듀서 만들기
가장 단순한 리듀서는 상태 자체만을 리턴한다(identity reducer라고 한다).
interface Action {
type: string;
payload?: any;
}
→ payload는 종류와 상관 없이 객체가 될 수 있다.
interface Reducer<T> {
(state: T, action: Action): T;
}
→ Reducer에는 타입 T의 state와 Action을 받아 새 state를 리턴하는 함수가 포함된다.
let reducer: Reducer<number> = (state: number, action: Action) => {
return state;
};
console.log(reducer(0, null)); // 0
→ 의미는 없지만, 여기서 알 수 있는 것은 리듀서는 기본적으로 원래 상태를 리턴한다.
state 변경하기
리덕스에서는 상태를 변경할 수 없다는 사실을 잊지 말자.
let incrementAction: Action = { type: 'INCREMENT' }
let decrementAction: Action = { type: 'DECREMENT' }
let reducer: Reducer<number> = (state: number, action: Action) => {
switch (action.type) {
case 'INCREMENT':
return state + 1;
case 'DECREMENT':
return state - 1;
default:
return state;
}
}
→ switch의 default case에서는 원래 state를 리턴한다. 그래야 알 수 없는 동작이 전달되어도 오류가 출력되지 않고 원래 state가 변경되지 않는다.
payload 파라미터 사용하기
→ 변경하는 내용을 설명하기 위해 payload 파라미터를 사용했음
...
let plusSevenAction: Action = {type: 'PLUS', payload: 7};
let reducer: Reducer<number> = (state: number, action: Action) => {
switch (action.type) {
case 'INCREMENT':
return state + 1;
case 'DECREMENT':
return state - 1;
case 'PLUS':
return state + action.payload;
default:
return state;
}
}
console.log(reducer(3, {type: 'PLUS', payload: 7})); // 10
console.log(reducer(3, {type: 'PLUS', payload: 9000})); // 9003
console.log(reducer(3, {type: 'PLUS', payload: -2})); // 1
상태 저장하기
리듀서는 순수한 함수이며, 외부 환경을 변경하지 않는다. 문제는 앱에서 모든 것이 변경된다는 점이다. 즉 상태는 변화하고 앱 어딘가에서는 새 상태를 유지하고 있어야 한다.
리덕스에서는 상태를 저장소(store)에 보관한다. 저장소는 리듀서를 실행하여 새 상태를 유지할 책임을 진다.
class Store<T> {
private _state: T;
constructor(
private reducer: Reducer<T>,
initialState: T
){
this._state = initialState;
}
getState(): T {
return this._state;
}
dispatch(action: Action): void {
this._state = this.reducer(this._state, action);
}
}
→ constructor에서는 _state를 초기 상태로 설정했다.
→ getState()는 단순히 현재 _state를 리턴한다.
→ dispatch는 동작을 받아 이를 리듀서로 보낸 뒤 _state의 값을 리턴값으로 업데이트한다.
→ dispatch는 아무것도 리턴하지 않는다. 저장소의 상태를 '업데이트'할 뿐이다. (리덕스의 중요한 원칙)
저장소 사용하기
let store = new Store<number>(reducer, 0);
console.log(store.getState()); // 0
store.dispatch({type: 'INCREMENT'});
console.log(store.getState()); // 1
store.dispatch({type: 'INCREMENT'});
console.log(store.getState()); // 2
store.dispatch({type: 'DECREMENT'});
console.log(store.getState()); // 1
옵저버(Observer) 패턴 구현하기 - subscribe
앞에 예시에서는 상태 변화를 알려면 store.getState()가 필요하다. 이를 개선하기 위해 옵저버 패턴을 구현해 보겠다. 모든 변화를 구독하는 콜백함수를 등록할 것이다.
- 동작 과정
- subscribe를 사용하여 리스너(listener) 함수를 등록한다.
- dispatch가 호출되면 모든 리스너를 반복 호출한다. 이는 상태가 변경되었다는 알림이다.
// 리스너 콜백함수 인터페이스
interface ListenerCallback {
(): void;
}
// '구독 해지' 함수용 인터페이스
interface UnsubscribeCallback {
(): void;
}
class Store<T> {
private _state: T;
private _listeners: ListenerCallback[] = [];
constructor(
private reducer: Reducer<T>,
initialState: T
){
this._state = initialState;
}
getState(): T {
return this._state;
}
dispatch(action: Action): void {
this._state = this.reducer(this._state, action);
this._listeners.forEach((listener: ListenerCallback) => listener()); // 상태가 변경될 때마다 모든 리스너가 호출된다.
}
subscribe(listener: ListenerCallback): UnsubscribeCallback {
this._listeners.push(listener);
return () => { // '구독할 수 없는' 함수를 리턴한다.
this._listeners = this._listeners.filter(l => l !== listener);
};
}
}
let store = new Store<number>(reducer, 0);
console.log(store.getState()); // 0
// 구독
let unsubscribe = store.subscribe(() => {
console.log('subscribed: ', store.getState());
});
store.dispatch({type: 'INCREMENT'}); // subscribed: 1
store.dispatch({type: 'INCREMENT'}); // subscribed: 2
unsubscribe();
store.dispatch({type: 'DECREMENT'}); // 로그 안찍힘
console.log(store.getState()); // 1 -> 주시하지 않아도 감소가 일어난다.
→ 동작은 여전히 디스패치할 수 있지만 저장소를 요청해야 그 결과를 확인할 수 있다.
'상태관리' 카테고리의 다른 글
처음 접해보는 MobX - 1편 (0) | 2020.02.13 |
---|---|
Redux Reducer와 Array.reduce (0) | 2019.12.26 |
[Redux & Reducer] 기초 개념 2 (0) | 2019.12.10 |