2부 Chapter 05. 자동완성 UI 만들기
1.1. 자동완성 UI란?
- 검색어를 입력하면 입력한 내용과 관련 있는 문장이나 결과를 보여주는 UI
1.2. 사용자의 키입력 처리 : 사용자가 입력한 검색어는 어떻게 얻나요?
- 자동완성 UI는 사용자의 키입력부터 시작
- 예제는 Gitbub 사용자를 검색하는 자동완성 UI
- 사용자의 키입력을 받은 이후 입력값을 서버로 전달하여 검색결과를 리스트로 표현
<!-- keyup 이벤트를 받는 input 박스 만들기 -->
<input id="search" type="input" placeholder="검색하고 싶은 사용자 아이디를 입력해주세요." />const {fromEvent} = rxjs;
const keyup$ = fromEvent(document.getElementById('search'),'keyup'); // formEvent 함수를 이용해서 keyup 이벤터를 Observable로 만듦
keyup$.subscribe(event => {
console.log('사용자 입력 키보드 이벤트',event); // 키 입력시 키보드 이벤트 전달 여부 확인
})1.2.1. 전달되는 값을 바꾸고 싶어요.
- Observable을 구독하는 Observer 에서는 로직을 다루지 않음
- 가능하면 데이터 처리에 대한 부분은 모두 Observable에서 처리
-
Observable은 처리가 완료된 데이터만 전달
- Observable을 하나의 독립된 데이터 소스로 취급
- Observable을 소비하는 Observer는 전달되는 데이터에만 초점을 맞춰 처리
-
Observer는 전달된 데이터만을 다루기 때문에 로직이 단순해져서 Observable과 느슨한 의존성을 가지게 됨
- 자동완성 UI에서 전달된 키보드이벤트는 단순히 사용자의 입력 여부를 확인하는 용도
-
키보드 이벤트로 전달되는 데이터를 현재 input에 입력된 검색어로 변환해야 함
- map 오퍼레이터 사용
- 기존 값을 특정 함수를 통해 다른 값으로 맵칭하는 작업을 수행
- Observable에 전달된 값을 변경하여 전달하는 역할
- input에 입력된 검색어는 이벤트가 발생한 타깃(search input)의 value 속성으로 얻을 수 있음
- 우리가 변경해야 할 값은 event.target.value
const { fromEvent } = rxjs;
const { map } = rxjs.operators;
const keyup$ = fromEvent(document.getElementById('search'),'keyup')
.pipe(
map(event => event.target.value)
);
keyup$.subscribe(event => {
console.log('search input에 입력된 값',event);
});1.3. 서버에서 데이터 받기 : 검색 결과는 어떻게 얻나요?
- 서버에서 데이터를 받는 것 역시 데이터 소스이기에 Observable로 만들 수 있음
- XMLHttpRequest 객체가 있지만 예제에서는 Prmose를 반환하는 Fetch API를 사용
- Fetch의 모든 메소드는 Promose로 반환
- 이를 Observable로 만들기 위해서는 from 함수를 사용
const { from } = rxjs;
const request$ = from(
fetch('https://api.github.com/search/users?q=sculove')
.then(res => res.json())
);
request$.subscribe(event => {
console.log('서버로 전달받은 JSON 값',event);
})1.4. 검색어로 서버 요청하기
- keyup$과 request$ 둘을 연결하면 원하는 데이터를 얻을 수있음
- keyup$이 전달한 데이터(검색어)를 request$이 받아서 서버의 결과값을 얻기
- 검색어가 서버의 검색결과로 변경된다는 것을 의미
- map 오퍼레이터를 이용하여 검색어를 서버의 검색 결과로 변경
const { fromEvent } = rxjs;
const { map } = rxjs.operators;
const { ajax } = rxjs.ajax;
const user$ = fromEvent(document.getElementById('search'),'keyup')
.pipe(
map(event => event.target.value),
map(query => ajax.getJSON(`https://api.github.com/search/users?q=${query}`))
);
user$.subscribe(event => {
console.log('서버로부터 받은 검색 결과',event);
});- Observable을 반환하기 때문에 우리가 원하는 값을 얻을 수 없음

1.4.1. The Observable of Observables
- 평탄화
- Observable 안에 있는 여러 개의 Observable을 꺼내서 Observable 형태로 만드는 것
- 중첩된 구조를 단순화 하는 것을 의미
-
RxJS에서는 각각의 Observable을 합쳐서 하나의 Observable로 만들기 때문에 Merge라는 용어를 사용
- mergeAll
- 여러 개의 Observable을 하나의 Observable로 평탄화 해주는 오퍼레이터

- 첫 번째 Observable에서 전달한 데이터 20, 40, 60, 80, 100과 두 번째 Observable에서 발생한 데이터 1,1을 합쳐서 새로운 Observable 데이터를 만듦
const { fromEvent } = rxjs;
const { map, mergeAll } = rxjs.operators;
const { ajax } = rxjs.ajax;
const user$ = fromEvent(document.getElementById('search'),'keyup')
.pipe(
map(event => event.target.value),
map(query => ajax.getJSON(`https://api.github.com/search/users?q=${query}`)),
mergeAll()
);
user$.subscribe(event => {
console.log('서버로부터 받은 검색 결과',event);
});
1.4.2. map 하고 mergeAll을 합치면 뭐라고 부르나?
- 자주 사용하는 오퍼레이터를 합쳐놓은 오퍼레이터가 있음
- map과 mergeAll을 합친 mergeMap

- 실무에서는 mergeAll 보다 mergeMap를 더 많이 사용함
const { fromEvent } = rxjs;
const { map, mergeMap } = rxjs.operators;
const { ajax } = rxjs.ajax;
const user$ = fromEvent(document.getElementById('search'),'keyup')
.pipe(
map(event => event.target.value),
mergeMap(query => ajax.getJSON(`https://api.github.com/search/users?q=${query}`))
);
user$.subscribe(event => {
console.log('서버로부터 받은 검색 결과',event);
});1.5. 데이터는 받아왔으나 사용할 수가 없다.
- 검색어를 입력하면 403에러와 433 에러가 발생하는 경우가 있음
1.5.1. 403 에러 처리 : 빈번한 요청은 안 돼요
- 403 에러가 발생하는 원인은 다수의 ajax request를 거의 동시에 호출하기 때문
- 가급적 불필요한 request를 보내지 않아야 함
- 매번 키를 입력할 때마다 보내지 않고 일정시간이 지난 이후에 검색어가 더이상 입력되지 않는 시기에 요청하는 것이 효과적
- 과다한 요청을 방지하게 위한 프로그래밍 기술, 디바운스(debounce)
- RxJS에서는 debounceTime 오퍼레이터를 제공

- 디바운스
- 요청이 반복되는 시간에는 요청을 방지
- 요청이 멈추었을 때 마지막 요청을 설정한 시간이 지난 이후에 요청하는 기술
- 자동완성 UI에 많이 사용
- 스로틀(throttle)
- 많은 요청이 오더라도 설정한 시간 내에 딱 한 번 호출되도록 만드는 기슬
- 스크롤 이벤트와 같이 빈번한 요청이 자주 일어나는 곳에 사용
- RxJS에서는 throttleTime 오퍼레이터를 제공
const { fromEvent } = rxjs;
const { map, mergeMap, debounceTime } = rxjs.operators;
const { ajax } = rxjs.ajax;
const user$ = fromEvent(document.getElementById('search'),'keyup')
.pipe(
debounceTime(300), // 300ms 뒤에 데이터를 전달
map(event => event.target.value),
mergeMap(query => ajax.getJSON(`https://api.github.com/search/users?q=${query}`))
);
user$.subscribe(event => {
console.log('서버로부터 받은 검색 결과',event);
});1.5.2. 422 에러 처리 : 빈 검색어는 서버로 요청하며 안 돼요
- 검색어를 다 지우게 되는 경우 422 에러가 발생
- 빈 검색어를 서버에 요청했기 때문
- 이런 특정 조건의 데이터만을 선별하기 위해서는 filter 오퍼레이터를 사용

- 검색어가 없는 경우 요청하지 않고 있는 경우에만 요청
const { fromEvent } = rxjs;
const { map, mergeMap, debounceTime, filter } = rxjs.operators;
const { ajax } = rxjs.ajax;
const user$ = fromEvent(document.getElementById('search'),'keyup')
.pipe(
debounceTime(300), // 300ms 뒤에 데이터를 전달
map(event => event.target.value),
filter(query => query.trim().length>0),
mergeMap(query => ajax.getJSON(`https://api.github.com/search/users?q=${query}`))
);
user$.subscribe(event => {
console.log('서버로부터 받은 검색 결과',event);
});1.5.3. keyup 이벤트는 꼭 문자만을 받지 않는다
- 문자키 이외에 화살표 키 같은 문자키 이외의 키를 누를 때도 데이터를 전달 함
- 동일한 데이터가 전달될 경우 이전과 다른 데이터가 전달되기 전까지 데이터를 전달하지 않는 distinctUntilChanged 오퍼레이터

const { fromEvent } = rxjs;
const { map, mergeMap, debounceTime, filter, distinctUntilChanged } = rxjs.operators;
const { ajax } = rxjs.ajax;
const user$ = fromEvent(document.getElementById('search'),'keyup')
.pipe(
debounceTime(300), // 300ms 뒤에 데이터를 전달
map(event => event.target.value),
distinctUntilChanged(), // 특수키가 입력된 경우에는 나오지 않기 위해 중복 데이터 처리
filter(query => query.trim().length>0),
mergeMap(query => ajax.getJSON(`https://api.github.com/search/users?q=${query}`))
);
user$.subscribe(event => {
console.log('서버로부터 받은 검색 결과',event);
});1.6. 데이터가 왔으니 이제 그림을 그리자
- user$ 데이터에 따라 DOM을 변경하는 작업
1.6.1. 검색 결과창 만들기
<div class="autocomplete">
<input id="search" type="input" placeholder="검색하고 싶은 사용자 아이디를 입력해주세요." />
<ul id="suggestLayer"></ul>
</div>.autocomplete{
position: absolute;
width: 300px;
}
#search{
width: 100%;
}
#suggestLayer{
position: absolute;
top: 20px;
color: #666;
padding: 0;
margin: 0;
width: 100%;
}
#suggestLayer li{
border: 1px solid #bec8d8;
list-style: none;
}
.user img{
position: relative;
float: left;
margin-right: 10px;
}
.user p{
line-height: 50px;
margin: 0;
padding: 0;
}const $layer = document.getElementById('suggestLayer');
function drawLayer(items){
$layer.innerHTML = items.map(user => {
return `<li class="user">
<img src="${user.avatar_url}" width="50px" height="50px">
<p><a href="${user.html_url}" target="_blank">${user.login}</a></p>
</li>`;
}).join('');
};
const { fromEvent } = rxjs;
const { map, mergeMap, debounceTime, filter, distinctUntilChanged } = rxjs.operators;
const { ajax } = rxjs.ajax;
const user$ = fromEvent(document.getElementById('search'),'keyup')
.pipe(
debounceTime(300), // 300ms 뒤에 데이터를 전달
map(event => event.target.value),
distinctUntilChanged(), // 특수키가 입력된 경우에는 나오지 않기 위해 중복 데이터 처리
filter(query => query.trim().length>0),
mergeMap(query => ajax.getJSON(`https://api.github.com/search/users?q=${query}`))
);
user$.subscribe(v => {
drawLayer(v.items);
});