본문 바로가기
JavaScript/React

25.componentDidCatch 로 에러 잡아내기 / Sentry 연동

by 송재근 2021. 8. 5.
반응형

componentDidCatch 라는 생명주기 메서드를 사용하여 리액트 애플리케이션에서 발생하는 에러를 처리하는 방법을 알아보도록 하겠음

 

새로운 프로젝트 생성

$ npx create-react-app error-catch

 

해당 디렉터리를 에디터로 열고, 개발 서버를 시작

$ cd error-catch
$ yarn start

 

리액트 앱에서 에러가 발생하는 상황

리액트 앱이 어떤 상황에서 에러가 발생하게 되는지 알아보자

 

User.js

import React from 'react';

function User({ user }) {
  return (
    <div>
      <div>
        <b>ID</b>: {user.id}
      </div>
      <div>
        <b>Username:</b> {user.username}
      </div>
    </div>
  );
}

export default User;

이 컴포넌트는 user 라는 props 를 받아와서 해당 데이터의 id 와 username 값을 보여줌

 

App.js

import React from 'react';
import User from './User';

function App() {
  const user = {
    id: 1,
    username: 'velopert'
  };
  return <User user={user} />;
}

export default App;

정상적으로 동작한다.

그러나 user props 를 제대로 설정하지 않았다면?

 

App.js

import React from 'react';
import User from './User';

function App() {
  const user = {
    id: 1,
    username: 'velopert'
  };
  return <User />;
}

export default App;

이렇게 에러가 발생

방금과 같은 에러를 방지하려면?

 

User.js

import React from 'react';

function User({ user }) {
  if (!user) {
    return null;
  }

  return (
    <div>
      <div>
        <b>ID</b>: {user.id}
      </div>
      <div>
        <b>Username:</b> {user.username}
      </div>
    </div>
  );
}

export default User;

user 값이 존재하지 않는다면 null 을 렌더링하게 됨

리액트 컴포넌트에서 null 을 렌더링하게되면 아무것도 나타나지 않게 됨 → 이를 "null checking" 이라고 부름

화면에 아무것도 보여지지 않는것은 마찬가지이지만, 적어도 에러는 발생하지 않음

보통 데이터를 네트워크 요청을 통하여 나중에 데이터를 받아오게 되는 상황이 발생하는 경우 이렇게 데이터가 없으면 null 을 보여주거나, 아니면 <div>로딩중</div>과 같은 결과물을 렌더링하면 됨

 

function Users({ users }) {
  return (
    <ul>
      {users.map(user => (
        <li key={user.id}>{user.username}</li>
      ))}
    </ul>
  );
}

만약에 위와 같은 컴포넌트에 users 값을 설정해주지 않았을 때에도 렌더링 과정에서 오류가 발생하게 됨

 users 가 undefined 이면 당연히 배열의 내장함수 map 또한 존재하지 않기 때문

 

function Users({ users }) {
  if (!users) return null;

  return (
    <ul>
      {users.map(user => (
        <li key={user.id}>{user.username}</li>
      ))}
    </ul>
  );
}

때문에 다음과 같이 users 가 없으면 다른 결과물을 반환하는 작업을 해야 함

 

function Users({ users, onToggle }) {
  if (!users) return null;

  return (
    <ul>
      {users.map(user => (
        <li key={user.id} onClick={() => onToggle(user.id)}>
          {user.username}
        </li>
      ))}
    </ul>
  );
}

만약에 위 컴포넌트에 onToggle props 를 전달하지 않으면, 에러가 발생하게 될 것

 

function Users({ users, onToggle }) {
  if (!users) return null;

  return (
    <ul>
      {users.map(user => (
        <li key={user.id} onClick={() => onToggle(user.id)}>
          {user.username}
        </li>
      ))}
    </ul>
  );
}

Users.defaultProps = {
  onToggle: () => {
    console.warn('onToggle is missing!');
  }
};

에러를 방지하기 위해선 onToggle 을 props 로 넣어주는 것을 까먹지 않기 위해서 다음과 같이 defaultProps 설정을 해주는 방법이 있음

다른 솔루션으로는 PropTypes 라는 것을 사용하는 방법도 있음

 PropTypes 를 사용하면 필요한 데이터를 넣지 않았을 때 개발 단계에서 경고를 볼 수 있기 때문에 실수로 props 설정을 깜박하는 일을 방지 할 수 있음

단, 사용법이 좀 불편하고 귀찮기 때문에 이를 사용하는 것 대신에 나중에 TypeScript 또는 Flow 를 사용해서 관리 하는 것을 권장

 

componentDidCatch 로 에러 잡아내기

자, 이번에는 componentDidCatch 생명주기 메서드를 사용하여 우리가 사전에 예외처리를 하지 않은 에러가 발생 했을 때 사용자에게 에러가 발생했다고 알려주는 화면을 보여주자

 

ErrorBoundary.js

import React, { Component } from 'react';

class ErrorBoundary extends Component {
  state = {
    error: false
  };

  componentDidCatch(error, info) {
    console.log('에러가 발생했습니다.');
    console.log({
      error,
      info
    });
    this.setState({
      error: true
    });
  }

  render() {
    if (this.state.error) {
      return <h1>에러 발생!</h1>;
    }
    return this.props.children;
  }
}

export default ErrorBoundary;

여기서 componentDidCatch 메서드에는 두개의 파라미터를 사용하게 되는데 첫번째 파라미터는 에러의 내용, 두번째 파라미터에서는 에러가 발생한 위치를 알려줌

이 메서드에서 현재 컴포넌트 상태 error 를 true 로 설정을 해주고, render() 메서드에서는 만약 this.state.error 값이 true 라면 에러가 발생했다는 문구를 렌더링하도록 하고 그렇지 않다면 this.props.children 을 렌더링하도록 처리

 

App.js

import React from 'react';
import User from './User';
import ErrorBoundary from './ErrorBoundary';

function App() {
  const user = {
    id: 1,
    username: 'velopert'
  };
  return (
    <ErrorBoundary>
      <User />
    </ErrorBoundary>
  );
}

export default App;

그리고 이전에 User 컴포넌트에서 null checking 을 하는 코드를 주석처리

 

User.js

import React from 'react';

function User({ user }) {
  // if (!user) {
  //   return null;
  // }

  return (
    <div>
      <div>
        <b>ID</b>: {user.id}
      </div>
      <div>
        <b>Username:</b> {user.username}
      </div>
    </div>
  );
}

export default User;

흰 화면이 아닌 "에러 발생!" 이라는 문구가 보여지게 될 것

 

Sentry 연동

componentDidCatch 를 사용해서 앱에서 에러가 발생했을 때 사용자에게 에러가 발생했음을 인지시켜줄 수 는 있지만, componentDidCatch 가 실제로 호출되는 일은 서비스에서 "없어야 하는게" 맞음

만약에 우리가 놓진 에러가 있다면, 우리가 이를 알아내어 예외 처리를 해주어야 함

우리는 발견해내지 못했지만, 사용자가 발견하게 되는 그런 오류들은 componentDidCatch 에서 error 와 info 값을 네트워크를 통하여 다른 곳으로 전달을 해주면 됨

 다만 이를 위해서 따로 서버를 만드는건 굉장히 번거로운 작업이므로 굉장히 괜찮은 솔루션으로, Sentry 라는 상용서비스를 사용

 

우리가 아까 만든 프로젝트에 한번 적용을 해보겠음

$ yarn add @sentry/browser
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import * as Sentry from '@sentry/browser';
import * as serviceWorker from './serviceWorker';

Sentry.init({
  dsn: 'https://87fba3b585d940f58806848807325ffb@sentry.io/1493504'
});

ReactDOM.render(<App />, document.getElementById('root'));

// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.
// Learn more about service workers: https://bit.ly/CRA-PWA
serviceWorker.unregister();

Sentry.init() 을 사용 할 때 넣는 dsn 값은 프로젝트마다 다름

 이렇게 에러가 발생 했을 때 Sentry 쪽으로 전달이 되는 것은 개발모드일땐 별도의 작업을 하지 않아도 잘 되지만, 나중에 프로젝트를 완성하여 실제 배포를 하게 됐을 때는 componentDidCatch 로 이미 에러를 잡아줬을 경우 Sentry 에게 자동으로 전달이 되지 않음

 

ErrorBoundary.js

import React, { Component } from 'react';
import * as Sentry from '@sentry/browser';

class ErrorBoundary extends Component {
  state = {
    error: false
  };

  componentDidCatch(error, info) {
    console.log('에러가 발생했습니다.');
    console.log({
      error,
      info
    });
    this.setState({
      error: true
    });
    if (process.env.NODE_ENV === 'production') {
      Sentry.captureException(error, { extra: info });
    }
  }

  render() {
    if (this.state.error) {
      return <h1>에러 발생!</h1>;
    }
    return this.props.children;
  }
}

export default ErrorBoundary;

componentDidCatch 에서 process.env.NODE_ENV 값을 조회했데, 이를 통하여 현재 환경에 개발 환경인지 프로덕션 환경인지 (production / development) 확인 할 수 있음

 

프로덕션 환경에서 잘 작동하는지 확인하기

프로덕션 환경에서도 잘 작동하는지 확인하기 위해서는 프로젝트를 빌드해주어야 함

 

$yarn build

 build 디렉터리에 있는 파일들을 제공하는 서버를 실행하기 위해서는 다음 명령어를 실행

 

$ npx serve ./build

serve 는 웹서버를 열어서 특정 디렉터리에 있는 파일을 제공해주는 도구

이번에는 아까와 달리 에러가 어디서 발생했는지 상세한 정보를 알아보기 쉽지가 않음

이는 빌드 과정에서 코드가 minify 되면서 이름이 c, Xo, Ui, qa 이런식으로 축소됐기 때문

만약에 코드 위치를 제대로 파악을 하고 싶다면 이 링크 를 참조

Sentry 에서 minified 되지 않은 이름을 보려면 Sourcemap 이란것을 사용해야 하는데요, 빌드를 할 때마다 자동으로 업로드 되도록 설정 할 수 있고, 직접 업로드 할 수도 있고, 만약에 Sourcemap 파일이 공개 되어 있다면 별도의 설정 없이 바로 minified 되지 않은 이름을 볼 수 있음

반응형