๐ ErrorBoundary & Suspense, ๊ฑฐ์ ์๋ฒฝํ ์ฌ์ฉ๋ฐฉ๋ฒ ๊ฐ์ด๋
์ ๋ง๋ Errorboundary
๋ฐฑ๊ฐ์ try-catch ์ ๋ถ๋ฝ๋ค.
ํ๋ก ํธ ๊ฐ๋ฐ์ ๊ณ์ํ๋ค ๋ณด๋ ๋ค์ํ ์ํฉ์ ๋ง์ฃผ์น๊ฒ ๋์๊ณ , ๊ทธ์ ๋ฐ๋ฅธ ์ ์ ํ ํ๋ฉด ํํ์ ์ค์์ฑ์ ์ ์ฐจ ๋๊ปด๊ฐ์ต๋๋ค.
๋ํ์ ์ผ๋ก ์๋ฒ ๋ฐ์ดํฐ์ ๋ํ ๋ก๋ฉ๊ณผ ์๋ฌ ์ฒ๋ฆฌ๊ฐ ์์ฃ .
๋์์์ด ์ฌ์ฉํด ๋ณด๊ณ ๊ฐ์ ํด ๋ณด๋ 1๋ ์ด ์ง๋ ์ง๊ธ์์์ผ ์ด๋ ์ ๋ ๊ฐ์ด ์กํ๋ ๊ฑฐ ๊ฐ์ ๊ทธ ๋ฐฉ๋ฒ์ ๊ณต์ ํด๋ณด๋ ค ํฉ๋๋ค.
์ด ๊ธ์ ์ค์ ์๋น์ค์์์ ์ ์ฉ ๋ฐฉ๋ฒ์ ๋ํด ์ค๋ช ํ๋ ๊ธ์ด๊ธฐ ๋๋ฌธ์ ErrorBoundary์ Suspense์ ๋ํ ๊ฐ๋ ์ ์๋ ๊ธ์ ์ฐธ์กฐํด ์ฃผ์ธ์.
- ErrorBoundary๋ฅผ ํตํ ์ ์ธ์ ์ธ ์๋ฌ ํธ๋ค๋ง, react-query๋ฅผ ์ด์ฉํ ์ฌํธ์ถ ๋ฐฉ๋ฒ
- Suspense์ ์ฌ์ฉํด ์ ์ธ์ ์ผ๋ก ๋ก๋ฉ ํ๋ฉด ๊ตฌํํ๊ธฐ
ErrorBoundary์ Suspense๋ฅผ ์ค์ ํ๋ก๋ํธ์์ ์ด๋ป๊ฒ ์ฌ์ฉํ๋์ง ์๊ฐ๋๋ฆฝ๋๋ค.
๐ ์ ์ฌ์ฉํด์ผ ํ๋๊ฐ
์ฌ์ค “์” ์ฌ์ฉํ๋์ง๊ฐ ๊ฐ์ฅ ์ค์ํ๊ฒ ์ฃ .
ํ ์ค๋ก ์์ฝํ์๋ฉด “ํ ๊ณณ์์ ๊ด๋ฆฌํ ์ ์๊ณ , ์ ์ธ์ ์ผ๋ก ์ฌ์ฉํ ์ ์๊ธฐ ๋๋ฌธ”์ ๋๋ค.
๋ ์์ธํ ์ค๋ช ํ์๋ฉด ๋ค์๊ณผ ๊ฐ์ต๋๋ค.
๐ ๋ฏธ์ฌ์ฉ ์์
function MyComponent() {
// ๋ฐ์ดํฐ์ ๋ก๋ฉ, ์๋ฌ ์ํ๋ฅผ ๊ด๋ฆฌํ๋ state๋ฅผ ์ ์ธํฉ๋๋ค.
const [data, setData] = useState(null);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState(null);
useEffect(() => {
// ์ปดํฌ๋ํธ๊ฐ ๋ง์ดํธ๋ ๋ ๋ฐ์ดํฐ๋ฅผ ๊ฐ์ ธ์ค๋ ํจ์๋ฅผ ํธ์ถํฉ๋๋ค.
const fetchData = async () => {
setIsLoading(true); // ๋ก๋ฉ ์์
try {
const result = await getData()
);
setData(result); // ๋ฐ์ดํฐ ์ค์
setError(null); // ์ค๋ฅ ์ด๊ธฐํ
} catch (error) {
setError(error); // ์ค๋ฅ ์ค์
} finally {
setIsLoading(false); // ๋ก๋ฉ ์ข
๋ฃ
}
};
fetchData();
}, []);
// ๋ก๋ฉ ์ค์ผ ๋ ๋ก๋ฉ ํ์, ์๋ฌ๊ฐ ์์ ๋ ์๋ฌ ๋ฉ์์ง๋ฅผ ํ์ํ๊ณ ,
// ๋ฐ์ดํฐ๊ฐ ์์ ๋ ๋ฐ์ดํฐ๋ฅผ ํ์ํฉ๋๋ค.
return (
<div>
{isLoading ? (
<div>Loading...</div>
) : error ? (
<div>Error: {error.message}</div>
) : (
<div>{data}</div>
)}
</div>
);
}
export default MyComponent;
ํ ์ปดํฌ๋ํธ ๋ด์์ useEffect๋ฅผ ์ด์ฉํด ๋ฐ์ดํฐ๋ฅผ ํธ์ถํ๊ณ ,try-catch์ ๊ฐ๊ฐ์ ์ํ๋ฅผ ํตํด ์๋ฌ์ ๋ก๋ฉ ์ํ๋ฅผ ํ์ํ๋ ๋ชจ์ต์ ๋ณด๊ธฐ๋ง ํด๋ ์ด์ง๋ฝ์ฃ .
๊ทธ ์ด์ ๋ ํ ์ปดํฌ๋ํธ์ ๋๋ฌด๋ ๋ง์ ์ญํ ์ด ๋ถ๋ด๋์๊ธฐ ๋๋ฌธ์ ๋๋ค.
์ด๋ฌํ ์ญํ ๋ค์ ํ์ํ๊ธฐ ์ํด์๋ ๋น์ฐํ ๊ทธ ์ปดํฌ๋ํธ ๋ด ์ฝ๋๊ฐ ํฌํจ๋์ด์ผ ํ ๊ฒ์ด๊ณ ,
์ด๋ค์ ํ๋ฐ ๋ค์์ฌ ๊ฐ๋
์ฑ์ ๋ง์ณ๋ฒ๋ฆฝ๋๋ค.
๊ทธ๋ ๋ค๋ฉด ์ด๋ฌํ ์ญํ ๋ค์ ๋ถ๋ฆฌํ๋ค๋ฉด ์ด๋ค ๋ชจ์ต์ผ๊น์?
๐ ์ญํ ๋ถ๋ฆฌ
<ErrorBoundary FallbackComponent={<span>์๋ฌ ๋ฐ์</span>}> // ์๋ฌ์ํ ๋ถ๋ฆฌ
<Suspense fallback={<span>๋ก๋ฉ์ค...</span>}> // ๋ก๋ฉ์ํ ๋ถ๋ฆฌ
<MyComponent />
</Suspense>
</ErrorBoundary>
function MyComponent() {
const { data } = useSuspenseQuery({ queryKey: ['data'], queryFn: getData })
return <div>{data}</div>
}
export default MyComponent;
์ฐ์ ๋ก๋ฉ์ํ๊ฐ ๊ฐ์ง๋์์ ๋ Suspense๊ฐ ๋ก๋ฉ์ํ์ ๋ํ ๋์ฒด UI๋ฅผ ํ์ํ๊ณ ,
์๋ฌ์ํ๊ฐ ๊ฐ์ง ๋์์ ๋ ErrorBoundary๊ฐ ์๋ฌ์ํ์ ๋ํ ๋์ฒด UI๋ฅผ ํ์ํ๊ฒ ๋ฉ๋๋ค.
๊ทธ๋ฌ๋ฉด MyComponent์์๋ ๋ก๋ฉ, ์๋ฌ ์ํ์ ๋ํ ๋ก์ง์ ์ ๊ฑฐํ ์ ์์ด,
์ ์ ์๋ ์ํ๊ด๋ฆฌ์ ์กฐ๊ฑด๋ถ ๋ ๋๋ง์ด ์ฌ๋ผ์ง๊ฒ ๋์ด ์ฑ๊ณตํ์ ๋๋ง ์์ฑํ ์ ์๊ฒ ๋ฉ๋๋ค.
๊ฒ๋ค๊ฐ Suspense์ ErrorBoundary๋ฅผ ์ฌ์ฉํ๊ธฐ ์ํด ํ์ํ ์๋ฒ ์ํ๊ด๋ฆฌ ๋๊ตฌ์ธ tanstack-query(react-query)์ ์ํด useEffect ๊ฐ์ ์ฌ์ด๋ ์ดํํธ๊น์ง ์ฌ๋ผ์ง๋ ๋ง๋ ์ ๋๊ฒ ๊น๋ํด์ง ๊ฒ์ ํ์ธํ ์ ์์ต๋๋ค.
๊ทธ๋ผ ์ค๊ณ ๋ฐฉ๋ฒ์ ์ค๋ช ํ๊ธฐ ์ ํ์ํ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ์๊ฐํฉ๋๋ค.
๐ ์ฌ์ ์ค์น
$ npm i @tanstack/react-query react-error-boundary react-toastify
tanstack-query (react-query)
์์ฆ์๋ ๊ฑฐ์ ๋์ธ๋ผ๊ณ ํ ์ ์์ฃ .
์๋ฒ ์ํ๋ฅผ ๊น๋ํ๊ฒ ๊ด๋ฆฌํ๋ ์ญํ ๋ฟ ์๋๋ผ Loading ์ํ๋ฅผ throw ํด Suspense๊ฐ catch ํ ์ ์๋๋ก ๊ธฐ๋ฅ์ ์ ๊ณตํฉ๋๋ค.
๋ํ ์๋ฌ ๋ฐ์ ์ ํด๋น ์ฟผ๋ฆฌ๋ฅผ refetch ํ๋ ๊ธฐ๋ฅ๋ ์ ๊ณตํ๋ ํ์์ ์ผ๋ก ์ค์นํด ์ค๋๋ค.
์ด ๊ธ์์๋ v5 ๋ฒ์ ์ ์ฌ์ฉํฉ๋๋ค.
react-error-boundary
ErrorBoundary๋ฅผ ์ฌ์ฉํ๊ธฐ ์ํด์๋ ํด๋์ค ์ปดํฌ๋ํธ์ ๊ฐ๋ ฅํ ์๋ช ์ฃผ๊ธฐ ๊ธฐ๋ฅ์ ํ์์ ์ผ๋ก ์ด์ฉํด์ผ ํฉ๋๋ค.
ํด๋์คํ์ผ๋ก ์ง์ ErrorBoundary๋ฅผ ๊ตฌํํ๊ธฐ ๊ท์ฐฎ์ผ๋ ์ ๋ง๋ค์ด์ง ํจํค์ง๋ฅผ ์ค์นํฉ๋๋ค.
react-toastify
๋์ฒด UI๋ฅผ ํ์ํ์ง ์๊ณ ์ฌ์ฉ์์๊ฒ ์๋ฌ๊ฐ ๋ฐ์ํ์์ ์๋ฆฌ๋ ํ ์คํธ ๋ฉ์์ง ๋ผ์ด๋ธ๋ฌ๋ฆฌ์ ๋๋ค.
๋ค๋ฅธ ๋ฐฉ์์ผ๋ก ์๋ฌ ์๋ฆผ์ ํํํด๋ ๊ด์ฐฎ์ต๋๋ค.
๐ ์๋ฌ ์ํ์ ์ข ๋ฅ ๋ถ๋ฅํ๊ธฐ
๋ก๋ฉ์ด์ผ ์๋ฒ ๋ฐ์ดํฐ ๋ฐ๊ธฐ ์ , ๋ฐ์ ํ๋ก๋ง ๋๋์ด์ ๋ฌ๋ฆฌ ์๊ฐํ ๊ฒ์ด ์์ง๋ง ์๋ฌ ์ํ๋ ๊ทธ๋ ์ง ์์ฃ .
๊ทธ๋์ ์ ๋ ๋ค์๊ณผ ๊ฐ์ด ๋ถ๋ฆฌํด์ ๊ตฌ๋ถํ๊ธฐ๋ก ํ์ต๋๋ค.
๐ JavaScript ์๋ฌ
ํ์ ์คํฌ๋ฆฝํธ๋ฅผ ์ฌ์ฉํด์ ์ปดํ์ผ ์์ ์ ์๋ฌ๋ฅผ ์ก์๋ด๋๋ผ๋ 100% ์๋ฒฝํ๊ฒ ํ ์ ์์ต๋๋ค.
์ฐธ์กฐ ์ค๋ฅ๊ฐ ๋ฐ์ํ์ ๋ ์๋ฌ๊ฐ ์ต์์ ์คํ ์ปจํ ์คํธ๊น์ง ์ ํ๋๋ฉด ์ฑ์ด crush ๋ฉ๋๋ค.
์ด ๊ฒฝ์ฐ ์๋ฌ ๋ฉ์์ง๋ฅผ error.message๋ก ์ก์๋ผ ์ ์์ง๋ง, ์ฌ์ฉ์์๊ฒ ๋ณด์ฌ์ค ํ์๊ฐ ์๊ธฐ์ default message๋ก ์ฒ๋ฆฌํฉ๋๋ค.
๐ ํฉ์๋ ์๋ฌ (error.response.data)
http status๊ฐ ์๋ฌ ์ํ์ฌ๋ Error ๊ฐ์ฒด์ error.response.data๋ฅผ ํตํด ๋ฐ์ดํฐ๋ฅผ ๋ฐ์ ์ ์์ต๋๋ค.
์ด๋ฅผ ํตํด ํน์ ํ ์ํฉ์์์ ์๋ฌ๋ฅผ ์๋ฒ ์ธก๊ณผ ์ ์ํด ์ด๋ค ์๋ฌ์ธ์ง ํํํ ์ ์์ต๋๋ค.
์ด๋ฌํ ๊ธฐ๋ฅ์ ๊ตฌํํ๊ธฐ ์ํด์๋ ์๋ฒ์ธก๊ณผ ์ด๋ค ๋ฐ์ดํฐ ๊ตฌ์กฐ๋ก ์ ์กํ ๊ฒ์ธ์ง, ์ด๋ค ์ฝ๋๊ฐ ์ด๋ ํ ์๋ฏธ์ธ์ง๋ฅผ ํฉ์ํ ํ ๊ณ์ํด์ ๋ค๋ฌ๋ ๊ณผ์ ์ด ํ์ํฉ๋๋ค.
์ข์ ์์๋ก ์นด์นด์ค ์ผํ์์ ๊ณต๊ฐํ ์๋ฌ ๋ฐ์ดํฐ ๊ตฌ์กฐ์ ํฉ์๋ ์๋ฌ ์ฝ๋๋ก ์ฐธ๊ณ ํ ์ ์์ต๋๋ค.
์๋ต ์ฝ๋ ๋ฐ ์๋ฌ ์ฝ๋

๐ http ์์ฒญ ์๋ฌ (error.response)
์๋ฒ ์ธก์์ ์์ธ์ฒ๋ฆฌ๊ฐ ๋์ด์์ง ์์ error.response.data๊ฐ ์๋ ๊ฒฝ์ฐ์ ํด๋นํฉ๋๋ค.
์๋ฒ ๊ฐ๋ฐ ์ ๊ฐ๋ฅํ ๋ชจ๋ ์์ธ ์ํฉ์ ์ฒ๋ฆฌํด ํด๋ผ์ด์ธํธ ์ธก์์ ์ ๋ณด๋ฅผ ์ถ์ถํ ์ ์๋๋ก ํ๋ ๊ฒ์ด ๊ฐ์ฅ ์ด์์ ์ด๋ ํ์ค์ ์ผ๋ก 100% ์๋ฒฝํ ์ํฉ์ ์์ต๋๋ค.
์ด๋ฐ ์ํฉ์์๋ error.response.status ์ HTTP ์ํ ์ฝ๋๋ฅผ ์ด์ฉํด ์ํฉ์ ์ ์ถํ ์ ์๋๋ก ํฉ๋๋ค.
์ํ ์ฝ๋์ ๋ํ ์ ๋ณด๋ ๋ค์์ ์ฐธ๊ณ ํด ์ฃผ์ธ์.
HTTP ์ํ ์ฝ๋ - HTTP | MDN
๐ ์๋ต์ด ์๋ ์๋ฌ (error)
์ด ๊ฒฝ์ฐ๋ ์ฃผ๋ก ๋คํธ์ํฌ ๋ฌธ์ ๋ ์๋ฒ๊ฐ ๋ค์ด๋์ด ์๋ฌด๋ฐ ์๋ต์ ๋ฐ์ง ๋ชปํ ๊ฒฝ์ฐ์ ํด๋นํฉ๋๋ค.
๋ํ ํด๋ผ์ด์ธํธ ์ธก์์ ์ค์ ํ ํ์์์์ด ์ด๊ณผ๋๊ฑฐ๋ DNS, CORS์ ์ํด์ ๋ฐ์ํ ์ ์์ผ๋ฉฐ error.code๋ก ์๋ณํฉ๋๋ค.
์ฌ์ค์ ์ตํ์ ๊ด๋ฌธ์ด๋ผ ํ ์ ์๊ธฐ ๋๋ฌธ์ ์๋ฒ ์์ฒญ์ ๋ํ ์๋ฌ ์ฒ๋ฆฌ๋ ์ด์ชฝ ๋จ๊ณ์์ ๋ง๋ฌด๋ฆฌํ ์ ์๋๋ก ํด์ผ ํฉ๋๋ค.
๐ ์๋ฌ ๋ฐ์ดํฐ ํ์ฑ
import { AxiosError } from 'axios';
type ErrorCodeType = {
[key: string]: { code: string; message: string; requireLogin?: boolean };
};
export const ERROR_CODE: ErrorCodeType = {
default: { code: 'ERROR', message: '์ ์ ์๋ ์ค๋ฅ๊ฐ ๋ฐ์ํ์ต๋๋ค.' },
// axios error
ERR_NETWORK: { code: 'ํต์ ์๋ฌ', message: '์๋ฒ๊ฐ ์๋ตํ์ง ์์ต๋๋ค. \nํ๋ก๊ทธ๋จ์ ์ฌ์์ํ๊ฑฐ๋ ๊ด๋ฆฌ์์๊ฒ ์ฐ๋ฝํ์ธ์.' },
ECONNABORTED: { code: '์์ฒญ ์๊ฐ ์ด๊ณผ', message: '์์ฒญ ์๊ฐ์ ์ด๊ณผํ์ต๋๋ค.' },
// http status code ๋ฐ ์ ์ ๋ ์ฝ๋
400: { code: '400', message: '์๋ชป๋ ์์ฒญ.' },
4001: { code: '4001', message: '์์ฒญ์ ๋ํ Validation ์๋ฌ์
๋๋ค.' },
401: { code: '401', message: '์ธ์ฆ ์๋ฌ.', requireLogin: true },
4011: { code: '4011', message: '์ธ์ฆ์ด ๋ง๋ฃ๋์์ต๋๋ค.', requireLogin: true },
403: { code: '403', message: '๊ถํ์ด ์์ต๋๋ค.' },
} as const;
export const getErrorDataByCode = (error: AxiosError<{ code: number; message: string }>) => {
const serverErrorCode = error?.response?.data?.code ?? '';
const httpErrorCode = error?.response?.status ?? '';
const axiosErrorCode = error?.code ?? '';
if (serverErrorCode in ERROR_CODE) {
return ERROR_CODE[serverErrorCode as keyof typeof ERROR_CODE];
}
if (httpErrorCode in ERROR_CODE) {
return ERROR_CODE[httpErrorCode as keyof typeof ERROR_CODE];
}
if (axiosErrorCode in ERROR_CODE) {
return ERROR_CODE[axiosErrorCode as keyof typeof ERROR_CODE];
}
return ERROR_CODE.default;
};
์ฌ๋ฌ ๊ฐ์ง ์๋ฌ๋ค์ ํ ๊ณณ์ ์ ์ํด ๋๊ณ ์ผ๋ฐํํ์ฌ ์ด๋์๋ ๋์ผํ ์๋ฌ ๋ฐ์ดํฐ ํ์ ์ ๋ฐ๋๋ก ๋ง๋ค์์ต๋๋ค.
์ต๋ํ Type Safety ํ๊ฒ ์๋ฌ ์ ๋ณด์ ์ ๊ทผํ๋ฉฐ, ๋ชจ๋ ์๋ฌ ์ ๋ณด๋ฅผ ํ ๊ณณ์ ๋ชจ์๋๊ธฐ ๋๋ฌธ์ ์๋ก์ด ์๋ฌ ์ ๋ณด๊ฐ ์ถ๊ฐ๋ ๋๋ ERROR_CODE๋ง ์์ ํ๋ฉด ๋ฉ๋๋ค.
return ํด์ฃผ๋ ๋ฐ์ดํฐ๋ [ํฉ์๋ ์๋ฌ] > [http status] > [axios error] ์์ผ๋ก ์ฐ์ ์์๋ฅผ ๋ก๋๋ค.
์ธ์ฆ ์ ๋ณด ๊ด๋ จ ์๋ฌ๊ฐ ๋ฐ์ํ ๋ requireLogin ์์ฑ์ ์ด์ฉํด ์ฌ์ฉ์๊ฐ ์ฌ๋ก๊ทธ์ธํ๋๋ก ์ ๋ํ ๊ฒ์
๋๋ค.
์ด์ ๋ํด ์์ธํ ๋ด์ฉ์ ๋ค์์ ์ค๋ช ํ๊ฒ ์ต๋๋ค.
๐ ์๋ฌ ํํ ๋ฐฉ๋ฒ์ ๋ฐ๋ฅธ QueryClient ์ธํ
์๋ฌ๋ฅผ ํํํ๋ ๋ฐฉ๋ฒ์ ๋ง์ด ์๊ฒ ์ง๋ง ์ ๋ 2๊ฐ์ง๋ก ๋๋๊ธฐ๋ก ํ์ต๋๋ค.
๐ ๋ฐ์ดํฐ ํํ(useQuery, useSuspenseQuery) → Fallback UI
get๊ณผ ๊ฐ์ด ๋จ์ํ ๋ฐ์ดํฐ๋ฅผ ๋ฐ์์ ํํํ๋ ํ๋ฉด์์ ์๋ฌ๊ฐ ๋ฐ์ํ์ ๋๋ ๋์ฒด UI๋ฅผ ํ์ํฉ๋๋ค.
์ ์ด์ ์๋ฌ๊ฐ ๋ฐ์ํด ๋ฐ์ดํฐ๋ฅผ ๋ฐ์ง ๋ชปํ์ ๊ฒฝ์ฐ์ ๋น ํ๋ฉด์ ํ์ํ๋ ๊ฒ๋ณด๋จ ์ฌ์ฉ์์๊ฒ ์๋ฌ๊ฐ ๋ฐ์ํ์์ ์๋ฆฌ๋ ์ญํ ์ ํฉ๋๋ค.
๐ ์ฌ์ฉ์ ์ํธ์์ฉ(useMutation) → Toast Message
์ฌ์ฉ์๊ฐ ํด๋ฆญ๊ณผ ๊ฐ์ ์ํธ์์ฉ์ ํตํด ์๋ฒ์ ํต์ ํ๋ ๊ฒฝ์ฐ๋ ํ ์คํธ ๋ฉ์ธ์ง๋ก ์๋ฌ๋ฅผ ์๋ฆฝ๋๋ค.
๋ฒํผ์ ๋๋ ๋๋ฐ ์๋ฌ๊ฐ ๋ฐ์ํด ๋์ฒด UI๋ก ์ ํ๋๋ฉด ๋นํฉ์ค๋ฌ์ธ ๊ฒ ๊ฐ๋ค๊ณ ์๊ฐํ์ต๋๋ค.
๐ QueryClient ์ธํ
const queryClient = new QueryClient({
defaultOptions: {
queries: {
throwOnError: true,
},
mutations: {
throwOnError: false,
onError: (error: any) => {
const errorData = getErrorDataByCode(error);
toast.error(`[${errorData.code}] ${errorData.message}`);
},
},
},
});
react-query์ QueryClient๋ ์ฟผ๋ฆฌ๋ค์ defaultOptions ์ธํ
์ ํ ์ ์์ต๋๋ค.
์ฌ๊ธฐ์ ์ฃผ์ํ๊ฒ ๋ด์ผ ํ ์์ฑ์ throwOnError์
๋๋ค.
throwOnError๋ ๊ธฐ๋ณธ๊ฐ์ด false๋ฉฐ true๋ก ์ค์ ํ ๊ฒฝ์ฐ ๋ฐ์ํ ์๋ฌ๋ฅผ ์ ํํ๊ฒ ๋ฉ๋๋ค.
queries(useQuery)์์ ๋ฐ์ํ ์๋ฌ๋ throw ํ์ฌ ErrorBoundary๋ก ๋ฐ์๋ฒ๋ฆฌ๊ณ ,mutations(useMutation)์์ ๋ฐ์ํ ์๋ฌ๋ ์ ํ๋์ง ์๋๋ก ์ค์ ํ ํ ์คํธ ๋ฉ์ธ์ง๋ง ๋์์ค ๊ฒ๋๋ค.
๐ ๋ก๋ฉ ๋ฐ ์๋ฌ ์ฒ๋ฆฌ Component
๋ณธ๊ฒฉ์ ์ธ ์ปดํฌ๋ํธ ์ค๋ช ์ ์์ ๊ฐ๋จํ๊ฒ ๊ตฌ์กฐ์ ๋ฐฐ์นํ๋ ๋ฐฉ๋ฒ์ ๋ํด ๊ฐ๋ตํ๊ฒ ์ค๋ช ํ๊ฒ ์ต๋๋ค.
๐ ๊ตฌ์กฐ
const Boundray = ({ children }: { children: React.ReactElement }) => {
return (
<ErrorBoundary>
<Suspense>
{children}
</Suspense>
</ErrorBoundary>
);
};
- ๊ธฐ๋ณธ์ ์ผ๋ก ๋ฐ์ดํฐ๋ฅผ Fetching ํ๋ ์ปดํฌ๋ํธ๋ฅผ ๊ฐ์ธ๋ ๊ตฌ์กฐ๋ฅผ ๊ฐ๊ณ ์์ต๋๋ค.
- Suspense๋ก ๋ก๋ฉ, ErrorBoundary๋ก ์๋ฌ๋ฅผ ์ผ๊ด์ ์ผ๋ก ์ฒ๋ฆฌํฉ๋๋ค.
๐ ๋ฐฐ์น ๋ฐฉ๋ฒ
export const App = () => {
return (
<GlobalBoundary>
<FetchBoundary>
<ComponentA />
</FetchBoundary>
<FetchBoundary>
<ComponentB />
</FetchBoundary>
<FetchBoundary>
<ComponentC />
</FetchBoundary>
<FetchBoundary>
<ComponentD />
</FetchBoundary>
</GlobalBoundary>
);
};
GlobalBoundary- ์ต์๋จ์ ๋ฐฐ์นํด ํ๋ฉด ์ ์ฒด์ ๋ก๋ฉ, ์๋ฌ ๋์ฒด UI๋ฅผ ํ์ํฉ๋๋ค.
- ๋ฏธ์ฒ ์ฒ๋ฆฌํ์ง ๋ชปํ ์๋ฌ์ ๋ก๋ฉ ์ํ๋ฅผ ํก์ํ ์ ์์ต๋๋ค.
- ์ธ์ฆ์ ๋ํ ์ฒ๋ฆฌ๋ฅผ ๋ด๋นํฉ๋๋ค. ex) ๋ก๊ทธ์ธ ํ๋ฉด์ผ๋ก ์ด๋
FetchBoundary- ๊ฐ์ผ ์ปดํฌ๋ํธ์ ๋ก๋ฉ, ์๋ฌ ๋์ฒด UI๋ฅผ ํ์ํฉ๋๋ค.
- ๋์ฒด๋ก
useQueryํน์useSuspenseQuery๋ฅผ ์ฌ์ฉํ ์ปดํฌ๋ํธ์ ๊ฐ์์ค๋๋ค.
๐ ๋ ์ด์์ ์ฃผ์์
์์ ๊ฐ์ด ๋ฐฐ์นํด ๋๋ฉด ๋ ๋ ํ๊ฒ ๋์ฒด UI๋ฅผ ํ์ํ์ง๋ง ์ฃผ์ํด์ผ ํ ์ ์ด ์์ต๋๋ค.
๊ธฐ์กด ์ปดํฌ๋ํธ๋ค๊ณผ ๋์ฒด UI์์ ์คํ์ผ์ด ๋ฌ๋ผ ๊ธ๊ฒฉํ๊ฒ ๋ณํ๊ฒ ๋ ๊ฐ๋ฅ์ฑ์ด ์์ต๋๋ค.
์ ๋ ์ด๋ฌํ ๋ฌธ์ ๋ฅผ ๋ค์๊ณผ ๊ฐ์ ๋ฐฉ๋ฒ์ผ๋ก ํด๊ฒฐํ์ต๋๋ค.
- Boundary ์ปดํฌ๋ํธ์ ๋ ์ด์์ ์กฐ์ ์์ฑ์ Props๋ก ๋ฐ๊ธฐ
- ๋๋ฌด ์ข์ ๊ณต๊ฐ์ ์์นํ ์ปดํฌ๋ํธ์ ๊ฒฝ์ฐ ์์ด์ฝ๊ณผ ํธ๋ฒ ์นด๋๋ก ๋์ฒด UIํ์ํ๊ธฐ
- ์ปดํฌ๋ํธ์ ๋ ์ด์์ ๊ด๋ จ ์ฝ๋๋ฅผ ๋ฐ๊นฅ์ผ๋ก ๋นผ๋ด๊ธฐ
๋ฐฉ๋ฒ์ ์ ๋ง ๋ง์ผ๋ ์ ๋ง์ ๋ง๊ฒ ์์ ๋กญ๊ฒ ์ปค์คํ ํ๋ฉด ๋ฉ๋๋ค.
์ด ๊ธ์ ์์ ์์๋ ๊ธฐ๋ฅ ์ค์ฌ์ผ๋ก ์ค๋ช ์ ๋๋ฆฌ๊ธฐ ๋๋ฌธ์ ์ด ๋ถ๋ถ์ ์๋ตํ์ต๋๋ค.
๐ GlobalBoundary
import { Suspense } from 'react';
import { ErrorBoundary } from 'react-error-boundary';
import { GlobalErrorFallback } from './GlobalErrorFallback';
import { Loading } from '@components/Loading';
export const GlobalBoundary = ({ children }: { children: React.ReactNode }) => {
return (
<ErrorBoundary FallbackComponent={GlobalErrorFallback}>
<Suspense fallback={<Loading />}>{children}</Suspense>
</ErrorBoundary>
);
};
import { FallbackProps } from 'react-error-boundary';
import { getErrorDataByCode } from '@components/boundary/getErrorDataByCode';
import { useNavigate } from 'react-router-dom';
import { Button, Container } from '@components/atoms';
export const GlobalErrorFallback = ({ error, resetErrorBoundary }: FallbackProps) => {
const navigate = useNavigate();
const navigatePage = (to: string) => {
// resetErrorBoundary๋ฅผ ํธ์ถํ์ฌ ์๋ฌ๋ฅผ ์ด๊ธฐํ
resetErrorBoundary();
navigate(to);
};
const errorData = getErrorDataByCode(error);
return (
<Container.Column>
<h1>{errorData.code}</h1>
<h2>{errorData.message}</h2>
<Button onClick={() => navigatePage(errorData.requireLogin ? '/login' : '/main')}>
{errorData.requireLogin ? '๋ก๊ทธ์ธ ์ด๋' : '๋ฉ์ธํ๋ฉด ์ด๋'}
</Button>
</Container.Column>
);
};

GlobalErrorFallback์ ์ฃผ๋ชฉํด ์ฃผ์ธ์.
ErrorBoundary๊ฐ catch ํ error ๊ฐ์ฒด์ resetErrorBoundary๋ฅผ props๋ก ๋ฐ์์ต๋๋ค.
์๋ฌ ๋ฐ์ดํฐ๋ ์์์ ๋ง๋ getErrorDataByCode ํจ์์ error ๊ฐ์ฒด๋ฅผ ๋ฃ์ด์ฃผ์ด ๋ณํํฉ๋๋ค.
resetErrorBoundary๋ ErrorBoundary์์ ๋์ฒด UI๋ฅผ ๋์์ฃผ๋ ์ํ๋ฅผ ์ด๊ธฐํํ๋ ํจ์์
๋๋ค.
์๋ฌ ์ํ๋ฅผ ํด์์ํค๊ณ ๋ค๋ฅธ ๋์์ผ๋ก ์ด์ด๊ฐ๋ ค๋ฉด ๋ฐ๋์ ํธ์ถํด ์ฃผ์ธ์.

๐ FetchBoundary
import { Suspense } from 'react';
import { ErrorBoundary } from 'react-error-boundary';
import { FetchErrorFallback } from './FetchErrorFallback';
import { Loading } from '@components/Loading';
export const FetchBoundary = ({ children }: { children: React.ReactElement }) => {
return (
<ErrorBoundary FallbackComponent={FetchErrorFallback}>
<Suspense fallback={<Loading />}>{children}</Suspense>
</ErrorBoundary>
);
};
import { FallbackProps } from 'react-error-boundary';
import { Button, Container } from '../atoms';
import { getErrorDataByCode } from '@components/boundary/getErrorDataByCode';
import { useQueryErrorResetBoundary } from '@tanstack/react-query';
export const FetchErrorFallback = ({ error, resetErrorBoundary }: FallbackProps) => {
const { reset } = useQueryErrorResetBoundary();
const errorData = getErrorDataByCode(error);
// ์ธ์ฆ์ด ํ์ํ ์๋ฌ์ผ ๊ฒฝ์ฐ ์์ Boundary๋ก Error๋ฅผ ์ ํ
if (errorData.requireLogin) throw error;
const handleClickReset = () => {
resetErrorBoundary();
reset();
};
return (
<Container.Column>
<h1>{errorData?.code}</h1>
<h2>{errorData?.message}</h2>
<Button onClick={handleClickReset}>์ฌ์๋</Button>
</Container.Column>
);
};
import { FetchBoundary } from '@components/boundary/FetchBoundary';
import { Content } from '@components/Content';
import { Container } from '@components/atoms';
export const Error400 = () => {
return (
<>
<Container.Card>
<FetchBoundary>
<Content param="400" />
</FetchBoundary>
</Container.Card>
<Container.Card>
<FetchBoundary>
<Content param="4001" />
</FetchBoundary>
</Container.Card>
</>
);
};


- ๊ฐ ์ปดํฌ๋ํธ๋ง๋ค ๊ฐ๋ณ์ ์ธ ๋ก๋ฉ ๋ฐ ์๋ฌ ํ์๋ฅผ ๋ด๋นํฉ๋๋ค.
useQueryErrorResetBoundary์reset์ ์ฌ์ฉํด ์๋ฌ ๋ฐ์ ์ฟผ๋ฆฌ ์ฌํธ์ถ ๊ธฐ๋ฅ์ ๊ตฌํํ ์ ์์ต๋๋ค.
๐ ๋ง๋ฌด๋ฆฌ
์ฐ๋ค ๋ณด๋ ๊ธ์ด ๋๋ฌด ๊ธธ์ด์ง ๊ฒ ๊ฐ๋ค์.
๊ธฐ๋ฅ์ ์ ์ธ์ ์ผ๋ก ์ถ์ํํ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๊ฐ ๋ก๋ฉ, ์๋ฌ์ ๊ด๋ จ๋ ๋ฐ์ดํฐ๋ฅผ ์ค๊ฐ์์ ์ฒ๋ฆฌํ๊ธฐ ๋๋ฌธ์ ์ง์ ์ ์ธ ํ๋ฆ์ด ๋์ ์ต์ง ์์ ์ฒ์์ ์ด๋ ค์์ ๊ฒช์ ์๋ ์์ต๋๋ค.
๊ทธ๋ ์ง๋ง ๋ฒ๊ฑฐ๋ก์ด ๋ก๋ฉ ๋ฐ ์๋ฌ ์ฒ๋ฆฌ๋ฅผ ์ผ๋ฐํํด ๊ฐํธํ๊ฒ ์ฒ๋ฆฌํ ์ ์๋ค๋ ์ ์ด ๋๋ฌด๋๋ ๋งค๋ ฅ์ ์ด๊ธฐ ๋๋ฌธ์ ๋์ ์ ๊ฐ๋ ฅ ์ถ์ฒํฉ๋๋ค.
๋ง์ง๋ง์ผ๋ก ์ด ๊ธฐ๋ฅ์ ํ๋ก ํธ๋ง ์ํ๋ค๊ณ ์์ฑ๋๋ ๊ธฐ๋ฅ์ด ์๋๊ธฐ์ ๋ฐฑ์๋ ๊ฐ๋ฐ์์ ์ถฉ๋ถํ ํ์๋ฅผ ๊ฑฐ์น๋ฉฐ ์ ์ฐจ ์์ฑ๋๋ฅผ ๋์ฌ๊ฐ์ผ ํ๋ค๋ ๊ฒ์ ๊ผญ ๋ช ์ฌํ์ธ์.
๐ ์ ์ฒด ์ฝ๋
์ดํด๊ฐ ์ ๋์ง ์๋ ๋ถ๋ค์ ์ํด react+express๋ก ๊ตฌํํ ์ ์ฒด ์ฝ๋๋ฅผ ๊นํ๋ธ์ ์ฌ๋ ค๋์์ต๋๋ค.
์ฝ๋๋ฅผ ๋ฐ์ ์ํ๋ค์ ํ๋ฆ์ ์ง์ ์ฒดํํด ๋ณด์๋ ๊ฒ์ ์ถ์ฒ๋๋ฆฝ๋๋ค.
https://github.com/devlasbe/error-boundary
GitHub - LasBe-code/error-boundary: ๊ฐ๋จํ Express API๋ฅผ ์ด์ฉํ React ErrorBoundary ํ ์คํธ์ ๋๋ค.
๊ฐ๋จํ Express API๋ฅผ ์ด์ฉํ React ErrorBoundary ํ ์คํธ์ ๋๋ค. Contribute to LasBe-code/error-boundary development by creating an account on GitHub.
github.com
์คํ ์ฑํ
๋๊ธ