๐ 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/LasBe-code/error-boundary
'React' ์นดํ ๊ณ ๋ฆฌ์ ๋ค๋ฅธ ๊ธ
Github-Actions Playwright Test Report ๊ฒฐ๊ณผ PR Commnet๋ก ๋จ๊ธฐ๊ธฐ (0) | 2024.12.01 |
---|---|
React ์นด๋ ์ธํฐ๋์ ์ ๋๋ฉ์ด์ ๋ผ์ด๋ธ๋ฌ๋ฆฌ (0) | 2024.09.21 |
Electron + React ํ๋ก์ธ์ค ๊ฐ IPC ํต์ ๊ณผ preload ์ฌ์ฉ ๋ฐฉ๋ฒ (0) | 2024.05.13 |
[React] ์ธ์ ์ฌ์ด ๋ฆฌ์กํธ ๋ชจ๋ฌ ๋ผ์ด๋ธ๋ฌ๋ฆฌ (0) | 2024.05.07 |
[Electron, React, TS] ๋ฐ์คํฌํฑ ์ฑ ํ๋ก์ ํธ ์์ฑํ๊ธฐ (4) | 2024.04.01 |
๋๊ธ