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,
})
})
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 요청을 수정하여 절대 경로를 사용하도록 하였습니다.
에러 메세지
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)
...