
Vue에서 input에 입력한 값에 따라 목록을 필터링하고, 검색 결과를 선택할 수 있는 간단한 자동완성 기능을 구현해봤다.
처음에는 입력값에 따라 목록만 필터링하고, 이후 검색 결과 선택과 초기화 기능까지 추가했다.
입력값으로 목록 필터링하기
먼저 검색할 데이터와 input 값을 저장할 searchTerm을 만든다.
<template>
<div>
<label>search number one to four</label>
<div>
<input
type="text"
:value="searchTerm"
@input="setSearchTerm"
/>
</div>
<ul>
<li
v-for="num in filteredList"
:key="num.value"
>
{{ num.name }}
</li>
</ul>
</div>
</template>
<script>
export default {
data() {
return {
dataList: [
{
name: 'One',
value: 'one',
},
{
name: 'Two',
value: 'two',
},
{
name: 'Three',
value: 'three',
},
{
name: 'Four',
value: 'four',
},
],
searchTerm: '',
};
},
methods: {
setSearchTerm(e) {
this.searchTerm = e.target.value;
},
},
computed: {
filteredList() {
if (this.searchTerm === '') {
return this.dataList;
}
return this.dataList.filter((num) =>
num.value.includes(this.searchTerm)
);
},
},
};
</script>
input 값이 변경될 때 setSearchTerm()에서 searchTerm을 변경하고, computed의 filteredList에서 해당 값을 기준으로 목록을 필터링한다.
return this.dataList.filter((num) =>
num.value.includes(this.searchTerm)
);
여기서는 간단하게 includes()를 사용했다.
따라서 문자열이 포함되어 있는지는 검색할 수 있지만, 한글 검색에서 초성 검색까지 지원하려면 별도의 로직이 필요하다.
검색 결과 선택 기능 추가
이제 input에 focus가 있을 때만 목록을 보여주고, 검색 결과를 클릭하면 해당 값을 input에 넣도록 수정했다.
<template>
<div>
<label>search number one to four</label>
<div>
<input
type="text"
:value="searchTerm"
@input="setSearchTerm"
@blur="setListOpen(false)"
@focus="setListOpen(true)"
/>
<button @click="removeSelectedNumber">
X
</button>
</div>
<ul v-if="isFocus">
<li v-if="filteredList.length === 0">
검색 결과가 없습니다.
</li>
<li
v-for="num in filteredList"
v-else
:key="num.value"
@mousedown="setSelectedNumber(num)"
>
{{ num.name }}
</li>
</ul>
</div>
</template>
<script>
export default {
data() {
return {
dataList: [
{
name: 'One',
value: 'one',
},
{
name: 'Two',
value: 'two',
},
{
name: 'Three',
value: 'three',
},
{
name: 'Four',
value: 'four',
},
],
searchTerm: '',
isFocus: false,
selectedObj: null,
};
},
methods: {
setSearchTerm(e) {
this.searchTerm = e.target.value;
},
setListOpen(isOpen) {
this.isFocus = isOpen;
},
setSelectedNumber(numObj) {
this.selectedObj = numObj;
this.searchTerm = numObj.name;
},
removeSelectedNumber() {
this.selectedObj = null;
this.searchTerm = '';
},
},
computed: {
filteredList() {
if (this.searchTerm === '') {
return this.dataList;
}
return this.dataList.filter((num) =>
num.value.includes(this.searchTerm)
);
},
},
};
</script>
추가한 기능은 다음과 같다.
- input에 focus가 있을 때만 검색 결과 표시
- 검색 결과가 없으면 안내 문구 표시
- 검색 결과를 선택하면 해당 값을 input에 표시
- X 버튼을 누르면 선택값과 검색어 초기화
목록은 실제 화면에서 사용할 때 position: absolute와 z-index를 적용하면 input 아래에 드롭다운 형태로 띄울 수 있다.
목록이 많다면 max-height와 overflow-y: auto를 같이 적용하면 된다.
.autocomplete-list {
position: absolute;
z-index: 10;
max-height: 200px;
overflow-y: auto;
}
@click 대신 @mousedown을 사용한 이유
구현하면서 한 가지 문제가 있었다.
검색 결과의 li에 처음에는 @click 이벤트를 사용했는데, 결과를 클릭해도 선택 이벤트가 실행되지 않았다.
원인은 input의 blur 이벤트였다.
input 밖의 검색 결과를 클릭하면 대략 다음 순서로 이벤트가 발생한다.
mousedown
→ blur
→ mouseup
→ click
input에서 blur가 발생하면 아래 코드가 먼저 실행된다.
setListOpen(false);
그러면 isFocus가 false로 변경되면서 검색 결과 목록이 화면에서 제거된다.
즉, 실제 click 이벤트가 발생할 시점에는 클릭하려던 li 자체가 이미 없어져 버리는 문제가 생긴다.
그래서 선택 이벤트를 click보다 먼저 발생하는 mousedown으로 변경했다.
<li
v-for="num in filteredList"
:key="num.value"
@mousedown="setSelectedNumber(num)"
>
{{ num.name }}
</li>
이렇게 변경하면 input의 blur로 목록이 사라지기 전에 선택 이벤트가 먼저 실행되어 정상적으로 값을 가져올 수 있다.
이 부분이 자동완성을 직접 구현하면서 가장 헷갈렸던 부분이었다.
참고: v-model을 사용해도 된다
이번 코드에서는 입력 이벤트를 직접 처리하기 위해 :value와 @input을 사용했다.
<input
:value="searchTerm"
@input="setSearchTerm"
/>
하지만 자동완성 기능을 구현한다고 해서 반드시 이 방식을 사용해야 하는 것은 아니다.
아래처럼 v-model로 구현해도 된다.
<input v-model="searchTerm" />
검색 결과를 선택할 때
this.searchTerm = numObj.name;
처럼 값을 변경하면 input에도 그대로 반영된다.
따라서 실제 프로젝트에서는 필요한 동작이나 컴포넌트 구조에 따라 편한 방식을 선택하면 된다.
정리
간단한 자동완성 기능이라면 별도의 라이브러리 없이도 Vue의 computed와 몇 가지 이벤트만으로 구현할 수 있다.
이번 구현에서는
- computed로 검색 결과 필터링
- focus, blur로 검색 목록 표시 여부 처리
- 검색 결과 선택
- 선택값 초기화
- 검색 결과가 없을 때 안내 문구 표시
정도까지 구현했다.
특히 input의 blur 때문에 검색 결과의 click 이벤트가 실행되지 않는다면 이벤트 발생 순서를 확인해보는 게 좋다.
내 경우에는 @click을 @mousedown으로 변경해서 해결했다.
최초작성: 2023년 2월 21일