01. React/Svelte가 해결하는 근본 문제
01. React/Svelte가 해결하는 근본 문제
이 문서에서 배우는 것
- 프론트엔드 프레임워크가 해결하는 진짜 문제가 무엇인지
- 순수 JavaScript로 UI를 관리할 때 왜 버그가 생기는지
UI = f(state)공식의 의미- "편하려고" 쓰는 게 아니라 "구조적 문제를 해결하려고" 쓰는 것임을 이해
PM의 요구사항
"장바구니에서 상품 삭제하면 수량, 총액, 빈 장바구니 메시지까지 다 업데이트되게 해주세요."
간단해 보이는 이 요구사항이, 프레임워크 없이는 왜 악몽이 되는지 살펴보겠습니다.
근본 문제: 데이터가 바뀌면 화면을 어떻게 업데이트할 것인가?
웹 개발에서 가장 오래되고, 가장 근본적인 문제가 하나 있습니다.
데이터(State)가 변경되었을 때, 화면(UI)을 어떻게 일치시킬 것인가?이 문제가 왜 어려운지, 장바구니 예시로 직접 느껴보겠습니다.
순수 JavaScript로 장바구니 만들기
데이터 구조
let cart = [
{ id: 1, name: "TypeScript 입문서", price: 25000, qty: 1 },
{ id: 2, name: "React 실전 가이드", price: 32000, qty: 2 },
{ id: 3, name: "Node.js 교과서", price: 28000, qty: 1 },
];화면에 표시되는 영역들
┌─────────────────────────────────────────┐
│ 🛒 장바구니 (3개 상품) ← ① 헤더 상품 수
├─────────────────────────────────────────┤
│ TypeScript 입문서 25,000원 [삭제] ← ② 상품 목록
│ React 실전 가이드 64,000원 [삭제] │
│ Node.js 교과서 28,000원 [삭제] │
├─────────────────────────────────────────┤
│ 총 3종 / 4권 ← ③ 요약 정보
│ 합계: 117,000원 ← ④ 총액
│ [🚀 결제하기] ← ⑤ 결제 버튼 (활성)
├─────────────────────────────────────────┤
│ 무료배송까지 13,000원 남음 (바) ← ⑥ 배송비 안내
└─────────────────────────────────────────┘"React 실전 가이드"를 삭제하면?
데이터 변경은 딱 한 줄입니다:
cart.splice(1, 1); // 인덱스 1번 상품 삭제하지만 화면 업데이트는 6곳입니다:
function removeItem(index) {
// 데이터 변경 — 1줄
cart.splice(index, 1);
// UI 업데이트 — 6곳을 각각 수동으로
// ① 헤더 상품 수
document.querySelector('.cart-count').textContent =
`🛒 장바구니 (${cart.length}개 상품)`;
// ② 상품 목록에서 해당 행 제거
document.querySelector(`.cart-item[data-index="${index}"]`).remove();
// 남은 항목들의 data-index도 다시 매겨야 함!
document.querySelectorAll('.cart-item').forEach((el, i) => {
el.setAttribute('data-index', i);
});
// ③ 요약 정보
const totalQty = cart.reduce((sum, item) => sum + item.qty, 0);
document.querySelector('.summary').textContent =
`총 ${cart.length}종 / ${totalQty}권`;
// ④ 총액
const total = cart.reduce((sum, item) => sum + item.price * item.qty, 0);
document.querySelector('.total').textContent =
`합계: ${total.toLocaleString()}원`;
// ⑤ 결제 버튼 (장바구니가 비면 비활성화)
document.querySelector('.checkout-btn').disabled = cart.length === 0;
// ⑥ 배송비 안내
const freeShippingThreshold = 130000;
const remaining = freeShippingThreshold - total;
if (remaining > 0) {
document.querySelector('.shipping').textContent =
`무료배송까지 ${remaining.toLocaleString()}원 남음`;
document.querySelector('.shipping-bar').style.width =
`${(total / freeShippingThreshold) * 100}%`;
} else {
document.querySelector('.shipping').textContent = '🎉 무료배송!';
document.querySelector('.shipping-bar').style.width = '100%';
}
// 장바구니가 비었으면 "빈 장바구니" 메시지도 표시해야...
if (cart.length === 0) {
document.querySelector('.cart-list').innerHTML =
'<p class="empty">장바구니가 비어있습니다</p>';
}
}문제: 하나라도 빠뜨리면?
function removeItem_buggy(index) {
cart.splice(index, 1);
// ② 상품 목록에서 행은 제거했는데...
document.querySelector(`.cart-item[data-index="${index}"]`).remove();
// ④ 총액은 업데이트했는데...
const total = cart.reduce((sum, item) => sum + item.price * item.qty, 0);
document.querySelector('.total').textContent =
`합계: ${total.toLocaleString()}원`;
// ① 헤더 상품 수를 깜빡함! → "3개 상품"이 그대로 표시
// ③ 요약 정보를 깜빡함! → "총 3종 / 4권"이 그대로
// ⑤ 결제 버튼 상태를 깜빡함! → 빈 장바구니에서도 결제 가능
// ⑥ 배송비 안내를 깜빡함! → 금액이 바뀌었는데 바 그대로
}데이터: [상품A, 상품C] ← 2개
화면: 🛒 장바구니 (3개 상품) ← 3개라고 표시
→ 데이터와 화면이 불일치 = 버그이것이 상태-UI 동기화 문제입니다. 그리고 이 장바구니는 아주 단순한 예시입니다. 실제 서비스에서는 수십, 수백 개의 상태가 수백 개의 UI 영역에 영향을 줍니다.
React로 같은 것 만들기
function ShoppingCart() {
const [cart, setCart] = useState([
{ id: 1, name: "TypeScript 입문서", price: 25000, qty: 1 },
{ id: 2, name: "React 실전 가이드", price: 32000, qty: 2 },
{ id: 3, name: "Node.js 교과서", price: 28000, qty: 1 },
]);
// 삭제 로직 — 딱 이것만 하면 됨
const removeItem = (id) => {
setCart(cart.filter(item => item.id !== id));
};
// 파생 데이터 — 자동 계산
const totalQty = cart.reduce((sum, item) => sum + item.qty, 0);
const total = cart.reduce((sum, item) => sum + item.price * item.qty, 0);
const freeShippingThreshold = 130000;
const remaining = freeShippingThreshold - total;
// UI 선언 — "이렇게 보여야 한다"
return (
<div>
<h2>🛒 장바구니 ({cart.length}개 상품)</h2> {/* ← 항상 맞음 */}
{cart.length === 0 ? (
<p className="empty">장바구니가 비어있습니다</p>
) : (
<ul>
{cart.map(item => (
<li key={item.id}>
{item.name} {(item.price * item.qty).toLocaleString()}원
<button onClick={() => removeItem(item.id)}>삭제</button>
</li>
))}
</ul>
)}
<p>총 {cart.length}종 / {totalQty}권</p> {/* ← 항상 맞음 */}
<p>합계: {total.toLocaleString()}원</p> {/* ← 항상 맞음 */}
<button disabled={cart.length === 0}>결제하기</button>
{remaining > 0
? <p>무료배송까지 {remaining.toLocaleString()}원 남음</p>
: <p>🎉 무료배송!</p>
}
</div>
);
}차이점
순수 JS: "상품을 삭제했으니, 여기도 바꾸고, 저기도 바꾸고, 이것도 바꿔라"
→ 명령형 (How)
→ 개발자가 모든 업데이트를 직접 관리
React: "장바구니 상태가 이러니까, 화면은 이렇게 보여야 한다"
→ 선언적 (What)
→ React가 알아서 바뀐 부분만 업데이트React에서 setCart()를 호출하면:
- React가 새로운 상태로 컴포넌트를 다시 실행
- 이전 결과와 새 결과를 비교 (Virtual DOM diffing)
- 바뀐 부분만 실제 DOM에 반영
개발자는 "6곳을 일일이 업데이트" 할 필요가 없습니다.
핵심 공식: UI = f(state)
UI = f(state)
UI : 사용자가 보는 화면
f : 컴포넌트 함수 (상태를 받아 UI를 반환)
state : 애플리케이션 데이터이 공식이 의미하는 것:
┌─────────────────────────────────────────────────┐
│ │
│ state가 같으면 → UI도 항상 같다 │
│ state가 바뀌면 → UI도 자동으로 바뀐다 │
│ │
│ 개발자는 state만 관리하면 된다 │
│ UI는 state의 함수이므로 자동으로 따라온다 │
│ │
└─────────────────────────────────────────────────┘순수 JS 방식:
state 변경 → 개발자가 수동으로 DOM 6곳 업데이트 → 빠뜨리면 버그
React 방식:
state 변경 → f(state) 재실행 → React가 자동으로 DOM 업데이트 → 빠뜨릴 수 없음React/Svelte가 해결하는 3가지 문제
| 문제 | 순수 JS | React/Svelte |
|---|---|---|
| 1. 상태-UI 동기화 | 데이터 변경마다 DOM을 수동 업데이트. 빠뜨리면 불일치 버그 | 상태만 바꾸면 UI가 자동으로 동기화 |
| 2. 컴포넌트 재사용 | 같은 UI를 다른 페이지에서 쓰려면 HTML/CSS/JS를 복사-붙여넣기 | <Badge count={3} /> 한 줄로 어디서든 재사용 |
| 3. 선언적 UI | "이 요소를 찾아서 텍스트를 바꾸고 클래스를 추가하고..." (How) | "상태가 이러면 UI는 이래야 한다" (What) |
이 3가지의 관계
┌─────────────────────┐
│ 상태-UI 동기화 │ ← 근본 문제
└─────────┬───────────┘
│
┌───────────┼───────────┐
▼ ▼
┌──────────────┐ ┌──────────────┐
│ 컴포넌트 재사용│ │ 선언적 UI │ ← 해결 방법
└──────┬───────┘ └──────┬───────┘
│ │
└──────────┬───────────┘
▼
┌─────────────────┐
│ 생산성 향상 │ ← 결과
└─────────────────┘중요: 생산성 향상은 이 3가지를 해결한 결과이지 목적이 아닙니다. "편하려고 React 쓴다"가 아니라 "상태-UI 동기화 문제를 해결하려고 React를 쓰니까 결과적으로 편해진 것"입니다.
시니어 개발자의 한마디
후배: "React 없이도 웹사이트 만들 수 있잖아요. 왜 굳이?"
시니어: "맞아, 만들 수는 있어. 하지만 장바구니에 상품 삭제 하나 구현하는데 DOM 6곳을 수동으로 관리하는 코드를 봤지? 그게 실제 서비스에서는 수백 개야. Facebook에서 메시지 하나 읽는 것만으로 DOM 7곳 이상을 업데이트해야 했고, 각 영역을 다른 팀이 담당했어. 그게 React가 만들어진 이유야."
후배: "그러니까 결국 '데이터 바뀌면 화면 업데이트'가 핵심 문제인 거네요?"
시니어: "정확해.
UI = f(state). 이 공식 하나가 React, Svelte, Vue 모두의 존재 이유야."
핵심 정리
- 프론트엔드 프레임워크의 근본 문제: 데이터(상태)가 바뀌면 화면(UI)을 어떻게 일치시킬 것인가?
- 순수 JS의 한계: 상태 변경마다 DOM을 수동으로 업데이트해야 하며, 하나라도 빠뜨리면 데이터와 화면이 불일치
- React의 해결책: 상태만 변경하면 (
setState) 화면은 자동으로 동기화 — 선언적 UI - 핵심 공식:
UI = f(state)— 같은 상태는 항상 같은 화면을 만든다 - 3가지 해결 과제: 상태-UI 동기화, 컴포넌트 재사용, 선언적 UI
- 생산성 향상은 목적이 아니라 결과 — 구조적 문제를 해결한 부산물
다음 문서에서는 이 문제를 실제로 겪었던 Facebook에서 React가 어떻게 탄생했는지 살펴봅니다.