https://sentry.io 사이트에서 회원가입 > Project 탭 > 프로젝트 생성

Next.js 프로젝트 터미널로 이동하여 npx @sentry/wizard@latest -i nextjs --saas --org jinii9 --project wms를 입력해준다.
위 과정을 진행하면 프로젝트 디렉토리가 생긴다.

DSN 키와 Auth Token을 .env 파일에 작성해준다.NEXT_PUBLIC_SENTRY_DSN= DNS키
NEXT_PUBLIC_SENTRY_AUTH_TOKEN= 토큰
공식문서
Sentry에서 제공하는 ErrorBoundary는 React의 ErrorBoundary 기능을 기반으로 구현되어 React 컴포넌트 렌더링 중에 발생하는 JS 오류를 감지하고, 오류가 발생하면 자동으로 Sentry로 해당 오류 정보를 전송한다. 추가로 fallback UI를 표시하는 기능도 제공한다.
보통은 Sentry의 ErrorBoundary와 React의 ErrorBoundary 중 하나만 선택해서 사용한다고 한다. 나는 react-error-boundary의 ErrorBoundary를 사용하고, 그 안에서 Sentry 기능을 통합하였다.
📁 frontend/src/components/providers/errorBoundaries/ApiErrorBoundary.tsx
"use client";
import { ErrorBoundary } from "react-error-boundary";
import { useQueryErrorResetBoundary } from "@tanstack/react-query";
import { ApiFallback } from "@/components/fallback/ApiFallback";
import { ErrorInfo } from "react";
import { isApiError } from "@/utils/errors";
import * as Sentry from "@sentry/nextjs";
type Props = {
children: React.ReactNode;
};
const handleError = (error: Error, info: ErrorInfo) => {
// global error boundaries로 에러 위임
if (!isApiError(error)) {
throw error;
}
Sentry.withScope((scope) => {
// scope.setTag("errorType", "api_error");
scope.setTag("errorBoundary", "ApiErrorBoundary");
// scope.setExtra("componentStack", info.componentStack);
scope.setContext("error_details", {
componentStack: info.componentStack,
});
Sentry.captureException(error);
});
console.log("API 에러 발생:", error);
console.log("컴포넌트 스택:", info.componentStack);
};
export function ApiErrorBoundary({ children }: Props) {
const { reset } = useQueryErrorResetBoundary();
return (
<ErrorBoundary
FallbackComponent={ApiFallback}
onReset={reset}
onError={handleError}
>
{children}
</ErrorBoundary>
);
}
📁 frontend/src/components/providers/errorBoundaries/RootErrorBoundary.tsx
import { RootFallback } from "@/components/fallback/RootFallback";
import { ErrorInfo } from "react";
import { ErrorBoundary } from "react-error-boundary";
import * as Sentry from "@sentry/nextjs";
const handleError = (error: Error, info: ErrorInfo) => {
Sentry.withScope((scope) => {
scope.setTag("errorBoundary", "RootErrorBoundary");
// scope.setTag("errorType", "unhandled_error");
scope.setLevel("fatal");
scope.setContext("error_details", {
componentStack: info.componentStack,
});
// 브라우저 환경 정보 추가
scope.setContext("browser_info", {
userAgent: navigator.userAgent,
url: window.location.href,
screenSize: `${window.innerWidth}x${window.innerHeight}`,
});
Sentry.captureException(error);
});
console.log("Root 에러 발생:", error);
console.log("컴포넌트 스택:", info.componentStack);
};
export function RootErrorBoundary({ children }: { children: React.ReactNode }) {
return (
<ErrorBoundary FallbackComponent={RootFallback} onError={handleError}>
{children}
</ErrorBoundary>
);
}
React는 React Error Boundary를 사용할지라도, 기본적으로 모든 오류를 콘솔에 로깅한다. React에서 오류가 발생하면
console.error()등을 사용해 콘솔에 로그를 남긴다.
- 개발 모드에서 React는 Error Boundary 내에서 잡힌 오류를 상위
글로벌로 다시 던지기 때문에 Sentry가 오류 출처를ErrorBoundary가 아니라 전역 오류로만 인식할 수도 있다.
나는 api 에러는 API 요청을 처리하는 공통 유틸리티 함수에서 핸들링하고, 네트워크 에러는 RootErrorBoundary에서 처리해주었다.
즉, 위에서 작성했던 RootErrorBoundary, ApiErrorBoundary를 수정하였다.
RootErroBoundary는 네트워크 에러와 그 외에 알 수 없는 에러를 담당하고, ApiErrorBoundary는 api 에러 관련해서만 개발환경의 콘솔에 띄워준다.
일단 커스텀 에러 클래스와 에러 체크 유틸리티 함수들을 모아둔 errors.ts를 작성한다.
📁 frontend/src/utils/errors.ts
/**
* 커스텀 에러 클래스 모음
* 에러 체크 유틸리티 함수 모음
* @param error
* @returns
*/
export enum ErrorType {
NETWORK = "network",
// SERVER = "server",
// CLIENT = "client",
NOT_FOUND = "404",
UNAUTHORIZED = "403",
INTERNAL_SERVER = "500",
BAD_REQUEST = "400",
TIMEOUT = "timeout",
}
// 커스텀 에러 클래스
export class NetworkError extends Error {
constructor(
message: string,
public status?: number,
public type?: ErrorType,
public endpoint?: string
) {
super(message);
this.name = "NetworkError";
}
}
export class ApiError extends Error {
constructor(
message: string,
public endpoint: string,
public status?: number, // HTTP 상태 코드
public type?: ErrorType
) {
super(message);
this.name = "ApiError";
}
}
// 에러 체크 유틸리티 함수
export function isApiError(error: Error): boolean {
return (
error instanceof ApiError ||
error.name === "ApiError" ||
error.message.includes("API")
);
}
export function isNetworkError(error: Error): boolean {
return (
error instanceof NetworkError ||
error.name === "NetworkError" ||
error.message.includes("network")
);
}
이제 공통 api 함수를 만들어줘야 한다. 이때 처음에는 단순하게 server, client로만 api 에러를 처리했는데, 조금 더 세분화하여 진행하였다.
📁 frontend/src/libs/api.ts
/**
* @param {string} endpoint 요청할 API 엔드포인트
* @param {RequestInit} options fetch 함수의 옵션 (옵션) ex) HTTP 메서드, 헤더, 바디..
* @returns {Promise<T>} 요청 성공 시 제네릭 타입 T의 데이터 반환
* @examples
* export function getUserData() {
* return useQuery ({
* queryKey: ["user"],
* queryFn: () => fetchApi("/user");
* })
*/
import * as Sentry from "@sentry/nextjs";
import { ApiError, ErrorType, NetworkError } from "@/utils/errors";
type Props = {
endpoint: string;
options?: RequestInit;
};
export async function fetchApi<T>({ endpoint, options }: Props): Promise<T> {
try {
const response = await fetch(`/api/${endpoint}`, options);
if (!response.ok) {
let message = "";
let type = "";
if (response.status) {
if (response.status >= 500) {
// scope.setTag("api_error", ApiErrorType.INTERNAL_SERVER);
message = "Server Error: 500";
type = ErrorType.INTERNAL_SERVER;
} else if (response.status === 404) {
message = "Client Error: 404";
type = ErrorType.NOT_FOUND;
} else if (response.status === 403) {
message = "Client Error: 403";
type = ErrorType.UNAUTHORIZED;
} else if (response.status === 400) {
message = "Client Error: 400";
type = ErrorType.BAD_REQUEST;
}
}
throw new ApiError(message, endpoint, response.status, type as ErrorType);
}
return response.json();
} catch (error) {
// 네트워크 에러 처리
if (error instanceof TypeError && error.message.includes("fetch")) {
throw new NetworkError("Networ Error", undefined, ErrorType.NETWORK);
}
// Sentry에 에러 전송
Sentry.withScope((scope) => {
scope.setTag(
"error_type",
error instanceof ApiError ? error.type : "unknown"
);
scope.setContext("api_details", {
endpoint,
status: error instanceof ApiError ? error.status : undefined,
type: error instanceof ApiError ? error.type : undefined,
});
Sentry.captureException(error);
});
throw error;
}
}