본문으로 건너뛰기

Data Fetching과 Server Function

Next.js의 App Router를 이해할 때 Data FetchingServer Function은 함께 등장해서 헷갈리기 쉽습니다.

하지만 두 개의 역할은 명확히 다릅니다.

  • Data Fetching → 서버에서 데이터를 읽어오는 것
  • Server Function → 클라이언트의 요청을 받아 서버에서 작업을 실행하는 것

쉽게 말하면 다음과 같습니다.

데이터 조회
→ Data Fetching

데이터 생성 / 수정 / 삭제
→ Server Function

이 차이를 먼저 이해하는 것이 중요합니다.


핵심 내용

Data Fetching

Next.js의 Server Component에서는 컴포넌트 안에서 직접 데이터를 가져올 수 있습니다.

export default async function Page() {
const response = await fetch('/api/posts');
const posts = await response.json();

return (
<ul>
{posts.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
);
}

Server Component는 서버에서 실행되기 때문에 async/await을 그대로 사용할 수 있습니다.

따라서 Client Component에서 데이터를 가져올 때 자주 사용하던

useEffect

fetch

setState

렌더링

과정을 반드시 거칠 필요가 없습니다.

대신 다음처럼 생각할 수 있습니다.

Server Component 실행

데이터 조회

컴포넌트 렌더링

브라우저로 결과 전달

Next.js에서는 fetch뿐 아니라 ORM이나 데이터베이스에도 Server Component에서 직접 접근할 수 있습니다.


왜 서버에서 데이터를 가져올까요?

Client Component에서 데이터를 가져오면 먼저 브라우저에 JavaScript가 전달된 뒤 추가 요청이 발생할 수 있습니다.

브라우저

JavaScript 실행

API 요청

데이터 응답

화면 렌더링

반면 Server Component에서는 서버에서 데이터를 가져온 뒤 필요한 UI 결과를 전달할 수 있습니다.

서버

데이터 조회

UI 생성

브라우저 전달

또한 데이터베이스 비밀번호나 서버 전용 로직을 브라우저에 노출하지 않고 사용할 수 있다는 장점이 있습니다.


Data Fetching과 Cache

여기서 한 가지 주의할 점이 있습니다.

현재 Next.js의 fetch 요청은 기본적으로 캐시되지 않습니다.

const data = await fetch('https://example.com/posts');

따라서 항상 캐시를 사용하는 것이 아니라, 필요하다면 명시적으로 캐시할 수 있습니다.

최근 Next.js에서는 Cache Components를 사용하는 경우 use cache를 이용해 함수나 컴포넌트의 결과를 캐시할 수 있습니다.

import { cacheLife } from 'next/cache';

async function getPosts() {
'use cache';

cacheLife('hours');

return db.posts.findMany();
}

즉,

항상 최신 데이터가 필요함
→ 요청 시 데이터 조회

여러 요청에서 재사용해도 됨
→ use cache

정도로 이해하면 됩니다. cacheLife를 이용하면 캐시가 얼마나 오래 유지될지도 지정할 수 있습니다.


Server Function

Server Function클라이언트에서 호출할 수 있지만 실제 코드는 서버에서 실행되는 함수입니다.

React에서는 "use server" 지시어를 사용해 Server Function을 정의할 수 있습니다.

'use server';

export async function createPost(formData: FormData) {
const title = formData.get('title');

await db.post.create({
data: { title },
});
}

클라이언트에서는 이 함수를 일반 함수처럼 사용할 수 있습니다.

'use client';

import { createPost } from './actions';

export default function Form() {
return (
<form action={createPost}>
<input name="title" />
<button type="submit">작성</button>
</form>
);
}

하지만 실제 실행 위치는 브라우저가 아닙니다.

사용자

버튼 클릭

Server Function 호출 요청

서버에서 함수 실행

DB 변경

결과 반환

React는 "use server"가 붙은 함수를 클라이언트 코드에 그대로 포함하는 대신 서버 함수에 대한 참조를 전달합니다.

클라이언트에서 이를 호출하면 React가 서버로 요청을 보내 실제 함수를 실행합니다.


Data Fetching과 Server Function의 차이

둘의 가장 큰 차이는 목적입니다.

Data Fetching

주로 데이터를 읽을 때 사용합니다.

const posts = await db.post.findMany();
DB

데이터 조회

UI 렌더링

Server Function

사용자의 행동으로 서버의 데이터를 변경할 때 주로 사용합니다.

'use server';

export async function createPost() {
await db.post.create(...);
}
사용자 행동

Server Function

DB 변경

따라서 보통 다음과 같은 구조가 됩니다.

페이지 진입
→ Data Fetching
→ 게시글 조회

게시글 작성 버튼 클릭
→ Server Function
→ 게시글 생성

Server Function은 API Route와 같은 것인가요?

완전히 같은 개념은 아닙니다.

기존에는 클라이언트에서 서버 작업을 실행하려면 직접 API를 만드는 경우가 많았습니다.

Client

POST /api/posts

Route Handler

DB

Server Function을 사용하면 React 컴포넌트와 서버 함수를 직접 연결할 수 있습니다.

Client

createPost()

Server Function

DB

즉 Server Function을 사용한다고 해서 네트워크 통신 자체가 사라지는 것은 아닙니다.

API 요청을 직접 설계하고 호출하는 부분을 React와 프레임워크가 추상화해 주는 것에 가깝습니다. React는 클라이언트가 Server Function을 호출하면 서버에 실행 요청을 보내고 결과를 돌려줍니다.


데이터가 변경되면 어떻게 하나요?

예를 들어 게시글 목록을 캐시하고 있다고 가정하겠습니다.

async function getPosts() {
'use cache';

return db.post.findMany();
}

게시글을 생성하는 Server Function이 실행되어도 기존 캐시가 남아 있다면 이전 목록이 보일 수 있습니다.

따라서 데이터 변경 이후 캐시를 갱신해야 할 수 있습니다.

'use server';

import { updateTag } from 'next/cache';

export async function createPost() {
await db.post.create(...);

updateTag('posts');
}

Next.js에서는 cacheTag, updateTag, revalidateTag, revalidatePath 등을 사용해 필요한 캐시를 갱신할 수 있습니다. 특히 updateTag는 Server Action에서 사용자가 변경한 내용을 바로 다시 읽어야 하는 경우를 위한 방식입니다.

전체 흐름은 다음과 같습니다.

Server Component

Data Fetching

화면 표시

사용자 입력

Server Function

DB 변경

Cache 갱신

새로운 데이터 표시

Streaming

여기서 Data Fetching을 이해하면 Streaming이 왜 필요한지도 자연스럽게 이해할 수 있습니다.

다음과 같은 페이지가 있다고 가정하겠습니다.

export default async function Page() {
const user = await getUser();
const posts = await getPosts();
const comments = await getComments();

return <Dashboard />;
}

만약 getComments()가 5초 걸린다면 어떻게 될까요?

Streaming이 없다면 서버가 필요한 작업을 모두 끝낼 때까지 사용자는 페이지를 기다려야 할 수 있습니다.

User 조회 완료
Posts 조회 완료
Comments 조회 .......... 완료



페이지 표시

문제는 빠르게 준비할 수 있는 UI까지 느린 데이터 때문에 함께 기다린다는 것입니다.


Streaming은 페이지를 나누어 전달합니다

Streaming은 서버가 페이지 전체를 완성한 뒤 한 번에 보내는 대신, 준비된 UI부터 조금씩 브라우저로 전달하는 방식입니다.

먼저 준비된 UI
→ 바로 전달

느린 데이터
→ 로딩 UI 표시

데이터 준비 완료
→ 해당 UI 추가 전달

즉 사용자 입장에서는 빈 화면을 오래 기다리는 대신 페이지의 일부를 먼저 볼 수 있습니다. Next.js는 이를 통해 HTML을 작은 단위로 나누어 점진적으로 전달할 수 있습니다.


Suspense와 Streaming

Next.js에서는 주로 React의 <Suspense>를 이용해 Streaming의 경계를 지정합니다.

import { Suspense } from 'react';

export default function Page() {
return (
<>
<Header />

<Suspense fallback={<PostsSkeleton />}>
<Posts />
</Suspense>
</>
);
}

Header가 바로 준비될 수 있고 Posts가 데이터를 기다려야 한다면 다음 순서로 동작할 수 있습니다.

1. Header 전달
2. PostsSkeleton 전달
3. Posts 데이터 조회
4. Posts 준비
5. 실제 Posts UI 전달

따라서 <Suspense>는 단순히 로딩 컴포넌트를 보여주는 기능만 하는 것이 아닙니다.

어디까지 먼저 보여주고, 어떤 부분을 나중에 Streaming할 것인지 정하는 경계라고 보는 것이 더 정확합니다.

아래 그림처럼 생각하면 이해하기 쉽습니다.


loading.tsx와 Suspense

Next.js에서는 Streaming을 적용하는 대표적인 방법이 두 가지 있습니다.

loading.tsx

페이지 또는 Route Segment 단위의 로딩 UI를 만들 때 사용합니다.

app/
└── posts/
├── page.tsx
└── loading.tsx

페이지가 준비되는 동안 loading.tsx가 먼저 표시됩니다.

<Suspense>

페이지 내부의 특정 영역만 나누고 싶을 때 사용합니다.

<Header />

<Suspense fallback={<PostSkeleton />}>
<Posts />
</Suspense>

<Suspense fallback={<CommentSkeleton />}>
<Comments />
</Suspense>

이렇게 하면 PostsComments가 각각 준비되는 시점에 화면에 나타날 수 있습니다.

즉,

페이지 전체 단위
→ loading.tsx

컴포넌트 단위
→ Suspense

라고 생각하면 이해하기 쉽습니다.


Streaming의 핵심

Streaming의 목적은 데이터를 더 빠르게 가져오는 것이 아닙니다.

예를 들어 API 요청 자체가 5초 걸린다면 Streaming을 사용한다고 해서 그 요청이 1초로 줄어들지는 않습니다.

대신,

기존

모든 데이터 준비

전체 UI 표시

에서

Streaming

준비된 UI 표시

Loading UI

나머지 데이터 준비

나머지 UI 표시

로 사용자에게 보여주는 방식을 바꿉니다.

따라서 Streaming은 실제 데이터 요청 시간을 줄이는 기술이라기보다 체감 로딩 시간을 줄이는 렌더링 방식이라고 이해하면 됩니다.


실무에서 볼 점

페이지를 만들 때 다음 순서로 생각하면 편합니다.

먼저 데이터가 어디에서 필요한지 판단합니다.

페이지를 렌더링하기 위한 데이터
→ Server Component에서 Data Fetching

사용자의 입력으로 데이터 변경
→ Server Function

그다음 캐시가 필요한지 판단합니다.

항상 최신 데이터 필요
→ 요청 시 조회

여러 사용자/요청에서 재사용 가능
→ Cache

마지막으로 느린 데이터가 전체 페이지를 막는지 확인합니다.

빠른 영역과 느린 영역이 함께 존재
→ Suspense
→ Streaming

헷갈리기 쉬운 점

Server Component와 Server Function은 같은 것인가요?

아닙니다.

Server Component
→ 서버에서 UI를 계산하는 컴포넌트

Server Function
→ 클라이언트가 호출할 수 있는 서버 함수

입니다.


Server Function은 데이터를 가져오는 용도인가요?

기술적으로 서버 로직을 실행할 수 있지만, 일반적인 페이지 데이터 조회는 Server Component에서 직접 처리하는 편이 자연스럽습니다.

Server Function은 주로 사용자의 행동에 따라 데이터를 생성하거나 수정하거나 삭제하는 작업에 사용합니다.


Streaming을 사용하면 API가 빨라지나요?

아닙니다.

Streaming은 데이터 요청 자체의 속도를 높이지 않습니다.

대신 준비된 UI를 먼저 보여줘 사용자가 기다리는 시간을 줄여줍니다.


Suspense는 단순한 Loading 컴포넌트인가요?

아닙니다.

fallback으로 Loading UI를 보여주는 역할도 있지만, 서버 렌더링에서는 Streaming의 경계를 만드는 역할도 합니다.


한 번에 정리

Data Fetching
→ 데이터를 읽는다

Server Function
→ 서버에서 작업을 실행한다

Cache
→ 가져온 결과를 재사용한다

Suspense
→ 느린 UI의 경계를 나눈다

Streaming
→ 준비된 UI부터 브라우저에 전달한다

전체 흐름을 연결하면 다음과 같습니다.

페이지 요청

Server Component

Data Fetching

준비된 UI 먼저 전달

느린 영역은 Suspense

Streaming으로 나중에 전달

사용자 행동

Server Function

DB 변경

Cache 갱신

새로운 UI 표시

가장 중요한 문장은 이것입니다.

Data Fetching은 데이터를 읽는 과정이고, Server Function은 서버의 작업을 실행하는 방법이며, Streaming은 준비된 UI부터 먼저 사용자에게 전달하는 렌더링 방식입니다.