DevTrend - 비동기 처리

백근영·2019년 10월 27일
0
post-thumbnail

이번 DevTrend 프로젝트에서는 비동기 처리 라이브러리로 RxJS를 사용하기로 결정했다. RxJS 및 Reactive X에 관한 설명은 https://velog.io/@dvmflstm/RxJS-Practice 에서 충분히 했으니 넘어가도 될 것 같다.

이번 프로젝트에서 고려해야 하는 비동기 처리 시나리오를 나열해 보면 다음과 같다.

  • /save/1 로 sql query를 formData에 담아서 post 요청을 보낸다.
  • 해당 query의 결과가 cache에 저장되어 있으면 바로 결과를 내보낸다.
  • query를 직접 수행해야 하는 경우면, sql문 처리가 완료될 때 까지 /job/{job_id} 로 get 요청을 보낸다.(response.running이 사라질 때까지)
  • 위 get 요청을 보내는 주기는 1초로 한다.
  • 위 get 요청이 실패할 시, 최대 3번까지 재시도한다.
  • 위 get 요청이 정상적으로 query 실행 결과를 반환하면 완료

위와 같은 비동기요청 시나리오를 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]);

복잡한 부분을 적절히 리팩토링하고, 적당한 함수 이름을 명명함으로써 한층 읽기 쉬워진 코드를 작성할 수 있었다.

profile
서울대학교 컴퓨터공학부 github.com/BaekGeunYoung

0개의 댓글