React를 실무 흐름으로 이해하기
Meta의 React 19로 현대적인 UI를 구축합니다. Hooks 기본기부터 Actions 기반 신기능, 상태 관리 전략, 서버 상태 페칭, 폼 검증, 컴포넌트 설계 패턴, 성능 최적화, 테스트, 접근성까지 실무 React 개발의 전 과정을 다룹니다. 이 가이드는 개념을 나열하기보다, 실제 프로젝트에서 판단해야 하는 순서대로 내용을 따라갈 수 있게 구성했습니다.
Meta의 React 19로 현대적인 UI를 구축합니다. Hooks 기본기부터 Actions 기반 신기능, 상태 관리 전략, 서버 상태 페칭, 폼 검증, 컴포넌트 설계 패턴, 성능 최적화, 테스트, 접근성까지 실무 React 개발의 전 과정을 다룹니다.
Meta의 React 19로 현대적인 UI를 구축합니다. Hooks 기본기부터 Actions 기반 신기능, 상태 관리 전략, 서버 상태 페칭, 폼 검증, 컴포넌트 설계 패턴, 성능 최적화, 테스트, 접근성까지 실무 React 개발의 전 과정을 다룹니다. 이 가이드는 개념을 나열하기보다, 실제 프로젝트에서 판단해야 하는 순서대로 내용을 따라갈 수 있게 구성했습니다.
화면을 그리는 법에서 멈추지 않고, 상태, 데이터 요청, 라우팅, 접근성, 배포 단위까지 함께 봅니다.
글로 읽은 내용을 머릿속에 오래 남기려면 먼저 흐름을 그림으로 잡는 편이 좋습니다. 아래 두 그림은 React를 학습할 때 계속 되돌아볼 수 있는 기준 지도입니다.
React를 처음 펼칠 때는 세부 명령보다 큰 그림이 먼저입니다. 이 섹션에서는 앞으로 배울 개념들이 어떤 문제를 풀기 위해 등장했는지부터 잡아봅니다.
# Next.js (추천 — SSR/Server Components 포함)
npx create-next-app@latest my-app --typescript --tailwind --app
# Vite (SPA)
npm create vite@latest my-app -- --template react-ts
cd my-app && npm install && npm run dev
# React 19 코어만 별도 설치할 때
npm install react@19 react-dom@19여기서는 Hooks 기본기을 실제 코드와 함께 확인합니다. 예제를 그대로 따라 하기보다, 입력과 출력, 그리고 바뀌기 쉬운 부분이 어디인지 보면서 읽어보세요.
import { useState, useEffect, useCallback } from 'react';
function useFetch<T>(url: string) {
const [data, setData] = useState<T | null>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
let cancelled = false;
fetch(url)
.then(r => r.json())
.then(d => { if (!cancelled) setData(d); })
.finally(() => { if (!cancelled) setLoading(false); });
return () => { cancelled = true; };
}, [url]);
return { data, loading };
}여기서는 React 19 신기능을 실제 코드와 함께 확인합니다. 예제를 그대로 따라 하기보다, 입력과 출력, 그리고 바뀌기 쉬운 부분이 어디인지 보면서 읽어보세요.
import { useActionState, useOptimistic, use } from 'react';
// 폼 제출 상태(pending/error)를 자동으로 관리
function ProfileForm({ updateName }: {
updateName: (prev: { error?: string }, formData: FormData) => Promise<{ error?: string }>;
}) {
const [state, formAction, isPending] = useActionState(updateName, {});
return (
<form action={formAction}>
<input name="name" />
<button type="submit" disabled={isPending}>저장</button>
{state.error && <p>{state.error}</p>}
</form>
);
}
// 서버 응답 전에 UI를 낙관적으로 갱신
function LikeButton({ likes, addLike }: { likes: number; addLike: () => Promise<void> }) {
const [optimisticLikes, addOptimisticLike] = useOptimistic(likes, (state) => state + 1);
return (
<button onClick={async () => { addOptimisticLike(); await addLike(); }}>
좋아요 {optimisticLikes}
</button>
);
}
// Promise를 조건부로 읽기 (Suspense와 연동)
function Comments({ commentsPromise }: { commentsPromise: Promise<{ id: string; text: string }[]> }) {
const comments = use(commentsPromise);
return <ul>{comments.map(c => <li key={c.id}>{c.text}</li>)}</ul>;
}| API | 용도 |
|---|---|
| useActionState | form action의 pending/에러 상태를 자동으로 관리 |
| useOptimistic | 서버 응답 전에 UI를 낙관적으로 먼저 갱신 |
| use() | Promise·Context를 조건부로 읽고 Suspense와 자동 연동 |
| React Compiler | memo/useMemo/useCallback을 빌드 타임에 자동 삽입하는 최적화 |
여기서는 상태 관리 전략을 실제 코드와 함께 확인합니다. 예제를 그대로 따라 하기보다, 입력과 출력, 그리고 바뀌기 쉬운 부분이 어디인지 보면서 읽어보세요.
import { create } from 'zustand';
interface BearStore {
count: number;
inc: () => void;
dec: () => void;
}
export const useBearStore = create<BearStore>((set) => ({
count: 0,
inc: () => set((s) => ({ count: s.count + 1 })),
dec: () => set((s) => ({ count: s.count - 1 })),
}));| 규모 | 도구 | 적합한 경우 |
|---|---|---|
| 컴포넌트 로컬 | useState / useReducer | 해당 컴포넌트와 자식만 사용하는 상태 |
| 일부 트리 공유 | Context + useReducer | 테마, 인증 사용자 등 자주 안 바뀌는 값 |
| 앱 전역 client state | Zustand | UI 상태, 폼 위저드 단계 등 (보일러플레이트 최소) |
| 대규모/복잡한 도메인 | Redux Toolkit | 액션 이력 추적·시간여행 디버깅이 필요한 대형 팀 |
| 서버에서 온 데이터 | TanStack Query / SWR | 캐싱·재검증·낙관적 업데이트가 필요한 모든 API 데이터 |
여기서는 서버 상태 & 데이터 페칭을 실제 코드와 함께 확인합니다. 예제를 그대로 따라 하기보다, 입력과 출력, 그리고 바뀌기 쉬운 부분이 어디인지 보면서 읽어보세요.
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
function TodoList() {
const queryClient = useQueryClient();
const { data, isPending, error } = useQuery({
queryKey: ['todos'],
queryFn: () => fetch('/api/todos').then(r => r.json()),
staleTime: 30_000,
});
const addTodo = useMutation({
mutationFn: (title: string) =>
fetch('/api/todos', { method: 'POST', body: JSON.stringify({ title }) }),
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['todos'] }),
});
if (isPending) return <p>로딩 중...</p>;
if (error) return <p>불러오기 실패</p>;
return (
<ul>
{data.map((todo: { id: string; title: string }) => (
<li key={todo.id}>{todo.title}</li>
))}
</ul>
);
}여기서는 폼 처리 & 검증을 실제 코드와 함께 확인합니다. 예제를 그대로 따라 하기보다, 입력과 출력, 그리고 바뀌기 쉬운 부분이 어디인지 보면서 읽어보세요.
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
const schema = z.object({
email: z.string().email('올바른 이메일을 입력하세요'),
password: z.string().min(8, '8자 이상 입력하세요'),
});
type FormValues = z.infer<typeof schema>;
function LoginForm() {
const { register, handleSubmit, formState: { errors, isSubmitting } } =
useForm<FormValues>({ resolver: zodResolver(schema) });
const onSubmit = async (values: FormValues) => {
await fetch('/api/login', { method: 'POST', body: JSON.stringify(values) });
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input {...register('email')} />
{errors.email && <p>{errors.email.message}</p>}
<input type="password" {...register('password')} />
{errors.password && <p>{errors.password.message}</p>}
<button type="submit" disabled={isSubmitting}>로그인</button>
</form>
);
}여기서는 컴포넌트 설계 패턴을 실제 코드와 함께 확인합니다. 예제를 그대로 따라 하기보다, 입력과 출력, 그리고 바뀌기 쉬운 부분이 어디인지 보면서 읽어보세요.
import { Component, createContext, useContext, useState, type ReactNode } from 'react';
// Error Boundary — 하위 트리 에러가 앱 전체를 무너뜨리지 않게 격리
class ErrorBoundary extends Component<{ fallback: ReactNode; children: ReactNode }, { hasError: boolean }> {
state = { hasError: false };
static getDerivedStateFromError() { return { hasError: true }; }
componentDidCatch(error: unknown) { console.error(error); }
render() {
return this.state.hasError ? this.props.fallback : this.props.children;
}
}
// Compound Component — 내부 상태를 Context로 공유
const TabsContext = createContext<{ active: string; setActive: (id: string) => void } | null>(null);
function Tabs({ defaultTab, children }: { defaultTab: string; children: ReactNode }) {
const [active, setActive] = useState(defaultTab);
return <TabsContext.Provider value={{ active, setActive }}>{children}</TabsContext.Provider>;
}
Tabs.Tab = function Tab({ id, children }: { id: string; children: ReactNode }) {
const ctx = useContext(TabsContext)!;
return <button onClick={() => ctx.setActive(id)} aria-selected={ctx.active === id}>{children}</button>;
};여기서는 성능 최적화을 실제 코드와 함께 확인합니다. 예제를 그대로 따라 하기보다, 입력과 출력, 그리고 바뀌기 쉬운 부분이 어디인지 보면서 읽어보세요.
import { memo, useMemo, useCallback } from 'react';
// 불필요한 리렌더링 방지
const ListItem = memo(({ item }: { item: Item }) => (
<div>{item.name}</div>
));
function List({ items }: { items: Item[] }) {
// 비싼 계산 캐싱
const sorted = useMemo(
() => [...items].sort((a, b) => a.name.localeCompare(b.name)),
[items]
);
// 핸들러 캐싱
const handleClick = useCallback((id: string) => {
console.log(id);
}, []);
return sorted.map(item => <ListItem key={item.id} item={item} />);
}여기서는 테스트을 실제 코드와 함께 확인합니다. 예제를 그대로 따라 하기보다, 입력과 출력, 그리고 바뀌기 쉬운 부분이 어디인지 보면서 읽어보세요.
import { render, screen, fireEvent } from '@testing-library/react';
import { describe, it, expect } from 'vitest';
import { Counter } from './Counter';
describe('Counter', () => {
it('클릭하면 값이 증가한다', () => {
render(<Counter />);
const button = screen.getByRole('button', { name: /count/i });
fireEvent.click(button);
expect(screen.getByText('1')).toBeInTheDocument();
});
});이 섹션은 접근성을 실무 관점에서 정리합니다. 개념을 외우기보다, 어떤 상황에서 이 기준을 꺼내 쓸지에 초점을 맞춰보세요.
이 섹션은 다음 단계을 실무 관점에서 정리합니다. 개념을 외우기보다, 어떤 상황에서 이 기준을 꺼내 쓸지에 초점을 맞춰보세요.
React 실무 설계은 선택지가 갈리는 지점입니다. 표를 기준으로 각 방법의 쓰임새와 운영상의 차이를 비교해두면 이후 판단이 훨씬 쉬워집니다.
| 결정 지점 | 확인 질문 | 실무 기준 |
|---|---|---|
| 경계 | React 코드에서 바뀌기 쉬운 부분은 어디인가? | 입출력, 설정, 외부 연동, 핵심 규칙을 분리합니다. |
| 상태 | 상태가 어디서 생성되고 어디서 사라지는가? | 상태 소유자와 수명 주기를 코드로 드러냅니다. |
| 장애 | 실패했을 때 호출자는 무엇을 받는가? | timeout, fallback, error contract를 먼저 정합니다. |
이 섹션은 React 운영 기준을 실무 관점에서 정리합니다. 개념을 외우기보다, 어떤 상황에서 이 기준을 꺼내 쓸지에 초점을 맞춰보세요.
React 검증 전략은 선택지가 갈리는 지점입니다. 표를 기준으로 각 방법의 쓰임새와 운영상의 차이를 비교해두면 이후 판단이 훨씬 쉬워집니다.
| 품질 축 | 검증 방법 | 완료 기준 |
|---|---|---|
| 정확성 | 정상/실패 케이스를 자동화합니다. | 핵심 시나리오가 재현 가능하게 통과합니다. |
| 회귀 방지 | 버그 수정 시 동일 케이스를 테스트로 남깁니다. | 같은 장애가 다시 배포되지 않습니다. |
| 운영성 | 로그, 메트릭, 알림을 확인합니다. | 문제가 생겼을 때 원인 추적 경로가 있습니다. |