๐ฅ 16. ๋น๋๊ธฐ UX์ ํจ๋ฌ๋ค์ ์ ํ (Suspense & ErrorBoundary)
๐ ๊ฐ์
๋ก๋ฉ๊ณผ ์๋ฌ ์ฒ๋ฆฌ ์ฑ ์์ ๋ถ๋ชจ ๋ ์ด์ด๋ก ์๋ฒฝํ ๋ ๋๊ธฐ๊ณ , ์์ ์ปดํฌ๋ํธ๋ '๋ฐ์ดํฐ๊ฐ 100% ์กด์ฌํ ๋์ ์ฑ๊ณตํ UI'๋ง ๋จ๊ฒจ๋๋ ๊ทน๊ฐ์ ์ ์ธ์ ๋น๋๊ธฐ ์ฒ๋ฆฌ ๊ธฐ๋ฒ์ ๋ฐฐ์๋๋ค.
๐ฏ ์ด ์น์ ์ ์ฝ๊ณ ๋๋ฉด:
if (isLoading)๊ณผif (isError)๋ก ๋๋ฐฐ๋ ๋์ฐํ ์ปดํฌ๋ํธ๋ฅผ ๊ตฌ์ถํ ์ ์๋ค.- ์ปดํฌ๋ํธ์ ์ฑ ์์ "๋ฐ์ดํฐ ๋ ๋๋ง" ํ๋๋ก๋ง ๊ทน๋จ์ ์ผ๋ก ์ขํ๋ ์ ์ธ์ (Declarative) ๋น๋๊ธฐ ์ฒ๋ฆฌ์ ๋ง๋ฒ์ ์ดํดํ๋ค.
- ์ฑ ์ด๋์ ์๋ฌ๊ฐ ํฐ์ง๋ ์ฑ ์ ์ฒด๊ฐ ํ์๊ฒ ์ฃฝ์ง ์๊ณ (ํ์ดํธ์์), ์ฐ์ํ๊ฒ ๋ถ๋ถ ์๋ฌ ํ๋ฉด์ ๋์์ฃผ๋ ์ํคํ ์ฒ๋ฅผ ์ค๊ณํ ์ ์๋ค.
๐ ๋ชฉ์ฐจ
- ๐ค ์ ์์์ผ ํ๋๊ฐ: ๋ช ๋ นํ ๋ก๋ฉ/์๋ฌ ์ฒ๋ฆฌ์ ์ฌ์
- ๐๏ธ ๋น์ ๋ก ๋จผ์ ์ดํดํ๊ธฐ
- ๐งฉ ๊ทน์ ์ฒด๋: ์ ์ธ์ ๋น๋๊ธฐ UX ์ค๊ณํ๊ธฐ
- ๐ (์ฌํ ๋ณด๋์ค) Next.js App Router์์ ๋ง๋จ
๐ ์ด ๋ฌธ์๋ฅผ ์ฝ๊ธฐ ์ ์
โฑ๏ธ ์์ ์ฝ๊ธฐ ์๊ฐ: 12๋ถ / ํต์ฌ ํํธ: 7๋ถ
๐บ๏ธ ์ด ๋ฌธ์์ ๋ฐฐ๊ฒฝ ์ธ๊ณ๊ด: '์์๋ค ์ปค๋ฎค๋ํฐ'
- ์์(PM):"์์ฒ ๋, ์ ์ ํ๋กํ ํ์ด์ง ๋ค์ด๊ฐ์ ๋ ๋ก๋ฉ ๋น๊ธ๋น๊ธ ๋๋ ์คํผ๋ ๋ง์ด์์. ์ง๊ธ์ ์ ์ฒด ํ๋ฉด์ ๊ฐ๋ฆฌ๋๋ฐ, ํ๋กํ ์ฌ์ง ์ชฝ๋ง ๋ฐ๋ก ๋๊ณ , ์์ฑํ ๊ธ ๋ชฉ๋ก ์ชฝ ๋ฐ๋ก ๋๊ฒ ์ค๋ฌด์คํ๊ฒ ๋ง๋ค์ด์ฃผ์ค ์ ์๋์? ์์ฐธ, ๊ธ ๋ชฉ๋ก ๋ถ๋ฌ์ค๋ค ์๋ฌ ๋๋ฉด ๊ฑฐ๊ธด '๋ค์ ์๋' ๋ฒํผ ๋ฌ์์ฃผ์๊ณ ์!"
- ์์ฒ (์ ์
):"๋ค? ๊ทธ๋ผ
ProfileAvatar์ปดํฌ๋ํธ ์์๋isLoading,isError์ํ ๋ง๋ค๊ณ ...PostList์ปดํฌ๋ํธ ์์๋isLoading,isError์ํ ๋ง๋ค์ด์ ๋คif (isLoading) return <Spinner/>๋ฐ์์ผ๊ฒ ๋ค์... ์ฝ๋๊ฐ 3๋ฐฐ๋ก ๋ถ์ด๋ ํ ๋ฐ ใ ใ " - ์ํธ(๋ฆฌ๋): "์์ฒ ๋, ๋ ์ปดํฌ๋ํธ ์์ ์กฐ๊ฑด๋ฌธ ์ง๋ขฐ๋ฅผ ๊น๊ณ ๊ณ์๊ตฐ์. ์ปดํฌ๋ํธ๋ '๋ฐ์ดํฐ๊ฐ ์ค๋ฉด ์ด๋ป๊ฒ ์์๊ฒ ๊ทธ๋ฆด๊น?' ๋จ ํ๋์๋ง ์ง์คํด์ผ ํฉ๋๋ค. ์ก๋คํ ์๋ฌ์ ๋ก๋ฉ ์ฒ๋ฆฌ๋ ๋ถ๋ชจํํ ๋์ ธ๋ฒ๋ฆฌ์ธ์. Suspense ์ ErrorBoundary ๋ก ๊ตฌ์ถํด ๋๋ฆฌ๊ฒ ์ต๋๋ค."
๐ค ์ ์์์ผ ํ๋๊ฐ: ๋ช ๋ นํ ๋ก๋ฉ/์๋ฌ ์ฒ๋ฆฌ์ ์ฌ์
1~3๋ ์ฐจ ํ๋ก ํธ์๋ ๊ฐ๋ฐ์๋ค์ ์ฝ๋๋ฅผ ๋ณด๋ฉด ์ญ์คํ๊ตฌ ์๋์ ๊ฐ์ ํํ๋ฅผ ๋๋๋ค.
// โ ์์ฒ ์ด์ ํํ ๋ช
๋ นํ ๋น๋๊ธฐ ์ปดํฌ๋ํธ (๊ด์ฌ์ฌ ํผ์ฌ)
function UserProfile({ userId }) {
const [data, setData] = useState(null);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
fetchUserData(userId)
.then(res => setData(res))
.catch(err => setError(err))
.finally(() => setIsLoading(false));
}, [userId]);
// ๐จ UI๋ฅผ ๊ทธ๋ฆฌ๋ ๋ณธ์ฐ์ ์๋ฌด ์ ์ ๋ฐฉ์ด๋ง(์กฐ๊ฑด๋ฌธ)์ด ๋๋ฌด๋๋ ๋ง๋ค!
if (isLoading) return <Spinner />;
if (error) return <ErrorMessage message={error.message} />;
// ์ฌ๊ธฐ๊น์ง ์ด์๋จ์์ผ ๊ฒจ์ฐ ์ง์ง ๋ชฉ์ (UI ๋ ๋๋ง) ๋ฌ์ฑ ๊ฐ๋ฅ
return <div>ํ์ํฉ๋๋ค, {data.name} ๋!</div>;
}์ด ๋ฐฉ์์ ์น๋ช ์ ์ธ ๋ฌธ์ ์ ์ ๋ค์๊ณผ ๊ฐ์ต๋๋ค.
- ๊ด์ฌ์ฌ ์ค์ผ: ์ปดํฌ๋ํธ์ ๋ณธ์ง์ "๋ฐ์ดํฐ๋ฅผ ์์๊ฒ UI๋ก ๋งตํํ๋ ๊ฒ"์ธ๋ฐ, ๋ฐ์ดํฐ๋ฅผ ์ด๋ป๊ฒ ๊ฐ์ ธ์ฌ์ง, ์คํจํ๋ฉด ์ด๋กํ ์ง ์จ๊ฐ ์ก์ผ์ ์๋ฌ๋ฆฝ๋๋ค.
- **"์ด ๋ฐ์ดํฐ๋ ์ง์ง ์๋๊ฐ?":**TypeScript๋ก ๋ฌ์๋ด๋
data๋ ์ด๊ธฐ์null์ ๋๋ค. ๋ฐ๋ผ์ ๋ฐ์์data.name์ ์ธ ๋๋ง๋ค ๋ถ์์ฆ์ธ(Optional Chainingdata?.name)์ ์๋ฌ๋ฆฌ๊ฒ ๋ฉ๋๋ค. - UI ๊ธฐํ ๋ณ๊ฒฝ ๋์ ๋ถ๊ฐ: PM์ด "๋ก๋ฉ ๋ฐ๋ฅผ ์ ์์ชฝ ํค๋๋ก ํฉ์ณ์ฃผ์ธ์"๋ผ๊ณ ํ๋ฉด, ๋ก๋ฉ ์ํ๋ฅผ ๋ ๋ถ๋ชจ๋ก ๋์ด์ฌ๋ฆฌ๋ฉฐ(Lifting State Up) ๋๊ณต์ฌ๋ฅผ ํด์ผ ํฉ๋๋ค.
๐๏ธ ๋น์ ๋ก ๋จผ์ ์ดํดํ๊ธฐ
๐ง 5์ด์๊ฒ ์ค๋ช ํ๋ค๋ฉด?
- **๊ธฐ์กด ๋ฐฉ์ (์์ฒ ์ด ํ๋ฒ๊ฑฐ ๊ฐ๊ฒ):**์๋ฆฌ์ฌ(์ปดํฌ๋ํธ)๊ฐ ํจํฐ๋ฅผ ๊ตฝ๋ค๊ฐ ๊ณ ๊ธฐ๊ฐ ๋จ์ด์ง๋ฉด(๋ก๋ฉ), ์๊ธฐ๊ฐ ์ง์ ์๋ฆฌ๋ฅผ ๋ฉ์ถ๊ณ ํ์ ๋๊ฐ "์๋, ๊ณ ๊ธฐ ์ค๋ ์ค ใ ใ " ํป๋ง์ ๋ญ๋๋ค. ๊ณ ๊ธฐ๊ฐ ์ํ์ผ๋ฉด(์๋ฌ) "์๋ ๊ณ ๊ธฐ ์ํจ!" ํป๋ง์ ๋ค๊ณ ์. ์๋ฆฌ์ ์ง์คํ ์๊ฐ ์์ฃ .
- Suspense & ErrorBoundary ๋ฐฉ์ (์ํธ์ ๊ณ ๊ธ ๋ ์คํ ๋): ์๋ฆฌ์ฌ๋ "๋ ๋ฌด์กฐ๊ฑด ๊ณ ๊ธฐ(๋ฐ์ดํฐ)๊ฐ ์๋ค๊ณ ๊ฐ์ ํ๊ณ ํ๋ฒ๊ฑฐ ์กฐ๋ฆฝ๋ง ํ๋ค" ๋ผ๊ณ ์ ์ธํฉ๋๋ค.
๋ง์ฝ ๊ณ ๊ธฐ๊ฐ ์์ง ์ ์์ผ๋ฉด? ์๋ฆฌ์ฌ๋ ์ฃผ๋ฐฉ ๋ฐ์ผ๋ก ์๋ฆฌ๋ฅผ ์ง๋ฅด๋ฉฐ ์ฐ๋ฌ์ง๋๋ค(Throw). ๊ทธ๋ผ ๋งค๋์ (Suspense)๊ฐ ํ์ด๋์์ ์๋์๊ฒ ์ฐ์ํ๊ฒ ์์ ๋นต(Spinner)์ ๋์ ํ๋ฉฐ ๊ธฐ๋ค๋ฆฌ๊ฒ ํฉ๋๋ค.
๋ง์ฝ ๊ณ ๊ธฐ๊ฐ ์ํ์ผ๋ฉด? ์ญ์ ์๋ฆฌ์ง๋ฅด๋ฉฐ ์ฐ๋ฌ์ง๊ณ , ์ ์ฅ(ErrorBoundary)์ด ๋ฐ์ด๋์ ์ ์คํ๊ฒ ์ฌ๊ณผ ์ฟ ํฐ(์๋ฌ UI)์ ๋๋ฆฝ๋๋ค. ์๋ฆฌ์ฌ๋ ์ค์ง '์๋ฆฌ'๋ผ๋ ๋ณธ์ง์๋ง ์ง์คํฉ๋๋ค!
์ด๊ฒ์ด ๋ฆฌ์กํธ๊ฐ ๋ฐ๊ณ ์๋ "๋์์ ํจ๊ณผ(Algebraic Effects)" ์ ๋ฉํ ๋ชจ๋ธ์ ๋๋ค. ์์ ์ปดํฌ๋ํธ์์ ๊ฐ๋นํ ์ ์๋ ์ฌ์ด๋ ์ดํํธ(๋ก๋ฉ, ์๋ฌ)๋ฅผ ๋ถ๋ชจ๊ฐ ์บ์นํ์ฌ ๋์ ์ฒ๋ฆฌํ๋ ๊ฒ ์ด์ฃ .
๐งฉ ๊ทน์ ์ฒด๋: ์ ์ธ์ ๋น๋๊ธฐ UX ์ค๊ณํ๊ธฐ
์์ฒ ์ด์ ๋์ฐํ๋ ์ฝ๋๋ฅผ Suspense ์ ErrorBoundary (React 18 & Next.js ์๋์ ํ์ค ๊ต์)๋ฅผ ์จ์ ๋ง๊ฐ์กฐํด ๋ด
์๋ค. (ํ์
์์๋ React Query์ useSuspenseQuery ๋ฑ๊ณผ ๊ฒฐํฉํด ๋ง์ด ์ฌ์ฉํฉ๋๋ค.)
โ 1๋จ๊ณ: ์ํธ์ ์ ์ธ์ ์ปดํฌ๋ํธ ์ค๊ณ
์ฐ์ ์์ ์ปดํฌ๋ํธ๋ ์๋ฌ๊ณ ๋๋ฐ์ด๊ณ ๋ค ๋ฒ๋ฆฌ๊ณ ํต์ฌ ๋ฐ์ดํฐ๋ง ๋ณด๊ณ ๋์งํ๋๋ก ๋๋ฅผ ๋น์๋๋ค.
// ๐ฏ 1. ๋๋ฅผ ๋น์ด ์ ์ธ์ ์์ ์ปดํฌ๋ํธ (์ฑ๊ณตํ ๋ฏธ๋๋ง ๋ฐ๋ผ๋ณธ๋ค)
function UserProfile({ userId }) {
// isLoading? isError? ๊ทธ๋ฐ ๊ฑด ๋ถ๋ชจ๋์ด ์์์ ํ์ญ๋๋ค.
// ๋๋ '๋ฐ์ดํฐ๊ฐ ๋ฌด์กฐ๊ฑด ์๋ฒฝํ๊ฒ ์จ ์ํ'๋ผ๊ณ ๊ฐ์ ํฉ๋๋ค!
const { data } = useSuspenseQuery(fetchUserData, userId);
// optional chaining(data?.name) ๋ฐ์๋ ๊ฐ๋ ์ค๋๋ค. ๋ฐ์ดํฐ๋ ์ธ์ ๋ 100% ์์ต๋๋ค.
return (
<div className="profile-card">
<img src={data.profileImage} />
<h3>ํ์ํฉ๋๋ค, {data.name} ๋!</h3>
</div>
);
}โ 2๋จ๊ณ: ๋งค๋์ (Suspense)์ ์ ์ฅ(ErrorBoundary) ๋ฐฐ์น
์์ ์ปดํฌ๋ํธ์์ ๋ฐ์ดํฐ๋ฅผ ๊ฐ์ ธ์ค๋ ๋์ ๋์ง(Throw) Promise ํ๋ฉ ์ํ์ Error ๋ฆฌ์ ์
์ํ๋ฅผ ๋ถ๋ชจ ๊ณ์ธต์์ ์ก์์ฑ๋๋ค.
// ๐ฏ 2. ๋น๋๊ธฐ ์ฑ
์์ ๋ชจ๋ ๋ ์์ ๋ถ๋ชจ ์ปจํ
์ด๋
import { Suspense } from 'react';
import { ErrorBoundary } from 'react-error-boundary'; // ์ธ๋ถ ๋ผ์ด๋ธ๋ฌ๋ฆฌ ์ฌ์ฉ ๊ฐ๋ ฅ ๊ถ์ฅ
function UserPage({ userId }) {
return (
<div className="page-layout">
<h1>์ ์ ํ์ด์ง</h1>
{/* ๐ด ์ ์ฅ: ๋ง์ฝ ์์์ ์น๋ช
์ ์๋ฌ๊ฐ ๋๋ฉด ์ด UI๋ก ๋ฎ์ด์์! */}
<ErrorBoundary fallback={<div>ํ๋กํ์ ๋ถ๋ฌ์ค๋ค ์๋ฒ๊ฐ ํฐ์ก์ต๋๋ค ๐ญ</div>}>
{/* ๐ก ๋งค๋์ : ์์์ ์์ง ๋ฐ์ดํฐ๋ฅผ ๊ธฐ๋ค๋ฆฌ๋ ์ค์ด๋ฉด ์ด Spinner๋ฅผ ๊ทธ๋ ค์ค! */}
<Suspense fallback={<Spinner size="large" />}>
{/* ์ฑ๊ณตํ ๋ฏธ๋๋ง ๊ทธ๋ฆฌ๋ ์๋ฆฌ์ฌ ์ปดํฌ๋ํธ */}
<UserProfile userId={userId} />
</Suspense>
</ErrorBoundary>
</div>
);
}โ 3๋จ๊ณ: ๊ธฐํ ๋ณ๊ฒฝ? ๋ ๊ณ ์กฐ๋ฆฝํ๋ฏ ์ฑ์น!
"์์ฒ ์จ, ํ๋กํ ๋ง๊ณ ํ๋จ์ ์๋ '์ต๊ทผ ์์ฑ ๊ธ ๋ชฉ๋ก'๋ ๋ฐ๋ก ๋ก๋ฉ ๋๊ฒ ํด์ฃผ์ธ์~"
๊ณผ๊ฑฐ์ ๋ด๋ถ ์ฝ๋๋ฅผ ๋ค ์ฐข์๊ฒ ์ง๋ง, ์ง๊ธ์ ์์ ์ปดํฌ๋ํธ๋ค์ ๊ฑด๋๋ฆฌ์ง ์๊ณ ๋ถ๋ชจ์ ๋ ์ด์์ ๊ป๋ฐ๊ธฐ๋ง ํ๋ํ๋ฉด ๋ฉ๋๋ค.
// ๐ฏ 3. ๊ทนํ์ ๋ถ๋ฆฌ ๋์ ์ป๊ฒ ๋ ์๋ฒฝํ UI ๋ ์ด์์ ํต์ ๊ถ
function UserPage({ userId }) {
return (
<>
<ErrorBoundary fallback={<TextError />}>
{/* ํ๋กํ์ ํ๋กํ๋๋ก ๋ฐ๋ก ๋น๊ธ๋น๊ธ ๋๊ณ ~ */}
<Suspense fallback={<ProfileSkeleton />}>
<UserProfile userId={userId} />
</Suspense>
</ErrorBoundary>
<ErrorBoundary fallback={<Button>๊ธ ๋ชฉ๋ก ๊ฐฑ์ ์คํจ. ์ฌ์๋</Button>}>
{/* ๊ฒ์๋ฌผ ๋ชฉ๋ก์ ๋ ๊ฑ๋ค๋๋ก ๋ฐ๋ก ๋น๊ธ๋น๊ธ ๋๋๋ค! (๋ณ๋ ฌ ๋ก๋ฉ) */}
<Suspense fallback={<ListSkeleton />}>
<UserPosts userId={userId} />
</Suspense>
</ErrorBoundary>
</>
);
}์ด ๊ตฌ์กฐ ๋๋ถ์ ํน์ ๊ตฌ์ญ(์: ๊ธ ๋ชฉ๋ก)์์ ์๋ฒ ์๋ฌ๊ฐ ํฐ์ ธ๋, ํ๋กํ ๋ ๋๋ง ๋ฐ์ค๋ ๋๋ก์์ด ์ด์๋จ์ต๋๋ค. ํ์ดํธ์์(์ฑ ์ ์ฒด๊ฐ ํ์๊ฒ ์ฃฝ๋ ํ์)์ ์๋ฒฝํ ๋ฐฉ์ดํ๋ ๋ถ๋ถ ์ฅ์ ๊ฒฉ๋ฆฌ ์์คํ ์ด ์์ฑ๋ ๊ฒ์ ๋๋ค.
๐ (์ฌํ ๋ณด๋์ค) Next.js App Router์์ ๋ง๋จ
Next.js์ ์ ํ App Router(app/ ๋๋ ํ ๋ฆฌ)๋ ์ด Suspense ์ ErrorBoundary ์ ๊ฐ๋
์ ์ฐ์ฃผ ๋๊น์ง ํ์ผ ์์คํ
์ผ๋ก ๋์ด์ฌ๋ ธ์ต๋๋ค.
- Next.js์์
loading.tsxํ์ผ์ ๋ง๋ค๋ฉด? ๐ ์๋์ผ๋ก ๊ฐ์ฅ ๊ฐ๊น์ดlayout.tsx๋ฐ์<Suspense fallback={<Loading />}>๊ป๋ฐ๊ธฐ๋ฅผ ๋ชฐ๋ ์์์ค๋๋ค! error.tsxํ์ผ์ ๋ง๋ค๋ฉด? ๐ ๋ชฐ๋<ErrorBoundary fallback={<Error />}>๊ป๋ฐ๊ธฐ๋ฅผ ์์์ค๋๋ค!
์ฐ๋ฆฌ๊ฐ ์ฌํ ์์ผ๋ก ์ง๋ ๋ณต์กํ ๋ถ๋ชจ ์ปดํฌ๋ํธ ๋ํ ์ฝ๋๋ฅผ, ๊ทธ์ ํ์ผ์ ๊ทธ ์์น์ ๋์ ธ๋๋ ๊ฒ๋ง์ผ๋ก 100% ๋ผ์ฐํ ์์ค์์ ๋ํํด ์ฃผ๋ ๊ฒ์ ๋๋ค. (์ด ๊ธฐ๋ฒ๋ค์ด ๋ฑ์ฅํ ๊ทผ๋ณธ ์๋ฆฌ๋ฅผ ์๋ฉด, Next.js ๊ณต์ ๋ฌธ์๊ฐ ์ผ๋ง๋ ํธ์์ฑ์ ๊ทน๋ํํ๋์ง ๋ผ์ ๋ฆฌ๊ฒ ์ฒด๊ฐํ๊ฒ ๋ฉ๋๋ค.)
๐ ์ด๋ฒ์ ๋ฐฐ์ด ๋ด์ฉ ์ด์ ๋ฆฌ
| ๊ด์ | ๋ช
๋ นํ ํจ๋ฌ๋ค์ (if (loading)) | ์ ์ธ์ ํจ๋ฌ๋ค์ (Suspense) |
|---|---|---|
| ์๋ฌ/๋ก๋ฉ ์ฒ๋ฆฌ ์ฃผ์ฒด | ์์ ์ปดํฌ๋ํธ ๋ณธ์ธ | ๋๋ฅผ ๊ฐ์ธ๊ณ ์๋ ๋ถ๋ชจ ์ปจํ ์ด๋ ๊ณ์ธต |
| ์ฝ๋์ ๊ด์ฌ์ฌ | ๋ก์ง(Fetch) + ๋ฐฉ์ด๋ง(If๋ฌธ) + ๊ตฌ์กฐ(UI) ๋ค์์ | ์ค์ง ํต์ฌ ๋ฐ์ดํฐ UI ๋ ๋๋ง ํ๋์๋ง ์ง์ค |
| ํ์ ์์ ์ฑ | ์คํฌ๋ฆฐ์ ๋ฐ ๋๊น์ง ๋ฐ์ดํฐ๊ฐ null์ผ ์ ์์ | ๋ ๋๋ง ๋๋ ์๊ฐ ๋ฐ์ดํฐ๋ ๋ฌด์กฐ๊ฑด ์กด์ฌํจ 100% ๋ณด์ฅ |
| ๋ ์ด์์ ์์ ๋ | ๊ธฐํ์ด ๋ฐ๋๋ฉด ๋ก์ง ์ฝ๋ ์ ์ฒด๋ฅผ ๊ฐ์์์ด์ผ ํจ | ์ปดํฌ๋ํธ๋ ๋๋๊ณ ๋ฐ๊นฅ Suspense ์์น๋ง ์กฐ๋ฆฝํ๋ฉด ๋จ |
๐ก ํ ์ค๋ก ๊ธฐ์ตํ๊ธฐ
"์ปดํฌ๋ํธ๋ ๋๋ฌผ์ ํ๋ฆฌ์ง ์๋๋ค."
์ํ๊ณ ํ๋ ๊ฒ(๋ก๋ฉ, ์๋ฌ)์ ๋ด๋ถ์์ ์ก๋์ฌ๋์ฒ๋ผ ์ฒ๋ฆฌํ์ง ๋ง๊ณ ์ํ์ ์ผ๋ก ๋ฐ์ผ๋ก ๋ด๋์ ธ๋ผ(Throw). ์ฐ์ํ ๋งค๋์ (Suspense,ErrorBoundary)๊ฐ ๋ฐ์์ ํผํผํ ์์๋ฅผ ์ณ์ค ๊ฒ์ด๋ค.
๐ฃ ์์ฒ ์ด์ ํด๊ทผ ์ผ๊ธฐ
์ค๋ ์ง์ง ๋จธ๋ฆฌ ํ ๋ ์ธ๊ฒ ๋ง์ ๊ธฐ๋ถ์ด๋ค. ๋ํํ ์ปดํฌ๋ํธ๋ ๊ฑด ๋ฌด์กฐ๊ฑด ์์์ ๋ถ์น๊ณ ์ฅ๊ตฌ์น๊ณ ์๋ฌ ์ก๊ณ ๋ก๋ฉ ๋ฐ ๋์ฐ๋ ๋ง๋ฅ ์ผ๊พผ์ด์๋๋ฐ, ๊ทธ๊ฑธ ๋ถ๋ชจํํ ํต์งธ๋ก ๋์ ธ๋ฒ๋ฆฌ๋(Throw) ๋์์ ํจ๊ณผ(Algebraic Effects)๋ผ๋...!
๐ก "์ปดํฌ๋ํธ๋ ๋๋ฌผ์ ํ๋ฆฌ์ง ์๋๋ค. ๋ก๋ฉ๊ณผ ์๋ฌ๋ผ๋ ๊ณจ์นซ๊ฑฐ๋ฆฌ๋ ๋ถ๋ชจ(Suspense, ErrorBoundary)์๊ฒ ๋์ง๊ณ ์ฑ๊ณตํ UI ์ฐ์ถ์๋ง ๋ชฐ๋นตํ์."
์ํธ ๋ฆฌ๋ ๋์ด ๊ณ ๊ธ ๋ ์คํ ๋ ์ฃผ๋ฐฉ ๋น์ ๋ฅผ ๋ค์ด์ฃผ์
จ์ ๋ ๊น๋ ค์๋ ์๋ง์ if ๋ฌธ๋ค์ด ๋ง์น ๋๋ฌ์ด ์ ์์ฒ๋ผ ๋๊ปด์ก๋ค. Next.js App Router๊ฐ ์ loading.tsx๋ error.tsx ํ์ผ๋ง ๋ก ๋ง๋ค์ด๋๋ฉด ์์์ ๋๋์ง๋ ์ด์ ์ผ ์๋ฒฝํ๊ฒ ์ดํด๊ฐ ๊ฐ๋ค. ๊ธฐ์ ์ ์ง๋ณด๋ฅผ ๋์์์ ๋ณธ ๊ธฐ๋ถ! ํ
์
์ฌ๋ผ์จ ๊น์ ์ค๋ ๋ฐค์ ์๋ฌ ๋ฐ์ด๋๋ฆฌ ๋ผ์ด๋ธ๋ฌ๋ฆฌ ์์ค์ฝ๋ ์ข ๋ฏ์ด๋ณด๋ค ์์ผ๊ฒ ๋ค.
๐ ๋ง๋ฌด๋ฆฌ ํด์ฆ
Q1. Suspense์ ErrorBoundary๋ฅผ ํ์ฉํ์ฌ ๋น๋๊ธฐ ์ฒ๋ฆฌ๋ฅผ ์ ์ธ์ ์ผ๋ก ๋ถ๋ฆฌํ์ ๋, ์์ ์ปดํฌ๋ํธ(์๋ฆฌ์ฌ)์ ๋ด๋ถ ์ฝ๋ ์ง๋์ด ๊ทน๋จ์ ์ผ๋ก ๊ฐ๋ฒผ์์ง๋ ์ํคํ
์ฒ์ ์ด์ ๋ ๋ฌด์์ธ๊ฐ์?
- A) ํด๋์ค ์ปดํฌ๋ํธ์ LifeCycle ๋ฉ์๋๋ฅผ Hooks๋ก ์ค๋ฒ๋ผ์ด๋ฉํ๋ฉด์ ์ฝ๋ฐฑ ์ง์ฅ์ด ํด์ง๊ธฐ ๋๋ฌธ.
- B) ์์ ์ปดํฌ๋ํธ ๋ด๋ถ์ ์กด์ฌํ๋
isLoading,isError์ํ ๋ณ์์ ๊ทธ์ ๋ฐ๋ฅธ ๋ฐฉํจ๋ง์ด ๋ถ๊ธฐ(if) ์ฝ๋๋ค์ด ํต์งธ๋ก ๋ฏ๊ฒจ๋๊ฐ๋ฉฐ ์ปดํฌ๋ํธ์ ์ ์ผํ ์ญํ ์ธ '๋ฐ์ดํฐ ์กด์ฌ ์์ UI ๋งคํ'๋ง ์์ํ๊ฒ ๋จ๊ธฐ ๋๋ฌธ. - C)
Suspense๊ตฌ๋ฌธ ๋ด๋ถ์ ์์๋ค์ React ๊ฐ์ ๋(VDOM) ๋ณํ์ ๊ฑฐ์น์ง ์๊ณ ์ง์ ๋ธ๋ผ์ฐ์ ๋ค์ดํฐ๋ธ ์ฝ๋๋ก C++ ์ปดํ์ผ๋์ด ๊ทธ๋ ค์ง๊ธฐ ๋๋ฌธ.
โ ์ ๋ต: B
๐ก ์์ธ ํด์ค: ์ ํํฉ๋๋ค! ๊ธฐ์กด ํจ๋ฌ๋ค์์์ ๊ฐ๋ฐ์๋ ํ ์ง๋ถ ํ ๊ฐ์กฑ ์์์ "์์ง ์ค์ง ์์ ๋ฐ์ดํฐ", "๋ง๊ฐ์ง ๋ฐ์ดํฐ", "์ ์์ ์ธ ๋ฐ์ดํฐ" 3๊ฐ์ง ํํ์ฐ์ฃผ๋ฅผ ๋ชจ๋ if-else ๋ฌธ์ผ๋ก ์ผ์ดํด์ผ ํ์ต๋๋ค. ํ์ง๋ง ์ฑ
์์ ์์(Throw)ํจ์ผ๋ก์จ ์ปดํฌ๋ํธ๋ ์ค์ง "๋ฐ์ดํฐ๊ฐ ๋์ฐฉํ ์๋ฆ๋ค์ด 1๊ฐ ์ฐ์ฃผ"์ ์ผ๊ตดํ์ ๋ง ๊ทธ๋ฆด ์ ์๊ฒ ๋์๊ณ , ํญ๋ฐ์ ์ธ ๊ฐ๋
์ฑ ์์น์ ์ด๋ค๋์ต๋๋ค.
Q2. ์ฌ๋ด ํ๋ก์ ํธ์์ ๋ง์ดํ์ด์ง๋ฅผ ๋ฆฌํฉํ ๋ง ์ค์
๋๋ค. ์ ์ ํ๋กํ ์น์
๊ณผ ๊ฒฐ์ ๋ด์ญ ์น์
, ๊ทธ๋ฆฌ๊ณ ์ถ์ฒ ์ํ 3๊ฐ์ ํํธ๊ฐ ๋ชจ๋ ๋
๋ฆฝ์ ์ธ ์ธ๋ถ API๋ฅผ ๊ธ์ด์ค๊ณ ์์ต๋๋ค. ๋ค์ ์ค Suspense์ ErrorBoundary๋ฅผ ์ค๊ณํ๋ ์๋์ด์ ๊ฐ์ฅ ๋ฐ๋์งํ ์์ผ๋?
- A) 3๊ฐ์ ์น์
์ ์ ๋ถ ํ๋์ ๊ฑฐ๋ํ
<Suspense>์<ErrorBoundary>๋ก ์ต์๋จ ๋ ์ด์์์ ๋ชฝ๋ ๋ฌถ์ด์ ๋จ์ผ ์์คํ ์ผ๋ก ๋ง๋ ๋ค. (ํ๋๋ผ๋ ๋ก๋ฉ ์ค์ด๋ฉด ํ๋ฉด ์ ์ฒด ํฝ์ด ๋๋ฆฌ๊ธฐ) - B) ๊ฐ 3๊ฐ์ ์ปดํฌ๋ํธ ํ์ผ ์์ ์ง์ ๋ค์ด๊ฐ์
catch๊ตฌ๋ฌธ์ ํ๊ณ , ์คํผ๋ ๋์ฐ๋if์ฝ๋๋ฅผ ์ฑ์ฌ๋ฃ์ด ์๊ธ์์กฑํ๊ฒ ํ๋ค. - C) ๊ฐ ์น์
์ ๊ฐ์ธ๋
<Suspense>3๊ฐ์<ErrorBoundary>3๊ฐ๋ฅผ ๋ถ๋ชจ ๋ ์ด์์์์ ๊ฐ๊ฐ ๋ ๋ฆฝ(๋ณ๋ ฌ)์ ์ผ๋ก ์์์ค๋ค. (ํ๋กํ์ ์ผ์ฐ ๋จ๋ฉด ๋จผ์ ๋ณด์ฌ์ฃผ๊ณ , ๊ฒฐ์ ๋ด์ญ์ด DB ์ฐ๊ฒฐ ๋ฌธ์ ๋ก ํฐ์ง๋๋ผ๋ ๋๋จธ์ง ํ์ด์ง๋ ์ ์ ๋ ๋๋ง ๋๋๋ก ์ปดํฌ๋ํธ ์ฌ(Island)์ ๊ตฌ์ถํ๋ค)
โ **์ ๋ต:**C
๐ก ์์ธ ํด์ค: ๋ง์ดํฌ๋ก ์ํคํ ์ฒ์ ์ ์์ ๋๋ค. A ๋ฐฉ์์ ๊ฒฐ์ ๋ด์ญ API ์๋ต์ด ํผ์ 5์ด ๊ฑธ๋ฆด ๊ฒฝ์ฐ, ์ด๋ฏธ 0.1์ด ๋ง์ ๋์ฐฉํ ์ ์ ํ๋กํ์กฐ์ฐจ ์ฌ์ฉ์ ๋์์ ์ ๋ณด์ฌ์ฃผ๋ ์ต์ ์ ์ฒด๊ฐ ๋ณ๋ชฉ(Waterfall bottleneck) ์ ๋ง๋ญ๋๋ค. ๋ฐ๋ฉด C ๋ฐฉ์์ฒ๋ผ ์๊ฒ ๊ฒฉ๋ฆฌ(Colocated Boundary)์ํค๋ฉด ๊ฐ ๊ตฌ์ญ์ด ์๊ธฐ ํ์ด์ค๋๋ก ๋ก๋ฉ ํฝ์ด๋ฅผ ๋๋ฆฌ๋ค ๋ ๋๋ง๋๋ฉฐ, ํ๋๊ฐ ํฐ์ ธ๋ ๋๋จธ์ง 2๊ฐ๋ ํ๋ฉด์ ์ด์๋จ๋ ๊ฐ์ธํ ํ๋ ฅ์ฑ(Resilience) ์ ํ๋ณดํ๊ฒ ๋ฉ๋๋ค.