성장, 그리고 노력

부족하더라도 어제보다 더 잘해지자. 노력은 절대 배신하지 않는다.

상태관리

[Redux & Reducer] 기초 개념 1

제이콥(JACOB) 2019. 12. 10. 01:13

리듀서(Reducer)란?

→ 리듀서는 이전 상태와 동작을 받아 새 상태를 리턴한다.

→ 리듀서는 반드시 순수 함수여야 한다. 이를테면 데이터베이스 호출이나 HTTP 호출 등 외부의 데이터 구조를 변형하는 호출은 허용되지 않는다.

→ 리듀서는 항상 현재 상태를 '읽기 전용'으로 다룬다. 기존 상태를 변경하지는 않지만 새 상태를 리턴은 할 수 있다. 

 

리듀서가 포함하고 있는 세 가지.

  1. 할 일을 정의하는 Action(인수는 옵션)
  2. 애플리케이션의 모든 데이터를 저장하는 state
  3. 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()가 필요하다. 이를 개선하기 위해 옵저버 패턴을 구현해 보겠다. 모든 변화를 구독하는 콜백함수를 등록할 것이다.

 - 동작 과정 

  1. subscribe를 사용하여 리스너(listener) 함수를 등록한다.
  2. 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