Firebase와 Next.js를 활용한 비동기 작업 오류 로깅

김선은·2024년 7월 16일
0

Firebase Realtime Database를 사용하여 알림을 생성하고, 비동기 처리 시 발생할 수 있는 에러를 Firestore DB에 기록하였습니다. 이 작업으로 비동기 작업에서 발생할 수 있는 에러를 기록하고 디버깅에 활용하는 방법을 구현해보았습니다.

프로젝트 환경 설명

  • 프로젝트 환경: Next.js 14 App Router 사용

  • 알림 생성 트리거: 게시물이 업로드될 때 Firebase Realtime Database에 알림이 생성됩니다.

  • 알림 생성 로직: 알림 생성 로직은 Next.js API 엔드포인트에 작성되어 있습니다. (create-notification)

  • 에러 로깅 목적: 알림 생성 비동기 작업에 오류가 발생하면 Firestore의 error_logs 컬렉션에 오류 내용을 저장하기 위함입니다.

에러 로깅을 추가 하게 된 이유

forEach -> Promise.allSettled를 사용한 비동기 처리

프로젝트에서 유저들에게 알림을 생성하는 기능을 구현했습니다.

처음에는 단순히 유저들의 정보를 가져와 forEach로 처리했으나, 비동기 작업의 안정성을 고려하여 Promise.allSettled를 사용하도록 변경했습니다.

또한, 알림 생성 중에 발생할 수 있는 실패 케이스를 대비하여 에러를 기록하는 기능을 추가하게 되었습니다.

기존의 알림 생성 코드

src/app/api/create-notification/route.ts

// 중략
export const POST = async (request: Request) => {
  try {
    const { title, body } = await request.json()

    const usersSnapshot = await getDocs(collection(firestore, 'users'))
    if (!usersSnapshot.empty) {
      const timestamp = Date.now()

      usersSnapshot.forEach(async (doc) => {
        const uid = doc.id // user의 uid
        const newNotificationRef = ref(database, `notifications/${uid}/${timestamp}`)
        await set(newNotificationRef, {
          title,
          body,
          timestamp,
          read: false,
        })
      })

Promise.allSettled 적용

export const POST = async (request: Request) => {
  try {
    const { title, body } = await request.json()

    // 오류 테스트를 위해 추가한 유효성 검사
    if (!title || !body || typeof title !== 'string' || typeof body !== 'string') {
      const error = new Error('Invalid data: title and body are required and must be strings')
      throw error // 유효성 검사 실패 에러 (1)
    }

    const usersSnapshot = await getDocs(collection(firestore, 'users'))
    if (!usersSnapshot.empty) {
      const timestamp = Date.now()

      const notificationPromises = usersSnapshot.docs.map((doc) => {
        const uid = doc.id
        const newNotificationRef = ref(database, `notifications/${uid}/${timestamp}`)
        return set(newNotificationRef, {
          title,
          body,
          timestamp,
          read: false,
        }).catch(async (error) => {
          throw error // 에러 (2)
        })
      })

      await Promise.allSettled(notificationPromises)

      return NextResponse.json({ message: 'Notifications created successfully' })
    } else {
      return NextResponse.json({ error: 'No users found' }, { status: 404 })
    }
  } catch // 생략

에러 로그 기록 추가

알림 생성 중에 발생할 수 있는 에러를 기록하기 위해 Firestore에 error_logs 컬렉션을 추가했습니다.

에러가 발생하면 해당 정보를 /api/log 엔드포인트로 전송하여 기록합니다.

// Promise.allSettled 적용 부분 코드의 뒷부분

catch (error: unknown) {
    // 타입 가드 사용과 throw error 처리
    if (error instanceof Error) {
      // 에러 시 로그 남기기
      const apiUrl = process.env.NEXT_PUBLIC_API_URL
      await fetch(`${apiUrl}/api/log`, {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({
          error: error.message,
          stack: error.stack,
        }),
      }).catch((logError) => {
        console.error('Error logging notification error:', logError)
      })

      console.error('Error creating notifications:', error)
      return NextResponse.json({ error: 'Error creating notifications' }, { status: 500 })
    } else {
      // Error 객체가 아닌 경우
      console.error('Unknown error', error)
      return NextResponse.json({ error: 'Unknown error' }, { status: 500 })
    }
  }

fetch 경로 문제

처음에는 fetch 요청에서 상대 경로를 사용했으나, 로컬 환경에서는 절대 경로를 사용해야 한다는 문제를 발견했습니다. 이에 따라 fetch 요청을 수정하여 절대 경로를 사용하도록 하였습니다.

에러 메세지

Error logging notification error: TypeError: Failed to parse URL from /api/log

문제 해결
next api의 엔드포인트 fetch 요청에서 절대 경로를 사용하도록 수정하였습니다.
src/app/api/create-notification/route.ts

await fetch('http://localhost:3000/api/log', { // 절대 경로 사용
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    uid: 'unknown',
    error: error.message,
    stack: error.stack,
  }),
});

결과 확인하기

테스트 API 작업은 포스트맨을 이용했습니다. 유효성에 걸리게 body의 값을 비우고 POST 요청을 보냅니다.

Firestore의 error_logs 컬렉션에 오류 로그가 추가된 것이 보입니다.

해당 에러 시 서버 콘솔입니다.

스택 트레이스를 통한 오류 분석

에러 로그에 stack 필드를 추가하여 에러 발생 위치와 경로를 추적할 수 있게 되었습니다. 예를 들어, POST 요청에서 body 필드를 아예 빼고 전송했을 때, 다음과 같은 스택 트레이스를 통해 문제를 분석할 수 있습니다.

SyntaxError: Unexpected token } in JSON at position 25
    at JSON.parse (<anonymous>)
    at parseJSONFromBytes (node:internal/deps/undici/undici:5584:19)
    at successSteps (node:internal/deps/undici/undici:5555:27)
    at fullyReadBody (node:internal/deps/undici/undici:1665:9)
    at process.processTicksAndRejections (node:internal/process/task_queues:95:5)
    at async POST (webpack-internal:///(rsc)/./src/app/api/create-notification/route.ts:16:33)
    ...
  • 마지막 줄을 보면 오류 발생 위치를 알 수 있습니다.
  • 유효성 검사 부분에서 body가 없어서 발생한 에러입니다.
profile
기록은 기억이 된다

0개의 댓글