๐Ÿ‘€ Suspensive v2์—์„œ์˜ ๋ณ€๊ฒฝ์„ ํ™•์ธํ•˜์„ธ์š”. ๋”๋ณด๊ธฐ โ†’
Suspensive with star
Suspensive v2

React Suspense๋ฅผ ์œ„ํ•œ ๋ชจ๋“  ๊ฒƒ

๋ชจ๋“  ์„ ์–ธ์  API๋ฅผ ์ œ๊ณต

<Suspense/>, <ErrorBoundary/>, <ErrorBoundaryGroup/> ๋“ฑ์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. ๋ณ„ ๋‹ค๋ฅธ ๋…ธ๋ ฅ์—†์ด ์‰ฝ๊ฒŒ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

Zero ์˜์กด์„ฑ, ์˜ค์ง React

๋‹จ์ˆœํžˆ React์˜ ๊ฐœ๋…์„ ํ™•์žฅํ•œ ๊ฒƒ์ž…๋‹ˆ๋‹ค. <Suspense/>, <ErrorBoundary/>, <ErrorBoundaryGroup/>์™€ ๊ฐ™์€ React ๊ฐœ๋ฐœ์ž์—๊ฒŒ ์นœ์ˆ™ํ•œ ์ด๋ฆ„์œผ๋กœ ์ปดํฌ๋„ŒํŠธ๋“ค์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.

์„œ๋ฒ„ ์‚ฌ์ด๋“œ ๋ Œ๋”๋ง์—์„œ๋„ ์‰ฝ๊ฒŒ

Suspensive๋Š” ๊ฐœ๋ฐœ์ž๊ฐ€ ์„œ๋ฒ„ ์‚ฌ์ด๋“œ ๋ Œ๋”๋ง ํ™˜๊ฒฝ์—์„œ๋„ React Suspense๋ฅผ ์ ์ง„์ ์œผ๋กœ ์ฑ„ํƒํ•  ์ˆ˜ ์žˆ๋„๋ก clientOnly๋ฅผ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.

๋Œ€ํ‘œ์ ์ธ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์ธ TanStack Query๋กœ Suspense ์—†์ด ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•œ๋‹ค๋ฉด ์ด๋ ‡๊ฒŒ ์ž‘์„ฑํ•ฉ๋‹ˆ๋‹ค.

์ด ๊ฒฝ์šฐ isLoading๊ณผ isError๋ฅผ ์ฒดํฌํ•˜์—ฌ ๋กœ๋”ฉ๊ณผ ์—๋Ÿฌ ์ƒํƒœ๋ฅผ ์ฒ˜๋ฆฌํ•˜๊ณ  ํƒ€์ž…์Šคํฌ๋ฆฝํŠธ์ ์œผ๋กœ data์—์„œ undefined๋ฅผ ์ œ๊ฑฐํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๊ทธ๋Ÿฐ๋ฐ ๋งŒ์•ฝ ์กฐํšŒํ•ด์•ผ ํ•  api๊ฐ€ ๋” ๋งŽ์•„์ง„๋‹ค๊ณ  ๊ฐ€์ •ํ•ด๋ด…์‹œ๋‹ค.

์กฐํšŒํ•ด์•ผ ํ•˜๋Š” API๊ฐ€ ๋” ๋งŽ์•„์ง„๋‹ค๋ฉด ์ด ๋กœ๋”ฉ์ƒํƒœ์™€ ์—๋Ÿฌ์ƒํƒœ๋ฅผ ์ฒ˜๋ฆฌํ•˜๋Š” ์ฝ”๋“œ๊ฐ€ ๋”์šฑ ๋ณต์žกํ•ด์ง‘๋‹ˆ๋‹ค.

Suspense๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ํƒ€์ž…์ ์œผ๋กœ ์ฝ”๋“œ๊ฐ€ ๊ฐ„๊ฒฐํ•ด์ง‘๋‹ˆ๋‹ค. ํ•˜์ง€๋งŒ ์ปดํฌ๋„ŒํŠธ์˜ ๊นŠ์ด๋Š” ๊นŠ์–ด์งˆ ์ˆ˜ ๋ฐ–์— ์—†์Šต๋‹ˆ๋‹ค.

useSuspenseQuery๋Š” Suspense์™€ ErrorBoundary๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์™ธ๋ถ€์—์„œ ๋กœ๋”ฉ๊ณผ ์—๋Ÿฌ ์ƒํƒœ๋ฅผ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ํ•˜์ง€๋งŒ useSuspenseQuery๋Š” hook์ด๊ธฐ ๋•Œ๋ฌธ์— ๋ถ€๋ชจ์— Suspense์™€ ErrorBoundary๋ฅผ ๋‘๊ธฐ ์œ„ํ•ด ์ปดํฌ๋„ŒํŠธ๊ฐ€ ๋ถ„๋ฆฌ๋˜์–ด์•ผ๋งŒ ํ•˜๊ธฐ ๋•Œ๋ฌธ์— ๋Ž์Šค๊ฐ€ ๊นŠ์–ด์ง€๋Š” ๋ฌธ์ œ๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค.

Suspensive์˜ SuspenseQuery ์ปดํฌ๋„ŒํŠธ๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด hook์˜ ์ œ์•ฝ์„ ํ”ผํ•ด ๊ฐ™์€ ๋Ž์Šค์—์„œ ๋”์šฑ ์‰ฝ๊ฒŒ ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

  1. SuspenseQuery๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด depth๋ฅผ ์ œ๊ฑฐํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
  2. UserInfo๋ผ๋Š” ์ปดํฌ๋„ŒํŠธ๋ฅผ ์ œ๊ฑฐํ•˜๊ณ  UserProfile๊ณผ ๊ฐ™์€ Presentational ์ปดํฌ๋„ŒํŠธ๋งŒ ๋‚จ์œผ๋ฏ€๋กœ ํ…Œ์ŠคํŠธํ•˜๊ธฐ ์‰ฌ์›Œ์ง‘๋‹ˆ๋‹ค.
const Page = () => {
const userQuery = useQuery(userQueryOptions())
const postsQuery = useQuery({
...postsQueryOptions(),
select: (posts) => posts.filter(({ isPublic }) => isPublic),
})
const promotionsQuery = useQuery(promotionsQueryOptions())
if (
userQuery.isLoading ||
postsQuery.isLoading ||
promotionsQuery.isLoading
) {
return 'loading...'
}
if (userQuery.isError || postsQuery.isError || promotionsQuery.isError) {
return 'error'
}
return (
<Fragment>
<UserProfile {...userQuery.data} />
{postsQuery.data.map((post) => (
<PostListItem key={post.id} {...post} />
))}
{promotionsQuery.data.map((promotion) => (
<Promotion key={promotion.id} {...promotion} />
))}
</Fragment>
)
}

์ด๊ฒƒ์ด ์šฐ๋ฆฌ๊ฐ€ Suspensive๋ฅผ ๋งŒ๋“œ๋Š” ์ด์œ ์ž…๋‹ˆ๋‹ค.

Suspense, ClientOnly, DefaultProps

Next.js์™€ ๊ฐ™์€ ํ”„๋ ˆ์ž„์›Œํฌ๋ฅผ ์‚ฌ์šฉํ•˜๋‹ค๋ณด๋ฉด ์„œ๋ฒ„์—์„œ Suspense๋ฅผ ์‚ฌ์šฉํ•˜๊ธฐ ์–ด๋ ค์šธ ๋•Œ๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค.

ํ˜น์€ ์„œ๋ฒ„์—์„œ Suspense๋ฅผ ์‚ฌ์šฉํ•˜๊ณ  ์‹ถ์ง€ ์•Š์„ ๋•Œ๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค.

์ด๋Ÿด ๋•Œ Suspensive์˜ ClientOnly๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ์‰ฝ๊ฒŒ ํ•ด๊ฒฐํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

ClientOnly๋ฅผ ๊ฐ์‹ธ์ฃผ๊ธฐ๋งŒ ํ•˜๋ฉด ํ•ด๊ฒฐ๋ฉ๋‹ˆ๋‹ค.

ํ˜น์€ Suspensive์˜ Suspense์—๋Š” clientOnly prop์„ ํ™œ์šฉํ•ด ์ด๋Ÿฌํ•œ ๊ฒฝ์šฐ๋ฅผ ์‰ฝ๊ฒŒ ๋Œ€์‘ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๊ฐ„๋‹จํ•˜์ฃ ?

๊ทธ๋Ÿฐ๋ฐ ๊ฐœ๋ฐœ์„ ํ•˜๋‹ค๋ณด๋ฉด Suspense์— ์ผ์ผ์ด fallback์„ ๋„ฃ์–ด์ฃผ๊ธฐ ์–ด๋ ค์šธ ๋•Œ๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค.

ํŠนํžˆ Admin๊ณผ ๊ฐ™์€ ์ œํ’ˆ์„ ํ•  ๋•Œ ๋””์ž์ด๋„ˆ๊ฐ€ ์ผ์ผ์ด ์ง€์ •ํ•ด์ฃผ์ง€ ์•Š๋Š” ๊ฒฝ์šฐ๊ฐ€ ์žˆ์–ด์„œ ๊ธฐ๋ณธ๊ฐ’์„ ์ฃผ๊ณ  ์‹ถ์„ ๋•Œ๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค. ๊ทธ๋Ÿด ๋•Œ DefaultProps๋ฅผ ํ™œ์šฉํ•ด๋ณด์„ธ์š”.

๊ธฐ๋ณธ fallback์ด ๋ฐ”๋กœ ๋‚˜์˜ค๋Š” ๊ฒƒ๋ณด๋‹ค๋Š” FadeIn๊ณผ ๊ฐ™์€ ํšจ๊ณผ๋ฅผ ์ฃผ๊ณ  ์‹ถ์„ ๋•Œ๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค.

๊ทธ๋ ‡๋‹ค๋ฉด FadeIn์„ ํ™œ์šฉํ•ด๋ณด๋ฉด ์–ด๋–จ๊นŒ์š”?

๋‹น์—ฐํžˆ ๊ธฐ๋ณธ fallback์„ Overrideํ•˜๊ณ  ์‹ถ๋‹ค๋ฉด ๊ทธ๋ƒฅ ๋„ฃ์–ด์ฃผ๋ฉด ๋ฉ๋‹ˆ๋‹ค.

๋””์ž์ด๋„ˆ๊ฐ€ ์ด ๋ถ€๋ถ„์— ๊ธฐ๋ณธ Spinner๋ณด๋‹ค Skeleton์œผ๋กœ ๋™์ž‘ํ•˜๋„๋ก ์ง€์›ํ•ด๋‹ฌ๋ผ๊ณ  ํ•˜๋„ค์š”~! ๊ทธ๋ƒฅ ๋„ฃ์œผ๋ฉด ๋˜์–ด์š”.

const Page = () => (
<Suspense fallback={<Spinner />}>
<SuspenseQuery {...notNeedSEOQueryOptions()}>
{({ data }) => <NotNeedSEO {...data} />}
</SuspenseQuery>
</Suspense>
)

ErrorBoundaryGroup, ErrorBoundary

ErrorBoundary์˜ fallback ์™ธ๋ถ€์—์„œ ErrorBoundary๋ฅผ resetํ•˜๊ณ  ์‹ถ์„ ๋•Œ resetKeys๋ฅผ ์‚ฌ์šฉํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

์ด๋Š” ๊นŠ์€ ์ปดํฌ๋„ŒํŠธ์˜ ๊ฒฝ์šฐ resetKey๋ฅผ ์ „๋‹ฌํ•ด์•ผ ํ•˜๋Š” ๋ฌธ์ œ๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค. ๋˜ํ•œ state๋ฅผ ๋งŒ๋“ค์–ด resetKey๋ฅผ ์ „๋‹ฌํ•ด์•ผ ํ•˜๋Š” ๋ฌธ์ œ๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค.

Suspensive๊ฐ€ ์ œ๊ณตํ•˜๋Š” ErrorBoundary์™€ ErrorBoundaryGroup์„ ์กฐํ•ฉํ•˜๋ฉด ์ด๋Ÿฌํ•œ ๋ฌธ์ œ๋ฅผ ๋งค์šฐ ๋‹จ์ˆœํžˆ ํ•ด๊ฒฐํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

ErrorBoundaryGroup์„ ์‚ฌ์šฉํ•ด๋ณด์„ธ์š”.

๊ทธ๋Ÿฐ๋ฐ ErrorBoundary๋ฅผ ์‚ฌ์šฉํ•˜๋‹ค๋ณด๋ฉด ํŠน์ • Error์— ๋Œ€ํ•ด์„œ๋งŒ ์ฒ˜๋ฆฌํ•˜๊ณ  ์‹ถ์„ ๋•Œ๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค.

๊ทธ๋Ÿด ๋•Œ์—๋Š” Suspensive๊ฐ€ ์ œ๊ณตํ•˜๋Š” ErrorBoundary์˜ shouldCatch๋ฅผ ์จ๋ณด์„ธ์š”. ์ด shouldCatch์— Error Constructor๋ฅผ ๋„ฃ์œผ๋ฉด ํ•ด๋‹น Error์— ๋Œ€ํ•ด์„œ๋งŒ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

ํ˜น์€ ๋ฐ˜๋Œ€๋กœ ๊ทธ Error๋งŒ ๋นผ๊ณ  ์ฒ˜๋ฆฌํ•  ์ˆ˜๋„ ์žˆ์Šต๋‹ˆ๋‹ค.

๊ทธ๋Ÿด ๋•Œ์—๋Š” shouldCatch์— callback์„ ๋„ฃ์–ด์„œ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

const Page = () => {
const [resetKey, setResetKey] = useState(0)
return (
<Fragment>
<button onClick={() => setResetKey((prev) => prev + 1)}>
error reset
</button>
<ErrorBoundary resetKeys={[resetKey]} fallback="error">
<ThrowErrorComponent />
</ErrorBoundary>
<DeepComponent resetKeys={[resetKey]} />
</Fragment>
)
}
const DeepComponent = ({ resetKeys }) => (
<ErrorBoundary resetKeys={resetKeys} fallback="error">
<ThrowErrorComponent />
<ErrorBoundary resetKeys={resetKeys} fallback="error">
<ThrowErrorComponent />
</ErrorBoundary>
</ErrorBoundary>
)