검색어와 태그를 이용한 검색 기능 구현

Project Diary/Vue (Roblox WebSite)

Vue.js를 이용해 검색어와 태그를 이용한 검색 기능을 구현하는 방법 기록


프로젝트 구조

  • TechTalks.vue: 메인 컴포넌트
  • EpisodeSearch.vue: 검색어 입력 컴포넌트
  • EpisodeTag.vue: 태그 선택 컴포넌트
  • EpisodeSection.vue: 에피소드 목록 컴포넌트

 

1. TechTalks.vue: 메인 컴포넌트

TechTalks.vue는 전체 컴포넌트를 관리하고, 검색어와 태그를 기반으로 데이터를 필터링합니다.

 

1-1 템플릿 구조

<template>
  <div>
    <host />
    <episode-search @onSearch="onSearch" />
    <episode-tag @onSearch="onSearch" />
    <episode-section :keyword="epKeyword" />
    <blog />
  </div>
</template>

여기서 EpisodeSearch와 EpisodeTag 컴포넌트는 검색어와 태그를 통해 검색 요청을 TechTalks 컴포넌트로 전달합니다.

 

1-2 스크립트: 데이터 정의 및 메서드

<script>
import Host from "@/components/techtalks/Host.vue";
import Blog from "@/components/techtalks/blog.vue";
import EpisodeSearch from "@/components/techtalks/EpisodeSearch.vue";
import EpisodeTag from "@/components/techtalks/EpisodeTag.vue";
import EpisodeSection from "@/components/techtalks/EpisodeSection.vue";

export default {
  components: {
    Host,
    EpisodeSearch,
    EpisodeTag,
    EpisodeSection,
    Blog,
  },
  name: "TechTalks",
  data() {
    return {
      epKeyword: "", // 검색어를 저장하는 변수
    };
  },
  methods: {
    onSearch(value) {
      this.epKeyword = value; // 검색어 업데이트
    },
  },
};
</script>

 

여기서 onSearch 메서드는 자식 컴포넌트로부터 검색어를 받아 epKeyword를 업데이트합니다.

 

2. EpisodeSearch.vue: 검색어 입력 컴포넌트

EpisodeSearch.vue는 사용자가 검색어를 입력할 수 있는 컴포넌트입니다.

 

2-1 템플릿 구조

<template>
  <section class="episode__search">
    <h2 :class="{ dark: changeDarkMode }">{{ $t("[7].search.h2") }}</h2>
    <div class="tech__search">
      <input
        ref="inputRef"
        type="search"
        :placeholder="$t('[7].search.placeholder')"
        @keypress="onKeyPress"
      />
      <button type="button" @click="onClick">
        {{ $t("[7].search.button") }}
      </button>
    </div>
  </section>
</template>

 

2-2 스크립트: 메서드 정의

<script>
export default {
  name: "EpisodeSearch",
  computed: {
    changeDarkMode() {
      return this.$store.getters.fnGetDark;
    },
  },
  methods: {
    onKeyPress(event) {
      if (event.key === "Enter") {
        this.onSearch();
      }
    },
    onClick() {
      this.onSearch();
    },
    onSearch() {
      let value = this.$refs.inputRef.value;
      if (value) {
        this.$emit("onSearch", value); // 검색어를 부모 컴포넌트로 전달
        this.$refs.inputRef.value = "";
      }
    },
  },
};
</script>

 

  • onKeyPress: Enter 키를 눌렀을 때 onSearch 메서드를 호출합니다.
  • onClick: 검색 버튼을 클릭했을 때 onSearch 메서드를 호출합니다.
  • onSearch: 검색어를 부모 컴포넌트로 전달하고 입력 필드를 초기화합니다.

 

검색어 검색기능

 

 

 

3. EpisodeTag.vue: 태그 선택 컴포넌트

EpisodeTag.vue는 사용자가 태그를 선택할 수 있는 컴포넌트입니다.

 

3-1 템플릿 구조

<template>
  <section id="ep__tag">
    <div class="tag__list" v-for="(item, index) in mediaTag" :key="index">
      <button
        :class="{ active: activeName == item.class, dark: changeDarkMode }"
        type="button"
        @click="onClick(item.class)"
      >
        <i :class="item.class"></i>
        <p>{{ $t(item.name) }}</p>
      </button>
    </div>
  </section>
</template>

 

3-2 스크립트: 데이터 및 메서드 정의

<script>
export default {
  name: "EpisodeTag",
  data() {
    return {
      activeName: "", // 선택된 태그를 저장하는 변수
      mediaTag: [
        { name: "[7].tag.podcast", class: "fa-solid fa-podcast" },
        { name: "[7].tag.spotify", class: "fa-brands fa-spotify" },
        { name: "[7].tag.youtube", class: "fa-brands fa-youtube" },
      ],
    };
  },
  computed: {
    changeDarkMode() {
      return this.$store.getters.fnGetDark;
    },
  },
  methods: {
    onClick(value) {
      this.activeName = value; // 선택된 태그 업데이트
      this.$emit("onSearch", value); // 선택된 태그를 부모 컴포넌트로 전달
      this.$store.commit("on__UpdateCurrent", 1);
    },
  },
};
</script>
  • onClick: 태그 버튼을 클릭했을 때 선택된 태그를 업데이트하고 부모 컴포넌트로 전달합니다.

 

4. EpisodeSection.vue: 에피소드 목록 컴포넌트

EpisodeSection.vue는 필터링된 에피소드 목록을 표시하는 컴포넌트입니다.

 

4-1 템플릿 구조

<template>
  <section class="row">
    <div class="card__wrapper">
      <div class="card" v-for="(item, index) in loadItem" :key="index">
        <div class="flexBox">
          <div class="span__wrap">
            <span v-if="item.num"
              >{{ $t("[7].section.span") }} {{ $t(item.num) }}</span
            >
          </div>
          <div class="image">
            <img :src="imgUrl" alt="techtalks image" />
          </div>
          <div class="text">
            <div class="topicRow">
              <h3>{{ $t(item.topic) }}</h3>
              <a :href="item.href" target="_blank"
                ><i :class="item.iconClass"></i
              ></a>
            </div>
            <h4>{{ $t("[7].section.h4") }} : {{ $t(item.guest) }}</h4>
            <p>{{ $t(item.content) }}</p>
          </div>
        </div>
      </div>
      <button class="loadMore" @click="loadMore">
        {{ $t("[7].section.leadmore") }}
      </button>
    </div>
  </section>
</template>

 

4-2 스크립트: 데이터 및 메서드 정의

<script>
export default {
  name: "EpisodeSection",
  data() {
    return {
      episodes: [
        // 에피소드 데이터
      ],
      itemsPerPage: 4,
      currentPage: 1,
      loadItemPage: [],
      imgUrl: "./assets/images/techtalks/ep1.jpg",
    };
  },
  props: ["keyword"], // 부모 컴포넌트로부터 전달받은 검색어
  computed: {
    loadItem() {
      this.currentPage = this.$store.getters.fnGetCurrent;
      if (!this.keyword) {
        this.loadItemPage = this.episodes.filter(
          (item, index) => index < this.itemsPerPage * this.currentPage
        );
      } else {
        let items = this.episodes.filter(
          (item) =>
            this.$t(item.topic).indexOf(this.keyword) > -1 ||
            this.$t(item.content).indexOf(this.keyword) > -1 ||
            this.$t(item.guest).indexOf(this.keyword) > -1 ||
            item.iconClass.indexOf(this.keyword) > -1
        );
        this.loadItemPage = items.filter(
          (item, index) => index < this.itemsPerPage * this.currentPage
        );
      }
      return this.loadItemPage;
    },
  },
  methods: {
    loadMore() {
      this.currentPage = this.$store.getters.fnGetCurrent;
      this.$store.commit("on__UpdateCurrent", ++this.currentPage);
    },
  },
};
</script>

 

  • props: 부모 컴포넌트로부터 keyword를 받아옵니다.
  • computed - loadItem: 검색어를 기반으로 에피소드 목록을 필터링하여 반환합니다.
  • methods - loadMore: 더보기 버튼을 클릭했을 때 페이지를 증가시킵니다.

 

필터링 조건 코드
let items = this.episodes.filter(
  (item) =>
    this.$t(item.topic).indexOf(this.keyword) > -1 || // 주제에 검색어가 포함되어 있는지 검사
    this.$t(item.content).indexOf(this.keyword) > -1 || // 내용에 검색어가 포함되어 있는지 검사
    this.$t(item.guest).indexOf(this.keyword) > -1 || // 게스트 이름에 검색어가 포함되어 있는지 검사
    item.iconClass.indexOf(this.keyword) > -1 // 아이콘 클래스에 검색어가 포함되어 있는지 검사
);
  • 주제 필터링
this.$t(item.topic).indexOf(this.keyword) > -1
  1. this.$t(item.topic)는 Vue I18n을 사용해 item.topic의 번역된 문자열을 가져옵니다.
  2. indexOf(this.keyword) > -1는 this.keyword가 해당 문자열에 포함되어 있는지를 검사합니다.
  3. indexOf 메서드는 문자열이 포함되어 있으면 해당 문자열의 시작 인덱스를, 포함되어 있지 않으면 -1을 반환합니다. 따라서, > -1 조건을 사용하여 문자열에 검색어가 포함되어 있는지를 확인합니다.
  • 내용 필터링
this.$t(item.content).indexOf(this.keyword) > -1

 

첫 번째 조건과 동일한 방식으로 item.content의 번역된 문자열에 검색어가 포함되어 있는지를 검사합니다

  • 게스트 이름 필터링
this.$t(item.guest).indexOf(this.keyword) > -1

첫 번째 조건과 동일한 방식으로 item.guest의 번역된 문자열에 검색어가 포함되어 있는지를 검사합니다.

  • 아이콘 클래스 필터링
item.iconClass.indexOf(this.keyword) > -1

이번에는 Vue I18n 번역을 사용하지 않고, item.iconClass 자체에 검색어가 포함되어 있는지를 검사합니다. 이는 iconClass가 번역이 필요 없는 CSS 클래스명이기 때문입니다

 

태그 검색 기능

 


전체 코드 흐름

  1. items 변수는 this.episodes 배열에서 조건에 맞는 항목들만 필터링하여 새로운 배열로 저장합니다.
  2. filter 메서드는 배열의 각 요소에 대해 주어진 함수(필터링 조건)를 호출하고, true를 반환하는 요소들만 모아 새로운 배열을 만듭니다.
  3. 여기서는 this.keyword가 item.topic, item.content, item.guest, 또는 item.iconClass에 포함되어 있는지를 검사합니다.
  4. indexOf 메서드를 사용하여 문자열 내에서 검색어를 찾고, > -1 조건으로 문자열에 검색어가 포함되어 있는지를 확인합니다.

  5. 이 코드를 통해 사용자는 입력한 검색어를 기반으로 에피소드 목록을 필터링할 수 있습니다.