๐ 11. Next.js App Router + Jotai SSR ์ ๋ต
๐ ๊ฐ์
SSR์์ Provider ํ์ ์ด์ , useHydrateAtoms๋ก ์๋ฒ ๋ฐ์ดํฐ ์ฃผ์ , ServerโClient Component ๋ฐ์ดํฐ ์ ๋ฌ, SWC ํ๋ฌ๊ทธ์ธ์ ๋ฐฐ์๋๋ค.
๐ฏ ์ด ์น์ ์ ์ฝ๊ณ ๋๋ฉด:
- SSR ํ๊ฒฝ์์ Provider ๊ฐ ์ ํ์์ด๋ฉฐ, ์์ผ๋ฉด ์ด๋ค ๋ณด์ ์ฌ๊ณ ๊ฐ ๋๋์ง ์ค๋ช ํ ์ ์๋ค.
useHydrateAtoms๋ก Server Component ์์ fetch ํ ๋ฐ์ดํฐ๋ฅผ Client Component ์ atom ์ ์ฃผ์ ํ๋ ํจํด์ ๊ตฌํํ ์ ์๋ค.- SWC ํ๋ฌ๊ทธ์ธ ์ค์ ์ผ๋ก DX ๋ฅผ ๊ฐ์ ํ ์ ์๋ค.
๐ ๋ชฉ์ฐจ
- ๐ ์ด ๋ฌธ์๋ฅผ ์ฝ๊ธฐ ์ ์
- ๐จ ์ค์ ๋ณด์ ์ฌ๊ณ โ Provider ์์ด ๋ฐฐํฌํ ๋
- ๐ SSR ์์ Provider ๊ฐ ํ์์ธ ์ด์
- ๐๏ธ layout.tsx Provider ์ค์ ํจํด
- ๐ useHydrateAtoms โ ์๋ฒ ๋ฐ์ดํฐ๋ฅผ atom ์ ์ฃผ์ ํ๊ธฐ
- ๐ Server โ Client Component ๋ฐ์ดํฐ ์ ๋ฌ ์ ์ฒด ํ๋ฆ
- โ๏ธ SWC ํ๋ฌ๊ทธ์ธ ์ค์
- ๐งช SSR ์์ atom ์ ์ง์ ์ฐ์ง ์๋ ์ด์
- ๐ฅ ์๋ฌ ํด๊ฒฐ ์นดํ๋ก๊ทธ
- ๐ ์ด๋ฒ์ ๋ฐฐ์ด ๋ด์ฉ ์ด์ ๋ฆฌ
- ๐ ๋ง๋ฌด๋ฆฌ ํด์ฆ
- ๐ฃ ์์ฒ ์ด์ ํด๊ทผ ์ผ๊ธฐ
- ๐ ๋ ์์๋ณด๊ธฐ
๐ ์ด ๋ฌธ์๋ฅผ ์ฝ๊ธฐ ์ ์
โฑ๏ธ ์์ ์ฝ๊ธฐ ์๊ฐ: 35๋ถ (์ ์ฒด) / ํต์ฌ ํํธ๋ง: 20๋ถ
๐บ๏ธ ์ด ๋ฌธ์์ ๋ฐฐ๊ฒฝ ์ธ๊ณ๊ด: ์ต์ ์ ๋ฐฐํฌ ๋
์์๋ค ํ์ด ์์๋ค ์ปค๋ฎค๋ํฐ ์ฑ์ ์ฒ์์ผ๋ก ์ค์๋ฒ์ ๋ฐฐํฌํ ๋ ์ด์ผ. ๋ก ์นญ 10๋ถ ๋ง์ ์์ ๋์ ์ ํ๊ฐ ์ธ๋ ธ์ด:
- ๐ ์์ ๋ (ํจ๋): "์ผ, ์ง๊ธ ์ฌ์ฉ์๋ค์ด ๋ค๋ฅธ ์ฌ๋ ์ด๋ฆ์ด๋ ์ด๋ฉ์ผ์ด ๋ณด์ธ๋ค๊ณ ๋๋ฆฌ๋ค! ๋ด ๊ณ์ ์ผ๋ก ๋ค์ด๊ฐ๋๋ ์ํธ ์ด๋ฆ์ด ๋ฌ๋ค๊ณ !"
- ๐ฆ ์ํธ ๋ (์นจ์ฐฉํ๊ฒ ์ฝ๋ ์ด์ด๋ณด๋ฉฐ): "์์ฒ ๋,
layout.tsx์ Provider ์์ด์?" - ๐ฃ ์์ฒ : (์ ๋จ๋ฉด์) "Provider ์? ๋ก์ปฌ์์ ์ ๋๋๋ฐ... ์, ์์ด์."
- ๐ฆ ์ํธ ๋: "์ง๊ธ ๋ฐ๋ก ๋กค๋ฐฑํ๊ณ Provider ์ถ๊ฐํด์ ์ฌ๋ฐฐํฌํด์. 5๋ถ ๊ฑธ๋ ค์."
- ๐ ์์ ๋: "์ ์ด๋ฐ ์ผ์ด ์๊ธด ๊ฑฐ์ผ?"
- ๐ฆ ์ํธ ๋: "์๋ฒ์์ ๋ชจ๋ ์์ฒญ์ด ์ ์ญ store ํ๋๋ฅผ ๊ณต์ ํด์์. A ์ ์ ๊ฐ ์ด store ๋ฅผ B ์ ์ ๊ฐ ์ฝ์ ๊ฑฐ์์. ์ค๋๋ถํฐ ์ด ์์ธ์ ์ ๋๋ก ์ดํดํ๊ณ ๋์ด๊ฐ์ผ ํด์."
๐จ ์ค์ ๋ณด์ ์ฌ๊ณ โ Provider ์์ด ๋ฐฐํฌํ ๋ ๐ด
๐ฏ ์ด ์น์ ์ ์ฝ๊ณ ๋๋ฉด:
- ์ ์ญ ์ฑ๊ธํด store ๊ฐ ์๋ฒ์์ ์ ์ํํ์ง ๊ตฌ์ฒด์ ์ธ ํ์๋ผ์ธ์ผ๋ก ์ดํดํ๋ค
์๊ฐ 00:00.000 โ ์์ฒ ์ด ์ธ์
์์ฒญ ๋์ฐฉ
โ ์๋ฒ๊ฐ userAtom ์ { name: '์์ฒ ', email: 'youngcheol@email.com' } ๋ฅผ set
โ HTML ๋ ๋๋ง ์์
์๊ฐ 00:00.050 โ ์์ ์ธ์
์์ฒญ ๋์ฐฉ (๋ ๋ ์๋ฃ ์ !)
โ ์๋ฒ๊ฐ userAtom.get() ํธ์ถ
โ ์ ์ญ store ์๋ ์์ง '์์ฒ ' ๋ฐ์ดํฐ๊ฐ ์์
โ ์์์๊ฒ ์์ฒ ์ ์ด๋ฆ, ์ด๋ฉ์ผ์ด ํฌํจ๋ HTML ์ ์ก ๐
์๊ฐ 00:00.100 โ ์์ฒ ์ด ์์ฒญ ์๋ฃ
โ ์ด๋ฏธ ๋ฆ์์
์ด๊ฒ Race Condition + ์ ์ญ ์ํ ๊ณต์ ๊ฐ ๋ง๋ค์ด๋ด๋ ์ต์ ์ ์๋๋ฆฌ์ค์ผ. ๋ณด์ ๊ฐ์ฌ์์ "๊ฐ์ธ์ ๋ณด ๋ฌด๋จ ๋ ธ์ถ" ๋ก ๋ถ๋ฅ๋ ์ ์๋ ์ฌ๊ณ ์ผ.
// โ ์ฌ๊ณ ์ ์์ธ ์ฝ๋ โ Provider ์๋ layout.tsx
// app/layout.tsx
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="ko">
<body>
{children} {/* โ ์ ์ญ ์ฑ๊ธํด store ๊ฐ ๋ชจ๋ ์์ฒญ์ ๊ณต์ ๋จ */}
</body>
</html>
)
}
// atoms/user.ts
// ๐ฃ ์์ฒ : "๋ชจ๋ ์ต์๋จ์ atom ์ ์ธํ๋๋ฐ, ์ด๊ฒ ์๋ฒ ์ ์ญ ๋ณ์๊ฐ ๋ผ๋ฒ๋ฆฌ๋ ๊ฑฐ์์ด์"
export const userAtom = atom<User | null>(null)
// โ ์ด atom ์ ๊ฐ์ด ์๋ฒ ์ธ์คํด์ค์ ์ ์ญ store ์ ์ ์ฅ๋จ
// โ ์์ฒญ ๊ฐ ๊ณต์ โ ๋ณด์ ์ฌ๊ณ ๐ SSR ์์ Provider ๊ฐ ํ์์ธ ์ด์ ๐ด
๐ฏ ์ด ์น์ ์ ์ฝ๊ณ ๋๋ฉด:
- Node.js ์ ๋ชจ๋ ์บ์ฑ์ด ์ ์ด ๋ฌธ์ ๋ฅผ ์ผ์ผํค๋์ง ์ดํดํ๋ค
- Provider ๊ฐ ์ด๋ป๊ฒ ์์ฒญ ๋จ์์ store ๊ฒฉ๋ฆฌ๋ฅผ ๋ง๋๋์ง ์ค๋ช ํ ์ ์๋ค
Node.js ์๋ฒ์ ํน์ฑ:
- ๋ชจ๋์ ํ ๋ฒ๋ง ๋ก๋๋๊ณ ์บ์ฑ๋จ
- atom config (์ค๊ณ๋)๋ ๋ชจ๋ ์ต์๋จ์์ ํ ๋ฒ ์์ฑ
- Provider-less ๋ชจ๋์ ์ ์ญ store ๋ ๋ชจ๋ ๋ ๋ฒจ์์ ํ ๋ฒ ์์ฑ
โ ๋ชจ๋ HTTP ์์ฒญ์ด ๊ฐ์ store ๋ฅผ ๊ณต์ !
Provider ์ ๋์:
- Provider ์ปดํฌ๋ํธ๋ ๋ด๋ถ์ ์ผ๋ก createStore() ๋ฅผ ํธ์ถํด์ ์ store ๋ฅผ ๋ง๋ฆ
- React ๋ ๋๋ง ์ Provider ๊ฐ ์ store ์ธ์คํด์ค๋ฅผ ์์ฑ
- SSR ์์๋ ์์ฒญ๋ง๋ค React ๊ฐ ๋ ๋๋ง๋๋ฏ๋ก โ ์์ฒญ๋ง๋ค ์ store ์์ฑ
โ ์์ฒญ ๊ฐ ์์ ํ ๊ฒฉ๋ฆฌ!
// โ
ํด๊ฒฐ๋ ์ํ โ Provider ๊ฐ ์์ฒญ๋ง๋ค ์ store ๋ฅผ ๊ฒฉ๋ฆฌ
// ์์ฒญ 1 (์์ฒ ์ธ์
): Provider ๋ ๋ โ createStore() โ store A ์์ฑ
// userAtom in store A = { name: '์์ฒ ' }
// ์์ฒญ 2 (์์ ์ธ์
): Provider ๋ ๋ โ createStore() โ store B ์์ฑ
// userAtom in store B = { name: '์์' }
// store A ์ store B ๋ ์์ ํ ๋
๋ฆฝ โ ๋ฐ์ดํฐ ๊ฒฉ๋ฆฌ โ
๐๏ธ layout.tsx Provider ์ค์ ํจํด ๐ก
๐ฏ ์ด ์น์ ์ ์ฝ๊ณ ๋๋ฉด:
- App Router ์์ Provider ๋ํผ ์ปดํฌ๋ํธ๋ฅผ ์ฌ๋ฐ๋ฅด๊ฒ ์ค์ ํ๋ ํจํด์ ์ ์ ์๋ค
- 'use client' ๊ฐ ์ ํ์ํ์ง, ์ด๋์ ๋ถ์ฌ์ผ ํ๋์ง ์ดํดํ๋ค
// ๐ฆ ์ํธ: "layout.tsx ๋ ๊ธฐ๋ณธ์ด Server Component ์ผ. Provider ๋ Client Component ์ด๋ผ์
// ๋ฐ๋ก ์ธ ์ ์์ด. ๋ํผ ์ปดํฌ๋ํธ๋ก ๋ถ๋ฆฌํด์."
// app/providers.tsx โ Client Component ๋ํผ
'use client'
import { Provider } from 'jotai'
import type { ReactNode } from 'react'
interface JotaiProviderProps {
children: ReactNode
}
export function JotaiProvider({ children }: JotaiProviderProps) {
return <Provider>{children}</Provider>
}// app/layout.tsx โ Server Component ์ ์ง (๋ฉํ๋ฐ์ดํฐ, ํฐํธ ๋ฑ ํ์ฉ ๊ฐ๋ฅ)
import { JotaiProvider } from '@/app/providers'
import type { Metadata } from 'next'
export const metadata: Metadata = {
title: '์์๋ค ์คํฐ๋ ์ปค๋ฎค๋ํฐ',
description: '๊ฐ๋ฐ์ ์คํฐ๋ ๋งค์นญ ํ๋ซํผ',
}
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="ko">
<body>
{/*
JotaiProvider ๋ Client Component.
children (Server Components) ์ ์ฌ์ ํ ์๋ฒ์์ ๋ ๋๋จ.
์ด ๊ตฌ์กฐ๋ Next.js์ "Client Component ์ Server Component ๋ฅผ ์์์ผ๋ก ๋๊ธฐ๊ธฐ" ํจํด.
*/}
<JotaiProvider>
{children}
</JotaiProvider>
</body>
</html>
)
}createStore ๋ฅผ ์ง์ ๋๊ธฐ๋ ๊ณ ๊ธ ํจํด
// ๐ฆ ์ํธ: "์ด๊ธฐ ๋ฐ์ดํฐ๋ฅผ ๋ฏธ๋ฆฌ store ์ ์ฌ์ด๋๊ณ ์ถ์ ๋๋ ์ด๋ ๊ฒ ํด์"
// app/providers.tsx
'use client'
import { Provider, createStore } from 'jotai'
import { useRef } from 'react'
export function JotaiProvider({ children }: { children: React.ReactNode }) {
// useRef ๋ก store ๋ฅผ ํ ๋ฒ๋ง ์์ฑ (๋ ๋๋ง๋ค ์ฌ์์ฑ ๋ฐฉ์ง)
// ๐ฃ ์์ฒ : "useState ๊ฐ ์๋๋ผ useRef ๋ฅผ ์ฐ๋ ์ด์ ๊ฐ ๋ญ๊ฐ์?"
// ๐ฆ ์ํธ: "useRef ๋ ๋ฆฌ๋ ๋๋ฅผ ํธ๋ฆฌ๊ฑฐํ์ง ์์์ store ์ด๊ธฐํ์ ์ ํฉํด์"
const storeRef = useRef<ReturnType<typeof createStore>>()
if (!storeRef.current) {
storeRef.current = createStore()
}
return <Provider store={storeRef.current}>{children}</Provider>
}๐ useHydrateAtoms โ ์๋ฒ ๋ฐ์ดํฐ๋ฅผ atom ์ ์ฃผ์ ํ๊ธฐ ๐ด
๐ฏ ์ด ์น์ ์ ์ฝ๊ณ ๋๋ฉด:
useHydrateAtoms์ ๋์ ์๋ฆฌ์ ์ฌ์ฉ๋ฒ์ ์ ํํ ์ดํดํ๋ค- Server Component ์์ fetch ํ ๋ฐ์ดํฐ๋ฅผ atom ์ ์ด๊ธฐ๊ฐ์ผ๋ก ์ฃผ์ ํ๋ ํจํด์ ๊ตฌํํ ์ ์๋ค
// ๊ณต์ ๋ ํผ๋ฐ์ค์ ํต์ฌ ๊ฒฝ๊ณ :
// "useHydrateAtoms ๋ ํด๋ผ์ด์ธํธ ์ฝ๋์์ ์ฌ์ฉํด์ผ ํฉ๋๋ค. 'use client' ์ง์์ด๊ฐ ํ์ํฉ๋๋ค."
import { useHydrateAtoms } from 'jotai/utils'
const countAtom = atom(0)
const ClientComponent = ({ countFromServer }: { countFromServer: number }) => {
// countAtom ์ countFromServer ๊ฐ์ผ๋ก ์ด๊ธฐํ
// ์ด ํธ์ถ์ ์ปดํฌ๋ํธ๊ฐ ์ฒ์ ๋ง์ดํธ๋ ๋๋ง ํจ๊ณผ์
useHydrateAtoms([[countAtom, countFromServer]])
const [count] = useAtom(countAtom)
// count ๋ 0 ์ด ์๋๋ผ countFromServer ๊ฐ
return <div>{count}</div>
}์ฃผ์: atom ์ store ๋น ํ ๋ฒ๋ง hydrate ๋๋ค
// โ ๏ธ ์ค์: ๊ฐ์ store ์์ atom ์ ํ ๋ฒ๋ง hydrate ๋จ
// initialValue ๊ฐ ๋ฆฌ๋ ๋ ์ ๋ฐ๋์ด๋ atom ๊ฐ์ ์
๋ฐ์ดํธ๋์ง ์์
const Component = ({ count }: { count: number }) => {
// count prop ์ด 5 โ 10 ์ผ๋ก ๋ฐ๋์ด๋ atom ์ ์ฌ์ ํ 5
useHydrateAtoms([[countAtom, count]])
// ์ด ๋์์ ์๋๋ ๊ฒ โ "์ด๊ธฐ๊ฐ ์ฃผ์
" ์ด ๋ชฉ์ ์ด๋๊น
}
// ๊ฐ์ ๋ก ์ฌ์ฃผ์
์ด ํ์ํ๋ฉด (์ํ ์ฃผ์)
useHydrateAtoms([[countAtom, count]], { dangerouslyForceHydrate: true })๐ Server โ Client Component ๋ฐ์ดํฐ ์ ๋ฌ ์ ์ฒด ํ๋ฆ ๐ด
๐ฏ ์ด ์น์ ์ ์ฝ๊ณ ๋๋ฉด:
- Server Component ์์ fetch ํ๊ณ Client Component ์์ atom ์ผ๋ก ๊ด๋ฆฌํ๋ ์์ ํ ํจํด์ ๊ตฌํํ ์ ์๋ค
// ์ ์ฒด ๋ฐ์ดํฐ ํ๋ฆ:
// [Server Component] fetch ๋ฐ์ดํฐ
// โ props ๋ก ์ ๋ฌ
// [Client Component] useHydrateAtoms ๋ก atom ์ ์ฃผ์
// โ ์ดํ
// [Client ์ํ] atom ์ผ๋ก ์์ ๋กญ๊ฒ ์ฝ๊ณ ์ฐ๊ธฐ
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
// 1๋จ๊ณ: atom ์ ์ (๊ณต์ ํ์ผ)
// atoms/study.ts
import { atom } from 'jotai'
export interface Study {
id: string
title: string
likeCount: number
memberCount: number
}
export const studyListAtom = atom<Study[]>([])
export const studyFiltersAtom = atom({ category: 'all', sortBy: 'latest' })// 2๋จ๊ณ: Server Component โ ๋ฐ์ดํฐ fetch
// app/studies/page.tsx (Server Component โ 'use client' ์์)
import { StudyListClient } from '@/components/StudyListClient'
async function fetchStudyList(): Promise<Study[]> {
// ๐ฆ ์ํธ: "Server Component ์์ fetch ํ๋ฉด ์๋ฒ์์ ์คํ๋ผ์.
// ์บ์ฑ, ํ ํฐ, DB ์ง์ ์ ๊ทผ ๋ชจ๋ ๊ฐ๋ฅํด์."
const res = await fetch('https://api.example.com/studies', {
cache: 'no-store', // ํญ์ ์ต์ ๋ฐ์ดํฐ
})
return res.json()
}
export default async function StudiesPage() {
// ์๋ฒ์์ ๋ฐ์ดํฐ ๊ฐ์ ธ์ค๊ธฐ
const initialStudyList = await fetchStudyList()
return (
<main>
<h1>์คํฐ๋ ๋ชฉ๋ก</h1>
{/*
Server Component ์์ fetch ํ ๋ฐ์ดํฐ๋ฅผ Client Component ์ props ๋ก ์ ๋ฌ.
Client Component ์์ useHydrateAtoms ๋ก atom ์ ์ฃผ์
.
*/}
<StudyListClient initialStudyList={initialStudyList} />
</main>
)
}// 3๋จ๊ณ: Client Component โ atom ์ ์ด๊ธฐ๊ฐ ์ฃผ์
ํ ์ฌ์ฉ
// components/StudyListClient.tsx
'use client'
import { useAtom, useAtomValue } from 'jotai'
import { useHydrateAtoms } from 'jotai/utils'
import { studyListAtom, studyFiltersAtom } from '@/atoms/study'
interface Props {
initialStudyList: Study[]
}
export function StudyListClient({ initialStudyList }: Props) {
// ๐ฆ ์ํธ: "useHydrateAtoms ๋ ๋ง์ดํธ ์ ํ ๋ฒ๋ง atom ์ ์ด๊ธฐ๊ฐ์ ์ฌ์ด์.
// ์ดํ atom ์ ์์ ํ ํด๋ผ์ด์ธํธ ์ํ๊ฐ ๋ผ์."
useHydrateAtoms([[studyListAtom, initialStudyList]])
// ์ดํ atom ์ ์์ ๋กญ๊ฒ ์ฌ์ฉ
const studies = useAtomValue(studyListAtom)
const [filters, setFilters] = useAtom(studyFiltersAtom)
const filteredStudies = studies.filter((s) =>
filters.category === 'all' ? true : s.category === filters.category
)
return (
<div>
<FilterBar filters={filters} onFilterChange={setFilters} />
<ul>
{filteredStudies.map((study) => (
<StudyCard key={study.id} study={study} />
))}
</ul>
</div>
)
}// 4๋จ๊ณ: ์ข์์ ๊ฐ์ ํด๋ผ์ด์ธํธ ์ํ ๋ณ๊ฒฝ
// components/StudyCard.tsx
'use client'
import { useAtom } from 'jotai'
import { studyListAtom } from '@/atoms/study'
export function StudyCard({ study }: { study: Study }) {
const [, setStudyList] = useAtom(studyListAtom)
const handleLike = async () => {
// ๋๊ด์ ์
๋ฐ์ดํธ โ ์๋ฒ ์๋ต ์ ์ UI ๋จผ์ ์
๋ฐ์ดํธ
setStudyList((prev) =>
prev.map((s) =>
s.id === study.id ? { ...s, likeCount: s.likeCount + 1 } : s
)
)
// ๋ฐฑ๊ทธ๋ผ์ด๋์์ ์๋ฒ์ ๋๊ธฐํ
await fetch(`/api/studies/${study.id}/like`, { method: 'POST' })
}
return (
<div>
<h3>{study.title}</h3>
<button onClick={handleLike}>โค๏ธ {study.likeCount}</button>
</div>
)
}์ฌ๋ฌ atom ์ ํ ๋ฒ์ hydrate
// ๐ฃ ์์ฒ : "์ฌ๋ฌ atom ์ ๋์์ ์ด๊ธฐํํ ์ ์๋์?"
// ๐ฆ ์ํธ: "useHydrateAtoms ๋ ๋ฐฐ์ด์ด๋ผ์ ์ฌ๋ฌ ์์ ํ ๋ฒ์ ๋๊ธธ ์ ์์ด์"
useHydrateAtoms([
[studyListAtom, initialStudyList],
[userAtom, initialUser],
[notificationsAtom, initialNotifications],
])โ๏ธ SWC ํ๋ฌ๊ทธ์ธ ์ค์ ๐ก
๐ฏ ์ด ์น์ ์ ์ฝ๊ณ ๋๋ฉด:
- Jotai SWC ํ๋ฌ๊ทธ์ธ์ด DX ๋ฅผ ์ด๋ป๊ฒ ๊ฐ์ ํ๋์ง ์ดํดํ๋ค
- Next.js ์์ SWC ํ๋ฌ๊ทธ์ธ์ ์ค์ ํ๋ ๋ฐฉ๋ฒ์ ์๋ค
Jotai SWC ํ๋ฌ๊ทธ์ธ์ ๋ ๊ฐ์ง ๊ธฐ๋ฅ์ ์๋์ผ๋ก ์ฒ๋ฆฌํด์ค:
- debugLabel ์๋ ์ถ๊ฐ โ ๊ฐ atom ์ ํ์ผ๋ช /๋ณ์๋ช ๊ธฐ๋ฐ ๋๋ฒ๊ทธ ๋ ์ด๋ธ ์๋ ์ถ๊ฐ
- Hot Reload ์ง์ โ ๊ฐ๋ฐ ์ค atom ๋ณ๊ฒฝ ์ ๋น ๋ฅธ HMR
npm install @swc-jotai/debug-label @swc-jotai/react-refresh// next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
experimental: {
swcPlugins: [
// atom ์ ์๋์ผ๋ก debugLabel ์ถ๊ฐ (DevTools ์์ ์๋ณ ์ฉ์ด)
['@swc-jotai/debug-label', {}],
// ๊ฐ๋ฐ ์ค atom ๋ณ๊ฒฝ ์ Hot Module Replacement ์ง์
['@swc-jotai/react-refresh', {}],
],
},
// jotai-devtools UI CSS ํธ๋์คํ์ผ
transpilePackages: ['jotai-devtools'],
}
module.exports = nextConfig// SWC ํ๋ฌ๊ทธ์ธ ์ ์ฉ ์
const studyListAtom = atom<Study[]>([])
// debugLabel ์์ โ DevTools ์์ "atom1", "atom2" ๋ก ํ์
// SWC ํ๋ฌ๊ทธ์ธ ์ ์ฉ ํ (์๋ ๋ณํ)
const studyListAtom = atom<Study[]>([])
studyListAtom.debugLabel = 'studyListAtom' // ์๋ ์ถ๊ฐ!
// โ DevTools ์์ 'studyListAtom' ์ผ๋ก ์ ํํ๊ฒ ํ์๐งช SSR ์์ atom ์ ์ง์ ์ฐ์ง ์๋ ์ด์ ๐ก
๐ฏ ์ด ์น์ ์ ์ฝ๊ณ ๋๋ฉด:
- Server Component ์์ React ํ ์ ์ธ ์ ์๋ ์ด์ ๋ฅผ ์ค๋ช ํ ์ ์๋ค
- ๋ฐ์ดํฐ ํ๋ฆ์ด "์๋ฒ fetch โ props โ Client atom" ์ด์ด์ผ ํ๋ ์ด์ ๋ฅผ ์ดํดํ๋ค
// โ Server Component ์์ atom ์ ์ง์ ์ฐ๋ ค ํ์ ๋ ์๋ฌ
// app/studies/page.tsx
// ์ด๋ ๊ฒ ํ๊ณ ์ถ์ด๋ ์ ๋จ!
import { useAtomValue } from 'jotai' // โ Server Component ์์ ํ
์ฌ์ฉ ๋ถ๊ฐ
export default async function StudiesPage() {
// Error: Invalid hook call.
// Hooks can only be called inside of the body of a function component.
const studies = useAtomValue(studyListAtom) // ๐ฅ ์๋ฌ!
}// ๐ฆ ์ํธ: "Server Component ๋ React ํ
์ ์คํํ ํ๊ฒฝ์ด ์์ด์.
// ํ
์ React ๊ฐ ์ปดํฌ๋ํธ ํธ๋ฆฌ๋ฅผ ๊ด๋ฆฌํ๋ฉด์ ์คํ๋๋๋ฐ,
// Server Component ๋ ์๋ฒ์์ ํ ๋ฒ ์คํ๋๊ณ ๋์ด์์."
// โ
์ฌ๋ฐ๋ฅธ ํจํด โ Server ์์ fetch, Client ์์ atom ์ผ๋ก ๊ด๋ฆฌ
// Server Component: ๋ฐ์ดํฐ๋ง ๊ฐ์ ธ์จ๋ค
export default async function StudiesPage() {
const studies = await fetchStudies() // fetch (ํ
์๋) โ
return <StudyListClient initialStudyList={studies} />
// โ Client Component ์ props ๋ก ์ ๋ฌ
}
// Client Component: atom ์ผ๋ก ์ํ ๊ด๋ฆฌ
'use client'
export function StudyListClient({ initialStudyList }: Props) {
useHydrateAtoms([[studyListAtom, initialStudyList]]) // ์ฃผ์
โ
const studies = useAtomValue(studyListAtom) // ์ดํ atom ์ผ๋ก ์์ ๋กญ๊ฒ ์ฌ์ฉ โ
}๐ฅ ์๋ฌ ํด๊ฒฐ ์นดํ๋ก๊ทธ
์๋ฌ ๋ฉ์์ง๊ฐ ๋จ๋ฉด Ctrl+F ๋ก ๊ฒ์ํด๋ด. ๋๋ถ๋ถ ์ฌ๊ธฐ ์์ด.
โ ๋ค๋ฅธ ์ ์ ์ ๋ฐ์ดํฐ๊ฐ ๋ณด์ด๋ ํ์
์์ธ: layout.tsx ์ <Provider> ์์ด ์ ์ญ atom ์ฌ์ฉ (08๋ฒ ๊ฐ์ด๋ ์ฐธ๊ณ )
ํด๊ฒฐ์ฑ :
// app/providers.tsx
'use client'
export function JotaiProvider({ children }: { children: React.ReactNode }) {
return <Provider>{children}</Provider>
}
// app/layout.tsx
import { JotaiProvider } from '@/app/providers'
export default function RootLayout({ children }) {
return <html><body><JotaiProvider>{children}</JotaiProvider></body></html>
}โ "Invalid hook call" โ Server Component ์์ useAtom ์ฌ์ฉ
ํด๊ฒฐ์ฑ
: Server Component ์์ ํ
์ฌ์ฉ ๋ถ๊ฐ. ๋ฐ์ดํฐ๋ฅผ props ๋ก Client Component ์ ์ ๋ฌํ๊ณ ๊ฑฐ๊ธฐ์ useHydrateAtoms ์ฌ์ฉ
โ useHydrateAtoms ๊ฐ prop ์ ๋ฐ์ดํธ์ ๋ฐ์ํ์ง ์์
์์ธ: useHydrateAtoms ๋ ๊ฐ์ store ์์ ํ ๋ฒ๋ง ๋์ํด
ํด๊ฒฐ์ฑ :
// ์ฌ์ฃผ์
์ด ๊ผญ ํ์ํ๋ฉด dangerouslyForceHydrate ์ต์
์ฌ์ฉ
// (concurrent rendering ์์ ์๋ชป๋ ๋์ ๊ฐ๋ฅ โ ์ฃผ์ํด์ ์ฌ์ฉ)
useHydrateAtoms([[countAtom, count]], { dangerouslyForceHydrate: true })โ SWC ํ๋ฌ๊ทธ์ธ ์ ์ฉ ํ "Module not found" ์๋ฌ
ํด๊ฒฐ์ฑ :
# ํ๋ฌ๊ทธ์ธ ํจํค์ง ์ค์น ํ์ธ
npm install @swc-jotai/debug-label @swc-jotai/react-refresh
# next.config.js ์ experimental.swcPlugins ์ค์ ํ์ธ๐ ์ด๋ฒ์ ๋ฐฐ์ด ๋ด์ฉ ์ด์ ๋ฆฌ
๐ Next.js App Router + Jotai ์ ์ฒด ๊ตฌ์กฐ
app/
โโโ layout.tsx โ Server Component + JotaiProvider ๋ํผ ์ฌ์ฉ
โโโ providers.tsx โ 'use client' JotaiProvider ๋ํผ
โโโ studies/
โ โโโ page.tsx โ Server Component: fetch ๋ฐ์ดํฐ
โโโ components/
โโโ StudyListClient.tsx โ 'use client': useHydrateAtoms + useAtom
atoms/
โโโ study.ts โ atom ์ ์ (Server/Client ๊ณต์ )
โ ๏ธ ํต์ฌ ์ฒดํฌ๋ฆฌ์คํธ
| ์ฒดํฌ | ๋ด์ฉ |
|---|---|
| โ | layout.tsx ์ JotaiProvider ๋ํผ ์ถ๊ฐ |
| โ | providers.tsx ์ 'use client' ์ถ๊ฐ |
| โ | Server Component ์์๋ fetch ๋ง, ํ
์ Client Component ์์ |
| โ | useHydrateAtoms ๋ Client Component ('use client')์์๋ง |
| โ | SWC ํ๋ฌ๊ทธ์ธ์ผ๋ก debugLabel ์๋ํ |
๐ ๋ง๋ฌด๋ฆฌ ํด์ฆ
Q1. ๐จ ๊ธด๊ธ ๋๋ฒ๊น (์์์ ํธํต)
๋ฐฐํฌ ์งํ "A ์ ์ ๊ฐ B ์ ์ ์ ๋ณด๋ฅผ ๋ณด๊ณ ์๋ค" ๊ณ ์ ๊ณ ๊ฐ ๋ค์ด์์ด.
์์ฒ ์ด๊ฐ ํ์ธํ๋layout.tsx์<Provider>๊ฐ ์์์ด.
์ํธ ๋์ ๊ธฐ์ ์ ์ค๋ช ์ผ๋ก ๊ฐ์ฅ ์ ํํ ๊ฒ์?
- A) ์๋ฒ์์ atom ์ ์ด๊ธฐ๊ฐ์ด
null์ด๋ผ์ ๋ค๋ฅธ ์ ์ ์ ๋ฐ์ดํฐ๋ฅผ ์ฝ์๋ค - B) Next.js SSR ์์ Provider ์์ด atom ์ ์ฐ๋ฉด, Node.js ๋ชจ๋ ๋ ๋ฒจ์ ์ ์ญ store ๊ฐ ๋ชจ๋ ์์ฒญ์ ๊ณต์ ๋์ด ์ด์ ์์ฒญ์ ๋ฐ์ดํฐ๊ฐ ๋ค์ ์์ฒญ์ ๋ ธ์ถ๋๋ค
- C)
useHydrateAtoms๋ฅผ ์ฐ์ง ์์์ ๋ฐ์ํ ๋ฌธ์ ๋ค - D) atom ์ Server Component ์์ ์ฌ์ฉํด์ ๋ฐ์ํ ๋ฌธ์ ๋ค
โ ์ ๋ต: B
๐ก ์์ธ ํด์ค:
- ์๋ฆฌ ์ค๋ช
: Node.js ์๋ฒ์์ JavaScript ๋ชจ๋์ ํ ๋ฒ ๋ก๋๋์ด ์บ์ฑ๋จ. Provider-less ๋ชจ๋์ ์ ์ญ default store ๋ ๋ชจ๋ ๋ ๋ฒจ์์ ํ ๋ฒ ์์ฑ๋์ด ๊ณต์ ๋ผ.
<Provider>๋ ๋ ๋ํ ๋๋ง๋ค ๋ด๋ถ์ ์ผ๋กcreateStore()๋ฅผ ํธ์ถํด์ ์ store ๋ฅผ ๋ง๋ค์ด. SSR ์์๋ ์์ฒญ๋ง๋ค React ๊ฐ ๋ ๋๋ง์ ์คํํ๋ฏ๋ก, ์์ฒญ๋ง๋ค ์ Provider โ ์ store โ ์์ ํ ๊ฒฉ๋ฆฌ๊ฐ ์ด๋ฃจ์ด์ ธ. - ์ค๋ต ํผ๋๋ฐฑ: A ๋ ์ด๊ธฐ๊ฐ ๋ฌธ์ ๊ฐ ์๋ store ๊ณต์ ๋ฌธ์ ์ผ. C ์ D ๋ ์ฌ๊ณ ์ ์์ธ์ด ์๋์ผ.
- ๐ ํต์ฌ ๊ธฐ์ต๋ฒ: "SSR + Provider ์์ = ๊ณต์ฉ ๋ฝ์ปค๋ฃธ. ์ ์ฌ์ฉ์ ์ท์ด ๊ทธ๋๋ก ๋จ์์์ด."
Q2. ๐ useHydrateAtoms ์ ๋์
์์ฒ ์ด๊ฐ
useHydrateAtoms([[countAtom, serverCount]])๋ฅผ ์ฌ์ฉํ์ด.
์ดํ ์๋ฒ์์serverCountprop ์ด5์์10์ผ๋ก ๋ฐ๋์์ด.
atom ์ ๊ฐ์ ์ด๋ป๊ฒ ๋๋๊ฐ?
- A) ์๋์ผ๋ก
10์ผ๋ก ์ ๋ฐ์ดํธ๋๋ค - B)
useHydrateAtoms๋ ์ปดํฌ๋ํธ ๋ง์ดํธ ์ ํ ๋ฒ๋ง atom ์ ๊ฐ์ ์ฃผ์ ํ๋ฏ๋ก, prop ์ด ๋ฐ๋์ด๋ atom ์ ์ฌ์ ํ5๋ค - C) ์๋ฌ๊ฐ ๋ฐ์ํ๋ค
- D) atom ์ด ๋ฆฌ์
๋์ด ์ด๊ธฐ๊ฐ
0์ผ๋ก ๋์๊ฐ๋ค
โ ์ ๋ต: B
๐ก ์์ธ ํด์ค:
- ์๋ฆฌ ์ค๋ช
: ๊ณต์ ๋ ํผ๋ฐ์ค์ ๋ช
์๋์ด ์์ด: "๊ฐ์ store ์์ atom ์ ํ ๋ฒ๋ง hydrate ๋๋ค".
useHydrateAtoms์ ๋ชฉ์ ์ "์ด๊ธฐ๊ฐ ์ฃผ์ " ์ด์ง, "prop ๋๊ธฐํ" ๊ฐ ์๋์ผ. ๋ง์ดํธ ํ์๋ atom ์ด ๋ ๋ฆฝ์ ์ธ ํด๋ผ์ด์ธํธ ์ํ๊ฐ ๋์ด, ์ฌ์ฉ์๊ฐ ์ง์ ๋ณ๊ฒฝํ๊ฑฐ๋setAtom์ ํธ์ถํด์ผ ๊ฐ์ด ๋ฐ๋์ด. - ์ค๋ต ํผ๋๋ฐฑ: A ๋
useEffect+setAtom์ผ๋ก prop ์ atom ์ ๋๊ธฐํํ๋ ๋ณ๋ ๋ก์ง์ด ํ์ํด. - ๐ ํต์ฌ ๊ธฐ์ต๋ฒ: "
useHydrateAtoms= ์จ์ ์ฌ๊ธฐ. ํ ๋ฒ ์ฌ์ผ๋ฉด ์ดํ ์๋ผ๋ ๊ฑด ์๋ฌผ(atom)์ ๋ชซ."
Q3. ๐๏ธ ์ํคํ ์ฒ ์ค๊ณ
์ํธ ๋์ด ๋ฌผ์์ด: "Server Component ์์๋ ์ atom ์ ์ง์ ์ธ ์ ์๋์?"
์์ฒ ์ด๊ฐ ๋ฉด์ ์ฐ์ต ์ผ์ ๋๋ตํด๋ดค์ด. ๊ฐ์ฅ ์ ํํ ๋ต๋ณ์?
์์ ๋ต๋ณ:
"Server Component ๋ ์๋ฒ์์ ํ ๋ฒ ์คํ๋๊ณ HTML ์ ๋ฐํํ๋ ํจ์์์. React ํ ์ React ๊ฐ ์ปดํฌ๋ํธ ํธ๋ฆฌ๋ฅผ ๋ฉ๋ชจ๋ฆฌ์ ์ ์งํ๋ฉด์ ๋ฆฌ๋ ๋์ ์ํ ๊ด๋ฆฌ๋ฅผ ํ๋ ์ปจํ ์คํธ์์๋ง ๋์ํด์. Server Component ์์ ํ ์ ํธ์ถํ๋ฉด 'Invalid hook call' ์๋ฌ๊ฐ ๋ฐ์ํด์. ๊ทธ๋์ Server Component ์์๋
fetch๋ DB ์ ๊ทผ์ผ๋ก ๋ฐ์ดํฐ๋ฅผ ๊ฐ์ ธ์ค๊ณ , ๊ทธ ๋ฐ์ดํฐ๋ฅผ Client Component ์ props ๋ก ์ ๋ฌํ ํ, Client Component ์์useHydrateAtoms๋ก atom ์ ์ฃผ์ ํ๋ ํจํด์ ์จ์ผ ํด์."
๐ก ์ด ๋ต๋ณ์ ์์ ์ ์ธ์ด๋ก ๋งํ ์ ์๋ค๋ฉด: ์ค๋ ๋ฐฐ์ด ํต์ฌ์ ์์ ํ ์ดํดํ ๊ฑฐ์ผ.
๐ฃ ์์ฒ ์ด์ ํด๊ทผ ์ผ๊ธฐ
์ค๋์ ์ง์ง ์์ง ๋ชปํ ํ๋ฃจ์๋ค. ๋ฐฐํฌ 10๋ถ ๋ง์ ๋ณด์ ์ฌ๊ณ ๊ฐ ๋๋ค๋.
์ฒ์์ Provider ๋ผ๋ ๊ฒ "๊ทธ๋ฅ ์์ผ๋ฉด ์ข๊ณ ์์ด๋ ๋๋ ๊ฑฐ" ๋ผ๊ณ ์๊ฐํ์ด. ๋ก์ปฌ์์ ์ ๋์ผ๋๊น. ๊ทผ๋ฐ Node.js ์๋ฒ๊ฐ ๋ชจ๋์ ์บ์ฑํ๋ค๋ ํน์ฑ ๋๋ฌธ์, ์ ์ญ store ๊ฐ ๋ชจ๋ ์์ฒญ์ ๊ณต์ ๋๋ค๋ ๊ฑธ ์ด๋ฒ์ ๋ผ์ ๋ฆฌ๊ฒ ๋ฐฐ์ ์ด. ์ด๋ก ์ผ๋ก ์๋ ๊ฑธ ์ค์๋ฒ ์ฌ๊ณ ๋ก ๋ฐฐ์ฐ๋ ๊ฑด ์์ ํ ๋ค๋ฅธ ๊ฒฝํ์ด์ผ.
์ํธ ๋์ด ์นจ์ฐฉํ๊ฒ 5๋ถ ์์ ๋กค๋ฐฑํ๊ณ Provider ์ถ๊ฐํ๊ณ ์ฌ๋ฐฐํฌํ์ ๋ ์ง์ง ๋๋จํ๋ค๊ณ ์๊ฐํ์ด. ๋๋ ์์ด ๋จ๋ ค์ ํค๋ณด๋๋ฅผ ๋ชป ์ณค๋๋ฐ. "์๋์ด๊ฐ ๋๋ฉด ์ด๋ฐ ์ํฉ์์๋ ์นจ์ฐฉํด์ง๋ ๊ฑด๊ฐ" ์ถ์์ด.
๐ก ์ค๋์ ๊ตํ: "SSR ์์ Provider ๋ ์ ํ์ด ์๋๋ผ ๋ณด์ ํ์ ์์๋ค. ๊ณต์ฉ ๋์ฅ๊ณ ์ ๊ฐ์ธ ์์์ ๋ฃ์ผ๋ฉด ๋ค๋ฅธ ์ฌ๋์ด ๋จน์ด๋ฒ๋ฆฐ๋ค."
useHydrateAtoms ํจํด๋ ์ค๋ ์ฒ์ ์ ๋๋ก ์จ๋ดค์ด. ์๋ฒ์์ fetch ํ๊ณ props ๋ก ๋๊ธฐ๊ณ atom ์ ์ฃผ์
ํ๋ ํ๋ฆ์ด ์ฒ์์ ๋ณต์กํด ๋ณด์๋๋ฐ, ํด๋ณด๋๊น ํ์คํ ๊น๋ํ ํจํด์ด์ผ. ์๋ฒ์ ์ฅ์ (๋น ๋ฅธ fetch, ์บ์ฑ)๊ณผ ํด๋ผ์ด์ธํธ์ ์ฅ์ (์ธํฐ๋ํฐ๋ธ ์ํ ๊ด๋ฆฌ)๋ฅผ ๊ฐ์ด ์ธ ์ ์์ผ๋๊น.
์ค๋์ ์ง์ง ์ผ์ฐ ์์ผ๊ฒ ๋ค. ๊ธด์ฅํด์ ์๋์ง๋ฅผ ๋ค ์จ๋ฒ๋ ธ์ด. ๋ด์ผ ์์นจ ์ถ๊ทผํด์ ์์ ๋ํํ ๊ฐ์ด ์ฌ๊ณผ๋๋ ค์ผ์ง.