React Error Boundaries๋ฅผ ์ฌ์ฉํ๋ฉด try catch์ ๊ฐ์ ๋ฐฉ๋ฒ์ผ๋ก ์๋ฌ๋ฅผ ์ก์ง ์์ต๋๋ค
React ๋ด๋ถ์์ ์๋ฌ๋ฅผ ์ฒ๋ฆฌํ๋ค ํ๋ฉด try catch ๋ฌธ์ ์ด์ฉํด์ ์๋ฌ๋ฅผ ํธ๋ค๋งํ๋ ๋ฐฉ๋ฒ์ ๋ฐ๋ก ๋ ์ฌ๋ฆด ๊ฒ์ ๋๋ค. ์ด๋ ๋ช ๋ น์ ์ผ๋ก ํ๋ก๊ทธ๋๋ฐ์ ์ง๋ ๊ฒ์ธ๋ฐ์.
function riskyFunction() {
if (Math.random() < 0.5) {
throw new Error("Something went wrong!");
}
return "Success!";
}
try {
const result = riskyFunction();
console.log(result);
} catch (error) {
console.error("Caught an error:", error);
}
๋ด๊ฐ ํ๊ณ ์ถ์ ์๋ฌ๋ฅผ ์ก๊ธฐ ์ํด์ ์ด๋ป๊ฒ ์๋ฌ๋ฅผ ์ก์ ๊ฒ์ธ์ง ํ๋ํ๋ ๋ช ๋ นํด์ ์์ฑํ๋ ๊ฒ์ด์ฃ . ํ์ง๋ง React์์๋ ์ ์ธ์ ์ธ ํ๋ก๊ทธ๋๋ฐ์ ์งํฅํฉ๋๋ค. ์ด๋ป๊ฒ ํ ๊ฒ์ธ์ง ์๋ ๋ฌด์์ ํ ๊ฒ ์ธ์ง ๋ง์ด์ฃ .
๊ทธ๋์ React์์๋ ์ ์ธ์ ์ผ๋ก ์๋ฌ์ฒ๋ฆฌ๋ฅผ ๋ ์ํ ์ ์๊ฒ ํ๋ ๋ฐฉ๋ฒ์ด ์กด์ฌํฉ๋๋ค. ๋ฐ๋ก React 16์์ ๋์ ๋ ErrorBoundary์ธ๋ฐ์. ์์์ ์ปดํฌ๋ํธ์์ ์ผ์ด๋ ์๋ฌ๋ฅผ ๊ฐ์งํด์ ์๋ก์ด ์ปดํฌ๋ํธ๋ฅผ ๋ณด์ฌ์ฃผ๋ ์ญํ ์ ํ๋ ์ปดํฌ๋ํธ์ ๋๋ค.
<ErrorBoundary>
<MyComponent />
</ErrorBoundary>
๋๊ฒ ๊ฐ๋จํ์ฃ ? ๋๋ ์๋ฌ๋ฅผ ์ก๊ธฐ ์ํด์ ErrorBoundary๋ฅผ ์ฌ์ฉํ ๊ฑฐ์ผ๋ผ๊ณ ์ ์ธํด ์ฃผ๋ฉด ๋ฉ๋๋ค.
๋จ์ ์ผ๋ก๋ class ์ปดํฌ๋ํธ๋ฅผ ์ฌ์ฉํด์ผ ํฉ๋๋ค.
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError(error) {
// ๋ค์ ๋ ๋๋ง์์ ํด๋ฐฑ UI๊ฐ ๋ณด์ด๋๋ก ์ํ๋ฅผ ์
๋ฐ์ดํธ ํฉ๋๋ค.
return { hasError: true };
}
componentDidCatch(error, errorInfo) {
// ์๋ฌ ๋ฆฌํฌํ
์๋น์ค์ ์๋ฌ๋ฅผ ๊ธฐ๋กํ ์๋ ์์ต๋๋ค.
logErrorToMyService(error, errorInfo);
}
render() {
if (this.state.hasError) {
// ํด๋ฐฑ UI๋ฅผ ์ปค์คํ
ํ์ฌ ๋ ๋๋งํ ์ ์์ต๋๋ค.
return <h1>Something went wrong.</h1>;
}
return this.props.children;
}
}
functional component์ ๋ค๋ฅด๊ฒ class component์์๋ ๋๋๋ง ์ฃผ๊ธฐ๋ฅผ ์ ๋ถ ํํํ ์ ์์ต๋๋ค. React์์ class component์์ functional component๋ก ๋ฐ๊พธ๋ฉด์ ๋ชจ๋ ๋๋๋ง ์ฃผ๊ธฐ์ ๋ํด ๋์ํ์ง ๋ชปํ๋๋ฐ ์ด ErrorBoundary์ ํต์ฌ์ ์ธ ๋๋๋ง ์ฃผ๊ธฐ๊ฐ ๋ฐ๋ก ์ด ์์์ ๋๋ค.
๋์ ์ react-error-boundary๋ฅผ ์ฌ์ฉํด์ ๋ด ํ๋ก์ ํธ ๋ด์ class ์ปดํฌ๋ํธ๋ฅผ ์ฌ์ฉํ์ง ์๊ฒ ํ ์ ์์ต๋๋ค.
๊ฐ๋จํ ์ฌ์ฉ ๋ฐฉ๋ฒ์ ๋ค์๊ณผ ๊ฐ์ต๋๋ค. ์๋ฌ๊ฐ ๋ฐ์ํ ๋ ์ค์ ํ FallbackComponent๋ก ์ ํ์ด ๋ฉ๋๋ค.
function ErrorFallback({ error, resetErrorBoundary }) {
return (
<div role="alert">
<p>Something went wrong:</p>
<pre>{error.message}</pre>
<button onClick={resetErrorBoundary}>Try again</button>
</div>
);
}
function MyComponent() {
return (
<ErrorBoundary
FallbackComponent={ErrorFallback}
// ์๋ฌ๋ฅผ ๊ฐ์ง ํด๋น ์ปดํฌ๋ํธ๋ฅผ ๋ณด์ฌ์ค
onReset={() => {
// reset the state of your app so the error doesn't happen again
}}
>
<ComponentThatMayThrowError />
</ErrorBoundary>
);
}
resetErrorBoundary๋ฅผ ์ด์ฉํด์ ์ปดํฌ๋ํธ์ api ์ฌ ํธ์ถ์ ์ํํ ์ ์์ต๋๋ค
const RetryComponent = ({ error, resetErrorBoundary }: FallbackProps) => {
return (
<div>
๋ค์ ์๋ํด ์ฃผ์ธ์
<button onClick={resetErrorBoundary}>Retry</button>
</div>
);
};
const APILocalErrorBoundary: React.FC<PropsWithChildren> = ({ children }) => {
const { reset } = useQueryErrorResetBoundary();
// react-query์ hook
return (
<ErrorBoundary fallbackRender={RetryComponent} onReset={reset}>
{children}
</ErrorBoundary>
);
};
ํ์ง๋ง ํด๋น react-error-boundary๋ ๋ง๋ฅ์ด ์๋๋ฐ์ ๋ค์๊ณผ ๊ฐ์ ๋ด์ฉ์ ํฌ์ฐฉํ์ง ์์ต๋๋ค.
์ฆ ์ด๊ฑธ ๊ฑฐ๊พธ๋ก ์๊ฐํด ๋ณด๋ฉด React์ ์๋ช ์ฃผ๊ธฐ ์ด์ธ์ ๊ฒ๋ค์ ํฌ์ฐฉํ์ง ๋ชปํ๋ค๋ ๊ฒ์ด์ฃ . ๊ทธ๋ ๊ธฐ ๋๋ฌธ์ JS์์ ์ง์ํ๋ try catch์๋ ์กฐ๊ธ ๋ค๋ฅธ ํน์ง์ ์ง๋ ์ต๋๋ค.
๋ง์ฝ์ ๋ด๊ฐ ๋น๋๊ธฐ ์ฒ๋ฆฌ์ ๋ํ ๋ด์ฉ์ error boundary๋ก ์ก๊ณ ์ถ๋ค๋ฉด React ์๋ช ์ฃผ๊ธฐ์ ์ด๋ฅผ ํฌํจํ๋ ์์ ์ด ํ์ํฉ๋๋ค ๋ผ์ด๋ธ๋ฌ๋ฆฌ ๋จ์์ ์ง์ํ ์๋ ์๊ณ (React-query์ throwOnError ์ต์ ) ์๋๋ฉด ์ง์ error์ ๊ด๋ จ๋ state๋ฅผ ๋ง๋ค์ด์ ๊ฐ์ ๋ก React ์ฌ์ดํด์ ์ง์ด๋ฃ์ ์๋ ์์ต๋๋ค.
errorBoundary๋ฅผ ์์ฑํ๋ ์ค ํ๋์ ์ด์๋ฅผ ๋ง๋ฌ๋๋ฐ์ ์ ๋ ๋ถ๋ช ํ ErrorBoundary๋ก ๊ฐ์ธ์ ์๋ฌ๋ฅผ ์ฒ๋ฆฌํ๊ณ ์์ง๋ง development ํ๊ฒฝ์์ ์์์ ์๋ฌ๋ฅผ ๋ฐ์ ์ ๋ค์๊ณผ ๊ฐ์ด ํธ๋ค๋ง๋์ง ์๋ ์๋ฌ๋ก ์ ์๊ฐ ๋๋ค๋ ์ ์ด์์ต๋๋ค.
์ฌ๊ธฐ์ ์ ๋ ์ค์ ๋ก ์๋ฌ๊ฐ ํธ๋ค๋ง๋์ง ์์์ ํฐ์ง ๋ฌธ์ ๋ผ ํ๋จํ์๊ณ ์ฌ๊ธฐ์ ErrorBoundary๋ ํฌ์ฐฉ๋ง ํ๊ณ ์ค์ ์๋ฌ๋ ํด๊ฒฐํ์ง ๋ชปํ ์ค ์์์ต๋๋ค. ํ์ง๋ง ๋น๋ ํด์ ์คํ์ ํ๋ฉด ๋ค์๊ณผ ๊ฐ์ ์๋ฌ ํฌ์ฐฉ์ ๋์ง ์์ต๋๋ค.
์ด๋ React์ ์๋ฌ ๋๋ฒ๊ฑฐ ๋๋ฌธ์ ์๊ธด ๋ฌธ์ ์๋๋ฐ์. ์ฌ์ฉํ ํ๋ก์ ํธ์์๋ webpack dev server๋ฅผ ์ฌ์ฉํ๊ณ ์์๊ณ webpack ๋ด์ debug tool์ด ์ฌ์ง๊ณผ ๊ฐ์ด ์๋ฌ๋ฅผ ์ถ์ ํด ์ฃผ๋ ์ญํ ํ๊ณ ์์ต๋๋ค. ErrorBoundary๋ฅผ ์ด์ฉํ๋ฉด ์๋น์ค์์๋ ์๋ฌ๋ฅผ ์บ์นํ์ง๋ง debug ํด์ ํด๋น ์๋ฌ๋ฅผ ์บ์นํ๋์ง ๋ชปํ๋์ง ๋ชจ๋ฅด๋ ์ํ๋ผ๊ณ ์ถ์ธกํ ์ ์์ต๋๋ค.
์ด๋ JS์์ ์ฒ๋ฆฌํ๋ try catch์ error boundary๊ฐ ์๋ฌ์ฒ๋ฆฌ ํ๋ ๋ฐฉ๋ฒ์ด ๋๊ฐ์ง ์๊ธฐ ๋๋ฌธ์ ์๊ธฐ๋ ๋ฌธ์ ์ ๋๋ค.
https://github.com/facebook/create-react-app/issues/6530
Why are Error Boundaries not triggered for event handlers? ยท Issue #11409 ยท facebook/react
์ด ๋ต๋ณ์ CRA๋ ํฌ์งํ ๋ฆฌ ๋ด์์ ์ด์ ๋ํ ๋ฌธ์ ์ ๋ต์ ํด ์ฃผ์๋๋ฐ์. ๊ฐ๋จํ ์ด์ผ๊ธฐํ๋ฉด React ๋ฉ์ปค๋์ฆ์์ ErrorBoundary๋ก ์กํ ์ค๋ฅ์ ์กํ์ง ์๋ ์ค๋ฅ๋ฅผ ๊ตฌ๋ณํ๋ ๋ฐฉ๋ฒ์ด ์๋ค๊ณ ํฉ๋๋ค.
๐ก ์ถ๋ก ์ฃผ์) ํด๋น ๋ด์ฉ์ ์ฝ๋๋ฅผ ๋จํธ์ ์ผ๋ก ๋ณธ ์ถ๋ก ์ผ๋ก์จ ์ค์ ๋ด์ฉ๊ณผ ๋ค๋ฅผ ์ ์์ต๋๋ค!! ์ฃผ์ํด์ ๋ด์ฃผ์ธ์
์ ์์๋๋ก ํ๋ฒ React ์ฝ๋ ๋ด๋ถ์ ์๋ฌ๋ฅผ ์ ํํ๋ ์ฝ๋๋ฅผ ๊น๋ณด์์ต๋๋ค.
function throwAndUnwindWorkLoop(unitOfWork: Fiber, thrownValue: mixed) {
// This is a fork of performUnitOfWork specifcally for unwinding a fiber
// that threw an exception.
//
// Return to the normal work loop. This will unwind the stack, and potentially
// result in showing a fallback.
resetSuspendedWorkLoopOnUnwind(unitOfWork);
const returnFiber = unitOfWork.return;
if (returnFiber === null || workInProgressRoot === null) {
// Expected to be working on a non-root fiber. This is a fatal error
// because there's no ancestor that can handle it; the root is
// supposed to capture all errors that weren't caught by an error
// boundary.
workInProgressRootExitStatus = RootFatalErrored;
workInProgressRootFatalError = thrownValue;
// Set `workInProgress` to null. This represents advancing to the next
// sibling, or the parent if there are no siblings. But since the root
// has no siblings nor a parent, we set it to null. Usually this is
// handled by `completeUnitOfWork` or `unwindWork`, but since we're
// intentionally not calling those, we need set it here.
// TODO: Consider calling `unwindWork` to pop the contexts.
workInProgress = null;
return;
}
try {
// Find and mark the nearest Suspense or error boundary that can handle
// this "exception".
throwException(
workInProgressRoot,
returnFiber,
unitOfWork,
thrownValue,
workInProgressRootRenderLanes
);
} catch (error) {
// We had trouble processing the error. An example of this happening is
// when accessing the `componentDidCatch` property of an error boundary
// throws an error. A weird edge case. There's a regression test for this.
// To prevent an infinite loop, bubble the error up to the next parent.
workInProgress = returnFiber;
throw error;
}
if (unitOfWork.flags & Incomplete) {
// Unwind the stack until we reach the nearest boundary.
unwindUnitOfWork(unitOfWork);
} else {
// Although the fiber suspended, we're intentionally going to commit it in
// an inconsistent state. We can do this safely in cases where we know the
// inconsistent tree will be hidden.
//
// This currently only applies to Legacy Suspense implementation, but we may
// port a version of this to concurrent roots, too, when performing a
// synchronous render. Because that will allow us to mutate the tree as we
// go instead of buffering mutations until the end. Though it's unclear if
// this particular path is how that would be implemented.
completeUnitOfWork(unitOfWork);
}
}
ํด๋น ์ฝ๋ ์กฐ๊ฐ์ Fiber์์ ์์ธ๊ฐ ๋ฐ์ํ์๋ ์คํํ๋ ์ฝ๋๋ก ์ถ์ธกํฉ๋๋ค. ์ฌ๊ธฐ์ ๋ด์ผ ํ๋ ๊ฑด throwException์ธ๋ฐ์ ErrorBoundary ์์์์๋ ์ปดํฌ๋ํธ์์ ๋ง์ฝ error๊ฐ ์ผ์ด๋ฌ๋ค๋ฉด ์ด๋ ์ปดํฌ๋ํธ์์ ์ผ์ด๋ ์๋ฌ ์ผ ๊ฒ์ ๋๋ค. ์ด๋ ํ์ฌ ๋ณด์ด๋ Fiber์์ ์์ธ๊ฐ ๋ฐ์ํ๋ค๋ ์ฆ๊ฑฐ๊ณ ๊ทธ์ ๋ํ ๋ด์ฉ์ผ๋ก ํด๋น ์ฝ๋์ throwException์ ํตํด ์์์ ์๋ ErrorBoundary๋ฅผ ์ฐพ์ ๊ฒ์ด์ฃ .
์ฌ๊ธฐ์ ๋ด์ผ ํ ์ ์ด ๋ฐ๋ก throw error๋ก ์ค์ ์๋ฌ๋ฅผ ๋์ง๋ ๊ฒ ์๋ JS ์ฝ๋๋ก ์๋ฌ์ ๋ํ ์ ๋ณด๋ฅผ ๋ณ์๋ก ์ง์ ํด ์ฃผ๊ณ ์๋ค๋ ์ ์ ๋๋ค.
throwException์์ ์ฝ๋์ ์ผ๋ถ๋ถ์ ๋ณด๊ฒ ์ต๋๋ค.
do {
switch (workInProgress.tag) {
case HostRoot: {
const errorInfo = value;
workInProgress.flags |= ShouldCapture;
const lane = pickArbitraryLane(rootRenderLanes);
workInProgress.lanes = mergeLanes(workInProgress.lanes, lane);
const update = createRootErrorUpdate(workInProgress, errorInfo, lane);
enqueueCapturedUpdate(workInProgress, update);
return;
}
case ClassComponent:
// Capture and retry
const errorInfo = value;
const ctor = workInProgress.type;
const instance = workInProgress.stateNode;
if (
(workInProgress.flags & DidCapture) === NoFlags &&
(typeof ctor.getDerivedStateFromError === "function" ||
(instance !== null &&
typeof instance.componentDidCatch === "function" &&
!isAlreadyFailedLegacyErrorBoundary(instance)))
) {
workInProgress.flags |= ShouldCapture;
const lane = pickArbitraryLane(rootRenderLanes);
workInProgress.lanes = mergeLanes(workInProgress.lanes, lane);
// Schedule the error boundary to re-render using updated state
const update = createClassErrorUpdate(workInProgress, errorInfo, lane);
enqueueCapturedUpdate(workInProgress, update);
return;
}
break;
default:
break;
}
// $FlowFixMe[incompatible-type] we bail out when we get a null
workInProgress = workInProgress.return;
} while (workInProgress !== null);
์๊น ErrorBoundary๋ class component๋ผ๊ณ ํ์ต๋๋ค. ๊ทธ๋ ๊ธฐ ๋๋ฌธ์ ํด๋น case๋ก ๋ค์ด๊ฐ ๊ฒ์ผ๋ก ์ถ์ธก์ด ๋๊ณ error๋ฅผ ๋ณ์๋ก ํ ๋นํด์ ๋ญ์ง๋ ๋ชจ๋ฅด๊ฒ ์ง๋ง enqueue์ ํด๋น ์๋ฌ๋ฅผ ์ ๋ฐ์ดํธ์ํค๋ ๊ฒ์ ๋ณผ ์ ์์ต๋๋ค. ๋ฐ๋ผ์ React์์๋ ์๋ฌ๋ฅผ JS ๋ณ์๋ก ๊ด๋ฆฌํ๊ธฐ ๋๋ฌธ์ ์ด๋ค ๊ฒ์ด ํธ๋ค๋ง ๋ ์๋ฌ์ธ์ง ์๋ ์๋ฌ์ธ์ง ์ ์ ์๋ค๊ณ ์ถ์ธกํ ์ ์์ต๋๋ค.
ErrorBoundary๋ฅผ ์ด์ฉํ๋ฉด ์ ์ธ์ ์ผ๋ก ์๋ฌ ํธ๋ค๋ง์ ํ ์ ์์ต๋๋ค. ํ์ง๋ง ErrorBoundary๋ try/catch์ ๋ค๋ฅด๊ธฐ ๋๋ฌธ์ ์ฃผ์ํด์ผ ํฉ๋๋ค. ์๋ฌํธ๋ค๋ง์ ๋ฌด์กฐ๊ฑด Errorboundary ์ฌ์ฉ์ด ์๋ ์ํฉ์ ๋ง๊ฒ ์ฌ์ฉํด์ผ๊ฒ ์ฃ . ์ ํ๋ก์ ํธ ๊ฐ์ ๊ฒฝ์ฐ์๋ ErrorBoundary๋ ์ด์ฉ ์ ์์ด ์๋ฌ ํ์ด์ง๋ฅผ ๋ณด์ฌ์ฃผ์ด์ผ ํ ๋ ์ฌ์ฉํ๊ณ ์๊ณ ๊ทธ ์ด์ธ์๋ axios๋ ๋ค๋ฅธ ๊ณ์ธต์ผ๋ก ๋ถ๋ฆฌํด์ ์ฌ์ฉ ์ค์ ๋๋ค. ๋ค์ ํฌ์คํ ์์ ํ๋ฒ ์์ฑํด ๋ณด๊ฒ ์ต๋๋ค.