Skip to main content
Advertisement

React 소개 — 선언형 UI와 가상 DOM

React란 무엇인가?

React는 Facebook(현 Meta)이 2013년에 오픈소스로 공개한 JavaScript UI 라이브러리입니다. "라이브러리"라는 점이 중요한데, Angular처럼 라우팅·폼·HTTP 클라이언트까지 모두 포함하는 "프레임워크"가 아니라, UI 렌더링이라는 한 가지 역할에 집중합니다.

React의 핵심 철학은 두 가지입니다.

  1. 선언형(Declarative) UI: "어떻게 DOM을 조작하느냐"가 아니라 "현재 상태에서 UI가 어떻게 보여야 하느냐"를 기술합니다.
  2. 컴포넌트 기반(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)**으로 줄이기 위해 두 가지 가정을 합니다.

  1. 타입이 다른 엘리먼트는 다른 트리를 만든다: divspan 으로 바뀌면 전체 서브트리를 교체합니다.
  2. 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의 컴파일러는 은탄환이 아니다

컴파일러가 최적화를 자동으로 해주지만, 컴포넌트가 순수 함수여야 한다는 전제 조건이 있습니다. 사이드 이펙트가 섞인 렌더링 로직은 컴파일러도 최적화할 수 없습니다.

Advertisement