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

2026년 3월 5일 수정됨

📋 개요

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 을 보내줄 수 있어요. 영철 님, 이제 구시대의 낡은 패턴은 버릴 때가 됐습니다."

🤔 왜 알아야 하는가

영수(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. 영철이가 회원가입 폼을 개발 중이야. 약관 동의 내용이 담긴 아주 긴 텍스트 컴포넌트 <Terms> 와 동의 체크박스 및 제출 버튼을 가진 <SignUpForm> 컴포넌트가 있어. 여기서 번들 사이즈를 가장 최적화하도록 컴포넌트를 설계하려면 'use client' 는 어디에 선언해야 할까?

정답: <SignUpForm> 컴포넌트에만 'use client' 를 선언하고, <Terms> 는 Server Component 로 유지한다.

💡 상세 해설:

  • 원리 설명: <Terms> 는 단순히 보여주는 텍스트일 뿐이므로 자바스크립트가 필요 없어. 이걸 서버 컴포넌트로 두면 사용자에게 전송되는 자스 번들에서 완전히 빠지게 돼.
  • 오답 피드백: "영철 님, 페이지 전체에 use client 를 붙이면 저 긴 약관 텍스트까지 유저의 데이터 요금을 갉아먹으며 다운로드돼요!"
  • 📌 핵심 기억법: "인터랙션이 없는 텍스트는 서버로!"

Q2. 영호(FE 리드) 가 영철이의 PR 을 보며 코멘트를 남겼다. "영철아, 이거 use client 선언한 파일 안쪽에서 무거운 lodash 같은 유틸을 쓰고 있네? 이러면 의미가 없어." 영호의 지적은 Next.js 번들 관점에서 어떤 의미인가?

정답: 'use client' 파일에서 import 한 도구는 모두 클라이언트 번들에 포함되어 유저가 다운로드해야 한다는 의미이다.

💡 상세 해설:

  • 원리 설명: use client 가 선언되는 순간, 그 하위로 딸려오는 모든 의존성(import) 은 브라우저용 번들로 묶여. 아무리 서버 연산이 빨라도 유저가 다운로드하는 데 시간이 다 가면 소용없지.
  • 베스트 프랙티스: 무거운 연산은 서버 컴포넌트에서 끝내고, 클라이언트에는 오직 직렬화된(Serialized) 순수 데이터 값만 Props 로 넘겨줘.
  • 📌 핵심 기억법: "도구는 서버에서 쓰고, 유저에겐 결과물만 주자."

🐣 영철이의 퇴근 일기

오늘 드디어 RSC 라는 녀석의 실체를 좀 알 것 같아. 처음엔 서버에서 다 해버린다길래 "내 일자리가 위협받는 거 아냐?" 하고 쫄았는데(농담), 알고 보니 내 자바스크립트 다이어트를 도와주는 고마운 친구였어.

💡 오늘의 교훈: "상호작용 없으면 무조건 서버로 밀어라. 유저의 배터리는 소중하니까!"

항상 무뚝뚝해 보이는 영호 리드 님 이지만, 내가 짠 비효율적인 코드를 보고 "유저의 배터리도 비용이다"라고 뼈 때리는 조언을 해주실 땐 정말 시니어의 포스가 느껴져. 나도 언젠가는 영호 님처럼 원리를 꿰뚫는 개발자가 되고 싶다. 오늘은 머리를 너무 많이 썼더니 배가 고프네. 집에 가는 길에 갓 구운 붕어빵이나 사 먹어야지! 🐣


🔗 더 알아보기

🔗 더 알아보기