토이 프로젝트 - Vue, Firebase로 서버리스 PWA 개발
25. Vue와 Firebase 서버리스 PWA myBlog 개발 - 블로그 검색
그랜파 개발자
2025. 3. 2. 03:53
블로그 검색
블로그 검색 기능을 구현하고자 합니다.
단순한 검색 기능으로 제목 또는 내용에 검색어를 포함하는 모든 글의 목록을 보여 주는 것입니다.

검색 화면에는 검색어를 입력하는 텍스트 입력창만 있습니다.
검색어를 입력하면 입력한 검색어가 제목 또는 내용에 포함된 글들을 찾아 보여줍니다.
전체 글들을 이미 상태의 posts에 저장하고 있으므로 posts에서 검색어를 포함하는 글들 필터링하여 보여 줍니다.
검색
// Firestore에서 게시물 검색
async searchPosts({ commit, state }, searchTerm ) {
// 검색어를 소문자로
const searchTermLower = searchTerm.toLowerCase();
let filteredPosts = [];
// 검색된 결과 필터링
filteredPosts = state.posts.filter((post) => {
// title 또는 content에 검색어가 포함된 게시물 검색
return (
post.title.toLowerCase().includes(searchTermLower) ||
post.content.toLowerCase().includes(searchTermLower)
);
});
commit('setFilteredPosts', filteredPosts); // 검색 결과를 상태에 저장
},
store의 post 모듈
// src/store/modules/post.js
import { v4 as uuidv4 } from 'uuid';
import { db, collection, getDocs, getDoc, setDoc, doc,
addDoc, updateDoc, deleteDoc, query, where, orderBy,
increment, arrayUnion } from "@/firebase";
const state = {
. . .
filteredPosts: [], // 검색된 글 목록
};
const mutations = {
. . .
setFilteredPosts(state, filteredPosts) { // 블로그 검색
state.filteredPosts = filteredPosts;
},
};
const actions = {
. . .
// Firestore에서 게시물 검색
async searchPosts({ commit, state }, searchTerm ) {
// 검색어를 소문자로
const searchTermLower = searchTerm.toLowerCase();
let filteredPosts = [];
// 검색된 결과 필터링
filteredPosts = state.posts.filter((post) => {
// title 또는 content에 검색어가 포함된 게시물 검색
return (
post.title.toLowerCase().includes(searchTermLower) ||
post.content.toLowerCase().includes(searchTermLower)
);
});
commit('setFilteredPosts', filteredPosts); // 검색 결과를 상태에 저장
},
};
const getters = {
};
export default {
namespaced: true,
state,
mutations,
actions,
getters
};
SearchView.vue
<!-- src/views/SearchView.vue -->
<template>
<v-container>
<v-row>
<v-col>
<v-text-field v-model="searchTerm" label="검색어" @input="doSearchPosts" ></v-text-field>
</v-col>
</v-row>
<v-row>
<v-col v-for="post in filteredPosts" :key="post.id" cols="12" @click="goToPost(post.id)">
<v-card>
<!-- eslint-disable -->
<v-card-title style="font-size:1em" v-html="highlight(post.title)"></v-card-title>
<v-card-text style="font-size:1em" class="mt-n2 mb-n6" v-html="highlight(sanitizeContent(post.content))"></v-card-text>
<!-- eslint-enable -->
<v-card-subtitle>
<span v-if="post.commentCount> 0" style="cursor: pointer">댓글 {{post.commentCount }} </span>
{{ post.userName }} . {{ formatDate(post.createdAt) }}
<span v-if="post.views > 0" > ({{ post.views }})</span>
</v-card-subtitle>
</v-card>
</v-col>
</v-row>
</v-container>
</template>
<script>
import { mapActions, mapState } from "vuex";
import sanitizeHtml from 'sanitize-html';
export default {
data() {
return {
searchTerm: "" // 사용자가 입력한 검색어
};
},
computed: {
...mapState('post',['filteredPosts']),
},
methods: {
...mapActions('post', ['searchPosts']),
// Firestore에서 게시물 검색
async doSearchPosts() {
if (this.searchTerm) {
// title 또는 content에 검색어가 포함된 게시물 검색
await this.searchPosts(this.searchTerm);
}
},
// content를 안전한 html로 바꿔준다.
sanitizeContent(content) {
return sanitizeHtml(content.replace(/\n/g, '<br>'), {
allowedTags: ['b', 'i', 'em', 'strong', 'p', 'br'],
allowedAttributes: {}
});
},
formatDate(date) {
if (date && date.toDate) {
return date.toDate().toISOString().replace('T', ' ').substring(0, 16);
}
return '';
},
// 상세 보기 페이지로 이동
goToPost(postId) {
this.$router.push({ name: "Post", params: { id: postId } });
},
highlight(text) {
if (!this.searchTerm)
return text;
const regex = new RegExp(`(${this.searchTerm})`, 'gi');
return text.replace(regex, '<span class="highlight">$1</span>');
}
}
};
</script>
<style>
.highlight {
background-color: yellow;
}
</style>