INDEX
Stack
#VanillaJS
, #Jquery
Preview
이번 포스팅은 초성 검색 알고리즘
을 참고해 실무에 적용해 본 기능에 대한 내용입니다.
검색 기능 중 마치 노래방 검색기능
초성 검색 기능이 있으면 매우 편리해집니다.
대부분의 금융앱
에서 이체 내역 화면을 보면 계좌번호 혹은 상대방의 이름을 통해서 검색해 원하는 계좌만 볼 수 있도록 하는 기능들 많이 보셨을 겁니다.
![](https://blog.kakaocdn.net/dn/czsDrP/btstrQCFuJn/PUDCs5w9D8UbC1Hl3pLMJK/img.png)
이 알고리즘을 활용된 부분입니다. Array.prototype.includes()
, String.prototype.includes()
를 사용해 문자열에서 원하는 원소를 찾는 방법을 사용할 수도 있지만 초성 검색 기능 혹은 한 문자열 안에서 이어져있는 문자열만 찾는 것이 아니라 좀 떨어져 있는 문자열도 찾는 기능을 추가하면 훨씬 더 나은 사용자 경험을 제공할 수 있을 겁니다.
Methodology
const testArr = [
{
'OPNG_BANK_ACNM' : "김철수",
'OPNG_BANK_ACNO' : '100-203230-001'
},
{
'OPNG_BANK_ACNM' : "이영희",
'OPNG_BANK_ACNO' : '100-123423-001'
},
{
'OPNG_BANK_ACNM' : "이홍길",
'OPNG_BANK_ACNO' : '100-129990-002'
},
{
'OPNG_BANK_ACNM' : "김인수",
'OPNG_BANK_ACNO' : '122-235830-001'
}
]
위와 같은 계좌정보가 담긴 객체 배열
이 있다고 가정합니다. 이체 내역 캡처 사진을 보면 검색어를 포함한 객체만 보여야 하고 또한 검색어에 highlight
가 들어가야 합니다. 가령 김
혹은 ㄱ
를 입력했을 때 이 초성을 포함한 객체만 필터링되고 김 철수
의 형태로 변환이 되어 이를 화면에 보여줘야지만 "김" 자에 highlight 가 들어가게 되겠죠.
1. 문자열이 초성 및 음절을 포함하는지 체크하는 정규식을 생성합니다.
2. 객체의 새로운 필드값으로 특정글자에 Highlight를 주는 script 코드를 저장합니다.
ex) {
OPNG_BANK_ACNM : "김인수",
OPNG_BANK_ACNO : "122-235830-001",
OPNG\_BANK_ACNM_2 = "김 철수"
}
3. 위에서 새로 만든 필드값을 보유하고 있는 객체만 배열에서 걸러냅니다.
Code
function matchList(search, arr, sTag = "", eTag = ""){
const reESC = /[\\^$.*+?()[\]{}|]/g, reChar = /[가-힣]/, reJa = /[ㄱ-ㅎ]/, offset = 44032;
const con2syl = Object.fromEntries('ㄱ:가,ㄲ:까,ㄴ:나,ㄷ:다,ㄸ:따,ㄹ:라,ㅁ:마,ㅂ:바,ㅃ:빠,ㅅ:사'.split(",").map((v)=>{
const entry = v.split(":");
entry[1] = entry[1].charCodeAt(0);
return entry;
}));
//입력된 문자열로 정규식 패턴을 만들어냅니다.
function pattern(ch){
let r;
if(reJa.test(ch)){
const begin = con2syl[ch] || ((ch.charCodeAt(0) - 12613) * 588 + con2syl['ㅅ']);
const end = begin + 587;
r = `[${ch}\\u${begin.toString(16)}-\\u${end.toString(16)}]`;
}else if(reChar.test(ch)){
const chCode = ch.charCodeAt(0) - offset;
if(chCode % 28 > 0) return ch;
const begin = Math.floor(chCode / 28) * 28 + offset;
const end = begin + 27;
r = `[\\u${begin.toString(16)}-\\u${end.toString(16)}]`;
}else r= ch.replace(reESC, '\\$&');
return `(${r})`;
};
//정규식 패턴으로 조건에 맞는 문자열을 Script Tag 로 감쌉니다.
function matcher (v, matches, sTag, eTag, tagLen){
let distance = Number.MAX_VALUE, first = -1, last = 0, vLast = 0, vPrev = 0, acc = v;
for(let i = 1, j = matches.length; i < j; i++){
const curr = matches[i];
vLast = v.indexOf(curr, vLast);
if(first == -1) first = vLast;
if(vLast && distance > vLast - vPrev) distance = vLast - vPrev;
vPrev = vLast;
last = acc.indexOf(curr, last);
acc = `${acc.substring(0, last)}${sTag}${curr}${eTag}${acc.substr(last + 1)}`;
last += tagLen;
}
return [acc, distance, v.length, first];
};
const reg = new RegExp(search.split('').map(pattern).join('.*?'), "i");
const tagLen = sTag.length + eTag.length;
//Tag로 감싸진 문자열을 포함한 내용을 새로운 필드 _2 에 삽입합니다.
const replaceArr = arr.map((item)=>{
const nItem = {...item};
const {OPNG_BANK_ACNM, OPNG_BANK_ACNO} = nItem;
const matches_name = reg.exec(OPNG_BANK_ACNM);
if(matches_name) nItem['OPNG_BANK_ACNM_2'] = matcher(OPNG_BANK_ACNM, matches_name, sTag, eTag, tagLen)[0];
const matches_acc = reg.exec(OPNG_BANK_ACNO);
if(matches_acc) nItem['OPNG_BANK_ACNO_2'] = matcher(OPNG_BANK_ACNO, matches_acc, sTag, eTag, tagLen)[0];
return nItem;
});
//조건에 맞는 아이템만 거릅니다.
return replaceArr.filter((item)=> item['OPNG_BANK_ACNM_2'] || item['OPNG_BANK_ACNO_2']);
};
console.log(matchList('ㅇㅎㄱ', testArr, '<span style={"color" : "red"}>', '</span>'));
console
에 찍힌 내용을 보면 다음과 같습니다.
![](https://blog.kakaocdn.net/dn/LFljD/btsrZwZ75Lv/ppXcDMbKinOWKafkRxkWr1/img.png)
초성에 대한 검색은 잘 됩니다. 하지만 특이한 점이 있습니다.
matchList("기", testArr, '<span style={"color" : "red"}>', "</span>")
![](https://blog.kakaocdn.net/dn/cGJY3i/btssaCc9HMc/UpqTkMdecnGQDet8GpRNC1/img.png)
이처럼 받침이 없는 글자를 입력한 경우 조건에 맞게 잘 처리하나 받침이 있는 글자를 입력하는 경우 필터링은 잘 되지만 script tag가 잘 감싸지지 않은걸 볼 수 있습니다.
matchList("김", testArr, '<span style={"color" : "red"}>', "</span>")
![](https://blog.kakaocdn.net/dn/bP4qwf/btsr61kdbR1/qpnPhNL1eQsS7TX46YxXcK/img.png)
초성 검색에 한해서 구현해 놓은 알고리즘 및 정규식이어서 그렇습니다.
이 부분에 대해 수정하려면
pattern 함수
에서 else if
부분을 자세히 봐야 합니다.
else if(reChar.test(ch)){
const chCode = ch.charCodeAt(0) - offset;
if(chCode % 28 > 0) return ch;
const begin = Math.floor(chCode / 28) * 28 + offset;
const end = begin + 27;
r = `[\\u${begin.toString(16)}-\\u${end.toString(16)}]`;
}
가-힣
까지의 글자는 이 분기문을 탈 텐데 받침이 들어간 글자의 경우 chCode % 28 > 0
의 조건을 만족시켜 글자 그대로 return
이 됩니다.
string.charCodeAt(index) 은 UTF-16 글자표에서 string의 index 번째에 있는 글자의 10진수 값을 반환합니다.
("".charCodeAt(0)-offset)%28;
"" 안에 문자열을 대입해 테스트를 해보니 초성의 경우 음수, 모음과 자음이 들어간 음절의 경우 0, 받침이 들어가면 양수가 나옵니다.
만약 주석으로 처리해 아래 코드까지 진행되게 한다면 다음과 같은 현상이 발생합니다.
![](https://blog.kakaocdn.net/dn/eEUIvZ/btsr66TzduN/TgFGsSrq2XZWuqkouR9f8K/img.png)
이홍길 씨의 "길" 글자도 변환이 되었네요. 우리가 원하는 건 이게 아닙니다.
우리는 이 받침이 들어간 글자까지 검색해 Highlight
효과를 주고 싶기 때문에 그대로 return
하지 않고 초성 검색과 같은 경우처럼 return
해주도록 합니다.
if (chCode % 28 > 0) return `(${ch})`;
이제 이 OPNG_BANK_ACNM_2 혹은 OPNG_BANK_ACNO_2를 사용해 스크립트를 표현하면 됩니다.
물론 직접 짜면 좋지만.. 저걸 다 짤 생각하니 머리가 지끈지끈해서 검색해 보니 좋은 자료가 있더라구요ㅎㅎ
이 글도 누군가에겐 도움이 되길 바라며 작성을 마칩니다.
Reference
https://www.bsidesoft.com/8517
[[js] 초성 자동완성 검색
초성검색을 통해 자동완성용 리스트를 만들어봅니다.
www.bsidesoft.com](https://www.bsidesoft.com/8517)
다른 몇몇 블로그의 알고리즘도 사용해 보았지만 몇 가지 에러('(', '', '?')와 같은 특수문자를 입력 시 invalide regular expression 정규식 에러가 발생해 사용할 수 없었습니다.*
'Front-End' 카테고리의 다른 글
ReactJs에서 addEventListener 사용하기 (1) | 2023.05.30 |
---|---|
프로그래머스 과제 테스트-좋아하는 언어 검색기 (0) | 2023.05.11 |
OpenWeatherMap api by fetch and axios (0) | 2023.05.10 |
Delaying of Function (0) | 2023.04.11 |
[JS] Object 는 call by reference (0) | 2023.01.16 |