[자료 구조 활용] 최근 검색어 기능 구현 예제

 

* 해당 예제는 복기의 일부 입니다. 소개되는 소스를 그대로 적용하면 여러분의 프로젝트에 많은 허들을 만들 수 있습니다.

 

이전 포스팅에서 자료구조 중 스택에 대해 알아봤습니다. 하지만 스택이나 큐 외에도 실제 프로젝트에서는 해당 스펙(기획안)에 따라서 그에 맞는 자료구조와 기능 들이 필요합니다.

 

만약 최근 검색어를 담아두는 기능을 구현한다고 가정해 봅시다. 이의 구현을 위해서는 다음과 같은 기능이 필요할 것입니다.

 

1. 사용자가 어플리케이션을 처음 사용하지 않고 검색한 내용이 있다면 그 내용을 바탕으로 초기화가 이루어져야 한다.

2. 최근 검색어는 모든 내용을 저장하지 않고 반드시 주어진 길이에 맞는 갯수만큼의 데이터만을 가지고 있는다.

3. 이미 검색된 내용을 다시 한번 선택 / 저장한다면 그 내용이 가장 최신으로 이동되어야 한다.

4. 해당 데이터를 이용하기 위하여 배열 형태로 자료를 받아야 한다.

 

이 외에도 여러가지 기능들이 필요하겠지만(ex. 해당 검색어의 만료시점을 파악하여 삭제 등) 가장 기본적인 기능들을 열거하면 위의 네가지 정도의 핵심 기능이 필요할 것입니다.

 

위의 내용을 종합해보면 데이터는 집합이어야 하며 가장 최신의 데이터데이터의 가장 처음 인덱스에 위치해야만 하며 중복된 기존 검색어는 삭제되며 가장 최신의 데이터가 되어야 합니다. 더불어 데이터의 최대 크기를 초과하면 가장 마지막 데이터(가장 먼저 생성된 데이터)는 삭제되어야 합니다.

 

이를 구현하기 위하여 앞으 예제를 조금 다듬어 클래스를 제작하겠습니다.

const keywords = new WeakMap();
const max = new WeakMap();

class SearchedKeywords {
  constructor (cached, len) {
    if (len === undefined || len === 0) {
      throw new Error('최근 검색어의 최대값은 필수 매개변수 입니다. 검색어 저장 최대값을 설정해주세요.');
    }
    keywords.set(this, (arguments[0] === undefined || arguments[0] === null) ? [] : cached);
    max.set(this, len);
  }
}

 

오... 아름다운 템플릿... 새로운 기능을 영접합니다. 발행해 놓으니 스타일이 달라지네요 ㅡ_ㅡ 아시는 분은 댓글 좀...

 

먼저 WeakMap을 이용하여 해당 클래스의 프라이빗 멤버를 설정한 후, SearchedKeywords 클래스의 내부에서 멤버변수로 사용합니다. 이 멤버들은 private합니다.

 

그러면 이번 포스팅에서 제법 귀찮은 기능을 구현하겠습니다. 바로 unshift입니다. 자료를 새롭게 저장해야하는데 문제는, 중복 데이터가 허용이 안되는 '집합'이며 기존의 중복된 원소가 있다면 새롭게 저장되어야 한다는 것입니다.

 

unshift (element) {
  const arr = keywords.get(this);
  const num = max.get(this);
  const duplicatedIdx = arr.findIndex(el => {
    return el === element;
  })
  if (duplicatedIdx > -1) {
    const tmp = arr.slice();
    this.clear();
    tmp.forEach(el => {
      if (el !== element) {
        arr.push(el)
      }
    });
  }
  arr.unshift(element)
  if (arr.length > num) {
    arr.pop();
  }
}

 

이 메서드에서는 데이터의 최대값이 필요하며 중복된 값을 체크하여 해당 원소를 제거한 후 가장 처음에 삽입해야 합니다. ㅡㅡ;; 뭔가 말만으로는 복잡하지만 위의 구현부를 확인하시면 이해하시기에 무리는 없을겁니다.

 

해당 인스턴스의 배열과 최댓값을 변수에 담고 중복값이 있는지 체크합니다. 만약 중복값이 존재한다면 임시 저장소(=tmp)에 해당 데이터를 담고 중복값이 아닌 원소들만 원래의 배열(=arr)에 담습니다. 그렇지 않다면 (중복값이 없다면) 단순히 배열에 새로운 데이터를 담고 최대값을 체크하여 필요시 삭제해줍니다.

 

나머지는 이전 포스팅의 예제와 비슷합니다.

 

다음은 이번 예제의 스펙에 맞게 구현한 클래스의 전체 코드입니다.

 

const keywords = new WeakMap();
const max = new WeakMap();

class SearchedKeywords {
  constructor (cached, len) {
    if (len === undefined || len === 0) {
      throw new Error('최근 검색어의 최대값은 필수 매개변수 입니다. 검색어 저장 최대값을 설정해주세요.');
    }
    keywords.set(this, (arguments[0] === undefined || arguments[0] === null) ? [] : cached);
    max.set(this, len);
  }
  // toString, toArray
  unshift (element) {
    const arr = keywords.get(this);
    const num = max.get(this);
    const duplicatedIdx = arr.findIndex(el => {
      return el === element;
    })
    if (duplicatedIdx > -1) {
      const tmp = arr.slice();
      this.clear();
      tmp.forEach(el => {
        if (el !== element) {
          arr.push(el)
        }
      });
    }
    arr.unshift(element)
    if (arr.length > num) {
      arr.pop();
    }
  }

  pop () {
    const arr = keywords.get(this);
    const ret = arr.pop();
    return ret;
  }

  peek () {
    if (this.isEmpty()) return undefined;
    const arr = keywords.get(this);
    return arr[arr.length - 1];
  }

  size () {
    const arr = keywords.get(this);
    return arr.length;
  }

  isEmpty () {
    const arr = keywords.get(this);
    return arr.length === 0;
  }

  clear () {
    while (!(this.isEmpty())) {
      this.pop();
    }
  }

  toString () {
    const arr = keywords.get(this);
    return arr.toString();
  }

  toArray () {
    const arr = keywords.get(this);
    return arr;
  }
}

 

요즘은 프레임워크를 사용하여 해당 컴포넌트에서 모든것을 처리하는 일이 많습니다. 하지만 관심사가 동일한, 여러 부수적인 기능이 필요한 부분이 있다면 하나의 클래스로 관리하는 것이 더 나을 것입니다.

+ Recent posts