이번 페이지에서는 서버 및 클라이언트 컴포넌트(Server and Client Components)에서 데이터를 어떻게 가져오는지, 그리고 데이터에 의존하는 컴포넌트들을 어떻게 스트리밍(stream)하는지에 대해 차근차근 살펴볼 거예요.
서버 컴포넌트에서는 다음과 같은 비동기(asynchronous) I/O를 사용해서 데이터를 아주 쉽게 가져올 수 있어요.
fetch APIfs 같은 Node.js API를 사용해 파일 시스템 읽기💡 강사의 팁! > 프론트엔드 개발자로 취업이나 이직을 준비하시면서 기존 React(SPA) 구조에서 데이터를 가져올 때 발생했던 여러 문제들(CORS 에러나 클라이언트 네트워크 지연 등)을 겪어보셨을 거예요. Next.js의 서버 컴포넌트에서 데이터를 가져오면 데이터베이스나 API 서버와 물리적으로 가까운 곳(서버)에서 통신하기 때문에 훨씬 빠르고 안전해요. 면접에서도 "왜 서버 컴포넌트에서 데이터를 페칭하는 게 좋나요?"라는 질문이 단골로 나오니 꼭 기억해두세요!
fetch API 사용하기fetch API로 데이터를 가져오려면 컴포넌트를 비동기(async) 함수로 만들고 fetch 호출 앞에 await를 붙여주시면 됩니다. 예를 들면 아래처럼요!
export default async function Page() {
const data = await fetch('https://api.vercel.app/blog')
const posts = await data.json()
return (
<ul>
{posts.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
)
}
export default async function Page() {
const data = await fetch('https://api.vercel.app/blog')
const posts = await data.json()
return (
<ul>
{posts.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
)
}
알아두면 좋은 점:
- 기본적으로
fetch응답은 캐싱되지 않아요. 하지만 Next.js는 라우트를 사전 렌더링(pre-render)하고, 성능 향상을 위해 그 결과물을 캐싱한답니다. 만약 동적 렌더링(dynamic rendering)을 적용하고 싶다면{ cache: 'no-store' }옵션을 사용해보세요. 자세한 내용은fetchAPI 레퍼런스를 참고해주세요.- 개발 중에는 가시성을 높이고 디버깅을 쉽게 하기 위해
fetch호출을 로그로 남길 수 있어요.loggingAPI 레퍼런스를 확인해보세요.
서버 컴포넌트는 오직 서버에서만 렌더링되기 때문에, ORM이나 데이터베이스 클라이언트를 사용해 데이터베이스 쿼리를 아주 안전하게 실행할 수 있답니다. 컴포넌트를 비동기 함수로 만들고 호출을 await 하시면 돼요.
import { db, posts } from '@/lib/db'
export default async function Page() {
const allPosts = await db.select().from(posts)
return (
<ul>
{allPosts.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
)
}
import { db, posts } from '@/lib/db'
export default async function Page() {
const allPosts = await db.select().from(posts)
return (
<ul>
{allPosts.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
)
}
클라이언트 컴포넌트에서 데이터를 가져오는 방법은 크게 두 가지가 있어요.
use APIuse API로 데이터 스트리밍하기React의 use API를 사용하면 서버에서 클라이언트로 데이터를 스트리밍(stream)할 수 있습니다. 먼저 서버 컴포넌트에서 데이터를 가져오는 함수를 호출하되 기다리지(await) 말고, 그 프로미스(Promise) 자체를 클라이언트 컴포넌트에 prop으로 쓱 넘겨주세요.
import Posts from '@/app/ui/posts'
import { Suspense } from 'react'
export default function Page() {
// 데이터 페칭 함수에 await를 걸지 마세요!
const posts = getPosts()
return (
<Suspense fallback={<div>Loading...</div>}>
<Posts posts={posts} />
</Suspense>
)
}
import Posts from '@/app/ui/posts'
import { Suspense } from 'react'
export default function Page() {
// 데이터 페칭 함수에 await를 걸지 마세요!
const posts = getPosts()
return (
<Suspense fallback={<div>Loading...</div>}>
<Posts posts={posts} />
</Suspense>
)
}
그런 다음, 여러분의 클라이언트 컴포넌트에서는 use API를 사용해 넘어온 프로미스를 읽어옵니다.
'use client'
import { use } from 'react'
export default function Posts({
posts,
}: {
posts: Promise<{ id: string; title: string }[]>
}) {
const allPosts = use(posts)
return (
<ul>
{allPosts.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
)
}
'use client'
import { use } from 'react'
export default function Posts({ posts }) {
const allPosts = use(posts)
return (
<ul>
{allPosts.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
)
}
위의 예제에서 <Posts> 컴포넌트는 <Suspense> 바운더리 안에 쏙 감싸져 있어요. 이것은 넘어온 프로미스가 해결(resolve)되는 동안에는 fallback으로 지정해둔 로딩 화면(<div>Loading...</div>)이 예쁘게 표시된다는 뜻이죠. 스트리밍에 대해 더 자세히 알아보세요.
💡 강사의 팁! >
use는 React 생태계에서 아주 핫한 훅(Hook)이에요. 기존에는 클라이언트 화면에서useEffect를 써서 데이터를 가져오느라 코드가 지저분해지는 경우가 많았죠. 하지만 이제는 서버에서 Promise를 넘겨주고, 클라이언트에서는use로 받아서 렌더링을 멈췄다가(Suspend) 데이터가 도착하면 마저 렌더링하는 아주 우아한 방식을 쓸 수 있게 되었어요. 최신 트렌드를 묻는 면접에서도 자신 있게 대답하기 좋은 내용입니다!
클라이언트 컴포넌트에서 데이터를 가져올 때 SWR이나 React Query 같은 멋진 라이브러리를 사용할 수도 있습니다. 이 라이브러리들은 캐싱, 스트리밍 등 다양한 기능을 위한 자기들만의 훌륭한 메커니즘을 가지고 있어요. SWR을 예로 들어볼게요.
'use client'
import useSWR from 'swr'
const fetcher = (url) => fetch(url).then((r) => r.json())
export default function BlogPage() {
const { data, error, isLoading } = useSWR(
'https://api.vercel.app/blog',
fetcher
)
if (isLoading) return <div>Loading...</div>
if (error) return <div>Error: {error.message}</div>
return (
<ul>
{data.map((post: { id: string; title: string }) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
)
}
'use client'
import useSWR from 'swr'
const fetcher = (url) => fetch(url).then((r) => r.json())
export default function BlogPage() {
const { data, error, isLoading } = useSWR(
'https://api.vercel.app/blog',
fetcher
)
if (isLoading) return <div>Loading...</div>
if (error) return <div>Error: {error.message}</div>
return (
<ul>
{data.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
)
}
💡 강사의 팁! > 서버 컴포넌트가 등장하면서 "강사님, 그럼 이제 React Query 같은 건 안 쓰나요?"라는 질문을 종종 받아요. 정답은 "여전히 강력하게 쓰입니다!" 에요. 데이터가 지속적으로 업데이트 되어야 하는 폴링(polling)이 필요하거나, 유저가 좋아요 버튼을 누르는 등의 상호작용에 따라 클라이언트 측에서 데이터를 낙관적으로 업데이트하고 무효화(invalidate)해야 하는 복잡한 상태 관리에는 React Query나 SWR이 여전히 최고의 선택입니다.
fetch 요청의 중복을 제거하는 한 가지 방법은 요청 메모이제이션(request memoization)을 사용하는 거예요. 이 메커니즘을 사용하면, 한 번의 렌더링 과정(render pass) 내에서 동일한 URL과 옵션을 가진 GET 또는 HEAD fetch 호출들이 단일 요청으로 자동으로 합쳐집니다. 이 작업은 백그라운드에서 알아서 처리되며, fetch에 Abort signal(중단 신호)을 넘겨주어 이 기능을 끌 수도 있어요(opt out).
요청 메모이제이션은 딱 하나의 요청(request) 생명주기 동안에만 유효하게 스코프가 잡혀있습니다.
또한 Next.js의 데이터 캐시(Data Cache)를 사용하여 fetch 요청의 중복을 방지할 수도 있어요. 예를 들어 fetch 옵션에 cache: 'force-cache'를 설정하는 식으로요.
데이터 캐시를 사용하면 현재 렌더링 과정뿐만 아니라 앞으로 들어오는 다른 유저들의 요청들 간에도 데이터를 공유할 수 있답니다.
만약 fetch를 사용하지 않고 ORM이나 데이터베이스를 직접 사용한다면, 데이터 접근 로직을 React의 cache 함수로 감싸주면 돼요.
import { cache } from 'react'
import { db, posts, eq } from '@/lib/db'
export const getPost = cache(async (id: string) => {
const post = await db.query.posts.findFirst({
where: eq(posts.id, parseInt(id)),
})
})
import { cache } from 'react'
import { db, posts, eq } from '@/lib/db'
import { notFound } from 'next/navigation'
export const getPost = cache(async (id) => {
const post = await db.query.posts.findFirst({
where: eq(posts.id, parseInt(id)),
})
})
💡 강사의 팁! > 여러 컴포넌트에서 똑같은 로그인한 사용자 정보를 가져오는 API를 호출한다고 가정해볼게요. 기존에는 최상위 부모 컴포넌트에서 한 번만 호출해서 자식들에게 props로 끝없이 내려주거나(Prop Drilling), Redux 같은 전역 상태 관리를 써야 했죠. 하지만 Next.js에서는 A, B, C 컴포넌트 각각에서 그냥 동일하게
fetch를 호출해도 알아서 딱 한 번만 네트워크 요청을 보냅니다. 이게 바로 메모이제이션의 마법이에요! 덕분에 컴포넌트를 훨씬 더 독립적이고 깔끔하게 작성할 수 있죠. 새로운 포트폴리오 만드실 때 이 패턴을 꼭 적용해 보세요.
경고 (Warning): 아래 내용은 Next.js 애플리케이션에서
cacheComponents설정 옵션이 켜져 있다는 것을 가정하고 있어요. 이 플래그는 Next.js 15 canary 버전에서 새롭게 도입되었답니다.
서버 컴포넌트에서 데이터를 가져올 때, 데이터는 서버에서 각각의 요청마다 페칭되고 렌더링됩니다. 만약 응답이 무척 느린 데이터 요청이 하나라도 껴있다면, 그 데이터가 다 가져와질 때까지 전체 라우트(페이지)의 렌더링이 꽉 막혀버리게(blocked) 되죠.
초기 로딩 시간과 사용자 경험(UX)을 획기적으로 개선하기 위해 스트리밍(streaming)을 사용할 수 있습니다. 스트리밍은 페이지의 완전한 HTML을 작은 청크(chunk, 덩어리)로 쪼개서 서버에서 클라이언트로 점진적으로 보내는 훌륭한 기술이에요.

애플리케이션에서 스트리밍을 활용하는 방법은 크게 두 가지가 있습니다.
loading.js 파일과 함께 묶기<Suspense>로 감싸기loading.js 사용하기페이지와 같은 폴더에 loading.js 파일을 만들면, 데이터를 가져오는 동안 페이지 전체를 통째로 스트리밍할 수 있어요. 예를 들어 app/blog/page.js를 스트리밍하려면 app/blog 폴더 안에 loading.js 파일을 추가하면 됩니다.

export default function Loading() {
// 여기에 로딩 UI를 정의하세요
return <div>Loading...</div>
}
export default function Loading() {
// 여기에 로딩 UI를 정의하세요
return <div>Loading...</div>
}
이렇게 해두면 사용자가 페이지를 이동(navigation)할 때, 텅 빈 흰 화면을 보는 대신 레이아웃과 로딩 상태를 즉시 볼 수 있어요. 그리고 렌더링이 모두 완료되면 새로운 콘텐츠가 스르륵 자동으로 교체됩니다.

내부적으로 살펴볼까요? loading.js는 layout.js 안에 중첩(nested)되며, page.js 파일과 그 아래에 있는 모든 자식 요소들을 <Suspense> 바운더리로 알아서 자동으로 감싸주게 됩니다.

이 방식은 라우트 세그먼트(레이아웃이나 큼직한 페이지 단위)에 아주 찰떡같이 잘 맞아요. 하지만 조금 더 세밀한(granular) 단위로 스트리밍을 적용하고 싶다면 직접 <Suspense>를 사용하는 것이 좋습니다.
<Suspense> 사용하기<Suspense>를 사용하면 페이지의 어떤 부분을 분리해서 스트리밍할지 훨씬 더 정교하게 제어할 수 있습니다. 예를 들어, <Suspense> 바운더리 바깥에 있는 페이지 내용(헤더나 텍스트 등)은 사용자에게 즉시 보여주고, 바운더리 안에 있는 블로그 게시글 목록만 데이터가 준비되는 대로 스트리밍해서 보여줄 수 있죠.
import { Suspense } from 'react'
import BlogList from '@/components/BlogList'
import BlogListSkeleton from '@/components/BlogListSkeleton'
export default function BlogPage() {
return (
<div>
{/* 이 콘텐츠는 즉시 클라이언트로 전송됩니다 */}
<header>
<h1>Welcome to the Blog</h1>
<p>Read the latest posts below.</p>
</header>
<main>
{/* 이 경계선 안에 동적인 콘텐츠가 있다면 스트리밍되어 들어옵니다 */}
<Suspense fallback={<BlogListSkeleton />}>
<BlogList />
</Suspense>
</main>
</div>
)
}
import { Suspense } from 'react'
import BlogList from '@/components/BlogList'
import BlogListSkeleton from '@/components/BlogListSkeleton'
export default function BlogPage() {
return (
<div>
{/* 이 콘텐츠는 즉시 클라이언트로 전송됩니다 */}
<header>
<h1>Welcome to the Blog</h1>
<p>Read the latest posts below.</p>
</header>
<main>
{/* 이 경계선 안에 동적인 콘텐츠가 있다면 스트리밍되어 들어옵니다 */}
<Suspense fallback={<BlogListSkeleton />}>
<BlogList />
</Suspense>
</main>
</div>
)
}
💡 강사의 팁! > 실무에서는 이 두 가지를 아주 적절히 섞어 씁니다. 전체적인 페이지 뼈대(UI 스켈레톤)는
loading.js로 시원하게 잡아주고, 유독 데이터를 가져오는 데 오래 걸리는 무거운 컴포넌트(예를 들어 복잡한 통계 차트나 아주 긴 댓글 목록)는<Suspense>로 한 번 더 감싸서 화면이 부분적으로 로딩되게 만드는 거죠! 체감 속도를 확 끌어올리는 비법이랍니다.
즉각적인 로딩 상태(Instant loading state)란 사용자가 네비게이션을 하자마자 즉시 보게 되는 폴백 UI(fallback UI)를 말합니다. 최고의 사용자 경험을 위해서는 앱이 멈춘 게 아니라 열심히 응답 중이라는 걸 사용자가 직관적으로 이해할 수 있도록 의미 있는 로딩 상태를 디자인하는 것을 강력히 권장해요. 뼈대를 보여주는 스켈레톤 UI나 빙글빙글 도는 스피너를 쓸 수도 있고, 앞으로 나올 화면의 일부(커버 사진, 제목 등)를 작지만 의미 있게 미리 보여주는 방법도 아주 좋습니다.
개발 중에는 React Devtools를 사용해서 컴포넌트의 로딩 상태를 미리 보고 꼼꼼히 점검할 수 있답니다.
순차적 데이터 페칭은 말 그대로 하나의 요청이 다른 요청의 데이터에 의존할 때 발생합니다.
예를 들어, 아래 코드에서 <Playlists> 컴포넌트는 반드시 artistID가 필요하기 때문에, 부모인 <Artist> 컴포넌트의 데이터 페칭이 완료된 후에만 자신의 데이터를 가져올 수 있어요.
export default async function Page({
params,
}: {
params: Promise<{ username: string }>
}) {
const { username } = await params
// 아티스트 정보 가져오기
const artist = await getArtist(username)
return (
<>
<h1>{artist.name}</h1>
{/* Playlists 컴포넌트가 로딩되는 동안 fallback UI 보여주기 */}
<Suspense fallback={<div>Loading...</div>}>
{/* Playlists 컴포넌트에게 아티스트 ID 넘겨주기 */}
<Playlists artistID={artist.id} />
</Suspense>
</>
)
}
async function Playlists({ artistID }: { artistID: string }) {
// 아티스트 ID를 사용해 플레이리스트 가져오기
const playlists = await getArtistPlaylists(artistID)
return (
<ul>
{playlists.map((playlist) => (
<li key={playlist.id}>{playlist.name}</li>
))}
</ul>
)
}
export default async function Page({ params }) {
const { username } = await params
// 아티스트 정보 가져오기
const artist = await getArtist(username)
return (
<>
<h1>{artist.name}</h1>
{/* Playlists 컴포넌트가 로딩되는 동안 fallback UI 보여주기 */}
<Suspense fallback={<div>Loading...</div>}>
{/* Playlists 컴포넌트에게 아티스트 ID 넘겨주기 */}
<Playlists artistID={artist.id} />
</Suspense>
</>
)
}
async function Playlists({ artistID }) {
// 아티스트 ID를 사용해 플레이리스트 가져오기
const playlists = await getArtistPlaylists(artistID)
return (
<ul>
{playlists.map((playlist) => (
<li key={playlist.id}>{playlist.name}</li>
))}
</ul>
)
}
이 예제에서 <Suspense>는 아티스트 데이터를 모두 불러온 뒤에 플레이리스트가 스트리밍되도록 해줍니다. 하지만 페이지 자체는 여전히 아티스트 데이터를 기다리는 동안 화면에 아무것도 표시하지 않고 멈춰있게 되죠. 이를 방지하려면 페이지 컴포넌트 전체를 <Suspense> 바운더리로 한 번 감싸서(예를 들어 loading.js 파일을 사용해서) 로딩 상태를 즉시 보여주는 것이 좋습니다.
첫 번째 요청은 다른 모든 작업의 진행을 꽉 막아버리므로, 데이터 소스에서 최대한 빠르게 응답할 수 있도록 항상 신경 써야 해요. 만약 쿼리나 요청을 더 이상 최적화할 수 없다면, 데이터가 자주 바뀌지 않는 경우 결과를 캐싱하는 것도 좋은 전략입니다.
💡 강사의 팁! > 실무에서는 이걸 흔히 "네트워크 워터폴(Waterfall) 현상"이라고 불러요. A를 가져와야만 B를 가져올 수 있는 종속적인 상황에서는 어쩔 수 없지만, 이 대기 시간을 사용자에게 어떻게 덜 지루하게 느끼게 할지 고민하고 로딩 UI를 우아하게 처리하는 것이 프론트엔드 개발자의 진짜 실력입니다!
병렬 데이터 페칭은 라우트 내의 여러 데이터 요청이 동시에 출발(eagerly initiated)할 때 발생합니다.
기본적으로 레이아웃과 페이지(layouts and pages)는 알아서 병렬로 렌더링돼요. 즉, 각 세그먼트가 가능한 한 빨리 멈추지 않고 데이터 페칭을 시작한다는 뜻이죠.
하지만 단일 컴포넌트 안에서 여러 개의 async/await 요청을 위아래로 나란히 배치하면 여전히 순차적으로 묶여버릴 수 있어요. 예를 들어 아래 코드에서 getAlbums는 getArtist가 완전히 해결(resolved)될 때까지 아무것도 못하고 기다려야 합니다.
import { getArtist, getAlbums } from '@/app/lib/data'
export default async function Page({ params }) {
// 이 요청들은 순차적으로 실행됩니다 (워터폴 발생!)
const { username } = await params
const artist = await getArtist(username)
const albums = await getAlbums(username)
return <div>{artist.name}</div>
}
이럴 때는 fetch를 먼저 호출해서 여러 요청을 동시에 시작해 둔 다음, Promise.all을 사용해서 한꺼번에 await 하세요. fetch는 호출되는 즉시 데이터를 가져오기 시작하니까요!
import Albums from './albums'
async function getArtist(username: string) {
const res = await fetch(`https://api.example.com/artist/${username}`)
return res.json()
}
async function getAlbums(username: string) {
const res = await fetch(`https://api.example.com/artist/${username}/albums`)
return res.json()
}
export default async function Page({
params,
}: {
params: Promise<{ username: string }>
}) {
const { username } = await params
// 요청들을 먼저 시작(initiate)합니다
const artistData = getArtist(username)
const albumsData = getAlbums(username)
const [artist, albums] = await Promise.all([artistData, albumsData])
return (
<>
<h1>{artist.name}</h1>
<Albums list={albums} />
</>
)
}
import Albums from './albums'
async function getArtist(username) {
const res = await fetch(`https://api.example.com/artist/${username}`)
return res.json()
}
async function getAlbums(username) {
const res = await fetch(`https://api.example.com/artist/${username}/albums`)
return res.json()
}
export default async function Page({ params }) {
const { username } = await params
// 요청들을 먼저 시작(initiate)합니다
const artistData = getArtist(username)
const albumsData = getAlbums(username)
const [artist, albums] = await Promise.all([artistData, albumsData])
return (
<>
<h1>{artist.name}</h1>
<Albums list={albums} />
</>
)
}
알아두면 좋은 점:
Promise.all을 사용할 때 어느 하나의 요청이라도 에러가 나면 전체 작업이 실패하게 됩니다. 만약 일부가 실패해도 괜찮도록 부드럽게 에러 처리를 하고 싶다면, 대신Promise.allSettled메서드를 사용해 보세요.
💡 강사의 팁! > 이거 진짜 중요합니다! 실무 프론트엔드 코드 리뷰에서 주니어분들이 정말 자주 지적받는 부분이에요. 서로 의존성이 없는 두 개의 독립적인 API를 호출할 때는 반드시
Promise.all로 묶어서 병렬 처리해야 렌더링 속도를 획기적으로 단축할 수 있습니다. 타입스크립트 기반으로 개인 프로젝트 하실 때 이 패턴을 꼼꼼하게 적용해두시면, 나중에 기술 면접에서 "성능 최적화를 위해 어떤 노력을 해보셨나요?"라는 질문에 당당하게 답변하실 수 있어요!
블로킹(blocking) 요청들보다 더 위에서 유틸리티 함수를 적극적으로 먼저 호출해 두는 방식으로 데이터를 프리로딩(preload)할 수 있습니다. 아래 코드에서 <Item>은 checkIsAvailable() 함수의 결과에 따라 조건부로 렌더링되고 있죠.
이때 checkIsAvailable()보다 먼저 preload()를 호출해서 <Item/>이 나중에 필요로 할 데이터 요청을 미리 찔러넣어(initiate) 둘 수 있어요. 그러면 막상 <Item/> 컴포넌트가 렌더링을 시작할 때쯤이면, 필요한 데이터를 이미 다 가져온 상태가 되어 딜레이가 사라집니다.
import { getItem, checkIsAvailable } from '@/lib/data'
export default async function Page({
params,
}: {
params: Promise<{ id: string }>
}) {
const { id } = await params
// item 데이터를 미리 로딩하기 시작합니다
preload(id)
// 다른 비동기 작업을 수행합니다
const isAvailable = await checkIsAvailable()
return isAvailable ? <Item id={id} /> : null
}
const preload = (id: string) => {
// void는 주어진 표현식을 평가하고 undefined를 반환합니다
// https://developer.mozilla.org/docs/Web/JavaScript/Reference/Operators/void
void getItem(id)
}
export async function Item({ id }: { id: string }) {
const result = await getItem(id)
// ...
}
import { getItem, checkIsAvailable } from '@/lib/data'
export default async function Page({ params }) {
const { id } = await params
// item 데이터를 미리 로딩하기 시작합니다
preload(id)
// 다른 비동기 작업을 수행합니다
const isAvailable = await checkIsAvailable()
return isAvailable ? <Item id={id} /> : null
}
const preload = (id) => {
// void는 주어진 표현식을 평가하고 undefined를 반환합니다
// https://developer.mozilla.org/docs/Web/JavaScript/Reference/Operators/void
void getItem(id)
}
export async function Item({ id }) {
const result = await getItem(id)
// ...
추가적으로, React의 cache 함수와 server-only 패키지를 조합하면 어디서든 재사용 가능한 프리로드 유틸리티 함수를 깔끔하게 만들 수 있어요. 이 방식을 사용하면 데이터를 가져오는 함수를 메모리에 캐싱하고, 이 로직이 오직 '서버'에서만 실행되도록 안전하게 보장할 수 있습니다.
import { cache } from 'react'
import 'server-only'
import { getItem } from '@/lib/data'
export const preload = (id: string) => {
void getItem(id)
}
export const getItem = cache(async (id: string) => {
// ...
})
import { cache } from 'react'
import 'server-only'
import { getItem } from '@/lib/data'
export const preload = (id) => {
void getItem(id)
}
export const getItem = cache(async (id) => {
// ...
})
💡 강사의 팁! > 이 프리로딩 패턴은 상당히 수준 높은 최적화 기법이에요! 컴포넌트 트리 깊숙한 곳에서 특정 데이터가 필요하다는 걸 앱이 뒤늦게 깨닫기 전에, 페이지 최상단에서 미리
preload를 걸어두면 사용자 입장에서는 페이지 전환과 동시에 내용이 번쩍! 하고 나타나는 놀라운 속도를 경험하게 됩니다.
이 문서에서 언급된 기능들에 대해 더 깊이 파고들고 싶다면 아래 API 레퍼런스를 읽어보세요.
fetch 함수에 대한 상세한 API 레퍼런스입니다.loading.js 파일 규칙에 대한 API 레퍼런스입니다.전체 문서의 구조적인 개요를 한눈에 보고 싶으시다면 /docs/sitemap.md를 확인해주세요.
모든 문서의 상세한 전체 색인(index)은 /docs/llms.txt에서 확인하실 수 있습니다.
궁금한 점이 있거나 실습하시다가 막히는 부분이 생기면 언제든 질문 남겨주세요! 제가 도와드릴까요?