이번 DevTrend 프로젝트에서는 비동기 처리 라이브러리로 RxJS를 사용하기로 결정했다. RxJS 및 Reactive X에 관한 설명은 https://velog.io/@dvmflstm/RxJS-Practice 에서 충분히 했으니 넘어가도 될 것 같다.
이번 프로젝트에서 고려해야 하는 비동기 처리 시나리오를 나열해 보면 다음과 같다.
위와 같은 비동기요청 시나리오를 axios 등의 일반적인 비동기 요청 라이브러리를 사용해 구현하려면 상당히 복잡하고 가독성이 떨어지는 코드가 나올 것이다. 이를 rxJS를 이용해 구현한 코드는 아래와 같다.
const isSameInfo = (x : Info, y : Info) => x.keyword === y.keyword && x.period === y.period
rx.infoSbj
.pipe(debounceTime(200))
.pipe(distinctUntilChanged(isSameInfo))
.pipe(switchMap(((value : Info) => {
const fd = new FormData();
fd.append('sql', sqlQuery.query1(value.keyword, value.period));
return ajax.post(`${SEDE_URL}/save/1`, fd)
.pipe(map(r => r.response))
.pipe(switchMap(response => {
if(response.running) {
return ajax.get(`${SEDE_URL}/job/${response.job_id}`)
.pipe(map(r => r.response))
.pipe(map(response => {
if(response.running) throw 'waiting'
else return response.resultSets[0];
}))
.pipe(retryWhen(errors =>
errors
.pipe(delay(1000))
.pipe(takeWhile(error => error === 'waiting'))))
.pipe(retry(3))
}
else return response.resultSets;
}))
})))
.subscribe((value => console.log(value)))
infoSbj는 검색에 대한 정보(language, period)를 방출하는 subject이며, 비동기 요청을 날리고 싶은 컴포넌트의 useEffect함수에서 이를 subscribe해주었다. 여러 가지 rxJS 함수들의 도움으로 어찌저찌 코드를 작성하긴 했지만, 가독성이 심각하게 떨어지는 관계로 좀 더 예쁘게 리팩토링해보았다.
const isSameInfo = (x : Info, y : Info) => x.keyword === y.keyword && x.period === y.period
const tryGetResult = (response : any) => {
if(response.captcha) {
alert('recaptcha 인증이 필요합니다. 아래 사이트에서 인증을 진행해주세요.\n\n ' +
'https://data.stackexchange.com/stackoverflow/query/new');
}
if(response.running) {
return ajax.get(`${SEDE_URL}/job/${response.job_id}`)
.pipe(map(r => r.response))
.pipe(map(response => {
if(response.running) throw 'waiting';
else return response.resultSets[0];
}))
.pipe(retryWhen(errors =>
errors
.pipe(delay(1000))
.pipe(takeWhile(error => error === 'waiting'))))
.pipe(retry(3))
}
else return response.resultSets;
};
useEffect(() => {
const queryPost = (info : Info) => {
const fd = new FormData();
if(info.keyword === '') fd.append('sql', sqlQuery.query2(info.period));
else fd.append('sql', sqlQuery.query1(info.keyword, info.period));
return ajax.post(`${SEDE_URL}/save/1`, fd)
.pipe(map(r => r.response))
.pipe(switchMap(tryGetResult))
};
const onNext = (value : any) => {
setData(value);
loadingDispatch({type : "FINISH"});
};
rx.infoSbj
.pipe(debounceTime(200))
.pipe(distinctUntilChanged(isSameInfo))
.pipe(switchMap(queryPost))
.subscribe(onNext)
}, [loadingDispatch]);
복잡한 부분을 적절히 리팩토링하고, 적당한 함수 이름을 명명함으로써 한층 읽기 쉬워진 코드를 작성할 수 있었다.