1 / 2

01. React/Svelte가 해결하는 근본 문제

예상 시간: 5분

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()를 호출하면:

  1. React가 새로운 상태로 컴포넌트를 다시 실행
  2. 이전 결과와 새 결과를 비교 (Virtual DOM diffing)
  3. 바뀐 부분만 실제 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가지 문제

문제순수 JSReact/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 모두의 존재 이유야."


핵심 정리

  1. 프론트엔드 프레임워크의 근본 문제: 데이터(상태)가 바뀌면 화면(UI)을 어떻게 일치시킬 것인가?
  2. 순수 JS의 한계: 상태 변경마다 DOM을 수동으로 업데이트해야 하며, 하나라도 빠뜨리면 데이터와 화면이 불일치
  3. React의 해결책: 상태만 변경하면 (setState) 화면은 자동으로 동기화 — 선언적 UI
  4. 핵심 공식: UI = f(state) — 같은 상태는 항상 같은 화면을 만든다
  5. 3가지 해결 과제: 상태-UI 동기화, 컴포넌트 재사용, 선언적 UI
  6. 생산성 향상은 목적이 아니라 결과 — 구조적 문제를 해결한 부산물

다음 문서에서는 이 문제를 실제로 겪었던 Facebook에서 React가 어떻게 탄생했는지 살펴봅니다.