클라이언트 컴포넌트에서 서버 컴포넌트를 왜 import 할 수 없을까?

sujin·2025년 10월 9일
0
post-thumbnail

Next.js 15 (App Router 기준)에서는 RSC(React Server Components) 아키텍처가 기본적으로 적용되어 있습니다. 이 환경에서는 컴포넌트를 서버에서 실행할지, 클라이언트에서 실행할지 구분해야 하죠!

그런데 만약, 클라이언트 컴포넌트안에서 서버 컴포넌트를 import 하면, 다음과 같은 에러가 발생합니다.

평소처럼 개발하던 중 문득, "왜 클라이언트 컴포넌트안에서는 서버 컴포넌트를 import 할 수 없는걸까?" 하는 의문이 생겼고 그 이유에 대해 알아봤습니다.


🤔 원인 및 이유

React Server Components의 렌더링 구조

React의 RSC 아키텍처는 단방향 렌더링 을 따릅니다.

🔄 렌더링 순서

1. 서버 단계
• React가 서버 컴포넌트를 렌더링
• RSC payload 생성 (직렬화된 컴포넌트 트리)
• Next.js가 HTML과 함께 클라이언트로 전송

2. 클라이언트 단계
• 브라우저에서 HTML 표시
• React가 hydration 및 reconciliation(재조정) 수행
• 클라이언트 컴포넌트 활성화

React는 화면을 만들 때 서버에서 먼저 컴포넌트를 실행하고, 그 다음에 클라이언트에서 이어서 처리하는 구조를 가지고 있습니다.
즉, 클라이언트 컴포넌트는 서버가 미리 만들어준 결과만 사용할 수 있고, 서버에서만 실행 가능한 컴포넌트 함수는 직접 실행할 수 없습니다.

실행 환경 차이

서버 컴포넌트클라이언트 컴포넌트
실행 위치Node.js (서버)브라우저 (클라이언트)
접근 가능 APIDB, 파일 시스템, 서버 APIDOM, Window, LocalStorage
빌드 대상서버 전용 번들브라우저용 JS 번들
렌더링 시점요청 시hydration 이후

클라이언트 컴포넌트가 서버 컴포넌트를 import한다는 건
브라우저 코드 안에서 Node.js 전용 코드를 실행하려는 시도가 되기 때문에, Next.js가 이를 감지하고 에러가 발생합니다.

브라우저 코드 안에서 Node.js 코드가 실행되지 않는 이유는?
➡️ 브라우저와 Node.js는 사용할 수 있는 API가 다르기 떄문입니다. 브라우저에는 파일 시스템, 데이터베이스, Node 전용 모듈이 없어서 서버 코드를 실행할 수 없어요!

컴포넌트 트리 구조의 제약

React는 다음과 같은 구조로 컴포넌트 트리를 렌더링합니다.

React.createElement(ClientWrapper, {}, React.createElement(ServerContent))

이 코드의 의미는 “ServerContent를 먼저 렌더링한 결과를 ClientWrapper의 children으로 전달한다” 입니다.
즉, 서버 컴포넌트는 이미 서버 단계에서 렌더링을 끝낸 후, 그 결과 (JSX Element)가 클라이언트 컴포넌트에 children으로 전달됩니다.
이때 중요한 점은 클라이언트 컴포넌트는 "컴포넌트 함수" 가 아니라, "렌더링된 결과(React Element)"를 받는다는 것입니다.


💡 올바른 컴포넌트 전달 과정

1단계: 서버에서 먼저 ServerContent 렌더링

// 서버에서 ServerContent가 실행되어 JSX 결과물 생성
const serverResult = {
  type: 'div',
  props: {
    children: 'Hello from server!'
  }

서버 컴포넌트는 서버에서 먼저 실행되어 JSX 결과물을 생성합니다.

2단계: 렌더링 결과를 RSC payload로 직렬화

{
  "type": "div",
  "props": {
    "children": "Hello from server!"
  }
}

서버에서 렌더링된 결과는 RSC payload로 직렬화됩니다.

3단계: 클라이언트로 전달

// 클라이언트에서는 이미 렌더링된 결과를 받음

function ClientWrapper({ children }) {
// children은 서버 컴포넌트가 아니라 렌더링된 React 엘리먼트
  console.log(children);
  // { type: 'div', props: { children: 'Hello from server!' } }
  
  return <div>{children}</div>
}

클라이언트 컴포넌트에서는 이미 렌더링된 React Element를 받습니다. 즉, 서버에서 만들어진 결과물을 그대로 표시합니다.


✨ 마무리

서버 컴포넌트를 클라이언트로 전달할 때 가장 중요한 점은 클라이언트는 결과물만 받을 수 있다 는 것입니다.

❌ 직접 import (불가능)

'use client'

import ServerComponent from './ServerComponent'
// 컴포넌트 함수 자체를 가져오려고 함

export default function ClientComponent() {
  return <ServerComponent />
  // 클라이언트에서 서버 컴포넌트 함수를 실행하려고 함
}

클라이언트에서 서버 컴포넌트 함수를 실행하려고 하면, 브라우저가 Node.js 전용 코드를 실행하려는 시도이기 때문에 에러가 발생합니다.

✅ Props로 전달 (가능)

// 서버에서
<ClientWrapper>
  <ServerContent /> // 서버에서 실행되어 결과물 생성
</ClientWrapper>


// 클라이언트에서
'use client'
export default function ClientWrapper({ children }) {
// children은 이미 렌더링된 결과물 (React 엘리먼트)
  return <div>{children}</div>
}

서버에서 먼저 컴포넌트를 실행하여 렌더링 결과(JSX)를 만들고, 이를 클라이언트 컴포넌트의 props(children)로 전달하면 안전하게 사용할 수 있습니다.

쉽게 비유하자면, 직접 import하는 것은 고객이 주방에 들어가 직접 요리 하려고 하는 것이고, props로 전달하는 것은 주방에서 요리를 완성한 후 완성된 음식을 고객에서 가져다 주는 것 입니다. 즉, 클라이언트 컴포넌트는 "완성된 음식"(렌더링 결과) 은 받을 수 있지만, "레시피"(서버 컴포넌트 함수) 로 직접 요리할 수는 없습니다!


평소에는 당연하게 사용했던 개념에 대해서 자세히 알아보고 정리해보면서 Next.js에 대해서 좀 더 깊이 알게된 것 같아요! 새로운 내용을 배우는 것도 중요하지만 내가 알고 있는 내용을 정확하고 깊게 이해하고 넘어가는 것도 중요하다는 것을 다시 한번 깨달았습니다...ㅎㅎ
기본을 알면 나중에 스스로 응용도 할 수 있을 거라고 생각합니다 🙌

profile
개발댕발

0개의 댓글