본문으로 건너뛰기
Previous
Next
React 완벽 가이드 2026 | Fiber 아키텍처부터 실무 패턴까지

React 완벽 가이드 2026 | Fiber 아키텍처부터 실무 패턴까지

React 완벽 가이드 2026 | Fiber 아키텍처부터 실무 패턴까지

이 글의 핵심

React 완벽 가이드에 대해 정리한 개발 블로그 글입니다. React는 선언적 UI를 컴포넌트와 상태로 표현하고, 내부적으로 Fiber 조정(reconciliation) 과 가상 DOM 비교(diffing) 을 통해 실제 DOM 변경을 최소화합니다. 이 글은 기본 사용법과 함께… 개념과 예제 코드를 단계적으로 다루며, 실무·학습에 참고할 수 있도록 구성했습니다. 관련 키워드:…

작년쯤 우리 쪽에 있던 내부 운영 대시보드가 있었는데, 필터·날짜·테이블이 한 화면에 몰려 있고 스프레드시트만큼 잘 안 돌아가는 전형적인 레거시였다. jQuery로 DOM 직접 만지다가 “React로 옮기면 팀원도 뽑기 쉽다”는 말에 말린 듯이 마이그레이션에 들어갔고, 그때가 내가 Fiber가 뭔지를 진짜로 느끼기 시작한 시점이었다. 키 입력할 때마다 전체가 버벅이던 건 React 탓이 아니라, 우리가 상태를 화면 꼭대기에 다 몰아넣고 useEffect로 fetch를 쏘던 탓이었다. 솔직히 이건 프레임워크 은총알이 아니다. 감이 없으면 똑같이 망가진다. 그래서 이 글은 “완벽한 교과서”가 아니라, 그때 터졌던 것들·나는 이렇게 쓴다는 쪽에 더 가깝다.

React가 하는 일을 한 문장으로 말하면 UI는 state의 함수고, 그걸 Fiber가 쪼개서 스케줄링하고, 가상 DOM으로 이전이랑 비교해서 진짜 DOM 손댈 곳만 고른다는 거다. “가상 DOM이 느리다/빠르다” 논쟁은 꽤 헛것 같다. 느린 건 대개 불필요하게 큰 트리를 자주 다시 그리는 것이고, 그건 Vue든 Svelte든 결국 똑같이 맞닥뜨린다. 내 입장에선 “가상 DOM이 느리다”보다 “우리가 그렇게 큰 diff를 샀는지”를 먼저 봐야 한다.

Vite로 새 프로젝트 띄울 때는 이 정도면 되고, 18+에서는 createRoot를 써라. StrictMode는 욕 나오는 이중 useEffect 맛보기로 유명하지만, 프로덕션에는 안 남는다는 점이 중요하다. 개발할 때만 지옥이고, 실서비스에서 사용자한테는 영향이 없다.

npm create vite@latest my-app -- --template react-ts
cd my-app
npm install
npm run dev
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import { App } from './App';

createRoot(document.getElementById('root')!).render(
  <StrictMode>
    <App />
  </StrictMode>
);

훅 예제는 문서에도 널렸는데, 위 대시보드에서 진짜로 통했던 건 useEffect에 취소 플래그를 두고 userId가 바뀌면 fetch를 끊는 패턴이었다. “경쟁 상황(race) 한 번 터지면” 검색 느릴 때 이전 응답이 나중에 덮어쓰는 그 버그 말인데, fetch만 바꾸지 말고 취소/무시까지 같이 봐야 한다. useMemo/useCallback은 “성능”이라는 이름으로 남용이 제일 잦다. 나는 측정(Profiler) 전에 memo를 붙이는 건 삼가는 편이다. “참조 동일성이 꼭 필요하다”가 아니면 대개 의미가 없다.

import { useState, useEffect, useMemo, useCallback } from 'react';

type User = { id: string; name: string };

export function UserPanel({ userId }: { userId: string }) {
  const [user, setUser] = useState<User | null>(null);

  useEffect(() => {
    let cancelled = false;
    (async () => {
      const res = await fetch(`/api/users/${userId}`);
      const data: User = await res.json();
      if (!cancelled) setUser(data);
    })();
    return () => {
      cancelled = true;
    };
  }, [userId]);

  const title = useMemo(() => (user ? `Hello, ${user.name}` : 'Loading…'), [user]);
  const onRetry = useCallback(() => {
    setUser(null);
  }, []);

  return <section aria-label={title}>{user?.name ?? '…'}</section>;
}

Fiber 이야기는 면접용으로는 자주 나오는데, 실전에서는 이렇게 이해하는 게 낫다. 예전 스택 reconciler는 메인 스레드를 한 번에 쭉 쓰는 쪽이었고, 16부터 작업을 잘라서 workInProgresscurrent를 왔다 갔다 하면서(연결 리스트, child/sibling/return) “렌더를 중간에 끊을 수 있게” 바뀐 것이다. 렌더 단계는 순수해야 하고 끊길 수 있고, 커밋 단계는 DOM 붙이는 쪽이라 중단이 안 되는 쪽. 그래서 “렌더 안에서 fetch 돌리지 마” 같은 말이 반복되는 거다. Concurrent로 가면 같은 렌더가 여러 번 시도되고 버려질 수 있어서, 부수효과를 렌더에 넣는 것이 더 위험해진다. 나는 useEffect vs useLayoutEffect도 그냥 “레이아웃 읽고 동기로 맞춰야 하면 useLayoutEffect, 대부분은 useEffect”로 끊는다. useLayoutEffect 남발하면 페인트 막힌다는 쪽이 더 현장에서 아프다.

useTransition/useDeferredValue는 그 대시보드에서 필터가 무거울 때 체감이 확 달랐다. 타이핑은 즉시, 큰 그래프/테이블은 뒤로 밀기. “동시성 켰다고 마법은 아님”인 건, 데이터가 10만 행이면 렌더를 미루는 것만으로는 부족하고 가상 스크롤·청크·서버 쪽 limit이랑 같이 가야 한다는 뜻이다. Zustand/Redux 붙일 때 useSyncExternalStore를 실제로 건드릴 일은 드물지만, “Concurrent에서 tearing 나면 스토어 구독 설계를 의심해라”는 건 알고 있으면 싸움의 레벨이 달라진다.

에러 바운더리는 “렌더 트리에서 터진 것”만 잡는다. 핸들러 안에서 throw한 건 그대로 날아간다. Suspense는 “로딩 경계”로 쓰기 좋은데, Next App Router랑 겹치면 RSC·클라이언트 경계가 한 번 더 머리 아파진다. 그래서 나는 “서버는 데이터·큰 의존성, 클라이언트는 상호작용” 정도의 선을 팀 룰로 못 박는 쪽이 실제로 효과가 있었다. 전역 Context자주 바뀌는 값을 넣어 놓고 “왜 전체가 리렌더되지?” 같은 건, 신입이 아니라 나도 가끔 낸다. 그럴 땐 Context 쪼개기나 외부 스토어를 솔직히 보는 편이 낫다.

운영 얘기를 표로 쓰지는 않을게(표는 문서 냄새가 나서). 관측이면 요청 ID랑 p95·p99, 안전이면 권한·감사 로그, 배포면 롤백 루트랑 카나리—이거 다 체크리스트로 쓰면 끝이 아니고, “우리 팀이 실제로 런북이 있는가”로 귀결된다. 스테이징이 프로덕션을 못 맞추면 재현이 안 돼서, 그건 React 문제가 아니라 문제다.

트러블슈팅도 표 대신 느낌으로: 간헐이면 레이스·타임아웃부터 의심, 느리면 N+1·캐시·동기 I/O, 메모리 늘면 구독 누수나 캐시 상한, 배포만 깨지면 환경 변수·lockfile—순서는 (1) 최소 재현 (2) 최근 배포/변경 범위 (3) 관측으로 가설 확인이다. C++ 시리즈 링크는 여기 전혀 안 맞는데, 예전에 붙어 있던 내부 링크 가짜 FAQ는 그냥 지웠다. React 더 파려면 React 18 심화TypeScript 가이드가 낫다.

내부 링크: Million.js 가이드 · Lit 웹 컴포넌트 · Million.js 성능

React, Fiber, Virtual DOM, Hooks, Concurrent—이 키워드로 찾아왔다면, 이 글의 요지는 “문서 흉내는 그만 내고, 프로젝트 한 번 망가뜨렸다가 살릴 때 제대로 이해된다” 쪽에 가깝다.

flowchart TD
  A[입력·요청·이벤트] --> B[파싱·검증·디코딩]
  B --> C[핵심 연산·상태 전이]
  C --> D[부작용: I/O·네트워크·동시성]
  D --> E[결과·관측·저장]
sequenceDiagram
  participant C as 클라이언트/호출자
  participant B as 경계(프로세스·런타임·게이트웨이)
  participant D as 의존성(외부 API·DB·큐)
  C->>B: 요청/이벤트
  B->>D: 조회·쓰기·RPC
  D-->>B: 지연·부분 실패·재시도 가능
  B-->>C: 응답 또는 오류(코드·상관 ID)

불변 조건, 결정성, 경계 비용, 백프레셔 같은 말은 서버/시스템 글에도 똑같이 쓰인다. React만 잘해도, API가 지옥이면 UX는 지옥이다. 대시보드 엔드투엔드로 생각해보면, 입력 스키마 고정·계측·실패 주입·롤백·부하 검증 정도는 “프론트 혼자”가 아니라 에서 같이 쳐야 한다. 의사코드로 쓰면 handle 안에서 validate → authorize → domainCore → persist(멱등) → metrics 흐름이고, 이건 React랑 무관하게 경계가 같은 거다.

handle(request):
  ctx = newCorrelationId()
  validated = validateSchema(request)        // 경계에서 거절
  authorize(validated, ctx)                  // 권한·테넌트
  result = domainCore(validated)             // 순수에 가까운 규칙
  persistOrEmit(result, idempotentKey)       // I/O: 멱등·재시도 정책
  recordMetrics(ctx, latency, outcome)
  return result

마지막으로 내 의견: React를 사랑/혐오로 나누는 건 쓸모가 없다. “우리 팀이 이미 쓰고 있고, 생태계·채용·라이브러리가 맞다”면 그냥 잘 쓰면 되고, “번들/복잡도가 너무 싫다”면 다른 걸로 옮기면 된다. 카더라가 아니라, 위에서 말한 대시보드처럼 한 번쯤 상태를 쪼개고, useEffect에 반칙하지 않고, Concurrent를 “체감”해보면, 문서에 없던 설명이 머릿속에 남는다.