Suspense & ErrorBoundary

์‹ ํƒœ์ผยท2024๋…„ 10์›” 17์ผ

๐Ÿ’ก ๋ฐ์ดํ„ฐ ์š”์ฒญ ์ƒํƒœ์— ๋”ฐ๋ผ ๋‹ค๋ฅด๊ฒŒ ๋…ธ์ถœ๋˜๋Š” UI/UX ์„ค๊ณ„๋Š” ๋งŽ์€ ๊ณ ๋ฏผ์„ ํ•„์š”๋กœ ํ•œ๋‹ค. ์ด๋Ÿฌํ•œ ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด React๋Š” ๋‹ค์–‘ํ•œ ๋ฐฉ๋ฒ•์„ ์ œ๊ณตํ•˜๋Š”๋ฐ, ๊ทธ ์ค‘ Suspense์™€ ErrorBoundary๋ฅผ ํ™œ์šฉํ•œ ์„ ์–ธ์  ๋ฐ์ดํ„ฐ ํŒจ์นญ์ด ์žˆ๋‹ค.


์ „ํ†ต์ ์ธ ๋ฐ์ดํ„ฐ ํŒจ์นญ ์ฒ˜๋ฆฌ

๋ฐ์ดํ„ฐ ํŒจ์นญ์€ ๋‚˜๋ฆ„ ์ „ํ†ต์ ์œผ๋กœ(?) ์ปดํฌ๋„ŒํŠธ ๋‚ด๋ถ€์—์„œ data, loading, error์˜ ์ƒํƒœ๋“ค์„ ์‚ฌ์šฉํ•˜์—ฌ ๋ฐ˜ํ™˜๋˜๋Š” ์ปดํฌ๋„ŒํŠธ์— ๋Œ€ํ•œ ๋ถ„๊ธฐ ์ฒ˜๋ฆฌ๋กœ ์ด๋ฃจ์–ด์กŒ๋‹ค. ์•„๋ž˜์˜ ์˜ˆ์‹œ ์ฝ”๋“œ๋Š” ๋น„๋™๊ธฐ ์š”์ฒญ์œผ๋กœ ๋ฐ์ดํ„ฐ๋ฅผ ๋ฐ›์•„์™€ ์‚ฌ์šฉ์ž์—๊ฒŒ ์ „๋‹ฌํ•˜๋Š” ๊ฐ„๋‹จํ•œ ์ปดํฌ๋„ŒํŠธ์ด๋‹ค.

// SampleContents.tsx

const SampleContents = () => {
  const [sampleDatas, setSampleDatas] = useState();
  const [isLoading, setIsLoading] = useState(false);
  const [error, setError] = useState();

  useEffect(() => {
    (async () => {
      try {
        setIsLoading(true); // isLoading ์ƒํƒœ๋ณ€๊ฒฝ
        const { data } = await queryFn(); // axios get ์š”์ฒญ ์˜ˆ์‹œ ํ•จ์ˆ˜
        setSampleDatas(data);
      } catch (e) {
        setError(e);
      } finally {
        setIsLoading(false); // ์„ฑ๊ณต์ด๋“  ์‹คํŒจ์ด๋“  isLoading์€ false
      }
    })();
  }, []);

  if (isLoading) {
    return <Loader />;
  }

  if (error) {
    return <Error error={error} />;
  }

  return (
    <div>
      {sampleDatas.map(data => (
        <Content data={data} />
      ))}
    </div>
  );
};

export default SampleContents;

๋น„๋™๊ธฐ ํ•จ์ˆ˜ queryFn์ด ๋ฐ˜ํ™˜ํ•˜๋Š” ๋ฐ์ดํ„ฐ๊ฐ€ ๋“ค์–ด์˜ค๊ธฐ ์ „๊นŒ์ง€ isLoading, error ์ƒํƒœ์— ๋”ฐ๋ผ ๋ฐ˜ํ™˜๋˜๋Š” ์ปดํฌ๋„ŒํŠธ๋ฅผ ๋ถ„๊ธฐ ์ฒ˜๋ฆฌํ•˜์—ฌ ๋กœ๋”ฉ ์ค‘์ผ ๋• <Loader />๋ฅผ ๋ฐ˜ํ™˜ํ•˜๊ณ  ์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒํ–ˆ์„๋• <Error />๋ฅผ ๋ฐ˜ํ™˜ํ•˜๋ฉฐ ์‚ฌ์šฉ์ž์—๊ฒŒ ์ •ํ™•ํ•œ ์ •๋ณด๋ฅผ ์ „๋‹ฌํ•  ์ˆ˜ ์žˆ๋‹ค.


React Query

React-Query๋Š” ๋น„๋™๊ธฐ ์š”์ฒญ์„ ํšจ๊ณผ์ ์œผ๋กœ ๊ด€๋ฆฌํ•  ์ˆ˜ ์žˆ๋Š” ์บ์‹ฑ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์ด๋‹ค. React-Query API๋“ค์ด ์ œ๊ณตํ•˜๋Š” ํ”„๋กœํผํ‹ฐ๋ฅผ ํ™œ์šฉํ•˜๋ฉด ์ข€ ๋” ๊น”๋”ํ•˜๊ฒŒ ๋น„๋™๊ธฐ ๋กœ์ง์„ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ๋‹ค.

// SampleContents.tsx

const SampleContents = () => {
  const {
    data: sampleDatas,
    isLoading,
    error,
  } = useQuery({ queryKey, queryFn });

  if (isLoading) {
    return <Loader />;
  }

  if (error) {
    return <Error error={error} />;
  }

  return (
    <div>
      {sampleDatas.map(data => (
        <Content data={data} />
      ))}
    </div>
  );
};

export default SampleContents;

React Query์˜ API useQuery๊ฐ€ ๋ฐ˜ํ™˜ํ•˜๋Š” isLoading, error์™€ ๊ฐ™์€ ํ”„๋กœํผํ‹ฐ๋ฅผ ํ†ตํ•ด useEffect๋ฅผ ์ œ๊ฑฐํ•˜๊ณ  useState ์ค„์ด๋Š” ๋“ฑ ์ปดํฌ๋„ŒํŠธ ๋‚ด๋ถ€ ๋กœ์ง์„ ์ข€ ๋” ๊ฐ„์†Œํ™”ํ•ด์„œ ์ฒซ ๋ฒˆ์งธ ์˜ˆ์‹œ์™€ ๋งˆ์ฐฌ๊ฐ€์ง€๋กœ ๋ฐ์ดํ„ฐ ์š”์ฒญ ์ƒํƒœ์— ๋”ฐ๋ผ ๋ฐ˜ํ™˜๋˜๋Š” ์ปดํฌ๋„ŒํŠธ ๋ Œ๋”๋ง ์—ฌ๋ถ€๋ฅผ ์ž‘์„ฑํ•  ์ˆ˜ ์žˆ๋‹ค.


์„ ์–ธ์  ๋ฐ์ดํ„ฐ ํŒจ์นญ

์„ ์–ธ์  ๋ฐ์ดํ„ฐ ํŒจ์นญ์€ React์˜ ์ฃผ์š” ๊ฐœ๋…์ธ โ€™์„ ์–ธํ˜• ํ”„๋กœ๊ทธ๋ž˜๋ฐโ€™ ์— ๊ธฐ๋ฐ˜ํ•œ๋‹ค. ์ด๋Š” ์ƒํƒœ์˜ ๋ณ€ํ™”์— ๋”ฐ๋ผ UI๋ฅผ ์ง์ ‘ ์กฐ์ž‘ํ•˜์ง€ ์•Š๊ณ , ์–ด๋–ค ์ƒํƒœ์— ๋”ฐ๋ผ UI๊ฐ€ ๋ณด์—ฌ์ ธ์•ผ ํ•˜๋Š”์ง€๋งŒ ์ง‘์ค‘ํ•  ์ˆ˜ ์žˆ๋‹ค.

์ด๋Ÿฌํ•œ ์ ‘๊ทผ๋ฒ•์€ ๋น„๋™๊ธฐ ์ž‘์—…์˜ ๋ณต์žก์„ฑ์„ ํฌ๊ฒŒ ์ค„์–ด์ฃผ๋ฉฐ ์ฝ”๋“œ์˜ ๊ฐ€๋…์„ฑ๊ณผ ์œ ์ง€๋ณด์ˆ˜์„ฑ์„ ํ–ฅ์ƒ์‹œํ‚ค๋Š”๋ฐ, Suspense์™€ ErrorBoundary๋Š” ์„ ์–ธํ˜• ํ”„๋กœ๊ทธ๋ž˜๋ฐ ๋ฐฉ์‹์œผ๋กœ ๋น„๋™๊ธฐ ์ž‘์—…๊ณผ ์—๋Ÿฌ ์ฒ˜๋ฆฌ๋ฅผ ๋‹ด๋‹นํ•œ๋‹ค.

์šฐ์„  Suspense์™€ Error Boundary์— ๋Œ€ํ•ด ๊ฐ„๋‹จํ•˜๊ฒŒ ์•Œ์•„๋ณด์ž.

Suspense

Suspense๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ์ž์‹์ด ๋กœ๋”ฉ์„ ์™„๋ฃŒํ•  ๋•Œ๊นŒ์ง€ fallback์„ ํ‘œ์‹œํ•  ์ˆ˜ ์žˆ๋‹ค. React๋Š” ์ž์‹์—๊ฒŒ ํ•„์š”ํ•œ ๋ชจ๋“  ์ฝ”๋“œ์™€ ๋ฐ์ดํ„ฐ๊ฐ€ ๋กœ๋“œ๋  ๋•Œ๊นŒ์ง€ ๋กœ๋”ฉ fallback์„ ํ‘œ์‹œํ•œ๋‹ค.

// React ๊ณต์‹ ๋ฌธ์„œ์˜ ์˜ˆ์ œ

<Suspense fallback={<Loading />}>
	<SampleContents />
</Suspense>

Error Boundary

Error Boundary๋Š” ํ•˜์œ„ ์ปดํฌ๋„ŒํŠธ ํŠธ๋ฆฌ์—์„œ ๋ฐœ์ƒํ•˜๋Š” ์—๋Ÿฌ๋ฅผ ์บก์ฒ˜ํ•ด์„œ ๋Œ€์ฒด UI๋ฅผ ๋ณด์—ฌ์ฃผ๊ฑฐ๋‚˜ ์—๋Ÿฌ ๋ฆฌํฌํŒ… ๋“ฑ์˜ ์ž‘์—…์„ ์ˆ˜ํ–‰ํ•œ๋‹ค.

ํ•„์š”์— ๋”ฐ๋ผ ์ปค์Šคํ„ฐ๋งˆ์ด์ง•์„ ํ•  ์ˆ˜ ์žˆ๋Š”๋ฐ ์ด๋Š” ์‹œ๊ฐ„์ด ๋œ๋‹ค๋ฉด ๋‹ค์Œ ํฌ์ŠคํŒ…์„ ์ •๋…ํ•˜๋ฉด ์ข‹์„ ๊ฒƒ ๊ฐ™๋‹ค.

ErrorBoundary๋กœ Toast, ErrorFallback ๋“ฑ ๊ณตํ†ต์ ์ธ ์—๋Ÿฌ๋ฅผ ์ฒ˜๋ฆฌํ•ด๋ณด์ž



React-Query์™€ Suspense, Error Boundary ์‚ฌ์šฉํ•˜๊ธฐ

useQuery๋ฅผ Suspense, Error Boundary์™€ ํ•จ๊ป˜ ์‚ฌ์šฉํ•˜๋ ค๋ฉด suspense: true ์˜ต์…˜์„ ๋„ฃ์–ด์ฃผ๋ฉด ๋œ๋‹ค.

// SampleWrapper.jsx

const SampleWrapper = () => {
  // ...

  return (
    <ErrorBoundary fallback={<Error />}>
      <Suspense fallback={<Loader />}>
        <SampleContents />
      </Suspense>
    </ErrorBoundary>
  );
};

export default SampleWrapper;

// SampleContents.jsx

const SampleContents = () => {
  const { data: sampleDatas } = useQuery({ queryKey, queryFn, suspense: true });

  return (
    <div>
      {sampleDatas.map(data => (
        <Content data={data} />
      ))}
    </div>
  );
};

export default SampleContents;

Suspense๋Š” ์ปดํฌ๋„ŒํŠธ๊ฐ€ ์ค€๋น„๋  ๋•Œ๊นŒ์ง€ ๋Œ€๊ธฐํ•˜๊ณ  fallback props๋กœ ๋ฐ›์€ ์ปดํฌ๋„ŒํŠธ๋ฅผ ๋ณด์—ฌ์ฃผ๋Š” ์—ญํ• ์„ ํ•œ๋‹ค. ์ด๋กœ ์ธํ•ด ๋กœ๋”ฉ ์ƒํƒœ์— ๋”ฐ๋ฅธ ๋กœ์ง์ด ์ปดํฌ๋„ŒํŠธ ์™ธ๋ถ€๋กœ ์ถ”์ถœ๋˜์–ด ๊ฐ ์ปดํฌ๋„ŒํŠธ์—์„œ ๋กœ๋”ฉ ์ƒํƒœ๋ฅผ ํ™•์ธํ•˜๊ณ  ์ฒ˜๋ฆฌํ•  ํ•„์š”๊ฐ€ ์—†์–ด์ง„๋‹ค.

Error Boundary๋Š” ํ•˜์œ„ ์ปดํฌ๋„ŒํŠธ ํŠธ๋ฆฌ์—์„œ ๋ฐœ์ƒํ•˜๋Š” ์—๋Ÿฌ๋ฅผ ์บก์ณํ•˜์—ฌ ๋Œ€์ฒด UI๋ฅผ ๋ณด์—ฌ์ฃผ๊ฑฐ๋‚˜ ์—๋Ÿฌ ๋ฆฌํฌํŒ… ๋“ฑ์˜ ์ž‘์—…์„ ์ˆ˜ํ–‰ํ•œ๋‹ค. ์ด๋กœ ์ธํ•ด ๊ฐ๊ฐ์˜ ์ปดํฌ๋„ŒํŠธ์—์„œ ์—๋Ÿฌ ์ฒ˜๋ฆฌ ๋กœ์ง์ด ๋ถ„์‚ฐ๋˜์ง€ ์•Š๊ณ  ํ•œ ๊ณณ์—์„œ ๊ด€๋ฆฌ๋  ์ˆ˜ ์žˆ๋‹ค.

์œ„์˜ ์ฝ”๋“œ์—์„œ ๋ณผ ์ˆ˜ ์žˆ๋“ฏ์ด, Suspense์™€ ErrorBoundary๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ๋กœ๋”ฉ ์ƒํƒœ์™€ ์—๋Ÿฌ ์ฒ˜๋ฆฌ ๋กœ์ง์„ ์ปดํฌ๋„ŒํŠธ ์™ธ๋ถ€๋กœ ๋ถ„๋ฆฌํ•˜์—ฌ ๊ด€๋ฆฌํ•  ์ˆ˜ ์žˆ๋Š”๋ฐ, ์ปดํฌ๋„ŒํŠธ๋กœ๋ถ€ํ„ฐ ๋ถ„๋ฆฌ๋œ ๋กœ์ง์œผ๋กœ ์ฝ”๋“œ์˜ ๊ฐ€๋…์„ฑ์„ ๋†’์ด๊ณ  ์œ ์ง€๋ณด์ˆ˜๋ฅผ ์šฉ์ดํ•˜๊ฒŒ ํ•œ๋‹ค.

๐Ÿ’ก ํŒ
๋ชจ๋“  ์ฟผ๋ฆฌ๋ฌธ์—์„œ ์‚ฌ์šฉํ•  ๊ฒƒ์ด๋ผ๋ฉด QueryClient๋ฅผ ์ƒ์„ฑํ•  ๋•Œ ๊ธฐ๋ณธ ์˜ต์…˜์œผ๋กœ ์ง€์ •ํ•ด ์ค„ ์ˆ˜ ์žˆ๋‹ค.

const queryClient = new QueryClient({
	defaultOptions: {
		queries: {
			suspense: true,
		},
	},
});

๐Ÿ’ก ํŒ
ErrorBoundary๋Š” ํ•˜์œ„ ์ปดํฌ๋„ŒํŠธ์˜ ์—๋Ÿฌ๋“ค์„ ๊ณตํ†ต์œผ๋กœ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ๋‹ค.

const ChildComponent = () => {
	// ...
	
	return (
		<Suspense fallback={<Loader />}>
			<SampleContents />
		</Suspense>
	);
};
const ParentComponent = () => {
	// ...
	// ํ•˜์œ„ ์ปดํฌ๋„ŒํŠธ์—์„œ ๋ฐœ์ƒํ•˜๋Š” error๋ฅผ ๋ชจ๋‘ ์บ์น˜
	
	return (
		<ErrorBoundary fallback={<Error />}>
			<ChildComponent />
			<ChildComponent2 />
			<ChildComponent3 />
		</ErrorBoundary>
	);
};

์„ ์–ธ์  ๋ฐ์ดํ„ฐ ํŒจ์นญ์„ ํ•  ๋•Œ์˜ ์ฃผ์˜์‚ฌํ•ญ

์„ ์–ธ์  ๋ฐ์ดํ„ฐ ํŒจ์นญ์€ ๋งŽ์€ ์žฅ์ ์„ ๊ฐ€์ง€๊ณ  ์žˆ์ง€๋งŒ, ๋ช‡ ๊ฐ€์ง€ ์ฃผ์˜์‚ฌํ•ญ๋„ ์กด์žฌํ•œ๋‹ค.

๐Ÿ’ก ์„ธ์ƒ์— ๊ณต์งœ๋Š” ์—†๋‹ค

๋น„๋™๊ธฐ ์š”์ฒญ Waterfall

Suspense๋กœ ๊ฐ์‹ผ ์ปดํฌ๋„ŒํŠธ ๋‚ด์—์„œ ์—ฌ๋Ÿฌ๊ฐœ์˜ useQuery๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๊ฒฝ์šฐ ๋น„๋™๊ธฐ ์š”์ฒญ Waterfall์ด ๋ฐœ์ƒํ•œ๋‹ค.

<Suspense fallback={<Loading />}>
	<SampleContents /> // 3๊ฐœ์˜ query๋ฌธ์ด ์žˆ๋Š” ์ปดํฌ๋„ŒํŠธ
</Suspense>

<Suspense fallback={<Loading />}>
	<SampleContents1 /> // 1๊ฐœ์˜ query๋ฌธ์ด ์žˆ๋Š” ์ปดํฌ๋„ŒํŠธ
	<SampleContents2 /> // 1๊ฐœ์˜ query๋ฌธ์ด ์žˆ๋Š” ์ปดํฌ๋„ŒํŠธ
	<SampleContents3 /> // 1๊ฐœ์˜ query๋ฌธ์ด ์žˆ๋Š” ์ปดํฌ๋„ŒํŠธ
</Suspense>

React Query v4.5 ์ดํ›„ ์ตœ์‹  ๋ฒ„์ „์„ ์‚ฌ์šฉํ•œ๋‹ค๋ฉด useQuery ๋Œ€์‹  useQueries์˜ ์‚ฌ์šฉ์„ ๊ณ ๋ คํ•ด ๋ณผ ๋งŒํ•˜๋‹ค.

๋น„๋™๊ธฐ ์š”์ฒญ์„ ๋ณ‘๋ ฌ๋กœ ์ฒ˜๋ฆฌํ•ด ์ฃผ๋ฉฐ Suspense๋ฅผ ์ง€์›ํ•ด ์ค€๋‹ค!


Loader์™€ Skeleton์˜ ์‚ฌ์šฉ์ž ๊ฒฝํ—˜์— ๋Œ€ํ•œ ๊ณ ๋ฏผํ•„์š”

๋ฐ์ดํ„ฐ ํŒจ์นญ ์‹œ๊ฐ„์ด ์งง์€ ๊ฒฝ์šฐ Loader๋‚˜ Skeleton UI ๊ฐ™์€ ์ปดํฌ๋„ŒํŠธ์˜ ๋…ธ์ถœ์€ ์‚ฌ์šฉ์ž์—๊ฒŒ ๊นœ๋นก์ด๋Š” ์˜ค๋ฅ˜ ํ˜„์ƒ ์ฒ˜๋Ÿผ ๋ณด์ผ ์ˆ˜ ์žˆ์–ด ์˜คํžˆ๋ ค ๋ถ€์ •ํ™•ํ•œ ์ •๋ณด ์ „๋‹ฌ ๋ฐ ์‚ฌ์šฉ์ž ๊ฒฝํ—˜ ์ €ํ•˜๋กœ ์ด์–ด์งˆ ์ˆ˜ ์žˆ์œผ๋ฏ€๋กœ ์‹ ์ค‘ํ•˜๊ฒŒ ๊ณ ๋ คํ•ด์•ผ ํ•œ๋‹ค.

const DeferredLoader = () => {
  const [isDeferred, setIsDeferred] = useState(false);

  useEffect(() => {
    const id = setTimeout(() => {
      setIsDeferred(true);
    }, 200);
    return () => clearTimeout(id);
  }, []);

  if (!isDeferred) {
    return null;
  }

  return <Loader />;
};

์งง์€ ์‹œ๊ฐ„ ๋‚ด์— ๋น„๋™๊ธฐ ์ƒํƒœ๊ฐ€ ๋ณ€๊ฒฝ๋œ๋‹ค๋ฉด Loader๋‚˜ Skeleton UI ๋Œ€์‹  null์„ ๋ฐ˜ํ™˜ํ•˜์—ฌ ํ™”๋ฉด์„ ๊ทธ๋ฆฌ์ง€ ์•Š๊ณ  ๊ทธ ์ด์ƒ์˜ ๋กœ๋”ฉ ์‹œ Loader๋‚˜ Skeleton UI๋ฅผ ๋…ธ์ถœํ•˜๋Š” ๋ฐฉ์‹๋„ ๊ณ ๋ คํ•  ๋งŒํ•˜๋‹ค.


Conclusion

์ง€๊ธˆ๊นŒ์ง€ Suspense์™€ ErrorBoundary๋ฅผ ํ™œ์šฉํ•œ ์„ ์–ธ์  ๋ฐ์ดํ„ฐ ํŒจ์นญ์— ๋Œ€ํ•ด ์•Œ์•„๋ดค๋‹ค.

Suspense์™€ ErrorBoundary๋ฅผ ํ™œ์šฉํ•œ ๋น„๋™๊ธฐ ์ฒ˜๋ฆฌ๋Š” ์ปดํฌ๋„ŒํŠธ์˜ ๊ฐ€๋…์„ฑ, ์œ ์ง€๋ณด์ˆ˜ ์ธก๋ฉด, ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง ๋ถ„๋ฆฌ ์ธก๋ฉด์—์„œ ์ƒ๋‹นํ•œ ์ด์ ์ด ์žˆ์ง€๋งŒ, ๋ฌด๋ถ„๋ณ„ํ•œ Suspense, ErrorBoundary๋Š” ์„ฑ๋Šฅ ์ €ํ•˜, ์‚ฌ์šฉ์ž ๊ฒฝํ—˜ ์ €ํ•˜๋ฅผ ์ผ์œผํ‚ฌ ์ˆ˜ ์žˆ๊ธฐ์— ์žฅ๋‹จ์ ์„ ์ข€ ๋” ๊ณ ๋ฏผํ•˜๊ณ  ๋™์ž‘ ์›๋ฆฌ์™€ ์ ์ ˆํ•œ ์‚ฌ์šฉ์„ฑ์— ๋Œ€ํ•ด์„œ ๋…ผ์˜ํ•ด์•ผ ํ•  ๊ฒƒ ๊ฐ™๋‹ค.

profile
๋…ธ์›๊ฑฐ์ธ

0๊ฐœ์˜ ๋Œ“๊ธ€