토이 프로젝트 - Vue, Firebase로 서버리스 PWA 개발

13. Vue(with Vuetify)와 Firebase로 서버리스 PWA myBlog 개발 - 블로그 카테고리

그랜파 개발자 2025. 2. 14. 03:28

블로그 카테고리 관리

블로그에서 카테고리를 사용하면 다음과 같은 장점이 있습니다.

1. 콘텐츠 정리 및 구조화

비슷한 주제의 글을 그룹화하여 블로그가 체계적으로 정리됨
예: "프로그래밍", "라이프스타일", "여행", "요리" 등

사용자 입장: 원하는 주제를 빠르게 찾을 수 있음
블로그 운영자 입장: 콘텐츠를 쉽게 관리 가능

2. 사용자 경험(UX) 향상

방문자가 관심 있는 글을 쉽게 찾도록 유도
카테고리를 클릭하면 관련 글 목록을 바로 확인 가능

방문자가 블로그에서 더 오래 머무를 가능성이 증가

3. SEO(Search Engine Optimization) 최적화

카테고리별로 검색엔진에 최적화된 URL 구조 생성 가능
예:
- https://example.com/category/vue
- https://example.com/category/javascript

구조화된 데이터 제공 → 검색엔진이 블로그를 더 잘 이해함
검색 노출(트래픽) 증가 효과

4. 블로그 콘텐츠 확장 가능

새로운 주제를 추가하거나 기존 카테고리를 세분화 가능
예:
- "개발" → "웹 개발", "모바일 개발", "데이터 사이언스"

다양한 관심사를 가진 독자를 유입할 수 있음

5. 추천 및 필터 기능 강화

방문자가 특정 카테고리의 글을 더 많이 읽으면 추천 시스템 활용 가능
예:
- "프론트엔드" 글을 자주 보면, 관련 글 추천

맞춤형 콘텐츠 제공으로 재방문율 증가

6. 광고 및 수익 최적화

특정 카테고리에 맞는 광고를 배치하여 광고 효과 극대화 가능
예:
- "IT & 개발" → 프로그래밍 도서 광고
- "여행" → 항공권 및 호텔 광고

광고 수익 최적화 가능

결론

카테고리는 블로그의 콘텐츠를 체계적으로 정리하고,
사용자 경험을 향상시키며,
SEO 및 수익에도 긍정적인 영향을 줍니다.

myBlog 카테고리 관리

다양한 주제의 글을 쓸 수 있을 것이고,
또 어떤 주제의 글은 여러 개로 나눠서 작성할 수도 있습니다.
그래서 글의 카테고리가 있으면 좋습니다.

1. 카테고리 개발 요구 사항

블로그에 카테고리를 추가하기 위해 개발해야 할 내용들을 생각하여 봅시다.

  • 사용자별로 카테고리가 다릅니다.
    그러므로 사용자별 카테고리 등록할 수 있어야 합니다.
  • 등록된 카테고리의 전체 목록을 볼 수 있고,
  • 등록된 카테고리를 선택하여 삭제할 수 있습니다.
  • 등록된 카테고리를 삭제해도 등록된 글 속에 있는 카테고리는 삭제되지 않습니다.
    그러므로 기본적으로 카테고리 없음에 대한 처리가 필요합니다.
  • 사용자의 블로그에서 카테고리를 선택하여 해당 카테고리의 글만 볼 수 있습니다.
  • 삭제된 카테고리 등의 처리를 위하여 기본적으로 ‘카테고리 없음’에 대한 카테고리가 필요합니다.
  • 글을 쓸 때에 카테고리를 선택합니다.
    글을 저장하면 선택한 카테고리도 함께 저장이 됩니다.

2. 카테고리 관리

1. 카테고리 관리 컴포넌트

  1. 등록된 전체 카테고리 목록
  2. 새로운 카테고리의 등록
  3. 등록된 카테고리의 삭제

2. 사용자별 카테고리 관리

카테고리는 사용자 별로 관리됩니다.
그러므로 로그인 상태에서 카테고리를 등록할 수 있고,
카테고리 컬렉션에는 사용자를 구분할 수 있는 userId가 있어야 합니다.

3. 카테고리 개발

  1. DB : firestore
  2. 카테고리 컬렉션 : categories
  3. 카테고리 문서 항목 :
    • id: 카테고리 Id (데이터베이스 자동 생성)
    • category : 카테고리 이름
    • userId : 사용자 Id
  4. 카테고리 데이터 처리
  5. 사용자 ID로 필터링된 카테고리 가져오기
const q = query( collection(db, "categories"), where("userId", "==", userId)
  • 카테고리 저장
const docRef = await addDoc(collection(db, "categories"), {
    name: newCategory, userId: userId, 
});
  • 카테고리 삭제
await deleteDoc(doc(db, "categories", id));

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 = {
  loading: false,
  categories: [],   //  카테고리
};

const mutations = {
  setLoading(state, loading) {
    state.loading = loading;
  },
  // 카테고리 관리
  setCatogories(state, categories) {  // 카테고리 
    state.categories = categories;
  },
  addCatogory(state, category) {    // 카테고리 추가
    state.categories.push(category);
  },
  removeCatogory(state, id) {    // 카테고리 제거
    let categories = [];
    categories = state.categories.filter((category) => category.id !== id);
    state.categories = categories;
  },
};

const actions = {
  // -- 카테고리 관리 ----
  async fetchCategories({ commit }, { userId }) { 
    if (!userId) return;

    try {
      // 사용자 ID로 필터링된 카테고리 가져오기
      const q = query(
        collection(db, "categories"),
        where("userId", "==", userId)
      );
      const querySnapshot = await getDocs(q);

      let categories = [];
      categories = querySnapshot.docs.map((doc) => ({
        id: doc.id,
        ...doc.data(),
      }));

      // 로드한 카테고리를 상태에 저장한다.
      commit("setCatogories", categories); 

    } catch (error) {
      alert("Error fetching categories : " + error.message);
    }
  },
  async addCategory({ commit }, { newCategory, userId } ) {
    if (!newCategory || !userId) return;

    try {
      commit('setLoading', true);
      const docRef = await addDoc(collection(db, "categories"), {
        name: newCategory, userId: userId, 
      });
      commit("addCatogory", { id: docRef.id, name: newCategory });
      commit('setLoading', false);
    } catch (error) { 
      alert("Error adding category : " + error.message);
    }
  },
  async removeCategory({ commit }, id) {
    try {
      commit('setLoading', true);
      await deleteDoc(doc(db, "categories", id)); // db에 저장된 카테고리 에서 제거
      commit("removeCatogory", id);     // state에 있는 카테고리에서 제거
      commit('setLoading', false);
    } catch (error) {
      console.error("Error deleting category:", error);
    }
  },
};

const getters = { 

};

export default {
  namespaced: true,
  state,
  mutations,
  actions,
  getters
};

CategoryView.vue

<!-- src/views/CategoryView.vue -->
<template>
  <v-container>
    <v-row>
      <v-col cols="12">
        <v-card class="pa-4">
          <v-card-title style="font-size:1.1em">카테고리 관리</v-card-title>

          <!-- 새로운 카테고리 추가 -->
          <v-text-field label="새 카테고리 추가" v-model="newCategory" @keyup.enter="doAddCategory"></v-text-field>
          <v-card-actions class="mt-n4">
            <v-spacer></v-spacer><v-btn color="primary" @click="doAddCategory">추가</v-btn>
          </v-card-actions>

          <v-list class="mt-n4">
            <v-card-title style="font-size:1.1em">전체 카테고리</v-card-title>
            <!-- Firestore에서 불러온 카테고리 -->
            <v-list-item v-for="category in categories" :key="category.id">
              <v-list-item-title style="font-size:1em">{{ category.name }}</v-list-item-title>
              <v-btn icon @click="doRemoveCategory(category.id)">
                <v-icon text>mdi-delete</v-icon>
              </v-btn>
            </v-list-item>
          </v-list>

        </v-card>
      </v-col>
    </v-row>

    <div class="text-center">
      <v-progress-circular v-if="loading" indeterminate></v-progress-circular>
    </div>
  </v-container>
</template>

<script>
import { mapState, mapActions } from 'vuex';

export default {
  data() {
    return {
      newCategory: "", // 새로운 카테고리 입력 값
    };
  },

  computed: {
    ...mapState('auth', ['profile']),
    ...mapState('post', ['categories', 'loading']),
  },

  methods: {
    ...mapActions('post', ['addCategory', 'removeCategory']),

    async doAddCategory() {
      if (!this.newCategory.trim()) return;

      const userId = this.profile.userId;
      this.addCategory({ 
        newCategory: this.newCategory.trim(), 
        userId: userId 
      });
      this.newCategory = ""; // 입력 필드 초기화        
    },

    async doRemoveCategory(id) {
      this.removeCategory(id);
    },
  },
};
</script>