Notice
Recent Posts
Recent Comments
Link
«   2025/05   »
1 2 3
4 5 6 7 8 9 10
11 12 13 14 15 16 17
18 19 20 21 22 23 24
25 26 27 28 29 30 31
Tags
more
Archives
Today
Total
관리 메뉴

likeornament 님의 블로그

[Next.js] Next.js 서버 컴포넌트, 정말 껍데기처럼만 써도 될까? 본문

공부/next.js

[Next.js] Next.js 서버 컴포넌트, 정말 껍데기처럼만 써도 될까?

likeornament 2025. 4. 15. 20:08
// word/learn/page.tsx
import { Suspense } from "react";
import WordLearningContainer from "../_component/WordLearningContainer";
import LoadingSpinner from "@/app/_component/LoadingSpinner";

export default function WordLearnPage() {
  return (
    <WordLearningContainer isReview={false}/>
  )
}

프로젝트 작업 중 문득 궁금한 게 생겼다.

 

위의 page.tsx를 보면, 서버 컴포넌트인 WordLearnPage가 단순히 WordLearningContainer라는 클라이언트 컴포넌트만 반환하고 있다.
(이렇게 구성한 이유는 내부 코드를 컴포넌트로 분리하여 다른 page.tsx에서도 재사용할 수 있도록 하기 위함이다.)

 

서버 컴포넌트는 서버에서 미리 정적인 HTML을 생성해 클라이언트에게 전달하므로, 초기 로딩 속도가 빠르다는 장점이 있다.

하지만 중요한 점은, 서버 컴포넌트 내부에 클라이언트 컴포넌트가 있다고 해서 해당 클라이언트 컴포넌트까지 HTML로 만들어지는 것은 아니라는 것이다.

서버는 클라이언트 컴포넌트가 들어갈 위치에 대해 "자리 표시자(placeholder)"만 HTML로 렌더링하고,
클라이언트 컴포넌트 자체는 브라우저에서 렌더링된다.

 

그렇다면 이렇게 서버 컴포넌트가 클라이언트 컴포넌트만을 반환하는 구조에도 이점이 있을까?


1. 서버 컴포넌트의 장점 일부 활용

  • 서버 컴포넌트는 렌더링 과정에서 클라이언트로부터 숨겨진 서버 사이드 로직을 실행할 수 있음
  • 지금은 단순해 보여도, 나중에 서버에서 인증 확인, DB에서 특정 권한 체크, layout-level에서만 필요한 서버 fetch 등을 추가할 수 있다.

2. Suspense나 ErrorBoundary 처리 용이

  • 서버 컴포넌트에서 Suspense를 사용하면 클라이언트 컴포넌트가 마운트되기 전 로딩 상태를 fallback UI로 표시할 수 있다.
  • 예를 들어,  WordLearningContainer 안에서 useQuery를 통해 데이터를 가져오는 도중에도 로딩 UI를 보여줄 수 있다.
<Suspense fallback={<LoadingSpinnder/>}>
  <WordLearningContainer/>
</Suspense>

 

❗ 참고: 클라이언트 컴포넌트가 내부에서 비동기 작업을 수행하는 경우, 이를 Suspense로 감싸지 않으면 React에서 렌더링 오류가 발생할 수 있다.

 

그 이유는?

  1. 비동기 작업이 완료 전, fallback UI로 일관된 상태를 보여주기 위함
  2. 컴포넌트 트리의 나머지를 블로킹하지 않기 위함
  3. 스트리밍 렌더링을 통해 성능을 최적화하기 위함

 

3. 클라이언트 번들 사이즈 최적화

만약 WordLearnPage가 클라이언트 컴포넌트였다면 해당 컴포넌트의 코드도 클라이언트 번들에 포함됐을 것이다.

하지만 현재는 서버 컴포넌트이기 때문에, 해당 컴포넌트의 JS 로직은 브라우저로 전송되지 않는다. 

이로 인해 클라이언트 측 JS 번들 사이즈를 줄일 수 있다.

 

4. 구조 확장에 유리

지금은 WordLearnContainer 하나만 반환하고 있지만, 이후에 서버에서 가져온 정보를 추가하거나, 다른 클라이언트 컴포넌트를 함께 렌더링하고 싶어질 수 있다.

 

현재 구조를 유지하면 추후 기능이 확장되어도 유연하게 대응할 수 있다.

 

5. 서버 prefetch + 클라이언트 hydrate 구조

const queryClient = new QueryClient();
await queryClient.prefetchQuery(...);

return (
  <HydrationBoundary state={dehydrate(queryClient)}>
    <WordLearningContainer/>
  </HydrationBoundary>
)

현재 이 서비스는 대부분이 localStorage의 토큰을 사용하는 api라서 서버 prefetch가 어렵지만,
비로그인 사용자나 공용 데이터 요청의 경우에는 이처럼 서버에서 prefetchQuery로 데이터를 미리 가져오고, 클라이언트에서 해당 데이터를 hydrate할 수 있는 구조로 확장 가능하다.


 

✅ 결론

단순히 서버 컴포넌트가 클라이언트 컴포넌트만을 감싸는 구조는 겉보기에는 이점이 없어 보여도,

실제로는 다음과 같은 측면에서 충분한 이점이 있다.

  • 로딩 처리(Suspense)
  • 서버 기능 활용(DB, 인증 등)
  • 클라이언트 번들 최적화
  • 구조 확장성 확보
  • 서버 prefetch => 클라이언트 hydrate 확장 가능