28. Race Condition이슈를 Promise로 해결하기
28. Race Condition이슈를 Promise로 해결하기

28. Race Condition이슈를 Promise로 해결하기

Authors
Date
Sep 6, 2025
Tags
Published
Published
Slug

1. Race Condition 이란

Race Condition은 둘 이상의 프로세스나 스레드가 동시에 공유자원에 접근할 때 실행 순서나 타이밍에 따라 결과가 달라지는 상황을 말한다. 자바스크립트는 단일 스레드지만, 네트워크 요청·타이머·I/O 같은 비동기 작업은 언제 완료될지 알 수 없어 완료 순서에 따라 로직의 결과가 달라질 수 있다는 의미에서 Race Condition이 발생할 수 있다.
function fetchUser(id) { fetch(`https://jsonplaceholder.typicode.com/users/${id}`) .then(response => response.json()) .then(data => console.log(`User ${id}: ${data.name}`)) } function main() { // userId=1 요청이 먼저 시작되었지만, // userId=2 요청이 더 빨리 끝날 수 있어 출력 순서가 뒤섞임 fetchUser(1) fetchUser(2) } main();
 
notion image

2. Promise로 해결하기

만약, 순서를 항상 일정하게 보장해야한다면 어떻게 할 수 있을까? 첫번째로 생각할 수 있는것은 fetchUser함수에서 Promise를 return 하도록 만드는 것이다.
function fetchUser(id) { return fetch(`https://jsonplaceholder.typicode.com/users/${id}`) .then(response => response.json()) .then(data => console.log(`User ${id}: ${data.name}`)) } function main() { // then 을 사용하여 체이닝 구성 fetchUser(1).then(() => fetchUser(2)) } main();
then 매소드를 사용하여 fetchUser(1)에서 리턴된 Promise가 fulfilled되었을때 fetchUser(2)를 실행하므로 순서가 보장된다.

3. 두번째 문제

하지만 아래 예제처럼 사용자의 이벤트에 의해 함수가 호출되는 상황이라면 조금 더 보완해야한다.
<!doctype html> <html lang="ko"> <head> <meta charset="utf-8" /> <title>fetchUser 랜덤 예제</title> </head> <body> <button id="runBtn">랜덤 사용자 가져오기</button> <script> function fetchUser(id) { return Promise.resolve( fetch(`https://jsonplaceholder.typicode.com/users/${id}`) .then(response => response.json()) .then(data => console.log(`User ${id}: ${data.name}`)) ); } // 버튼 클릭 시 1~5 사이 랜덤 id 선택 후 실행 document.getElementById('runBtn').addEventListener('click', () => { const randomId = Math.floor(Math.random() * 5) + 1; // 1~5 fetchUser(randomId); }); </script> </body> </html>
사용자가 버튼을 연속해서 클릭하면, 각 fetchUser 호출이 거의 동시에 실행되고 완료 순서는 네트워크 지연에 따라 달라진다. 따라서 실행 순서와 출력 순서를 항상 일치시키려면 체인을 동적으로 이어야 한다.

4. Chaining을 동적으로 연결하기

이전 문제와는 다르게 호출되는 함수가 미리 정해져있지 않다.
이를 해결하기 위해서는 Promise를 저장해두었다가 새로운 Promise가 발생한다면, 이전의 Promise 체이닝에 새로운 Promise를 연결하는 방법을 구현할 수 있다.
<script> function useChaining() { // 체인의 초기값 let queue = Promise.resolve() return function addChain(func) { // .then으로 체인을 이어붙이고, 두개의 프로퍼티를 넘겨주어 // fulfilled나 rejected상태 모두 상관없이 다음 함수를 호출하여 체인이 끊기지 않도록 함. queue = queue.then(func, func) return queue } } function fetchUser(id) { return Promise.resolve( fetch(`https://jsonplaceholder.typicode.com/users/${id}`) .then(response => response.json()) .then(data => console.log(`User ${id}: ${data.name}`)) ); } // useChaining호출로 체인 생성 const addChain = useChaining(); // 버튼 클릭 시 1~5 사이 랜덤 id 선택 후 실행 document.getElementById('runBtn').addEventListener('click', () => { const randomId = Math.floor(Math.random() * 5) + 1; // 새로운 체인을 이어 붙임 addChain(function() {fetchUser(randomId)}) }); </script>
  • useChaining은 내부적으로 queue라는 변수를 유지한다.
  • addChain을 호출할 때마다 queue = queue.then(func, func) 형식으로 이어 붙여, 항상 이전 실행이 끝난 뒤에 다음 실행이 시작되도록 한다.
  • 이때 addChain에 넘겨주는 것은 Promise가 아니라 Promise를 반환하는 함수여야 한다. 그래야 실행 시점이 체인에 맞춰 조절된다.
결과적으로 버튼을 여러 번 빠르게 눌러도 요청들이 동시에 실행되지 않고, 항상 앞선 요청이 끝난 뒤 다음 요청이 순서대로 실행된다.

5. React와 React-Query 에서 응용해보기

react와 react-query를 사용하는 환경에서 위와같은 문제를 겪는 경우에도 비슷한 해결책을 사용할 수 있다.
useMutation훅을 사용하는 경우에 race condition문제를 겪는다면 항상 순서를 보장하는 새로운 훅useSerialMutation을 만들어 해결할 수 있다.
export function useSerialMutation<TData, TError, TVars>( mutationOptions: UseMutationOptions<TData, TError, TVars> ): UseMutationResult<TData, TError, TVars> & { mutateSerial: (vars: TVars) => Promise<TData>; } { const base = useMutation<TData, TError, TVars>(mutationOptions); const runningRef = useRef<Promise<unknown>>(Promise.resolve()); const mutateSerial = useCallback( async (vars: TVars) => { const run = () => base.mutateAsync(vars); // 기존 체인이 있으면 그 뒤에 연결 runningRef.current = runningRef.current.then(run, run) // 타입 보정을 위해 as unknown as Promise<TData> return runningRef.current as unknown as Promise<TData> }, [base] ); return Object.assign(base, { mutateSerial }); }
  • 기본 useMutation은 여러 번 호출되면 요청이 동시에 실행되어 순서가 뒤섞일 수 있다.
  • useSerialMutation은 내부적으로 Promise 체인을 유지하여, 새로운 요청이 들어올 때마다 앞선 요청 뒤에 이어 붙인다.
  • 따라서 버튼을 여러 번 눌러도 요청이 항상 순차적으로 실행된다.
 
여기서 직접 실행
버튼을 누르면 함수 호출이 3번 동시에 이뤄진다.
 

Refs

serial-mutation
entrolECUpdated Sep 14, 2025