프로필사진
RxJS - 검색 Input 만들기 : debounceTime and distinctUntilChanged

2020. 7. 6. 19:00🔴 FE/RxJS

300x250

Angular로 검색 기능의 input box를 개발하려고 한다.

그냥 단순한 input box라고 생각할 수도 있겠지만, 여러 번의 불필요한 검색 호출을 막아 좀 더 효율적인 검색 기능을 만들기로 한다.
이를 위해 나는RxJS의  debounceTime + distinctUntilChanged 조합을 사용하기로 했다.

출처 : @kavisha.talsania

내가 원하는 검색 input은 대충 위와 같은 모습이다.
크게 2가지 조건이 있는데,

1. 시간을 정해서 지정된 시간 안에
여러 번 검색 버튼을 연속으로 눌러도
한 번의 결과값만 검색되도록 한다.

2. 직전의 검색어와 현재 검색한 검색어를 비교하여
같은 검색어가 두 번 검색되지 않도록 한다.


debounceTime과 distinctUntilChanged을
아주 깔끔하게 정리해놓은 포스트가 있어서
정리도 해볼겸 번역을 해보았다.

https://medium.com/@kavisha.talsania/rxjs-debouncetime-and-distinctuntilchanged-c9f11db4d45f

 

RxJS — debounceTime and distinctUntilChanged

RxJS operators are awesome, but you need to aware of where you might use them to enhance your application.

medium.com

RxJS operator인 debounceTime과 distinctUntilChanged을 활용한 예제를 보자.
우리는 username을 위한 input이 하나 있고, 키를 누를 때마다 유효성을 검사해야 한다.
따라서 사용자가 로그인을 할 때, 사용자가 적은 username이 유효한지 아닌지 바로 알려주려고 한다.
여기서 우리가 직면한 이슈는 API나 HTTP request를 호출하여 해당 username이 사용 가능한지 확인해봐야 한다는 것이다.
만약 사용자가 키를 누를 때마다 확인을 한다면 서버에 많은 양의 request를 보내야 한다.
이러한 이슈를 해결하기 위해서 우리는 RxJS의 debounceTime과 distinctUntilChanged를 사용할 수 있다.

먼저 간단하게 observable를 생성하여 키를 누를 때마다 이벤트를 emit 해보자.

let input = document.querySelector('input');
let observable = Rx.Observable.fromEvent(input, 'input');

observable
.subscribe({
  next: function(event) {
    console.log(event.target.value);
  }
});

그 결과는 이러하다 :

자판을 하나씩 누를 때마다 value가 로그에 찍힌다.
하지만 사실 우리는 HTTP request를 호출해야 하는데, 이렇게 서버에 많은 request를 호출하면 안 된다.
여기서 우리는 RxJS operator를 활용해서 얼마나 자주 특정 행동을 해야 하는지 컨트롤할 수 있다.
첫 번째로 소개할 유용한 operator은 바로 debounceTime이다.

debounceTime은 output사이에서 특정 시간보다 적은 시간에 배출된 값들을 버린다.

debounceTime에서 우리는 파라미터로 밀리세컨드 단위의 시간을 지정한다.
그렇게 하면 해당 시간보다 적은 시간 안에 배출된 값들은 받지 않게 된다.
(대충 이야기하자면.. 5초로 지정했으면, 4.9999초 내에서 나온 결과들은 버린다는 뜻)

이리하여 지정된 시간마다 새로운 값을 받을 수 있게 되었다.
아래 예시에서는 사용자가 타이핑을 멈추고 500 밀리세컨드가 지나면 새로운 값이 배출된다.

let input = document.querySelector('input');
let observable = Rx.Observable.fromEvent(input, 'input');

observable
.debounceTime(500)
.subscribe({
  next: function(event) {
    console.log(event.target.value);
  }
});

그리고 결과는 이러하다 :

하지만 여기에는 또 다른 이슈가 있다.
만약 사용자가 "DAN"이라고 쓰고 잠시 멈춰서 "DAN"이라는 값이 배출되고 난 뒤,
뒤에 "NY"를 붙여서 "DANNY"라고 쓰고 나서,
새로 추가한 "NY"를 재빨리 지우고 멈춘다면
"DAN"이라는 똑같은 값이 배출된다.

우리는 distinctUntilChanged operator를 사용하여 똑같은 값이 또다시 배출되는 것을 막을 수 있다.

distinctUntilChanged()은 현재의 값이 마지막 입력한 값과 다를 때만 값을 내보낸다.

var input = document.querySelector('input');
var observable = Rx.Observable.fromEvent(input, 'input');

observable
.debounceTime(500)
.distinctUntilChanged()
.subscribe({
  next: function(event) {
    console.log(event.target.value);
  }
});

결과 :

또다시 "DAN"이 로그에 찍혔다.. 그런데 우리는 더 이상 같은 값을 받지 않기로 설정하지 않았나..?
맞는 말이다. 하지만 여기서 배출된 값은 event object 전체이다.
우리는 event.target.value를 통해 값을 로그에 찍었기 때문에
아무리 값이 동일해도 event object는 그 전의 객체와 다르다.
우리는 아래와 같이 map()을 사용해서 간단하게 해결할 수 있다.

var input = document.querySelector('input');
var observable = Rx.Observable.fromEvent(input, 'input');

observable
.map(event => event.target.value)
.debounceTime(500)
.distinctUntilChanged()
.subscribe({
  next: function(value) {
    console.log(value);
  }
});

이렇게 해서 만약 새로운 값이 이전의 값과 동일하다면 배출되지 않게 만들었다.
이렇게 두 개의 operator를 사용해서 한 번에 서버에 request 하여 uesrname의 유효성을 검사할 수 있게 되었다.


https://alligator.io/angular/real-time-search-angular-rxjs/

 

Building a Real Time Search in Angular With RxJS

Building a simple real time search in Angular 2+ with observables and RxJs.

alligator.io

추가적으로 위의 사이트에서 만든 실시간 검색 기능 코드를 참고하면,
우리는 switchMap()을 활용해서 서버에서 받은 여러 개의 결과값들을 하나의 observable로 받을 수 있다.

switchMap

구독 중이던 Observable이 끝나기 전에 새로운 Observable을 구독하면
이전에 구독하고 있던 Observable을 구독 취소하고 다음 Observable을 구독한다.

즉, 서버에 요청하는 도중에 또 새로운 요청을 하면
처음에 요청 중이던 작업은 취소하고 그다음에 들어온 요청을 작업한다.

search(terms: Observable<string>) {
    return terms.debounceTime(400)
      .distinctUntilChanged()
      .switchMap(term => this.searchEntries(term));
  }

  searchEntries(term) {
    return this.http
        .get(this.baseUrl + this.queryUrl + term)
        .map(res => res.json());
  }

searchEntries에서 받은 결과들을 하나로 합쳐서 가장 최근의 request을 통해 받은 값을 전달한다.
결과적으로 사용자는 제일 마지막에 호출한 request에 대한 결과만을 받게 된다.

300x250