콜스택과 이벤트루프를 통해 ErrorBoundary가 비동기 에러를 감지하지 못하는 이유 이해하기

dodog·2024년 9월 1일

얼마전에 왜 ErrorBoundary는 비동기 코드에서 발생하는 에러는 캐치하지 못하는가?
에 대한 의문을 해결하지 못해서 멘토링 수업에서 질문을 하게 되었는데
이벤트 루프와 콜스택을 잘 이해하지 못하면 모를 수 있다는 생각지도 못한 답변을 듣게 되었다.

React에서 발생하는 문제인데, JS에서만 생각해보던 이벤트 루프를 이해해야 한다니
처음엔 무언가 세계관이 꼬인 듯한 어색한 느낌을 받았다.

왜 ErrorBoundary는 비동기 함수의 에러는 잡아주지 못할까?

위 그림은 간단하게 App 컴포넌트를 ErrorBoundary로 감쌌을 때,
이벤트 루프가 어떻게 돌아가는지 간략하게 그림으로 표시한 것이다.

먼저 ErrorBoundary 컴포넌트가 호출되면서 콜스택 최하단에 쌓이고,
Children인 App컴포넌트가 호출되어 ErrorBoundary 위에 또 쌓이게 된다.

그리고 App 컴포넌트 내부에서 api 요청이 발생하면 Promise를 리턴하므로
Promise는 콜스택에 쌓이지 않고 microtask queue로 가게 된다.

그래서 App 컴포넌트는 렌더링을 마치면 콜스택에서 내려가게 되고,
ErrorBoundary도 콜스택에서 내려가서 콜스택이 다 비워진 후에야
Microtask queue에 있던 api 호출 함수가 콜스택에 올라와서 실행되는 것이다.

이 시점에서 만약 api 함수 호출에서 에러가 발생하여 상위로 에러를 전파하더라도
ErrorBoundary는 이미 실행이 완료되어 콜스택에서 제거된 뒤이므로
당연하게도 에러를 잡아서 대신 처리해주지 못하는 것이다.

그럼 ErrorBoundary가 비동기 에러를 처리하게 하는 방법은 없을까?

그럼에도 풀리지 않는 의문은, 분명히 다른 아티클에서는
API 호출에서 발생한 에러를 ErrorBoundary가 처리하도록 하는 것을 봤다는 것이다.

이것도 사실 React와 이벤트 루프를 잘 이해하면 충분히 가능했다.

아래와 같은 방식으로 비동기 컨텍스트를 동기 컨텍스트로 가져오는 방식으로
비동기 에러를 ErrorBoundary가 감지할 수 있게 할 수 있다.

const [error, setError] = useState();

useEffect(() => {
  const fetchTodos = async () => {
    try {
      await getTodos();
    } catch (e) {
      setError(e);
    }
  };

  fetchTodos();
}, []);

if (error) {
  throw error;
}

위 코드가 App 컴포넌트 내부 코드라고 가정하고
App 컴포넌트는 ErrorBoundary에 의해 감싸져있고
App 컴포넌트 내부의 getTodos api 함수에서 에러가 발생했다고 하자.

  1. ErrorBoundary가 콜스택에 쌓인다.
  2. App 컴포넌트가 콜스택에 쌓인다.
  3. App 컴포넌트가 렌더링을 완료하고 콜스택에서 제거된다.
  4. ErrorBoundary도 실행을 마치고 콜스택에서 제거된다.
  5. App 컴포넌트의 useEffect 콜백이 실행되고 getTodos에서 에러가 발생한다.
  6. try catch에 의해 잡힌 에러가 setError에 인자로 전달되어 호출한다.
  7. setError에 의해 상태가 변경되었으므로 App 컴포넌트에 리렌더링이 발생한다.
  8. 자식 컴포넌트에서 발생하는 에러를 감지할 수 있는 ErrorBoundary의 특별한 메서드인
    getDerivedStateFromError()가 호출되어 ErrorBoundary가 업데이트되고 리렌더링이 발생한다.
  9. 다시 ErrorBoundary와 그 자식인 App 컴포넌트가 콜스택에 1,2번처럼 쌓인다.
  10. App 함수가 실행되면서 동기적으로 error를 throw 한다.
  11. 에러가 상위로 전파되어 ErrorBoundary에서 감지된다.

이런 순차적인 단계를 통해서 비동기 컨텍스트에서 발생하는 에러를
동기 컨텍스트로 가져와서 ErrorBoundary가 감지할 수 있게 할 수 있는 것이다.

이걸 이해하기 위해서는 리액트 hook의 자세한 동작에 대한 이해도 필요하면서
동시에 JS 이벤트 루프에 대한 이해도 필요했기 때문에 정말 쉽지 않았던 것 같다.

마무리

이번 계기를 통해 면접을 준비하며 공부하고 제대로 이해했다고 생각했던 이벤트 루프를
사실은 다 이해하지 못했고 심지어 리액트조차도 기본기가 부족했다는걸 느낄 수 있었다.

리액트 컴포넌트도 함수이므로 콜스택에 올라가서 실행된다는 사실을 왜 몰랐을까?
머리로는 React도 결국 Javascript라는걸 암기식으로 이해했지만
실제로는 제대로 이해하고 있지 않았던게 문제 아닐까 하는 생각이 들었다.

앞으로는 React나 Next.js같은 기술에만 국한된 좁은 시야에서 벗어나
결국은 모두 JS로 돌아간다는 더 넓은 개념에서의 시야를 가지도록 노력해야겠다.

profile
심리학, 사회문제해결에 관심이 많은 프론트엔드 개발자

0개의 댓글