본문 바로가기

개발 일기

검색 기능 간단한 개선/리팩토링 과정

안녕하세요👋🏻 프론트엔드 개발자 코디(Kody)입니다.

최근에는 서비스에서 검색 기능을 사용하다가 문제점을 발견해서 살짝 개선해보고자 리팩토링을 해보았는데요,

이번 글에서는 기존 검색 기능의 한계를 느낀 상황부터 개선을 통해 얻은 성과까지 공유해 보려고 합니다.

기존 검색 기능의 한계

1. 단일 필드 검색의 제약

기존 검색 로직은 데이터의 특정 컬럼(예: 브랜드명, 교육과정명 등)에서만 검색어를 찾는 방식이었습니다. 즉, 유저의 검색어가 각 캠프 컬럼 데이터 안에 포함되는지 여부를 통해 필터링 하여 검색되고 있던 것입니다.😓

function applySearchFilter(campList: CampListElement[], searchState: SearchItemType | null): CampListElement[] {
  const keyword = getNormalizedText(searchState.value)
  return campList.filter((camp) => {
    const campValues = getCampValue(camp, ["brandName", "title", ...등 검색할 컬럼 ]);
    return campValues.some((value) => value.includes(keyword));
  });
}

 

예를 들어, 다음과 같은 캠프 데이터가 있다고 가정해봅시다.

{
  브랜드명: "AA브랜드",
  교육과정명: "그래픽 디자인 부트캠프",
  기수: "1월",
  키워드: ["디자인", "취업", "주말"],
  지역: "서울",
  수강료: 0
}

사용자가 “AA브랜드 디자인”이라는 검색어를 입력하면, 다음과 같은 이유로 검색 결과가 표시되지 않았습니다.

  • 검색어 “AA브랜드”브랜드명에서 찾을 수 있지만, “디자인”교육과정명에만 포함되어 있었습니다.
  • 기존 검색 로직은 검색어 전체가 하나의 필드에 포함되어야 검색 결과로 반환했기 때문에, 이 캠프는 검색되지 않았습니다.

2. 날것의 데이터로 인한 검색 불편

검색 대상이 되는 데이터가 정제되지 않았다는 점도 문제였습니다. 예를 들어, 다음과 같은 상황을 상상해보세요. 사용자 A는 수강료가 무료인 부트캠프를 찾고싶어서 “무료 부트캠프”를 검색했는데, 데이터상에서 수강료는 “0”으로 저장되어 있어 검색 결과가 나오지 않는 것입니다.

  1. 검색어 분할: 사용자의 검색어를 띄어쓰기 단위로 분할하여 각 키워드가 데이터의 어느 필드에 포함되더라도 검색 결과로 반환되도록 했습니다.
  2. 데이터 정제: 캠프와 관련된 모든 데이터를 하나의 배열로 정리하여, 다양한 정보(브랜드명, 캠프명, 키워드, 날짜, 지역 등)를 검색 대상으로 포함시켰습니다. 이를 통해 단순히 텍스트 매칭을 넘어서, 더 유연하고 풍부한 검색이 가능하게 만들었습니다.

기존 코드의 복잡함과 비효율을 줄이기 위해 getRefinedValues라는 함수를 새로 만들었습니다. 이 함수는 캠프 데이터를 받아 정제된 배열로 반환합니다.

 

getRefinedValues 함수

function getRefinedValues(camp: CampListElement): string[] {
  const dateStrings = [camp.startDate, camp.endDate, camp.regDate, camp.regEndDate].map(getDateText);
  const cityLabels = getCityData(camp.city) || [];

  return [
    getRegStatus(camp),
    getGovtCostValue(camp),
    getBootOption(camp),
    dateStrings,
    cityLabels,
    ...등등
  ].flat().filter((val) => Boolean(val) && val.length > 0);
}

 

이제 applySearchFilter 함수는 훨씬 깔끔해졌습니다.

export function applySearchFilter(
  campList: CampListElement[],
  searchState: SearchItemType | null
): CampListElement[] {

  const keywords = searchState.value.toLowerCase().trim().split(/\\s+/);

  return campList.filter((camp) => {
    const refinedValues = getRefinedValues(camp);
    const normalizedText = getNormalizedText(refinedValues.join(""));
    const searchValues = [...refinedValues, normalizedText].map(String).map((v) => v.toLowerCase());

    const joinedKeywords = keywords.join("");
    const includesJoined = searchValues.some((value) => value.includes(joinedKeywords));
    const everyKeywordPresent = keywords.every((keyword) =>
      searchValues.some((value) => value.includes(keyword))
    );

    return includesJoined || everyKeywordPresent;
  });
}

 

개선 효과: 더 똑똑한 검색

1. 키워드 조합으로 원하는 결과 찾기

사용자가 “AA브랜드 디자인”을 검색하면, 각각의 키워드가 캠프 데이터의 다른 필드에 포함되더라도 결과로 표시됩니다. 이제 검색어 조합의 유연성이 한층 더 강화되었습니다.

2. 데이터 정제로 정확도 향상

  • 날짜, 지역 등 비정형 데이터를 통일된 포맷으로 처리하여 검색 정확도를 높였습니다.
  • 캠프 데이터의 여러 측면을 검색 대상에 포함시킴으로써 더 풍부한 결과를 제공합니다.

3. 코드 가독성 및 유지보수성 개선

getRefinedValues로 데이터 정제 로직을 분리하여, 코드가 훨씬 깔끔하고 유지보수하기 쉬워졌습니다.


마무리하며

물론, 여기서 멈출 수는 없겠죠. 앞으로는 다음과 같은 기능을 추가할 계획입니다.

  1. 자동 완성 및 추천 키워드: 검색창에 입력할 때 관련 키워드를 실시간으로 추천하는 기능을 추가해 검색 과정을 더 편리하게 만들 예정입니다.
  2. 사용자 맞춤형 검색: 검색 로그를 분석해 개인화된 추천 기능을 도입하려 합니다. 이를 통해 사용자가 관심을 가질 만한 캠프를 더 쉽게 찾을 수 있을 것입니다.

작은 검색 기능 하나를 개선하면서도 많은 것을 배울 수 있었던 시간이었습니다. 긴 글 읽어주셔서 정말 감사합니다!