์ ํ๋ฆฌ์ผ์ด์ ์ด ๋ ๋๋ง ๋์ค ์๋ฌ๋ฅผ ๋ฐ์์ํค๋ฉด ๋ฆฌ์กํธ๋ ํ๋ฉด์์ ํด๋น UI๋ฅผ ์ ๊ฑฐํ๋๋ฐ, ์ด๋ฅผ ๋ฐฉ์งํ๊ธฐ ์ํด UI ์ผ๋ถ๋ฅผ Error Bounary๋ก ๊ฐ์ธ ์๋ฌ ๋ฉ์ธ์ง์ ๊ฐ์ fallback UI๋ฅผ ํ์ํ ์ ์๋ ์ปดํฌ๋ํธ์ด๋ค.
โช ๋ฆฌ์กํธ ํ์ด๋ฒ ํธ๋ฆฌ์ ๋ํ ์ ์ธ์ try...catch์ด๋ค.
๋ง์ฝ Error Boundary๊ฐ ์๋ค๋ฉด ์๋ฌ๋ ์ ํ๋ฆฌ์ผ์ด์ ์ ์ ์ฒด ์ปดํฌ๋ํธ ํธ๋ฆฌ๋ก ์ ํ๋์ด ์ ํ๋ฆฌ์ผ์ด์ ์ด ๋ค์ด๋ ์ ์๋ค.
๋ฆฌ์กํธ ํ์ ์๋ฌ์ ๊ด๋ จ๋ UI๋ฅผ ๋ ๋๋งํ๋ ๊ฒ๋ณด๋ค ์์ ์๋ฌด๊ฒ๋ ๋ณด์ฌ์ฃผ์ง ์๋ ๊ฒ์ด ๋ ๋์๋ค๊ณ ํ๋จํ์ฌ Error Boundary๋ฅผ ํตํด ์ด๋ฌํ ๋ฌธ์ ๋ฅผ ๋ฐ์ํ์ง ์๋๋ก ํ์๋ค.
์๋ฌ ํฌ์ฐฉ: Error Boundary๋ ์์ ์ปดํฌ๋ํธ์์ ๋ฐ์ํ๋ ์๋ฌ๋ฅผ ํฌ์ฐฉํ์ฌ, ํด๋น ์๋ฌ๋ก ์ธํด ์ ์ฒด ์ ํ๋ฆฌ์ผ์ด์ ์ด ๋ค์ด๋์ง ์๋๋ก ํ๋ค.
๋์ฒด UI ์ ๊ณต: ์๋ฌ๊ฐ ๋ฐ์ํ์ ๋, Error Boundary๋ fallback UI๋ฅผ ๋ ๋๋งํ์ฌ ์ฌ์ฉ์์๊ฒ ์ค๋ฅ๊ฐ ๋ฐ์ํ์์ ์๋ฆฌ๊ณ , ์ ํ๋ฆฌ์ผ์ด์ ๋๋จธ์ง ๋ถ๋ถ์ ์ ์์ ์ผ๋ก ๋์ํ ์ ์๋๋ก ํ๋ค.
์๋ช
์ฃผ๊ธฐ ๋ฉ์๋: componentDidCatch(error, info)
, static getDerivedStateFromError(error)
๋ ๊ฐ์ง ์๋ช
์ฃผ๊ธฐ ๋ฉ์๋๋ฅผ ์ฌ์ฉํ์ฌ ์ค๋ฅ ์ฒ๋ฆฌ ๋ก์ง์ ๊ตฌํํ ์ ์๋ค.
static getDerivedStateFromError(error)
: UI๊ฐ ๊ทธ๋ ค์ง๊ธฐ ์ ์ ํธ์ถ๋๋ฉฐ ์๋ฌ ์ํ๋ฅผ ๋ณ๊ฒฝ์์ผ fallback UI๋ฅผ ๋ ๋๋งํ๋ค.
componentDidCatch(error, info)
: UI๊ฐ ๊ทธ๋ ค์ง ํ ํธ์ถ๋๋ฉฐ ์๋ฌ๋ฅผ ๊ธฐ๋กํ๊ฑฐ๋ ํ์ ์์
(์๋ฌ ๋ฆฌํฌํ
์๋น์ค ๋ก๊ทธ ์ ์ก ๋ฑ)์ ํ ๋ ์ฌ์ฉ๋๋ค.
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError(error) {
// state๋ฅผ ์
๋ฐ์ดํธํ์ฌ ๋ค์ ๋ ๋๋ง์ fallback UI ํ์
return { hasError: true };
}
componentDidCatch(error, info) {
// ์๋ฌ๋ฅผ ๊ธฐ๋กํ๊ฑฐ๋ ํ์ ์์
(์๋ฌ ๋ฆฌํฌํ
์๋น์ค ๋ก๊ทธ ์ ์ก ๋ฑ) ์งํ
logErrorToMyService(error, info.componentStack);
}
render() {
if (this.state.hasError) {
// ์ปค์คํ
fallback UI๋ฅผ ๋ ๋๋งํ ์ ์๋ค.
return this.props.fallback;
}
return this.props.children;
}
}
๐ก ํ์ฌ Error Boundary๋ฅผ ํจ์ํ ์ปดํฌ๋ํธ๋ก ์์ฑํ ์ ์๋ ๋ฐฉ๋ฒ์ ์๋ค.
ํ์ง๋ง Error Boundary๋ฅผ ์ง์ ์์ฑํ์ง ์๊ณ react-error-boundary ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ์ฌ์ฉํ๋ ๋ฐฉ๋ฒ์ด ์๋ค.
โช ๋ฆฌ์กํธ ๊ณต์ ๋ฌธ์(v18.3.1)
์๋ฌ ๋ฐ์: ์ปดํฌ๋ํธ์์ ์๋ฌ๊ฐ ๋ฐ์ํ๋ฉด, ๋ฆฌ์กํธ๋ ํด๋น ์๋ฌ๋ฅผ ์์ ์ปดํฌ๋ํธ๋ก ์๋ฌ๋ฅผ ๋ฒ๋ธ๋ง(bubbling)ํ์ฌ ์๋ฌ ์ฒ๋ฆฌ๋ฅผ ์๋ํ๋ค.
๊ฐ์ฅ ๊ฐ๊น์ด Error Boundary ์ฐพ๊ธฐ: ์๋ฌ๊ฐ ๋ฐ์ํ ์์น์์ ๊ฐ์ฅ ๊ฐ๊น์ด Error Boundary๋ฅผ ์ฐพ๋๋ค. ์ด Error Boundary๋ ์๋ฌ๋ฅผ ์ฒ๋ฆฌํ๊ณ Fallback UI๋ฅผ ๋ณด์ฌ์ค ์ ์๋ ์ปดํฌ๋ํธ์ด๋ค.
์ปค์ ์ด๋: ๋ฆฌ์กํธ๋ ํ์ฌ ์์ ํ๊ณ ์๋ ์ปดํฌ๋ํธ ํธ๋ฆฌ์ current ์ปค์๋ฅผ Error Boundary๋ก ์ด๋ํด์ผ ํ๋ค.
๐ก current ์ปค์: ๋ฆฌ์กํธ๊ฐ ์ด๋ค ์ปดํฌ๋ํธ๋ฅผ ๋ ๋๋งํ๊ณ ์๋์ง ์ถ์ ํ๊ณ ์๋ ํฌ์ธํฐ์ด๋ค.
ShouldCapture ํ๋๊ทธ: ์ธ์์ด๋ฉ ๊ณผ์ ์์ ShouldCapture
๋ผ๋ ํ๋๊ทธ๊ฐ DidCapture
๋ก ๋ฐ๋๋ค. ์ด๊ฒ์ Error Boundary๊ฐ ์๋ฌ๋ฅผ ์บก์ณํ๊ณ ์๋ค๋ ๊ฒ์ ๋ํ๋ธ๋ค.
Error Boundary์์ ๋ฆฌ๋ ๋๋ง ์๋: Error Boundary์ ๋๋ฌํ ํ, ๋ฆฌ์กํธ๋ ํด๋น ๊ฒฝ๊ณ์์ ๋ค์ ๋ ๋๋ง์ ์๋ํฉ๋๋ค. ์ด ๊ณผ์ ์์ Error Boundary๋ ์๋ฌ๋ฅผ ์ฒ๋ฆฌํ๊ณ , ๋์ฒด UI๋ฅผ ๋ ๋๋งํ๋ค.
์๋ฃ ์์
: ์ด๋ฌํ ์ธ์์ด๋ฉ ๊ณผ์ ์ completeUnitOfWork()
๋ผ๋ ๋ด๋ถ ๋ฉ์๋์์ ์ด๋ฃจ์ด์ง๋ค. ์ด ๋ฉ์๋๋ ๊ฐ ํ์ด๋ฒ(๋ฆฌ์กํธ์ ๊ฐ์ DOM ๋จ์)์ ๋ํ ์์
์ ์๋ฃํ๋ ์ญํ ์ ํ๋ค.
๐ก ์ธ์์ด๋ฉ ๊ณผ์ : ๋ฆฌ์กํธ์์ ๋ฐ์ํ ์๋ฌ๋ฅผ ์ฒ๋ฆฌํ๋ ๋ฐฉ๋ฒ ์ค ํ๋๋ก, ์๋ฌ๊ฐ ๋ฐ์ํ ์ปดํฌ๋ํธ์์ ๊ฐ์ฅ ๊ฐ๊น์ด Error Boundary(์๋ฌ ๊ฒฝ๊ณ)๋ก ์ด๋ํ์ฌ ํด๋น ๊ฒฝ๊ณ์์ ์๋ฌ๋ฅผ ์ฒ๋ฆฌํ๋ ๊ณผ์ ์ ์๋ฏธํ๋ค.
์๋ฌ๊ฐ ๋ฐ์ํ๋ฉด ๋ฆฌ์กํธ๋ ๊ฐ์ฅ ๊ฐ๊น์ด Error Boundary๋ฅผ ์ฐพ๊ณ , ํด๋น ๊ฒฝ๊ณ๋ก ์ปค์๋ก ์ด๋์ํจ๋ค.
ShouldCapture
ํ๋๊ทธ๊ฐ DidCapture
๋ก ๋ฐ๋๋ฉด์ Error Boundary๊ฐ ์๋ฌ๋ฅผ ์ฒ๋ฆฌํ๊ฒ ๋๋ค.
์ด ๋ชจ๋ ๊ณผ์ ์ completeUnitOfWork()
๋ฉ์๋์์ ๊ด๋ฆฌ๋์ด, ์๋ฌ ๊ด๋ฆฌ๊ฐ ์ฒด๊ณ์ ์ผ๋ก ์ด๋ฃจ์ด์ง๋๋ก ํ๋ค.
- ์ด๋ฒคํธ ํธ๋ค๋ฌ
- ๋น๋๊ธฐ ์ฝ๋(setTimeout, fetch ๋ฑ)
- ์๋ฒ ์ธก ๋ ๋๋ง
- Error Boundary ์์ฒด์์ ๋ฐ์ํ ์ค๋ฅ
import * as React from "react";
import { createRoot } from 'react-dom/client'
function MainComponents(){
return (
<ErrorBoundary>
<TestComponents />
</ErrorBoundary>
);
}
function TestComponents() {
return (
<button type="button" onClick={() => {
throw new Error("์๋ฌ ๋ฐ์!")
}}>
์๋ฌ ๋ฐ์ ๋ฒํผ
</button>
);
}
const root = createRoot(document.querySelector('#root'))
root.render(<MainComponents/>)
์ ์ฝ๋์ ๊ฐ์ด ์๋ฌ ๋ฐ์ ๋ฒํผ์ ํด๋ฆญํด์ ์๋ฌ๋ฅผ throwํ์ง๋ง ๋ฆฌ์กํธ ๋ ๋๋ง ๊ณผ์ ์์ ๋ฐ์ํ๋ ์๋ฌ๊ฐ ์๋๊ธฐ ๋๋ฌธ์ Error Boundary๋ ์๋ฌ๋ฅผ ํฌ์ฐฉํ์ง ๋ชปํ๋ค.
๋ง์ฝ ์ด๋ฒคํธ ํธ๋ค๋ฌ๋ ๋น๋๊ธฐ ์ฝ๋์ ์ํด ๋ฐ์ํ ์๋ฌ๋ฅผ Error Boundary์์ ํฌ์ฐฉํ๊ณ ์ถ๋ค๋ฉด ์ํ ์ ๋ฐ์ดํธ๋ฅผ ํตํ์ฌ ๋ฆฌ๋ ๋๋ง์ ๋ฐ์์์ผ ์๋ฌ๋ฅผ throwํ๋ฉด ๋๋ค.
import * as React from "react";
import { useState } from "react";
import { createRoot } from 'react-dom/client'
function MainComponents(){
return (
<ErrorBoundary>
<TestComponents />
</ErrorBoundary>
);
}
function TestComponents() {
const [isError, setIsError] = useState(false);
if(isError){
throw new Error("์๋ฌ ๋ฐ์!")
}
return (
<button type="button" onClick={() => setIsError(true)}>
์๋ฌ ๋ฐ์ ๋ฒํผ
</button>
);
}
Error Boundary๋ getDerivedStateFromError()
๋ฉ์๋๋ฅผ ํตํด ์๋ฌ ์ํ๋ฅผ ๋ณ๊ฒฝํ์ฌ ๋์ํ๋๋ฐ, ์ด ์๋ช
์ฃผ๊ธฐ ๋ฉ์๋๋ ํด๋ผ์ด์ธํธ ์ฌ์ด๋์์ ๋์ํ๊ธฐ ๋๋ฌธ์ ์๋ฒ ๋ ๋๋ง์์ ๋ฐ์ํ ์๋ฌ๋ Error Boundary๊ฐ ํฌ์ฐฉํ ์ ์๋ค.
๊ฒฐ๊ณผ์ ์ผ๋ก ์๋ฒ ๋ ๋๋ง ์ค์ ๋ฐ์ํ๋ ์ค๋ฅ๋ ๋ณ๋๋ก ์ฒ๋ฆฌํด์ผ ํ๋ฉฐ, Error Boundary๋ ํด๋ผ์ด์ธํธ ์ธก์์ ๋ ๋๋ง ์ค ๋ฐ์ํ๋ ์ค๋ฅ๋ฅผ ์ฒ๋ฆฌํ๋ ๋ฐ ์ง์คํ๊ณ ์๋ค.
Error Boundary๋ ์์ ์ปดํฌ๋ํธ์์ ๋ฐ์ํ๋ ์ค๋ฅ๋ฅผ ์ฒ๋ฆฌํ๊ธฐ ์ํด ์ค๊ณ๋์๋ค. ๋ฐ๋ผ์ ์์ ์ด ๊ฐ์ธ๊ณ ์๋ ์ปดํฌ๋ํธ์์ ๋ฐ์ํ๋ ์ค๋ฅ๋ฅผ ํฌ์ฐฉํ๋ ๊ฒ์ด ์ฃผ๋ ๋ชฉ์ ์ด๋ฉฐ, Error Boundary ์์ฒด์์ ๋ฐ์ํ ์ค๋ฅ๋ Error Boundary์ ๋ณดํธ๋ฅผ ๋ฐ์ง ์๋๋ค.
์ฌ์ฉ์ ๊ฒฝํ(UX) ๊ฐ์
Error Boundary๋ฅผ ์ฌ์ฉํ์ฌ ์์ ์ปดํฌ๋ํธ์์ ๋ฐ์ํ ์๋ฌ๊ฐ ์ ํ๋ฆฌ์ผ์ด์
์ ์ฒด๋ก ์ ํ๋๋ ๊ฒ์ ๋ฐฉ์งํ์ฌ, ์๋ฌ๊ฐ ๋ฐ์ํ๋๋ผ๋ ์ ํ๋ฆฌ์ผ์ด์
์ ๋๋จธ์ง ๋ถ๋ถ์ ๊ณ์ ์ฌ์ฉํ ์ ์๋๋ก ํ์ฌ ์ฌ์ฉ์์๊ฒ ๋ ๋์ ๊ฒฝํ์ ์ ๊ณตํ๋ค.
์ค๋ฅ ๋ก๊น
๋ฐ ๋ชจ๋ํฐ๋ง
componentDidCatch()
์๋ช
์ฃผ๊ธฐ ๋ฉ์๋๋ฅผ ์ฌ์ฉํ์ฌ ๋ฐ์ํ ์๋ฌ ์ ๋ณด๋ฅผ ๋ก๊น
ํ๊ฑฐ๋ ์ธ๋ถ ๋ชจ๋ํฐ๋ง ์๋น์ค์ ์ ์กํ ์ ์๋ค. ์ด๋ฅผ ํตํด ๊ฐ๋ฐ์๋ ์ค๋ฅ ๋ฐ์ ์์ธ์ ํ์
ํ๊ณ ๊ฐ์ ํ ์ ์๋ค.
์ปดํฌ๋ํธ ๋ถ๋ฆฌ
๊ฐ Error Boundary๋ ํน์ ์์ ์ปดํฌ๋ํธ๋ง ๊ฐ์ธ๋๋ก ์ค์ ํ์ฌ ํ์ํ ๋ถ๋ถ์์๋ง ์๋ฌ ์ฒ๋ฆฌ๋ฅผ ์ ์ฉํ ์ ์๋ค. ์ด๋ก ์ธํด ์ฝ๋์ ๋ชจ๋์ฑ๊ณผ ์ฌ์ฌ์ฉ์ฑ์ด ํฅ์๋๋ค.
๊ฐ๋ฐ์ ๊ฒฝํ(DX) ํฅ์
์๋ฌ ๋ฐ์ ์ fallback UI๋ ์คํํด์ผ ํ ํจ์๊ฐ ๋ณ๊ฒฝ๋์ด์ผ ํ๋ค๋ฉด, ํด๋น ์ปดํฌ๋ํธ์์ ๊ฐ์ฅ ๊ฐ๊น์ด Error Boundary๋ฅผ ์์ ํ๋ฉด ๋๊ธฐ ๋๋ฌธ์ ์ ์ง ๋ณด์ํ๊ธฐ ์ฝ๊ณ , ์๋ฌ ์ฒ๋ฆฌ๋ฅผ ์ ์ธ์ ์ผ๋ก ํ์ฌ ๋ฌด์์ ํด์ผํ๋์ง ๋ช
ํํ ํํํ์ฌ ๋ณต์กํ ํ๋ฆ์ ๊ฐ๊ฒฐํ๊ฒ ํ์
ํ ์ ์๋ค.
์์์ ๋น๋๊ธฐ ์ฝ๋์ ์ํด ๋ฐ์ํ ์๋ฌ๋ฅผ Error Boundary์์ ํฌ์ฐฉํ๊ณ ์ถ๋ค๋ฉด ์ํ ์
๋ฐ์ดํธ๋ฅผ ํตํ์ฌ ๋ฆฌ๋ ๋๋ง์ ๋ฐ์์์ผ์ผ ํ๋ค๊ณ ์ค๋ช
ํ๋๋ฐ, Tanstack Query(React Query)๋ฅผ ์ฌ์ฉํ๋ค๋ฉด throwOnError
์ต์
์ ์ค์ ํ์ฌ ๋ณ๋์ ์ํ ์
๋ฐ์ดํธ ์์ด Error Boundary์์ ์๋ฌ๋ฅผ ํฌ์ฐฉํ๊ฒ ํ ์ ์๋ค.
๐ก
throwOnError
๋ ๋ฌด์์ผ๊น?
Tanstack Query์์ ์๋ฌ ๋ฐ์ ์์ ์ฒ๋ฆฌ ๋ฐฉ์์ ์ค์ ํ๋ ์ต์ ์ด๋ค.
์ด ๊ฐ์true
๋ก ์ค์ ํ๋ฉด ์๋ฌ๊ฐ ๋ ๋๋ง ๋จ๊ณ์์ ๋ฐ์ํ์ฌ ๊ฐ์ฅ ๊ฐ๊น์ด Error Boundary๋ก ์ ํ๋๋ค.
const fetchTodo = async () => {
const res = await fetch("https://jsonplaceholder.typicode.com/todos/1");
throw new Error("์๋ฌ ๋ฐ์");
};
export default function Home() {
return (
<>
<ErrorBoundary fallback={<div>์๋ฌ๊ฐ ๋ฐ์ํ์์ต๋๋ค</div>}>
<TestComponent throwOnErrorOption={true} /> // throwOnError: true
</ErrorBoundary>
<ErrorBoundary fallback={<div>์๋ฌ๊ฐ ๋ฐ์ํ์์ต๋๋ค</div>}>
<TestComponent throwOnErrorOption={false} /> // throwOnError: false
</ErrorBoundary>
</>
);
}
function TestComponent({ throwOnErrorOption }: { throwOnErrorOption: boolean }) {
const { data, refetch } = useQuery({
queryKey: ["todos"],
queryFn: fetchTodo,
throwOnError: throwOnErrorOption,
enabled: false, // fetch button์ ๋๋ฌ์ผ๋ง fetch๊ฐ ์คํ๋จ
});
return (
<div>
<h1>Error Boundary</h1>
<h2>throwOnError: {`${throwOnErrorOption}`}</h2>
<button type="button" onClick={() => refetch()}>
fetch button
</button>
{data && <div>{data.title}</div>}
</div>
);
}
error boundary๋ก ๋ ๋๋ง ์ค๋ฅ ํฌ์ฐฉํ๊ธฐ - React ๊ณต์ ๋ฌธ์(v18.3.1)
Error Boundaries - React ๊ณต์ ๋ฌธ์(v18.2.0)
ErrorBoundary ์ค๋ช
ํ
How does ErrorBoundary work internally in React?