⚛️ Next.js 1장: React 멘탈 모델에서 RSC로의 진화

2026년 4월 30일 수정됨

📋 개요

React 멘탈 모델에서 RSC로의 진화 — Server Component가 왜, 어떻게 다른지 이해합니다.

📋 목차


📌 이 문서를 읽기 전에

⏱️ 예상 읽기 시간: 15분(전체) / 핵심 파트만: 7분

🗺️ 이 문서의 흐름
기존 리액트의 한계 → RSC 도입 배경 → 서버 기반 렌더링 원리 → 두 컴포넌트 간 경계 구분 ( 영수네 커뮤니티 예제 )

🎯 이 문서를 다 읽으면 할 수 있는 것

  • 기존 React 앱과 Next.js App Router 앱의 근본적인 차이를 시니어의 언어로 설명할 수 있다
  • RSC ( React Server Component ) 가 왜 필요한지 명확히 알고, 상황에 맞게 적용할 수 있다.
  • 자바스크립트 번들 사이즈를 극단적으로 줄이는 설계 방식을 실무에 도입할 수 있다.

🗺️ 이 문서의 배경 세계관: '영수네 커뮤니티'

  • 🐣 영철 ( 신입 ): "리드 님! 방금 스터디 첫 화면 완성했는데, 화면이 뜰 때마다 1초 정도 하얗게 비었다가 스피너가 돌아요. 원래 리액트는 다 이런 건가요?"
  • 🦁 영호 ( 리드 ): "영철 님, 그건 전형적인 클라이언트 페칭 ( Client Fetching ) 의 현상이에요. 자바스크립트가 브라우저에 다 내려갈 때까지 유저는 구경만 해야 하거든요."
  • 👔 영수 ( PM ): "영철 씨, 유저들은 1초도 못 기다려요. 이탈률이 벌써 걱정되는데요?"
  • 🎨 영숙 ( UX ): "맞아요, 영철 씨. 화면이 덜컹거리는 느낌( Layout Shift )도 너무 심해요. 좀 더 우아하게 보여줄 순 없나요?"
  • 🦁 영호 ( 리드 ): "자, 다들 진정하시고! Next.js 의 RSC 를 쓰면 브라우저에 가기도 전에 서버에서 데이터를 꽉꽉 채운 HTML 을 보내줄 수 있어요. 영철 님, 이제 이전 SPA 중심 패턴에서 한 단계 넘어갈 때입니다."

🤔 왜 알아야 하는가

영수(PM/BE) 와 영호(프론트 리드), 그리고 갓 입사한 주니어 영철이는 "개발자 스터디 매칭 커뮤티니" 를 만들고 있어.

어느 날 영철이가 "스터디 첫 화면" 을 React 로 열심히 구현했어. 그런데 화면이 뜰 때마다 하얀 화면이 1초 뜨고 나서 스피너가 돌더니 데이터가 나오네? 영수는 "초기 로딩이 너무 느려서 유저들이 다 이탈하겠어요!" 라고 불만을 표했지. 영철이는 useEffect 안에서 API 데이터를 불러오고 있었거든 ( 전형적인 클라이언트 페칭 ).

React 로 개발하다 보면 컴포넌트 안에 useEffect 를 깔고 API 요청을 보내는 게 당연하게 느껴져. 하지만 JS 번들 사이즈는 기하급수적으로 늘어나고, 사용자의 스마트폰이 그 거대한 JS 를 다운받고 파싱해서 실행할 때까지 화면은 하얗게 비어있어.

이 문제를 해결하기 위해 도입된 Next.js 의 핵심 무기, RSC ( React Server Component ) 의 멘탈 모델로 전환하지 않으면, Next.js 위에서도 리액트 시절의 낡은 패턴만 반복하게 될 거야.


🏗️ 비유로 먼저 이해하기

🤔 잠깐, 먼저 생각해봐
우리가 웹사이트를 보는 과정을 '음식 배달'에 비유한다면, 지금까지의 리액트는 어떤 방식이었을까?

📦 이케아 가구 조립 비유

기본 리액트(CSR)는 가구 도면과 조립 도구(JS 묶음), 그리고 가공되지 않은 나무 판자 를 우리 집(브라우저)에 다 보내주는 거야. 우리가 집에서 직접 땀 흘리며 조립(렌더링)해야 비로소 가구가 보이기 시작하지.

반면, Next.js 의 서버 컴포넌트 ( RSC )공장 ( 서버 ) 에서 완벽하게 완성된 가구 를 통째로 보내주는 방식이야. 우리는 그냥 제 자리에 두기만 하면 돼!

🧒 5살에게 설명한다면?
"기존 리액트는 레고 블록이랑 설명서를 집으로 보내줘서 우리가 직접 맞춰야 했어. 하지만 서버 컴포넌트는 삼촌이 미리 다 맞춰놓은 멋진 성을 통째로 선물해주는 거야. 우린 그냥 받아서 놀기만 하면 돼!"

💡 한 줄로 기억하기
CSR 은 재료 배달, RSC 는 완성품 배달이다.


🧩 왜 Next.js이고, 왜 RSC인가? 🟢

🎯 이 섹션을 읽고 나면:

  • 단순히 "빠르다" 가 아니라, 설계 관점에서 RSC 가 가져다준 패러다임 변화를 설명할 수 있다.
  • 번들 사이즈 최적화의 원리를 완전히 통제할 수 있다.

이게 뭔가?

RSC(React Server Component) 란 오로지 서버 환경에서만 렌더링되고, 클라이언트(브라우저) 로는 아예 자바스크립트 코드가 전송되지 않는 컴포넌트야.

📖 용어: RSC — React Server Component . 리액트의 핵심 렌더링 엔진이 서버로 옮겨간 혁신적인 기술.

왜 이게 따로 존재하는가?

이게 없었다면? ( 기존 React 의 한계 )
영철이가 스터디 모집글 날짜를 이쁘게 보여주기 위해 무거운 무거운 라이브러리( moment.js 등 ) 를 썼다고 치자. 기존 리액트에서는 사용자가 그 무거운 모듈 수백 KB 를 무리하게 넘겨받아야 했어. 화면 하나 그리는데 데이터보다 도구가 더 무거운 배보다 배꼽이 큰 상황이었지.

RSC 가 생긴 뒤에는?
가장 극적인 차이점은 데이터 페칭과 라이브러리 연산 전체가 브라우저에 도달하기도 전에 서버에서 종결 된다는 거야. 결과물인 HTML 만 전달되기 때문에 사용자 기기의 부담이 제로(0) 에 수렴해.

// app/studies/page.tsx
// 🦁 영호: "영철 님, 이 코드는 브라우저에서 단 1바이트의 JS도 쓰지 않아요."
 
import db from '@/lib/db'            // 👔 영수: "서버 DB 커넥터를 직접 부를 수 있으니 편하네요."
import { format } from 'date-fns'    // 🦁 영호: "아무리 무거운 라이브러리를 써도 클라이언트는 몰라요."
 
export default async function StudiesPage() {
  // 🦁 영호: "컴포넌트 자체가 async라니, 정말 우아하지 않나요?"
  const studies = await db.study.findMany()
 
  return (
    <main>
      <h1>스터디 모집글 목록</h1>
      <ul>
        {studies.map(study => (
          <li key={study.id}>
            {study.title} - {format(study.createdAt, 'yyyy-MM-dd')}
          </li>
        ))}
      </ul>
    </main>
  )
}

💡 한 줄로 기억하기
RSC 는 서버에서 도는 비밀 요원이야. 결과물만 브라우저에 던져주고 흔적도 없이 사라져.

여기서 쓰면 안 되는 것

  • useState, useEffect 불가 (서버에는 브라우저 메모리 상태가 없음)
  • 브라우저 이벤트(onClick) 할당 불가
  • windowdocument 객체 접근 불가

🔗 연결 고리
이 제약 사항들을 해결하는 방법이 바로 다음에 배울 Client Component 야.


⚖️ Client Component vs Server Component 🟢

🎯 이 섹션을 읽고 나면:

  • 두 컴포넌트의 차이를 명확히 구분하고, 언제 무엇을 쓸지 결정할 수 있다.
  • Leaf Pattern 을 활용해 번들 사이즈를 최소화하는 설계를 할 수 있다.

영철이가 RSC 의 맛을 알더니 모든 것을 서버에서 하려고 해. 하지만 "좋아요" 버튼을 만들려고 <button onClick={...}> 을 박는 순간 터미널에 시뻘건 에러가 났지. 영호( FE 리드 ) 가 다가와서 웃으며 말했어.

"영철아, 상호작용은 결국 클라이언트에서 일어나야 해. use client 를 쓸 시간이야."

어떻게 다른가?

비교Server Component(RSC)Client Component(use client)
기본 상태App Router 의 기본값 ( Default )최상단에 "use client" 명시 필요
JS 번들 크기+0 KB ( 결과물 HTML 만 내려감 )코드가 그대로 번들에 포함됨
데이터 페칭async/await 직접 호출useEffect, 커스텀 훅 등
제약 조건이벤트 리스너 불가민감한 정보(API 키) 노출 위험

실무에서는 어떻게 분리해야 하는가? ( Leaf Pattern 체화 )

"상호작용이 필요한 나뭇잎(Leaf) 끝자락 부분만 밀어내라." 이것이 시니어들의 멘탈 모델이자 가장 우아한 패턴이야.

순진한 코드 ( Naive Approach ) - 영철이의 책임 전가
영철이는 에러가 나자 귀찮아서 페이지 전체 최상단에 "use client" 를 추가했어.

// app/studies/page.tsx
'use client' // 🐣 영철: "아, 에러나니까 일단 'use client' 박고 볼게요!"
             // 🦁 영호: "영철 님, 이러면 하위의 모든 정적 UI까지 JS 번들로 말려들어갑니다!"
 
import StudyDesc from './StudyDesc'
import LikeButton from './LikeButton'
 
export default function StudiesPage() {
  return (
    <div>
      <StudyDesc /> {/* 🦁 영호: "텍스트 덩어리인데 클라이언트 번들에 잡혀갑니다 (장애)" */}
      <LikeButton />
    </div>
  )
}

우아한 코드 ( Pro Approach ) - 영호의 리팩토링
영호(FE 리드) 는 LikeButton.tsx 만 따로 파일로 빼서, 그 안에만 "use client" 를 선언하게 했어.

// app/studies/LikeButton.tsx
'use client' // 🦁 영호: "딱 상호작용이 필요한 이 부분만 클라이언트로 보냅니다."
import { useState } from 'react'
 
export default function LikeButton() {
  const [likes, setLikes] = useState(0)
  return <button onClick={() => setLikes(l => l + 1)}>👍 {likes}</button>
}
// app/studies/page.tsx
// 🦁 영호: "부모는 계속 순수한 Server Component로 남겨서 최적화하세요."
import StudyDesc from './StudyDesc'
import LikeButton from './LikeButton' // Leaf 부분만 Client Component
 
export default function StudiesPage() {
  return (
    <div>
      <StudyDesc />
      <LikeButton />
    </div>
  )
}

💡 한 줄로 기억하기
대들보와 기둥은 튼튼한 RSC 로 세워두고, 불이 켜져야 하는 전구(나뭇잎) 부분만 클라이언트 컴포넌트로 달아준다.


🧪 따라해보기: 가장 단순한 RSC 눈으로 확인하기 🟢

🎯 이 섹션을 읽고 나면:

  • 서버 컴포넌트 로그가 찍히는 위치를 통해 실행 환경의 차이를 오감으로 느낄 수 있다.

실제로 서버 컴포넌트가 브라우저 콘솔을 남기지 않고, 서버에서만 도는 걸 확인해보자.

// app/sandbox/page.tsx
 
export default function SandboxPage() {
  // 🦁 영호: "이 로그는 브라우저 F12 콘솔에 찍힐까요?"
  console.log("🔥 이 로그는 과연 어디서 찍힐까요?");
 
  return (
    <div>
      <h1>영수네 커뮤니티 샌드박스</h1>
    </div>
  )
}

🔍 코드 한 줄씩 뜯어보기:

부분의미
console.log일반적인 출력 함수지만, RSC 에서는 터미널(서버) 에만 찍혀.
SandboxPage"use client" 가 없으므로 기본적으로 서버에서만 렌더링돼.

🔍 따라해보기 결과 분석:
브라우저에는 절대 안 찍히고 터미널 창(서버를 돌리는 Node.js 환경) 에만 로그가 남아. 이 단순한 사실이 왜 SSR / RSC 환경에서 window is not defined 가 발생하는지를 완벽히 대변해.

✅ 실습 후 체크리스트

  • 서버 컴포넌트 로그가 브라우저에 찍히지 않는 이유를 설명할 수 있다
  • 렌더링 트리의 하단(Leaf) 에 있는 컴포넌트만 use client 로 밀어내는 패턴의 이유를 안다.

💥 에러 해결 카탈로그

에러 메시지가 뜨면 Ctrl+F로 검색해봐.

Event handlers cannot be passed to Client Component props...

원인: 서버 컴포넌트에서 클릭(onClick)을 달아서 에러남.
해결책: 클릭이 필요한 그 컴포넌트 파일 단위만 쪼개어 최상단에 'use client'를 선언한다.

window is not defined

원인: 서버 측에서 페이지를 렌더링하고 있는데 브라우저 객체(window, localStorage)를 참조함.
해결책: 컴포넌트를 클라이언트로 만들고 useEffect 내부에서 접근하거나 조건을 걸어 사용한다.


🏁 이번에 배운 내용 총정리

오늘 배운 핵심을 한눈에 정리해볼까? 실무에서 길을 잃었을 때 이것만 봐도 돼.

📋 핵심 모델 차이

| HTML 렌더링 | 빈 div 받고 폰에서 자스로 채움 | 서버에서 HTML 꽉꽉 채워서 내려줌 |
| JS 번들 크기 | 라이브러리 추가할수록 비대해짐 | 서버 전용 라이브러리는 0KB 달성 |
| 데이터 페칭 | 화면 돌리고 나서야 useEffect 호출 | HTML 그릴 때 이미 데이터를 들고 옴 |

⚠️ 절대 하지 말 것

상황❌ 나쁜 예✅ 좋은 예
에러 해결귀찮다고 최상단에 "use client" 박기말단 컴포넌트(Leaf) 만 클라이언트로 분리
라이브러리 사용클라이언트 컴포넌트에서 무거운 유틸(lodash 등) 사용서버 컴포넌트에서 연산 후 데이터만 넘기기
객체 접근서버 컴포넌트에서 window, localStorage 호출useEffect 안에서 접근하거나 클라이언트로 위임


📝 마무리 퀴즈

Q1. 서버 컴포넌트가 브라우저 번들 크기를 줄일 수 있는 이유는?

정답: 컴포넌트 코드가 서버에서 실행되고 브라우저에는 렌더링 결과만 전달되기 때문이다.

💡 상세 해설: DB 조회나 서버 전용 라이브러리를 쓰는 목록 컴포넌트는 브라우저 JS로 내려갈 필요가 없다. 반대로 클릭, 상태, 브라우저 API가 필요한 버튼은 클라이언트 컴포넌트가 되어야 한다.


Q2. 서버 컴포넌트 안에서 onClick을 바로 쓰면 왜 문제가 될까?

정답: onClick은 브라우저 이벤트이므로 서버에서 실행되는 컴포넌트에는 연결할 수 없다.

💡 상세 해설: 서버 컴포넌트는 HTML/RSC Payload를 만드는 쪽이고, 이벤트 핸들러는 하이드레이션된 클라이언트 코드가 필요하다. 상호작용이 필요한 가장 작은 잎사귀만 클라이언트로 분리해야 한다.


Q3. 영철이의 테스트 타임: 게시글 목록은 DB에서 읽고 좋아요 버튼은 클릭되어야 한다. 어떻게 나누는 게 좋을까?

정답: 목록과 데이터 조회는 서버 컴포넌트에 두고, 좋아요 버튼만 클라이언트 컴포넌트로 분리한다.

💡 상세 해설: 이렇게 하면 서버 전용 데이터 접근을 브라우저에 보내지 않고, 필요한 상호작용 비용만 지불한다. 영철은 이제 컴포넌트를 화면 모양이 아니라 실행 위치 기준으로 나누기 시작했다.

🐣 영철이의 퇴근 일기

오늘은 컴포넌트를 화면 조각이 아니라 실행 위치가 다른 조각으로 보기 시작했다.

💡 "서버 컴포넌트는 브라우저로 코드를 보내지 않는 선택이고, 클라이언트 컴포넌트는 상호작용 비용을 감수하는 선택이다."

다음 PR에서는 use client를 붙이기 전에 정말 이벤트와 상태가 필요한지 먼저 확인하겠다.

🔗 더 알아보기

🔗 더 알아보기