ComponentsSuspenseBoundary & ErrorBoundary

SuspenseBoundary & ErrorBoundary

๋กœ๋”ฉ(Suspense)๊ณผ ์—๋Ÿฌ(ErrorBoundary)๋ฅผ ํ•˜๋‚˜์˜ ์„ ์–ธ์  ์ปดํฌ๋„ŒํŠธ๋กœ ๋ฌถ์–ด์ฃผ๋Š” ์œ ํ‹ธ๋ฆฌํ‹ฐ์ž…๋‹ˆ๋‹ค.

์–ธ์ œ ์‚ฌ์šฉํ•˜๋‚˜์š”?

  • ๐Ÿ”„ ๋น„๋™๊ธฐ ์ปดํฌ๋„ŒํŠธ ๋กœ๋”ฉ/์—๋Ÿฌ ์ฒ˜๋ฆฌ
  • ๐Ÿ” ์žฌ์‹œ๋„ ๋กœ์ง ๊ตฌํ˜„
  • ๐Ÿ“Š ์—๋Ÿฌ ์ถ”์  ๋ฐ ๋กœ๊น…
  • ๐ŸŽฏ ์—๋Ÿฌ ๊ฒฉ๋ฆฌ (๋ถ€๋ถ„ ์‹คํŒจ ํ—ˆ์šฉ)

๊ธฐ๋ณธ ์‚ฌ์šฉ๋ฒ•

SuspenseBoundary

import { SuspenseBoundary } from '@frontend-toolkit-js/components';
 
<SuspenseBoundary
  pendingFallback={<Spinner />}
  errorFallback={<ErrorMessage />}
>
  <UserProfile userId={userId} />
</SuspenseBoundary>;

ErrorBoundary (๋‹จ๋…)

import { ErrorBoundary } from '@frontend-toolkit-js/components';
 
<ErrorBoundary fallback={<ErrorMessage />}>
  <NonSuspenseWidget />
</ErrorBoundary>;

์ฃผ์š” ์‚ฌ์šฉ ์‚ฌ๋ก€

1. ํ•จ์ˆ˜ํ˜• ์—๋Ÿฌ Fallback (์žฌ์‹œ๋„)

<SuspenseBoundary
  pendingFallback={<Loading />}
  errorFallback={(error, reset) => (
    <div>
      <p>{error.message}</p>
      <button onClick={reset}>๋‹ค์‹œ ์‹œ๋„</button>
    </div>
  )}
>
  <UserProfile userId={userId} />
</SuspenseBoundary>

2. resetKeys๋กœ ์ž๋™ ์ƒํƒœ ์ดˆ๊ธฐํ™”

<SuspenseBoundary
  pendingFallback={<Loading />}
  errorFallback={<ErrorMessage />}
  resetKeys={[userId]}
>
  <UserProfile userId={userId} />
</SuspenseBoundary>

userId๊ฐ€ ๋ฐ”๋€Œ๋ฉด ์—๋Ÿฌ ์ƒํƒœ๊ฐ€ ์ž๋™์œผ๋กœ ๋ฆฌ์…‹๋ฉ๋‹ˆ๋‹ค.

3. onError / onReset ํ›…

<SuspenseBoundary
  pendingFallback={<Loading />}
  errorFallback={<ErrorMessage />}
  onError={(error, info) => {
    console.error('Error:', error);
    Sentry.captureException(error, { contexts: { react: info } });
  }}
  onReset={() => {
    queryClient.invalidateQueries(['user', userId]);
  }}
>
  <UserProfile userId={userId} />
</SuspenseBoundary>

4. ์ค‘์ฒฉ ์‚ฌ์šฉ (์—๋Ÿฌ ๊ฒฉ๋ฆฌ)

<SuspenseBoundary pendingFallback={<Spinner />} errorFallback={<ErrorUI />}>
  {/* ๋ฉ”์ธ ์ฝ˜ํ…์ธ  */}
  <MainContent />
 
  {/* ์œ„์ ฏ ์—๋Ÿฌ๊ฐ€ ์ „์ฒด๋ฅผ ๋ง๊ฐ€๋œจ๋ฆฌ์ง€ ์•Š์Œ */}
  <ErrorBoundary fallback={<WidgetError />}>
    <AsyncWidget />
  </ErrorBoundary>
</SuspenseBoundary>

API Reference

SuspenseBoundary

interface SuspenseBoundaryProps {
  children: ReactNode;
  pendingFallback: ReactNode;
  errorFallback: ReactNode | ((error: Error, reset: () => void) => ReactNode);
  onError?: (error: Error, errorInfo: ErrorInfo) => void;
  onReset?: () => void;
  resetKeys?: unknown[];
}

ErrorBoundary

interface ErrorBoundaryProps {
  children: ReactNode;
  fallback: ReactNode | ((error: Error, reset: () => void) => ReactNode);
  onError?: (error: Error, errorInfo: ErrorInfo) => void;
  onReset?: () => void;
  resetKeys?: unknown[];
}

Props ์ƒ์„ธ

pendingFallback (SuspenseBoundary๋งŒ)

Suspense ๋Œ€๊ธฐ ์ƒํƒœ UI์ž…๋‹ˆ๋‹ค.

<SuspenseBoundary pendingFallback={<Spinner />}>
  • ํƒ€์ž…: ReactNode
  • ํ•„์ˆ˜: โœ…

errorFallback / fallback

์—๋Ÿฌ ํ‘œ์‹œ UI ๋˜๋Š” ํ•จ์ˆ˜ํ˜• UI์ž…๋‹ˆ๋‹ค.

// ReactNode
<SuspenseBoundary errorFallback={<ErrorMessage />}>
 
// ํ•จ์ˆ˜ํ˜• (์—๋Ÿฌ + reset)
<SuspenseBoundary
  errorFallback={(error, reset) => (
    <div>
      <p>{error.message}</p>
      <button onClick={reset}>์žฌ์‹œ๋„</button>
    </div>
  )}
>
  • ํƒ€์ž…: ReactNode | (error: Error, reset: () => void) => ReactNode
  • ํ•„์ˆ˜: โœ…

resetKeys

๊ฐ’์ด ๋ณ€๊ฒฝ๋˜๋ฉด ์ž๋™์œผ๋กœ ์—๋Ÿฌ ์ƒํƒœ๋ฅผ ์ดˆ๊ธฐํ™”ํ•ฉ๋‹ˆ๋‹ค.

<SuspenseBoundary
  errorFallback={<Error />}
  resetKeys={[userId, postId]}
>
  • ํƒ€์ž…: unknown[]
  • ๊ธฐ๋ณธ๊ฐ’: undefined

onError

์—๋Ÿฌ ๋ฐœ์ƒ ์‹œ ์ฝœ๋ฐฑ์ž…๋‹ˆ๋‹ค.

<SuspenseBoundary
  onError={(error, info) => {
    console.error('Error:', error);
    console.error('Component Stack:', info.componentStack);
  }}
>
  • ํƒ€์ž…: (error: Error, errorInfo: ErrorInfo) => void
  • ๊ธฐ๋ณธ๊ฐ’: undefined

onReset

์—๋Ÿฌ ์ƒํƒœ๊ฐ€ ๋ฆฌ์…‹๋  ๋•Œ ํ˜ธ์ถœ๋ฉ๋‹ˆ๋‹ค.

<SuspenseBoundary
  onReset={() => {
    queryClient.invalidateQueries(['user']);
  }}
>
  • ํƒ€์ž…: () => void
  • ๊ธฐ๋ณธ๊ฐ’: undefined

ํŒจํ„ด ๊ฐ€์ด๋“œ

โœ… ๊ถŒ์žฅ

// resetKeys๋กœ ์‚ฌ์šฉ์ž ์ „ํ™˜ ์‹œ ์ž๋™ ์ดˆ๊ธฐํ™”
<SuspenseBoundary resetKeys={[userId]} />
 
// ํ•จ์ˆ˜ํ˜• fallback์—์„œ ์บ์‹œ ์‚ญ์ œ ํ›„ reset
<SuspenseBoundary
  errorFallback={(error, reset) => {
    removeCache(error);
    return <RetryButton onClick={reset} />;
  }}
/>

โŒ ์ง€์–‘

// ๊ธด ๋กœ๋”ฉ UI๋ฅผ ์ง์ ‘ children์—์„œ if/else๋กœ ๋ถ„๊ธฐ
{
  loading ? <Spinner /> : error ? <Error /> : <Content />;
}
// โ†’ SuspenseBoundary๋กœ ๊ฐ์‹ธ์„œ ์„ ์–ธ์ ์œผ๋กœ ํ‘œํ˜„

๋‚ด๋ถ€ ๋™์ž‘

React Error Boundary

ErrorBoundary๋Š” React์˜ Error Boundary๋ฅผ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.

class ErrorBoundaryImpl extends Component {
  static getDerivedStateFromError(error: Error) {
    return { error };
  }
 
  componentDidCatch(error: Error, errorInfo: ErrorInfo) {
    this.props.onError?.(error, errorInfo);
  }
 
  render() {
    if (this.state.error) {
      return this.props.fallback;
    }
    return this.props.children;
  }
}

resetKeys ๋™์ž‘

componentDidUpdate(prevProps) {
  // resetKeys๊ฐ€ ๋ณ€๊ฒฝ๋˜๋ฉด ์—๋Ÿฌ ๋ฆฌ์…‹
  if (hasResetKeysChanged(prevProps.resetKeys, this.props.resetKeys)) {
    this.reset();
  }
}

์„ฑ๋Šฅ ํŠน์„ฑ

๋ฒˆ๋“ค ํฌ๊ธฐ

  • ~1.5 KB (minified)
  • ~0.6 KB (gzipped)

๋ฉ”๋ชจ๋ฆฌ

  • ์—๋Ÿฌ ์ƒํƒœ 1๊ฐœ๋งŒ ์œ ์ง€
  • ํด๋ฆฐ์—… ๋ถˆํ•„์š”

๋””๋ฒ„๊น… ํŒ

  • ๊ฐœ๋ฐœ ๋ชจ๋“œ์—์„œ ์—๋Ÿฌ ์Šคํƒ ์ž๋™ ์ถœ๋ ฅ
  • resetKeys ๋ฐฐ์—ด ์ˆœ์„œ ๋ณ€๊ฒฝ ์‹œ ๋ฆฌ์…‹๋จ
  • Suspense ๋ฆฌ์†Œ์Šค์™€ ํ•จ๊ป˜ ์‚ฌ์šฉ ๊ถŒ์žฅ

TypeScript

<SuspenseBoundary
  errorFallback={(error, reset) => {
    error.message;
    //    ^? string
    reset();
    // ^? () => void
  }}
>
  <Component />
</SuspenseBoundary>

๊ด€๋ จ ๋ฌธ์„œ


์ฐธ๊ณ