프롤로그: 2016년 5월, 은밀한 시작
2016년 5월 11일, React 저장소에 조용한 Pull Request가 올라왔습니다.[^134-1]
제목은: "Initial PR for async architecture (Fiber)"
작성자는 Andrew Clark. 하지만 설계자는 따로 있었어요. Sebastian Markbåge.[^135-1]
Sebastian은 누구였을까요? 스웨덴 출신의 개발자로, 90년대부터 웹을 만들어왔어요.[^136-1] 2006년, 클라이언트 사이드 JavaScript가 부활할 때 MooTools 커뮤니티에서 활동했죠. 그곳에서 평생 친구들을 만들었습니다.[^136-1]
2012년경, 그는 Facebook에 입사했어요.[^136-1] Jordan Walke가 React 초기 버전을 개발하고 있을 때였죠. Sebastian은 React를 보자마자 직감했습니다: "이거다."
그는 심리학을 전공했어요.[^137-1] 그래서 그런지 그의 말은 항상 "몇 달 후에야 이해가 되는" 것으로 유명했죠.[^137-1]
하지만 그의 비전은 명확했습니다: "React는 단순히 UI 라이브러리가 아니다. 완전히 새로운 렌더링 모델이다."
1막: Fiber - 쪼갤 수 있는 렌더링 (2016-2017)
문제의 발견
2015-2016년, React는 거대한 문제에 직면했습니다.[^138-1]
JavaScript는 단일 스레드예요. React가 큰 컴포넌트 트리를 렌더링하면? 메인 스레드를 블로킹했죠. 애니메이션이 끊기고, 입력이 먹통이 되고, 사용자 경험이 망가졌습니다.[^138-1]
// 이런 코드가 문제였어요
function BigComponent() {
return (
<div>
{Array.from({ length: 10000 }, (_, i) => (
<ExpensiveChild key={i} />
))}
</div>
)
}
// 10000개를 한 번에 렌더링 → 메인 스레드 블로킹!
JavaScript
복사
Sebastian은 생각했습니다:[^138-1]
브라우저는 60fps를 유지하려면
각 프레임을 16ms 안에 그려야 해요.
렌더링을 16ms 청크로 쪼갤 수 있다면?
더 중요한 작업(사용자 입력)을 먼저 처리하고,
덜 중요한 작업(백그라운드 업데이트)은 나중에?
**우선순위 기반 스케줄링**이 필요해요.
Plain Text
복사
Fiber의 탄생
Fiber는 "렌더링을 중단 가능하게 만드는" 새로운 reconciler였습니다.[^138-1]
핵심 아이디어:[^138-1]
•
렌더링 작업을 작은 단위로 쪼갠다
•
각 단위마다 브라우저에게 제어권을 돌려준다
•
긴급한 작업(클릭 이벤트)이 들어오면 렌더링을 멈추고 먼저 처리한다
•
그 다음 렌더링을 재개한다
2017년 9월, React 16이 릴리즈됐고 Fiber가 들어갔습니다.[^138-1]
사용자들은 몰랐어요. 겉보기엔 똑같았으니까요. 하지만 내부는 완전히 다시 써졌죠.
이건 다음 단계를 위한 토대였습니다.
2막: Suspense와 Concurrent Mode - 기다림의 예술 (2018)
2018년 10월, React Conf
React Conf 2018. 이틀째 아침.[^139-1][^140-1]
Andrew Clark와 Brian Vaughn이 무대에 올랐습니다.
제목: "Concurrent Rendering in React"
Andrew가 말했습니다:[^140-1][^141-1]
"개발자로서 우리는 앱을 빠르게 만들려다가 오히려 사용자 경험을 망치곤 합니다. 코드 스플리팅을 하면? 스피너가 깜빡입니다. 데이터를 페칭하면? 또 스피너. 스피너 지옥이죠."
그리고 데모를 보여줬어요:[^140-1][^141-1]
function ProfilePage() {
return (
<Suspense fallback={<Spinner />}>
<ProfileDetails />
<Suspense fallback={<PostsGlimmer />}>
<Posts />
</Suspense>
</Suspense>
)
}
JavaScript
복사
<Suspense>. 컴포넌트가 데이터를 기다리는 동안 fallback을 보여주는 경계선.[^140-1][^141-1]
하지만 진짜 마법은 Concurrent Mode와 결합될 때였어요:[^141-1]
•
빠른 네트워크? 스피너를 건너뛰고 바로 콘텐츠 표시
•
느린 네트워크? 설정한 threshold(예: 1초) 이상 기다릴 때만 스피너 표시
•
탭 전환? 다음 탭을 미리 렌더링해두고, 사용자가 전환하면 즉시 표시
깜빡이는 스피너 문제가 사라졌습니다.[^140-1][^141-1]
청중은 감탄했어요. 하지만 한 가지가 빠져있었죠.
서버 사이드 렌더링은?
3막: BigPipe의 귀환 - Fizz (2018-2021)
잊혀지지 않은 아이디어
Sebastian Markbåge는 Facebook에서 일했어요. 그는 BigPipe를 알고 있었죠.[^142-1]
2010년, Changhao Jiang이 만든 그 기술. HTML을 스트리밍하는 방법.[^142-1]
클라이언트가 요청
→ 서버가 페이지의 위쪽부터 HTML을 만들며 보냄
→ 느린 부분은 placeholder
→ 나중에 준비되면 그 부분의 HTML을 추가로 보냄
→ 작은 script로 placeholder를 교체
Plain Text
복사
Sebastian은 생각했습니다: "이걸 React로 할 수 있지 않을까?"[^143-1]
2018년, 코드명 Fizz
React 팀은 조용히 새 프로젝트를 시작했어요.[^143-1]
코드명: Fizz
목표: BigPipe + React + Suspense = Streaming SSR
기존의 renderToString은 동기적이었어요:[^143-1]
const html = renderToString(<App />)
// 전체 트리가 렌더링될 때까지 기다림
JavaScript
복사
Fizz는 비동기적이었죠:[^143-1][^144-1]
const { pipe } = renderToPipeableStream(<App />, {
onShellReady() {
// 위쪽(shell)이 준비되면 스트리밍 시작
res.setHeader('content-type', 'text/html')
pipe(res)
}
})
JavaScript
복사
Suspense + Streaming의 마법
function App() {
return (
<html>
<body>
<Header /> {/* 빠름 */}
<Suspense fallback={<Spinner />}>
<SlowData /> {/* 느림 */}
</Suspense>
<Footer /> {/* 빠름 */}
</body>
</html>
)
}
JavaScript
복사
무슨 일이 일어날까요?[^144-1][^145-1]
1. 서버가 Header를 렌더링 → 즉시 클라이언트로 전송
2. SlowData를 기다리는 동안 Spinner HTML 전송
3. Footer도 렌더링 → 전송
4. 사용자는 Header와 Footer를 바로 볼 수 있음!
5. SlowData 준비됨 → HTML + 작은 script 전송
6. Script가 Spinner를 실제 데이터로 교체
Plain Text
복사
BigPipe가 React로 돌아왔습니다.[^144-1][^145-1]
2021년, React 18
2021년 6월, React 18 beta가 발표됐습니다.[^146-1]
새로운 API:[^146-1]
•
renderToPipeableStream (Node.js용)
•
renderToReadableStream (Web Streams용)
•
Selective Hydration
Selective Hydration이 특히 혁신적이었어요:[^146-1]
기존: 전체 페이지를 hydrate한 후에야 인터랙티브
새로운 방식: 일부만 hydrate되어도 그 부분은 인터랙티브
사용자가 버튼을 클릭하면? React가 그 부분을 우선 hydrate합니다.[^146-1]
하지만... 여전히 한계가 있었어요.
4막: 근본적인 질문 - Server Components (2019-2020)
2019년, Sebastian의 질문
Sebastian Markbåge가 React 팀에 질문을 던졌습니다:[^147-1][^148-1]
"우리는 2013년부터 'isomorphic'을 외쳤습니다. 같은 코드를 서버와 클라이언트에서 돌리자고요. 하지만... 이게 정말 맞는 방향일까요?"
문제는 명확했어요:[^147-1][^148-1]
// Next.js에서 이렇게 해야 했죠
export async function getServerSideProps() {
const db = require('database')
const posts = await db.query('SELECT * FROM posts')
return { props: { posts } }
}
function Page({ posts }) {
// 컴포넌트는 데이터만 받음
// 데이터 페칭 로직과 분리됨
return <PostList posts={posts} />
}
JavaScript
복사
왜 컴포넌트 안에서 직접 못할까요?[^147-1]
// 이렇게 하고 싶었어요
async function Page() {
const db = require('database') // ❌ 클라이언트에서 실행 불가!
const posts = await db.query('SELECT * FROM posts')
return <PostList posts={posts} />
}
JavaScript
복사
패러다임의 전환
Sebastian과 Andrew Clark, Dan Abramov, Joe Savona는 논의했습니다.[^147-1][^148-1]
"같은 코드를 양쪽에서 돌릴 필요가 없다면?"[^147-1]
컴포넌트를 두 종류로 나누는 거예요:[^147-1][^148-1]
Server Components:
•
서버에서만 실행
•
데이터베이스 직접 접근 가능
•
큰 라이브러리 사용해도 번들 크기 증가 없음
•
코드가 클라이언트로 전송되지 않음
Client Components:
•
클라이언트에서 실행
•
인터랙티브
•
기존 React 컴포넌트와 동일
그런데 어떻게 이 둘을 섞을까요?
Flight - 가상 DOM의 직렬화
핵심 아이디어:[^147-1][^148-1]
1. 서버에서 Server Component를 렌더링
2. 가상 DOM을 직렬화 (JSON-like 형식)
3. 네트워크로 전송
4. 클라이언트가 받아서 실제 DOM으로 변환
Plain Text
복사
이것이 Flight였습니다. React Server Components의 transport layer.[^147-1]
예시:[^148-1]
// Note.server.js
import db from 'database' // ✅ 서버에서만 실행
export default async function Note({ id }) {
const note = await db.notes.get(id)
return (
<div>
<h1>{note.title}</h1>
<p>{note.body}</p>
<NoteEditor note={note} /> {/* Client Component */}
</div>
)
}
// NoteEditor.client.js
'use client' // Client Component 표시
export default function NoteEditor({ note }) {
const [text, setText] = useState(note.body)
return (
<textarea
value={text}
onChange={(e) => setText(e.target.value)}
/>
)
}
JavaScript
복사
서버가 보내는 데이터:[^147-1][^148-1]
{
"type": "div",
"children": [
{ "type": "h1", "children": "Meeting Notes" },
{ "type": "p", "children": "Discuss Q4 plans..." },
{
"type": "NoteEditor",
"props": { "note": { "body": "..." } }
// NoteEditor는 Client Component이므로 클라이언트에서 hydrate
}
]
}
JSON
복사
2020년 12월 21일, 공개
크리스마스 전.[^149-1]
Dan Abramov, Lauren Tan, Joseph Savona, Sebastian Markbåge가 글을 올렸습니다.[^149-1]
제목: "Introducing Zero-Bundle-Size React Server Components"
Server Components는 아직 연구 개발 중입니다.
투명성의 정신으로 공유합니다.
서둘러 배울 필요 없어요!
Plain Text
복사
영상과 데모, RFC를 공개했죠.[^149-1]
커뮤니티의 반응
혼란:[^149-1][^150-1]
"Server Components vs SSR?"
"Client Component는 기존 컴포넌트?"
"어떻게 섞어?"
".server.js 파일?"
Dan Abramov는 Twitter에서 몇 달간 질문에 답했습니다.[^150-1]
핵심 정리:[^150-1][^151-1]
•
Server Components ≠ SSR (보완 관계)
•
SSR: HTML을 빨리 보여줌
•
Server Components: JavaScript 번들 크기 줄임
•
함께 쓸 수 있음
하지만 실제로 쓸 수 있는 건 아직 멀었어요.
5막: 통합 - Next.js 13 App Router (2021-2023)
Vercel과의 협업
Vercel 팀(Shu Ding, Jiachi Liu, Tobias Koppers, Tim Neutkens)이 참여했습니다.[^147-1]
그들은 React Server Components를 프로덕션에 통합하는 첫 번째 프레임워크를 만들기로 했죠.
2년간의 작업.[^147-1]
왜 이렇게 오래 걸렸을까요?
번들러 통합이 악몽이었거든요.[^147-1]
Server Component에서 Client Component import → 번들에 포함
Client Component에서 Server Component import → 안 됨!
파일 확장자로 구분? (.server.js, .client.js)
→ 개발자들이 싫어함
새로운 방법: 'use client' directive
Plain Text
복사
2022년 10월 25일, Next.js Conf
6년 전 Next.js를 발표했던 바로 그날.[^152-1]
Vercel이 발표했습니다: Next.js 13
App Router - React Server Components 기반의 새로운 라우팅 시스템.[^152-1]
// app/page.js (Server Component by default!)
import db from '@/lib/db'
export default async function Page() {
// ✅ 컴포넌트 안에서 직접 데이터 페칭!
const posts = await db.query('SELECT * FROM posts')
return (
<div>
{posts.map(post => (
<PostCard key={post.id} post={post} />
))}
</div>
)
}
// app/post/[id]/page.js
async function PostPage({ params }) {
const post = await db.posts.get(params.id)
return (
<article>
<h1>{post.title}</h1>
<LikeButton postId={post.id} /> {/* Client Component */}
</article>
)
}
JavaScript
복사
드디어. 2013년부터 원했던 게 가능해졌습니다.
2023년, 혼란과 분열
하지만 커뮤니티는 분열됐어요.
찬성파:
•
"드디어 제대로 된 SSR이다"
•
"번들 크기가 극적으로 줄었다"
•
"PHP로 회귀? 아니, 더 나은 형태로 진화다"
반대파:[^153-1]
•
"너무 복잡하다"
•
"Server/Client 구분이 헷갈린다"
•
"use client를 언제 써야 하나?"
•
"디버깅이 어렵다"
다른 프레임워크들의 반응:
Remix (Ryan Florence, Michael Jackson):[^153-1]
•
"우리는 웹 표준에 충실하겠다"
•
"Form, loader, action - 브라우저가 이미 하는 방식"
•
React Server Components는 지원 안 함 (2023년 기준)
SvelteKit, SolidStart:
•
각자의 서버 컴포넌트 모델 개발
React Router v7 (2024):[^153-1]
•
Single Fetch 패턴
•
RSC 지원하지 않기로 결정
Sebastian Markbåge, Vercel로
2023년 6월, 놀라운 소식.[^154-1]
Sebastian Markbåge가 Vercel에 입사했습니다.
하지만 여전히 React Core Team의 리더십을 유지하기로 했죠.[^154-1]
이건 큰 신호였어요: Vercel과 React의 긴밀한 협력.
에필로그: 끝나지 않은 여정
2024-2025년 현재.
React Server Components는 여전히 논란적입니다.
해결된 것:[^147-1][^148-1][^149-1]
•
renderToString의 블로킹 → renderToPipeableStream으로 해결
•
Isomorphic의 제약 → Server Components로 깨짐
•
BigPipe의 아이디어 → Suspense + Streaming으로 부활
•
번들 크기 문제 → Server Components로 해결
여전히 어려운 것:
•
높은 학습 곡선
•
복잡한 정신 모델
•
디버깅의 어려움
•
생태계 분열
역사의 교훈
2013년, React는 "같은 코드를 양쪽에서"라고 말했어요.[^155-1]
2023년, React는 "다른 코드를 다른 곳에서"라고 말합니다.[^147-1][^148-1]
둘 다 맞았을까요? 아니면 둘 다 틀렸을까요?
아마도 답은: 시대마다 다른 정답이 있다는 것.
2010년: BigPipe (HTML 스트리밍)
2013년: Isomorphic (같은 코드)
2018년: Fizz (React 스트리밍)
2020년: Flight (서버 컴포넌트)
2023년: App Router (전부 통합)
각 단계는 이전 단계의 한계를 극복하려는 시도였습니다.
그리고 모든 혁신의 중심에는 한 사람이 있었어요.
Sebastian Markbåge.
그는 심리학을 전공했죠.[^137-1] 그래서 그런지 그의 질문은 항상 근본적이었습니다:
"왜 우리는 이렇게 하고 있지?"
"더 나은 방법은 없을까?"
"사용자 경험을 진짜로 개선하려면?"
BigPipe → Fiber → Suspense → Fizz → Flight
이 모든 여정은 하나의 질문에서 시작됐습니다:
"어떻게 하면 웹을 더 빠르게 만들 수 있을까?"
다음 장 예고:
2023-2024년, 개발자들은 혼란 속에 있었습니다.
"Server Components를 써야 하나?"
"Remix가 맞나?"
"아니면 SvelteKit?"
"또는 그냥 MPA(Multi-Page App)로 돌아갈까?"
SSR의 미래는 어디로 가고 있을까요?
그리고 우리는 정말로 "PHP로 회귀"하고 있는 걸까요?
아니면 나선형으로 올라가고 있는 걸까요?
참고 문헌 표기:
[^134-1]: Fatih's Blog, "The Suspense is Killing Me: Part 1" - React Fiber timeline
[^135-1]: React Blog, "React Conf recap: Hooks, Suspense, and Concurrent Rendering" (2018)
[^136-1]: Patrick Aljord, "ReactEurope interview #17: Sebastian Markbåge", Medium (2015)
[^137-1]: React Team page, "Meet the Team - Sebastian Markbåge"
[^138-1]: Fatih's Blog, "The Suspense is Killing Me: Part 1" - Fiber and time-slicing
[^139-1]: React Blog, "React Conf recap: Hooks, Suspense, and Concurrent Rendering" (2018)
[^140-1]: FreeCodeCamp, "Lessons Learned at React Conf 2018" (2019)
[^141-1]: Auth0 Blog, "Beyond React 16: Time Slicing and Suspense API"
[^142-1]: Facebook Engineering, "BigPipe: Pipelining web pages for high performance" (2010)
[^143-1]: GitHub reactwg/react-18, "Upgrading to React 18 on the server" (#22)
[^144-1]: LogRocket Blog, "A guide to streaming SSR with React 18" (2024)
[^145-1]: Prateek Surana, "The future of rendering in React"
[^146-1]: React Documentation, "renderToPipeableStream"
[^147-1]: React RFC, "Server Components" (#188)
[^148-1]: React Blog, "Introducing Zero-Bundle-Size React Server Components" (2020)
[^149-1]: React Blog, "Introducing Zero-Bundle-Size React Server Components" (2020)
[^150-1]: FreeCodeCamp, "What are React Server Components?" (2021)
[^151-1]: Patterns.dev, "React Server Components"
[^152-1]: Next.js 13 announcement (2022)
[^153-1]: Various community responses on Twitter, Reddit, and Hacker News (2023-2024)
[^154-1]: Vercel Blog, "Supporting the Future of React" (2023)
[^155-1]: Airbnb Rendr announcement (2013)
