React 소개 — 선언형 UI와 가상 DOM
React란 무엇인가?
React는 Facebook(현 Meta)이 2013년에 오픈소스로 공개한 JavaScript UI 라이브러리입니다. "라이브러리"라는 점이 중요한데, Angular처럼 라우팅·폼·HTTP 클라이언트까지 모두 포함하는 "프레임워크"가 아니라, UI 렌더링이라는 한 가지 역할에 집중합니다.
React의 핵심 철학은 두 가지입니다.
- 선언형(Declarative) UI: "어떻게 DOM을 조작하느냐"가 아니라 "현재 상태에서 UI가 어떻게 보여야 하느냐"를 기술합니다.
- 컴포넌트 기반(Component-based): UI를 독립적이고 재사용 가능한 조각으로 분리합니다.
명령형 vs 선언형
// ❌ 명령형 (Vanilla JS) — "어떻게" 조작하는지 기술
const button = document.createElement('button');
button.textContent = '좋아요';
button.classList.add('btn-primary');
button.addEventListener('click', () => {
count++;
button.textContent = `좋아요 ${count}`;
});
document.body.appendChild(button);
// ✅ 선언형 (React) — "무엇이 보여야 하는지" 기술
function LikeButton() {
const [count, setCount] = React.useState(0);
return (
<button onClick={() => setCount(count + 1)}>
좋아요 {count}
</button>
);
}
가상 DOM(Virtual DOM)
개념
가상 DOM은 실제 브라우저 DOM의 경량 JavaScript 객체 복사본입니다. React는 상태가 변경될 때마다 새 가상 DOM 트리를 만들고, 이전 트리와 비교하여 변경된 부분만 실제 DOM에 반영합니다.
상태 변경
↓
새 Virtual DOM 트리 생성
↓
이전 트리와 Diffing (재조정 알고리즘)
↓
최소한의 실제 DOM 업데이트
재조정(Reconciliation) 알고리즘
React의 Diffing 알고리즘은 O(n³) 문제를 **O(n)**으로 줄이기 위해 두 가지 가정을 합니다.
- 타입이 다른 엘리먼트는 다른 트리를 만든다:
div→span으로 바뀌면 전체 서브트리를 교체합니다. - key prop으로 자식 요소를 식별한다: 리스트에서 key가 같은 요소는 재사용합니다.
// key를 사용하지 않으면 전체 재렌더링
function BadList({ items }) {
return (
<ul>
{items.map((item, index) => (
<li key={index}>{item.name}</li> // ❌ 인덱스는 나쁜 key
))}
</ul>
);
}
// 고유한 key 사용
function GoodList({ items }) {
return (
<ul>
{items.map((item) => (
<li key={item.id}>{item.name}</li> // ✅ 고유 ID 사용
))}
</ul>
);
}
Fiber 아키텍처 (React 16+)
React 16에서 Fiber라는 새 재조정 엔진이 도입되었습니다. Fiber는 렌더링 작업을 잘게 쪼개 우선순위를 부여하고, 중요한 업데이트(사용자 입력 등)를 먼저 처리합니다.
- 동시 모드(Concurrent Mode): 렌더링을 중단·재개·포기할 수 있습니다.
- 시간 분할(Time Slicing): 긴 렌더링을 여러 프레임으로 나눠 UI 반응성을 유지합니다.
React 버전 히스토리
| 버전 | 주요 변경 |
|---|---|
| React 16 (2017) | Fiber 아키텍처, Error Boundary, Portal, Fragment |
| React 17 (2020) | 이벤트 위임 변경, JSX Transform (import 불필요) |
| React 18 (2022) | Concurrent 기본화, createRoot, useTransition, useDeferredValue, Suspense 강화 |
| React 19 (2024) | Server Components 안정화, Actions, use() 훅, useActionState, useOptimistic, React Compiler |
React 18 주요 변경점
createRoot API
// React 17 이하 (구식)
import ReactDOM from 'react-dom';
ReactDOM.render(<App />, document.getElementById('root'));
// React 18+ (현재)
import { createRoot } from 'react-dom/client';
const root = createRoot(document.getElementById('root'));
root.render(<App />);
자동 배칭(Automatic Batching)
React 18 이전에는 이벤트 핸들러 외부(setTimeout, Promise 등)에서의 상태 업데이트는 배칭되지 않았습니다.
// React 18 이전: 2번 렌더링
setTimeout(() => {
setCount(c => c + 1); // 렌더링 1
setFlag(f => !f); // 렌더링 2
}, 1000);
// React 18: 자동으로 배칭되어 1번만 렌더링
setTimeout(() => {
setCount(c => c + 1); // 배칭됨
setFlag(f => !f); // 배칭됨 → 합쳐서 1번 렌더링
}, 1000);
React 19 주요 변경점
Server Components
서버에서만 렌더링되는 컴포넌트로, 클라이언트 번들에 포함되지 않습니다.
// app/page.jsx (Next.js App Router) — 서버 컴포넌트
async function UserProfile({ userId }) {
// 서버에서 직접 DB 조회 가능
const user = await db.user.findById(userId);
return <h1>{user.name}</h1>;
}
Actions
폼 처리 패턴의 혁신 — action prop에 함수를 전달합니다.
function AddToCart() {
async function addItem(formData) {
'use server'; // Next.js Server Action
const id = formData.get('productId');
await db.cart.add(id);
}
return (
<form action={addItem}>
<input name="productId" value="123" />
<button type="submit">장바구니 추가</button>
</form>
);
}
React Compiler (React Forget)
React 19의 컴파일러는 useMemo, useCallback, React.memo를 자동으로 추가하여 개발자가 수동으로 최적화하지 않아도 됩니다.
// 컴파일 전 (개발자가 작성)
function ExpensiveComponent({ items, filter }) {
const filtered = items.filter(item => item.type === filter);
return <List items={filtered} />;
}
// 컴파일 후 (React Compiler가 변환)
function ExpensiveComponent({ items, filter }) {
const filtered = useMemo(
() => items.filter(item => item.type === filter),
[items, filter]
);
return <List items={filtered} />;
}
React 생태계 개요
React (코어 UI)
├── 상태 관리
│ ├── Zustand (경량, 간단)
│ ├── Jotai (원자적)
│ ├── Redux Toolkit (대규모)
│ └── Recoil (Facebook)
├── 데이터 페칭
│ ├── TanStack Query (React Query)
│ └── SWR (Vercel)
├── 라우팅
│ └── React Router v6
├── 메타 프레임워크
│ ├── Next.js (Vercel, SSR/SSG)
│ ├── Remix (중첩 라우트)
│ └── Gatsby (SSG)
├── 스타일링
│ ├── Tailwind CSS
│ ├── CSS Modules
│ └── styled-components / Emotion
└── 테스팅
├── React Testing Library
├── Jest
└── Playwright (E2E)
실전 예제: 할 일 목록 앱 미리 보기
import { useState } from 'react';
function TodoApp() {
const [todos, setTodos] = useState([
{ id: 1, text: 'React 배우기', done: false },
{ id: 2, text: 'Next.js 배우기', done: false },
]);
const [input, setInput] = useState('');
function addTodo() {
if (!input.trim()) return;
setTodos([
...todos,
{ id: Date.now(), text: input, done: false },
]);
setInput('');
}
function toggleTodo(id) {
setTodos(todos.map(todo =>
todo.id === id ? { ...todo, done: !todo.done } : todo
));
}
return (
<div>
<h1>할 일 목록</h1>
<input
value={input}
onChange={e => setInput(e.target.value)}
onKeyDown={e => e.key === 'Enter' && addTodo()}
placeholder="할 일 입력..."
/>
<button onClick={addTodo}>추가</button>
<ul>
{todos.map(todo => (
<li
key={todo.id}
onClick={() => toggleTodo(todo.id)}
style={{ textDecoration: todo.done ? 'line-through' : 'none' }}
>
{todo.text}
</li>
))}
</ul>
<p>남은 할 일: {todos.filter(t => !t.done).length}개</p>
</div>
);
}
export default TodoApp;
고수 팁
1. React를 "상태 머신"으로 생각하기
UI = f(state) — 같은 state면 항상 같은 UI. 이 순수 함수 관점으로 생각하면 버그가 줄어듭니다.
2. 불필요한 리렌더링 파악하기
React DevTools의 "Highlight updates when components render" 옵션을 켜두면 어떤 컴포넌트가 렌더링되는지 시각적으로 확인할 수 있습니다.
3. StrictMode 반드시 사용
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
createRoot(document.getElementById('root')).render(
<StrictMode>
<App />
</StrictMode>
);
StrictMode는 개발 환경에서 컴포넌트를 두 번 렌더링하여 사이드 이펙트 버그를 조기에 발견합니다.
4. React 19의 컴파일러는 은탄환이 아니다
컴파일러가 최적화를 자동으로 해주지만, 컴포넌트가 순수 함수여야 한다는 전제 조건이 있습니다. 사이드 이펙트가 섞인 렌더링 로직은 컴파일러도 최적화할 수 없습니다.