ㆍ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
- this.$t(item.topic)는 Vue I18n을 사용해 item.topic의 번역된 문자열을 가져옵니다.
- indexOf(this.keyword) > -1는 this.keyword가 해당 문자열에 포함되어 있는지를 검사합니다.
- 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 클래스명이기 때문입니다
전체 코드 흐름
- items 변수는 this.episodes 배열에서 조건에 맞는 항목들만 필터링하여 새로운 배열로 저장합니다.
- filter 메서드는 배열의 각 요소에 대해 주어진 함수(필터링 조건)를 호출하고, true를 반환하는 요소들만 모아 새로운 배열을 만듭니다.
- 여기서는 this.keyword가 item.topic, item.content, item.guest, 또는 item.iconClass에 포함되어 있는지를 검사합니다.
- indexOf 메서드를 사용하여 문자열 내에서 검색어를 찾고, > -1 조건으로 문자열에 검색어가 포함되어 있는지를 확인합니다.
이 코드를 통해 사용자는 입력한 검색어를 기반으로 에피소드 목록을 필터링할 수 있습니다.