관리 메뉴

빵 입니다.

Cloud Firestore ID Auto Increment (w/ 꼼수) 본문

프론트엔드

Cloud Firestore ID Auto Increment (w/ 꼼수)

bread-gee 2025. 3. 28. 15:01
반응형

Nuxt 프로젝트를 진행하는데 DB로 Firebase의 Cloud Firestore를 사용 중입니다.

 

/board/:boardId로 라우팅했는데, /board/vOvuWpHHefg8ymJXDytZ 형식으로 라우팅되었습니다.

Firestore에서 document ID를 기본적으로 랜덤 문자열로 생성하기 때문입니다.

 

그래서 알아보니 Firestore는 MySQL처럼 auto increment 기능을 기본적으로 제공하지 않았습니다.

분산형 NoSQL이기 때문에 숫자 기반 auto increment는 충돌 위험이 크기 때문입니다.

 

그렇지만 각 게시글이 숫자로 된 고유 ID를 사용할 수 있도록 만들고 싶었습니다.

그래서 찾아보았습니다.

 

Firestore에서 공식 지원하는 방법은 아니고...;;;

Counter 컬렉션을 따로 만들어서 관리하는 방법이 있습니다.

 

Counter 컬렉션을 만들고, boardCounter 문서를 만들고, lastId 필드를 추가해서 사용하면 됩니다.

 

📌 신규 저장

Firestore에서 제공하는 runTransaction 메서드를 이용해 Counter 컬렉션의 boardCounter 문서를 가져와 lastId값을 증가시키고 반영하면 됩니다.

* runTransaction은 트랜잭션 내에서 읽기와 쓰기 작업을 처리하는데, 트랜잭션이 시작된 시점에서 실시간으로 데이터의 변화를 반영할 수 있습니다.

🧿 API 호출

// /server/api/board/index.post.ts
export default defineEventHandler(async (event: Event) => {
  // Counter의 boardCounter 가져오기
  const counterRef = doc(db, "counters", "boardCounter");
  let newId = 0;

  // 카운터 증가
  await runTransaction(db, async (transaction) => {
    const counterSnap = await transaction.get(counterRef);
    if (!counterSnap.exists()) {
      throw "Counter 문서가 존재하지 않습니다.";
    }
    const currentId = counterSnap.data().lastId || 0;
    newId = currentId + 1;
    transaction.update(counterRef, { lastId: newId });
  });

  try {
    const body = await readBody(event);
    const { title, content }: Board = body;

    if (!title || !content) {
      return { success: false, message: "제목과 내용을 입력해 주세요." };
    }

    // Firestore에 데이터 추가
    const data = await addDoc(collection(db, "board"), {
      id: newId,
      title,
      content,
      createdAt: new Date(),
    });

    return {
      success: true,
      data,
    };
  } catch (error) {
    return {
      success: false,
      error,
    };
  }
});

 

🧿 View에서 사용

// /pages/board/write.vue
const title = ref<string>("");
const content = ref<string>("");
const { data } = await $fetch("/api/board", {
  method: "POST",
  body: {
      title: title.value,
      content: content.value,
    },
});



📌 글 가져오기

특정 id의 데이터 가져올 때엔, board 컬렉션에서 해당 id 값을 검색해서 가져오면 됩니다.

 

🧿 API 호출

// /server/api/board/[id].get.ts
export default defineEventHandler(async (event: Event) => {
  const id = Number(event.context.params?.id);

  try {
    const q = query(collection(db, "board"), where("id", "==", id));
	// id 값으로 검색 (랜덤 docId가 아닌 우리가 만든 id 필드로 조회)
    const querySnapshot = await getDocs(q);
    const doc = querySnapshot.docs[0]; // 첫 번째 문서 반환 (id는 unique 하니까 하나만 나옴)
    const data = doc.data();

    return { success: true, data };
  } catch (error) {
    return { success: false, error };
  }
});


🧿 View에서 사용

// /pages/board/[id].vue
const boardId = route.params.id;
const board = ref<Board | null>(null);
const res = await $fetch(`/api/board/${boardId}`);
board.value = res.data;



📌 글 삭제

특정 id의 글을 삭제할 때에도, board 컬렉션에서 해당 id 값을 검색해서 가져오면 됩니다.

 

🧿 API 호출

// /server/api/board/[id].delete.ts
export default defineEventHandler(async (event: Event) => {
  const id = Number(event.context.params?.id);

  try {
    // 'id' 필드가 주어진 id와 일치하는 문서 찾기
    const q = query(collection(db, "board"), where("id", "==", id));
    const querySnapshot = await getDocs(q);

    // 문서가 없으면 에러 반환
    if (querySnapshot.empty) {
      throw createError({
        statusCode: 404,
        statusMessage: "게시글을 찾을 수 없습니다.",
      });
    }

    // 일치하는 문서가 있으면 삭제
    querySnapshot.forEach(async (docSnap) => {
      await deleteDoc(doc(db, "board", docSnap.id));
    });

    return { success: true, message: "게시글 삭제 완료" };
  } catch (error) {
    return { success: false, message: "게시글 삭제 실패" };
  }
});


🧿 View에서 사용

// /pages/board/[id].vue
const boardId = route.params.id;
const { message } = await $fetch(`/api/board/${boardId}`, {
    method: "DELETE",
});

 

반응형

'프론트엔드' 카테고리의 다른 글

ETag(Entity Tag)와 Last-Modified  (0) 2024.12.19
Multirepo VS Monorepo  (0) 2024.02.26
Monolithic Frontend VS MicroService Frontend  (0) 2024.02.26
스토리북(Storybook)  (0) 2023.10.19