React 서버 컴포넌트: 서버에서 실행한 컴포넌트는 브라우저에 무엇을 보내는가
노트 파일 읽기와 화면 펼치기를 한 컴포넌트에 섞지 않고, 서버에서 만든 렌더 결과와 브라우저 상호작용 코드를 분리해 RSC와 SSR의 차이를 확인한다.
목차
표준·구현·측정·해석 표시는 무엇인가요?
- 표준웹 표준이나 언어 명세가 정한 동작
- 구현특정 기술이나 브라우저가 실제로 구현한 동작
- 측정명시한 환경에서 직접 실행해 관찰한 결과
- 해석앞선 근거에서 도출한 설계 판단
- 미확인아직 공식 근거나 재현 결과를 확인하지 못한 내용
30초 요약
React 서버 컴포넌트는 클라이언트 앱과 분리된 서버 환경에서 먼저 실행된다. 브라우저는 그 컴포넌트 함수와 서버 전용 의존성을 실행하지 않고 렌더 결과를 받는다. 클릭, 상태와 브라우저 기능이 필요한 작은 부분만 React 클라이언트 컴포넌트로 조합한다. 이는 첫 HTML을 서버에서 만드는 SSR과 같은 질문이 아니다.
이 글에서 **서버 사이드 렌더링(SSR)**은 첫 HTML을 서버에서 만드는 방식, slug는
/notes/immutability에서 immutability처럼 URL의 동적 자리를 채운 문자열을 뜻한다.
이 글을 관통하는 상황: 파일 읽기와 펼치기 상태를 어디에서 실행할까
Next.js 16.2 App Router에서 노트 파일을 읽고, 버튼으로 본문을 펼치는 화면을 만든다고 하자. 파일 읽기는 서버 파일 시스템이 필요하지만 펼치기 상태는 사용자의 브라우저 사건에 반응해야 한다.
먼저 실행용 텍스트 파일을 하나 둔다.
불변 업데이트는 이전 상태를 보존하고 변경된 경로에 새 객체를 만든다.실패부터 재현한다: 한 파일에 두 환경의 책임을 섞는다
처음에는 page.jsx 하나에서 파일도 읽고 펼치기 상태도 만들려고 하기 쉽다.
import { readFile } from 'node:fs/promises'
import { useState } from 'react'
export default async function NotePage({ params }) {
const { slug } = await params
const body = await readFile(
`${process.cwd()}/content/notes/${slug}.txt`,
'utf8',
)
const [isOpen, setIsOpen] = useState(false)
return (
<main>
<button onClick={() => setIsOpen((open) => !open)}>노트 열기</button>
{isOpen ? <pre>{body}</pre> : null}
</main>
)
}pnpm dev로 이 경로를 열면 Next.js는 useState를 사용하는 부분에 클라이언트 컴포넌트가
필요하다고 알린다.
표지 하나를 고르는 문제가 아니라 서버 파일 읽기와 브라우저 상태라는 두 책임이 한 파일에 섞인 것이 원인이다.
두 책임을 각 실행 환경에 남긴다
런타임은 코드를 실제로 실행하는 환경이다. Next.js의 Node.js 런타임은 서버 파일 시스템 기능을 제공하고, Edge 런타임은 일부 Node.js 기능이 없는 별도 서버 실행 환경이다. 이 예제는 로컬 파일을 읽으므로 Node.js를 선택한다.
App Router의 page와 layout은 기본적으로 서버 컴포넌트다. page.jsx에는 별도 지시문을 쓰지
않고, 허용한 slug만 실제 파일명으로 바꿔 경로 입력을 제한한다.
import { readFile } from 'node:fs/promises'
import path from 'node:path'
import Disclosure from './disclosure'
const NOTE_FILES = {
immutability: 'immutability.txt',
}
export const runtime = 'nodejs'
export default async function NotePage({ params }) {
const { slug } = await params
const fileName = NOTE_FILES[slug]
if (!fileName) return <p>노트를 찾을 수 없습니다.</p>
const body = await readFile(
path.join(process.cwd(), 'content', 'notes', fileName),
'utf8',
)
console.log('SERVER_ONLY_NOTE_READER:', typeof window)
return (
<main>
<h1>{slug}</h1>
<Disclosure>
<pre>{body}</pre>
</Disclosure>
</main>
)
}펼치기 상태만 별도 클라이언트 파일에 둔다.
'use client'
import { useState } from 'react'
export default function Disclosure({ children }) {
const [isOpen, setIsOpen] = useState(false)
function handleToggle() {
console.log('CLIENT_ONLY_DISCLOSURE')
setIsOpen((open) => !open)
}
return (
<section>
<button
aria-expanded={isOpen}
onClick={handleToggle}
>
{isOpen ? '노트 닫기' : '노트 열기'}
</button>
{isOpen ? children : null}
</section>
)
}/notes/immutability를 열면 서버 터미널에 SERVER_ONLY_NOTE_READER: undefined가 기록된다.
브라우저 콘솔에는 이 로그가 없다. 버튼을 누르면 브라우저 콘솔에 CLIENT_ONLY_DISCLOSURE가
기록되고, 새 파일 읽기 요청 없이 이미 전달된 본문이 열리고 닫힌다.
브라우저용 JavaScript에는 Disclosure의 상태와 사건 처리 코드가 필요하지만, node:fs/promises와
NotePage 코드는 포함할 이유가 없다.
서버 컴포넌트는 클라이언트 앱과 다른 환경에서 실행된다
여기서 서버는 반드시 매 요청마다 실행되는 한 대의 웹 서버만 뜻하지 않는다. 정적 노트는 CI 빌드 중 실행될 수 있고 사용자별 화면은 요청 중 실행될 수 있다. 실제 실행 시점, 캐시와 다시 렌더 조건은 React 이름만으로 정해지지 않고 사용하는 프레임워크의 정책을 확인해야 한다.
따라서 NotePage는 Effect에서 두 번째 요청을 시작하지 않고 렌더 중 파일을 읽는다. 파일 시스템,
데이터베이스 client나 서버 비밀값처럼 브라우저에 보낼 필요가 없는 의존성을 서버 환경에 남길 수 있다.
서버 결과와 클라이언트 상호작용을 조합한다
예제에서 Disclosure는 브라우저가 알아야 할 isOpen만 소유한다. 노트 본문은 NotePage가 서버에서
읽고 렌더한 결과다. 시각적으로는 클라이언트 컴포넌트 안에 있지만 NotePage가 Disclosure 모듈에
import된 것이 아니므로 서버 모듈이 클라이언트 모듈 그래프 안으로 이동하지 않는다.
모듈 그래프는 import로 연결된 파일 관계다. 'use client' 파일에서 시작해 import로 이어지는
모듈들은 브라우저에서 실행할 클라이언트 쪽 그래프에 포함된다.
서버 컴포넌트에는 use server를 붙이지 않는다
RSC Payload는 서버 렌더 결과와 클라이언트 자리를 잇는다
브라우저는 첫 HTML로 화면을 빠르게 표시하고, RSC Payload로 서버·클라이언트 트리를 맞춘다. 이후
클라이언트 JavaScript가 Disclosure 같은 클라이언트 컴포넌트에 사건 handler를 연결해 상호작용하게
한다. 이 연결 과정을 hydration, 즉 미리 만든 HTML에 사건 처리를 붙이는 과정이라고 한다.
RSC와 SSR은 서로 다른 질문에 답한다
클라이언트 컴포넌트도 첫 HTML에는 미리 렌더될 수 있다. 하지만 브라우저에서 상태와 사건 처리를 위해 해당 JavaScript를 받아 hydration해야 한다. 서버 컴포넌트는 원래 함수 코드를 브라우저에서 다시 실행하지 않는다. 따라서 “HTML에 보였다”만으로 서버 컴포넌트인지 판단할 수 없다.
실행 결과를 세 증거로 확인한다
- 실행 위치: 첫 로드의
SERVER_ONLY_NOTE_READER는 서버 터미널에만, 버튼을 누른 뒤CLIENT_ONLY_DISCLOSURE는 브라우저 콘솔에만 나타나는가? - 전달 결과: 첫 HTML에는 닫힌 버튼이 보이고 본문은 아직 보이지 않는가? 개발자 도구 Network를
비운 뒤 버튼을 누르면 새 문서·fetch·RSC 요청과 새 서버 로그 없이 본문이 나타나는가? 이는 닫힌
children의 렌더 결과가 초기 RSC 데이터에 이미 전달됐다는 관찰 증거다. RSC 데이터의 실제 전송 형식과 Network 요청 이름은 프레임워크 버전의 구현 세부사항이므로 이 실험의 판정 기준으로 삼지 않는다. - 클라이언트 코드: 프로덕션 빌드 뒤 브라우저로 보내는
.next/static/chunks에서CLIENT_ONLY_DISCLOSURE는 찾을 수 있지만SERVER_ONLY_NOTE_READER와node:fs/promises는 찾을 수 없는가? 예제 프로젝트 루트에서 다음 명령을 실행한다.
pnpm build
rg -n 'CLIENT_ONLY_DISCLOSURE' .next/static/chunks
rg -n 'SERVER_ONLY_NOTE_READER|node:fs/promises' .next/static/chunks첫 번째 검색은 클라이언트 파일 경로를 한 개 이상 출력해야 한다. 두 번째 검색은 아무것도 출력하지 않고 종료 코드 1을 반환해야 성공이다. 출력 경로는 Next.js 버전에 따라 달라질 수 있으므로, 바뀌었다면 해당 버전의 클라이언트 빌드 결과 위치를 확인한다.
버튼을 눌렀다는 사실만으로 서버 컴포넌트가 브라우저에서 실행됐다고 결론 내리지 않는다. 버튼은
Disclosure가 처리하고, 이미 서버에서 만든 노트 결과를 보이거나 숨긴다.
프레임워크 경계를 함께 기록한다
이 글의 예제와 기본값은 Next.js 16.2 App Router 기준이다. 다른 프레임워크는 서버 파일 판정, 데이터 전달 형식과 캐시 정책이 다를 수 있다.
문제를 만났을 때의 조사 순서
- 문제가 난 컴포넌트 모듈을 서버와 클라이언트 중 누가 실행하는지 확인한다.
- 서버 전용 의존성과 브라우저 상호작용 코드가 한 모듈에 섞였는지 확인한다.
- 브라우저가 받은 것이 원래 컴포넌트 코드인지 렌더 결과인지 구분한다.
- 첫 HTML 생성과 클라이언트 hydration을 RSC 실행 위치와 별도로 기록한다.
- React 일반 규칙과 사용하는 프레임워크의 기본값·캐시 정책을 나눈다.
한 번 더 예측하고 확인한다
Disclosure에서'use client'를 제거하면useState와 클릭 처리에 어떤 문제가 생길지 예측한다.NotePage전체에'use client'를 붙이면 async 컴포넌트와node:fs/promisesimport라는 두 위반이 왜 남는지 설명한다.- 버튼을 열고 닫을 때 서버 파일을 다시 읽는지 네트워크와 서버 로그로 확인한다.
- 클라이언트 컴포넌트가 첫 HTML에 보일 수 있어도 브라우저 JavaScript가 필요한 이유를 설명한다.
문제 서버 파일 읽기와 브라우저 펼치기 상태를 한 실행 환경으로 생각함
잘못된 판단 서버에서 HTML이 보이면 모든 컴포넌트가 서버 컴포넌트임
관찰 서버 파일 코드에 state를 두면 실패하고 전체를 client로 바꾸면 node:fs가 경계를 넘음
원인 서로 다른 실행 환경이 필요한 책임을 한 컴포넌트 파일에 둠
행동 서버 데이터 렌더와 작은 클라이언트 상호작용을 한 트리에서 조합
검증 실행 로그, RSC 결과, 브라우저 bundle에서 각 경계 증거 확인PR에서 바로 확인할 항목
- 서버 컴포넌트가 빌드와 요청 중 언제 실행되는지 프레임워크 정책으로 확인했는가?
- 서버 전용 파일·데이터 접근 코드가 클라이언트 모듈 그래프에 들어가지 않는가?
- 상태와 사건 처리는 필요한 작은 클라이언트 컴포넌트에만 있는가?
- RSC Payload, 첫 HTML과 hydration의 역할을 구분했는가?
'use server'를 서버 컴포넌트 표시로 오해하지 않는가?- React 일반 모델과 Next.js 버전별 구현 규칙을 따로 기록했는가?
기억할 질문은 하나다.
이 컴포넌트 함수는 어디에서 실행되고, 브라우저에는 코드와 렌더 결과 중 무엇이 전달되는가?
Active recall
기억에서 꺼내 보기
답을 쓰지 않아도 됩니다. 먼저 머릿속으로 답한 뒤 펼쳐서 정답·이유·흔한 오해를 비교하세요.
01파일에 use server를 적으면 React 서버 컴포넌트가 되는가?
정답
아니다. React에는 서버 컴포넌트를 표시하는 지시문이 없고, use server는 클라이언트에서 호출할 수 있는 서버 함수를 표시한다.
관련 설명 다시 읽기왜 그런가
서버 컴포넌트 분류는 사용하는 프레임워크의 모듈 규칙으로 정해지며 Next.js App Router의 page와 layout은 기본적으로 서버 컴포넌트다.
선택한 상태와 다음 복습일은 이 브라우저에만 저장됩니다.
02서버에서 HTML을 만들면 모두 React 서버 컴포넌트인가?
정답
아니다. SSR은 첫 HTML을 어디서 만드는지, RSC는 어떤 컴포넌트 코드를 어느 환경에서 실행하고 어떤 결과를 전달하는지에 관한 모델이다.
관련 설명 다시 읽기왜 그런가
Next.js 첫 로드는 서버 컴포넌트 결과와 클라이언트 컴포넌트로 HTML도 만들지만 클라이언트 컴포넌트에는 상호작용용 JavaScript가 필요하다.
선택한 상태와 다음 복습일은 이 브라우저에만 저장됩니다.
03트리에 클릭 버튼이 하나 있으면 상위 페이지 전체를 클라이언트 컴포넌트로 바꿔야 하는가?
정답
아니다. 서버 컴포넌트가 렌더한 내용을 작은 클라이언트 컴포넌트의 children으로 전달해 상호작용만 분리할 수 있다.
관련 설명 다시 읽기왜 그런가
서버 데이터 접근과 브라우저 상태를 각각 필요한 환경에 남기면 서버 전용 의존성을 클라이언트 코드에 넣지 않아도 된다.
선택한 상태와 다음 복습일은 이 브라우저에만 저장됩니다.
출처와 검증 범위
아래 날짜는 링크를 마지막으로 열어 본 날입니다. 문서 상단의 검증일은 글의 설명과 적용 범위를 다시 확인한 날입니다.
- Server ComponentsReact · 공식 문서 · 확인 2026-08-09
- use clientReact · 공식 문서 · 확인 2026-08-09
- Server and Client ComponentsNext.js · 공식 문서 · 확인 2026-08-09
- use clientNext.js · 공식 문서 · 확인 2026-08-09
- runtimeNext.js · 공식 문서 · 확인 2026-08-09
- Edge RuntimeNext.js · 공식 문서 · 확인 2026-08-09
- No async Client ComponentNext.js · 공식 문서 · 확인 2026-08-09