๐Ÿ’ฅ 16. ๋น„๋™๊ธฐ UX์˜ ํŒจ๋Ÿฌ๋‹ค์ž„ ์ „ํ™˜ (Suspense & ErrorBoundary)

๐Ÿ“‹ ๊ฐœ์š”

๋กœ๋”ฉ๊ณผ ์—๋Ÿฌ ์ฒ˜๋ฆฌ ์ฑ…์ž„์„ ๋ถ€๋ชจ ๋ ˆ์ด์–ด๋กœ ์™„๋ฒฝํžˆ ๋– ๋„˜๊ธฐ๊ณ , ์ž์‹ ์ปดํฌ๋„ŒํŠธ๋Š” '๋ฐ์ดํ„ฐ๊ฐ€ 100% ์กด์žฌํ•  ๋•Œ์˜ ์„ฑ๊ณตํ•œ UI'๋งŒ ๋‚จ๊ฒจ๋‘๋Š” ๊ทน๊ฐ•์˜ ์„ ์–ธ์  ๋น„๋™๊ธฐ ์ฒ˜๋ฆฌ ๊ธฐ๋ฒ•์„ ๋ฐฐ์›๋‹ˆ๋‹ค.

๐ŸŽฏ ์ด ์„น์…˜์„ ์ฝ๊ณ  ๋‚˜๋ฉด:

  • if (isLoading)๊ณผ if (isError)๋กœ ๋„๋ฐฐ๋œ ๋”์ฐํ•œ ์ปดํฌ๋„ŒํŠธ๋ฅผ ๊ตฌ์ถœํ•  ์ˆ˜ ์žˆ๋‹ค.
  • ์ปดํฌ๋„ŒํŠธ์˜ ์ฑ…์ž„์„ "๋ฐ์ดํ„ฐ ๋ Œ๋”๋ง" ํ•˜๋‚˜๋กœ๋งŒ ๊ทน๋‹จ์ ์œผ๋กœ ์ขํžˆ๋Š” ์„ ์–ธ์ (Declarative) ๋น„๋™๊ธฐ ์ฒ˜๋ฆฌ์˜ ๋งˆ๋ฒ•์„ ์ดํ•ดํ•œ๋‹ค.
  • ์•ฑ ์–ด๋””์„œ ์—๋Ÿฌ๊ฐ€ ํ„ฐ์ง€๋“  ์•ฑ ์ „์ฒด๊ฐ€ ํ•˜์–—๊ฒŒ ์ฃฝ์ง€ ์•Š๊ณ (ํ™”์ดํŠธ์•„์›ƒ), ์šฐ์•„ํ•˜๊ฒŒ ๋ถ€๋ถ„ ์—๋Ÿฌ ํ™”๋ฉด์„ ๋„์›Œ์ฃผ๋Š” ์•„ํ‚คํ…์ฒ˜๋ฅผ ์„ค๊ณ„ํ•  ์ˆ˜ ์žˆ๋‹ค.

๐Ÿ“‹ ๋ชฉ์ฐจ


๐Ÿ“Œ ์ด ๋ฌธ์„œ๋ฅผ ์ฝ๊ธฐ ์ „์—

โฑ๏ธ ์˜ˆ์ƒ ์ฝ๊ธฐ ์‹œ๊ฐ„: 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>;
}

์ด ๋ฐฉ์‹์˜ ์น˜๋ช…์ ์ธ ๋ฌธ์ œ์ ์€ ๋‹ค์Œ๊ณผ ๊ฐ™์Šต๋‹ˆ๋‹ค.

  1. ๊ด€์‹ฌ์‚ฌ ์˜ค์—ผ: ์ปดํฌ๋„ŒํŠธ์˜ ๋ณธ์งˆ์€ "๋ฐ์ดํ„ฐ๋ฅผ ์˜ˆ์˜๊ฒŒ UI๋กœ ๋งตํ•‘ํ•˜๋Š” ๊ฒƒ"์ธ๋ฐ, ๋ฐ์ดํ„ฐ๋ฅผ ์–ด๋–ป๊ฒŒ ๊ฐ€์ ธ์˜ฌ์ง€, ์‹คํŒจํ•˜๋ฉด ์–ด๋–กํ• ์ง€ ์˜จ๊ฐ– ์žก์ผ์— ์‹œ๋‹ฌ๋ฆฝ๋‹ˆ๋‹ค.
  2. **"์ด ๋ฐ์ดํ„ฐ๋Š” ์ง„์งœ ์žˆ๋Š”๊ฐ€?":**TypeScript๋กœ ๋‹ฌ์•„๋ด๋„ data๋Š” ์ดˆ๊ธฐ์— null์ž…๋‹ˆ๋‹ค. ๋”ฐ๋ผ์„œ ๋ฐ‘์—์„œ data.name์„ ์“ธ ๋•Œ๋งˆ๋‹ค ๋ถˆ์•ˆ์ฆ์„ธ(Optional Chaining data?.name)์— ์‹œ๋‹ฌ๋ฆฌ๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.
  3. UI ๊ธฐํš ๋ณ€๊ฒฝ ๋Œ€์‘ ๋ถˆ๊ฐ€: PM์ด "๋กœ๋”ฉ ๋ฐ”๋ฅผ ์ € ์œ„์ชฝ ํ—ค๋”๋กœ ํ•ฉ์ณ์ฃผ์„ธ์š”"๋ผ๊ณ  ํ•˜๋ฉด, ๋กœ๋”ฉ ์ƒํƒœ๋ฅผ ๋˜ ๋ถ€๋ชจ๋กœ ๋Œ์–ด์˜ฌ๋ฆฌ๋ฉฐ(Lifting State Up) ๋Œ€๊ณต์‚ฌ๋ฅผ ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

๐Ÿ—๏ธ ๋น„์œ ๋กœ ๋จผ์ € ์ดํ•ดํ•˜๊ธฐ

๐Ÿง’ 5์‚ด์—๊ฒŒ ์„ค๋ช…ํ•œ๋‹ค๋ฉด?

  1. **๊ธฐ์กด ๋ฐฉ์‹ (์˜์ฒ ์ด ํ–„๋ฒ„๊ฑฐ ๊ฐ€๊ฒŒ):**์š”๋ฆฌ์‚ฌ(์ปดํฌ๋„ŒํŠธ)๊ฐ€ ํŒจํ‹ฐ๋ฅผ ๊ตฝ๋‹ค๊ฐ€ ๊ณ ๊ธฐ๊ฐ€ ๋–จ์–ด์ง€๋ฉด(๋กœ๋”ฉ), ์ž๊ธฐ๊ฐ€ ์ง์ ‘ ์š”๋ฆฌ๋ฅผ ๋ฉˆ์ถ”๊ณ  ํ™€์— ๋‚˜๊ฐ€ "์†๋‹˜, ๊ณ ๊ธฐ ์˜ค๋Š” ์ค‘ ใ… ใ… " ํŒป๋ง์„ ๋“ญ๋‹ˆ๋‹ค. ๊ณ ๊ธฐ๊ฐ€ ์ƒํ–ˆ์œผ๋ฉด(์—๋Ÿฌ) "์†๋‹˜ ๊ณ ๊ธฐ ์ƒํ•จ!" ํŒป๋ง์„ ๋“ค๊ณ ์š”. ์š”๋ฆฌ์— ์ง‘์ค‘ํ•  ์ˆ˜๊ฐ€ ์—†์ฃ .
  2. 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) ์„ ํ™•๋ณดํ•˜๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.