Server Component를 PHP의 재림이라고 오해했다

Shin Jinseop·2026년 7월 1일
post-thumbnail

Next.js의 Server Component를 처음 접했을 때 솔직한 생각은 이랬다.

프론트엔드 코드에서 서버 함수를 만들고 DB까지 바로 접근한다고? 이러면 PHP와 뭐가 다르지?

프론트엔드와 백엔드의 경계를 다시 한 프로젝트에 섞는 것처럼 보였다. 프론트엔드 개발자가 인증, DB, 트랜잭션까지 떠안아야 하는 구조라고 생각했고, 자연스럽게 Server Component에 대한 인상도 좋지 않았다.

그런데 개념을 하나씩 분리해 보니, 내가 부정적으로 바라보던 대상은 Server Component가 아니었다. 나는 Server Component와 Server Function, 그리고 Next.js에서 DB에 직접 접근하는 아키텍처를 하나의 개념처럼 묶어서 생각하고 있었다.

이 글은 그 오해가 어떻게 풀렸는지를 정리한 기록이다.

세 가지를 하나로 생각하고 있었다

Next.js App Router에서는 다음 세 가지 표현을 자주 본다.

아무 지시어 없음 → Server Component
'use client'     → Client Component
'use server'     → Server Function

처음에는 이것을 서버 컴포넌트, 클라이언트 컴포넌트, 서버 컴포넌트의 또 다른 형태 정도로 생각했다. 하지만 정확히는 두 개의 컴포넌트와 하나의 함수다.

컴포넌트
├── Server Component: 지시어 없음
└── Client Component: 'use client'

함수
└── Server Function: 'use server'

가장 큰 오해는 'use server'가 Server Component를 선언하는 문법이라고 생각한 것이었다. Server Component에는 별도의 지시어가 없다. App Router에서 컴포넌트는 기본적으로 Server Component다.

'use server'는 컴포넌트가 아니라, 클라이언트에서 호출할 수 있는 서버 함수를 표시한다.

이 구분 하나만 명확해져도 전체 구조가 달리 보이기 시작한다.

Server Component는 서버에서 UI를 만드는 컴포넌트다

Server Component의 역할은 생각보다 단순하다.

서버에서 데이터를 준비하고 React UI를 만든다.

export default async function ProductPage() {
  const product = await backendApi.getProduct()

  return (
    <main>
      <h1>{product.name}</h1>
      <p>{product.price}</p>
    </main>
  )
}

이 컴포넌트는 서버에서 실행된다. 브라우저가 ProductPage()를 내려받아 실행하는 것이 아니다. 브라우저는 서버가 만든 HTML과 RSC Payload를 받는다.

따라서 Server Component 코드와 서버 전용 라이브러리는 클라이언트 JavaScript 번들에 포함되지 않는다.

이제 Server Component를 DB 접근 기능이 아니라 React UI의 실행 위치를 서버로 옮기는 모델로 볼 수 있게 됐다.

물론 Server Component에서 DB에 직접 접근할 수 있다.

export default async function ProductPage() {
  const product = await db.product.findFirst()
  return <h1>{product.name}</h1>
}

하지만 반드시 그래야 하는 것은 아니다. 기존 백엔드 API를 호출해도 된다.

export default async function ProductPage() {
  const product = await backendApi.getProduct()
  return <h1>{product.name}</h1>
}

DB 직접 접근 여부는 Server Component의 정의가 아니다. 팀이 Next.js에 어디까지 책임을 줄 것인지 결정하는 아키텍처 문제다.

PHP처럼 느껴졌던 것은 Server Function이었다

'use server'를 붙인 함수는 Server Function이 된다.

'use server'

export async function deleteProduct(productId: string) {
  await db.product.delete({
    where: { id: productId },
  })
}

Client Component에서는 이 함수를 일반 함수처럼 호출할 수 있다.

'use client'

import { deleteProduct } from './actions'

export function DeleteButton({ productId }: { productId: string }) {
  return (
    <button onClick={() => deleteProduct(productId)}>
      삭제
    </button>
  )
}

코드만 보면 브라우저에서 서버 함수를 바로 실행하는 것처럼 보이지만, 실제로는 네트워크 요청이 발생한다.

브라우저에서 deleteProduct(productId) 호출
→ Next.js가 네트워크 요청 생성
→ 서버에서 deleteProduct 실행
→ DB 또는 백엔드 API 변경
→ 실행 결과 반환

일반 함수처럼 작성하지만 다른 실행 환경의 함수를 호출한다는 점에서 RPC에 가깝다.

내가 PHP와 비슷하다고 느낀 지점도 여기였다.

전통적인 PHP
요청 → 서버 로직 → DB 변경 → HTML 응답

Next.js
클릭 → Server Function → DB 변경 → UI 갱신

Next.js가 DB까지 직접 담당한다면 PHP와 구조적으로 비슷한 부분이 있는 것이 맞다. 서버 코드와 데이터 접근 코드가 한 애플리케이션에 존재하고, 사용자의 요청을 서버에서 처리한다.

그러므로 처음 가졌던 의문이 완전히 틀린 것은 아니었다. 다만 그것은 Server Component 자체에 대한 비판이 아니라, Next.js가 백엔드 책임까지 가져가는 풀스택 아키텍처에 대한 우려였다.

서버 함수가 DB에 직접 접근할 필요는 없다

Server Function도 기존 백엔드 API를 호출할 수 있다.

'use server'

export async function cancelOrder(orderId: string) {
  const session = await requireSession()

  await backendApi.cancelOrder({
    orderId,
    userId: session.user.id,
  })
}

이 구조에서 역할은 다음과 같이 나뉜다.

브라우저
└── Client Component
      사용자 상태와 이벤트
          │
Next.js 서버
├── Server Component
│     화면 데이터 조회·조합
└── Server Function
      사용자 명령 전달, 캐시 갱신
          │
백엔드 API
├── 핵심 비즈니스 규칙
├── 최종 권한 검증
├── 트랜잭션
└── DB

이렇게 보면 Next.js는 기존 백엔드를 대체하는 것이 아니라 UI 전용 BFF 역할을 할 수 있다.

내가 세운 경계는 다음과 같다.

Next.js 서버에는 화면을 위한 로직을 두고, 백엔드에는 사업을 위한 로직을 둔다.

Next.js는 세션을 확인하고, 화면에 필요한 여러 API를 조합하고, 캐시를 갱신한다. 주문 취소 가능 여부, 결제와 환불, 트랜잭션과 데이터 정합성 같은 핵심 규칙은 백엔드가 최종 책임진다.

Server Function은 외부에서 호출 가능한 엔드포인트이므로 입력값을 신뢰해서도 안 된다. 인증과 권한 검사는 여전히 필요하다.

성능 관점에서 다시 본 Server Component

개념을 분리하고 나니 Server Component는 다르게 보였다. 이것은 프론트엔드의 백엔드화를 위한 기능이라기보다, 브라우저가 처리하던 비용을 서버로 이동시키고 클라이언트 JavaScript를 줄이는 방식이었다.

Client Component 중심
JavaScript 다운로드 → React 실행 → API 요청 → 데이터 수신 → 재렌더링

Server Component 중심
서버에서 데이터 조회·렌더링 → 완성된 UI 결과 전달

이를 통해 기대할 수 있는 효과는 다음과 같다.

  • 브라우저로 전달되는 JavaScript 감소
  • hydration 대상 감소
  • 클라이언트 데이터 요청 waterfall 감소
  • 무거운 라이브러리를 서버에만 유지
  • 빠른 초기 콘텐츠 표시
  • Suspense를 이용한 점진적 스트리밍

예를 들어 상품 페이지에서 대부분은 상품 정보이고 구매 버튼만 상호작용한다면 다음처럼 구성할 수 있다.

ProductPage        Server Component
├── ProductInfo    Server Component
├── Description    Server Component
└── BuyButton      Client Component

페이지 전체를 'use client'로 만드는 대신, 상호작용이 필요한 BuyButton의 JavaScript만 브라우저에 전달한다.

물론 Server Component를 사용한다고 성능이 자동으로 좋아지는 것은 아니다. 캐시 없이 매번 느린 API나 DB를 기다린다면 서버 응답 시간은 길어진다.

Client Component 중심 → 브라우저 비용 증가
Server Component 중심 → 서버 연산과 데이터 조회 비용 증가

중요한 것은 비용이 사라지는 것이 아니라 적절한 실행 환경으로 이동한다는 점이다.

지금은 이렇게 판단한다

현재 내가 사용하는 기준은 다음과 같다.

1. 기본은 Server Component로 시작한다.

2. 상태·이벤트·브라우저 API가 필요한가?
   → 필요한 가장 작은 영역만 'use client'

3. 사용자가 서버 데이터를 변경하는가?
   → Server Function 사용

4. 여러 클라이언트가 사용하는 공개 API인가?
   → Route Handler 또는 별도 백엔드 API

데이터 흐름으로 보면 더 단순하다.

읽어서 화면에 보여준다       → Server Component
브라우저에서 상호작용한다     → Client Component
사용자가 서버 데이터를 변경한다 → Server Function
핵심 비즈니스 규칙을 처리한다  → Backend

마치며

Server Component를 처음 봤을 때는 프론트엔드와 백엔드의 경계를 다시 섞는 기술이라고 생각했다. 특히 'use server'와 DB 직접 접근 사례를 보면서 PHP로 돌아가는 것 같다는 인상도 받았다.

하지만 그 판단에는 세 가지 개념이 섞여 있었다.

Server Component = 서버에서 UI를 생성
Client Component = 브라우저에서 상호작용
Server Function   = 클라이언트가 서버 작업을 호출

DB에 직접 접근할지는 별도의 아키텍처 결정이다.

지금은 Server Component를 다음과 같이 이해한다.

Server Component는 백엔드를 프론트엔드로 가져오는 기술이 아니라, React UI의 실행 위치를 서버까지 확장해 브라우저의 부담을 줄이는 모델이다.

PHP식 서버 렌더링과 닮은 점은 있지만, React의 클라이언트 상태와 컴포넌트 단위 상호작용을 함께 유지한다는 점에서 목표가 다르다.

결국 중요한 것은 모든 것을 서버로 옮기는 것도, 모든 것을 클라이언트에서 처리하는 것도 아니다. 데이터 조회와 정적인 UI는 서버에 두고, 실제 상호작용이 필요한 최소 영역만 클라이언트에 남기는 것. 그것이 Server Component를 활용하는 현실적인 출발점이라고 생각한다.

참고 자료

0개의 댓글