⚛️ Next.js 1장: React 멘탈 모델에서 RSC로의 진화
📋 개요
React 멘탈 모델에서 RSC로의 진화 — Server Component가 왜, 어떻게 다른지 이해합니다.
📋 목차
- 📌 이 문서를 읽기 전에
- 🤔 왜 알아야 하는가
- 🏗️ 비유로 먼저 이해하기
- 🧩 왜 Next.js이고, 왜 RSC인가? 🟢
- ⚖️ Client Component vs Server Component 🟢
- 🧪 따라해보기: 가장 단순한 RSC 눈으로 확인하기
- 💥 에러 해결 카탈로그
- 🏁 이번에 배운 내용 총정리
- 📝 마무리 퀴즈
- 🔗 더 알아보기
📌 이 문서를 읽기 전에
⏱️ 예상 읽기 시간: 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) 할당 불가 window나document객체 접근 불가
🔗 연결 고리
이 제약 사항들을 해결하는 방법이 바로 다음에 배울 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 라는 녀석의 실체를 좀 알 것 같아. 처음엔 서버에서 다 해버린다길래 "내 일자리가 위협받는 거 아냐?" 하고 쫄았는데(농담), 알고 보니 내 자바스크립트 다이어트를 도와주는 고마운 친구였어.
💡 오늘의 교훈: "상호작용 없으면 무조건 서버로 밀어라. 유저의 배터리는 소중하니까!"
항상 무뚝뚝해 보이는 영호 리드 님 이지만, 내가 짠 비효율적인 코드를 보고 "유저의 배터리도 비용이다"라고 뼈 때리는 조언을 해주실 땐 정말 시니어의 포스가 느껴져. 나도 언젠가는 영호 님처럼 원리를 꿰뚫는 개발자가 되고 싶다. 오늘은 머리를 너무 많이 썼더니 배가 고프네. 집에 가는 길에 갓 구운 붕어빵이나 사 먹어야지! 🐣