| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 4 | 5 | ||
| 6 | 7 | 8 | 9 | 10 | 11 | 12 |
| 13 | 14 | 15 | 16 | 17 | 18 | 19 |
| 20 | 21 | 22 | 23 | 24 | 25 | 26 |
| 27 | 28 | 29 | 30 |
- BFS
- programmers
- JavaScript
- 다이나믹 프로그래밍
- 그래프 이론
- 그래프이론
- level2
- 문자열
- 그래프 탐색
- 너비 우선 탐색
- 스택
- 딕셔너리
- 웹 프론트엔드
- 프로그래머스스쿨
- 정렬
- 백준
- DP
- 알고리즘
- DFS
- 파이썬
- 자료구조
- 구현
- lv2
- web
- 프로그래머스
- BASIC
- 브루트포스 알고리즘
- 자바스크립트
- CSS
- 그리디 알고리즘
- Today
- Total
DevLog:-)
Vue 2 + Element UI 자동완성 검색창에서 한글 입력 지연 해결하기 본문
1. 목표
업체명 자동완성 검색창을 구글 검색처럼 자연스럽게 만드는 것이 목표였습니다.
사용자가 대한을 입력한다면 다음과 같이 동작해야 합니다.
- 입력 중인 최신 검색어를 즉시 인식한다.
- 이전 검색어인 대의 추천 결과가 계속 남아 있지 않는다.
- 빠르게 입력해도 마지막 검색어를 기준으로 결과를 표시한다.
- 검색 과정에서 화면이 버벅이지 않는다.
- 빈 검색창에서는 불필요한 업체 목록을 표시하지 않는다.
2. 기존 문제
화면에서는 Element UI의 el-autocomplete를 사용하고 있었습니다.
<el-autocomplete
v-model="form.companyNm"
:fetch-suggestions="querySearchVendor"
:trigger-on-focus="true"
:debounce="0"
/>
검색 함수는 사용자가 글자를 입력할 때마다 전체 업체 목록을 순회했습니다.
querySearchVendor(queryString, cb) {
const query = (queryString || '').trim().toLowerCase();
const vendors = this.allVendors || [];
const results = [];
for (let i = 0; i < vendors.length; i++) {
const name = vendors[i];
if (!query || name.toLowerCase().includes(query)) {
results.push({ value: name });
if (results.length >= 20) {
break;
}
}
}
cb(results);
}
이 구조에는 크게 세 가지 문제가 있었습니다.
문제 1. 한글 조합 중 최신 검색어가 전달되지 않음
한글은 영문과 입력 방식이 다릅니다.
대한을 입력할 때 브라우저 내부에서는 자음과 모음을 조합하는 과정이 발생합니다. 이를 IME 조합 입력이라고 합니다.
그런데 현재 프로젝트가 사용하는 Element UI 2.x의 입력 컴포넌트는 한글 조합 중이면 입력 이벤트를 외부로 전달하지 않습니다.
실제 Element UI 내부 코드는 다음과 같은 구조입니다.
handleInput(event) {
if (this.isComposing) return;
this.$emit('input', event.target.value);
}
isComposing이 true인 동안에는 함수가 바로 종료됩니다.
따라서 사용자가 대한을 빠르게 입력해도 자동완성 컴포넌트에는 한동안 이전 값인 대만 전달됩니다. 화면에는 대의 추천 결과가 계속 남고, 한글 조합이 완전히 끝난 후에야 대한 검색이 시작됩니다.
이것이 검색이 한 박자 이상 늦게 반응한 핵심 원인이었습니다.
문제 2. 디바운스가 0ms로 설정됨
:debounce="0"
디바운스가 없어서 전달되는 입력 이벤트마다 검색을 실행하고 있었습니다.
빠르게 여러 글자를 입력하면 짧은 시간에 검색이 반복되기 때문에 불필요한 연산과 화면 갱신이 발생할 수 있습니다.
문제 3. 매 검색마다 업체명을 다시 가공함
기존 코드는 검색할 때마다 모든 업체명에 toLowerCase()를 실행했습니다.
name.toLowerCase().includes(query)
업체 목록이 많아질수록 같은 문자열 변환 작업을 반복하게 됩니다. 검색 결과가 뒤쪽에 있거나 검색 결과가 적으면 전체 목록에 가까운 범위를 순회해야 하므로 지연이 더 크게 느껴질 수 있습니다.
3. 해결 방법
해결은 다음 세 단계로 진행했습니다.
- 한글 조합 중에도 실제 입력값을 직접 받는다.
- 검색 요청에 짧은 디바운스를 적용한다.
- 업체 목록을 처음 받을 때 검색용 문자열을 미리 만든다.
4. 한글 조합 중 실제 입력값 받기
el-autocomplete의 컴포넌트 이벤트만 사용하지 않고, 브라우저의 네이티브 input 이벤트도 함께 받도록 변경했습니다.
<el-autocomplete
ref="vendorAutocomplete"
v-model="form.companyNm"
:fetch-suggestions="querySearchVendor"
placeholder="업체명 (검색어 입력)"
@select="handleVendorSelect"
@input.native="onVendorNativeInput"
:trigger-on-focus="false"
:debounce="80"
class="form"
/>
핵심은 다음 부분입니다.
@input.native="onVendorNativeInput"
Element UI가 한글 조합 중 컴포넌트 input 이벤트를 차단하더라도 브라우저의 실제 input 이벤트를 직접 받을 수 있습니다.
네이티브 입력 이벤트에서는 현재 input의 값을 읽어 자동완성 검색을 실행합니다.
onVendorNativeInput(event) {
const value = event && event.target ? event.target.value : '';
if (this.form.companyNm !== value) {
this.form.companyNm = value;
}
const autocomplete = this.$refs.vendorAutocomplete;
if (autocomplete && autocomplete.debouncedGetData) {
autocomplete.suggestionDisabled = !value;
autocomplete.debouncedGetData(value);
}
}
이제 사용자가 대한을 입력하는 도중에도 실제 입력창의 최신 값을 읽을 수 있습니다.
기존 동작은 다음과 같았습니다.
사용자 입력: 대 → 대한
검색 컴포넌트가 인식한 값: 대 → 한글 조합 완료 → 대한
수정 후에는 다음과 같이 동작합니다.
사용자 입력: 대 → 대한
네이티브 이벤트가 읽은 값: 대 → 대한
추천 결과: 대 결과 → 대한 결과
5. 디바운스 적용하기
디바운스를 0ms에서 80ms로 변경했습니다.
:debounce="80"
디바운스는 사용자가 입력할 때마다 검색하는 대신, 아주 짧은 시간 동안 입력이 더 들어오는지 기다렸다가 마지막 검색어만 처리하는 방식입니다.
예를 들어 사용자가 매우 빠르게 다음과 같이 입력했다고 가정해 보겠습니다.
대 → 대한 → 대한제
각 입력을 모두 검색하는 대신 이전 예약을 취소하고 마지막 입력을 중심으로 검색합니다.
대 검색 예약
→ 대한 입력으로 기존 예약 취소
→ 대한 검색 예약
→ 대한제 입력으로 기존 예약 취소
→ 대한제 검색 실행
80ms는 사용자가 지연을 거의 느끼지 않으면서 불필요한 중간 검색을 줄일 수 있는 값입니다.
일반적인 서버 검색은 150~300ms 정도를 사용하기도 하지만, 이번 화면은 이미 내려받은 업체 목록을 브라우저에서 검색하므로 더 짧은 값을 사용했습니다.
6. 검색용 업체명 미리 가공하기
기존에는 검색할 때마다 모든 업체명을 소문자로 변환했습니다.
이를 업체 목록을 처음 가져오는 시점에 한 번만 처리하도록 변경했습니다.
async fetchAllVendors() {
try {
const path = selectConfig.enterCheck.vendorNames.url;
const url = `${process.env.BASE_API}${path}`;
const res = await axios.get(url, {
headers: { 'Cache-Control': 'no-cache' },
});
if (res.data && Array.isArray(res.data)) {
this.vendorSearchItems = Object.freeze(
res.data.map(name => ({
value: name,
normalized: String(name)
.trim()
.toLocaleLowerCase(),
}))
);
}
} catch (error) {
console.error('Failed to fetch vendors', error);
}
}
각 업체 데이터는 다음과 같은 형태로 저장됩니다.
{
value: '대한산업',
normalized: '대한산업'
}
영문 업체명이 포함된 경우에도 검색용 값이 미리 정규화됩니다.
{
value: 'KG Steel',
normalized: 'kg steel'
}
검색할 때는 이미 만들어진 normalized 값만 비교합니다.
querySearchVendor(queryString, cb) {
const query = String(queryString || '')
.trim()
.toLocaleLowerCase();
const vendors = this.vendorSearchItems || [];
const results = [];
if (!query) {
cb(results);
return;
}
for (let i = 0; i < vendors.length; i++) {
const vendor = vendors[i];
if (vendor.normalized.includes(query)) {
results.push({
value: vendor.value,
});
if (results.length >= 20) {
break;
}
}
}
cb(results);
}
이를 통해 입력할 때마다 반복하던 문자열 가공 비용을 줄였습니다.
7. 빈 검색어 처리
기존에는 다음 설정 때문에 검색창에 포커스만 생겨도 추천 목록을 검색했습니다.
:trigger-on-focus="true"
이를 false로 변경했습니다.
:trigger-on-focus="false"
검색 함수에서도 빈 검색어는 바로 종료하도록 처리했습니다.
if (!query) {
cb([]);
return;
}
이제 검색창을 누르기만 했을 때 전체 업체 목록이 열리거나 불필요한 검색이 실행되지 않습니다.
8. 최종 코드
최종 자동완성 컴포넌트는 다음과 같습니다.
<el-autocomplete
ref="vendorAutocomplete"
v-model="form.companyNm"
:fetch-suggestions="querySearchVendor"
placeholder="업체명 (검색어 입력)"
@select="handleVendorSelect"
@input.native="onVendorNativeInput"
class="form"
:trigger-on-focus="false"
:debounce="80"
popper-class="clean-autocomplete-dropdown"
/>
검색 관련 메서드는 다음과 같습니다.
querySearchVendor(queryString, cb) {
const query = String(queryString || '')
.trim()
.toLocaleLowerCase();
const vendors = this.vendorSearchItems || [];
const results = [];
if (!query) {
cb(results);
return;
}
for (let i = 0; i < vendors.length; i++) {
const vendor = vendors[i];
if (vendor.normalized.includes(query)) {
results.push({
value: vendor.value,
});
if (results.length >= 20) {
break;
}
}
}
cb(results);
},
onVendorNativeInput(event) {
const value = event && event.target
? event.target.value
: '';
if (this.form.companyNm !== value) {
this.form.companyNm = value;
}
const autocomplete = this.$refs.vendorAutocomplete;
if (autocomplete && autocomplete.debouncedGetData) {
autocomplete.suggestionDisabled = !value;
autocomplete.debouncedGetData(value);
}
}
9. 결과
수정 후 검색창은 다음과 같이 개선되었습니다.
- 한글 조합 중에도 최신 입력값으로 검색
- 대 결과가 남아 있는 현상 해소
- 빠르게 대한을 입력해도 대한 결과를 즉시 표시
- 중간 검색을 줄여 화면 버벅임 완화
- 업체명 문자열 변환을 반복하지 않아 검색 속도 개선
- 빈 검색창에서 전체 목록을 표시하지 않음
이번 문제의 핵심은 검색 알고리즘 자체보다 오래된 Element UI가 한글 IME 입력을 처리하는 방식이었습니다. 자동완성 검색창을 구현할 때는 디바운스뿐 아니라 한글 조합 이벤트가 최신 검색어를 제대로 전달하는지도 함께 확인해야 합니다.
'프론트엔드' 카테고리의 다른 글
| SEO와 Next.js: 검색엔진 최적화와 효율적인 웹 렌더링 방법 (0) | 2024.08.07 |
|---|---|
| 프로젝트 캠프 : Next.js 2기 - 2주 회고 (0) | 2024.07.29 |
| 자료구조 & 알고리즘(1) (시간 복잡도, 배열, 연결 리스트, 스택) (0) | 2023.09.22 |
| Networking-[프로그래머스 스쿨]자바스크립트와 웹 프론트엔드(4) (0) | 2023.07.17 |
| Event + JS - [프로그래머스 스쿨]자바스크립트와 웹 프론트엔드(3) (0) | 2023.07.14 |