Skip to main content
Advertisement

React 19 신기능

React 19는 Server Components, Actions API, use() 훅, React Compiler 등 획기적인 기능을 도입했습니다. 이 기능들은 개발자 경험과 성능을 동시에 향상시킵니다.


Server Components 개념 및 동작 원리

React Server Components(RSC)는 서버에서만 렌더링되는 컴포넌트입니다. 클라이언트로 JavaScript 번들을 전송하지 않아 초기 로드 성능이 크게 향상됩니다.

// server-component.jsx (서버 컴포넌트 - 기본값)
// 'use client' 없으면 서버 컴포넌트

import { db } from './database';

// 서버 컴포넌트에서 직접 데이터베이스 접근 가능!
async function UserList() {
// await 직접 사용 (서버 컴포넌트는 async 가능)
const users = await db.query('SELECT * FROM users');

return (
<ul>
{users.map(user => (
<li key={user.id}>
{user.name} - {user.email}
</li>
))}
</ul>
);
}

export default UserList;
// client-component.jsx (클라이언트 컴포넌트)
'use client'; // 이 지시어로 클라이언트 컴포넌트임을 명시

import { useState } from 'react';

function Counter() {
const [count, setCount] = useState(0);

return (
<button onClick={() => setCount(c => c + 1)}>
클릭 수: {count}
</button>
);
}

Server vs Client 컴포넌트 비교

서버 컴포넌트:
- 서버에서 실행, 번들에 포함 안 됨
- async/await 직접 사용 가능
- DB, 파일시스템 접근 가능
- useState, useEffect 사용 불가
- 이벤트 핸들러 사용 불가
- API 키 등 서버 비밀 안전하게 사용

클라이언트 컴포넌트:
- 브라우저에서 실행
- React 훅 사용 가능
- DOM 조작, 이벤트 처리 가능
- 인터랙티브 UI 구현
// 서버 컴포넌트 안에 클라이언트 컴포넌트 포함 (가능)
// server-page.jsx
import { ClientComponent } from './client-component';
import { getServerData } from './data';

async function Page({ params }) {
const data = await getServerData(params.id);

return (
<div>
<h1>{data.title}</h1>
{/* 서버에서 데이터를 props로 전달 */}
<ClientComponent initialData={data} />
</div>
);
}

// 클라이언트 컴포넌트 안에 서버 컴포넌트는 불가
// (서버 컴포넌트를 children으로 전달하는 것은 가능)

Actions API

React 19의 Actions는 폼 제출과 비동기 상태 업데이트를 간소화합니다.

useActionState

'use client';
import { useActionState } from 'react';

// 서버 액션 (server-actions.js)
'use server';
async function createUser(prevState, formData) {
const name = formData.get('name');
const email = formData.get('email');

try {
const user = await db.users.create({ name, email });
return { success: true, user, error: null };
} catch (err) {
if (err.code === 'DUPLICATE_EMAIL') {
return { success: false, user: null, error: '이미 사용 중인 이메일입니다' };
}
return { success: false, user: null, error: '오류가 발생했습니다' };
}
}

// 클라이언트 컴포넌트
function SignupForm() {
const [state, action, isPending] = useActionState(
createUser,
{ success: false, user: null, error: null } // 초기 상태
);

return (
<form action={action}>
<input name="name" placeholder="이름" required />
<input name="email" type="email" placeholder="이메일" required />

{state.error && (
<p className="error">{state.error}</p>
)}

{state.success && (
<p className="success">{state.user.name}님, 가입을 환영합니다!</p>
)}

<button type="submit" disabled={isPending}>
{isPending ? '처리 중...' : '가입하기'}
</button>
</form>
);
}

useFormStatus

'use client';
import { useFormStatus } from 'react-dom';

// 폼의 자식 컴포넌트에서 부모 폼 상태 접근
function SubmitButton() {
const { pending, data, method, action } = useFormStatus();

return (
<button type="submit" disabled={pending}>
{pending ? '제출 중...' : '제출'}
</button>
);
}

function Form() {
return (
<form action={serverAction}>
<input name="title" placeholder="제목" />
<SubmitButton /> {/* 부모 폼 상태 자동 접근 */}
</form>
);
}

useOptimistic

'use client';
import { useOptimistic, useActionState } from 'react';

function TodoList({ todos, addTodo }) {
// 낙관적 업데이트: 서버 응답 전에 미리 UI 반영
const [optimisticTodos, addOptimisticTodo] = useOptimistic(
todos,
(currentTodos, newTodo) => [...currentTodos, { ...newTodo, pending: true }]
);

async function submitTodo(formData) {
const text = formData.get('text');
const tempTodo = { id: Date.now(), text, completed: false };

addOptimisticTodo(tempTodo); // 즉시 UI 업데이트

await addTodo(text); // 서버에 실제 저장 (완료 후 실제 데이터로 교체)
}

return (
<div>
<ul>
{optimisticTodos.map(todo => (
<li key={todo.id} style={{ opacity: todo.pending ? 0.5 : 1 }}>
{todo.text} {todo.pending && '(저장 중...)'}
</li>
))}
</ul>

<form action={submitTodo}>
<input name="text" placeholder="할 일 입력" />
<button type="submit">추가</button>
</form>
</div>
);
}

use() 훅

use()는 Promise와 Context를 컴포넌트 내 어디서든 읽을 수 있는 새로운 API입니다.

Promise 읽기

import { use, Suspense } from 'react';

// 서버에서 데이터 가져오기
async function getUser(id) {
const response = await fetch(`/api/users/${id}`);
return response.json();
}

// 클라이언트에서 use()로 Promise 읽기
function UserProfile({ userPromise }) {
// Promise가 resolve될 때까지 Suspense 경계로 일시 중단
const user = use(userPromise); // await처럼 동작하지만 훅

return (
<div>
<h2>{user.name}</h2>
<p>{user.email}</p>
</div>
);
}

// Suspense로 로딩 상태 처리
function App() {
const userPromise = getUser(1); // Promise를 직접 전달

return (
<Suspense fallback={<div>로딩 중...</div>}>
<UserProfile userPromise={userPromise} />
</Suspense>
);
}

// 기존 훅과 달리 조건부 사용 가능
function ConditionalData({ loadData, condition }) {
if (condition) {
const data = use(loadData()); // 조건부로 사용 가능!
return <div>{data.value}</div>;
}
return <div>데이터 없음</div>;
}

Context 읽기

import { use, createContext } from 'react';

const ThemeContext = createContext('light');

// use()로 Context 읽기 (useContext 대체 가능)
function ThemedButton({ children }) {
const theme = use(ThemeContext);

return (
<button className={`btn-${theme}`}>
{children}
</button>
);
}

// 조건부로 Context 읽기 (use()의 장점)
function SmartComponent({ isThemed }) {
if (isThemed) {
const theme = use(ThemeContext); // 조건부 사용 가능
return <div className={theme}>테마 적용됨</div>;
}
return <div>기본 스타일</div>;
}

React Compiler (자동 메모이제이션)

React Compiler는 React 19와 함께 도입된 빌드 타임 최적화 도구입니다. useMemo, useCallback, React.memo 없이도 자동으로 메모이제이션을 적용합니다.

// React 18 이전: 수동 최적화 필요
import { useMemo, useCallback, memo } from 'react';

const ExpensiveComponent = memo(function ExpensiveComponent({ data, onAction }) {
const processed = useMemo(() => {
return data.map(item => expensiveProcess(item));
}, [data]);

const handleAction = useCallback((id) => {
onAction(id);
}, [onAction]);

return (
<ul>
{processed.map(item => (
<li key={item.id} onClick={() => handleAction(item.id)}>
{item.value}
</li>
))}
</ul>
);
});

// React 19 + Compiler: 자동 최적화
// 'react-compiler' babel 플러그인 또는 Next.js 설정만 추가하면 됨
function ExpensiveComponent({ data, onAction }) {
// Compiler가 자동으로 메모이제이션 최적 위치 결정
const processed = data.map(item => expensiveProcess(item));

const handleAction = (id) => {
onAction(id);
};

return (
<ul>
{processed.map(item => (
<li key={item.id} onClick={() => handleAction(item.id)}>
{item.value}
</li>
))}
</ul>
);
}
// memo, useMemo, useCallback 없이도 동일한 성능!

React Compiler 설정

// babel.config.js
module.exports = {
plugins: [
['babel-plugin-react-compiler', {
// 컴파일러 설정
target: '18', // React 18과도 호환
}]
]
};

// Next.js 15+ (자동 지원)
// next.config.js
module.exports = {
experimental: {
reactCompiler: true
}
};

React 19 vs 18 마이그레이션 가이드

변경사항 요약

// 1. ref 콜백 클린업 (React 19)
function Component() {
return (
<input
ref={(node) => {
// node: 마운트 시 DOM 요소, 언마운트 시 null
if (node) {
node.focus();
}

// React 19: 클린업 함수 반환 가능
return () => {
// 언마운트 시 실행
console.log('입력 필드 언마운트됨');
};
}}
/>
);
}

// 2. forwardRef 제거 (React 19에서 불필요)
// React 18
import { forwardRef } from 'react';
const Input18 = forwardRef(function Input({ label }, ref) {
return <input ref={ref} placeholder={label} />;
});

// React 19: ref를 일반 prop으로 전달 가능
function Input19({ label, ref }) {
return <input ref={ref} placeholder={label} />;
}

// 3. Context Provider 단순화
// React 18
<ThemeContext.Provider value={theme}>
{children}
</ThemeContext.Provider>

// React 19
<ThemeContext value={theme}>
{children}
</ThemeContext>

// 4. useDeferredValue 초기값 지원
import { useDeferredValue } from 'react';

// React 19: 초기값 지정 가능
const deferredValue = useDeferredValue(value, { initialValue: '' });

마이그레이션 체크리스트

// React 18 → 19 마이그레이션 시 확인 사항

// 1. ReactDOM.render → createRoot (이미 완료해야 함)
// 구식 (제거됨):
// ReactDOM.render(<App />, document.getElementById('root'));

// 현대식:
import { createRoot } from 'react-dom/client';
const root = createRoot(document.getElementById('root'));
root.render(<App />);

// 2. hydrate → hydrateRoot
import { hydrateRoot } from 'react-dom/client';
hydrateRoot(document, <App />);

// 3. 엄격 모드에서 useEffect 두 번 실행 처리
// React 18 Strict Mode: 개발 환경에서 마운트-언마운트-마운트 순서로 실행
// → 클린업 함수 올바르게 구현했는지 확인

useEffect(() => {
const subscription = subscribeToData(userId);

return () => {
subscription.unsubscribe(); // 클린업 필수!
};
}, [userId]);

// 4. Concurrent Mode 호환성 확인
// 렌더링이 중단/재시작될 수 있으므로 사이드 이펙트는 useEffect에서만

실전 예제: Server Component + Client Component 조합

// app/dashboard/page.jsx (서버 컴포넌트)
import { DashboardStats } from './DashboardStats';
import { RecentActivity } from './RecentActivity';
import { InteractiveChart } from './InteractiveChart'; // 클라이언트
import { getUserDashboard } from '@/lib/data';

export default async function DashboardPage({ params }) {
// 서버에서 직접 데이터 가져오기
const { stats, activity, chartData } = await getUserDashboard(params.userId);

return (
<div className="dashboard">
{/* 서버 컴포넌트: 정적 데이터 */}
<DashboardStats stats={stats} />
<RecentActivity activities={activity} />

{/* 클라이언트 컴포넌트: 인터랙티브 */}
<InteractiveChart initialData={chartData} />
</div>
);
}

// app/dashboard/InteractiveChart.jsx (클라이언트 컴포넌트)
'use client';

import { useState, useTransition } from 'react';

function InteractiveChart({ initialData }) {
const [data, setData] = useState(initialData);
const [isPending, startTransition] = useTransition();

function handlePeriodChange(period) {
startTransition(async () => {
const response = await fetch(`/api/chart?period=${period}`);
const newData = await response.json();
setData(newData);
});
}

return (
<div>
<div className="period-selector">
{['1주', '1개월', '3개월', '1년'].map(period => (
<button key={period} onClick={() => handlePeriodChange(period)}>
{period}
</button>
))}
</div>

{isPending ? (
<div className="loading">데이터 로딩 중...</div>
) : (
<Chart data={data} />
)}
</div>
);
}

고수 팁

1. Server Component에서 메타데이터 생성

// Next.js 14+ App Router에서
export async function generateMetadata({ params }) {
const product = await getProduct(params.id);
return {
title: product.name,
description: product.description,
openGraph: {
images: [product.imageUrl]
}
};
}

export default async function ProductPage({ params }) {
const product = await getProduct(params.id);
return <ProductDetail product={product} />;
}

2. Streaming으로 점진적 로딩

import { Suspense } from 'react';

async function SlowComponent() {
await new Promise(resolve => setTimeout(resolve, 2000));
const data = await fetchSlowData();
return <div>{data.value}</div>;
}

export default function Page() {
return (
<div>
<h1>빠른 콘텐츠</h1>
<Suspense fallback={<Skeleton />}>
<SlowComponent /> {/* 2초 후 스트리밍 */}
</Suspense>
</div>
);
// 빠른 콘텐츠 먼저 전송, SlowComponent는 준비되면 스트리밍
}

3. Error Boundary와 Server Actions 조합

'use client';
import { useActionState } from 'react';

function Form({ serverAction }) {
const [state, action, isPending] = useActionState(
async (prev, formData) => {
try {
return await serverAction(formData);
} catch (err) {
return { error: err.message };
}
},
null
);

return (
<form action={action}>
{state?.error && <Alert>{state.error}</Alert>}
{/* 폼 필드 */}
<button disabled={isPending}>
{isPending ? <Spinner /> : '제출'}
</button>
</form>
);
}
Advertisement