데이터 가져오기와 캐싱
이 가이드는 Next.js에서 데이터 가져오기와 캐싱의 기본 사항을 안내하며, 실용적인 예시와 모범 사례를 제공합니다.
다음은 Next.js에서 데이터 가져오기의 최소한의 예시입니다:
export default async function Page() {
let data = await fetch("https://api.vercel.app/blog");
let posts = await data.json();
return (
<ul>
{posts.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
);
}
export default async function Page() {
let data = await fetch("https://api.vercel.app/blog");
let posts = await data.json();
return (
<ul>
{posts.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
);
}
이 예시는 비동기 React 서버 컴포넌트에서 fetch
API를 사용한 기본적인 서버 측 데이터 가져오기를 보여줍니다.
참조
fetch
- React
cache
- Next.js
unstable_cache
예시
fetch
API를 사용하여 서버에서 데이터 가져오기
이 컴포넌트는 블로그 게시물 목록을 가져와 표시합니다. fetch
에서의 응답은 자동으로 캐시됩니다.
export default async function Page() {
let data = await fetch("https://api.vercel.app/blog");
let posts = await data.json();
return (
<ul>
{posts.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
);
}
export default async function Page() {
let data = await fetch("https://api.vercel.app/blog");
let posts = await data.json();
return (
<ul>
{posts.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
);
}
애플리케이션의 다른 곳에서 동적 함수를 사용하지 않는 경우, 이 페이지는 next build
동안 정적 페이지로 사전 렌더링됩니다. 그런 다음 점진적 정적 재생성을 사용하여 데이터를 업데이트할 수 있습니다.
fetch
의 응답을 캐시하지 않으려면 다음과 같이 할 수 있습니다:
let data = await fetch("https://api.vercel.app/blog", { cache: "no-store" });
ORM 또는 데이터베이스를 사용하여 서버에서 데이터 가져오기
이 컴포넌트는 항상 동적이고 최신 블로그 게시물 목록을 가져와 표시합니다.
import { db, posts } from "@/lib/db";
export default async function Page() {
let 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() {
let allPosts = await db.select().from(posts);
return (
<ul>
{allPosts.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
);
}
데이터베이스 호출은 캐시되지 않습니다. 이 예시는 Next.js 애플리케이션을 서버 사이드 렌더링으로 전환합니다. 응답을 캐시하고 페이지가 사전 렌더링되도록 하려면 이 예시를 참조하세요.
클라이언트에서 데이터 가져오기
먼저 서버 측에서 데이터를 가져오는 것을 권장합니다.
그러나 클라이언트 측 데이터 가져오기가 합리적인 경우도 있습니다. 이러한 시나리오에서는 useEffect
에서 수동으로 fetch
를 호출하거나(권장하지 않음), 커뮤니티의 인기 있는 React 라이브러리(예: SWR 또는 React Query)를 활용하여 클라이언트 가져오기를 할 수 있습니다.
"use client";
import { useState, useEffect } from "react";
export function Posts() {
const [posts, setPosts] = useState(null);
useEffect(() => {
async function fetchPosts() {
let res = await fetch("https://api.vercel.app/blog");
let data = await res.json();
setPosts(data);
}
fetchPosts();
}, []);
if (!posts) return <div>로딩 중...</div>;
return (
<ul>
{posts.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
);
}
"use client";
import { useState, useEffect } from "react";
export function Posts() {
const [posts, setPosts] = useState(null);
useEffect(() => {
async function fetchPosts() {
let res = await fetch("https://api.vercel.app/blog");
let data = await res.json();
setPosts(data);
}
fetchPosts();
}, []);
if (!posts) return <div>로딩 중...</div>;
return (
<ul>
{posts.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
);
}
ORM 또는 데이터베이스를 사용한 데이터 캐싱
unstable_cache
API를 사용하여 응답을 캐시하여 next build
를 실행할 때 페이지가 사전 렌더링되도록 할 수 있습니다.
import { unstable_cache } from "next/cache";
import { db, posts } from "@/lib/db";
const getPosts = unstable_cache(
async () => {
return await db.select().from(posts);
},
["posts"],
{ revalidate: 3600, tags: ["posts"] }
);
export default async function Page() {
const allPosts = await getPosts();
return (
<ul>
{allPosts.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
);
}
import { unstable_cache } from "next/cache";
import { db, posts } from "@/lib/db";
const getPosts = unstable_cache(
async () => {
return await db.select().from(posts);
},
["posts"],
{ revalidate: 3600, tags: ["posts"] }
);
export default async function Page() {
const allPosts = await getPosts();
return (
<ul>
{allPosts.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
);
}
이 예시는 데이터베이스 쿼리 결과를 1시간(3600초) 동안 캐시합니다. 또한 posts
캐시 태그를 추가하여 점진적 정적 재생성으로 무효화할 수 있습니다.
여러 함수에서 데이터 재사용
Next.js는 generateMetadata
와 generateStaticParams
와 같은 API를 사용하여 page
에서 가져온 동일한 데이터를 사용해야 할 수 있습니다.
fetch
를 사용하는 경우, 요청은 자동으로 메모이제이션됩니다. 이는 동일한 URL과 옵션으로 안전하게 여러 번 호출할 수 있고 하나의 요청만 이루어진다는 것을 의미합니다.
import { notFound } from "next/navigation";
interface Post {
id: string;
title: string;
content: string;
}
async function getPost(id: string) {
let res = await fetch(`https://api.example.com/posts/${id}`);
let post: Post = await res.json();
if (!post) notFound();
return post;
}
export async function generateStaticParams() {
let posts = await fetch("https://api.example.com/posts").then((res) =>
res.json()
);
return posts.map((post: Post) => ({
id: post.id,
}));
}
export async function generateMetadata({ params }: { params: { id: string } }) {
let post = await getPost(params.id);
return {
title: post.title,
};
}
export default async function Page({ params }: { params: { id: string } }) {
let post = await getPost(params.id);
return (
<article>
<h1>{post.title}</h1>
<p>{post.content}</p>
</article>
);
}
import { notFound } from "next/navigation";
async function getPost(id) {
let res = await fetch(`https://api.example.com/posts/${id}`);
let post = await res.json();
if (!post) notFound();
return post;
}
export async function generateStaticParams() {
let posts = await fetch("https://api.example.com/posts").then((res) =>
res.json()
);
return posts.map((post) => ({
id: post.id,
}));
}
export async function generateMetadata({ params }) {
let post = await getPost(params.id);
return {
title: post.title,
};
}
export default async function Page({ params }) {
let post = await getPost(params.id);
return (
<article>
<h1>{post.title}</h1>
<p>{post.content}</p>
</article>
);
}
fetch
를 사용하지 않고 ORM이나 데이터베이스를 직접 사용하는 경우, React cache
함수로 데이터 가져오기를 래핑할 수 있습니다. 이렇게 하면 중복을 제거하고 하나의 쿼리만 수행됩니다.
import { cache } from "react";
import { db, posts, eq } from "@/lib/db"; // Drizzle ORM 예시
import { notFound } from "next/navigation";
export const getPost = cache(async (id) => {
const post = await db.query.posts.findFirst({
where: eq(posts.id, parseInt(id)),
});
if (!post) notFound();
return post;
});
캐시된 데이터 재검증
점진적 정적 재생성을 통해 캐시된 데이터를 재검증하는 방법에 대해 자세히 알아보세요.
패턴
병렬 및 순차적 데이터 가져오기
컴포넌트 내에서 데이터를 가져올 때는 두 가지 데이터 가져오기 패턴을 알아야 합니다: 패턴에는 '병렬(Parallel)'과 '순차적(Sequential)'이 있습니다.

- 순차적: 컴포넌트 트리의 요청이 서로 의존적입니다. 이는 더 긴 로딩 시간으로 이어질 수 있습니다.
- 병렬: 라우트의 요청이 즉시 시작되어 동시에 데이터를 로드합니다. 이는 데이터를 로드하는 데 걸리는 총 시간을 줄입니다.
순차적 데이터 가져오기
중첩된 컴포넌트가 있고 각 컴포넌트가 자체 데이터를 가져오는 경우, 해당 데이터 요청이 메모이제이션되지 않으면 데이터 가져오기가 순차적으로 발생합니다.
한 가져오기가 다른 가져오기의 결과에 의존하는 경우 이 패턴이 필요할 수 있습니다. 예를 들어, Playlists
컴포넌트는 Artist
컴포넌트가 데이터 가져오기를 완료한 후에만 데이터 가져오기를 시작합니다. 왜냐하면 Playlists
는 artistID
prop에 의존하기 때문입니다:
export default async function Page({
params: { username },
}: {
params: { username: string };
}) {
// 아티스트 정보 가져오기
const artist = await getArtist(username);
return (
<>
<h1>{artist.name}</h1>
{/* Playlists 컴포넌트가 로딩되는 동안 폴백 UI 표시 */}
<Suspense fallback={<div>로딩 중...</div>}>
{/* 아티스트 ID를 Playlists 컴포넌트에 전달 */}
<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: { username } }) {
// 아티스트 정보 가져오기
const artist = await getArtist(username);
return (
<>
<h1>{artist.name}</h1>
{/* Playlists 컴포넌트가 로딩되는 동안 폴백 UI 표시 */}
<Suspense fallback={<div>로딩 중...</div>}>
{/* 아티스트 ID를 Playlists 컴포넌트에 전달 */}
<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>
);
}
loading.js
(라우트 세그먼트용) 또는 React <Suspense>
(중첩된 컴포넌트용)를 사용하여 React가 결과를 스트리밍하는 동안 즉각적인 로딩 상태를 표시할 수 있습니다.
이렇게 하면 전체 라우트가 데이터 요청에 의해 차단되는 것을 방지하고, 사용자는 준비된 페이지의 부분과 상호 작용할 수 있게 됩니다.
병렬 데이터 가져오기
기본적으로 레이아웃과 페이지 세그먼트는 병렬로 렌더링됩니다. 이는 요청이 병렬로 시작된다는 것을 의미합니다.
그러나 async
/await
의 특성상, 동일한 세그먼트나 컴포넌트 내의 대기된 요청은 그 아래의 모든 요청을 차단합니다.
병렬로 데이터를 가져오려면 데이터를 사용하는 컴포넌트 외부에서 요청을 정의하여 즉시 시작할 수 있습니다. 이렇게 하면 두 요청을 병렬로 시작하여 시간을 절약할 수 있지만, 사용자는 두 프로미스가 모두 해결될 때까지 렌더링된 결과를 볼 수 없습니다.
아래 예시에서 getArtist
와 getAlbums
함수는 Page
컴포넌트 외부에서 정의되고 Promise.all
을 사용하여 컴포넌트 내에서 시작됩니다:
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: { username },
}: {
params: { username: string };
}) {
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: { username } }) {
const artistData = getArtist(username);
const albumsData = getAlbums(username);
// 두 요청을 병렬로 시작
const [artist, albums] = await Promise.all([artistData, albumsData]);
return (
<>
<h1>{artist.name}</h1>
<Albums list={albums} />
</>
);
}
또한 Suspense 경계를 추가하여 렌더링 작업을 나누고 결과의 일부를 최대한 빨리 표시할 수 있습니다.
데이터 프리로딩
요청이 순차적으로 처리되는 문제(waterfalls)를 방지하는 또 다른 방법은 차단 요청 위에서 즉시 호출하는 프리로드 패턴을 사용하여 유틸리티 함수를 만드는 것입니다. 예를 들어, checkIsAvailable()
가 <Item/>
의 렌더링을 차단하므로 그 전에 preload()
를 호출하여 <Item/>
의 데이터 의존성을 즉시 시작할 수 있습니다. <Item/>
이 렌더링될 때쯤이면 이미 데이터가 가져와져 있을 것입니다.
preload
함수는 checkIsAvailable()
의 실행을 차단하지 않습니다.
import { getItem } from "@/utils/get-item";
export const preload = (id: string) => {
// void는 주어진 표현식을 평가하고 undefined를 반환합니다
// https://developer.mozilla.org/docs/Web/JavaScript/Reference/Operators/void
void getItem(id);
};
export default async function Item({ id }: { id: string }) {
const result = await getItem(id);
// ...
}
import { getItem } from "@/utils/get-item";
export const preload = (id) => {
// void는 주어진 표현식을 평가하고 undefined를 반환합니다
// https://developer.mozilla.org/docs/Web/JavaScript/Reference/Operators/void
void getItem(id);
};
export default async function Item({ id }) {
const result = await getItem(id);
// ...
}
import Item, { preload, checkIsAvailable } from "@/components/Item";
export default async function Page({
params: { id },
}: {
params: { id: string };
}) {
// 아이템 데이터 로딩 시작
preload(id);
// 다른 비동기 작업 수행
const isAvailable = await checkIsAvailable();
return isAvailable ? <Item id={id} /> : null;
}
import Item, { preload, checkIsAvailable } from "@/components/Item";
export default async function Page({ params: { id } }) {
// 아이템 데이터 로딩 시작
preload(id);
// 다른 비동기 작업 수행
const isAvailable = await checkIsAvailable();
return isAvailable ? <Item id={id} /> : null;
}
알아두면 좋은 점: "프리로드" 함수는 API가 아닌 패턴이므로 어떤 이름이든 가질 수 있습니다.
프리로드 패턴에 React cache
와 server-only
사용하기
cache
함수, preload
패턴, server-only
패키지를 결합하여 앱 전체에서 사용할 수 있는 데이터 가져오기 유틸리티를 만들 수 있습니다.
import { cache } from "react";
import "server-only";
export const preload = (id: string) => {
void getItem(id);
};
export const getItem = cache(async (id: string) => {
// ...
});
import { cache } from "react";
import "server-only";
export const preload = (id) => {
void getItem(id);
};
export const getItem = cache(async (id) => {
// ...
});
이 접근 방식을 사용하면 데이터를 미리 가져오고, 응답을 캐시하며, 이 데이터 가져오기가 서버에서만 발생하도록 보장할 수 있습니다.
utils/get-item
내보내기는 레이아웃, 페이지 또는 다른 컴포넌트에서 사용하여 아이템의 데이터가 언제 가져와지는지 제어할 수 있습니다.
알아두면 좋은 점:
- 서버 데이터 가져오기 함수가 클라이언트에서 절대 사용되지 않도록 하기 위해
server-only
패키지를 사용하는 것을 권장합니다.
중요한 데이터가 클라이언트에 노출되는 것을 방지하기
React의 taint API인 taintObjectReference
와 taintUniqueValue
를 사용하여 전체 객체 인스턴스나 중요한 값이 클라이언트에 전달되는 것을 방지하는 것을 권장합니다.
애플리케이션에서 tainting을 활성화하려면 Next.js 구성의 experimental.taint
옵션을 true
로 설정하세요:
module.exports = {
experimental: {
taint: true,
},
};
그런 다음 taint하려는 객체나 값을 experimental_taintObjectReference
또는 experimental_taintUniqueValue
함수에 전달하세요:
import { queryDataFromDB } from "./api";
import {
experimental_taintObjectReference,
experimental_taintUniqueValue,
} from "react";
export async function getUserData() {
const data = await queryDataFromDB();
experimental_taintObjectReference(
"전체 사용자 객체를 클라이언트에 전달하지 마세요",
data
);
experimental_taintUniqueValue(
"사용자의 주소를 클라이언트에 전달하지 마세요",
data,
data.address
);
return data;
}
import { queryDataFromDB } from "./api";
import {
experimental_taintObjectReference,
experimental_taintUniqueValue,
} from "react";
export async function getUserData() {
const data = await queryDataFromDB();
experimental_taintObjectReference(
"전체 사용자 객체를 클라이언트에 전달하지 마세요",
data
);
experimental_taintUniqueValue(
"사용자의 주소를 클라이언트에 전달하지 마세요",
data,
data.address
);
return data;
}
import { getUserData } from "./data";
export async function Page() {
const userData = getUserData();
return (
<ClientComponent
user={userData} // 이는 taintObjectReference로 인해 오류를 발생시킬 것입니다
address={userData.address} // 이는 taintUniqueValue로 인해 오류를 발생시킬 것입니다
/>
);
}
import { getUserData } from "./data";
export async function Page() {
const userData = await getUserData();
return (
<ClientComponent
user={userData} // 이는 taintObjectReference로 인해 오류를 발생시킬 것입니다
address={userData.address} // 이는 taintUniqueValue로 인해 오류를 발생시킬 것입니다
/>
);
}