⚛️ 18. React 동시성(Concurrent) 매운맛

2026년 4월 30일 수정됨

📋 개요

React 18에 도입된 동시성 엔진의 진짜 의도를 파헤치고, useTransition과 useDeferredValue를 통해 메인 스레드 렌더링 렉을 양보(Yield)하는 현업의 필수 기법을 전수합니다.

🎯 이 섹션을 읽고 나면:

  • 키보드에 한 글자를 칠 때마다 화면 전체가 뚝뚝 끊기는 '블로킹(Blocking) 렌더링' 늪의 진실을 마주한다.
  • useTransition 을 활용하여 무거운 렌더링을 백그라운드로 미루고 유저의 키보드 입력을 최우선 권력으로 모시는 법을 체득한다.
  • Debounce/Throttle 외부 훅을 버리고, 진정한 선언적 리액트 타이밍 훅인 useDeferredValue의 동작 원리를 이해할 수 있다.

📋 목차


📌 이 문서를 읽기 전에

⏱️ 예상 읽기 시간: 10분 / 핵심 파트: 6분

🗺️ 이 문서의 배경 세계관: '영수네 커뮤니티'

  • 영수(PM):"영호 님, 유저 검색 페이지에서 검색창에 '김철수'라고 3글자를 치는데, 한 글자 칠 때마다 하단에 있는 1만 개 데이터짜리 테이블이 렌더링되느라 키보드가 안 먹혀요!"
  • 영철(신입):"아... 그건 setTimeout으로 Debounce 500ms 묶어놔서 유저가 타자 다 칠 때까지 무시하도록 꼼수를 부려야 합니다만... UX가 좀 답답해지긴 하죠."
  • 영호(리드): "영철 님, 우리는 React 18 시대를 살고 있습니다. 이제 앱 렌더링을 '시작하면 못 멈추는' 단거리 달리기 구조가 아닙니다. 렌더링 도중 멈추고 딴짓(키보드 입력)을 먼저 처리하는 동시성(Concurrent) 엔진으로 진입할 때입니다."

🤔 왜 알아야 하는가: 단거리 달리기와 심장마비

React 17 이하 버전에선, 렌더링은 동기식(Synchronous) 이었습니다. 한 번 렌더링이 시작되면, 브라우저는 그 렌더링 연산이 끝날 때까지 심장이 멎은 듯 모든 사용자 입력을 철저히 틀어막습니다(메인 스레드 블로킹).

// ❌ 렉 걸리는 검색창 (React 17 이하의 중단의 렌더링)
function UserSearch() {
  const [keyword, setKeyword] = useState('');
 
  // 🚨 한 글자 칠 때마다 만 개의 무거운 리스트가 리렌더링 (블로킹!)
  return (
    <div>
      <input
        value={keyword}
        onChange={(e) => setKeyword(e.target.value)} // 유저 입력 1순위 위협
      />
      <MegaHeavyUserList listFilter={keyword} /> {/* 500ms 걸리는 렌더링 */}
    </div>
  );
}

이 상황의 문제점:

  • 사용자가 "안녕" 두 글자를 빠르게 쳐도, 'ㅇ'을 그리고 밑의 표를 0.5초 동안 그리느라 'ㅏ' 입력을 받아줄 브라우저의 체력이 없습니다. 키보드가 씹힙니다!
  • 과거엔 이걸 무리하게 피하려고 Lodash의 _.debounce를 써서 타자를 멈추고 0.5초 뒤에 표를 그리게 했지만, 반사속도가 이질적입니다.

🏗️ 비유로 먼저 이해하기

🧒 5살에게 설명한다면?

  1. **과거의 화가 (동기식 렌더링):**왕(유저)이 초상화를 부탁함. 화가가 붓을 쥐고 그리기 시작하면 문을 아예 잠가버림. 왕이 "야! 물 좀 가져와!"라고 두들겨도 그림을 10시간 다 그릴 때까지 무시함.
  2. 리액트 18의 붓놀림 (동시성 - Concurrent): 화가(React 18) 는 슥삭슥삭 그림을 그리다가, "왕(유저) 이 문을 두드려 키보드 한 글자를 치면 즉시 붓을 허공에 멈춥니다(Yield)."
    기민하게 뛰어나가 왕의 요구사항(키보드 타이핑 화면 표시) 부터 0.1초 만에 깔끔하게 처리하고, 다시 캔버스로 돌아와서 아까 멈췄던 그림(무거운 1만 개 표 필터링) 을 틈틈이 이어서 그립니다!

🧩 극의 체득: 동시성 API 발동하기

이제 영철이의 렉망진창 코드를 **React 18의 useTransition**을 써서 구원해 봅시다.

✅ 방법 1: useTransition (너는 나중에 그려져라!)

핵심 문법: **긴급한 업데이트(키보드 화면 표시)**와 **안 긴급한 업데이트(아래 표 필터링)**를 분리하여 생명을 부여합니다.

import { useState, useTransition } from 'react';
 
function SmoothSearch() {
  const [keyword, setKeyword] = useState(''); // 긴급한 UI (인풋 창 텍스트)
  const [deferredKeyword, setDeferredKeyword] = useState(''); // 안 긴급 (필터링)
 
  // 🎯 동시성의 심장! (isPending은 이면 렌더링 중인지 여부)
  const [isPending, startTransition] = useTransition();
 
  const handleChange = (e) => {
    // 1️⃣ [극강의 긴급] - 키보드 타이핑 화면에는 0.001초만에 즉시 그려져라!
    setKeyword(e.target.value);
 
    // 2️⃣ [우아한 양보] - 아래 표 필터링용 데이터 렌더링은 여유로울 때 뒤에서 해라!
    startTransition(() => {
      setDeferredKeyword(e.target.value);
    });
  };
 
  return (
    <div>
      <input value={keyword} onChange={handleChange} />
 
      {/* 백그라운드에서 뒤늦게 렌더링 되는 걸 isPending으로 흐리게 처리! */}
      <div style={{ opacity: isPending ? 0.5 : 1 }}>
        <MegaHeavyUserList listFilter={deferredKeyword} />
      </div>
    </div>
  );
}

이 코드가 발동되는 직관적 프로세스:

  • "안녕"이라고 매우 빠른 속도로 타자를 쳐도, keyword실시간 메인 스레드 1순위 권력을 가지므로 화면 창에 렉 없이 그대로 타이핑됩니다.
  • 한편 startTransition 안에 갇힌 하단 1만 줄 표 그리기 명령은 렌더링 짬짬이 멈췄다 재개하며('Time Slicing' 기법), 키보드 입력을 절대 씹지 않고 가장 최신 텍스트 필터 상태(최종 글자 '녕')로 자연스럽게 귀결됩니다.

✅ 방법 2: useDeferredValue (입력 창 권한이 나에게 없을 때)

때로는 남이 만든 라이브러리 프롭스로 저 무거운 keyword 글자를 내려받아 올 때가 있습니다. 이 땐 startTransition으로 감쌀 타이밍조차 놓쳤죠? 이때 쓰는 마법의 도구입니다.

import { useDeferredValue } from 'react';
 
// 외부에서 텍스트(text)를 속수무책으로 내려받은 무거운 컴포넌트
function ListBox({ text }) {
  // 🎯 "React야, 이 글자는 이면 세계에서 천천히 계산(Deffer)해서 반영해라!"
  const deferredText = useDeferredValue(text);
 
  return (
    <>
      <p>실제 유저 타이핑 (즉시 렌더): {text}</p>
 
      {/* ⚠️ 오직 deferred된 지연 텍스트에만 무거운 표를 연결한다! */}
      <MegaHeavyUserList listFilter={deferredText} />
    </>
  );
}

방금 debounce 라이브러리를 완전히 대체할 수 있습니다. 타임아웃(200ms)처럼 인위적인 고정 시간을 기다리는 게 아닙니다. 유저의 기기 성능(CPU)이 좋으면 0.01초 만에 업데이트되고, 성능이 낮은 기기이면 0.5초 뒤에 뜨게 알아서 **자연 방임형 최적화(Native Schedulling)**를 해 준 것입니다.


🏁 이번에 배운 내용 총정리

관점과거의 Debounce (setTimeout)현재의 Concurrent (useTransition)
업데이트 타이밍정해진 강제 고정시간 (예: 500ms) 무조건 대기내 컴퓨터 상태와 유저 입력 횟수를 보아가며 지능적 양보(Yield)
멈춤 기능 (Interruptible)못 멈춤. 한 번 시작한 렌더링은 심장마비 유발렌더링 도중 붓을 내던지고 긴급한 유저 입력부터 먼저 그려줄 수 있음 (Time Slicing)
목표 철학"일단 서버/연산 발생하는 것만 막자""사용자 클릭/키보드 감각이 끊기는 것만큼은 무조건 막자!"

📝 마무리 퀴즈

Q1. 당신이 만드는 쇼핑몰 검색창에 사용자가 아이폰으로 무거운 렌더링을 유발하는 단어를 타이핑했습니다. 이때 startTransition 내부에 묶어둔 검색 결과 리스트 상태 업데이트 때문에 발생하는 현상에 대한 올바른 설명은 무엇입니까?

  • A) React는 검색 결과 리스트 상태 업데이트를 즉시 실행하며, 대신 사용자의 키보드 입력 처리를 뒤로 미루어 화면이 일시정지(프리징)된다.
  • B) startTransition 내부의 강제적 setTimeout 로직 때문에 모든 렌더링이 무조건 1초(고정 시간) 뒤에야 서버로 요청을 날리게 변환된다.
  • C) React는 일단 가장 긴급한 입력 필드의 글자 렌더링부터 처리하고, 이후 남은 브라우저 자투리 시간을 활용하여 백그라운드 캔버스에서 무거운 검색 결과 리스트 작업을 그리기 시작한다(낮은 우선순위 부여). 중간에 또 다른 키보드 입력이 들어오면 기존 백그라운드 렌더링은 기꺼이 폐기/중단된다.

정답: C

💡 상세 해설: 동시성 렌더링(Concurrent)의 백미입니다. C가 말하듯, 우선순위가 낮춰진 업데이트 작업(startTransition)은 렌더링 도중 언제든 더 급한 작업(onChange)에게 멱살을 잡혀 중단될 수 있는 유연성을 거머쥐었습니다.

Q2. 기존 useDebounce(value, 300) 커스텀 훅과, 최신 리액트의 useDeferredValue(value) 훅의 가장 근본적인 철학적/작동 방식의 차이를 단지 한 줄의 주관식으로 비유/요약해 보세요.

정답 및 주관식 해설:
"디바운스는 컴이 좋든 나쁘든 무리하게 초를 재서 무조건 300ms 뒤에 벽돌처럼 그림을 그리기 시작하는 '인위적인 지연(고정 지연)'이지만, useDeferredValue는 유저가 자판을 안 치고 CPU가 한가하다는 판단이 들면 0.001초 만에 최강급 컴퓨터 속도로 화면을 뽑아내는 유기적이고 똑똑한 '지능형 양보 패턴(유동 지연)'이다."

Q3. 영철이의 테스트 타임: 영수네 커뮤니티 검색 화면에서 입력창의 글자는 즉시 보여야 하고, 5천 개 스터디 카드 필터링 결과는 조금 늦게 따라와도 됩니다. 영호가 startTransition을 제안할 때 가장 중요한 분리는 무엇인가요?

  • A) 입력창의 query 업데이트까지 transition 안에 넣어 모든 상태를 낮은 우선순위로 만든다.
  • B) 입력창 값처럼 사용자의 직접 조작에 반응하는 상태는 긴급 업데이트로 두고, 무거운 결과 리스트 계산/렌더링만 transition이나 deferred value로 낮은 우선순위에 둔다.
  • C) startTransition은 네트워크 요청을 자동으로 취소하므로 API 호출만 감싸면 렌더링 렉도 사라진다.

정답: B

💡 상세 해설: 동시성 API는 모든 일을 늦추는 버튼이 아닙니다. 사용자가 타이핑한 글자, 클릭 피드백, 포커스 이동처럼 즉시 반응해야 하는 업데이트는 긴급하게 처리되어야 합니다. 반대로 큰 리스트 렌더링처럼 "조금 늦게 보여도 되는 작업"은 낮은 우선순위로 내려 React가 중간에 끊고 다시 시작할 수 있게 해야 합니다. 영호의 리뷰 포인트는 빠르게 보일 것과 늦어도 되는 것을 명확히 분리했는지입니다.

🐣 영철이의 퇴근 일기

React 18 동시성을 예전에는 "렌더링이 더 빨라지는 기능" 정도로 생각했다. 오늘 다시 보니 핵심은 속도 자체가 아니라 우선순위였다. 사용자가 지금 손으로 만지는 입력과, 화면 아래에서 늦게 따라와도 되는 결과 리스트를 같은 급으로 취급한 게 문제였다.

💡 "useTransition은 느린 일을 빠르게 만드는 주문이 아니라, 급한 일과 늦어도 되는 일을 React에게 알려주는 우선순위 표시다."

영호 님이 "입력값 자체를 transition에 넣으면 사용자는 키가 씹힌다고 느낀다"고 짚어준 게 특히 컸다. 내일부터 검색 화면을 볼 때는 입력 상태, URL 동기화, 결과 리스트 렌더링, 네트워크 요청을 따로 나눠서 어떤 업데이트가 긴급한지 표시하겠다. 이제 영철은 디바운스 숫자를 감으로 찍는 사람이 아니라, 사용자가 기다려도 되는 일을 분류하는 사람이 되어야 한다.