๐ก 04. ๋น๋๊ธฐ atom + Suspense โ ๋ก๋ฉ ์ง์ฅ ํ์ถ
๐ ๊ฐ์
async read atom, AbortController(signal), Suspense+ErrorBoundary ์ฐ๋, loadable() ์ ํธ์ ํตํด ๋น๋๊ธฐ ์ํ๋ฅผ ์ ์ธ์ ์ผ๋ก ์ฒ๋ฆฌํ๋ ๋ฒ์ ๋ฐฐ์๋๋ค.
๐ฏ ์ด ์น์ ์ ์ฝ๊ณ ๋๋ฉด:
atom(async (get) => {...})ํจํด์ผ๋ก ๋น๋๊ธฐ ์ํ๋ฅผ ์ ์ธ์ ์ผ๋ก ์ฒ๋ฆฌํ ์ ์๋ค.signal์ ํตํ AbortController ์ฐ๋์ผ๋ก ์ด์ ์์ฒญ์ ์๋ ์ทจ์ํ๋ ๋ฒ์ ์๋ค.loadable()์ ํธ๋ก Suspense ์์ด ๋ก๋ฉ/์๋ฌ ์ํ๋ฅผ ์ง์ ๋ค๋ฃจ๋ ๋ฒ์ ์ค๋ช ํ ์ ์๋ค.
๐ ๋ชฉ์ฐจ
- ๐ ์ด ๋ฌธ์๋ฅผ ์ฝ๊ธฐ ์ ์
- ๐ค ์ ๋น๋๊ธฐ atom + Suspense ์ธ๊ฐ?
- ๐ฎ async read atom ๊ธฐ๋ณธ
- ๐ options.signal โ AbortController ์๋ ์ฐ๋
- ๐งฑ Suspense + ErrorBoundary ์ฐ๋
- ๐ loadable() ์ ํธ โ Suspense ์์ด ๋ก๋ฉ ๋ค๋ฃจ๊ธฐ
- โ๏ธ TanStack Query ์์ ์ญํ ๋น๊ต
- ๐ฅ ์๋ฌ ํด๊ฒฐ ์นดํ๋ก๊ทธ
- ๐ ์ด๋ฒ์ ๋ฐฐ์ด ๋ด์ฉ ์ด์ ๋ฆฌ
- ๐ ๋ง๋ฌด๋ฆฌ ํด์ฆ
- ๐ฃ ์์ฒ ์ด์ ํด๊ทผ ์ผ๊ธฐ
- ๐ ๋ ์์๋ณด๊ธฐ
๐ ์ด ๋ฌธ์๋ฅผ ์ฝ๊ธฐ ์ ์
โฑ๏ธ ์์ ์ฝ๊ธฐ ์๊ฐ: 22๋ถ (์ ์ฒด) / ํต์ฌ ํํธ๋ง: 13๋ถ
๐บ๏ธ ์ด ๋ฌธ์์ ํ๋ฆ
[๋ช ๋ น์ isLoading ์ง์ฅ] โ [์ ์ธ์ async atom] โ [AbortController signal] โ [Suspense + ErrorBoundary] โ [loadable() ๋์] โ [TanStack Query ์ญํ ๋ถ๋ฆฌ]
๐บ๏ธ ์ด ๋ฌธ์์ ๋ฐฐ๊ฒฝ ์ธ๊ณ๊ด: '์์๋ค ์ปค๋ฎค๋ํฐ'
์ค๋์ ์์ฒ ์ด๊ฐ ์คํฐ๋ ๋ชฉ๋ก API ์ฐ๋ ์์ ์ ํ๋ ๋ ์ด์ผ.
๐ฃ ์์ฒ : (์์ ๋ง๋งํ๊ฒ) "์ํธ ๋! ์คํฐ๋ ๋ชฉ๋ก API ์ฐ๋ ์๋ฃํ์ด์. ํ๋ฒ ๋ด์ฃผ์ธ์."
๐ฆ ์ํธ ๋: (์ฝ๋ ๋ฆฌ๋ทฐ ํ๋ฉด์ ์ด๋ฉฐ) "์์ฒ ๋,
isLoading,isError,datauseState3๊ฐ์useEffectํ๋... ์ปดํฌ๋ํธ ํ ๊ฐ์ ๋น๋๊ธฐ ๋ก์ง์ด ์ด๋ ๊ฒ ๋ง์ด ๋ค์ด๊ฐ ์์ผ๋ฉด ๊ทธ๊ฒ ๋ฐ๋ก '๋ก๋ฉ ์ง์ฅ'์ด์์."๐ฃ ์์ฒ : "๊ทผ๋ฐ ์ด๊ฒ ๋ค ํ์ํ ๊ฑฐ ์๋๊ฐ์...? ๋ก๋ฉ ์ํ ๋ณด์ฌ์ค์ผ ํ๊ณ , ์๋ฌ ์ฒ๋ฆฌ๋ ํด์ผ ํ๊ณ ..."
๐ฆ ์ํธ ๋: "๋ง์์. ๊ทผ๋ฐ ๊ทธ ์ฑ ์์ ์ปดํฌ๋ํธ๊ฐ ํผ์ ๋ค ์ง์ด์ง ํ์๋ ์์ด์.
async atomํ๋๊ฐ ๊ทธ ์ง์ ๋์ ์ง ์ ์๊ฑฐ๋ ์. ๊ทธ๋ฆฌ๊ณSuspense๊ฐ ๋ก๋ฉ UI ๋ฅผ ์ ์ธ์ ์ผ๋ก ์ฒ๋ฆฌํด์ค์."๐ฃ ์์ฒ : (์ง์ง๋ก ๋ฏฟ๊ธฐ์ง ์๋ ํ์ ) "๊ทธ๊ฒ ๋ง์ด ๋ผ์...?"
๐ค ์ ๋น๋๊ธฐ atom + Suspense ์ธ๊ฐ? ๐ข
๐ฏ ์ด ์น์ ์ ์ฝ๊ณ ๋๋ฉด:
- ๋ช ๋ น์ (Imperative) ๋น๋๊ธฐ ์ฒ๋ฆฌ์ ๋ฌธ์ ์ ์ ๊ตฌ์ฒด์ ์ผ๋ก ์ค๋ช ํ ์ ์๋ค
- ์ ์ธ์ (Declarative) ๋น๋๊ธฐ ์ฒ๋ฆฌ๊ฐ ์ ์ฝ๋ ํ์ง์ ๋์ด๋์ง ์ดํดํ๋ค
๐ค ์ ๊น, ๋จผ์ ์๊ฐํด๋ด
isLoading,isError,data๋ฅผuseState3๊ฐ๋ก ๊ด๋ฆฌํ๋ฉด ์ด๋ค ๋ฌธ์ ๊ฐ ์๊ธธ๊น?
"๊ฐ ์ํ์ ์กฐํฉ์ด ๋ง์ด ์ ๋๋ ๊ฒฝ์ฐ๊ฐ ์๊ธธ ์๋ ์๊ฒ ๋ค" ๊น์ง ๋ ์ฌ๋ ธ์ผ๋ฉด ์ถฉ๋ถํด.
๋ช ๋ น์ ์ ๊ทผ โ ์์ฒ ์ด์ "๋ก๋ฉ ์ง์ฅ"
์์ฒ ์ด๊ฐ ์ฒ์ ์ง ์คํฐ๋ ๋ชฉ๋ก ์ปดํฌ๋ํธ์ผ:
// โ ๐ฃ ์์ฒ : "์ผ๋จ ์ด๋ ๊ฒ ํ๋ฉด ๋์ง ์์๊น์? ๋ค ๋ฃ์ผ๋ฉด ๋์์์"
const StudyList = () => {
const [studies, setStudies] = useState<Study[]>([])
const [isLoading, setIsLoading] = useState(false)
const [isError, setIsError] = useState(false)
const [error, setError] = useState<Error | null>(null)
// ๐ฃ ์์ฒ : "๋ฒ์จ useState 4๊ฐ... ๊ทผ๋ฐ ์ด๊ฒ ๋ง๋ ๊ฒ ๊ฐ์๋ฐ์?"
useEffect(() => {
setIsLoading(true)
setIsError(false) // ๐ฃ ์์ฒ : "์ด๊ฑฐ ์ด๊ธฐํ๋ ํด์ค์ผ ํ๊ณ ..."
fetch('/api/studies')
.then((res) => res.json())
.then((data) => {
setStudies(data)
setIsLoading(false)
})
.catch((err) => {
setError(err)
setIsError(true)
setIsLoading(false) // ๐ฃ ์์ฒ : "์๋ฌ์ผ ๋๋ isLoading ๋๋ ๊ฑฐ ์์ผ๋ฉด ์ ๋๊ณ ..."
})
}, [])
if (isLoading) return <Spinner />
if (isError) return <ErrorMessage error={error} />
return <ul>{studies.map((s) => <StudyCard key={s.id} study={s} />)}</ul>
}์ํธ ๋์ ์ฝ๋ ๋ฆฌ๋ทฐ:
๐ฆ ์ํธ: "์์ฒ ๋, ์ด ์ฝ๋์ ์จ์ด์๋ ํญํ์ ์ฐพ์๋ณผ๊น์?
1. isLoading=true, isError=true ๊ฐ ๋์์ ์ฑ๋ฆฝํ ์ ์์ด์. ๋
ผ๋ฆฌ์ ์ผ๋ก ๋ถ๊ฐ๋ฅํ ์ํ์์.
2. ๊ฒ์์ด๊ฐ ๋ฐ๋ ๋๋ง๋ค ์ด useEffect ๋ฅผ ๋ค ๋ณต์ฌํด์ ์จ์ผ ํด์.
3. ์ด ์ปดํฌ๋ํธ๊ฐ ๋ง์ดํธ ํด์ ๋๋ฉด fetch ๊ฐ ์ทจ์๋์ง ์์์ โ ๋ฉ๋ชจ๋ฆฌ ๋์์์."
์ด๊ฒ ๋ฐ๋ก ๋ช ๋ น์ (Imperative) ๋น๋๊ธฐ ์ฒ๋ฆฌ ์ ๋ฌธ์ ์ผ. ์ํ๋ฅผ ์ง์ ํ๋ํ๋ ๋ช ๋ นํด์ ๊ด๋ฆฌํ๋ค ๋ณด๋ฉด, ์ํ๋ค ์ฌ์ด์ ๋ ผ๋ฆฌ์ ๋ถ์ผ์น๊ฐ ์๊ธฐ๊ณ ์ปดํฌ๋ํธ๊ฐ ์ ์ ๋ฑ๋ฑํด์ ธ.
์ ์ธ์ ์ ๊ทผ โ "์ด๋ค ์ํ์ธ์ง"๋ฅผ ์ ์ธํ๋ค
// โ
๐ฆ ์ํธ: "async atom ํ๋๋ฉด ์ด ์ง์ ์ ๋ถ ๋์ ์ง ์ ์์ด์"
const studyListAtom = atom(async (get) => {
const res = await fetch('/api/studies')
if (!res.ok) throw new Error('์คํฐ๋ ๋ชฉ๋ก ๋ถ๋ฌ์ค๊ธฐ ์คํจ')
return res.json() as Promise<Study[]>
})
// ์ปดํฌ๋ํธ๋ ์ด๊ฒ ์ ๋ถ์ผ โ ๋ก๋ฉ? Suspense ๊ฐ ์ฒ๋ฆฌ. ์๋ฌ? ErrorBoundary ๊ฐ ์ฒ๋ฆฌ.
const StudyList = () => {
const studies = useAtomValue(studyListAtom)
// ๐ฆ ์ํธ: "studies ๋ ํญ์ Study[] ํ์
์ด์์. undefined, null ์ฒดํฌ ํ์ ์์ด์."
return <ul>{studies.map((s) => <StudyCard key={s.id} study={s} />)}</ul>
}๐ก ํ ์ค๋ก ๊ธฐ์ตํ๊ธฐ
๋ช ๋ น์ ์ฒ๋ฆฌ๋ "๋ก๋ฉ์ด๋ฉด ์คํผ๋ ๋ณด์ฌ์ค, ์๋ฌ๋ฉด ์๋ฌ ๋ฉ์์ง ๋ณด์ฌ์ค, ์๋๋ฉด ๋ชฉ๋ก ๋ณด์ฌ์ค" ๋ผ๊ณ ์ปดํฌ๋ํธ์ ์ธ์ธํ๊ฒ ์ง์ํด. ์ ์ธ์ ์ฒ๋ฆฌ๋ "๋ฐ์ดํฐ๊ฐ ์ค๋น๋์ ๋๋ง ์ด๊ฑธ ๊ทธ๋ ค์ค" ๋ผ๊ณ ์๋๋ง ์ ์ธํด. Suspense ์ ErrorBoundary ๊ฐ ๋๋จธ์ง๋ฅผ ๋งก์.
๐ฎ async read atom ๊ธฐ๋ณธ ๐ข
๐ฏ ์ด ์น์ ์ ์ฝ๊ณ ๋๋ฉด:
atom(async (get) => {...})๋ฌธ๋ฒ์ ์ฌ๋ฐ๋ฅด๊ฒ ์ฌ์ฉํ ์ ์๋ค- async atom ์ ์ธ ๋
Suspense๊ฐ ๋ฐ๋์ ํ์ํ ์ด์ ๋ฅผ ์ค๋ช ํ ์ ์๋ค
๊ธฐ๋ณธ ๋ฌธ๋ฒ
import { atom } from 'jotai'
// ๐ฆ ์ํธ: "read ํจ์์ async ๋ฅผ ๋ถ์ด๋ ๊ฒ๋ง์ผ๋ก async atom ์ด ๋ผ์"
const studyListAtom = atom(async (get) => {
const response = await fetch('/api/studies')
if (!response.ok) {
throw new Error(`API ์ค๋ฅ: ${response.status}`)
}
return response.json() as Promise<Study[]>
})๋ค๋ฅธ atom ์ ์์กดํ๋ async atom
derived atom ์ฒ๋ผ, async atom ๋ ๋ค๋ฅธ atom ์ ๊ฐ์ ์์กดํ ์ ์์ด:
const searchKeywordAtom = atom('')
// ๐ฆ ์ํธ: "searchKeyword ๊ฐ ๋ฐ๋ ๋๋ง๋ค ์ด atom ์ด ์๋์ผ๋ก ์ฌ์คํ๋ผ์"
const filteredStudyListAtom = atom(async (get) => {
const keyword = get(searchKeywordAtom) // ๋๊ธฐ atom ๊ตฌ๋
const response = await fetch(`/api/studies?q=${encodeURIComponent(keyword)}`)
if (!response.ok) throw new Error('๊ฒ์ ์คํจ')
return response.json() as Promise<Study[]>
})๐ ์ฐ๊ฒฐ ๊ณ ๋ฆฌ
์ด ํจํด์ด derived atom ์ async ๋ฒ์ ์ด์ผ. 03. derived atom ๊ณผ ํจ๊ป ์ฝ์ผ๋ฉด ๋ ๋ช ํํด.
Suspense ๊ฐ ๋ฐ๋์ ํ์ํ ์ด์
async atom ์ ์ฌ์ฉํ๋ ์ปดํฌ๋ํธ๋ Promise ๊ฐ resolve ๋๊ธฐ ์ ๊น์ง ๋ ๋๋ง์ "์ค๋จ(suspend)" ํด. React ๋ ์ด ์ค๋จ ์ ํธ๋ฅผ ๋ฐ์์ ๊ฐ์ฅ ๊ฐ๊น์ด ์์ <Suspense> ์ fallback ์ ๋ณด์ฌ์ค.
// โ ๐ฃ ์์ฒ : "์ด๋ ๊ฒ ํ๋ฉด ๋์ง ์๋์?"
const App = () => (
// Suspense ์์ด async atom ์ฐ๋ ์ปดํฌ๋ํธ ๋ ๋๋ง ์๋
<StudyList /> // React ๊ฐ "์ค๋จ ์ ํธ๋ฅผ ๋ฐ์ Suspense ๊ฐ ์์ด์!" ์๋ฌ๋ฅผ ๋์ ธ
)// โ
๐ฆ ์ํธ: "Suspense ๋ก ๋ฐ๋์ ๊ฐ์ธ์ผ ํด์"
const App = () => (
<Suspense fallback={<StudyListSkeleton />}>
<StudyList />
{/* Promise ๊ฐ pending ์ด๋ฉด โ <StudyListSkeleton /> ํ์ */}
{/* Promise ๊ฐ resolve ๋๋ฉด โ <StudyList /> ๋ ๋๋ง */}
</Suspense>
)๐ก ํ Jotai ๋ ํผ๋ฐ์ค์๋ "Provider ๊ฐ ์์ ๊ฒฝ์ฐ,
<Suspense>๋ฅผ Provider ๋ด๋ถ์ ๋ฐฐ์นํ๋ผ" ๊ณ ๋ช ์๋ผ ์์ด. Provider ๋ฐ๊นฅ์ Suspense ๋ฅผ ๋๋ฉด ๋ฌดํ ๋ ๋๋ง ๋ฃจํ๊ฐ ์๊ธธ ์ ์์ด.// โ ์ฌ๋ฐ๋ฅธ ์์ <Provider> <Suspense fallback="๋ก๋ฉ ์ค..."> <Layout /> </Suspense> </Provider>
async atom ์ด ๋ ๋ค๋ฅธ async atom ์ ์์กดํ ๋
const rawStudyListAtom = atom(async (get) => {
const res = await fetch('/api/studies')
return res.json() as Promise<Study[]>
})
// ๐ฆ ์ํธ: "async atom ์ get ์ผ๋ก ๊ฐ์ ธ์ฌ ๋๋ await ๋ฅผ ๋ถ์ฌ์"
const enrichedStudyListAtom = atom(async (get) => {
const studies = await get(rawStudyListAtom) // await ํ์!
return studies.filter((s) => s.isActive).map((s) => ({
...s,
memberCount: s.members.length,
}))
})๐ options.signal โ AbortController ์๋ ์ฐ๋ ๐ก
๐ฏ ์ด ์น์ ์ ์ฝ๊ณ ๋๋ฉด:
signal์ต์ ์ผ๋ก ์ด์ ์์ฒญ์ ์๋ ์ทจ์ํ๋ ๋ฒ์ ์ ์ ์๋ค- ์๋์์ฑ(debounce) ๊ฒ์์์
signal์ด ์ ํ์์ธ์ง ์ดํดํ๋ค
๐ค ์ ๊น, ๋จผ์ ์๊ฐํด๋ด
์ฌ์ฉ์๊ฐ ๊ฒ์์ฐฝ์ "R", "Re", "Rea", "Reac", "React" ๋ฅผ ๋น ๋ฅด๊ฒ ์ ๋ ฅํ๋ฉด API ์์ฒญ์ด 5๋ฒ ๋๊ฐ. "React" ๊ฒฐ๊ณผ๋ณด๋ค "R" ๊ฒฐ๊ณผ๊ฐ ๋ฆ๊ฒ ๋์์ค๋ฉด ์ด๋ค ์ผ์ด ์๊ธธ๊น?
Race Condition โ ์์ฒญ ์์์ ์๋ต ์์๊ฐ ๋ค๋ฐ๋๋ ์ฌ์
์์ฒญ 1: "R" โ ์๋ต 2์ด ํ ๋์ฐฉ (๋๋ฆฐ ์๋ฒ)
์์ฒญ 2: "Re" โ ์๋ต 1.8์ด ํ ๋์ฐฉ
์์ฒญ 3: "Rea" โ ์๋ต 1.5์ด ํ ๋์ฐฉ
์์ฒญ 4: "Reac" โ ์๋ต 1.2์ด ํ ๋์ฐฉ
์์ฒญ 5: "React" โ ์๋ต 1์ด ํ ๋์ฐฉ (๋จผ์ ๋์ฐฉ!)
ํ๋ฉด: "React" ๊ฒฐ๊ณผ ํ์
โ 1.2์ด ํ: "Reac" ๊ฒฐ๊ณผ๋ก ๋ฎ์ด์
โ 1.5์ด ํ: "Rea" ๊ฒฐ๊ณผ๋ก ๋ฎ์ด์
โ ์ต์ข
ํ๋ฉด: "R" ์ ๋ํ ๊ฒฐ๊ณผ๊ฐ ๋ณด์ ๐ฑ
Jotai ์ signal ์ผ๋ก ์๋ ํด๊ฒฐ
Jotai ๋ async read atom ์ ๋ ๋ฒ์งธ ์ธ์๋ก { signal } ์ ์ ๊ณตํด. ์ด signal ์ atom ์ด ์ฌ์คํ๋๊ธฐ ์ง์ ์ ์๋์ผ๋ก abort ๋ผ:
// โ
๐ฆ ์ํธ: "signal ํ๋๋ก Race Condition ์ ์์ ํ ์ฐจ๋จํ ์ ์์ด์"
const searchKeywordAtom = atom('')
const searchResultAtom = atom(async (get, { signal }) => {
// ๐ฆ ์ํธ: "searchKeyword ๊ฐ ๋ฐ๋ ๋๋ง๋ค ์ด์ ์์ฒญ์ signal ์ด abort ๋ผ์"
const keyword = get(searchKeywordAtom)
if (!keyword.trim()) return [] // ๋น ๊ฒ์์ด๋ ๋น ๋ฐฐ์ด ๋ฐํ
const response = await fetch(
`/api/studies/search?q=${encodeURIComponent(keyword)}`,
{ signal } // ๐ฆ ์ํธ: "fetch ์ signal ์ ๋๊ธฐ๋ฉด ์ด์ ์์ฒญ์ด ์๋ ์ทจ์๋ผ์"
)
if (!response.ok) throw new Error('๊ฒ์ ์คํจ')
return response.json() as Promise<Study[]>
// ๐ฃ ์์ฒ : "์ด๊ฒ ์ง์ง์์? signal ํ๋๋ก race condition ์ด ํด๊ฒฐ๋๋ค๊ณ ์?"
})// ๊ฒ์ ์ปดํฌ๋ํธ โ ์๋์์ฑ UI
const StudySearchBar = () => {
const [keyword, setKeyword] = useAtom(searchKeywordAtom)
return (
<input
value={keyword}
onChange={(e) => setKeyword(e.target.value)}
placeholder="์คํฐ๋ ๊ฒ์..."
/>
)
}
const StudySearchResults = () => {
// ๐ฆ ์ํธ: "์ด ์ปดํฌ๋ํธ๋ ํญ์ ์ต์ ๊ฒ์ ๊ฒฐ๊ณผ๋ง ๋ณด์ฌ์ค์"
const results = useAtomValue(searchResultAtom)
return (
<ul>
{results.map((study) => (
<li key={study.id}>{study.title}</li>
))}
</ul>
)
}
// ๋ ์ด์์
const SearchPage = () => (
<div>
<StudySearchBar />
<Suspense fallback={<SearchSkeleton />}>
<StudySearchResults />
</Suspense>
</div>
)signal ์ด abort ๋ ๋ ๋ด๋ถ์์ ์ผ์ด๋๋ ์ผ
์ฌ์ฉ์: "Re" ์
๋ ฅ
โ
searchKeyword ๋ณ๊ฒฝ โ searchResultAtom ์ฌ์คํ ์์ฝ
โ
์ด์ "R" ์์ฒญ์ signal.abort() ์๋ ํธ์ถ
โ
fetch('/api/studies/search?q=R', { signal }) โ AbortError ๋ฐ์
โ
Jotai ๊ฐ AbortError ๋ฅผ ์กฐ์ฉํ ๋ฌด์ (ํ๋ฉด์ ํ์ ์ ๋จ)
โ
"Re" ์ ๋ํ ์ ์์ฒญ ์์
๐ก ํ ์ค๋ก ๊ธฐ์ตํ๊ธฐ
signal์ "๊ฒ์์ฐฝ์ ๋ญ๊ฐ ์๋ก ์น ๋๋ง๋ค ์ด์ ํ์๋ฅผ ์บ์ฌํ๋ ๋ฒํผ" ์ด์ผ. Jotai ๊ฐ ์์์ ๋๋ฌ์ค.
๐งฑ Suspense + ErrorBoundary ์ฐ๋ ๐ก
๐ฏ ์ด ์น์ ์ ์ฝ๊ณ ๋๋ฉด:
<Suspense>์<ErrorBoundary>๋ฅผ ์กฐํฉํ๋ ์ฌ๋ฐ๋ฅธ ํจํด์ ์ ์ ์๋ค- Next.js App Router ์์ ์ด ํจํด์ ์ ์ฉํ๋ ๋ฒ์ ์ดํดํ๋ค
์์ ํ ์ ์ธ์ ๋น๋๊ธฐ UI ํจํด
// ์๋ฌ ๋ฐ์ด๋๋ฆฌ (react-error-boundary ๋ผ์ด๋ธ๋ฌ๋ฆฌ ์ฌ์ฉ)
import { ErrorBoundary } from 'react-error-boundary'
// ๐ฆ ์ํธ: "์ด 3๊ฐ์ ๋ ์ด์ด๊ฐ ๊ฐ์ ํ ์ผ์ ๋ช
ํํ ๋ถ๋ดํด์"
const StudySection = () => (
<ErrorBoundary
fallback={<StudyErrorFallback />}
// ์๋ฌ๊ฐ throw ๋๋ฉด ErrorBoundary ๊ฐ ์ก์์ fallback ํ์
>
<Suspense fallback={<StudyListSkeleton />}>
{/* Promise pending ์ด๋ฉด Skeleton ํ์, resolve ๋๋ฉด StudyList ํ์ */}
<StudyList />
</Suspense>
</ErrorBoundary>
)
// ์๋ฌ ๋ฐ์ ์ ํ์ํ ์ปดํฌ๋ํธ
const StudyErrorFallback = () => (
<div className="error-box">
<p>์คํฐ๋ ๋ชฉ๋ก์ ๋ถ๋ฌ์ค์ง ๋ชปํ์ด์ ๐ข</p>
<button onClick={() => window.location.reload()}>๋ค์ ์๋</button>
</div>
)Next.js App Router ์์์ ํจํด
Next.js App Router ์์๋ loading.tsx ์ error.tsx ํ์ผ์ด ๊ฐ๊ฐ Suspense ์ ErrorBoundary ์ญํ ์ ํด:
// app/studies/loading.tsx โ ์๋์ผ๋ก Suspense fallback ์ญํ
export default function StudiesLoading() {
return <StudyListSkeleton />
}
// app/studies/error.tsx โ ์๋์ผ๋ก ErrorBoundary fallback ์ญํ
'use client'
export default function StudiesError({
error,
reset,
}: {
error: Error
reset: () => void
}) {
return (
<div>
<p>์ค๋ฅ๊ฐ ๋ฐ์ํ์ด์: {error.message}</p>
<button onClick={reset}>๋ค์ ์๋</button>
</div>
)
}
// app/studies/page.tsx โ Server Component
// ๐ฆ ์ํธ: "ํ์ด์ง ์์ฒด๋ Server Component ๋ก ์ ์งํ๊ณ ,
// atom ์ ์ฐ๋ ๋ถ๋ถ์ Client Component ๋ก ๋ถ๋ฆฌํด์"
import { Suspense } from 'react'
import { StudyListClient } from './StudyListClient'
export default function StudiesPage() {
return (
<main>
<h1>์คํฐ๋ ๋ชฉ๋ก</h1>
<Suspense fallback={<StudyListSkeleton />}>
<StudyListClient />
</Suspense>
</main>
)
}// app/studies/StudyListClient.tsx โ Client Component
'use client'
import { useAtomValue } from 'jotai'
import { studyListAtom } from '@/atoms/study'
export const StudyListClient = () => {
// ๐ฃ ์์ฒ : "์ปดํฌ๋ํธ ์ฝ๋๊ฐ ์ด๊ฒ ๋ค์์? ๋ก๋ฉ ๋ถ๊ธฐ ํ๋๋ ์๊ณ ?"
const studies = useAtomValue(studyListAtom)
return (
<ul>
{studies.map((study) => (
<StudyCard key={study.id} study={study} />
))}
</ul>
)
}์ค์ฒฉ Suspense โ ์ธ๋ฐํ ๋ก๋ฉ UI ์ ์ด
์ปดํฌ๋ํธ ํธ๋ฆฌ์ ์ฌ๋ฌ ๊ณณ์ Suspense ๋ฅผ ๋ฐฐ์นํ๋ฉด, ๊ฐ ์์ญ์ ๋ก๋ฉ ์ํ๋ฅผ ๋
๋ฆฝ์ ์ผ๋ก ๊ด๋ฆฌํ ์ ์์ด:
// ๐ฆ ์ํธ: "Suspense ๋ฅผ ์ธ๋ถํํ๋ฉด ์ผ๋ถ๋ง ๋ก๋ฉ ์ค์๋ ๋๋จธ์ง๋ ๋ณด์ฌ์ค ์ ์์ด์"
const StudyPage = () => (
<div>
{/* ํค๋๋ ํญ์ ํ์ */}
<StudyPageHeader />
<div className="layout">
{/* ํํฐ๋ ๋
๋ฆฝ์ ์ผ๋ก ๋ก๋ฉ */}
<Suspense fallback={<FilterSkeleton />}>
<StudyFilterPanel />
</Suspense>
{/* ๋ชฉ๋ก์ ๋
๋ฆฝ์ ์ผ๋ก ๋ก๋ฉ */}
<Suspense fallback={<StudyListSkeleton />}>
<StudyList />
</Suspense>
</div>
</div>
)๐ loadable() ์ ํธ โ Suspense ์์ด ๋ก๋ฉ ๋ค๋ฃจ๊ธฐ ๐ก
๐ฏ ์ด ์น์ ์ ์ฝ๊ณ ๋๋ฉด:
loadable()์ด ์ด๋ค ์ํฉ์์ ์ ์ฉํ์ง ์ ์ ์๋คloadable()์ ๋ฐํ ํ์ (loading | hasData | hasError)์ ํ์ฉํ ์ ์๋ค
loadable() ์ด ํ์ํ ์ํฉ
Suspense ๋ฅผ ์ธ ์ ์๊ฑฐ๋ ์ํ์ง ์๋ ์ํฉ์ด ์์ด:
- ์๋ํํฐ ์ปดํฌ๋ํธ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๊ฐ Suspense ์ ํธํ๋์ง ์์ ๋
- ๋ก๋ฉ/์๋ฌ/๋ฐ์ดํฐ ์ํ๋ฅผ ํ ์ปดํฌ๋ํธ ์์์ ์ง์ ์ฒ๋ฆฌํ๊ณ ์ถ์ ๋
- ๊ธฐ์กด ์ฝ๋๋ฒ ์ด์ค์ ์ ์ง์ ์ผ๋ก ๋์ ํ ๋
import { loadable } from 'jotai/utils'
// ๐ฆ ์ํธ: "loadable ๋ก ๊ฐ์ธ๋ฉด Suspense ์์ด ์ํ๋ฅผ ์ง์ ์ฝ์ ์ ์์ด์"
const loadableStudyListAtom = loadable(studyListAtom)
const StudyListWithLoadable = () => {
// ๐ฃ ์์ฒ : "ํ์
์ด ๋ญ๊ฐ ๋์ค๋ ๊ฑฐ์์?"
const studiesLoadable = useAtomValue(loadableStudyListAtom)
// studiesLoadable ํ์
:
// { state: 'loading' }
// { state: 'hasData'; data: Study[] }
// { state: 'hasError'; error: unknown }
if (studiesLoadable.state === 'loading') {
return <StudyListSkeleton />
}
if (studiesLoadable.state === 'hasError') {
return (
<div>
์๋ฌ: {studiesLoadable.error instanceof Error
? studiesLoadable.error.message
: '์ ์ ์๋ ์ค๋ฅ'}
</div>
)
}
// state === 'hasData' ์ด๋ฉด data ์ ๊ทผ ๊ฐ๋ฅ
return (
<ul>
{studiesLoadable.data.map((study) => (
<StudyCard key={study.id} study={study} />
))}
</ul>
)
}Suspense vs loadable() ์ ํ ๊ธฐ์ค
| ์ํฉ | ์ถ์ฒ |
|---|---|
| ์ ํ๋ก์ ํธ, ์ปดํฌ๋ํธ ํธ๋ฆฌ ์ค๊ณ ์์ ๋ก์ | โ
Suspense + ErrorBoundary |
| ๋ก๋ฉ/์๋ฌ/๋ฐ์ดํฐ๋ฅผ ํ ์ปดํฌ๋ํธ์์ ์ธ๋ฐํ ์ ์ด | โ
loadable() |
| ์๋ํํฐ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๊ฐ Suspense ๋ฏธ์ง์ | โ
loadable() |
Next.js App Router ์ loading.tsx ํ์ฉ | โ
Suspense (์๋ ํตํฉ) |
๊ธฐ์กด isLoading/isError ํจํด์ ์ ์ง์ ์ผ๋ก ๋ง์ด๊ทธ๋ ์ด์
| โ
loadable() (์ค๊ฐ ๋จ๊ณ) |
๐ก ํ
loadable()์ Suspense ๋ฅผ ์์ ํ ๋์ฒดํ๋ ๊ฒ ์๋๋ผ ๋ณด์ ํ๋ ๋๊ตฌ์ผ. ์์์ Suspense ๋ก, ์ธ๋ฐํ ์ ์ด๊ฐ ํ์ํ ๊ณณ๋งloadable()๋ก ์ ํํ๋ ์ ๋ต์ด ์ข์.
โ๏ธ TanStack Query ์์ ์ญํ ๋น๊ต ๐ก
๐ฏ ์ด ์น์ ์ ์ฝ๊ณ ๋๋ฉด:
- "์๋ฒ ์ํ๋ Query, ํ์/ํด๋ผ์ด์ธํธ ์ํ๋ Jotai" ์์น์ ์๋ฏธ๋ฅผ ์ดํดํ๋ค
- ๋ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ํจ๊ป ์ธ ๋ ๊ฐ์์ ์ญํ ์ ๋ช ํํ ์ค๋ช ํ ์ ์๋ค
์์๋ค ํ์ TanStack Query ์ Jotai ๋ฅผ ํจ๊ป ์ฐ๊ณ ์์ด. ์ด ๋ ๋ผ์ด๋ธ๋ฌ๋ฆฌ์ ์ญํ ์ ํท๊ฐ๋ฆฌ๋ฉด ์ค๋ณต ์ฝ๋๊ฐ ์๊ฒจ:
์ญํ ๋ถ๋ฆฌ ์์น
TanStack Query ๊ฐ ๋ด๋น
โโโ ์๋ฒ ๋ฐ์ดํฐ ํ์นญ (์บ์ฑ, ์ฌ์๋, invalidation, stale-while-revalidate)
โโโ ์๋ฒ ์ํ์ ํด๋ผ์ด์ธํธ ์ํ ๋๊ธฐํ
Jotai ๊ฐ ๋ด๋น
โโโ ํด๋ผ์ด์ธํธ UI ์ํ (๋ชจ๋ฌ ์ด๋ฆผ ์ฌ๋ถ, ์ ํ๋ ํญ, ํํฐ ๊ฐ)
โโโ ์๋ฒ ๋ฐ์ดํฐ๋ก๋ถํฐ ํ์๋ ํด๋ผ์ด์ธํธ ๊ณ์ฐ ๊ฒฐ๊ณผ
โโโ ์ปดํฌ๋ํธ ๊ฐ ๊ณต์ ๋์ด์ผ ํ๋ UI ์ํ
// โ
์ญํ ์ ๋ช
ํํ ๋ถ๋ฆฌํ ์์๋ค ํ์ ์ฝ๋
// ๐ฆ ์ํธ: "์๋ฒ ๋ฐ์ดํฐ๋ Query, UI ์ํ์ ํ์ ์ํ๋ Jotai"
// --- TanStack Query: ์๋ฒ ์ํ ๊ด๋ฆฌ ---
const useStudiesQuery = () =>
useQuery({
queryKey: ['studies'],
queryFn: () => fetch('/api/studies').then((r) => r.json()),
staleTime: 60 * 1000, // 1๋ถ๊ฐ fresh ์ ์ง
})
// --- Jotai: ํด๋ผ์ด์ธํธ UI ์ํ ---
const searchKeywordAtom = atom('')
const selectedTagAtom = atom<string | null>(null)
const isFilterOpenAtom = atom(false)
// --- Jotai: ์๋ฒ ๋ฐ์ดํฐ๋ฅผ ํ์์ํจ ํด๋ผ์ด์ธํธ ๊ณ์ฐ ---
// Query ๊ฒฐ๊ณผ๋ฅผ ์ง์ atom ์ ์ฃผ์
ํ๋ ํจํด
const rawStudiesAtom = atom<Study[]>([]) // Query ๊ฒฐ๊ณผ๋ฅผ ์ ์ฅ
const filteredStudiesAtom = atom((get) => {
const studies = get(rawStudiesAtom)
const keyword = get(searchKeywordAtom)
const tag = get(selectedTagAtom)
return studies
.filter((s) => s.title.includes(keyword))
.filter((s) => !tag || s.tags.includes(tag))
})
// Query ๊ฒฐ๊ณผ๋ฅผ atom ์ ๋๊ธฐํํ๋ ์ปดํฌ๋ํธ
const StudyDataSync = () => {
const { data: studies = [] } = useStudiesQuery()
const setRawStudies = useSetAtom(rawStudiesAtom)
useEffect(() => {
setRawStudies(studies)
}, [studies, setRawStudies])
return null
}๐ ๋ ๊น์ด ํ๊ณ ๋ค๊ธฐ
TanStack Query ์ Jotai ์ ํ์ ํจํด์ 12. TanStack Query + Jotai ์ค์ ์กฐํฉ ์์ ์ํคํ ์ฒ ์์ค์ผ๋ก ๋ค๋ค.
๐ฅ ์๋ฌ ํด๊ฒฐ ์นดํ๋ก๊ทธ
์๋ฌ ๋ฉ์์ง๊ฐ ๋จ๋ฉด Ctrl+F ๋ก ๊ฒ์ํด๋ด. ๋๋ถ๋ถ ์ฌ๊ธฐ ์์ด.
โ Error: A component suspended while responding to synchronous input
์ฆ์: ๊ฒ์์ด ์ ๋ ฅ ์ React ๊ฐ ์๋ฌ๋ฅผ ๋์ง
์์ธ:
// ๐ฃ ์์ฒ : "์ ์๋ฌ๊ฐ ๋์ง? Suspense ๋ก ๊ฐ์ธ๋จ๋๋ฐ..."
const SearchResults = () => {
const [keyword, setKeyword] = useAtom(searchKeywordAtom)
// searchKeywordAtom ๋ณ๊ฒฝ โ searchResultAtom ์ฌ์คํ โ suspend
// React 18 ์ดํ์์ ๋๊ธฐ ์ด๋ฒคํธ ํธ๋ค๋ฌ ๋ด์์ suspend ์ ์๋ฌ
const results = useAtomValue(searchResultAtom)
return <ul>{...}</ul>
}ํด๊ฒฐ์ฑ :
// โ
์ฝ๊ธฐ/์ฐ๊ธฐ ์ปดํฌ๋ํธ๋ฅผ ๋ถ๋ฆฌํด์ suspend ๋ฅผ ๊ฒฉ๋ฆฌ
const SearchBar = () => {
const [keyword, setKeyword] = useAtom(searchKeywordAtom)
return <input value={keyword} onChange={(e) => setKeyword(e.target.value)} />
}
// ์ด ์ปดํฌ๋ํธ๋ง Suspense ๋ด๋ถ์ ๋ฐฐ์น
const SearchResults = () => {
const results = useAtomValue(searchResultAtom) // ์ด ์ปดํฌ๋ํธ๋ง suspend
return <ul>{results.map(...)}</ul>
}
const SearchPage = () => (
<>
<SearchBar />
<Suspense fallback={<SearchSkeleton />}>
<SearchResults />
</Suspense>
</>
)โ fetch ์ทจ์ ํ AbortError ๊ฐ ErrorBoundary ์ ์กํ
์ฆ์: ๊ฒ์ํ ๋๋ง๋ค ErrorBoundary ์ ์๋ฌ fallback ์ด ๋ณด์
์์ธ: AbortError ๋ฅผ ๋ฐ๋ก ์ฒ๋ฆฌํ์ง ์์์ ErrorBoundary ๋ก ์ ํ๋จ
ํด๊ฒฐ์ฑ :
const searchResultAtom = atom(async (get, { signal }) => {
const keyword = get(searchKeywordAtom)
try {
const response = await fetch(`/api/search?q=${keyword}`, { signal })
return response.json()
} catch (error) {
// โ
AbortError ๋ ์ ์ ์ทจ์์ด๋ฏ๋ก ๋น ๋ฐฐ์ด ๋ฐํ or rethrow ์ ํจ
if (error instanceof Error && error.name === 'AbortError') {
return [] // ๋๋ ์ด์ ๊ฒฐ๊ณผ ์ ์ง
}
throw error // ์ค์ ์๋ฌ๋ง throw
}
})โ Suspense ๋ด๋ถ์ Jotai Provider ๋ฐฐ์น ์ ๋ฌดํ ๋ฃจํ
์ฆ์: ํ์ด์ง๊ฐ ์์ํ ๋ก๋ฉ ์ค ์ํ
์์ธ: Provider ๋ฐ๊นฅ์ Suspense ๋ฅผ ๋ฐฐ์นํ๋ฉด Jotai ๊ฐ ๋ฌดํ ๋ ๋๋ง
ํด๊ฒฐ์ฑ :
// โ ์๋ชป๋ ์์
<Suspense fallback="๋ก๋ฉ...">
<Provider>
<App />
</Provider>
</Suspense>
// โ
์ฌ๋ฐ๋ฅธ ์์ โ Provider ๊ฐ Suspense ๋ฅผ ๊ฐ์ธ์ผ ํจ
<Provider>
<Suspense fallback="๋ก๋ฉ...">
<App />
</Suspense>
</Provider>๐ ์ด๋ฒ์ ๋ฐฐ์ด ๋ด์ฉ ์ด์ ๋ฆฌ
๐ ๋น๋๊ธฐ atom ์ฒ๋ฆฌ ๋ฐฉ์ ์์ฝ
| ๋ฐฉ์ | ์ฝ๋ ๋ณต์ก๋ | ์ปดํฌ๋ํธ ์ฑ ์ | ์ถ์ฒ ์ํฉ |
|---|---|---|---|
| isLoading useState | ๋์ (ํญํ 4๊ฐ) | ์ปดํฌ๋ํธ๊ฐ ์ ๋ถ | ๋ ๊ฑฐ์ ์ฝ๋ ์ ์ง๋ณด์ |
| async atom + Suspense | ๋ฎ์ | ์ ์ธ๋ง | ์ ํ๋ก์ ํธ, ํ์ค ํจํด |
| async atom + loadable() | ์ค๊ฐ | ์ปดํฌ๋ํธ๊ฐ ์ํ ๋ถ๊ธฐ | ์ธ๋ฐํ ์ ์ด ํ์ ์ |
โ ๏ธ ์ ๋ ํ์ง ๋ง ๊ฒ
| ์ํฉ | โ ๋์ ์ | โ ์ข์ ์ |
|---|---|---|
| Provider ์์น | Suspense ์์ Provider ๋ฐฐ์น | Provider ์์ Suspense ๋ฐฐ์น |
| Race Condition | signal ์์ด fetch | { signal } ๋๊ฒจ์ ์๋ ์ทจ์ |
| ๋น๋๊ธฐ ์๋ฌ ์ฒ๋ฆฌ | AbortError ๋ฌด์กฐ๊ฑด throw | AbortError ๋ ์กฐ์ฉํ ์ฒ๋ฆฌ |
๐ ๋ง๋ฌด๋ฆฌ ํด์ฆ
Q1. ๐ด ์๋์ด ๋ฉด์ ์ง๋ฌธ (์์ฒ ์ ๋ฉด์ ๋์ )
์์ฒ ์ด๊ฐ ์ด์ง ๋ฉด์ ์์ ๋ฌผ์์ด:
"Jotai ์ async atom ์์signal์ต์ ์ ์ฌ์ฉํ๋ฉด ์ด๋ค ๋ฌธ์ ๊ฐ ํด๊ฒฐ๋๋์?"
- A) ๋น๋๊ธฐ ์์ฒญ์ ์๋ต ์๋๊ฐ ๋นจ๋ผ์ง๋ค
- B) ์ด์ ๋น๋๊ธฐ ์์ฒญ์ด ์๋์ผ๋ก ์ทจ์๋์ด Race Condition ์ด ๋ฐฉ์ง๋๋ค
- C)
Suspense์์ด๋ ๋น๋๊ธฐ atom ์ ์ฌ์ฉํ ์ ์๋ค - D) ErrorBoundary ๊ฐ ํ์ ์์ด์ง๋ค
โ ์ ๋ต: B
๐ก ์์ธ ํด์ค:
- ์๋ฆฌ ์ค๋ช
:
signal์AbortController.signal์ด์ผ. Jotai ๋ async atom ์ด ์ฌ์คํ๋ ๋ ์ด์ ์คํ์ signal ์ abort ํด.fetch(url, { signal })์ ๋๊ธฐ๋ฉด, atom ์ด ์ฌ์คํ๋๋ ์๊ฐ ์ด์ fetch ์์ฒญ์ด ์๋์ผ๋ก ์ทจ์๋ผ. ์ด๋ฅผ ํตํด "์ด์ ์ ๋ณด๋ธ ์์ฒญ์ ์๋ต์ด ๋์ค์ ์จ ์๋ต์ ๋ฎ์ด์ฐ๋" Race Condition ์ด ์์ฒ ์ฐจ๋จ๋ผ. - ์ค๋ต ํผ๋๋ฐฑ: A ๋ ํ๋ ธ์ด โ signal ์ ์๋์ ๋ฌด๊ดํด. C ๋ ํ๋ ธ์ด โ
loadable()์ด ๊ทธ ์ญํ ์ด์ผ. D ๋ ํ๋ ธ์ด โ signal ์ AbortError ์ทจ์์ฉ์ด๊ณ , ์ค์ ์๋ฒ ์๋ฌ๋ ์ฌ์ ํ ErrorBoundary ๊ฐ ์ฒ๋ฆฌํด์ผ ํด. - ๐ ํต์ฌ ๊ธฐ์ต๋ฒ: "signal ์ ๊ฒ์ํ ๋๋ง๋ค ์ด์ ํ์๋ฅผ ์บ์ฌํ๋ ๋ฒํผ. Jotai ๊ฐ ์๋์ผ๋ก ๋๋ฌ์ค."
Q2. ๐ฅ ๊ธด๊ธ ๋๋ฒ๊น (์์์ UX ํผ๋๋ฐฑ)
์์ ๋์ด ํผ๋๋ฐฑ์ ๋จ๊ฒผ์ด: "๊ฒ์์ฐฝ์ ํ์ดํํ๋ฉด ํ๋ฉด์ ์๋ฌ ๋ฉ์์ง๊ฐ ๊น๋นก์๋ค ์์ด์ก๋ค ํด์. ๋ฌด์ธ๊ฐ ๋ฌธ์ ๊ฐ ์์ด ๋ณด์ฌ์."
์์ฒ ์ด๊ฐ ์ฝ๋๋ฅผ ์ด์ด๋ดค๋๋:
const searchResultAtom = atom(async (get, { signal }) => {
const keyword = get(searchKeywordAtom)
const response = await fetch(`/api/search?q=${keyword}`, { signal })
return response.json()
// ๐ฃ ์์ฒ : "signal ๋ ๋ฃ์๋๋ฐ ์ ์๋ฌ๊ฐ ๋์ง?"
})๊ฐ์ฅ ๊ฐ๋ฅ์ฑ์ด ๋์ ์์ธ๊ณผ ํด๊ฒฐ์ฑ ์?
- A)
signal๋์AbortController๋ฅผ ์ง์ ๋ง๋ค์ด์ผ ํ๋ค - B)
AbortError๋ฅผ catch ํ์ง ์์์ ErrorBoundary ๋ก ์ ํ๋๊ณ ์๋ค - C)
Suspense๋ฅผ ์ฌ๋ฌ ๊ฐ ์ค์ฒฉํด์ผ ํ๋ค - D)
loadable()๋ก ๋ณ๊ฒฝํด์ผ ํ๋ค
โ ์ ๋ต: B
๐ก ์์ธ ํด์ค:
- ์๋ฆฌ ์ค๋ช
: ๊ฒ์์ด๊ฐ ๋ฐ๋ ๋๋ง๋ค ์ด์ atom ์คํ์
signal์ด abort ๋ผ. ๊ทธ๋ฌ๋ฉดfetch์์AbortError๊ฐ throw ๋๋๋ฐ, ์ด๊ฑธ ์ก์ง ์์ผ๋ฉดErrorBoundary๋ก ์ ํ๋ผ.AbortError๋ ์ ์์ ์ธ ์ทจ์ ์ ํธ์ด๋ฏ๋ก catch ํด์ ๋ฌด์ํด์ผ ํด. - ํด๊ฒฐ์ฑ
:
try/catch๋ธ๋ก์์error.name === 'AbortError'๋ฅผ ์ฒดํฌํด์ ์กฐ์ฉํ ์ฒ๋ฆฌํ๋ฉด ๋ผ. - ๐ ํต์ฌ ๊ธฐ์ต๋ฒ: "AbortError ๋ ์๋ฌ๊ฐ ์๋๋ผ '์ผ, ๊ทธ๋งํด๋ ๋ผ' ์ ํธ์ผ. ErrorBoundary ์๊ฒ ์ ๋ฌํ ํ์ ์์ด."
Q3. ์น๊ตฌ์๊ฒ ์ค๋ช ํ๋ค๋ฉด?
Suspense+ async atom ์กฐํฉ๊ณผisLoadinguseState ์กฐํฉ์ ์ฐจ์ด๋ฅผ ๊ฐ๋ฐ์ ์น๊ตฌ์๊ฒ ํ ๋ฌธ์ฅ์ผ๋ก ์ค๋ช ํด๋ด.
์์ ๋ต๋ณ:
"
isLoadingํจํด์ '๋ฐฐ๋ฌ ์ฃผ๋ฌธํ๊ณ 30๋ถ๋ง๋ค ์ ํํด์ ์ด๋จ๋๊ณ ๋ฌผ์ด๋ณด๋ ๊ฒ'์ด๊ณ ,Suspense + async atom์ '๋ฐฐ๋ฌ ์ฑ์์ ๋์ฐฉํ๋ฉด ์๋ฆผ ์ค๋๋ก ์ค์ ํด๋๋ ๊ฒ'์ด์ผ. ์ ์๋ ๋ด๊ฐ ์ง์ ์ฑ๊ฒจ์ผ ํ๊ณ , ํ์๋ ์ ์ธ๋ง ํ๋ฉด ์์์ ์ฒ๋ฆฌ๋ผ."
๐ก ์ด ๋น์ ๋ฅผ ์ง์ ๋ง๋ค์๋ค๋ฉด: ์ ์ธ์ vs ๋ช ๋ น์ ํจ๋ฌ๋ค์์ ์์ ํ ์ดํดํ ๊ฑฐ์ผ. ๋ค์ ๊ฐ์ด๋๋ก ๋์ด๊ฐ๋ ์ถฉ๋ถํด!
๐ฃ ์์ฒ ์ด์ ํด๊ทผ ์ผ๊ธฐ
์ค๋๋ ์ง๊ฐํ ๋ปํ๋ค๊ฐ ๊ฒจ์ฐ ์ด์๋จ์ ํ๋ฃจ์๋ค.
์์นจ์ ์์ ์๊ฒ PR ์ฌ๋ ธ๋ค๊ฐ ์ํธ ๋ํํ
์ฝ๋ ๋ฆฌ๋ทฐ ํญํ์ ๋ง์์ด. isLoading, isError, data useState 3๊ฐ์ useEffect ๊น์ง โ ๋ด๊ฐ ๋ด๋ ๋ญ๊ฐ ๋ณต์กํ๋ค ์ถ์๋๋ฐ, ์ญ์ ๋ค์ผฐ๋ค.
async atom ์ด Promise ๋ฅผ ๋ฐ์์ Suspense ๊ฐ ์์์ ์ฒ๋ฆฌํ๋ค๋ ๊ฒ ์ง์ง๋ก ๋ฏฟ๊ธฐ์ง ์์๋๋ฐ, ์ง์ ์ฝ๋ ๋ฐ๊พธ๊ณ ๋๋๊น ์ปดํฌ๋ํธ๊ฐ ์ ๋ฐ ์ดํ๋ก ์ค์์ด. ๋ก๋ฉ ๋ถ๊ธฐ ์ฝ๋๊ฐ ํ๋๋ ์๋๋ฐ ์ ๋๋ก ๋์ํ๋ ๊ฒ ์ ๊ธฐํด์ ํผ์ ์ข ๋ฉ ๋๋ ธ๋ค.
๐ก ์ค๋์ ๊ตํ: "์ปดํฌ๋ํธ๋ '๋ฌด์์ ๋ณด์ฌ์ค์ง'๋ง ์๋ฉด ๋ผ. '์ธ์ ๋ก๋ฉ์ธ์ง, ์ธ์ ์๋ฌ์ธ์ง'๋ Suspense ์ ErrorBoundary ์๊ฒ ๋งก๊ฒจ."
signal ๋ก Race Condition ์ ๋ง๋ ๊ฒ๋ ์ฒ์ ์์๋๋ฐ... ์์งํ ์ด์ ์ ์ง ๊ฒ์ ๊ธฐ๋ฅ์ signal ์ด ์์๋ค๋ ๊ฒ ์ข ๋ฌด์์์ก์ด. ๊ฒ์์ด ๋น ๋ฅด๊ฒ ์น๋ฉด ์๋ฑํ ๊ฒฐ๊ณผ๊ฐ ๋ณด์ผ ์ ์์๋ ๊ฑฐ์์. ์ค๋ ๊ณ ์ณ์ ๋คํ์ด๋ค.
๋ฐฐ๋ ์์ฒญ ๊ณ ํ๋ค. ํด๊ทผํ๊ณ ์นผ๊ตญ์ ๋จน๊ณ ์ถ๋ค. ์ง ๊ทผ์ฒ ์นผ๊ตญ์ ์ง์ด 9์์ ๋ซ๋๋ฐ ๋ฑ ๋ง์ถฐ๊ฐ ์ ์์ ๊ฒ ๊ฐ์. ๋นจ๋ฆฌ ๊ฐ์.