티스토리 뷰

때는 2016년, 고등학교 3학년 시절.


노래방에 놀러갔던 후배가 노래를 검색하다가 초성으로 검색이 되는 것을 확인하고 했던 말이 있다.
"이거 기능대회 문제에 출제하죠?"
이 때부터 이 망할 초성 검색의 열풍이 불기 시작했다.

 

단순한 초성검색이라면 어렵지도 않은 일이었다.

하지만 이 곳은 어디인가, 전국대회 아니겠는가.


그 평범하지 않은 기능인들은 검색 기능도 평범하게 구현할 리가 없겠지.
초성검색은 기본이요, AND OR 검색, 하이라이트 추가, 초성과 완전글자의 합성 검색 등
절대 3시간 안에는 나오지 않을 법한 검색 엔진으로 무장한 문제를 출제하는데 혈안이 되어있었고, 나는 그런 문제를 해결하려고 노력했던 한명의 학생이었다.

 

단순한 초성검색 기능은 구현한지 옛날이요. 그러나, and or 기능을 도입하면 하이라이트가 안되고, 하이라이트를 도입하면 합성 검색이 이루어지질 않아 고생하면서 결국 알고리즘을 제작하지 못한채 전국대회를 맞이해야 했다.
하지만 전국대회에서 정작 그 문제는 뽑히지 않았다는게 함정.

그렇게 기능대회를 마무리하고, 고등학교를 졸업하게 되었다.

 

그렇게 3년 뒤인 2019년.
대학교 2학년에 재학중인 나는 과거 고등학교 때 배웠던 기술과 대학교에서 배운 이론을 토대로 나름 잔뼈가 굵어져 있었고, 과거에 해결하지 못했던 금단의 문제를 다시금 꺼내들었다.

 

그리고 그 금단의 문제는 생각보다 쉽게 깨어지게 되었다.

<script>

    /* 초성 변환 함수 */

    function cho(char) {
        var cho = ['ㄱ', 'ㄲ', 'ㄴ', 'ㄷ', 'ㄸ', 'ㄹ', 'ㅁ', 'ㅂ', 'ㅃ', 'ㅅ', 'ㅆ', 'ㅇ', 'ㅈ', 'ㅉ', 'ㅊ', 'ㅋ', 'ㅌ', 'ㅍ', 'ㅎ'];
        var index = Math.floor((char.charCodeAt() - 44032) / 588);

        return cho[index] || char;
    }

    /* 검색 함수 */

    function search(keyword, str, startIndex) {

        var data = str.substr(startIndex); // 검색 기준 문자를 startIndex를 기준으로 자름
        var keywordLength = keyword.length; // 검색어 글자 수
        var dataLength = data.length; // 자른 문자의 글자 수
        var dataCho = ''; // 검색 키워드의 초성
        var count = 0; // 검색어의 첫글자가 검색 기준 문자 안에 몇 개 있는지 저장하는 함수
        var start = -1; // 최초로 탐색된 인덱스
        var index = -1; // 탐색 정보를 저장할 인덱스

        // 초성 데이터 불러오기
        for (var i = 0; i < dataLength; i += 1) {
            dataCho += cho(data.substr(i, 1));
        }

        // 글자 검색 알고리즘 시작
        for (var i = 0; i < keywordLength; i += 1) {
            var char = keyword.substr(i, 1); // 한글자씩 떼서 검색
            var regex = new RegExp(char, 'g'); // 검색 기준 문자 안에 현재 탐색중인 키워드가 몇개 있는지 알아내는 정규식
            var compare = data; // 비교 문자

            if (char.match(/[ㄱ-ㅎ]/)) compare = dataCho; // 검색어가 초성이라면 비교 문자도 초성으로 변환

            if (index === -1) { // 최초 탐색 시
                if (compare.indexOf(char) > -1) { // 현재 찾는 문자가 존재한다면
                    count = (compare.match(char) || []).length; // 갯수 반영
                    start = index = compare.indexOf(char); // 인덱스와 시작 인덱스 수정
                } else {
                    break; // 없으면 반복문 종료
                }
            } else {
                var compareChar = compare.substr(index + 1, 1); // 최초로 찾은 인덱스의 다음 글자를 추출
                if (char === compareChar) { // 현재 찾는 글자와 다음 글자가 일치한다면
                    index += 1; // 인덱스 증가
                } else {
                    start = count > 0 ? search(keyword, data, index + 1) : -1; // 글자가 일치하지 않는 상태인데 이후에 탐색할 글자가 여러개 더 있는 상태라면 재귀함수 실행
                    break;
                }
            }
        }

        return start + (startIndex || 0);
    }

    // 검색어 하이라이트 구현을 위한 함수
    function replaceAt(data, start, length, replacement) {
        var startStr = data.substr(0, start);
        var keyword = data.substr(start, length);
        var endStr = data.substr(start + length);

        return `${startStr}<span>${keyword}</span>${endStr}`;
    }

    // 검색 데이터
    var json = [
        { name: '새우' },
        { name: '깐쇼새우' },
        { name: '짜장면' },
        { name: '새우볶음밥' },
        { name: '시원새우탕면' },
    ];

    // 검색 키워드 (or 검색 예제)
    var keyword = 'ㅅ우|시원';
    keyword = keyword.split('|');

    // 검색 하이라이트 반영 알고리즘
    json.map(obj => {

        var name = obj.name;
        var indexes = [];
        var count = [];
        var match = [];

        // 검색 키워드를 파이프문자로 구분하여 나눈 배열을 반복함
        for (var i = 0; i < keyword.length; i += 1) {
            var index = search(keyword[i], name); // 현재 키워드에서 해당 검색어가 존재하는 인덱스를 찾는다.
            var count = keyword[i].length; // 검색어가 몇글자인지 구한다.
            if (index > - 1) { // 찾는 글자가 있다면
                var data = name.substr(index, count); // 일치된 키워드를 인덱스로 추출한다.
                match.push(data); // 추출된 키워드를 match 배열에 푸쉬한다.
            }
        }

        var regex = new RegExp(`(${match.join('|')})`, 'g'); // match에 푸쉬된 원소들을 파이프문자로 구분한 정규식을 구성한다.
        if (match.length > 0) name = name.replace(regex, "<span>$1</span>"); // match 안에 원소가 있다면 해당 원소와 일치되는 모든 키워드를 하이라이트 처리한다.

        obj.name = name; // name에 반영한다.

        return obj; // obj의 변경 사항을 반영한다.
    });

</script>

이게 Javascript로 구현한 초성검색 알고리즘이다.

 

비록 코드가 100줄을 조금 넘기긴 하지만, 기능대회 때 작성했던 영문 모를 코드에 비하면 터무니 없이 간결하고 군더더기 없이 작성 된 것 같다. 심지어 그 106줄마저도 하이라이트, or 검색 기능 및 소스코드 여백이 추가되어서 106줄이 나온 것이니, 실질적인 기능 구현은 더욱 간단하다고 할 수 있겠다.

 

나름 고등학교 3학년 때는 기능경기대회 참가 학교 사이에서 유명했고, 잘한다는 소리를 많이 들었던 터라 자만에 빠져 살았었는데, 시간이 지나고 되돌아보니 이런 간단한 알고리즘조차 구현하지 못했던 수준이었다니..

 

이래서 사람은 배우는걸 멈추지 말아야 하나보다.

LIST