๐ Next.js 6์ฅ: Data Fetching ๋ฅด๋ค์์ค โ fetch ๋ก์๊ณผ Memoization์ ๋น๋ฐ
๐ ๊ฐ์
fetch ๋ฉ๋ชจ์ด์ ์ด์ ๊ณผ RSC์์์ ๋ฐ์ดํฐ ํจ์นญ ํจํด, waterfall ๋ฐฉ์ง ์ ๋ต์ ๋ฐฐ์๋๋ค.
๐ ๋ชฉ์ฐจ
- ๐ ์ด ๋ฌธ์๋ฅผ ์ฝ๊ธฐ ์ ์
- ๐ค ์ ์์์ผ ํ๋๊ฐ
- ๐๏ธ ๋น์ ๋ก ๋จผ์ ์ดํดํ๊ธฐ
- ๐งฉ
fetchAPI์ ๋ถํ: Next.js๊ฐ ํ์น ํ์ค ๐ข - ๐ก๏ธ
Request Memoizationโ ๋๊ฐ์ API ํธ์ถ์ ์ค๋ณต ๋ฐฉ์ด ๐ก - ๐ฑ
fetch๊ฐ ๋ถ๊ฐ๋ฅํ ํ๊ฒฝ: ORM๊ณผ ์๋ฒ SDK ์ฐํ๋ฒ ๐ด - ๐ API ์ ๋ณด ๋๋ฆด๋ง ๋ฐฉ์ง โ ์์ฒญ ์ปจํ ์คํธ ์ง์ ์ ๊ทผ ํจํด ๐ก
- ๐ฅ ์๋ฌ ํด๊ฒฐ ์นดํ๋ก๊ทธ
- ๐ ์ด๋ฒ์ ๋ฐฐ์ด ๋ด์ฉ ์ด์ ๋ฆฌ
- ๐ ๋ง๋ฌด๋ฆฌ ํด์ฆ
- ๐ ๋ ์์๋ณด๊ธฐ
๐ ์ด ๋ฌธ์๋ฅผ ์ฝ๊ธฐ ์ ์
โฑ๏ธ ์์ ์ฝ๊ธฐ ์๊ฐ: 15๋ถ(์ ์ฒด) / ํต์ฌ ํํธ๋ง: 8๋ถ
๐บ๏ธ ์ด ๋ฌธ์์ ํ๋ฆ
๊ฐ๋ก์ฑ fetch ์ ์ ์ฒด โ ์บ์ฑ 1๋จ๊ณ(Memoization) ๋ฉ์ปค๋์ฆ โ DB ์ฟผ๋ฆฌ ์ค๋ณต ๋ฐฉ์ง ํจํด
๐ฏ ์ด ๋ฌธ์๋ฅผ ๋ค ์ฝ์ผ๋ฉด ํ ์ ์๋ ๊ฒ
- Props Drilling์ ํผํ๊ธฐ ์ํด ์ฌ๋ฌ ์ปดํฌ๋ํธ์์ ๊ฐ์ API๋ฅผ ์ค๋ณต ํธ์ถํ๋ ์ํคํ ์ฒ๋ฅผ ๋น๋นํ๊ฒ ์งค ์ ์๋ค.
- Next.js๊ฐ ๋ฌดํจํ ๋ถ๊ฐ๋ฅํ๊ฒ ๊ฐ์ ๋ก ๊ฑธ์ด๋ฒ๋ฆฌ๋ 1๋จ๊ณ ๋จ๋ฐ์ฑ ์บ์(Memoization) ์ ์๋ช ์ ์ ํํ ์ ์ ์๋ค.
- Prisma, Drizzle ๋ฑ
fetch๋ฅผ ์ ์ฐ๋ ORM ํ๊ฒฝ์์๋ ์ค๋ณต ์ฟผ๋ฆฌ๋ฅผ ๋ฐฉ์ดํ๋cache()๋ํ์ ์ ์ฉํ ์ ์๋ค.
๐บ๏ธ ์ด ๋ฌธ์์ ๋ฐฐ๊ฒฝ ์ธ๊ณ๊ด: '์์๋ค ์ปค๋ฎค๋ํฐ'
- ์์ฒ (์๋ก ์จ ์ฃผ๋์ด): "๋ฆฌ๋ ๋... ๋ ์ด์์์์๋ ์ ์ ์ ๋ณด๊ฐ ํ์ํ๊ณ ๋ณธ๋ฌธ์์๋ ํ์ํด์
fetch๋ฅผ ๋ ๋ฒ ์ผ๊ฑฐ๋ ์. ๊ทธ๋ฐ๋ฐ ๋ก๊ทธ๋ฅผ ๋ณด๋ DB ์กฐํ๊ฐ ๋ ๋ฒ์ฉ ๋ฐ์ํด์ ์๋ฒ ๋น์ฉ์ด ๋ ๋ฐฐ๋ก ๊นจ์ง๊ณ ์์ด์! ์ ์ญ ์ํ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ผ๋ ์จ์ผ ํ ๊น์? ๐ญ" - ์ํธ(FE ๋ฆฌ๋): "์์ฒ ๋... App Router ํ๊ฒฝ์์๋ Next.js๊ฐ ๋ชฐ๋
fetchํจ์๋ฅผ ๊ฐ๋ก์ฑ์ ๋๊ฐ์ ์์ฒญ์ ํ๋๋ก ๋ณํฉ(Memoization) ํด์ค์! ๋ฌด๊ฑฐ์ด ์ ์ญ ์ํ ๊ด๋ฆฌ์๋ ๋ ์ด์ ๋ฐ์ดํฐ ๊ณต์ ์ฉ์ผ๋ก ์ฐ์ง ์์๋ ๋๋ค๊ณ ์!"
๐ค ์ ์์์ผ ํ๋๊ฐ
์ค๋๋ ๋ฆฌ์กํธ ๋ฐฉ์์์๋ ์ปดํฌ๋ํธ A์ ์ ์ด์ด ๋ฐ์ ์๋ ์ปดํฌ๋ํธ Z๊ฐ ๋ ๋ค "ํ์ฌ ๋ก๊ทธ์ธํ ์ ์ ์ ๋ณด"๋ฅผ ๋ณด๊ณ ์ถ์ ๋ ์ฐธ ๊ดด๋ก์ ์ง.
- ์ต์๋จ์์ API๋ฅผ ํ ๋ฒ ํธ์ถํด Props Drilling์ผ๋ก ์๋ ์ปดํฌ๋ํธ๊น์ง ๋ด๋ ค์ฃผ๋ ๊ฐ,
- Redux, Recoil, React Query (Tanstack Query) ๊ฐ์ ์ ์ญ(Global) ํด๋ผ์ด์ธํธ ์บ์ฑ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ๋ถ์ด๋ ๊ฐ.
ํ์ง๋ง ์ด ๋ฐฉ์๋ค์ ๋ฐฉ๋ํ ์๋ฐ์คํฌ๋ฆฝํธ ๋ฒ๋ค์ ๋ธ๋ผ์ฐ์ ์ ๋ด๋ ค๋ณด๋ด๊ณ use client ์ต์๋จ ์ค์ผ์ ์ ํํ๋ ๊ฑฐ๋ํ ๋ณ๋ชฉ์ด์์ด.
Next.js 13+ App Router ์ธ์์์๋ ์๋ฒ ์ปดํฌ๋ํธ๊ฐ ๋์ธ๊ฐ ๋์์ด. ์๋ฒ ์ปดํฌ๋ํธ๋ผ๋ฆฌ ๋ฐ์ดํฐ๋ฅผ ๊ณต์ ํ๋ ค๋ฉด ์ด๋ป๊ฒ ํด์ผ ํ ๊น?
๋๋๊ฒ๋ ๋ฆฌ์กํธ์ ๋ฅ์คํธ ํ์ด ์ ์ํ ํด๋ต์ "๊ทธ๋ฅ ์ฌ๋ฌ ๊ตฐ๋ฐ์์ ๋๊ฐ์ API๋ฅผ ๊ฐ์ ํธ์ถํ์ธ์! ์ค๋ณต ์ ๊ฑฐ๋ ์ฐ๋ฆฌ๊ฐ ๋ชฐ๋ ์์์ ํด์ค๊ฒ์." ์์ด. ์ด ๋ง์ ๊ฐ์ "๋จ๋ฐ์ฑ ์บ์(Request Memoization)" ์๋ฆฌ๋ฅผ ๋ชจ๋ฅด๋ฉด ์๋ ๋ฐฉ์์ผ๋ก ๋์๊ฐ ๋ถํ์ํ๊ฒ ์๋ฒ ๋ถํ์ ํด๋ผ์ด์ธํธ ๋ฒ๋ค์ ๋ญ๋นํ๊ฒ ๋ผ.
๐๏ธ ๋น์ ๋ก ๋จผ์ ์ดํดํ๊ธฐ
๐ง 5์ด์๊ฒ ์ค๋ช ํ๋ค๋ฉด? (์์๋ค ๊ตญ๋ฐฅ์ง ๊ณ ๊ธฐ ๊ณ ๋ช )
์๋(์ฌ์ฉ์) ์ด ๋ค์ด์์ "์์ก ๊ตญ๋ฐฅ ํ๋์!" ๋ผ๊ณ ์ฃผ๋ฌธ์ ํ์ด.(๋จ์ผ ๋ ๋๋ง ์์ฒญ)
์ฃผ๋ฐฉ ๋ณด์กฐ A(๋ ์ด์์) ๊ฐ "์ด! ์์ก ๊ณ ๊ธฐ ํ ์ ์ ํ์ํด!" ๋ผ๊ณ ์๋ฆฌ์น๋ฉฐ ๋์ฅ๊ณ (
fetch) ๋ก ๋ฐ์ด๊ฐ์ด. ์ ์๋ฅผ ๊บผ๋ด์.
1์ด ๋ค์ ์ฃผ๋ฐฉ ๋ณด์กฐ B(๋ณธ๋ฌธ) ๋ ๋๊ฐ์ด "์ด! ๋๋ ์์ก ํ ์ ์ ํ์ํ๋ฐ!" ๋ผ๋ฉฐ ๋์ฅ๊ณ (fetch) ๋ก ๋ฐ์ด๊ฐ์ง.๊ทธ๋ฐ๋ฐ ์ฃผ๋ฐฉ์ฅ(Next.js) ์ด ์ฒ ๊ฐ๋ก๋ง์ผ๋ฉฐ ๋งํด.
"์ดํ! ๋ฐฉ๊ธ A๊ฐ ๊บผ๋ธ ๊ณ ๊ธฐ ์ ์, ๋ด๊ฐ ์ฌ๊ธฐ ๋๋ง ์์(์์ ๋ฉ๋ชจ๋ฆฌ) ๋๊ฐ์ด ์ฐ์ด๋จ์ด! ๋์ฅ๊ณ ๋ฌธ ๋ ์ด์ง ๋ง๊ณ ์ด๊ฑฐ ๊ทธ๋ฅ ๋๋ ์ ๋ด์!"๋จ, ์๋์ด ๊ตญ๋ฐฅ์ ๋ค ๋จน๊ณ ๋๊ฐ๋ ์๊ฐ(๋ ๋๋ง ์๋ฃ), ๋๋ง ์์ ๋จ์ ๊ณ ๊ธฐ ์ ๋ณด๋ ๊นจ๋์ด ์น์์ ธ์ ๋ฆฌ์ ๋ผ. ๋ค์ ์๋์ด ์ค๋ฉด ๋ ์๋ก ๊บผ๋ด์ผ ํ์ง. ์ด๊ฒ
Request Memoization์ด์ผ.
๐งฉ fetch API์ ๋ถํ: Next.js๊ฐ ํ์น ์๋ ํ์ค ๐ข
๊ธฐ์กด ํ๋ก ํธ์๋ ๊ฐ๋ฐ์๋ค์ axios๋ฅผ ์ ํธํ์ด.
ํ์ง๋ง Next.js App Router ์ํ๊ณ์์๋ ์์ ๋ธ๋ผ์ฐ์ API์๋ fetch๋ฅผ ๋ฌด์กฐ๊ฑด 1์์๋ก ์จ์ผ ํด.
์๋๊ณ ? Next.js ์ฝ์ด ํ์ด ๋ธ๋ผ์ฐ์ ์ ์ ์ ๋ฌผ์ด์๋ fetch ํจ์๋ฅผ Node.js ํ๊ฒฝ์์ ๋์๊ฐ ์ ์๋๋ก ๋ชฐ๋ ๋ฎ์ด์์์(Monkey Patching) ํ์ฅ ๊ฐ์กฐํด๋จ๊ฑฐ๋ .
์ด ๊ฐ์กฐ๋ fetch๋ ๋จ์ํ ํต์ ๋ชจ๋์ ๋์ด์ ์๋ฒ ์บ์ ์ ์ฅ์(next: { revalidate }, cache: 'force-cache')์ ์ฐ๊ฒฐ๋ ๊ฑฐ๋ํ ์บ์ ์ปจํธ๋กค๋ฌ์ผ. axios๋ ์์ ์๋ํํฐ์ด๋ฏ๋ก ์ด ๋ง๋ฒ์ ํํ์ 1๋ ๋ณผ ์๊ฐ ์์ด! (๋จ๋ฐ์ฑ ์ค๋ณต ์ ๊ฑฐ๋ ์ฌ์ ํ ์ค์ค๋ก ํด์ผ ํจ)
๐ก๏ธ Request Memoization โ ๋๊ฐ์ API ํธ์ถ์ ์ค๋ณต ๋ฐฉ์ด ๐ก
๐ก ์ด ๋ฌธ์์์ ๋ค๋ฃจ๋ ์บ์ฑ์ ๋จ ํ๋์ ์ฌ์ฉ์ ์ ์(1 Request) ์ฃผ๊ธฐ ๋์๋ง ์ด์๋จ๋ ๋จ๋ฐ์ฑ "๊ธฐ์ต(Memoization)" ์ด๋ค! ์๊ตฌ ๋ณด์กด๋๋ ๋ฐ์ดํฐ ์บ์ ์ฑํฐ๋ [Advanced]์์ ๋ฐ๋ก ๋ฅ๋ค์ด๋ธ ํ ์์ .
์ปดํฌ๋ํธ ๊ตฌ์กฐ
// ์์๋ค ์ปค๋ฎค๋ํฐ ํ์ด์ง ๊ตฌ์กฐ
app/
โโ layout.tsx (์ฌ์ด๋๋ฐ ๋ฉ๋ด - ์ ์ ์ ์ ํ๋กํ ํ์)
โโ user/[id]/page.tsx (๋ณธ๋ฌธ - ์ญ์๋ ์ ์ ์ ์ ํ๋กํ ํ์)โ ๊ณผ๊ฑฐ์ ๊ณต๋ฃก ๋ฐฉ์ (์ ์ญ ์คํ ์ด)
๋ ์ด์์์์ ๋ฐ์ดํฐ๋ฅผ ๋ฐ์์จ ๋ค Context API๋ Redux/Zustand Store์ ์ต์ง๋ก ๋ฐ์ด ๋ฃ๊ณ page.tsx์์ ๊บผ๋ด ์ฐ๊ฒ ํ๋ ๋ฐฉ์. ์ฝ๋๊ฐ ์ง์ ๋ถํด์ง๊ณ ํด๋ผ์ด์ธํธ ์ปดํฌ๋ํธํ๊ฐ ๊ฐ์ ๋๋ค.
โ RSC ์ต์ ํผ (๊ฐ์๋์ ๋ ๋๋ง)
// 1. app/layout.tsx
// ํค๋์ ๋ก๊ทธ์ธํ ์ฌ์ฉ์ "์์ฒ "์ ์ด๋ฆ์ ๋์์ผ ํ๋ค!
export default async function Layout({ children }) {
const res = await fetch('https://api.my.com/user/1');
const user = await res.json(); // { name: "์์ฒ ", job: "ํ๋ก ํธ์๋ ๊ฐ๋ฐ์" }
return <nav>ํ์ํฉ๋๋ค, {user.name} ๋</nav>{children}
}// 2. app/user/[id]/page.tsx
// ๋ณธ๋ฌธ์์๋ ํ๋กํ ์์ ํผ์ ์ํด ๋ ๋ด ์ ๋ณด ์ ์ฒด๊ฐ ํ์ํ๋ค!
export default async function Page() {
const res = await fetch('https://api.my.com/user/1'); // URL๊ณผ ๋ฉ์๋๊ฐ 100% ๋๊ฐ๋ค!
const user = await res.json();
return <div>๋ด ์ง์
: {user.job} </div>
}[๋ด๋ถ์์ ์ผ์ด๋๋ ์ผ]
- ์ฌ์ฉ์๊ฐ ํ์ด์ง ์ ์. (๋ ๋๋ง ํธ๋ฆฌ ํ์ ์์)
layout.tsx์คํ ์คfetch('/user/1')์กฐ์ฐ. Next.js๊ฐ "๋ด ๋จ๋ฐ์ฑ ๋ฉ๋ชจ๋ฆฌ์ ์ด ์์ฒญ๊ฒฐ๊ณผ ์๋? ์๋ค. ์ง์ง DB ๊ฐ์ ๋ฐ์ดํฐ ๊ฐ์ ธ์จ๋ค(MISS)."- ๊ฒฐ๊ณผ๋ฅผ ๊ฐ์ ธ์จ ๋ค, ์๋ต ๊ฐ์ฒด๋ฅผ ์ปดํฌ๋ํธ์ ๋๊น๊ณผ ๋์์ Next.js ๋ด๋ถ ๋ฉ๋ชจ๋ฆฌ(์บ์ ํธ๋ฆฌ)์ ์ด์ง ์ ์ฅํด๋ .
- ํธ๋ฆฌ ์๋์ชฝ
page.tsx์คํ. ๋fetch('/user/1')์กฐ์ฐ. - Next.js๊ฐ ์์ผ๋ฉด์ ๊ฐ๋ก์ฑ. "์ํ! ์ด URL, ์๊น 1๋ฐ๋ฆฌ์ด ์ ์ ์์์ ๋๊ฐ์ด ๋ถ๋ ๋ ๊ฑฐ์์! ์ง์ง ์๋ฒ๋ก ๋คํธ์ํฌ ์ ์กํ์ง ๋ง๊ณ ์๊น ์ ์ฅํด๋ ๊ฑฐ ๋ฐ๋ก ๋๋ ค์ค๊ฒ (HIT)."
- ๋ ๋๋ง ์ข ๋ฃ ํ HTML ์์ฑ. ์ด ๋ฉ๋ชจ์ด์ ์ด์ ์์ฒฉ์ ์ฆ์ ํ๊ธฐ ๋จ. (์๋ฒ ๋ฆฌ์์ค ๋ฐํ)
์ด๋? ์ฐ๋ฆฌ๋ ์๋ฌด๋ฐ ์ ์ญ ์ํ ๋ผ์ด๋ธ๋ฌ๋ฆฌ(Zustand ๋ฑ)๋ฅผ ๊น์ง ์๊ณ ๋, ๊ทธ์ ๋๊ฐ์ ์ฃผ์์ fetch๋ฅผ ์ฌ๋ฌ ์ค ๋ณต๋ถํ์ ๋ฟ์ธ๋ฐ ์๋ฒ ๋ถํ๋ ๋ฌด์กฐ๊ฑด 1ํ๋ก ํ์ ๋๋ ์ถ๋ณต์ ๋๋ฆฌ๊ฒ ๋ ๊ฑฐ์ผ. Props Drilling์์ ์์ํ ํด๋ฐฉ๋ ๊ฑฐ์ง!
๐ฑ fetch๊ฐ ๋ถ๊ฐ๋ฅํ ํ๊ฒฝ: ORM๊ณผ ์๋ฒ SDK ์ฐํ๋ฒ ๐ด
์์ฒ (์ ์
)์ด๊ฐ ํ๋ ๋ฒ๋ก ๋ฐ์ด์. "๋ฆฌ๋๋!! ํฐ์ผ ๋ฌ์ด์! ์ ํฌ๋ ํ๋ก ํธ ๋จ๋
fetch๋ก API ํธ์ถ ์ ํ๊ณ , ๋ฐ์ดํฐ๋ฒ ์ด์ค๋ฅผ ์ง์ ์ฐ๋ฅด๋ Prisma๋ฅผ ์ฐ๊ณ ์์ด์ db.user.findUnique() ํจ์๋ฅผ ์ฐ์์์! ์๋ fetch๊ฐ ์๋๋ผ์ ๋ ์ด์์์ด๋ ๋ณธ๋ฌธ์ ๋ ๋ฒ ์ฐ๋ฉด DB ์ฟผ๋ฆฌ๊ฐ ์ง์ง๋ก ๋ ๋ฒ ๋๊ฐ์ ๋ฐ์ดํฐ๋ฒ ์ด์ค ๋ถํ๊ฐ ๋ ๋ฐฐ๋ก ๋์ด๋์!"
์ํธ(๋ฆฌ๋)๊ฐ ๋๋ตํด. "๋ค, Next.js ์ฝ์ด ํ์ด ๊ฐ๋ก์ฑ์ ๋ง๊ฐ์กฐํ ๊ฑด ์ค์ง fetch ๋ฟ์ด์์. ์ธ๋ถ ๋ผ์ด๋ธ๋ฌ๋ฆฌ์ธ Prisma, Drizzle, Firebase SDK, Stripe ๋ฑ์ ๋ฐ๋ณด์ฒ๋ผ ๋งค๋ฒ ์ ์งํ๊ฒ ์ฟผ๋ฆฌ๋ฅผ ๋ ๋ฆฌ์ฃ . ์ด๋ด ๋ ์ฐ๋ฆฌ๊ฐ ์๋์ผ๋ก React.cache() ๋ผ๋ ๋ฐฉ์ด๋ง์ ์ง์ ์น๋ ๊ฒ๋๋ค."
โ
์๋ฒฝํ ์๋ ๋ํ (React cache API ๋์
)
fetch๋ฅผ ๋ชป ์ด๋ค๋ฉด, ๋ด ์ผ๋ฐ ํจ์๋ผ๋ ์ 1ํ์ฉ ๋จ๋ฐ์ฑ ๋ฉ๋ชจ๋ฆฌ(Memoization)์ ์ถ๋ณต์ ๋ฐ๊ฒ๋ ๋ชจ์(cache)๋ฅผ ์์์ฃผ๋ฉด ๋ผ.
// app/lib/getUser.ts (๐ฅ ์ค์ ์ ํธ ํจ์ ํ์ผ)
import { cache } from 'react' // Next.js๊ฐ ์๋๋ผ React ์ฝ์ด์์ ์ง์ํ๋ API!
import db from './prismaClient'
// ์์ ํจ์์ธ getUserById๋ฅผ cache() ๋ฐฉ์ด๋ง์ผ๋ก ๊ฐ์ผ๋ค!
export const getUserById = cache(async (id: string) => {
console.log('์ด ๋ก๊ทธ๊ฐ ๋ฑ ํ ๋ฒ๋ง ์ฐํ์ผ ์ ์์
๋๋ค!'); // ์ฆ๋ช
์ฉ ๋ก๊ทธ
const user = await db.user.findUnique({ where: { id } });
return user;
})์ด์ layout.tsx์ page.tsx์์ ์ด getUserById('123') ํจ์๋ฅผ 10๋ฒ, 100๋ฒ ๋ณต๋ถํด์ ๋ง๊ตฌ์ก์ด๋ก ํธ์ถํด๋, ๋์ผํ ์ธ์(id='123')์ ๋ํ ๋ฆฌ์กํธ ์ฌ์ดํด ๋ด์์์ DB ์ฟผ๋ฆฌ๋ ์ฝ์ ๋ณด์ฅ ๋จ 1๋ฒ๋ง ๋ฐ๋ํด.
๐ก ์ฌํ ์ฃผ์: ์ด
React.cache()๋ Next.js์ 4์ค ์บ์ ์ค ์ต๊ณ ๊ธ์ธ Data Cache(์๊ตฌ ๋ณด๊ด ์บ์)๊น์ง๋ ๊ฑด๋๋ฆฌ์ง ๋ชปํด! ์ค์ง '์ด๋ฒ ํ ๋ฒ์ ํ์ด์ง ๋ฐฉ๋ฌธ(1 Request Lifecycle)'์ ํํด์๋ง ์ค๋ณต์ ๋ง๋ ๋๋ํ ์ง์ฐ๊ฐ๊ฐ ๋ฌ๋ฆฐ ์์ฒฉ์ผ ๋ฟ์ด์ผ.
๐ API ์ ๋ณด ๋๋ฆด๋ง ๋ฐฉ์ง โ ์์ฒญ ์ปจํ ์คํธ ์ง์ ์ ๊ทผ ํจํด ๐ก
๐ฏ ์ด ์น์ ์ ์ฝ๊ณ ๋๋ฉด:
cookies()/headers()๋ฅผ ์ด๋ ๊น์ด์ ์๋ฒ ์ปดํฌ๋ํธ์์๋ ์ง์ ํธ์ถํ ์ ์์์ ์๋ค- ์ธ์ฆ ํ ํฐ์ Props๋ก ๋ด๋ ค๋ฐ์ง ์์๋ ๋๋ ์ด์ ๋ฅผ ์ค๋ช ํ ์ ์๋ค
React.cache()๊ฐ '์ด๋ฒ ์์ฒญ(1 Request Lifecycle)' ๋ฒ์์์๋ง ์ ํจํจ์ ์๊ณ ์ฌ๋ฐ๋ฅด๊ฒ ์ฌ์ฉํ ์ ์๋ค
๋ฌธ์ : "์ธ์ฆ ์ ๋ณด๋ ๊ฒฐ๊ตญ Prop Drilling์ด ์๊ธฐ์์์!"
์์ฒ ์ด๊ฐ ๋ ๋ฐ์ด์. "๋ฆฌ๋๋! fetch('/api/posts')๋ฅผ ์ฌ๋ฌ ์ปดํฌ๋ํธ์์ ๊ฐ์ ํธ์ถํ๋ ๊ฑด ์ด์ ์๊ฒ ์ด์. ๊ทธ๋ฐ๋ฐ... ๊ทธ API๊ฐ ์ธ์ฆ์ด ํ์ํด์ Authorization: Bearer <ํ ํฐ> ํค๋๋ฅผ ๋ถ์ฌ์ผ ํ๋ค๋ฉด์? ํ ํฐ์ ์ฟ ํค์์ ๊บผ๋ด์ผ ํ๋๋ฐ, ์ต์๋จ ๋ ์ด์์์์ ๊บผ๋ธ ํ ํฐ์ ํ์ด์ง๋ก Props๋ก ๋ด๋ ค์ฃผ๊ณ , ๋ ๊ทธ ์์ ์ปดํฌ๋ํธ๋ก Prop Drillingํด์ผ ํ๋ ๊ฒ ์๋๊ฐ์? ์, ๊ทธ๋ฆฌ๊ณ URL ํ๋ผ๋ฏธํฐ๋ ์ฟผ๋ฆฌ์คํธ๋ง๋ ๋ง์ฐฌ๊ฐ์ง ์๋๊ฐ์? /posts/[id] ์ id ๋ฅผ ์์ ์ปดํฌ๋ํธ๊น์ง ๋ด๋ ค์ค์ผ ํ์์์?"
์ํธ๊ฐ ์์ผ๋ฉฐ ๋๋ตํด. "์ฟ ํค/ํค๋๋ , params๋ , ์๋ฒ ์ปดํฌ๋ํธ ํ๊ฒฝ์์๋ ๋ชจ๋ ์ด๋ ๊น์ด์์๋ ์ง์ ์ ๊ทผ์ด ๊ฐ๋ฅํด์. next/headers์์ ์ ๊ณตํ๋ cookies()์ headers() ํจ์, ๊ทธ๋ฆฌ๊ณ params์ searchParams ๋ ๋ ๋ ํธ๋ฆฌ ์ด๋ ๊น์ด์์๋ ์ง์ ๋ฐ์ ์ธ ์ ์๊ฑฐ๋ ์. ์๋ฒ์ด๊ธฐ ๋๋ฌธ์ ๊ฐ๋ฅํ ํน๊ถ์ด์์!"
โ ๊ณผ๊ฑฐ ๋ฐฉ์ (์ฟ ํค Prop Drilling)
// app/layout.tsx โ ์ฟ ํค ๊บผ๋ด์ ์๋๋ก ๋ด๋ ค์ค
export default async function Layout({ children }) {
const cookieStore = await cookies()
const token = cookieStore.get('session-token')?.value
return <Main token={token}>{children}</Main> // token์ props๋ก ๋๊ฒจ์ผ ํจ
}
// app/page.tsx โ ๋ฐ์์ ๋ ๋ด๋ ค์ค
export default function Page({ token }) {
return <PostList token={token} /> // ๊ณ์ ๋ด๋ ค๊ฐ์ผ ํจ
}โ RSC ๋ฐฉ์ (์ด๋์๋ ์ง์ ์ ๊ทผ)
// app/posts/PostList.tsx โ ์ด๋ ๊น์ด์ ์์ด๋ ์ง์ ์ฟ ํค ์ ๊ทผ!
import { cookies } from 'next/headers'
export default async function PostList() {
const cookieStore = await cookies()
const token = cookieStore.get('session-token')?.value
const posts = await fetch('https://api.my.com/posts', {
headers: { Authorization: `Bearer ${token}` }, // Props ์์ด ์ง์ ์ฌ์ฉ
})
const data = await posts.json()
return <ul>{data.map(p => <li key={p.id}>{p.title}</li>)}</ul>
}Props ๋๋ฆด๋ง์ด ์์ ํ ์ฌ๋ผ์ก์ด. cookies()์ headers() ๋ HTTP ์์ฒญ ๊ฐ์ฒด์ ์ง์ ์ ๊ทผํ๋ ์๋ฒ ์ ์ฉ ํจ์์ผ. Node.js ์๋ฒ ์์์ ์คํ๋๊ธฐ ๋๋ฌธ์ ์ด๋ ๊น์ด์ ์๋ฒ ์ปดํฌ๋ํธ์์ ํธ์ถํด๋ ๋์ผํ ํ์ฌ ์์ฒญ ์ปจํ
์คํธ๋ฅผ ์ฐธ์กฐํด.
params์ searchParams๋ ์ง์ ๋ฐ์ผ๋ฉด ๋ผ
์ฟ ํค๋ง์ด ์๋์ผ. URL ๋์ ํ๋ผ๋ฏธํฐ(params)์ ์ฟผ๋ฆฌ์คํธ๋ง(searchParams)๋ ์๋ฒ ์ปดํฌ๋ํธ์์ ์ง์ props๋ก ๋ฐ๊ฑฐ๋, Next.js 15 ์ด์์์๋ await ํํ๋ก ๋ฐ์ ์ ์์ด.
// app/posts/[id]/CommentSection.tsx
// โ
page.tsx๋ฅผ ๊ฑฐ์น์ง ์๊ณ ์ง์ params๋ฅผ ๋ฐ์์ ์ธ ์ ์์ด
export default async function CommentSection({
params,
}: {
params: Promise<{ id: string }> // Next.js 15 ๊ธฐ์ค Promise ํ์
}) {
const { id } = await params // URL: /posts/123 โ id = "123"
const comments = await fetch(`https://api.my.com/posts/${id}/comments`)
return <ul>...</ul>
}// app/search/ResultList.tsx
// โ
๊ฒ์ ์ฟผ๋ฆฌ(?q=react&page=2)๋ ์ง์ ๋ฐ์์ ์ธ ์ ์์ด
export default async function ResultList({
searchParams,
}: {
searchParams: Promise<{ q?: string; page?: string }>
}) {
const { q, page } = await searchParams // ์ฟผ๋ฆฌ์คํธ๋ง ์ง์ ์ ๊ทผ
const results = await fetch(`https://api.my.com/search?q=${q}&page=${page}`)
return <ul>...</ul>
}๐ก Next.js 15 ์ฃผ์์ฌํญ:
params์searchParams๊ฐ ๋น๋๊ธฐ(Promise) ํ์ ์ผ๋ก ๋ฐ๋์์ด.await๋ฅผ ๋ฐ๋์ ๋ถ์ฌ์ผ ํด. Next.js 14 ์ดํ์์๋params.id์ฒ๋ผ ๋ฐ๋ก ์ ๊ทผ ๊ฐ๋ฅํด.
React.cache()๋ก ์ธ์
์ ๋ณด 1ํ๋ง ์กฐํํ๊ธฐ
์ฌ๋ฌ ์ปดํฌ๋ํธ์์ ์ฟ ํค๋ฅผ ๊ฐ์ ๊บผ๋ด API ์์ฒญ์ ํ๋ค๋ฉด, ๋งค๋ฒ ์ธ์ฆ API๊ฐ ๋ฐ๋ก ํธ์ถ๋ ์ ์์ด. React.cache()๋ก ๊ฐ์ธ์ ์์ฒญ๋น 1ํ๋ง ์คํ๋๋๋ก ๋ณด์ฅํด๋ผ.
// app/lib/auth.ts
import { cache } from 'react'
import { cookies } from 'next/headers'
// ์ด ํจ์๋ ํ ์์ฒญ ์ฌ์ดํด์์ ๋ช ๋ฒ์ ํธ์ถํด๋ ์ค์ fetch๋ ๋ฑ 1๋ฒ๋ง ์คํ๋ผ
export const getSession = cache(async () => {
const cookieStore = await cookies()
const token = cookieStore.get('session-token')?.value
if (!token) return null
const res = await fetch('https://api.my.com/me', {
headers: { Authorization: `Bearer ${token}` },
})
return res.json() // { userId: 'abc123', name: '์์ฒ ', role: 'user' }
})์ด์ layout.tsx, page.tsx, ์ด๋ค ์๋ฒ ์ปดํฌ๋ํธ์์๋ getSession()์ ํธ์ถํด๋ ์ค์ HTTP ์์ฒญ์ ๋จ 1๋ฒ๋ง ๋๊ฐ.
// ๋ ์ด์์์์๋ ํธ์ถ
export default async function Layout({ children }) {
const session = await getSession() // 1๋ฒ์งธ ํธ์ถ โ ์ค์ fetch ๋ฐ๋ (MISS)
return <nav>์๋
ํ์ธ์, {session?.name} ๋</nav>
}
// ๊น์ ์์ ์ปดํฌ๋ํธ์์๋ ํธ์ถ
export default async function UserCard() {
const session = await getSession() // 2๋ฒ์งธ ํธ์ถ โ ์บ์์์ ์ฆ์ ๋ฐํ (HIT)
return <div>์ญํ : {session?.role}</div>
}โฑ๏ธ React.cache ์ ํจ ๋ฒ์ โ "์ด๋ฒ ์์ฒญ"์ด ์ ๋ถ๋ค
โ ๏ธ ์ค์:
React.cache()์ ๋ฉ๋ชจ์ด์ ์ด์ ์ ๋จ์ผ ๋ ๋ ์ฌ์ดํด(1 HTTP Request) ๋ฒ์์์๋ง ์ ํจํด. ๋ธ๋ผ์ฐ์ ๊ฐ ์๋ก๊ณ ์นจํ๊ฑฐ๋ ๋ค๋ฅธ ์ฌ์ฉ์๊ฐ ์ ์ํ๋ฉด ์บ์๋ ์์ ํ ๋น์์ง๊ณ ์๋ก ์์ํด.
์ฌ์ฉ์ A์ ์์ฒญ ์์ โ getSession() 1๋ฒ์งธ ํธ์ถ (MISS) โ ์ค์ API fetch ์คํ
โ getSession() 2๋ฒ์งธ ํธ์ถ (HIT) โ ๋ฉ๋ชจ๋ฆฌ์์ ์ฆ์ ๋ฐํ
์ฌ์ฉ์ A์ ์๋ต ์๋ฃ โ โ ์บ์ ํ๊ธฐ๋จ
์ฌ์ฉ์ B์ ์์ฒญ ์์ โ getSession() 1๋ฒ์งธ ํธ์ถ (MISS) โ ์ฒ์๋ถํฐ ๋ค์ fetch์ด ๋๋ถ์ ์ฌ์ฉ์ ๊ฐ ๋ฐ์ดํฐ ์ค์ผ(Cross-user data leak) ์ด ๋ฐ์ํ์ง ์์. ์ฌ์ฉ์ A์ ์ธ์ ์ด ์ฌ์ฉ์ B์๊ฒ ํ๋ฌ๊ฐ๋ ์ผ์ ์ ๋ ์์ด.
๐ก ํ ์ค๋ก ๊ธฐ์ตํ๊ธฐ
RSC์์๋ ์ธ์ฆ ํ ํฐ์ props๋ก ๋ค๊ณ ๋ฐ์ด๋ค๋ ํ์ ์์ด.next/headers์์ ์ง์ ๊บผ๋ด๊ณ ,React.cache()๋ก ๊ฐ์ธ๋ฉด ํ ์์ฒญ ์์์ ์ค๋ณต ํธ์ถ๋ ๋ง์ ์ ์์ด. ๋จ, ์ด ์บ์๋ ํ์ฌ ์ฌ์ฉ์์ ์์ฒญ์ด ๋๋๋ ์๊ฐ ์๋ ํ๊ธฐ๋ผ.
โก๏ธ ๋ค์ ์น์ ์์๋: ์์ฃผ ๋ง๋๋ ์๋ฌ ๋ฉ์์ง๋ค์ ์ฆ์๋ณ๋ก ๋ฌถ์ ์นดํ๋ก๊ทธ๋ฅผ ํ์ธํด๋ด.
๐ฅ ์๋ฌ ํด๊ฒฐ ์นดํ๋ก๊ทธ
์๋ฌ ๋ฉ์์ง๊ฐ ๋จ๋ฉด Ctrl+F๋ก ๊ฒ์ํด๋ด.
โ You are using an unsupported version of Next.js for React.cache
์์ธ: ๋ฌด์ฌ์ฝ ๋ค๋ฅธ ํ์ผ์์ cache ํจ์๋ฅผ ์๋ชป ์ผ๊ฑฐ๋ ํ๊ฒฝ ์ด์.
ํด๊ฒฐ์ฑ
: cache๋ ์ค๋ก์ง ์๋ฒ ์ปดํฌ๋ํธ(Server Components) ์ํ๊ณ์ ์๋ฒ ๋จ์์๋ง ๋์ํ๋ ๋ฉ๋ชจ์ด์ ์ด์
์ด๋ค! ํด๋ผ์ด์ธํธ ์ปดํฌ๋ํธ ์ปดํ์ผ๋ฌ ์ธก์์ ์ด ํจ์๋ฅผ importํด์ ์ฐ๋ ค ํ๋ฉด ํฐ์ง๋ค. (ํด๋ผ์ด์ธํธ๋ ๊ธฐ์กด ๋ฐฉ์์ useMemo๋ useQuery ๋ฑ์ ์จ์ผ ํจ)
โ (fetch) ์ค๋ณต ์ ๊ฑฐ๊ฐ ์ ๋๊ณ API๊ฐ 2๋ฒ ์ฐ๋ ค์!
์์ธ: Request Memoization์ ์๋ ์กฐ๊ฑด์ ์ฒ ์ ํ๊ฒ "๋ฉ์๋(GET ๋ฑ)์ URL(๊ฒฝ๋ก+์ฟผ๋ฆฌ์คํธ๋ง)๊ณผ ์ต์
(headers ๋ฑ)์ด 100% ์๋ฒฝํ ์ผ์นํ ๋" ๋ง ๋ฐ๋ํจ. ๋ง์ฝ ํ์ชฝ์์ Authorization ํค๋๋ฅผ ๋ฃ๊ณ ํ์ชฝ์์ ๋นผ๋จน์๋ค๋ฉด ๋ธ๋ผ์ฐ์ (Next.js)๋ ๋ค๋ฅธ ์์ฒญ์ผ๋ก ๊ฐ์ฃผํ๊ณ ๋ ๋ฒ ์๋ค.
ํด๊ฒฐ์ฑ
: ์์ getUserProfileFetch() ๊ฐ์ด fetch ์ต์
๋ฌด์ฅ์ ๋๋ง์น ์ ํธ ํจ์ ํ๋๋ฅผ ๋ง๋ค์ด์ ๊ณณ๊ณณ์์ ๋๊ฐ์ด ๋ถ๋ฌ๋ค ์ฐ๋ ํจํด์ ์ฌ์ฉํด๋ผ.
๐ ์ด๋ฒ์ ๋ฐฐ์ด ๋ด์ฉ ์ด์ ๋ฆฌ
| ์ํฉ (์๋ฒ ์ปดํฌ๋ํธ ํ๊ฒฝ) | ๊ณต๋ฃก ๋ ๊ฑฐ์ (๊ณผ๊ฑฐ) โ | ํธ๋ ๋ํ ๋ฐฉ์ (ํ์ฌ) โ |
|---|---|---|
| ์ฌ๋ฌ ์ปดํฌ๋ํธ์์ ๊ฐ์ ๋ฐ์ดํฐ ํ์ํ ๋ | ์ต์๋จ์์ ํ ๋ฒ ํธ์ถํด Props Drilling์ผ๋ก ๋ด๋ ค๊ฐ๋ฉฐ ๊ณต์ | ๊ฐ ์ปดํฌ๋ํธ๊ฐ ๊ฐ์ API๋ฅผ ํธ์ถํจ. (Next.js๊ฐ ์์์ 1๊ฐ๋ก ํฉ์ณ์ค) |
| ํด๋ผ์ด์ธํธ ๋จ ์ ์ญ ์ํ (Zustand) | ์๋ฒ ๋ฐ์ดํฐ(์ ์ , ๊ฒ์๋ฌผ ๋ฑ) ๋ณต์ฌํด ๋ด๋ ๊ฑฐ๋ํ ์ฐ๋ ๊ธฐํต ๋ณด์ผ๋ฌํ๋ ์ดํธ | ์น ๊ฑท์ด๋ด๊ณ , ๋ณต์กํ UI ๋ชจ๋ฌ ํ ๊ธ ๊ฐ์ "์์ ๋ธ๋ผ์ฐ์ ์ํ(State)" ์ ์ฉ์ผ๋ก๋ง ๋จ๊น |
| ORM์ด๋ DB ํด๋ผ์ด์ธํธ ์ฟผ๋ฆฌ ์ค๋ณต ๋ฐฉ์ด | ๋ถ๊ฐ๋ฅ! ๋ฌด์กฐ๊ฑด ๋ถ๋ชจ์์ ์ฟผ๋ฆฌํด์ ์์์๊ฒ ๋ฟ๋ฆผ | ํจ์ ์ ์ธ์ React.cache(async () => DB์กฐํ) ๋ก ๊ฐ์ธ์ ์๋์ผ๋ก Request Memoization ํํ ์ ์ฉ |
๐ก ํ ์ค๋ก ๊ธฐ์ตํ๊ธฐ
์๋ฒ ์ปดํฌ๋ํธ ์ธ์์์๋ ๋์น ๋ณด์ง ๋ง๊ณ ํ์ํ ๊ณณ์์ ํ์ํ ๋งํผ ๋ฐ์ดํฐ๋ฅผ ์ง์ ์์ฒญํด(Fetch)๋ผ. ๋๋ํ ์๋ฌธ์ฅ(Next.js Memoization)์ด ๋ค ์์์ ์ค๋ณต ์๊ธ์ ๋ฉด์ ํด ์ค ๊ฑฐ๋๊น. ๋จ, ์๋์ด ์ง์ ๊ฐ๋ ์๊ฐ ๋ฉค๋ฒ์ญ ๋ฌดํจํ!
๐ฃ ์์ฒ ์ด์ ํด๊ทผ ์ผ๊ธฐ
์ค๋์ ์ ๋ง ํฐ ์ง์ ํ๋ ๋ด๋ ค๋์ ๊ธฐ๋ถ์ด์ผ. ์์ ์ ์ปดํฌ๋ํธ๋ง๋ค ๊ฐ์ ๋ฐ์ดํฐ๋ฅผ ๋ถ๋ฅด๋ฉด ์๋ฒ๊ฐ ํฐ์ง๊น ๋ด ๋ฌด์์์ Props Drilling ํ๋๋ผ ๋ ๋ป๋ป ํ๋ ธ๊ฑฐ๋ . ๊ทธ๋ฐ๋ฐ ์ํธ ๋ฆฌ๋ ๋์ด ๋ง์ํ์ '๋๋ง ์์ ๊ณ ๊ธฐ(Memoization)' ๋๋ถ์ ์ด์ ๋ ๋น๋นํ๊ฒ ๊ฐ์ ํ์ํ ๊ณณ์์ fetch ๋ฅผ ๋ ๋ฆด ์ ์๊ฒ ๋์ด!
๐ก ์ค๋์ ๊ตํ: "์๋ฒ ์ปดํฌ๋ํธ๋ผ๋ฉด Props๋ก ๋ฐ์ดํฐ๋ฅผ ์ฎ๊ธฐ์ง ๋ง๊ณ , ํ์ํ ๊ณณ์์ ์ง์ fetchํ์! ๋๋จธ์ง๋ ๋ฆฌ์กํธ ์ฃผ๋ฐฉ์ฅ์ด ์์์ ๋ณํฉํด์ค๋ค."
React.cache() ๋ผ๋ ๋ ๋ ํ ๋ฐฉ์ด๋ง๋ ๋ฐฐ์ ์ผ๋, ์ด์ ์ฐ๋ฆฌ ์๋น์ค์ DB ๋ถํ๋ ๊ฑฑ์ ์์ด. ์ค๋์ ๋จธ๋ฆฌ๋ฅผ ๋ง์ด ์ผ๋๋ ์ข ๋ฉํ๋ค. ์ง์ ๊ฐ์ ์์ํ๊ฒ ์ค์ํ๊ณ ํน ์์ผ์ง. ๋ด์ผ์ ์ค๋๋ณด๋ค ๋ ๋น ๋ฅด๊ณ ํจ์จ์ ์ธ ๋ฐ์ดํฐ ํ์นญ ์ฝ๋๋ฅผ ์ง๋ณผ ๊ฑฐ์ผ! ๐ฃ