비동기 처리에서의 Race Condition 정리

2025. 12. 11. 16:18·FRONTEND/React

최근 비동기 처리와 관련해서 Race Condition(경쟁 상태)이라는 용어를 처음 접하게 되었다.

JS는 싱글 스레드 기반이다. 근데 왜 이러한 문제가 생기는지 궁금해졌고, Race Condition이 무엇인지, 어떤 상황에서 발생하는지 정리해 봤다.

 

1. Race Condition이란

: 둘 이상의 비동기 작업이 같은 자원(= 상태)을 갱신하려고 할 때, 실행 순서에 따라 결과가 달라지는 문제

 

핵심은 순서 제어 불능 !! (= 비동기 작업의 순서를 확실히 제어 X)

비동기 작업은 요청한 순서대로 끝나지 않는다.

 

따라서 먼저 보낸 요청의 응답이 나중에 보낸 요청보다 늦게 도착하면, 화면이나 데이터가 예상과 다른 상태를 보이게 된다.

이런 상황이 바로 Race Condition이다.

 

2. JS는 싱글 스레드인데 Race Condition이 왜 생길까?

JavaScript는 싱글 스레드로 동작하지만, 비동기 로직은 별도 환경에서 처리된다.

 

- 네트워크 요청(fetch)

- 타이머(setTimeout)

- 브라우저 Web APIs

이러한 비동기 작업들의 결과는 이벤트 루프를 통해 메인 스레드로 전달되는데 이 과정에서 완료된 순서대로 콜백이 실행된다.

 

즉, 논리적 병렬성이 존재하고, 이러한 이유로 싱글 스레드 환경에서도 Race Condition이 발생한다.

 

3. React에서 Race Condition이 자주 발생하는 이유

React에서는 다음 두 요소 때문에 Race Condition이 쉽게 드러난다.

 

3-1. 상태 업데이트는 비동기적

하나의 state 갱신이 완료되기 전에 또 다른 갱신이 들어올 수 있다.

 

3-2. UI는 state 하나만 잘못 들어와도 즉시 오염

늦게 도착한 비동기 결과가 최신 상태를 덮어쓰는 문제가 생긴다. 

 

 

4. 실제 React에서 발생하는 Race Condition 예시

어떤 상황에서 Race Condition이 발생하는지 찾아봤다.

 

4-1. 검색 요청 순서 뒤바뀜

useEffect(() => {
  fetch(`/api/search?q=${query}`)
    .then(res => res.json())
    .then(setResult);
}, [query]);

 

 

[예시]

  1. "ab" 검색 → 요청 A 전송
  2. "abc" 검색 → 요청 B 전송
  3. B가 먼저 응답
  4. A가 늦게 도착하며 state를 덮어씀

[결과]

화면에는 "ab" 검색 결과가 보임 → 최신 상태가 사라짐


이것을 전형적인 Race Condition이라고 본다.

 

4-2. 컴포넌트 언마운트 후 setState 호출

useEffect(() => {
  let ignore = false;

  fetchUser().then(data => {
    if (!ignore) setUser(data);
  });

  return () => { ignore = true; };
}, []);

탭 이동처럼 컴포넌트가 빠르게 교체되는 환경에서 이미 사라진 컴포넌트가 늦게 돌아온 응답으로 state(상태)를 갱신하려 하면 문제가 발생한다.

 

4-3. stale state 문제

setTimeout(() => {
  setCount(count + 1); // count는 이미 오래된 값
}, 1000);

비동기 내부의 state는 렌더 시점의 캡처된 값이기 때문에 여러 번 클릭해도 기대와 다른 결과가 나올 수 있다.

 

5. Race Condition 해결 방법

더 많은 방법이 존재하지만 대표적인 4가지 방법을 살펴보겠다.

 

5-1. 이전 요청은 반드시 취소하거나 무효화하기

AbortController 사용 (권장)

const controller = new AbortController();

fetch(url, { signal: controller.signal })
  .then(res => res.json())
  .then(setResult);

return () => controller.abort(); // 이전 요청 취소

 

요청 ID 비교 방식

const idRef = useRef(0);
const id = ++idRef.current;

fetch(url)
  .then(res => res.json())
  .then(data => {
    if (id === idRef.current) setResult(data);
  });

최신 요청의 응답만 반영되도록 보장한다.

 

 

5-2. 비동기 내부에서 state를 직접 사용하지 않기

함수형 업데이트로 stale state 방지

setCount(prev => prev + 1);

 

 

5-3. useRef로 최신 값 보관하기

const latestQuery = useRef(query);
useEffect(() => {
  latestQuery.current = query;
}, [query]);

비동기 콜백 안에서 항상 최신 값을 읽을 수 있다.

 

 

5-4. React Query 사용하기

대부분의 Race Condition이 라이브러리 내부에서 자동 관리된다.
(중복 요청 제거, 최신 요청만 반영, 취소 처리 등)

 

6. 더 중요한 비동기 작업을 먼저 처리하려면?

Race Condition을 이해하다 보니 다음 질문이 생각났다.

 

"여러 개의 비동기 작업이 동시에 발생한다면, 어떻게 더 중요한 작업을 먼저 처리하도록 만들 수 있을까?"

 

- 사용자의 가장 최근 입력값이 가장 중요할 때 (검색창)

- 화면에 바로 반영되어야 하는 긴급 UI 업데이트

 

아마 '중요도가 낮은 것보다 중요한 것의 결과가 우선 적용되도록 제어하는 것'이 핵심 아이디어라고 생각한다.

 

처음에는 단순하게 '더 중요한 작업이 걸리는 시간을 예측해서 먼저 실행시키고, 그 작업이 끝나고 일정 시간이 지나면 나머지 작업을 실행하는 방식'을 떠올렸다.

 

하지만 이 방식은 실제 비동기 환경에서는 거의 불가능하다. 비동기 작업의 걸리는 시간을 미리 알 수 없기 때문이다.

 

네트워크 속도나 서버 응답 시간은 매번 달라진다. 그리고 비동기 작업은 요청 순서와 상관없이 준비되는 순서대로 완료된다.

즉, '이 작업이 더 빨리 끝날 것이다'를 예측하는 것은 의미가 없다.

 

따라서 시간을 기준으로 우선순위를 조절하는 방식은 실질적으로 적용하기 어렵다.

그렇다면 실제 개발에서는 어떻게 더 중요한 비동기 작업을 먼저 반영할까?

 

찾아보니 다음 2가지 방식을 많이 사용한다고 한다.

 

 

6-1. 최신 작업(= 더 중요한 작업)만 유효하게 만들기

요청 ID 또는 AbortController를 사용하면 자연스럽게 '더 중요한 작업 = 가장 최신 작업'이라는 규칙을 만들 수 있다.

 

- 최신 요청만 state 갱신

- 이전 요청은 취소하거나 응답이 와도 무시

 

즉, 자연스럽게 가장 중요한 작업이 이기는 구조가 된다.

요청 ID 방식은 중요도(최신성)을 기준으로 비동기 작업의 우선순위를 부여하는 전략이다.

 

6-2. React 18의 startTransition으로 우선순위 분리하기

React 18에서는 UI 업데이트를 긴급 / 비긴급으로 나눌 수 있다.

startTransition(() => {
  // 급하지 않은 상태 업데이트
});

- 클릭, 입력 등 빨리 반영되어야 하는 업데이트 = 긴급 !!

- 무거운 리스트 렌더링이나 데이터 패칭 = 비긴급

 

이렇게 하면 React가 내부적으로 더 중요한 UI 업데이트를 먼저 반영하고,
부하가 큰 작업은 뒤로 미뤄 자연스러운 우선순위가 만들어진다.

 

마무리

 

Race Condition = 비동기 흐름 안에서 '이전 작업의 결과가 최신 상태를 덮어쓰는 것'을 막는 문제

 

- 가장 최신 요청만 반영되게 하기

- 오래된 상태를 기반으로 계산하지 않기

 

비동기 로직이 많은 UI일수록 이 개념을 명확히 이해하는 것이 중요하다는 점을 배웠다 !!

'FRONTEND > React' 카테고리의 다른 글

리액트 훅 | useContext에 대해 알아보기  (1) 2025.08.18
리액트 훅 | useActionState에 대해 알아보기  (3) 2025.08.14
리액트 훅 | useMemo에 대해 알아보기  (6) 2025.08.13
리액트 훅 | useCallback에 대해 알아보기  (8) 2025.08.12
리액트 훅 | useEffect에 대해 알아보기  (5) 2025.08.11
'FRONTEND/React' 카테고리의 다른 글
  • 리액트 훅 | useContext에 대해 알아보기
  • 리액트 훅 | useActionState에 대해 알아보기
  • 리액트 훅 | useMemo에 대해 알아보기
  • 리액트 훅 | useCallback에 대해 알아보기
uoaheu
uoaheu
uoaheu 님의 블로그 입니다.
  • uoaheu
    uoaheu 님의 블로그
    uoaheu
  • 전체
    오늘
    어제
    • 분류 전체보기 (50)
      • 알고리즘 (7)
      • CS (9)
      • FRONTEND (9)
        • React (12)
        • Kotlin (1)
        • JS (5)
        • HTML (2)
      • SQL (2)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    toss 분석
    리액트usestate
    토스분석
    BFS
    mysql
    혼자서 공부하는 네트워크
    toss uiux
    토스 uiux
    알고리즘
    useactionstate
    엘지유플러스유레카프론트엔드
    boj25418
    mysql 피벗테이블
    유레카3기
    mysql로 피벗테이블만들기
    백준1926번
    이더넷프레임
    멀티캠퍼스it부트캠프
    부트캠프후기
    토스 앱 분석
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.3
uoaheu
비동기 처리에서의 Race Condition 정리
상단으로

티스토리툴바