Error Boundary(feat. React)
๐ฆ ์ก๋์ฌ๋
ํ๋์ ํค์๋๋ฅผ ์ก๊ณ ์ข ํธํ๊ฒ ์ ๋ฆฌํ๊ณ ์ถ์ด ๋ง๋ ์ก๋์ฌ๋
์ก๋์ฌ๋๋ ์กฐ์ ํ๊ธฐ ํ์
์์ ๋ณต
์ด ํธ์ฐฌํ์ก๋์ฐ์ด(้ๅๆฃ็ฐ)
์์ ์ ๋๋ ๋ง์ด๋ค.
์ก๋์ฐ์ด๋์ก๊ธฐ(้่จ)
์ ํํ๋ฅผ ๋น๋ ค์จ ์ฑ ์ผ๋ก ๊ตฌ์ฒด์ ์ธ ์ฒด๊ณ๊ฐ ์กํ์์ง ์์ ํ์์ด๋ค.
ํญ๋ชฉ์ด ๋ค์ ๋์กํ๊ณ ๋ด์ฉ์ ๊ตฌ๋ถ์ด ํผ๋๋์ด์๋ค๊ณ ํ๋ค. ๐คฃ
๐๏ธ Error Boundary
Error Boundary๋ ์๋ฌ์ ๋ํ ๊ฒฝ๊ณ
๋ฅผ ์๋ฏธํ๋ค. ์ฆ, ํน์ Error Boundary๋ก ๊ฐ์ธ์ฌ์ง ๊ตฌ๊ฐ์์ ์๋ฌ๊ฐ ๋ฐ์ํ๋ฉด ๊ทธ ์๋ฌ๋ฅผ ์ก์๋ด์ ์ฒ๋ฆฌํ ์ ์๋ค๋ ๊ฒ์ด๋ค. ์ด์ ๋ํ ๊ฐ๋
์ ์ ์ฉํ Error Boundary ์ปดํฌ๋ํธ๋ฅผ ๋ฆฌ์กํธ์์๋ ๊ณต์๋ฌธ์๋ฅผ ํตํด ์ ๊ณตํ๊ณ ์๋ค. ๋จผ์ ๋ฆฌ์กํธ ๊ณต์๋ฌธ์์์ ์ค๋ช
ํ๋ Error Boundary๋ฅผ ์ดํด๋ณด์.
์๋๋ ๋ฆฌ์กํธ ๊ณต์๋ฌธ์๋ฅผ ๋ฒ์ญํ์ฌ ์ ๋ฆฌํ ๋ด์ฉ์ด๋ค.
๋ฆฌ์กํธ๋ ๊ธฐ๋ณธ์ ์ผ๋ก ์ดํ๋ฆฌ์ผ์ด์
์์ ๋ ๋๋ง ์ค์
์๋ฌ๋ฅผ ๋์ง๋ค๋ฉด ํ๋ฉด์ผ๋ก๋ถํฐ UI๋ฅผ ์ง์ธ ๊ฒ์ด๋ค. ์ด๊ฒ์ ๋ฐฉ์งํ๊ธฐ ์ํด์๋ ์ปดํฌ๋ํธ๋ค์ Error Boundary๋ก ๊ฐ์ธ์ผํ๋ค. Error Boundary๋ ํน๋ณํ ์ปดํฌ๋ํธ์ด๋ค. ์ด ์ปดํฌ๋ํธ๋ ์๋ฌ๋ก ์ธํ ์ถฉ๋์ผ ๋ฐ์ํ์ ๋, ์๋ฌ๋ฅผ ์ก์๋ด์ fallback UI๋ฅผ ๋ณด์ฌ์ค ์ ์๋ค.
Error Boundary๋ ํด๋์ค ์ปดํฌ๋ํธ๋ก ๊ตฌํ๋๋ค. ์ด ๋, static getDerivedStateFromError()
๋ฉ์๋๋ฅผ ์ ๊ณตํด์ผ ํ๋ค. ์ด ๋ฉ์๋๋ฅผ ํตํด ์๋ฌ๊ฐ ๋ฐ์ํ์ ๋, ์๋ฌ์ ๋ํ ์๋ต์ผ๋ก state๋ฅผ ์
๋ฐ์ดํธํ ์ ์๋ค. ๋ํ ์ ํ์ ์ผ๋ก componentDidCatch()
๋ฅผ ํตํด์ ์๋ฌ๊ฐ ๋ฐ์ํ์ ๋, ์๋ฌ๋ฅผ ๊ธฐ๋กํ๊ฑฐ๋ ์๋ฌ ๋ฆฌํฌํ
์๋น์ค์ ์๋ฌ๋ฅผ ๊ธฐ๋กํ ์ ์๋ค.(์๋ฌ์ ๊ดํ ์ด๋ค ํ์๋ฅผ ํ ์ ์๋ค.)
์๋๋ Error Boundary๋ฅผ ๊ตฌํํ ์์์ด๋ค.
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError(error) {
// Update state so the next render will show the fallback UI.
return { hasError: true };
}
componentDidCatch(error, info) {
// optional
// Example "componentStack":
// in ComponentThatThrows (created by App)
// in ErrorBoundary (created by App)
// in div (created by App)
// in App
// ex) logErrorToMyService(error, info.componentStack);
}
render() {
if (this.state.hasError) {
// You can render any custom fallback UI
return this.props.fallback;
}
return this.props.children;
}
}
ํ์ฌ๋ ํจ์ ์ปดํฌ๋ํธ๋ก Error Boundary๋ฅผ ๊ตฌํํ ์ ์๋ค. ๋ง์ฝ ๋งค๋ฒ ์ง์ ์์ class component๋ฅผ ์์ฑํ๊ณ ์ถ์ง ์๋ค๋ฉด react-error-boundary ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ์ฌ์ฉํ๋ฉด ๋๋ค.
๐๏ธ Error Boundary ์ ์ฉํด๋ณด๊ธฐ
Error Boundary๋ฅผ ์ ์ฉํด๋ณด๊ธฐ ์ํด ์์ฃผ์์ฃผ ๊ฐ๋จํ ์์ ๋ฅผ ๋ง๋ค์ด๋ณด์๋ค.
App.jsx
import { ErrorBoundary } from './ErrorBoundary';
import { Temp } from './Temp';
export default function App() {
return (
<div className="App">
<ErrorBoundary>
<h1>Hello CodeSandbox</h1>
<h2>Start editing to see some magic happen!</h2>
<Temp />
</ErrorBoundary>
</div>
);
}
ErrorBoundary.jsx
import React from 'react';
export class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError(error) {
return { hasError: true };
}
componentDidCatch(error, info) {
console.log('error: ', error);
console.log('info: ', info);
}
render() {
if (this.state.hasError) {
return <div>์๋ฌ์
๋๋ค!!!</div>;
}
return this.props.children;
}
}
Temp.jsx
export const Temp = () => {
throw new Error('๊ณ ์์ ์ธ ์๋ฌ ๋ฐ์!');
return <div>Temp Component์
๋๋ค.</div>;
};
์์ ์ฝ๋๋ฅผ ์คํํ๋ฉด ์๋์ ๊ฐ์ ํ๋ฉด์ ๋ณผ ์ ์๋ค.
๊ทธ๋ฆฌ๊ณ ์ด ์๋ฌ iframe์ ๋ซ์ผ๋ฉด ์๋์ ๊ฐ์ด fallback UI๊ฐ ๋ ๋๋ง๋ ๊ฒ์ ๋ณผ ์ ์๋ค.
์์ ๊ฐ์ด ์ฐ๋ฆฌ๊ฐ ์ํ๋ ์ปดํฌ๋ํธ๊ฐ ๋ ๋๋ง๋ ๊ฒ์ ํ์ธํ ์ ์๋ค. ๊ทธ๋ฐ๋ฐ client ๋จ์ ์๋ฌ๊ฐ ์๋๋ผ ์๋ฒ์์ ํต์ ์์ ๋ฐ์ํ๋ ์๋ฌ๋ผ๋ฉด ์ด๋ป๊ฒ ํด์ผํ ๊น?
๐๏ธ Error Boundary์ ํ๊ณ
์์ ์์ ์์ Temp ์ปดํฌ๋ํธ์์ ์๋์ ์ผ๋ก ๋น๋๊ธฐ ํต์ ์ ๋ํ ์๋ฌ๋ฅผ ๋ฐ์์์ผ๋ณด์.
Temp.jsx
import { useEffect } from 'react';
export const Temp = () => {
useEffect(() => {
async function temp() {
throw new Error('๊ณ ์์ ์ธ ์๋ฌ ๋ฐ์!');
}
temp();
}, []);
return <div>Temp Component์
๋๋ค.</div>;
};
๊ทธ๋ผ ๋ง์ฐฌ๊ฐ์ง๋ก ์๋ฌ iframe์ด ๋ณด์ธ๋ค. ๊ทธ๋ฐ๋ฐ ์ด๋ฅผ ๋ซ์ผ๋ฉด ์๋์ ๊ฐ์ด ๊ทธ๋ฅ ์ ์์ ์ธ ํ๋ฉด์ด ๋ ๋๋ง๋๋ ๊ฒ์ ๋ณผ ์ ์๋ค.
๋ถ๋ช ํ ์๋ฌ๋ ๋ฐ์ํ๋ค. ๊ทธ๋ฐ๋ฐ ์ ์ฐ๋ฆฌ๊ฐ ์ ๋ฌํ fallback UI๊ฐ ์๋๋ผ ์ ์์ ์ธ ํ๋ฉด์ด ๋ ๋๋ง๋์ด์๋ ๊ฒ์ผ๊น???
2๊ฐ์ง๋ฅผ ์๊ฐํด๋ณผ ์ ์๋ค.
-
Error Boundary์ ์ค๋ช ์ ๋ค์ ์ดํด๋ณด์. Error Boundary๋
๋ ๋๋ง ์ค์
์๋ฌ๋ฅผ ์ก์๋ธ๋ค๊ณ ํ๋ค. ๊ทธ๋ฐ๋ฐ ์ฐ๋ฆฌ๊ฐ ๋ฐ์์ํจ ์๋ฌ๋๋ ๋๋ง ์ค์ด ์๋
๋น๋๊ธฐ ํต์ ์ค์ ๋ฐ์ํ ์๋ฌ์ด๋ค. ๊ทธ๋ ๊ธฐ ๋๋ฌธ์ Error Boundary๊ฐ ์ก์๋ผ ์ ์๋ ๊ฒ์ด๋ค. -
useEffect๋ ๋น๋๊ธฐ ํต์ ์ ์ํ ํ ์ด๋ผ๊ธฐ๋ณด๋จ, ๋ ๋๋ง์ด ๋๋ ํ์ ์คํ๋๋ ํ ์ด๋ค. ๊ทธ๋ ๊ธฐ ๋๋ฌธ์ useEffect ๋ด๋ถ์์ ๋ฐ์ํ ์๋ฌ๋ Error Boundary๊ฐ ์ก์๋ผ ์ ์๋ ๊ฒ์ด๋ค.(๋ ๋๋ง ์ค์ด ์๋๊ธฐ ๋๋ฌธ์)
๊ทธ๋ ๋ค๋ฉด ์ด๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด์๋ ์ด๋ป๊ฒ ํด์ผํ ๊น? ๊ฐ๋จํ๊ฒ ๋๋ ์๊ฐ์ ์ด๋ค ์ํ๋ฅผ ๋๊ณ ๋น๋๊ธฐ ํต์ ์ด ์คํจํ์ ๋, ๊ทธ ์ํ๋ฅผ ๋ณ๊ฒฝํ๋ ๋ฐฉ๋ฒ์ด๋ค. ๊ทธ๋ฆฌ๊ณ ๊ทธ ์ํ๋ฅผ Error Boundary์์ ๊ฐ์งํ์ฌ fallback UI๋ฅผ ๋ ๋๋งํ๋ฉด ๋ ๊ฒ์ด๋ค.
์๋์ ๊ฐ์ด ์ฝ๋๋ฅผ ์์ ํด๋ณผ ์ ์๋ค.
Temp.jsx
import { useEffect, useState } from 'react';
export const Temp = () => {
const [isError, setIsError] = useState(false);
useEffect(() => {
async function temp() {
try {
throw new Error('๊ณ ์์ ์ธ ์๋ฌ ๋ฐ์!');
} catch (error) {
setIsError(true);
}
}
temp();
}, []);
if (isError) throw new Error('์ฌ๊ธฐ๋ ๊ณ ์์ ์ธ ์๋ฌ ๋ฐ์!');
return <div>Temp Component์
๋๋ค.</div>;
};
์์ ๊ฐ์ด ์์ ํ๋ฉด Error Boundary๋ฅผ ํตํด fallback UI๊ฐ ์ ๋ ๋๋ง๋๋ ๊ฒ์ ๋ณผ ์ ์๋ค. ์ฌ๊ธฐ๊น์ง๋ง ๋ณด๋ฉด ๋ญ๊ฐ ๊ทธ๋ด์ธํ์ง๋ง, ์ด๋์ ๋ ๋ฆฌ์กํธ๋ฅผ ๊ฐ๋ฐํด๋ดค๋ค๋ฉด ๋ญ๊ฐ ์ด์ํ๋ค..?
์ถ์ ๊ฒ์ด๋ค. ์์ ๋ก์ง์ ์์ธํ ์ดํด๋ณด๋ฉด try, catch ๊ตฌ๋ฌธ์ ํตํด์ ์๋ฌ๋ฅผ ์ ์ก์๋๊ณ ๋ค์ throw๋ฅผ ํตํด ์๋ฌ๋ฅผ ๋ฐ์์ํค๊ณ ์๋ค. ์ด ํ๋ฆ์ด ์ด๋๊ฐ ์ด์ํ๊ณ ๋ถํ์ํ๋ค๊ณ ๋๊ปด์ง์ง์๋๊ฐ? ํด์ ๋ฆฌ์กํธ์ ๊ตฌ๋ฒ์ ๊ณต์๋ฌธ์๋ฅผ ์ดํด๋ณด์๋ค.
๐๏ธ Error Boundary์ ํ๊ณ(feat. ๋ฆฌ์กํธ ๊ตฌ๋ฒ์ )
๋ฆฌ์กํธ์ ๊ตฌ๋ฒ์ ๊ณต์๋ฌธ์๋ฅผ ์ดํด๋ณด๋ฉด ์๋์ ๊ฐ์ด ๋์์๋ค.
๊ณผ๊ฑฐ์๋ ์ปดํฌ๋ํธ ๋ด๋ถ์ ์๋ฐ์คํฌ๋ฆฝํธ ๋ก์ง ์๋ฌ๊ฐ React์ ๋ด๋ถ ์ํ๋ฅผ ํผ์ํ๊ณ ์ฑ์ ๋ถ์์ ํ๊ฒ ๋ง๋ค์๋ค. ์ด๋ฐ ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด React 16๋ถํฐ๋ ์ปดํฌ๋ํธ ๋ด๋ถ์ ์๋ฐ์คํฌ๋ฆฝํธ ๋ก์ง ์๋ฌ๊ฐ React์ ๋ด๋ถ ์ํ๋ฅผ ํผ์ํ์ง ์๋๋ก ์๋ฌ ๊ฒฝ๊ณ(Error Boundary)๋ฅผ ๋์ ํ๋ค.(๋ํ React 16๋ถํฐ๋ ๋ ๋๋ง ์ค ์๋ฌ๊ฐ ๋ฐ์ํ๋ฉด ์ ์ฒด ์ปดํฌ๋ํธ ํธ๋ฆฌ์ ๋ง์ดํธ๊ฐ ํด์ ๋๋ค.)
Error Boundary๋ ์๋์ ์๋ฌ๋ ํฌ์ฐฉํ์ง ์๋๋ค.
- ์ด๋ฒคํธ ํธ๋ค๋ฌ(์๋ฅผ ๋ค์ด,
onClick
์์ฑ์ผ๋ก ์ ๋ฌ๋ ํจ์) - ๋น๋๊ธฐ ์ฝ๋(์๋ฅผ ๋ค์ด,
setTimeout
๋๋requestAnimationFrame
์ฝ๋ฐฑ ํจ์, ์๋ฒ ํต์ ๋ผ์ด๋ธ๋ฌ๋ฆฌ์ ์ฝ๋ฐฑ ํจ์) - ์๋ฒ ์ฌ์ด๋ ๋ ๋๋ง
- ์์ ์ปดํฌ๋ํธ๊ฐ ์๋ Error Boundary ์์ฒด์์ ๋ฐ์ํ๋ ์๋ฌ
์ฆ, Error Boundary๋ ๊ทธ์ ์ปดํฌ๋ํธ ํธ๋ฆฌ๊ฐ ๋ ๋๋ง๊ณผ ๊ด๋ จ๋ ์๋ฌ๋ฅผ ํฌ์ฐฉํ๊ณ ์ฒ๋ฆฌํ๊ธฐ ์ํด ์ฌ์ฉ๋๋ค. ์ด ๋ณธ์ฐ์ ์ทจ์ง์ ๋ง๊ฒ ๋ฐ๋ก ์์ Temp.jsx๋ฅผ ์์ ํ๋ฉด ์๋์ ๊ฐ๋ค.
Temp.jsx
import { useEffect, useState } from 'react';
export const Temp = () => {
const [isError, setIsError] = useState(false);
useEffect(() => {
async function temp() {
try {
throw new Error('๊ณ ์์ ์ธ ์๋ฌ ๋ฐ์!');
} catch (error) {
setIsError(true);
}
}
temp();
}, []);
if (isError) return <div>์๋ฌ์
๋๋ค!!!</div>;
return <div>Temp Component์
๋๋ค.</div>;
};
์์ ์ฝ๋๊ฐ ์ผ๋ฐ์ ์ธ fetch, axios๋ฅผ ํตํ ์ปค์คํ ํ ์ isError๋ฅผ ํตํ ์๋ฌ ์ฒ๋ฆฌ ๋ฐฉ๋ฒ์ด๊ณ tanstack/react-query์ useQuery๋ฅผ ํตํ ์๋ฌ ์ฒ๋ฆฌ ๋ฐฉ๋ฒ์ด๋ค. ์ด ๋ฐฉ๋ฒ์ด Error Boundary๋ฅผ ์ฌ์ฉํ๋ ๊ฒ๋ณด๋ค ๋ ๊น๋ํ๊ณ , ๋์ฑ ๋ ํจ์จ์ ์ด๋ค.
๐๏ธ (์ถ๊ฐ) react-router์์์ route ์๋ฌ ์ฒ๋ฆฌ
react-router์์๋ errorElement๋ฅผ ์ฌ์ฉํ์ฌ ํน์ ๊ฒฝ๋ก์์ ์๋ฌ๊ฐ ๋ฐ์ํ์ ๋, ํน์ ์ปดํฌ๋ํธ๋ฅผ ๋ ๋๋งํ ์ ์๋ค. react-router-dom์ ๊ณต์๋ฌธ์์ github ์ฝ๋๋ฅผ ์ดํด๋ณด๋ฉด ์๋์ ๊ฐ์ด ์ฌ์ฉํ ์ ์๋ค.
let router = createBrowserRouter([
{
path: '/',
element: <Layout />,
children: [
{
path: '',
element: <Outlet />,
errorElement: <RootErrorBoundary />, // RootErrorBoundary ์ปดํฌ๋ํธ๋ฅผ ํตํด ์๋ฌ๋ฅผ ์ก์๋ธ๋ค.
children: [
{
path: 'projects/:projectId',
element: <Project />,
errorElement: <ProjectErrorBoundary />, // ProjectErrorBoundary ์ปดํฌ๋ํธ๋ฅผ ํตํด ์๋ฌ๋ฅผ ์ก์๋ธ๋ค.
loader: projectLoader,
},
],
},
],
},
]);
export default function App() {
return <RouterProvider router={router} fallbackElement={<Fallback />} />;
}
RootErrorBoundary.jsx
export function RootErrorBoundary() {
let error = useRouteError() as Error;
return (
<div>
<h1>Uh oh, something went terribly wrong ๐ฉ</h1>
<pre>{error.message || JSON.stringify(error)}</pre>
<button onClick={() => (window.location.href = "/")}>
Click here to reload the app
</button>
</div>
);
}
ProjectErrorBoundary.jsx
export function ProjectErrorBoundary() {
let error = useRouteError();
// We only care to handle 401's at this level, so if this is not a 401
// ErrorResponse, re-throw to let the RootErrorBoundary handle it
if (!isRouteErrorResponse(error) || error.status !== 401) {
throw error;
}
return (
<>
<h1>You do not have access to this project</h1>
<p>
Please reach out to{' '}
<a href={`mailto:${error.data.contactEmail}`}>{error.data.contactEmail}</a> to obtain
access.
</p>
</>
);
}
๐ ์ ๋ฆฌ
๊ฒฐ๊ตญ React์ Error Boundary๋ ๋ ๋๋ง ๊ณผ์ ์์ ์ผ์ด๋๋, ์์์น ๋ชปํ ์๋ฌ๋ฅผ ํธ๋ค๋งํ๊ธฐ ์ํจ์ด๊ณ ์๋ฒ ํต์ ์์ ๋ฐ์ํ๋ ์๋ฌ๋ ๊ฒฐ๊ตญ ๋น๋๊ธฐ ํต์ ์ ์ํ ํ ์ ํตํด ํธ๋ค๋งํด์ผํ๋ค. ๋, react-router๋ฅผ ํตํ routing์์ ๋ฐ์ํ๋ ์๋ฌ(ex. ์ ์ ๊ฐ ์กด์ฌํ์ง ์๋ path๋ฅผ ํตํด ์ ๊ทผํ๋ ๊ฒฝ์ฐ)๋ errorElement์ useRouteError๋ฅผ ํตํด ๋ผ์ฐํฐ ๋ณ๋ก ์๋ฌ ๋ฐ์ด๋๋ฆฌ๋ฅผ ๋ง๋ค์ด์ ํธ๋ค๋งํ๋ฉด ๋๋ค!