[FE 워크샵] 자동 완성 a.k.a 초성 검색 구현하기

3/31/2024
notion image

자동 완성?

  1. 사용자가 제출을 위한 submit 버튼을 클릭하지 않아도
    1. 서버에 각 입력 값마다 관련된 요청을 보내야 합니다
    2. 클라이언트에 저장해둔 리스트에서 계산을 통해 해당 값을 추출해야 합니다.
  1. 한글의 경우 두 가지를 고려해야 합니다.
    1. 자음만 입력해도 검색 데이터를 요청할지 = 초성 검색
    2. 최소 자음 + 모음이 합하여 음절이 되었을 때 검색 데이터를 요청할지
  1. 자신이 원하는 단어가 리스트에서 검색되거나 혹은 해당 단어를 본인이 전부 입력할 때까지 계속 호출해야 합니다.
  1. 오타 등의 이유로 글자 단위로 삭제할 수도 있습니다. 그때마다 다시 호출하기보다 캐싱된 데이터를 보여줍니다.
⇒ 정리하자면,
💡
1. click, submit 이벤트가 아닌 change 이벤트를 감지하여 구현합니다. 해당 이벤트가 감지될 때마다 서버에 요청을 보낼지, 클라이언트에서 계산을 할지 결정해야 합니다. 서비스의 특성상 100명, 1000명 내외의 간단한 Id, name, profileURL 정보만 가져오면 되므로, 클라이언트에 저장해두고 사용하겠습니다. (단순 GET이므로 서버가 알아야 할 필요도 없습니다)
💡
2. 서비스의 필요에 따라 어떤 조건이 만족되었을 때 (언제) 검색 데이터를 요청할지 결정합니다. 관심 주제를 검색하는 것이 아니라, 한정적인 멤버(수강생)의 이름을 검색하기 위하여 사용할 예정입니다. 이름의 경우 의미를 가지기보다 외워야 하는 데이터에 가깝다고 판단하여, 글자 단위로 검색할 수 있도록 구현하기로 결정했습니다.
💡
3. 해당 수강생의 이름이 목록에 나타날 때까지 반복됩니다.
💡
4. change 이벤트가 발생하는 특정 시간동안은 해당 api 요청의 결과를 클라이언트는 캐싱해두어야 중복 api 호출에 대한 비용을 줄일 수 있습니다.
 

✅ onChange 이벤트

서비스에서 초성 검색, 자음 검색은 필요없고 음절 단위의 검색만 필요했다면, debounce를 사용했을 것입니다. onChange는 유저가 어떤 값을 눌러도 발생되므로, 불필요하게 많은 이벤트를 발생시키기 때문입니다. 그러나 초성/자음 검색으로 구현할 예정이므로, debounce를 사용할 필요가 없습니다.
✅ 한글이 아닌 알파벳, 숫자, 특수문자를 입력할 경우 해당 이벤트를 무시해야 합니다.
✅ 무시할 뿐만 아니라 해당 값이 검색 창에 노출되지도 않아야 합니다.
 

✅ 초성 검색

음절 단위 검색이라면 입력값을 포함하고 있는 (include) 리스트를 계산하여 보여주기만 하면 됩니다. 하지만 초성 검색은 그렇지 않습니다. 사과에서 ‘ㅅㄱ’를 입력하였을 때 사가 ‘ㅅ’을 포함하고 있는지 그 여부는 정규식으로 test를 하여 알아낼 수 있습니다. 이제 초성을 찾아낼 수 있는 방법을 살펴봅시다! ( 초성 관련 식, 알고리즘에 대한 텍스트는 제가 참고한 블로그와 동일합니다. 하지만 과정에서 생략된 세세한 값을 추가적으로 덧대거나, 저의 언어로 설명을 덧대거나 다시금 풀이했습니다.)

한글에 대하여

  • 한글은 영문과 당연히 같지 않습니다. 자음 + 모음 + 종성을 조합하여 하나의 음절을 만들어 냅니다
  • 한글이 놓인 위치에 따라 같은 ㄱ이라도 초성이 될 수도, 종성이 될 수도 있습니다. 이때 필요한게 ‘가중치’입니다. 가중치라는 건 결국 ‘가’와 ‘까’사이에 존재하는 값들의 수 588, ‘가’와 ‘개’사이에 존재하는 값들의 수 28입니다. 다른 설명에서는 초성의 주기, 중성의 주기로도 표현하기도 합니다.
    • 여러 숫자들을 살펴봅시다
      • 44032: ‘가’라는 유니 코드 즉, 한글의 시작 코드 포인트입니다.
      • 588: 초성에 대한 가중치, 주기
      • 28: 중성에 대한 가중치,주기
      • 0~18 : 초성 번호
      • 0~20: 중성 번호
      • 0~27: 종성 번호 (0은 none)
notion image
notion image
notion image
[참고] 태곤님의 글과 같이 위키피디아에서 이미지를 인용하였습니다.

원하는 한글 음절을 출력하기

💡
(자음번호 * 588 + 모음번호 * 28 + 종성번호) + 44032
⇒ 해당 식으로 구한 값을 String.fromCharCode()에 넘겨주면 내가 찾는 글자를 반환합니다

정규식 생성에 필요한 한글 조합 함수 만들기

const INITIAL_CONSONANT_HANGUL = [ 'ㄱ', 'ㄲ', 'ㄴ', 'ㄷ', 'ㄸ', 'ㄹ', 'ㅁ', 'ㅂ', 'ㅃ', 'ㅅ', 'ㅆ', 'ㅇ', 'ㅈ', 'ㅉ', 'ㅊ', 'ㅋ', 'ㅌ', 'ㅍ', 'ㅎ', ]; const HANGUL_START_CHARCODE = '가'.charCodeAt(0); const CHO_PERIOD = Math.floor('까'.charCodeAt(0) - '가'.charCodeAt(0)); //588 const JUNG_PERIOD = Math.floor('개'.charCodeAt(0) - '가'.charCodeAt(0)); //28 function combine(cho: number, jung: number, jong: number): string { return String.fromCharCode( HANGUL_START_CHARCODE + cho * CHO_PERIOD + jung * JUNG_PERIOD + jong ); }

유저가 입력한 값으로 초성을 걸러내는 정규식 만들기

이제 사용자가 입력한 초성에 대한 정규식을 만듭니다.
  • 초성 19글자를 순회하면서
  • search 파라미터로 제공된 초기값을 활용합니다
    • search 파라미터로 ‘ㄷㄹ’가 제공된 경우
      • ㄷㄹ.replace(new RegExp(’ㄱ’, “g”), `[${(combine(0,0,0)}-${combine(1,0,-1)}]`) => ㄱ이 있다면 [가-깋]으로 대체하겠지만 => 해당되지 않으므로 그냥 ㄷㄹ을 그대로 return합니다
      • ㄷㄹ.replace(new RegExp(’ㄷ’, “g”), `[${(combine(3,0,0)}-${combine(4,0,-1)}]`) => ㄷ에 대한 정규식을 기반으로, ㄷ을 [다-딯]으로 대체합니다 => '[다-딯]ㄹ' 을 return 합니다
// 초성(ㄱ)으로 정규식 (가-깋) 만들기 function makeRegexByCho(search = "") { const regex = INITIAL_CONSONANT_HANGUL.reduce( (acc, cho, index) => acc.replace( new RegExp(cho, "g"), `[${combine(index, 0, 0)}-${combine(index + 1, 0, -1)}]` // 초성 배열 순회하면서 search에 해당하는 경우 ㄱ을 [가-깋] 방식으로 변환 ), search ); return new RegExp(`(${regex})`, "g"); } makeRegexByCho('ㄱ'); // /[가-깋]/
 

초성 정규식으로 테스트하기 (선택)

function includeByCho(search, targetWord) { return makeRegexByCho(search).test(targetWord); }
 

[이슈] 정규식의 생성 및 테스트

현상 : ㅊ으로 김철수, 최지연은 OK, 최지민은 못 찾습니다.

아래와 같은 코드로 정규식 검사를 진행하면, 최지연, 최지민, 김철수가 모두 나와야 하지만, 콘솔 출력에서는 최지민이 제외됩니다. 왜 그럴까요?
//검색할 수 있는 학생 목록 const students = ['김철수', '김민수', '최지연', '최지민'] //정규식 생성 및 테스트 코드 const handleChange = (e: ChangeEvent<HTMLInputElement>) => { setKeyword(e.target.value); const search = e.target.value.trim(); console.log(`${search} 입력`); const regex = makeRegexByCho(search); students.forEach((name) => { if (regex.test(name)) { console.log(`${search}로 ${name}을 찾았어요`); } }); };
 
검색 UI와 콘솔을 참고해주세요. 화면은 mock 데이터로 기능 구현과 상관이 없습니다.
검색 UI와 콘솔을 참고해주세요. 화면은 mock 데이터로 기능 구현과 상관이 없습니다.

원인: lastIndex 속성

💡
정규식 객체의 “lastIndex” 속성이 이전 검색에서 찾은 문자열의 끝을 가리키고 있기 때문에 같은 자리에 놓인 문자열은 찾아내지 못합니다.
notion image

해결: lastIndex의 초기화

  1. 결국 만들어진 정규식을 저장한 후 사용하지 않고,
  1. 루프 안에서 매번 새로운 정규식을 생성한 후에 테스트를 해야합니다.
    1. notion image
 

[완성] 초성으로 검색하기

notion image
notion image
 

reference

hangul-util
hyuksonUpdated Sep 14, 2024
 
©JIYOUNG CHOI, All rights reserved