Redux-observable 예제 따라해보기 (feat. React + TypeScript)

Redux-observable 예제 따라해보기 (feat. React + TypeScript)

목차

오류가 있다면 언제든지 지적해주세요.
코드: https://github.com/feel5ny/redux-observable-practice


https://www.youtube.com/watch?v=AslncyG8whg
Not familiar with Observables/RxJS v6?
redux-observable requires an understanding of Observables with RxJS v6. If you’re new to Reactive Programming with RxJS v6, head over to http://reactivex.io/rxjs/ to familiarize yourself first.

redux-observable (because of RxJS) truly shines the most for complex async/side effects. If you’re not already comfortable with RxJS you might consider using redux-thunk for simple side effects and then use redux-observable for the complex stuff. That way you can remain productive and learn RxJS as you go. redux-thunk is much simpler to learn and use, but that also means it’s far less powerful. Of course, if you already love Rx like we do, you will probably use it for everything!

If you’re not already comfortable with RxJS you might consider using redux-thunk for simple side effects and then use redux-observable for the complex stuff.

“만약 당신이 아직 rxjs에 익숙하지 않다면, 간단한 사이드이팩트용으로는 redux-thunk를 사용하는 걸 고려하고, 복잡한 stuff에서는 redux-observable을 사용하는 것이 좋습니다.” 링크

그래서 한번 공부해보기로 했다.


## 1. Basic ### 1.1 epic이라는 개념 리덕스 옵저버블에는 epic이라는 구조를 사용한다.
1
function (action$: Observable<Action>, state$: StateObservable<State>): Observable<Action>;

It is a function which takes a stream of actions and returns a stream of actions. Actions in, actions out.
에픽함수는 액션스트림을 가져가고, 액션스트림을 반환하는 함수이다.
간단하게 말하자면, action 객체를 store에서 ActionObservable로 얻게되는데,
에픽에서는 액션 옵저버블을 스토어에서 받아서 추가적인 처리후 다시 옵저버블로 반환할 수 있다.
추가적인 처리에는 rxjs의 operator 등을 사용한다.

1.2 미들웨어 셋팅

  • createEpicMiddleware를 사용한다.
    store로 들어오는 action객체를 옵저버블로 반환하는 역할을 한다.
  • redux 상태변화를 관찰하기 위해 logger 셋팅.
  • store에 리듀서와 미들웨어를 적용하면서 create한다.
  • epic을 모아둔 rootEpic을 등록한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// store.ts
import { applyMiddleware, createStore } from "redux";
import { createEpicMiddleware } from "redux-observable";
import logger from "redux-logger";
import { rootReudcer } from "./reducers";
import { rootEpic } from "./epics";

const epicMiddleware = createEpicMiddleware();

export default function configureStore() {
const store = createStore(
rootReudcer,
applyMiddleware(epicMiddleware, logger)
);

epicMiddleware.run(rootEpic);

return store;
}
1
2
3
4
5
6
7
8
9
10
// createEpicMiddleware : index.d.ts
...
export declare interface Epic<Input extends Action = any, Output extends Input = Input, State = any, Dependencies = any> {
(action$: ActionsObservable<Input>, state$: StateObservable<State>, dependencies: Dependencies): Observable<Output>;
}

export interface EpicMiddleware<T extends Action, O extends T = T, S = void, D = any> extends Middleware {
run(rootEpic: Epic<T, O, S, D>): void;
}
export declare function createEpicMiddleware<T extends Action, O extends T = T, S = void, D = any>(options?: Options<D>): EpicMiddleware<T, O, S, D>;

1.3 액션 셋팅

깃헙에서 정보를 받아오는 ajax요청 처리를 해보자.

액션파일에는

  1. 시작액션 FETCH_USER
  2. 성공액션 FETCH_USER_FULFILLED
  3. 실패액션 FETCH_USER_REJECTED
  4. 취소액션 (이건 필요에 따라) FETCH_USER_CANCELLED
1
2
3
4
5
// 액션 타입
export const FETCH_USER = "FETCH_USER";
export const FETCH_USER_FULFILLED = "FETCH_USER_FULFILLED";
export const FETCH_USER_CANCELLED = "FETCH_USER_CANCELLED";
export const FETCH_USER_REJECTED = "FETCH_USER_REJECTED";
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 액션 함수
export const cancelFetch = () => ({
type: FETCH_USER_CANCELLED
});

export const fetchUser = (username: string) => ({
type: FETCH_USER,
payload: username
});

export const fetchUserFulfilled = (payload: any) => ({
type: FETCH_USER_FULFILLED,
payload
});

1.4 reducer 셋팅

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
const users = (
state = {
joy: {
avatar_url: ""
},
error: "",
isFetchingUser: false
},
action: ActionInterface
) => {
switch (action.type) {
case FETCH_USER:
return { ...state, isFetchingUser: true };
case FETCH_USER_FULFILLED:
return {
...state,
joy: action.payload,
isFetchingUser: false
};
case FETCH_USER_REJECTED:
return { ...state, error: action.payload, isFetchingUser: false };
case FETCH_USER_CANCELLED:
return { ...state, isFetchingUser: false };
default:
return state;
}
};

export const rootReudcer = combineReducers({
ping: pingReducer,
user: users
});

## 2. epic 작업 - epic이 여러개가 있을 경우 combineEpics를 사용한다. - 도트체이닝 없이 pipe를 사용하여 순차적으로 받아서 처리한다. - action type에 따라 처리하기 위해 redux-observable의 `ofType` 메서드를 사용한다. - `mergeMap`: 내부 옵저버블이 방출되면, 해당 값을 바깥 옵저버블과 함께 병합한다. - `race`: 레이스 오퍼레이터. 인자로 들어온 옵저버블 중에 제일 빨리 처리된 옵저버블을 반환한다. - `delay`: 해당 초만큼 딜레이 이후 다음 오퍼레이터를 실행한다. - `takeUntil`: 특정 액션이 들어올 때 동작을 취소할 수 있다. - `catchError`: 에러 핸들링
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
export const fetchUserEpic = (
action$: Observable<Action>,
state$: StateObservable<any>
): Observable<Action> => {
return action$.pipe(
ofType(FETCH_USER),
mergeMap((action: ActionInterface) =>
race(
ajax.getJSON(`https://api.github.com/users/${action.payload}`).pipe(
delay(1000),
map(response => fetchUserFulfilled(response)),
takeUntil(action$.pipe(ofType(FETCH_USER_CANCELLED))),
catchError(error =>
of({
type: FETCH_USER_REJECTED,
payload: error.xhr.response,
error: true
})
)
)
)
)
);
};

export const rootEpic = combineEpics(fetchUserEpic, ...);

참고

  1. https://redux-observable.js.org/docs/basics/Epics.html
  2. https://blog.sapzil.org/2017/07/16/redux-observable/
  3. https://wonism.github.io/redux-saga-vs-redux-observable/
📚