02. React 탄생 이야기
02. React 탄생 이야기
이 문서에서 배우는 것
- 2011년 Facebook이 겪었던 실제 기술적 문제
- "읽지 않은 메시지" 버그가 왜 구조적으로 해결 불가능했는지
- Jordan Walke가 PHP(XHP)에서 어떤 영감을 얻었는지
- FaxJS에서 React로 발전한 과정
- React가 오픈소스로 공개된 후 초기 반응과 확산 과정
PM의 요구사항 (2011년, 실화)
"채팅 알림 뱃지에 읽지 않은 메시지가 1개라고 뜨는데, 클릭해서 다 읽어도 사라지지 않아요. 고쳐주세요."
이 한 줄의 버그 리포트가, 결국 React를 탄생시켰습니다.
2011년 Facebook 기술 스택
당시 Facebook의 기술 스택은 두 세계로 나뉘어 있었습니다:
┌──────────────────────────────────────────────────┐
│ Facebook (2011) │
├──────────────────────┬───────────────────────────┤
│ 서버 사이드 │ 클라이언트 사이드 │
│ │ │
│ PHP + XHP │ 순수 JavaScript (Bolt) │
│ │ │
│ • 서버에서 HTML 생성 │ • 브라우저에서 DOM 조작 │
│ • 컴포넌트 개념 있음 │ • 명령형 코드 │
│ • 데이터→HTML 자동 │ • 수동 DOM 업데이트 │
│ • 매 요청마다 새로 생성│ • 상태 관리는 개발자 몫 │
└──────────────────────┴───────────────────────────┘XHP — PHP 쪽의 컴포넌트 시스템
Facebook은 PHP에 XHP라는 확장을 만들어 쓰고 있었습니다:
// XHP — PHP 안에서 HTML을 컴포넌트처럼 사용
class :ui:badge extends :x:element {
attribute int count @required;
protected function render() {
$count = $this->getAttribute('count');
if ($count === 0) {
return <x:frag />; // 0이면 아무것도 안 보임
}
return <span class="badge">{$count}</span>;
}
}
// 사용
<ui:badge count={$unreadCount} />XHP의 장점: 데이터($unreadCount)가 바뀌면 다음 요청에서 HTML을 처음부터 다시 만들기 때문에, 동기화 문제가 없었습니다.
Bolt — 클라이언트 쪽의 JavaScript
브라우저에서의 인터랙션은 Bolt라는 내부 프레임워크로 처리했습니다:
// Bolt — 명령형 DOM 조작
var ChatBadge = {
update: function(unreadCount) {
var badge = document.getElementById('chat-badge');
if (unreadCount > 0) {
badge.style.display = 'inline';
badge.textContent = unreadCount;
} else {
badge.style.display = 'none';
}
}
};
// 메시지 읽음 처리 시
MessageStore.onRead(function(messageId) {
var count = MessageStore.getUnreadCount();
ChatBadge.update(count); // ← 직접 업데이트
HeaderNotification.update(count); // ← 여기도
TabTitle.update(count); // ← 여기도
// ... 더 있을 수 있음
});유명한 버그: 사라지지 않는 알림 뱃지
문제의 화면
┌────────────────────────────────────────────────────┐
│ Facebook [🔍 검색] [홈] [👤] [💬1] [🔔3] [▼] │
│ ↑ │
│ 이 "1"이 사라지지 않음 │
├────────────────────────────────────────────────────┤
│ │
│ ┌─ 채팅 창 ──────────┐ │
│ │ John: Hey! │ ← 이미 읽었는데 │
│ │ (이미 읽음 상태) │ 뱃지는 여전히 "1" │
│ └────────────────────┘ │
│ │
└────────────────────────────────────────────────────┘왜 이 버그가 발생했을까?
메시지 수신 이벤트 발생 시 업데이트해야 하는 곳:
① 채팅 뱃지 숫자 → Chat 팀 담당
② 상단 알림 아이콘 → Notifications 팀 담당
③ 브라우저 탭 타이틀 → Platform 팀 담당
④ 채팅 목록의 읽지 않음 표시 → Chat 팀 담당
⑤ 채팅 창 내부 메시지 상태 → Chat 팀 담당
⑥ 사이드바 친구 목록 상태 → Social 팀 담당
⑦ 모바일 웹 뷰의 뱃지 → Mobile 팀 담당// 메시지 읽음 처리 — 여러 팀이 각자 작성한 코드
MessageActions.markAsRead = function(threadId) {
// 서버에 읽음 상태 전송
API.post('/messages/read', { thread: threadId });
// 로컬 상태 업데이트
MessageStore.markRead(threadId);
var count = MessageStore.getUnreadCount();
// ① Chat 팀이 작성
ChatBadge.update(count);
// ② Notifications 팀이 작성
NotificationIcon.refresh();
// ③ Platform 팀이 작성
TabTitle.setCount(count);
// ④ Chat 팀이 작성
ChatList.markThreadRead(threadId);
// ⑤ Chat 팀이 작성
ChatWindow.updateReadStatus(threadId);
// ⑥ 새로 추가된 사이드바 — 누군가 여기에 추가하는 걸 잊음!
// SidebarFriends.updateStatus(threadId); ← 빠뜨림!
// ⑦ 모바일 웹 뷰 — 아직 연동 안 됨
// MobileView.sync(); ← 빠뜨림!
};구조적 문제
이건 개발자 한 명의 실수가 아니었습니다. 구조적으로 발생할 수밖에 없는 문제였습니다:
문제의 근본 원인:
1. 새 UI 영역이 추가될 때마다, 기존의 모든 이벤트 핸들러에
새 영역 업데이트 코드를 추가해야 함
2. 각 영역을 다른 팀이 담당하므로, 다른 팀의 코드에
자기 영역 업데이트를 추가해달라고 요청해야 함
3. 하나라도 빠뜨리면 → 불일치 버그
4. 이 패턴이 메시지뿐 아니라 좋아요, 댓글, 친구 요청,
그룹 알림 등 모든 기능에 존재고치면 또 다른 곳에서 터지고, 그걸 고치면 또 다른 곳에서 터지는 두더지 잡기 게임이었습니다.
Jordan Walke의 아이디어
Jordan Walke는 Facebook의 광고 팀에서 일하던 엔지니어였습니다. 그는 이 문제를 보면서 한 가지를 깨달았습니다.
PHP(XHP)에서 온 영감
서버(XHP):
요청이 올 때마다 → 데이터로 HTML을 처음부터 새로 만듦
→ 이전 상태와 동기화할 필요 없음
→ 그래서 동기화 버그가 없음
브라우저(Bolt):
이벤트가 올 때마다 → 기존 DOM을 부분적으로 수정
→ 어디를 수정해야 하는지 추적 필요
→ 빠뜨리면 동기화 버그Jordan의 질문:
"브라우저에서도 XHP처럼 매번 처음부터 다시 그리면 되지 않을까?"
단순하지만 강력한 발상
기존 방식:
상태 변경 → 바뀐 부분을 찾아서 DOM 수정 (명령형)
→ 개발자가 "어디를 바꿀지" 알아야 함
Jordan의 방식:
상태 변경 → 전체 UI를 새로 "선언" → 이전과 비교 → 바뀐 부분만 실제 반영
→ 개발자는 "최종 상태"만 선언하면 됨FaxJS: React의 프로토타입
2011년, Jordan Walke는 이 아이디어를 FaxJS라는 프로토타입으로 구현했습니다.
기존 Bolt 코드 (명령형)
// Bolt 방식 — "어떻게 바꿀지" 일일이 지시
var NotificationBadge = {
element: null,
count: 0,
init: function(el) {
this.element = el;
this.render();
},
setCount: function(newCount) {
var oldCount = this.count;
this.count = newCount;
// 변경사항을 직접 추적하고 DOM을 수동 업데이트
if (oldCount === 0 && newCount > 0) {
this.element.style.display = 'inline';
this.element.textContent = newCount;
this.element.classList.add('badge-pulse');
} else if (oldCount > 0 && newCount === 0) {
this.element.style.display = 'none';
this.element.classList.remove('badge-pulse');
} else if (newCount !== oldCount) {
this.element.textContent = newCount;
this.element.classList.add('badge-pulse');
setTimeout(function() {
this.element.classList.remove('badge-pulse');
}.bind(this), 300);
}
// 상태별 분기가 점점 복잡해짐...
}
};FaxJS 코드 (선언형)
// FaxJS 방식 — "어떻게 보여야 하는지"만 선언
var NotificationBadge = FaxJS.createComponent({
getInitialState: function() {
return { count: 0 };
},
render: function() {
// 상태에 따른 최종 모습만 선언
if (this.state.count === 0) {
return FaxJS.Div({ style: { display: 'none' } });
}
return FaxJS.Span({
className: 'badge',
content: this.state.count
});
}
// DOM 업데이트? FaxJS가 알아서 함
// 이전 렌더와 비교? FaxJS가 알아서 함
// 애니메이션 분기? CSS transition으로 처리
});핵심 차이
Bolt (명령형):
"count가 0에서 1로 바뀌면 display를 inline으로 바꾸고,
1에서 0으로 바뀌면 none으로 바꾸고,
1에서 2로 바뀌면 textContent만 바꾸고..."
→ 모든 전환(transition)을 개발자가 직접 관리
FaxJS (선언적):
"count가 0이면 안 보이고, 0이 아니면 숫자를 보여줘"
→ 최종 상태만 선언, 전환은 프레임워크가 관리FaxJS → React로 발전
2011 2012 초 2012 중 2012 말 2013.05
│ │ │ │ │
▼ ▼ ▼ ▼ ▼
FaxJS Facebook 내부 뉴스피드 적용 Instagram 오픈소스
프로토타입 검색에 첫 적용 + JSX 도입 전면 도입 공개주요 이정표
2011년: FaxJS 프로토타입
- Jordan Walke가 개인 프로젝트로 시작
- "매번 다시 그리기" 아이디어 검증
2012년 초: Facebook 검색에 적용
- Facebook 내부에서 첫 실전 테스트
- 검색 결과 UI에 적용 → 버그가 크게 줄어듦
- 내부 이름이 "React"로 변경
2012년 중: 뉴스피드에 적용 + JSX 도입
- Facebook의 핵심 기능인 뉴스피드에 도입
- JSX 문법 추가 — JavaScript 안에서 HTML과 비슷한 문법 사용
// JSX 이전 (React.createElement)
React.createElement('div', { className: 'badge' },
React.createElement('span', null, count)
);
// JSX 도입 후
<div className="badge">
<span>{count}</span>
</div>2012년 말: Instagram 전면 도입
- Facebook이 Instagram을 인수한 후
- Instagram 웹 버전을 React로 전면 재작성
- "외부 프로젝트에서도 잘 동작한다"는 확인
2013년 5월: JSConf에서 오픈소스 공개
- Pete Hunt가 JSConf US에서 React 발표
- GitHub에 공개
초기 반응: "미친 거 아니야?"
React가 공개되었을 때, 커뮤니티의 반응은 대부분 부정적이었습니다.
비판 1: "HTML을 JavaScript 안에 넣는다고?"
// 2013년의 개발자들이 본 것:
function Badge({ count }) {
return (
<div className="badge"> {/* ← 이게 JS 파일 안에?! */}
<span>{count}</span>
</div>
);
}2013년 커뮤니티 반응:
"SoC(관심사의 분리) 원칙을 완전히 위반하잖아"
"HTML은 .html에, JS는 .js에 있어야 하는 거 아닌가?"
"이건 PHP 스파게티 코드로 돌아가는 것과 같다"
"Facebook이 또 이상한 걸 만들었네"비판 2: "Virtual DOM이라니, 느리지 않아?"
비판: "실제 DOM을 직접 수정하는 게 더 빠르지 않나?
매번 전체를 다시 그리고 비교한다고?"
실제: DOM 조작 자체가 비싼 연산.
Virtual DOM은 JavaScript 객체 비교(빠름)를 먼저 하고
실제 DOM 변경은 최소화함.
그리고 성능보다 중요한 건 "버그 없는 코드"였음.비판 3: "기존의 MVC 패턴이 있는데 왜?"
2013년 인기 프레임워크들:
- Angular 1.x — Google의 MVC 프레임워크
- Backbone.js — 경량 MVC
- Ember.js — Convention over Configuration
이들은 모두 Model-View-Controller 패턴을 따름
React는 "V(View)만 담당한다"고 했음
비판: "프레임워크도 아닌 게 뭘 하겠다는 거야?"하지만... 써본 사람들의 이야기
2013년 말 ~ 2014년:
"처음엔 JSX가 이상해 보였는데, 일주일 쓰니까
이전으로 돌아갈 수 없게 됐다"
"뉴스피드에서 매주 터지던 상태 버그가 React 적용 후 사라졌다"
"새 기능 추가할 때 기존 코드가 깨질까 두려웠는데,
React에서는 컴포넌트만 추가하면 되니까 두려움이 없다"
"jQuery로 6시간 걸리던 걸 React로 1시간에 했다.
코드 양이 줄어서가 아니라, 생각할 게 줄어서."코드로 보는 전환: jQuery vs React (2014년 기준)
채팅 뱃지 — jQuery
// 여러 이벤트에서 각각 DOM을 직접 조작
$(document).on('newMessage', function(e, data) {
var $badge = $('#chat-badge');
var currentCount = parseInt($badge.text()) || 0;
var newCount = currentCount + 1;
$badge.text(newCount).show().addClass('pulse');
$('#header-count').text('(' + newCount + ')');
document.title = '(' + newCount + ') Facebook';
setTimeout(function() {
$badge.removeClass('pulse');
}, 300);
});
$(document).on('messageRead', function(e, data) {
var $badge = $('#chat-badge');
var currentCount = parseInt($badge.text()) || 0;
var newCount = Math.max(0, currentCount - 1);
if (newCount === 0) {
$badge.hide();
document.title = 'Facebook';
} else {
$badge.text(newCount);
document.title = '(' + newCount + ') Facebook';
}
$('#header-count').text(newCount > 0 ? '(' + newCount + ')' : '');
// 채팅 목록도 업데이트...
$('#thread-' + data.threadId).removeClass('unread');
});
// 새 영역 추가 시: 위의 두 핸들러 모두에 코드 추가 필요!채팅 뱃지 — React
function ChatBadge({ unreadCount }) {
// 선언적: 상태에 따른 최종 모습만 정의
if (unreadCount === 0) return null;
return <span className="badge">{unreadCount}</span>;
}
function Header({ unreadCount }) {
return (
<header>
<h1>Facebook {unreadCount > 0 && `(${unreadCount})`}</h1>
<nav>
<ChatBadge unreadCount={unreadCount} />
</nav>
</header>
);
}
function App() {
const [unreadCount, setUnreadCount] = useState(0);
// 이벤트 처리: 상태만 변경하면 모든 UI가 자동으로 맞춰짐
useEffect(() => {
socket.on('newMessage', () => setUnreadCount(c => c + 1));
socket.on('messageRead', () => setUnreadCount(c => Math.max(0, c - 1)));
}, []);
// unreadCount가 바뀌면 Header, ChatBadge, 탭 타이틀 등
// 모든 곳이 자동으로 업데이트됨
return <Header unreadCount={unreadCount} />;
}jQuery: 이벤트 2개 × DOM 영역 4개 = 8곳에서 수동 업데이트
React: 상태 1개 변경 → 모든 UI 자동 동기화
새 영역 추가 시:
jQuery → 모든 이벤트 핸들러에 새 DOM 조작 코드 추가
React → 새 컴포넌트만 추가하면 끝Facebook 내부 도입 타임라인
2012.01 검색 UI에 첫 적용
→ "확실히 버그가 줄었다"
2012.06 뉴스피드에 적용
→ "매주 터지던 상태 버그가 사라졌다"
2012.09 Instagram 웹 전면 도입
→ "인수 후 웹사이트를 React로 재작성"
2013.03 Facebook 내부 대부분의 신규 프로젝트가 React 사용
→ "이제 Bolt로 새 프로젝트를 시작하는 팀이 없다"
2013.05 JSConf US에서 오픈소스 공개
→ Pete Hunt: "React의 요점은 성능이 아니라
예측 가능한 코드를 작성할 수 있게 하는 것이다"시니어 개발자의 한마디
후배: "React가 처음에 욕을 엄청 먹었다면서요?"
시니어: "응. 'HTML을 JS에 넣는다고? 미쳤다'는 반응이 대부분이었어. 그런데 면접에서 React 경험을 물어보기 시작한 건 공개 후 2년도 안 돼서부터야."
후배: "왜 그렇게 빨리 퍼졌을까요?"
시니어: "React가 해결하는 문제가 진짜였으니까. Facebook만의 문제가 아니라, 웹 앱을 만드는 모든 팀이 같은 '상태-UI 동기화' 문제로 고통받고 있었거든. '이론적으로 나쁘다'보다 '실제로 버그가 줄었다'가 더 강력했어."
핵심 정리
- Facebook의 구조적 문제: 하나의 이벤트(메시지 읽음)가 7개 이상의 UI 영역에 영향 → 각 영역을 다른 팀이 관리 → 동기화 버그 불가피
- Jordan Walke의 통찰: PHP(XHP)처럼 매번 처음부터 다시 그리면 동기화 문제가 없다 → 이걸 브라우저에서 해보자
- FaxJS → React: 프로토타입에서 시작 → Facebook 내부 검증 → 오픈소스 공개
- 명령형 → 선언적: "어떻게 바꿔라" → "이렇게 보여야 한다"
- 초기 비판 → 빠른 확산: "JSX가 이상하다" → 써본 사람들이 장점을 체감 → 업계 표준
- 핵심 교훈: 진짜 문제를 해결하는 기술은 초기 비판을 이겨냄
다음 문서에서는 React의 JSX가 촉발한 "관심사의 분리(SoC)" 논쟁을 깊이 살펴봅니다. HTML/CSS/JS를 분리하는 것이 정말 "관심사의 분리"였을까요?