1. catchError 연산자
catchError 연산자는 에러가 발생했을 때 인자로 사용하는 선택자 함수로 해당 에러를 전달하여 리턴하는 옵저버블을 대신 구독하는 연산자다.

연산자 원형
catchError<T, R>(
selector: (err: any, caught: Observable<T>) => ObservableInput<R>
): OperatorFunction<T, T | R>
소스 옵저버블에서 에러가 발생했을 때는 값을 더 발행하지 않는다. 이 때 catchError 연산자는 선택자(selector) 함수에서 리턴하는 옵저버블을 구독해 값을 발행한다.
- 에러 발생후에도 값을 발행해야 할 때
- 에러 발생 없이 프로그램 실행을 끝낼 때
/* catchError 연산자의 사용 예 */
const { from, of } = require('rxjs');
const { map, tap, pluck, catchError } = require('rxjs/operators');
const integers = ['1', '2', '3', 'r', '5'];
from(integers).pipe(
map((value, index) => ({ value, index })),
tap(valueIndex => {
const { value } = valueIndex;
const { index } = valueIndex;
if (!Number.isInteger(parseInt(value, 10))) {
const error = new TypeError(`${value}은(는) 정수가 아닙니다`);
error.index = index;
error.integerCheckError = true;
throw error;
}
}),
pluck('value'),
catchError(err => {
if (err.name === 'TypeError' && err.integerCheckError) {
const catchArray = [err.message];
const restArray = integers
.slice(err.index, integers.length)
.map(x => `에러 후 나머지 값 ${x}`);
return from([err.message].concat(restArray));
}
return of(err.message);
})
).subscribe(x => console.log(x), err => console.error(err));
[코드 10-1] catchError 연산자의 사용 예
실행 결과
1
2
3
r은(는) 정수가 아닙니다
에러 후 나머지 값 r
에러 후 나머지 값 5
tap 연산자에서 해당 값을 parseInt 로 바꿔 정수인지 검사하는데 r은 정수값이 아니므로 TypeError 를 전달한다. 이 때 에러각 발생한 지점의 index 값을 error에 넣고 catchError 연산자의 선택자 함수는 err 객체를 전달 받는다. 해당 레러가 정수 확인 중 전달된 에러가 맞다면 에러 메세지를 출력한 후 해당 index 부터 나머지 값을 자례로 발행한다. 다른 에러면 에러 메세지만 발행하도록 옵저버블을 리턴한다.
subscribe 함수에 error 함수도 설정했지만, catchError 연산자가 에러를 적절히 처리해줘서 해당 함수를 호출하지 않는다. 하지만, catchError 연산자의 선택자 함수에서 리턴받아 구독한 옵저버블에서 에러가 발생하면 error 함수를 호출한다.
1-1. mergeMap 연산자를 사용한 catchError 연산자 응용
[코드 10-1]은 에러가 발생한 원래 스트림에서 에러가 발생한 값만 처리하고 나머지 값들을 그대로 처리할 수 없다. 따라서 mergeMap 연산자를 추가한후 소스 옵저버블에서 발행하는 값 각각을 옵저버블로 감싸서 여기에 catchError 연산자를 적용해야 한다. 이렇게 하면 에러가 발생해도 나머지 값들을 계속 발행할 수 있다.
/* mergeMap 연산자 안에서 catchError 연산자 사용 */
const { from, of } = require('rxjs');
const { mergeMap, tap, catchError } = require('rxjs/operators');
from(['1', '2', '3', 'r', '5', '6', 'u', '8']).pipe(
mergeMap(x => {
return of(x).pipe(
tap(value => {
if (!Number.isInteger(parseInt(value, 10))) {
throw new TypeError(`${value}은(는) 정수가 아닙니다`);
}
}),
catchError(err => of(err.message))
);
})
).subscribe(x => console.log(x), err => console.error(err));
[코드 10-2] mergeMap 연산자 안에서 catchError 연산자 사용
실행 결과
1
2
3
r은(는) 정수가 아닙니다
5
6
u은(는) 정수가 아닙니다
8
에러가 없는 값을 그대로 발행하면서 에러가 발생했을 때는 에러 메세지를 발행하도록 동작한다.
2. retry 연산자
retry 연산자는 에러가 발생했을 때 인자로 설정한 정수값만큼 소스 옵저버블 구독을 재시도하는 연산자다. 인자로 설정한 값만큼 재시도하다가 다시 에러가 발생하면 이를 처리한다. 에러가 발생했을 때만 구독을 재시도 한다.
- 서버와 통신하면서 간헐적으로 에러가 발생했을 때 통신을 재시도하는 용도로 사용

연산자 원형
retry<T>(count: number = -1): MonoTypeOperatorFunction<T>
count 는 에러 발생 전 재시도 횟수를 설정한다. 기본값은 -1인데 이는 에러가 발생하지 않는다면 재시도를 하지 않는다는 것이다.
retry 연산자는 구독을 재시도 할 때 소스 옵저버블을 다시 구독해서 처음부터 값을 발행한다.
/* retry 연산자의 사용 예 */
const { interval, of } = require('rxjs');
const { take, mergeMap, tap, retry, catchError } = require('rxjs/operators');
interval(100).pipe(
take(30),
mergeMap(x => {
return of(x).pipe(
tap(value => {
if (Math.random() <= 0.3) {
throw new Error(`RANDOM ERROR ${value}`);
}
}),
retry(10), // 재시도 횟수와 유무에 따라 에러를 방지할 수 있음
catchError(err => of(err.message))
);
})
).subscribe(x => console.log(x), err => console.error(err));
[코드 10-3] retry 연산자의 사용 예
실행 결과 - retry 연산자 사용 할 경우
0
1
2
3
... 중간 생략
27
28
29
실행 결과 - retry 연산자 주석 처리할 경우
0
1
2
3
4
RANDOM ERROR 5
6
7
8
RANDOM ERROR 9
10
11
12
13
14
RANDOM ERROR 15
RANDOM ERROR 16
RANDOM ERROR 17
18
19
RANDOM ERROR 20
21
22
23
24
25
26
27
28
RANDOM ERROR 29
참고로 소스 옵저버블 구독을 처음부터 다시 재시도하려면 mergeMap 연산자를 사용하지 않아도 된다. 하지만, 여러 값을 발행한는 스트림 각각에 재시도를 해야 한다면 mergeMap 연산자를 사용해야 한다.
3. retryWhen 연산자
retrtWhen 연산자는 소스 옵저버블 구독을 재시도한다는 점에서는 retry 연산자와 비슷하지만, 구독을 재시도하기 전 에러를 전달받아 특정한 옵저버블에서 발행한 후 구독을 재시도하는 연산자다.

연산자 원형
retryWhen<T>(
notifier: (errors: Observable<any>) => Observable<any>
): MonoTypeOperatorFunction<T>
- notifier 함수 - 소스 옵저버블에서 에러가 발생했을 때 에러를 errors 라는 옵저버블로 다룬다. 이 옵저버블의 스트림을 전달받아 notifier에 리턴하는 옵저버블을 구독한다.
notifier 에서 리턴하는 옵저버블의 값을 발행하면 이어소 소스 옵저버블 구독을 재시도한다. 에러가 발행하면 전체 스트림의 구독을 종료한다. 따라서, 구독 재시도가 필요하면 재시도하기 전 해줘야 할 일을 처리한 후 아무 값이든 발행해 해당 스트림을 종료시키지 않도록 해야한다.
/* retryWhen 연산자의 사용 예 */
const { interval, of } = require('rxjs');
const { take, mergeMap, tap, retryWhen, scan, catchError } = require('rxjs/operators');
interval(100).pipe(
take(30),
mergeMap(x => {
return of(x).pipe(
tap(value => {
if (Math.random() <= 0.3) {
throw new Error(`RANDOM ERROR ${value}`);
}
}),
retryWhen(errors => {
return errors.pipe(
scan((acc, error) => {
return {
count: acc.count + 1,
error
};
}, { count: 0 }),
tap(errorInfo => {
console.error(`retryCount: ${errorInfo.count}, error message: ${errorInfo.error.message}`);
})
);
}),
catchError(err => of(err.message))
);
})
).subscribe(x => console.log(x), err => console.error(err));
[코드 10-4] retryWhen 연산자의 사용 예
실행 결과
0
1
retryCount: 1, error message: RANDOM ERROR 2
retryCount: 2, error message: RANDOM ERROR 2
2
retryCount: 1, error message: RANDOM ERROR 3
3
4
retryCount: 1, error message: RANDOM ERROR 5
5
6
7
8
9
10
11
12
13
retryCount: 1, error message: RANDOM ERROR 14
retryCount: 2, error message: RANDOM ERROR 14
retryCount: 3, error message: RANDOM ERROR 14
14
15
16
17
18
retryCount: 1, error message: RANDOM ERROR 19
retryCount: 2, error message: RANDOM ERROR 19
19
20
21
22
23
24
25
retryCount: 1, error message: RANDOM ERROR 26
retryCount: 2, error message: RANDOM ERROR 26
26
27
28
29
errors 로 에러를 전달 받으면 scan 연산자를 통해 count 를 누적해 몇 번째 구독 재시도인지와 그에 따른 에러 메세지를 로그로 출력된다.
[코드 10-4]를 응용하면 가끔 에러가 발생하는 서버 환경에서 어떤 에러 때문에 몇 번을 구독 재시도해 성공이나 실패했는지 테스트 할 수 있다.
3-1. n회 재시도 후 에러 없이 complete 함수 호출
/* 재시도 후 에러 없이 complete 함수 호출 */
const { interval, of } = require('rxjs');
const { take, mergeMap, tap, retryWhen, scan, catchError } = require('rxjs/operators');
interval(100).pipe(
take(30),
mergeMap(x => {
return of(x).pipe(
tap(value => {
if (Math.random() <= 0.5) {
throw new Error(`RANDOM ERROR ${value}`);
}
}),
retryWhen(errors => {
return errors.pipe(
take(2),
scan((acc, error) => {
return {
count: acc.count + 1,
error
};
}, { count: 0 }),
tap(errorInfo => {
console.error(`retryCount: ${errorInfo.count}, error message: ${errorInfo.error.message}`);
})
);
})
);
}),
catchError(err => of(err.message))
).subscribe(x => console.log(x), err => console.error(err));
[코드 10-5] 재시도 후 에러 없이 complete 함수 호출
실행 결과
0
retryCount: 1, error message: RANDOM ERROR 1
1
retryCount: 1, error message: RANDOM ERROR 2
2
retryCount: 1, error message: RANDOM ERROR 3
3
4
5
retryCount: 1, error message: RANDOM ERROR 6
6
retryCount: 1, error message: RANDOM ERROR 7
retryCount: 2, error message: RANDOM ERROR 7
8
9
retryCount: 1, error message: RANDOM ERROR 10
10
retryCount: 1, error message: RANDOM ERROR 11
11
12
13
14
retryCount: 1, error message: RANDOM ERROR 15
retryCount: 2, error message: RANDOM ERROR 15
retryCount: 1, error message: RANDOM ERROR 16
16
17
18
19
retryCount: 1, error message: RANDOM ERROR 20
retryCount: 2, error message: RANDOM ERROR 20
20
21
22
retryCount: 1, error message: RANDOM ERROR 23
retryCount: 2, error message: RANDOM ERROR 23
retryCount: 1, error message: RANDOM ERROR 24
24
retryCount: 1, error message: RANDOM ERROR 25
25
retryCount: 1, error message: RANDOM ERROR 26
26
27
retryCount: 1, error message: RANDOM ERROR 28
28
retryCount: 1, error message: RANDOM ERROR 29
retryCount: 2, error message: RANDOM ERROR 29
29
에러가 발생했을 때 원하는 횟수만큼만 구독을 재시도하다가 마지막 시도에서도 에러가 발생하면 재시도와 에러 처리 없이 complete 함수를 호출한다.
여기서 20은 두번의 재시도 끝에 성공했으므로 20이 발행된 것이다. 23은 두번의 재시도에서도 에러가 발생하여 23을 구독 하지 않고 완료한 것이다.
3-2. n회 재시도 후 에러 처리
/* 마지막 재시도 후 에러 처리 */
const { interval, of, throwError} = require('rxjs');
const { take, mergeMap, tap, retryWhen, scan, catchError } = require('rxjs/operators');
const n = 2;
interval(100).pipe(
take(30),
mergeMap(x => {
return of(x).pipe(
tap(value => {
if (Math.random() <= 0.5) {
throw new Error(`RANDOM ERROR ${value}`);
}
}),
retryWhen(errors => {
return errors.pipe(
scan((acc, error) => {
return {
count: acc.count + 1,
error
};
}, { count: 0 }),
mergeMap(errorInfo => {
if (errorInfo.count === n + 1) {
return throwError(errorInfo.error);
}
return of(errorInfo);
}),
tap(errorInfo => {
console.error(`retryCount: ${errorInfo.count}, error message: ${errorInfo.error.message}`);
})
);
}),
catchError(err => of(err.message))
);
})
).subscribe(x => console.log(x), err => console.error(err));
[코드 10-6] 마지막 재시도 후 에러 처리
실행 결과
retryCount: 1, error message: RANDOM ERROR 0
retryCount: 2, error message: RANDOM ERROR 0
0
retryCount: 1, error message: RANDOM ERROR 1
1
2
retryCount: 1, error message: RANDOM ERROR 3
3
4
retryCount: 1, error message: RANDOM ERROR 5
5
6
retryCount: 1, error message: RANDOM ERROR 7
7
8
retryCount: 1, error message: RANDOM ERROR 9
retryCount: 2, error message: RANDOM ERROR 9
RANDOM ERROR 9
retryCount: 1, error message: RANDOM ERROR 10
retryCount: 2, error message: RANDOM ERROR 10
10
retryCount: 1, error message: RANDOM ERROR 11
retryCount: 2, error message: RANDOM ERROR 11
11
retryCount: 1, error message: RANDOM ERROR 12
12
13
retryCount: 1, error message: RANDOM ERROR 14
retryCount: 2, error message: RANDOM ERROR 14
14
retryCount: 1, error message: RANDOM ERROR 15
retryCount: 2, error message: RANDOM ERROR 15
15
16
retryCount: 1, error message: RANDOM ERROR 17
17
retryCount: 1, error message: RANDOM ERROR 18
18
retryCount: 1, error message: RANDOM ERROR 19
19
retryCount: 1, error message: RANDOM ERROR 20
20
retryCount: 1, error message: RANDOM ERROR 21
21
retryCount: 1, error message: RANDOM ERROR 22
22
23
retryCount: 1, error message: RANDOM ERROR 24
retryCount: 2, error message: RANDOM ERROR 24
RANDOM ERROR 24
retryCount: 1, error message: RANDOM ERROR 25
retryCount: 2, error message: RANDOM ERROR 25
RANDOM ERROR 25
26
retryCount: 1, error message: RANDOM ERROR 27
27
28
retryCount: 1, error message: RANDOM ERROR 29
29
catchError 연산자가 없다면 n + 1 번째에서 에러가 발생했을 때 전체 스트림이 종료된다.
- 최대 n 번 구독 재시도하다가 에러가 발생한다면 retry 연산자를 사용
- 에러 처리 전, 에러 순서, 에러 특성에 따라 처리해야 할 것 이 있다면 retryWhen 연산자를 사용
next(재시도) 함수를 호출할지, error(에러를 냄) 함수를 호출할지, complete(에러 없이 완료) 함수를 호출할지 적절히 선택해서 사용해야 한다.
'RxJS' 카테고리의 다른 글
| 12장. 멀티캐스팅 연산자 요약 (0) | 2024.08.07 |
|---|---|
| 11장. 서브젝트 요약 (0) | 2024.08.06 |
| 9장. 조건 연산자 요약 (0) | 2024.08.05 |
| 8장. 유틸리티 연산자 요약 (0) | 2024.08.02 |
| 7장. 수학 및 결합 연산자 요약 (0) | 2024.08.02 |