Nodemailer 를 이용한 에러 로깅 시스템 도입기

D uuu·2024년 12월 14일
0

Next.js14 프로젝트

목록 보기
13/15
post-thumbnail

같은 실수를 반복하지 않기 위해

먹기록 프로젝트를 운영하면서 사이트 접속 장애를 겪은 적이 있습니다. 당시 에러 로그를 남기지 않아서 문제를 추적하는 데 어려움을 겪었는데요. 그 경험을 바탕으로 에러 핸들링의 중요성에 대해 회고글을 작성한 적이 있습니다.

서비스 운영 회고 1탄 : 에러 로그의 중요성

서비스가 배포된 이후에는 사용자의 환경에서 발생하는 다양한 문제를 실시간으로 파악하기가 어렵습니다. 이럴 때 에러 로깅 시스템이 잘 구축되어 있다면, 사용자가 어떤 환경에서 접속했는지, 그리고 어떤 에러가 발생했는지 즉시 확인하고 신속히 대응할 수 있습니다. 이를 교훈 삼아, 잇핏 프로젝트에서는 에러 로깅 시스템을 도입했는데요. 이번 글에서는 도입 과정과 느낀 점을 공유하려 합니다.

에러 로깅 시스템 : Nodemailer 도입기

📝왜 Nodemailer를 선택했을까?

에러 로깅 도구로 여러 옵션을 고민한 끝에 Nodemailer 를 이용하기로 했습니다. 그 이유로는 다음과 같습니다.

  1. 비용 효율성 : Nodemailer 는 무료로 이메일 전송 API를 제공하여 부담 없이 시작할 수 있습니다.
  2. 간단한 커스터마이징 : 에러 메시지를 HTML 형식으로 작성해, 사용자 환경, 기기 정보, 발생 위치 등 핵심 정보를 구조적으로 담아 전송할 수 있습니다.
  3. 프로젝트 규모 적합성 : 초기 단계의 소규모 프로젝트에 적합하며, 추후 프로젝트가 성장하면 Sentry와 같은 고급 모니터링 도구로 확장 가능합니다.

물론 Sentry 는 JavaScript 런타임 에러, 네트워크 에러 등 다양한 문제를 더 정밀하게 추적하고 성능 모니터링도 가능하지만, 현재 프로젝트에서는 Nodemailer 가 간단하고 빠르게 설정할 수 있다는 점에서 더 적합하다고 판단했습니다.

웹에서 발생할 수 있는 에러 유형

웹에서 발생할 수 있는 에러는 크게 세 가지로 나눌 수 있습니다.

1. UI 에러

  • 정의되지 않은 데이터를 (null, undefined) 표시하다가 발생하는 등 웹을 표시하는 과정에서 발생하는 오류를 의미합니다.

2. 네트워크 에러

  • 서버와의 통신에서 발생하며, 예를 들어 잘못된 데이터 형식, 불안정한 서버 상태, 존재하지 않는 API 요청 등이 원인이 될 수 있습니다.

3. 비즈니스 로직 에러

  • 사용자 권한 미충족, 잘못된 입력값 등 비즈니스 규칙을 위반했을 때 발생합니다.

에러 로깅 시스템 구현 과정

💡네트워크 에러 처리

그동안의 에러 처리 방식을 살펴보면 React Query를 사용한다면 useQuery 객체의 error를 활용했으며, fetch나 axios를 사용한다면 try-catch 를 통해 에러 핸들링을 해왔습니다.

useQuery 사용

 const { data, error, isError, isLoading } = useQuery(['blogs'], getPosts, {
    useErrorBoundary: true,
  });
  
 if(error) {
 	// 에러 처리
 }

try-catch 사용

export async function getPosts() {
  try {
    const res = await axios.get('/posts');
    return res.data;
  } catch (err) {
    if (axios.isAxiosError(err) {
      console.error(err.response?.data.message || '알수없는 에러');
    } else {
      console.error('알수없는 에러');
    }
  }
}

선언적으로 에러 핸들링 하기

앞서 설명한 방법대로 에러 처리를 구현할 수 있지만, 모든 API 호출마다 에러 처리 로직을 작성해야 하는 번거로움이 있었습니다. 또한, API 요청 로직과 에러 처리 로직이 한 곳에 작성되어 있어 분리할 필요성을 느꼈습니다.

이를 해결하기 위해 Fetch 를 래핑한 함수를 만들어 사용했습니다. 이 함수의 응답 인터셉터에서 에러가 발생하면 자동으로 이메일로 로그를 전송하도록 설정했습니다. 이렇게 구현하면, defaultFetch를 사용하는 모든 로직에서 일괄적으로 에러 처리가 가능하며, 중복 코드를 줄이고 에러 관리의 일관성을 확보할 수 있습니다.

따라서 네트워크 요청 시 에러가 발생하면 응답 interceptor 에서 에러를 감지해 에러 정보를 email 로 전송하는 로직을 추가했습니다.

export const defaultFetch = returnFetch({
    baseUrl: `${BASE_URL}/api`,
    defaultHeaders: {
        'Content-Type': 'application/json',
    },
    interceptors: {
        request: async (args: [string, RequestInit]) => {
            const [url, options] = args;

            return args;
        },
        response: async (response: Response, requestArgs: [string, RequestInit]) => {
            if (!response.ok) {
                const errorMessage = `Fetch request failed with status: ${response.status}`;

                const errorOptions = {
                    errorLocation: requestArgs[0],
                    errorMessage: errorMessage,
                };
                sendErrorMail(errorOptions);

                throw new Error(errorMessage);
            }

            return response;
        },
    },
});

클라이언트에서 발생하는 에러 처리

UI와 비즈니스 로직과 관련된 에러는 대개 배포 전에 테스트를 하는 과정에서 발견되지만, 예기치 않은 상황에 대비해 클라이언트에서도 에러 처리를 해야 합니다.

잇핏 프로젝트는 Next.js 를 사용했기 때문에 error.tsxnot-found.tsx 파일을 적극 활용했습니다. 이러한 파일을 활용하면 에러 발생 시 적절한 UI를 표시하거나 재시도 옵션을 제공해, 사용자 경험을 최대한 유지하면서 에러를 처리할 수 있습니다. 특히, error.tsx 는 Error Boundary 와 유사한 역할을 수행하며 하위 컴포넌트에서 발생한 에러가 최상위로 전파되는 구조를 가지고 있습니다. 이 파일에서 에러를 감지해 이메일로 로그를 전송하는 기능을 추가하면, 클라이언트에서 발생하는 에러가 상단으로 전파되어 이 곳에서 간단하게 에러 처리를 수행할 수 있습니다.

'use client';

export default function Error({ error, reset }: { error: Error; reset: () => void }) {
  useEffect(() => {
    // 에러 발생 시 이메일 로그 전송
    sendErrorMail({ errorLocation: path, errorMessage: error.message });
  }, [error]);

  return (
    <div>
      <h1>Something went wrong!</h1>
      <button onClick={reset}>Retry</button>
    </div>
  );
}

실제로 받은 에러 로그

아래는 실제로 수집된 에러 로그의 일부 입니다. 로그에는 요청 URL, 에러 메시지, 사용자 환경 정보 등이 포함되어 있어 문제를 빠르게 파악할 수 있었습니다.

마무리하며..

잇핏 프로젝트를 배포하며, 이번에는 에러 로그 시스템 덕분에 예상치 못한 오류에도 빠르게 대응할 준비가 되어 있었습니다. 이전 프로젝트와 비교했을 때, 단순히 안정적인 서비스를 제공하는 것을 넘어, 문제를 예측하고 대비할 수 있다는 든든함을 느낄 수 있었습니다.

다만, 아쉬운 점은 에러 로그가 이메일로만 전달되다 보니, 이를 분석하고 패턴을 파악하기에 불편함이 있었습니다. 예를 들어, 어떤 유형의 에러가 자주 발생하는지 또는 특정 환경에서만 발생하는지 파악하기 위해서는 더 체계적인 도구가 필요하다는 생각이 들었고, 이런 면에서 Sentry와 같은 에러 모니터링 툴의 필요성을 느끼기도 했습니다.

또한, 프로젝트 개선점으로는 에러 로그를 단순히 오류 수집에 그치지 않고, 사용자 행동 데이터를 분석하는 데 활용하는 방안이 떠올랐습니다. 실제로 프로젝트를 친구들에게 배포해 사용을 요청했을 때, 예상만큼 많은 기능이 활용되지 않는 모습을 보며 아쉬움이 있었습니다. 사용자가 어떤 단계에서 이탈했는지, 특정 기능의 사용률이 낮은 이유는 무엇인지 등의 데이터를 알게 된다면, 서비스 개선에 큰 도움이 될 것 같습니다. 앞으로는 이러한 데이터를 수집하고 기록하는 시점과 방법에 대한 고민이 필요할 것 같습니다.

profile
배우고 느낀 걸 기록하는 공간

0개의 댓글