본문 바로가기
RxJS(Reactive X)

[RxJS] 실습 - 스마트한 키워드 검색창 만들기

by 찬찬2 2023. 1. 16.

링크 : https://www.yalco.kr/@rxjs/3-1/

 

 

소개 : 사용자의 입력에 따라 AJAX 호출로 서버에서 사용자 이름을 조회하는 기능이다.

 

계획(initial plan) : 기능 구현을 위해 단계별로 생각해보기. 작업 전 1차적으로 구상해보고 작업 완료 후 1차에서의 계획이 그대로 실현되었는지 생각해보고 다시 정리해 보기.

 

단계 : ①키 이벤트(입력) → ②Ajax 호출(검색) → ③결과 하단에 출력

 

사용될 연산자 : mergeMap, switchMap, pluck, filter, debounceTime, distinctUntilChange

 

■ mergeMap

const url = 'http://127.0.0.1:3000/people/quarter-error'
const keyword = document.querySelector('#keyword')
const result = document.querySelector('#result')

fromEvent(keyword, 'keyup').pipe(
  pluck('target', 'value'),
  mergeMap(keyword => 
    ajax(`${url}?name=${keyword}`).pipe(retry(3))
  ),
  pluck('response')
).subscribe(console.log)

 

mergeMap의 문제점 : 

1. 키를 입력할 때 마다 ajax 호출이 일어나고 그에 따른 결과값을 받아오기 때문에 아래 그림과 같이 키를 하나씩 입력시 자동완성탭이 수시로 바뀌는걸 볼 수 있다.

 

2. 불필요한 ajax 요청에 의해 서버에 부담을 줄 수 있다.

 

 

■ switchMap

 

const url = 'http://127.0.0.1:3000/people/quarter-error'
const keyword = document.querySelector('#keyword')
const result = document.querySelector('#result')

fromEvent(keyword, 'keyup').pipe(
  pluck('target', 'value'),
  // meregeMap 대신 switchMap 사용
  switchMap(keyword => 
    ajax(`${url}?name=${keyword}`).pipe(retry(3))
  ),
  pluck('response')
).subscribe(showResults)

function showResults (results) {
  from(results).pipe(
    map(person => `${person.first_name} ${person.last_name}`),
    map(name => `<article>${name}</article>`),
    scan((acc, article) => acc += article, '')
  ).subscribe(people => result.innerHTML = people)
}

 

1번 문제를 해결하기 위해 switchMap을 사용할 수 있다.

switchMap은 메인 스트림이 새 값을 발행하면 진행중이던 스트림을 멈추는 기능을 한다.

c를 입력 후 response가 도착하기 전에 h를 연달아 입력하면 c를 입력했을때의 ajax 요청이 멈추고 ch로 새로운 스트림을 만들어 ajax를 호출하는 것이다. 그리고 이렇게 발생된 모든 스트림들을 병합한다.

즉, 키가 입력될 때 마다 이전 ajax호출을 멈주는 것이다.

 

 

2번 문재를 해결하기 위해 pluck, filter, debounceTime, distinctUntilChange를 사용한다.

 

fromEvent(keyword, 'keyup').pipe(
  filter(event => event.code != 'Backspace'), // 백스페이스 생략
  pluck('target', 'value'),
  filter(typed => typed.length > 1),
  debounceTime(500),
  distinctUntilChanged(),
  switchMap(typed => 
    ajax(`${url}?name=${typed}`).pipe(retry(3))
  ),
  pluck('response')
).subscribe(showResults)

 

pipe에서 가공되는 과정은 아래와 같다...

1단계 filter - input 박스에 백스페이스키 입력을 제한한다.

2단계 pluck - input 박스에 입력된 값을 받아온다.

3단계 filter - input 박스의 문자열 길이가 하나 이상이어야 한다.

4단계 debounceTime - input 박스에 키를 입력할때 마다 0.5초의 시간을 재도록 한다. 그래야 input 박스에 키를 연속적으로 입력했을 때 마다 ajax 호출이 일어나지 않게 된다.

5단계 distinctUntilChanged - 사용자의 실수로 연속적인 알파벳을 입력했을때를 대비해 같은 문자가 연속적으로 입력되지 않도록 한다.

6단계 switchMap - 4단계에서 역속적인 입력 이벤트를 방지해주지만 switchMap으로 다시 한번 예방할 수 있다.

 

 

이로써 기능은 모두 완성되었다.

리팩토링을 조금 해보면, 기능의 성격에 따라 분리해보자.

 

추가 - ajax에 대한 ajax의 response가 오기 전  "searching 문구 출력"

스트림 분리1 - input 박스에서의 키입력

스트림 분리2 - 입력값에 따라 ajax 호출

마지막으로 두 개의 스트림(분리1, 분리2)을 병합 → 결과값을 HTML에 노출

 

// 추가
const searching$ = searchInit$.pipe(
  mapTo('<div class="searching">Searching...</div>')
)

// 분리1
const searchInit$ = fromEvent(keyword, 'keyup').pipe(
  filter(event => event.code != 'Backspace'), // 백스페이스 생략
  pluck('target', 'value'),
  filter(typed => typed.length > 1),
  debounceTime(500),
  distinctUntilChanged()
)

// 분리2
const searchResult$ = searchInit$.pipe(
  switchMap(keyword => 
    ajax(`${url}?name=${keyword}`).pipe(retry(3))
  ),
  pluck('response'),
  mergeMap(results => from(results).pipe(
    map(person => `${person.first_name} ${person.last_name}`),
    map(name => `<article>${name}</article>`),
    scan((acc, article) => acc += article, '')
  ))
)

// 두 개의 스트림 병합
merge(
  searching$,
  searchResult$
).subscribe(text => result.innerHTML = text)

 

 


 

처음 구상했던 계획은 아래와 같다.

 

①키 이벤트(입력) → ②Ajax 호출(검색) → ③결과 하단에 출력

 

그렇다면 처음 계획했던 내용과 무엇이 수정되고 달라졌는지 보자.

 

①키 이벤트(입력)

단순히 입력만 하는 것만 고려할 것이 아니라 이벤트 처리과정에 대해 생각해 볼 필요가 있었다. mergeMap을 사용했을 때 불필요한 Ajax 호출이 키를 입력할 때 마다 일어났었고, 이를 해결하기 위해 switchMap, debounceTime 연산자를 사용해 문제를 해결했다.

사용자는 입력뿐만 아니라 입력한 내용을 지울 수도 있다라는 사용자관점을 생각하지 못했다. 백스페이스를 눌렀을때 어떻게 처리할지 생각하지 못했다. 우선 filter 연산자를 통해 키 코드가 backspace인 것을 골라내었다.

 

②Ajax 호출(검색)

성공, 실패 각각 결과에 따라 어떻게 처리할 것인지 생각했다. retry 연산자로 ajax요청 실패 시 최소 3번은 다시 시도할 수 있도록 한다.

 

결론

크게 수정되거나 달라진 것은 없지만, 전반적인 "처리과정"에 대해 머릿속으로 설계도를 그리는 연습을 해야할 것 같고, 사용자관점에서도 조금 더 깊게 생각하면 좋을 것 같다.

 

댓글