블로그
frontend
4遺??뚯슂

반쯤 깨진 UI를 막는 법: Next.js 에러 바운더리 경계 설계

핵심은 이거: 에러 바운더리는 ‘기능 경계’에 둬야 UX가 산다. Next.js에서는 라우트 세그먼트(error.js)와 컴포넌트 경계를 조합해 실패를 국소화한다.

결론: 에러 바운더리는 ‘기능 경계’에 둬야 UX가 산다. 한 군데만 두면 전부 같이 무너진다. 너무 촘촘하면 반쯤 깨진 화면이 남는다. 여기서 갈린다. “실패가 퍼져야 하는 범위”를 먼저 정하고, 그 경계에만 바운더리를 둔다.

배경/문제

현대적인 웹 앱은 수많은 조각이 맞물려 돌아간다. 어느 한 조각이 멈추면 예상 못한 방식으로 UI가 깨진다. 문제는 “에러가 나느냐”가 아니라, 에러가 났을 때 사용자 경험이 어떻게 붕괴하느냐다. 그래서 필요한 게 장애허용성(fault tolerance, 일부 구성 요소가 실패해도 시스템이 계속 동작하도록 하는 특성)이다.

핵심 개념

에러 바운더리

React에서 에러 바운더리는 렌더링 중 발생한 에러를 잡아 폴백 UI로 전환하는 장치다. 구현은 componentDidCatch/getDerivedStateFromError를 가진 클래스 컴포넌트 형태다. React Error Boundaries

포인트: 에러 바운더리는 “에러를 숨기는 도구”가 아니라 “손상된 UI를 남기지 않기 위한 경계”다.

Next.js에서의 경계

Next.js는 라우트 세그먼트 단위로 에러를 흡수하는 error.js(에러 UI)를 제공한다. 이 파일은 해당 세그먼트에서 발생한 렌더링 에러를 잡고, 같은 세그먼트 안에서 복구(재시도 버튼 등)할 수 있는 지점을 만든다. Next.js Error Handling

다이어그램 불러오는 중...

→ 기대 결과/무엇이 달라졌는지: 에러가 나도 “앱 전체”가 아니라 “세그먼트/기능 단위”로만 무너진다. 복구 버튼의 위치도 명확해진다.

해결 접근

핵심은 이거다.

  1. 딱 하나의 바운더리만 두지 않는다
  • 왜: 한 군데에서 터지면 전체가 함께 내려간다.
  • 기대 결과: 실패가 국소화된다.
  1. 모든 컴포넌트를 바운더리로 감싸지 않는다
  • 왜: ‘반쯤 깨진 UI’가 남으면, 사용자 입장에서는 완전히 망가진 UX다.
  • 기대 결과: 기능이 불완전한 상태를 최소화한다.
  1. 기능 경계를 찾아 그 경계에 배치한다
  • 왜: “이 에러가 형제 컴포넌트까지 같이 죽여야 하나?”가 경계를 만든다.
  • 기대 결과: 예측 가능한 실패 범위 + 예측 가능한 폴백 UI.

구현(코드)

1) 기능 단위 에러 바운더리

React 클래스 기반으로 직접 구현해도 되고, 라이브러리(예: react-error-boundary)를 써도 된다. react-error-boundary

typescript
// components/FeatureErrorBoundary.tsx
import React from "react";

type Props = {
  children: React.ReactNode;
  fallback: React.ReactNode;
};

type State = { hasError: boolean };

export class FeatureErrorBoundary extends React.Component<Props, State> {
  state: State = { hasError: false };

  static getDerivedStateFromError() {
    return { hasError: true };
  }

  componentDidCatch(error: unknown) {
    // 여기서 로깅/리포팅 훅을 연결할 수 있다.
    // (정책/환경에 따라 달라질 수 있다)
    console.error(error);
  }

  render() {
    if (this.state.hasError) return this.props.fallback;
    return this.props.children;
  }
}

→ 기대 결과/무엇이 달라졌는지: 특정 기능이 렌더링 에러를 던져도, 그 기능 영역만 폴백으로 전환된다.

2) Next.js에서 ‘기능 경계’로 감싸기

브라우저 전용 API(window, document)를 쓰는 컴포넌트는 Client Component에서만 실행 위치가 명확해진다.

typescript
// app/(home)/page.tsx
import { FeatureErrorBoundary } from "@/components/FeatureErrorBoundary";

function Trends() {
  return <aside>...</aside>;
}

function WhoToFollow() {
  return <section>...</section>;
}

export default function HomePage() {
  return (
    <main>
      <FeatureErrorBoundary fallback={<div>타임라인을 불러오지 못했습니다.</div>}>
        <Timeline />
      </FeatureErrorBoundary>

      <FeatureErrorBoundary fallback={<div>추천을 불러오지 못했습니다.</div>}>
        <WhoToFollow />
      </FeatureErrorBoundary>

      <FeatureErrorBoundary fallback={<div>트렌드를 불러오지 못했습니다.</div>}>
        <Trends />
      </FeatureErrorBoundary>
    </main>
  );
}

→ 기대 결과/무엇이 달라졌는지: 추천 섹션이 터져도 타임라인은 살아 있다. 사용자는 “어디가 문제인지”를 즉시 이해한다.

3) 라우트 세그먼트 경계: error.js

세그먼트 단위 폴백은 “페이지 전체가 아닌, 해당 구역만” 대체한다.

typescript
// app/(home)/error.tsx
"use client";

export default function Error({ reset }: { reset: () => void }) {
  return (
    <div>
      <p>이 섹션에서 문제가 발생했습니다.</p>
      <button onClick={() => reset()}>다시 시도</button>
    </div>
  );
}

→ 기대 결과/무엇이 달라졌는지: “새로고침” 대신, 사용자가 같은 자리에서 재시도할 수 있다.

검증 방법(체크리스트)

특정 기능 컴포넌트에서 throw new Error()를 넣었을 때 그 기능만 폴백으로 전환된다.
폴백 UI가 레이아웃을 깨지지 않고, 기능의 ‘불완전함’을 명확히 전달한다.
error.js가 있는 세그먼트는 에러가 나도 다른 세그먼트로 전파되지 않는다. Next.js Error Handling
브라우저 전용 API는 Client Component에서만 실행된다.

흔한 실수/FAQ

Q. 최상단에 바운더리 하나면 충분하지 않나? 대부분은 아니다. 한 번 터지면 전부 같이 죽는다. 실패가 “퍼질 필요가 없는 영역”까지 같이 내려간다.

Q. 그럼 컴포넌트마다 바운더리를 붙이면 되나? 아니다. 반쯤 깨진 UI가 남으면 사용자는 어디서부터 어떻게 해야 할지 모른다. 기능 단위 경계가 핵심이다.

Q. 에러 바운더리는 모든 에러를 잡나? 렌더링 과정의 에러를 잡는 데 초점이 있다. 이벤트 핸들러/비동기 흐름 등은 별도 처리 전략이 필요할 수 있다. React Error Boundaries

요약

에러 바운더리는 ‘많이’가 아니라 ‘제대로’다. 기능 경계를 먼저 정하면, 에러가 나도 UX가 무너지지 않는다. Next.js에서는 세그먼트 경계(error.js)와 기능 경계(컴포넌트 바운더리)를 조합하면 복구 지점이 선명해진다. 테스트는 단순하다. 의도적으로 망가뜨려 보고, “무엇이 살아남는지” 확인한다.

결론

장애허용성은 옵션이 아니다. 앱이 커질수록 필수다. 바로 적용 포인트는 하나: **“이 에러가 형제까지 같이 죽어야 하나?”**를 계속 묻고, 그 답이 ‘아니오’인 곳에 경계를 세워라.

참고

관련 게시물