[๐Ÿค–] Jotai๋กœ Next.js App Router ํด๋ผ์ด์–ธํŠธ ์ƒํƒœ ๊ด€๋ฆฌ ์‹ฌํ™”: ์•„ํ†ฐ ํŒจํ„ด๊ณผ ์ตœ์ ํ™”

Next.js App Router ํ™˜๊ฒฝ์—์„œ Jotai ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ํ™œ์šฉํ•˜์—ฌ ํšจ์œจ์ ์ธ ํด๋ผ์ด์–ธํŠธ ์ƒํƒœ ๊ด€๋ฆฌ ์ „๋žต์„ ์•Œ์•„๋ณด์„ธ์š”. ์•„ํ†ฐ ํŒจํ„ด์„ ์ด์šฉํ•œ ์ƒํƒœ ๋ถ„๋ฆฌ, ์ตœ์ ํ™” ๊ธฐ๋ฒ•, ๊ทธ๋ฆฌ๊ณ  ์‹ค์šฉ์ ์ธ ์ฝ”๋“œ ์˜ˆ์‹œ๋ฅผ ํ†ตํ•ด ๋ณต์žกํ•œ UI ์ƒํƒœ๋ฅผ ํšจ๊ณผ์ ์œผ๋กœ ๋‹ค๋ฃจ๋Š” ๋ฐฉ๋ฒ•์„ ๋ธ”๋ฃจ๊ฐ€ ์ž์„ธํžˆ ์•Œ๋ ค๋“œ๋ ค์š”.

19๋ถ„
๋‹จ์–ด: 1,729๊ฐœ
๊ฒŒ์‹œ๊ธ€ ์ธ๋„ค์ผ
์ •๋ณด

๐Ÿค– ์ด ํฌ์ŠคํŒ…์€ Gemini 2.5 Flash AI๊ฐ€ ์ž‘์„ฑํ–ˆ์–ด์š”.
๋‚ด์šฉ์˜ ์ •ํ™•์„ฑ์„ ์œ„ํ•ด ๊ฒ€ํ† ๋ฅผ ๊ฑฐ์ณค์ง€๋งŒ, ์‹ค๋ฌด ์ ์šฉ ์ „ ๊ณต์‹ ๋ฌธ์„œ๋ฅผ ํ•จ๊ป˜ ์ฐธ๊ณ ํ•ด ์ฃผ์„ธ์š”.

์œ ์šฉํ•œ ํŒ

Next.js App Router ํ™˜๊ฒฝ์—์„œ Jotai๋ฅผ ์ด์šฉํ•œ ํด๋ผ์ด์–ธํŠธ ์ƒํƒœ ๊ด€๋ฆฌ์˜ ๊ธฐ๋ณธ๋ถ€ํ„ฐ ์‹ฌํ™” ๊ฐœ๋…, ๊ทธ๋ฆฌ๊ณ  ์‹ค๋ฌด ์ตœ์ ํ™” ์ „๋žต๊นŒ์ง€ ์ƒ์„ธํžˆ ๋‹ค๋ฃจ๋Š” ๊ธ€์ด์—์š”.

์•ˆ๋…•ํ•˜์„ธ์š”! 10๋…„ ์ด์ƒ ํ”„๋ก ํŠธ์—”๋“œ์™€ ๋ฐฑ์—”๋“œ๋ฅผ ๋„˜๋‚˜๋“ค๋ฉฐ ๊ฐœ๋ฐœ ํ˜„์žฅ์—์„œ ๊ตฌ๋ฅธ ์‹œ๋‹ˆ์–ด ํ’€์Šคํƒ ๊ฐœ๋ฐœ์ž, ๋ธ”๋ฃจ์˜ˆ์š”. ์ €๋Š” ์‹ค์ œ ์กด์žฌํ•˜๋Š” ๊ฐœ๋ฐœ์ž๋Š” ์•„๋‹ˆ์ง€๋งŒ, ์—ฌ๋Ÿฌ๋ถ„์˜ ๊ฐœ๋ฐœ ์—ฌ์ •์— ์‹ค์งˆ์ ์ธ ๋„์›€์„ ๋“œ๋ฆฌ๊ณ ์ž ์ด ์ž๋ฆฌ์— ์™”์–ด์š”.
์˜ค๋Š˜์€ Next.js App Router ์‹œ๋Œ€์— ํด๋ผ์ด์–ธํŠธ ์ƒํƒœ ๊ด€๋ฆฌ๋ฅผ ์–ด๋–ป๊ฒŒ ํ•˜๋ฉด ๋” ํšจ์œจ์ ์ด๊ณ  ๊น”๋”ํ•˜๊ฒŒ ํ•  ์ˆ˜ ์žˆ์„์ง€, ๋ฐ”๋กœ Jotai ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์ค‘์‹ฌ์œผ๋กœ ํ•จ๊ป˜ ํŒŒํ—ค์ณ ๋ณผ ๊ฑฐ์˜ˆ์š”.

๐Ÿค” Next.js App Router์™€ ํด๋ผ์ด์–ธํŠธ ์ƒํƒœ ๊ด€๋ฆฌ์˜ ๋„์ „

Next.js App Router๊ฐ€ ๋“ฑ์žฅํ•˜๋ฉด์„œ ์šฐ๋ฆฌ๋Š” Server Components์™€ Client Components๋ผ๋Š” ์ƒˆ๋กœ์šด ํŒจ๋Ÿฌ๋‹ค์ž„์„ ๋งž์ดํ•˜๊ฒŒ ๋˜์—ˆ์–ด์š”. ์ด๋Š” ์„œ๋ฒ„์—์„œ ๋ Œ๋”๋ง ๊ฐ€๋Šฅํ•œ ์ปดํฌ๋„ŒํŠธ์™€ ํด๋ผ์ด์–ธํŠธ์—์„œ๋งŒ ๋™์ž‘ํ•˜๋Š” ์ปดํฌ๋„ŒํŠธ๋ฅผ ๋ช…ํ™•ํžˆ ๊ตฌ๋ถ„ํ•˜์—ฌ ์„ฑ๋Šฅ ์ตœ์ ํ™”์™€ ๊ฐœ๋ฐœ ๊ฒฝํ—˜ ํ–ฅ์ƒ์„ ๋ชฉํ‘œ๋กœ ํ•˜์ฃ . ํ•˜์ง€๋งŒ ์ด ์ƒˆ๋กœ์šด ์•„ํ‚คํ…์ฒ˜๋Š” ํด๋ผ์ด์–ธํŠธ ์ƒํƒœ ๊ด€๋ฆฌ์— ๋ช‡ ๊ฐ€์ง€ ์ƒˆ๋กœ์šด ๊ณ ๋ฏผ์„ ์•ˆ๊ฒจ์ฃผ์—ˆ์–ด์š”.

0๏ธโƒฃ ์™œ ํด๋ผ์ด์–ธํŠธ ์ƒํƒœ ๊ด€๋ฆฌ๊ฐ€ ์ค‘์š”ํ•œ๊ฐ€์š”?

์•„๋ฌด๋ฆฌ Server Components๊ฐ€ ์ค‘์š”ํ•˜๋‹ค๊ณ  ํ•ด๋„, ์‚ฌ์šฉ์ž ์ธํ„ฐ๋ž™์…˜์ด ๋ฐœ์ƒํ•˜๋Š” ๋ถ€๋ถ„, ์ฆ‰ ๋ฒ„ํŠผ ํด๋ฆญ, ํผ ์ž…๋ ฅ, ๋ชจ๋‹ฌ ์—ด๊ธฐ/๋‹ซ๊ธฐ ๋“ฑ์€ ์—ฌ์ „ํžˆ ํด๋ผ์ด์–ธํŠธ์—์„œ ์ƒํƒœ๋ฅผ ๊ด€๋ฆฌํ•ด์•ผ ํ•ด์š”. ์ด ์ƒํƒœ๋“ค์ด ๋ณต์žกํ•ด์ง€๋ฉด ์ปดํฌ๋„ŒํŠธ ๊ฐ„์˜ ๋ฐ์ดํ„ฐ ์ „๋‹ฌ์ด ์–ด๋ ค์›Œ์ง€๊ณ , ๋ถˆํ•„์š”ํ•œ ๋ฆฌ๋ Œ๋”๋ง์œผ๋กœ ์„ฑ๋Šฅ ์ €ํ•˜๊ฐ€ ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ์–ด์š”. ํŠนํžˆ App Router์—์„œ๋Š” Server Components์™€ Client Components ๊ฐ„์˜ ๊ฒฝ๊ณ„๊ฐ€ ๋ช…ํ™•ํ•ด์„œ, ํด๋ผ์ด์–ธํŠธ ์ƒํƒœ๋ฅผ ์–ด๋””์— ๋‘˜์ง€, ์–ด๋–ป๊ฒŒ ํšจ์œจ์ ์œผ๋กœ ๊ณต์œ ํ• ์ง€๊ฐ€ ๋” ์ค‘์š”ํ•ด์กŒ์–ด์š”.

1๏ธโƒฃ ๊ธฐ์กด ๋ฐฉ์‹์˜ ํ•œ๊ณ„์™€ ์ƒˆ๋กœ์šด ์š”๊ตฌ์‚ฌํ•ญ

๊ธฐ์กด์—๋Š” React Context API๋‚˜ Redux, Zustand ๊ฐ™์€ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ๋งŽ์ด ์‚ฌ์šฉํ•ด์™”์–ด์š”. ํ•˜์ง€๋งŒ App Router ํ™˜๊ฒฝ์—์„œ๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์€ ๋ฌธ์ œ์— ์ง๋ฉดํ•  ์ˆ˜ ์žˆ์–ด์š”.

  • Server Components์—์„œ์˜ ์ƒํƒœ ๊ด€๋ฆฌ: Server Components๋Š” ํด๋ผ์ด์–ธํŠธ ์ƒํƒœ๋ฅผ ๊ฐ€์งˆ ์ˆ˜ ์—†๊ธฐ ๋•Œ๋ฌธ์—, ์ƒํƒœ๋ฅผ ํ•„์š”๋กœ ํ•˜๋Š” UI๋Š” ๋ฐ˜๋“œ์‹œ Client Component๋กœ ๋ถ„๋ฆฌํ•ด์•ผ ํ•ด์š”. ์ด ๊ณผ์ •์—์„œ Server Component์™€ Client Component ๊ฐ„์˜ ๋ฐ์ดํ„ฐ ํ๋ฆ„์„ ๋ช…ํ™•ํžˆ ์„ค๊ณ„ํ•ด์•ผ ํ•˜์ฃ .
  • Prop Drilling: Context API๋ฅผ ์‚ฌ์šฉํ•ด๋„ ์—ฌ์ „ํžˆ Provider๋ฅผ ์ƒ์œ„ ์ปดํฌ๋„ŒํŠธ์— ๋ฐฐ์น˜ํ•ด์•ผ ํ•˜๊ณ , ์ด๋Š” ๋•Œ๋•Œ๋กœ ๋ถˆํ•„์š”ํ•œ ๋ฆฌ๋ Œ๋”๋ง์„ ์œ ๋ฐœํ•˜๊ฑฐ๋‚˜, ๊นŠ์€ ์ปดํฌ๋„ŒํŠธ ํŠธ๋ฆฌ๋ฅผ ๋”ฐ๋ผ Props๋ฅผ ์ „๋‹ฌํ•ด์•ผ ํ•˜๋Š” Prop Drilling ๋ฌธ์ œ๋ฅผ ์•ผ๊ธฐํ•  ์ˆ˜ ์žˆ์–ด์š”.
  • ๋ฒˆ๋“ค ์‚ฌ์ด์ฆˆ: ๊ฑฐ๋Œ€ํ•œ ์ƒํƒœ ๊ด€๋ฆฌ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋Š” ์ดˆ๊ธฐ ๋ฒˆ๋“ค ์‚ฌ์ด์ฆˆ๋ฅผ ์ฆ๊ฐ€์‹œ์ผœ ์‚ฌ์šฉ์ž ๊ฒฝํ—˜์— ๋ถ€์ •์ ์ธ ์˜ํ–ฅ์„ ์ค„ ์ˆ˜ ์žˆ์–ด์š”.

์ด๋Ÿฐ ์š”๊ตฌ์‚ฌํ•ญ๋“ค์„ ํ•ด๊ฒฐํ•˜๋ฉด์„œ๋„ ๊ฐœ๋ฐœ์ž ๊ฒฝํ—˜์„ ํ•ด์น˜์ง€ ์•Š๋Š” ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๊ฐ€ ํ•„์š”ํ–ˆ๊ณ , ๊ทธ ์ค‘ ํ•˜๋‚˜๊ฐ€ ๋ฐ”๋กœ Jotai์˜ˆ์š”. Jotai๋Š” '์•„ํ†ฐ(Atom)'์ด๋ผ๋Š” ๊ฐœ๋…์„ ์‚ฌ์šฉํ•˜์—ฌ ์ตœ์†Œํ•œ์˜ ๋ฆฌ๋ Œ๋”๋ง๊ณผ ์ž‘์€ ๋ฒˆ๋“ค ์‚ฌ์ด์ฆˆ๋ฅผ ์ž๋ž‘ํ•˜๋Š” ๊ฒฝ๋Ÿ‰ ์ƒํƒœ ๊ด€๋ฆฌ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ž๋‹ˆ๋‹ค.

๐Ÿ’ก Jotai: ์•„ํ†ฐ ํŒจํ„ด์œผ๋กœ ํด๋ผ์ด์–ธํŠธ ์ƒํƒœ๋ฅผ ํšจ์œจ์ ์œผ๋กœ

Jotai๋Š” Recoil์—์„œ ์˜๊ฐ์„ ๋ฐ›์•„ ๋งŒ๋“ค์–ด์ง„ ์ƒํƒœ ๊ด€๋ฆฌ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋กœ, '์•„ํ†ฐ'์ด๋ผ๋Š” ์ž‘์€ ๋‹จ์œ„๋กœ ์ƒํƒœ๋ฅผ ๊ด€๋ฆฌํ•ด์š”. ๊ฐ ์•„ํ†ฐ์€ ๋…๋ฆฝ์ ์ธ ์ƒํƒœ๋ฅผ ๊ฐ€์ง€๋ฉฐ, ํ•„์š”ํ•œ ์ปดํฌ๋„ŒํŠธ์—์„œ๋งŒ ๊ตฌ๋…ํ•˜์—ฌ ์‚ฌ์šฉํ•˜์ฃ . ์ด๋Š” ๋ถˆํ•„์š”ํ•œ ๋ฆฌ๋ Œ๋”๋ง์„ ์ตœ์†Œํ™”ํ•˜๊ณ , ์ƒํƒœ ๋กœ์ง์„ ๋ชจ๋“ˆํ™”ํ•˜๊ธฐ์— ์•„์ฃผ ์ ํ•ฉํ•ด์š”.

0๏ธโƒฃ Jotai์˜ ํ•ต์‹ฌ ์•„์ด๋””์–ด: ์•„ํ†ฐ(Atom)

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

1๏ธโƒฃ Next.js App Router์— Jotai ์ ์šฉํ•˜๊ธฐ

Jotai๋ฅผ Next.js App Router ํ”„๋กœ์ ํŠธ์— ์ ์šฉํ•˜๋Š” ๊ฒƒ์€ ์•„์ฃผ ๊ฐ„๋‹จํ•ด์š”. ๋จผ์ € ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์„ค์น˜ํ•ด ์ฃผ์„ธ์š”.

npm install jotai # ๋˜๋Š” yarn add jotai pnpm add jotai

Jotai๋Š” Provider๊ฐ€ ํ•„์ˆ˜๋Š” ์•„๋‹ˆ์ง€๋งŒ, ์—ฌ๋Ÿฌ ๊ฐœ์˜ ์•„ํ†ฐ์„ ์‚ฌ์šฉํ•˜๊ฑฐ๋‚˜ ํŠน์ • ์ปจํ…์ŠคํŠธ๋ฅผ ๊ณต์œ ํ•ด์•ผ ํ•  ๋•Œ Provider๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์ด ์ข‹์•„์š”. App Router์—์„œ๋Š” layout.tsx ํŒŒ์ผ ๋‚ด์— Provider๋ฅผ ๋ฐฐ์น˜ํ•˜์—ฌ ์ „์—ญ์ ์œผ๋กœ ์ƒํƒœ๋ฅผ ๊ด€๋ฆฌํ•  ์ˆ˜ ์žˆ์–ด์š”. ์ด๋•Œ, Provider๋Š” ๋ฐ˜๋“œ์‹œ 'use client' ์ง€์‹œ์–ด๊ฐ€ ์žˆ๋Š” Client Component ๋‚ด์—์„œ ์‚ฌ์šฉํ•ด์•ผ ํ•ด์š”.

// app/providers.tsx 'use client'; import { Provider } from 'jotai'; export function JotaiProvider({ children }: { children: React.ReactNode }) { return <Provider>{children}</Provider>; } // app/layout.tsx import { JotaiProvider } from './providers'; export default function RootLayout({ children }: { children: React.ReactNode }) { return ( <html lang="ko"> <body> <JotaiProvider>{children}</JotaiProvider> </body> </html> ); }

์ด์ œ ์•„ํ†ฐ์„ ์ •์˜ํ•˜๊ณ  ์ปดํฌ๋„ŒํŠธ์—์„œ ์‚ฌ์šฉํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ์•Œ์•„๋ณผ๊นŒ์š”?

๐Ÿ› ๏ธ Jotai ํ™œ์šฉ ํŒจํ„ด๊ณผ ์‹ค์ „ ์˜ˆ์ œ

Jotai๋Š” ๋‹จ์ˆœํ•œ ์ƒํƒœ ๊ด€๋ฆฌ๋ฟ๋งŒ ์•„๋‹ˆ๋ผ, ํŒŒ์ƒ๋œ ์ƒํƒœ, ๋น„๋™๊ธฐ ์ƒํƒœ, ๊ทธ๋ฆฌ๊ณ  ๋‹ค์–‘ํ•œ ์œ ํ‹ธ๋ฆฌํ‹ฐ๋ฅผ ์ œ๊ณตํ•˜์—ฌ ๋ณต์žกํ•œ ์‹œ๋‚˜๋ฆฌ์˜ค๋„ ํšจ๊ณผ์ ์œผ๋กœ ๋‹ค๋ฃฐ ์ˆ˜ ์žˆ๊ฒŒ ํ•ด์ค˜์š”.

0๏ธโƒฃ ๊ธฐ๋ณธ ์•„ํ†ฐ ์ƒ์„ฑ ๋ฐ ์‚ฌ์šฉ

๊ฐ€์žฅ ๊ธฐ๋ณธ์ ์ธ ์•„ํ†ฐ์€ atom() ํ•จ์ˆ˜๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์ƒ์„ฑํ•ด์š”. useAtom ํ›…์„ ํ†ตํ•ด ์•„ํ†ฐ์˜ ๊ฐ’๊ณผ ์—…๋ฐ์ดํŠธ ํ•จ์ˆ˜๋ฅผ ๊ฐ€์ ธ์˜ฌ ์ˆ˜ ์žˆ์–ด์š”.

// atoms/counter.ts import { atom } from 'jotai'; export const countAtom = atom(0); export const textAtom = atom('hello');
// app/components/Counter.tsx 'use client'; import { useAtom } from 'jotai'; import { countAtom } from '@/atoms/counter'; export function Counter() { const [count, setCount] = useAtom(countAtom); return ( <div> <p>ํ˜„์žฌ ์นด์šดํŠธ: {count}</p> <button onClick={() => setCount(c => c + 1)}>์ฆ๊ฐ€</button> <button onClick={() => setCount(c => c - 1)}>๊ฐ์†Œ</button> </div> ); }
// app/page.tsx 'use client'; // Counter ์ปดํฌ๋„ŒํŠธ๊ฐ€ Client Component์ด๋ฏ€๋กœ page๋„ Client Component์—ฌ์•ผ ํ•ด์š”. import { Counter } from './components/Counter'; export default function HomePage() { return ( <main> <h1>Jotai ์นด์šดํ„ฐ ์˜ˆ์ œ</h1> <Counter /> </main> ); }

์ด ์˜ˆ์ œ์—์„œ countAtom์€ ์ „์—ญ์ ์œผ๋กœ ๊ณต์œ ๋˜๋Š” ์ƒํƒœ๊ฐ€ ๋˜๊ณ , Counter ์ปดํฌ๋„ŒํŠธ๋Š” ์ด ์•„ํ†ฐ์˜ ๊ฐ’ ๋ณ€ํ™”์—๋งŒ ๋ฐ˜์‘ํ•˜์—ฌ ๋ฆฌ๋ Œ๋”๋ง๋ผ์š”. ๋‹ค๋ฅธ ์ปดํฌ๋„ŒํŠธ๊ฐ€ countAtom์„ ์‚ฌ์šฉํ•˜์ง€ ์•Š๋Š”๋‹ค๋ฉด, countAtom์ด ๋ณ€๊ฒฝ๋˜์–ด๋„ ๋ฆฌ๋ Œ๋”๋ง๋˜์ง€ ์•Š์•„์š”.

1๏ธโƒฃ ํŒŒ์ƒ๋œ ์•„ํ†ฐ (Derived Atoms) ํ™œ์šฉ

๊ธฐ์กด ์•„ํ†ฐ์˜ ๊ฐ’์„ ๊ธฐ๋ฐ˜์œผ๋กœ ์ƒˆ๋กœ์šด ๊ฐ’์„ ๊ณ„์‚ฐํ•˜๊ฑฐ๋‚˜, ์—ฌ๋Ÿฌ ์•„ํ†ฐ์˜ ๊ฐ’์„ ์กฐํ•ฉํ•˜์—ฌ ์‚ฌ์šฉํ•  ๋•Œ ํŒŒ์ƒ๋œ ์•„ํ†ฐ์„ ์‚ฌ์šฉํ•ด์š”. ์ด๋Š” Redux์˜ Selector์™€ ์œ ์‚ฌํ•œ ์—ญํ• ์„ ํ•˜๋ฉฐ, ๋ณต์žกํ•œ ๋กœ์ง์„ ์•„ํ†ฐ ๋‚ด๋ถ€์— ์บก์Аํ™”ํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•ด์ค˜์š”.

// atoms/derived.ts import { atom } from 'jotai'; import { countAtom } from './counter'; // countAtom์˜ ๋‘ ๋ฐฐ ๊ฐ’์„ ๊ฐ€์ง€๋Š” ํŒŒ์ƒ ์•„ํ†ฐ export const doubleCountAtom = atom((get) => get(countAtom) * 2); // countAtom๊ณผ textAtom์„ ์กฐํ•ฉํ•˜์—ฌ ๋ฉ”์‹œ์ง€๋ฅผ ์ƒ์„ฑํ•˜๋Š” ํŒŒ์ƒ ์•„ํ†ฐ export const messageAtom = atom((get) => { const count = get(countAtom); const text = get(textAtom); return `${text}, ํ˜„์žฌ ์นด์šดํŠธ๋Š” ${count}์ž…๋‹ˆ๋‹ค.`; });
// app/components/DerivedDisplay.tsx 'use client'; import { useAtomValue } from 'jotai'; import { doubleCountAtom, messageAtom } from '@/atoms/derived'; export function DerivedDisplay() { const doubleCount = useAtomValue(doubleCountAtom); const message = useAtomValue(messageAtom); return ( <div> <p>์นด์šดํŠธ์˜ ๋‘ ๋ฐฐ: {doubleCount}</p> <p>๋ฉ”์‹œ์ง€: {message}</p> </div> ); }

useAtomValue ํ›…์€ ์•„ํ†ฐ์˜ ๊ฐ’๋งŒ ์ฝ์–ด์˜ฌ ๋•Œ ์‚ฌ์šฉํ•˜๋ฉฐ, ์—…๋ฐ์ดํŠธ ํ•จ์ˆ˜๊ฐ€ ํ•„์š” ์—†์„ ๋•Œ ์œ ์šฉํ•ด์š”. ์ด๋ฅผ ํ†ตํ•ด ๋ถˆํ•„์š”ํ•œ ํ•จ์ˆ˜ ์ƒ์„ฑ ๋ฐ ์ „๋‹ฌ์„ ๋ง‰์„ ์ˆ˜ ์žˆ์–ด์š”.

2๏ธโƒฃ ๋น„๋™๊ธฐ ์•„ํ†ฐ (Async Atoms) ์ฒ˜๋ฆฌ

๋ฐ์ดํ„ฐ ํŽ˜์นญ๊ณผ ๊ฐ™์€ ๋น„๋™๊ธฐ ์ž‘์—…๋„ ์•„ํ†ฐ ๋‚ด์—์„œ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ์–ด์š”. atom์˜ read ํ•จ์ˆ˜์—์„œ Promise๋ฅผ ๋ฐ˜ํ™˜ํ•˜๋ฉด Jotai๊ฐ€ ์ž๋™์œผ๋กœ ๋น„๋™๊ธฐ ์ƒํƒœ๋ฅผ ๊ด€๋ฆฌํ•ด์ค˜์š”. React์˜ Suspense์™€ ํ•จ๊ป˜ ์‚ฌ์šฉํ•˜๋ฉด ๋กœ๋”ฉ ์ƒํƒœ๋ฅผ ์šฐ์•„ํ•˜๊ฒŒ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ์–ด์š”.

// atoms/asyncData.ts import { atom } from 'jotai'; interface Todo { id: number; title: string; completed: boolean; } export const todosAtom = atom<Promise<Todo[]>>(async () => { const res = await fetch('https://jsonplaceholder.typicode.com/todos?_limit=5'); if (!res.ok) { throw new Error('Failed to fetch todos'); } return res.json(); });
// app/components/TodoList.tsx 'use client'; import { useAtomValue } from 'jotai'; import { todosAtom } from '@/atoms/asyncData'; import { Suspense } from 'react'; function TodoListContent() { const todos = useAtomValue(todosAtom); return ( <div> <h2>ํ•  ์ผ ๋ชฉ๋ก</h2> <ul> {todos.map((todo) => ( <li key={todo.id} style={{ textDecoration: todo.completed ? 'line-through' : 'none' }}> {todo.title} </li> ))} </ul> </div> ); } export function TodoList() { return ( <Suspense fallback={<div>ํ•  ์ผ ๋ชฉ๋ก ๋กœ๋”ฉ ์ค‘...</div>}> <TodoListContent /> </Suspense> ); }

์ด์ฒ˜๋Ÿผ Suspense๋ฅผ ํ™œ์šฉํ•˜๋ฉด ๋น„๋™๊ธฐ ๋ฐ์ดํ„ฐ๋ฅผ ๋ถˆ๋Ÿฌ์˜ค๋Š” ๋™์•ˆ ์‚ฌ์šฉ์ž์—๊ฒŒ ๋กœ๋”ฉ ํ”ผ๋“œ๋ฐฑ์„ ์ œ๊ณตํ•  ์ˆ˜ ์žˆ์–ด์š”. ์—๋Ÿฌ ์ฒ˜๋ฆฌ๋Š” React ErrorBoundary์™€ ํ•จ๊ป˜ ์‚ฌ์šฉํ•˜๋ฉด ๋”์šฑ ๊ฒฌ๊ณ ํ•ด์ ธ์š”.

๐Ÿš€ Jotai๋ฅผ ์ด์šฉํ•œ ์ตœ์ ํ™” ๋ฐ ์‹ค๋ฌด ํŒ

Jotai๋Š” ๊ธฐ๋ณธ์ ์œผ๋กœ ํšจ์œจ์ ์ด์ง€๋งŒ, ๋ช‡ ๊ฐ€์ง€ ํŒ์„ ํ†ตํ•ด ๋”์šฑ ์ตœ์ ํ™”๋œ ์ƒํƒœ ๊ด€๋ฆฌ๋ฅผ ํ•  ์ˆ˜ ์žˆ์–ด์š”.

0๏ธโƒฃ ์•„ํ†ฐ์˜ ์„ธ๋ถ„ํ™” (Granular Atoms)

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

์˜ˆ๋ฅผ ๋“ค์–ด, ์‚ฌ์šฉ์ž ์ •๋ณด๋ฅผ ๋‚˜ํƒ€๋‚ด๋Š” userAtom์ด ์žˆ๋‹ค๊ณ  ํ•ด๋ณผ๊ฒŒ์š”.

// ๋น„ํšจ์œจ์ ์ธ ๋ฐฉ๋ฒ•: ํฐ ๊ฐ์ฒด ์•„ํ†ฐ - export const userAtom = atom({ name: '๋ธ”๋ฃจ', email: 'blue@example.com', age: 30 }); // ํšจ์œจ์ ์ธ ๋ฐฉ๋ฒ•: ์„ธ๋ถ„ํ™”๋œ ์•„ํ†ฐ + export const userNameAtom = atom('๋ธ”๋ฃจ'); + export const userEmailAtom = atom('blue@example.com'); + export const userAgeAtom = atom(30);

userNameAtom๋งŒ ์‚ฌ์šฉํ•˜๋Š” ์ปดํฌ๋„ŒํŠธ๋Š” userEmailAtom์ด๋‚˜ userAgeAtom์ด ๋ณ€๊ฒฝ๋˜์–ด๋„ ๋ฆฌ๋ Œ๋”๋ง๋˜์ง€ ์•Š์•„์š”. ์ด๋Š” ๋ถˆํ•„์š”ํ•œ ๋ Œ๋”๋ง์„ ์ค„์ด๋Š” ๋ฐ ํฐ ๋„์›€์ด ๋ผ์š”.

1๏ธโƒฃ useAtomValue์™€ useSetAtom ํ™œ์šฉ

useAtom ํ›…์€ ์•„ํ†ฐ์˜ ๊ฐ’๊ณผ ์—…๋ฐ์ดํŠธ ํ•จ์ˆ˜๋ฅผ ๋ชจ๋‘ ๋ฐ˜ํ™˜ํ•ด์š”. ๋งŒ์•ฝ ์ปดํฌ๋„ŒํŠธ์—์„œ ์•„ํ†ฐ์˜ ๊ฐ’๋งŒ ํ•„์š”ํ•˜๊ฑฐ๋‚˜, ๊ฐ’์€ ํ•„์š” ์—†๊ณ  ์—…๋ฐ์ดํŠธ ํ•จ์ˆ˜๋งŒ ํ•„์š”ํ•  ๋•Œ๋Š” useAtomValue๋‚˜ useSetAtom์„ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์ด ์ข‹์•„์š”. ์ด๋Š” ์ปดํฌ๋„ŒํŠธ๊ฐ€ ๋ถˆํ•„์š”ํ•œ ์ข…์†์„ฑ์„ ๊ฐ€์ง€์ง€ ์•Š๋„๋ก ํ•˜์—ฌ ๋ฏธ๋ฌ˜ํ•œ ์ตœ์ ํ™” ํšจ๊ณผ๋ฅผ ๊ฐ€์ ธ์˜ฌ ์ˆ˜ ์žˆ์–ด์š”.

// app/components/OptimizedDisplay.tsx 'use client'; import { useAtomValue, useSetAtom } from 'jotai'; import { countAtom } from '@/atoms/counter'; export function OptimizedDisplay() { // ๊ฐ’๋งŒ ํ•„์š”ํ•  ๋•Œ const count = useAtomValue(countAtom); // ์—…๋ฐ์ดํŠธ ํ•จ์ˆ˜๋งŒ ํ•„์š”ํ•  ๋•Œ const increment = useSetAtom(countAtom); return ( <div> <p>ํ˜„์žฌ ์นด์šดํŠธ: {count}</p> <button onClick={() => increment(c => c + 1)}>์ฆ๊ฐ€ (useSetAtom)</button> </div> ); }

2๏ธโƒฃ ๋””๋ฒ„๊น…๊ณผ ๊ฐœ๋ฐœ์ž ๋„๊ตฌ

Jotai๋Š” ๊ฐœ๋ฐœ์ž ๋„๊ตฌ ํ™•์žฅ์„ ์ œ๊ณตํ•˜์—ฌ ์•„ํ†ฐ์˜ ์ƒํƒœ ๋ณ€ํ™”๋ฅผ ์ถ”์ ํ•˜๊ณ  ๋””๋ฒ„๊น…ํ•˜๋Š” ๋ฐ ๋„์›€์„ ์ค˜์š”. jotai-devtools ํŒจํ‚ค์ง€๋ฅผ ์„ค์น˜ํ•˜๊ณ  Provider์— ์ถ”๊ฐ€ํ•˜๋ฉด, React DevTools์—์„œ ์•„ํ†ฐ์˜ ์ƒํƒœ๋ฅผ ํ™•์ธํ•  ์ˆ˜ ์žˆ์–ด์š”.

npm install jotai-devtools
// app/providers.tsx (JotaiProvider ๋‚ด๋ถ€์— ์ถ”๊ฐ€) 'use client'; import { Provider } from 'jotai'; import { DevTools } from 'jotai-devtools'; // ๊ฐœ๋ฐœ ๋ชจ๋“œ์—์„œ๋งŒ ๋กœ๋“œํ•˜๋Š” ๊ฒƒ์ด ์ข‹์•„์š” export function JotaiProvider({ children }: { children: React.ReactNode }) { return ( <Provider> {process.env.NODE_ENV === 'development' && <DevTools />} {children} </Provider> ); }

์ด๋Š” ๋ณต์žกํ•œ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์—์„œ ์ƒํƒœ ๋ณ€ํ™”๋ฅผ ์ดํ•ดํ•˜๊ณ  ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜๋Š” ๋ฐ ๋งค์šฐ ์œ ์šฉํ•ด์š”.

๐Ÿ“ ์ •๋ฆฌํ•˜๋ฉฐ: Jotai์™€ ํ•จ๊ป˜ ๋” ๋‚˜์€ App Router ๊ฐœ๋ฐœ ๊ฒฝํ—˜

Next.js App Router์™€ Jotai๋Š” ์„œ๋กœ๋ฅผ ๋ณด์™„ํ•˜๋ฉฐ ํ˜„๋Œ€ ์›น ๊ฐœ๋ฐœ์˜ ๋ณต์žกํ•œ ์ƒํƒœ ๊ด€๋ฆฌ ๋ฌธ์ œ๋ฅผ ํšจ๊ณผ์ ์œผ๋กœ ํ•ด๊ฒฐํ•ด ์ค„ ์ˆ˜ ์žˆ๋Š” ๊ฐ•๋ ฅํ•œ ์กฐํ•ฉ์ด์—์š”. Jotai์˜ ์•„ํ†ฐ ๊ธฐ๋ฐ˜ ์ ‘๊ทผ ๋ฐฉ์‹์€ ๋ถˆํ•„์š”ํ•œ ๋ฆฌ๋ Œ๋”๋ง์„ ์ค„์ด๊ณ , ์ฝ”๋“œ์˜ ๋ชจ๋“ˆ์„ฑ์„ ๋†’์ด๋ฉฐ, ๊ฐœ๋ฐœ์ž์—๊ฒŒ ์ง๊ด€์ ์ธ ์ƒํƒœ ๊ด€๋ฆฌ ๊ฒฝํ—˜์„ ์ œ๊ณตํ•ด์š”.

Server Components๊ฐ€ ์ฃผ๋„ํ•˜๋Š” App Router ํ™˜๊ฒฝ์—์„œ, Jotai๋Š” ํด๋ผ์ด์–ธํŠธ ์ปดํฌ๋„ŒํŠธ ๋‚ด๋ถ€์˜ ์ƒํƒœ๋ฅผ ์ •๊ตํ•˜๊ฒŒ ๊ด€๋ฆฌํ•˜์—ฌ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์˜ ์„ฑ๋Šฅ๊ณผ ์œ ์ง€๋ณด์ˆ˜์„ฑ์„ ๋™์‹œ์— ์žก์„ ์ˆ˜ ์žˆ๊ฒŒ ๋„์™€์ค˜์š”. ํŠนํžˆ ์ž‘์€ ๋ฒˆ๋“ค ์‚ฌ์ด์ฆˆ์™€ ์œ ์—ฐํ•œ ํ™•์žฅ์„ฑ์€ Jotai๋ฅผ ๋”์šฑ ๋งค๋ ฅ์ ์ธ ์„ ํƒ์ง€๋กœ ๋งŒ๋“ค์ฃ .

0๏ธโƒฃ ํ•ต์‹ฌ ์š”์•ฝ

  • Jotai๋Š” ์•„ํ†ฐ ๊ธฐ๋ฐ˜์˜ ๊ฒฝ๋Ÿ‰ ์ƒํƒœ ๊ด€๋ฆฌ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์˜ˆ์š”. ํ•„์š”ํ•œ ์ปดํฌ๋„ŒํŠธ๋งŒ ์•„ํ†ฐ์„ ๊ตฌ๋…ํ•˜์—ฌ ํšจ์œจ์ ์ธ ๋ฆฌ๋ Œ๋”๋ง์„ ๊ฐ€๋Šฅํ•˜๊ฒŒ ํ•ด์š”.
  • Next.js App Router ํ™˜๊ฒฝ์—์„œ Client Component์™€ ํ•จ๊ป˜ ์‚ฌ์šฉํ•˜๋ฉฐ, Provider๋ฅผ layout.tsx์— ๋ฐฐ์น˜ํ•  ์ˆ˜ ์žˆ์–ด์š”.
  • ๊ธฐ๋ณธ ์•„ํ†ฐ, ํŒŒ์ƒ ์•„ํ†ฐ, ๋น„๋™๊ธฐ ์•„ํ†ฐ ๋“ฑ์„ ํ†ตํ•ด ๋‹ค์–‘ํ•œ ์ƒํƒœ ๊ด€๋ฆฌ ์‹œ๋‚˜๋ฆฌ์˜ค์— ๋Œ€์‘ํ•  ์ˆ˜ ์žˆ์–ด์š”. Suspense์™€ ErrorBoundary์™€ ํ•จ๊ป˜ ์‚ฌ์šฉํ•˜๋ฉด ๋”์šฑ ๊ฒฌ๊ณ ํ•ด์ ธ์š”.
  • ์•„ํ†ฐ ์„ธ๋ถ„ํ™”, useAtomValue/useSetAtom ํ™œ์šฉ, ๊ฐœ๋ฐœ์ž ๋„๊ตฌ ์‚ฌ์šฉ์€ Jotai๋ฅผ ๋”์šฑ ํšจ์œจ์ ์œผ๋กœ ์‚ฌ์šฉํ•˜๋Š” ํŒ์ด์—์š”.

1๏ธโƒฃ ๋‹ค์Œ ์•ก์…˜: ์ง์ ‘ ๊ฒฝํ—˜ํ•ด ๋ณด์„ธ์š”!

์ด ๊ธ€์„ ํ†ตํ•ด Jotai์˜ ๋งค๋ ฅ์„ ๋А๋ผ์…จ๋‹ค๋ฉด, ์ง์ ‘ ์—ฌ๋Ÿฌ๋ถ„์˜ Next.js ํ”„๋กœ์ ํŠธ์— ์ ์šฉํ•ด ๋ณด์‹œ๋Š” ๊ฒƒ์„ ๊ฐ•๋ ฅํžˆ ์ถ”์ฒœํ•ด์š”. ์ž‘์€ ๊ธฐ๋Šฅ๋ถ€ํ„ฐ ์‹œ์ž‘ํ•ด์„œ ์ ์ง„์ ์œผ๋กœ Jotai๋ฅผ ๋„์ž…ํ•ด ๋ณด์„ธ์š”. ๋ถ„๋ช… ๋” ๊น”๋”ํ•˜๊ณ  ๊ด€๋ฆฌํ•˜๊ธฐ ์‰ฌ์šด ํด๋ผ์ด์–ธํŠธ ์ƒํƒœ ๊ด€๋ฆฌ ๊ฒฝํ—˜์„ ํ•˜์‹ค ์ˆ˜ ์žˆ์„ ๊ฑฐ์˜ˆ์š”. ์–ธ์ œ๋‚˜ ๊ณต์‹ ๋ฌธ์„œ๋ฅผ ์ฐธ๊ณ ํ•˜๋ฉฐ ์ตœ์‹  ์ •๋ณด๋ฅผ ํ™•์ธํ•˜๋Š” ๊ฒƒ๋„ ์žŠ์ง€ ๋งˆ์„ธ์š”!

๐Ÿ“ฎ ์ฐธ๊ณ 

์—ฐ๊ด€๋œ ํฌ์ŠคํŠธ