코드노트

Race Condition 정리 및 해결방법 본문

Code note/Error문제해결

Race Condition 정리 및 해결방법

코드노트 2024. 8. 29. 17:17

Race Condition?

- 두 개 이상의 작업이 동시에 실행되거나 예상치 못한 순서로 완료될 때 발생하는 문제점이다.
리액트를 사용하면서 한번쯤은 만나본 문제이기도 하다. 이로 인해서 데이터 상태가 예측할 수 없는 상태로 변경될 수 있다.
특히 비동기 코드에서는 필수적으로 생각하고 코드를 작성해야한다!


그럼 Race Condition는 왜 일어나며 어떤 문제점을 가지고 있을까?


- 여러 비동기 작업이 동시에 실행되면서 서로 경쟁하는 상황이 발생할 수 있다. 비동기 작업이 순차적으로 이뤄지지 않고 병렬적으로 실행되기 때문에 동시에 업데이트가 된다면 예상치 못한 상태 변경이 발생할 수 있다.
- 리엑트를 사용할 때 컴포넌트가 비동기적으로 데이터를 가져오고 동시에 setState를 통해 상태를 업데이트를 할때 주로 발생한다.
- 어? 나는 이런적이 없는데? 아니 코드를 100번, 1,000번 실행했을 때 실행 결과가 같아야하지 않을까? 단 한번이라도 Race Condition가 발생한다면 그건 꼭 해결해야하는 문제이다.
 


useEffect – React

The library for web and native user interfaces

react.dev

- react 공식 dev에서도 이야기를 하고 있는것을 볼 수 있다. 참고!


React에서의 Race Condition 예시

import React, { useState, useEffect } from 'react';

function UserProfile({ userId }) {
    const [userData, setUserData] = useState(null);

    useEffect(() => {
        async function fetchData() {
            const response = await fetch(`https://api.example.com/user/${userId}`);
            const data = await response.json();
            setUserData(data);
        }

        fetchData();
    }, [userId]);

    return (
        <div>
            {userData ? <h1>{userData.name}</h1> : <p>Loading...</p>}
        </div>
    );
}

- 이 코드는 userId를 props로 받아서 userId가 변경될때마다 fetch로 api요청 후 data를 setUserData로 상태를 변경시켜준다.
- 여기서 문제 userId가 계속해서 바뀌고 이전 요청이 나중에 도착하여 잘못된 userData로 변경될 수 있다.
- 이 문제는 then을 사용해도되지 않나요? 라고도 하지만 계속해서 userId가 바뀐다면 비동기로 진행된 fetch는 언제 data를 가져와서 어떤 데이터가 set함수에 적용될지 완벽하게 예상할 수 없다.


Race Condition 해결 방법

 

1. Cleanup 함수 사용 (useEffect)

import React, { useState, useEffect } from 'react';

function UserProfile({ userId }) {
    const [userData, setUserData] = useState(null);

    useEffect(() => {
        let isMounted = true;
        async function fetchData() {
            const response = await fetch(`https://api.example.com/user/${userId}`);
            const data = await response.json();
            if (isMounted) {
                setUserData(data);
            }
        }
        fetchData();
        return () => {
            isMounted = false;
        };
    }, [userId]);

    return (
        <div>
            {userData ? <h1>{userData.name}</h1> : <p>Loading...</p>}
        </div>
    );
}

- isMounted 변수를 통해서 컴포넌트가 마운트되어있는지 확인한다.
- fetchData함수로 비동기 작업이 완료된 후 isMounted가 true일때만 set함수로 userData를 업데이트 한다.
- 이후 isMounted를 false로 처리하여 컴포넌트가 언마운트 상태에서는 set함수를 호출하지 않게하여 race condition로 인한 불필요한 상태 업데이트를 방지할 수 있다.


2. AbortController 사용

import React, { useState, useEffect } from 'react';

function UserProfile({ userId }) {
    const [userData, setUserData] = useState(null);

    useEffect(() => {
        const controller = new AbortController();
        const signal = controller.signal;
        async function fetchData() {
            try {
                const response = await fetch(`https://api.example.com/user/${userId}`, { signal });
                const data = await response.json();
                setUserData(data);
            } catch (error) {
                if (error.name === 'AbortError') {
                    console.log('Fetch aborted');
                } else {
                    console.error('Fetch error:', error);
                }
            }
        }
        fetchData();
        return () => {
            controller.abort();
        };
    }, [userId]);

    return (
        <div>
            {userData ? <h1>{userData.name}</h1> : <p>Loading...</p>}
        </div>
    );
}

- AbortController는 비동기 요청을 취소시킬 수 있다. 네트워크 요청을 명시적으로 중단시켜 Race Condition를 방지할 수 있다.
- signal 객체는 fetch 요청에 전달되고 요청이 중단될 때 이 신호를 감지할 수 있다. fetch 함수에 signal을 전달하여 요청이 중단될 수 있도록 설정한다. 요청이 만약 성곡적으로 완료되면 set함수에 상태를 업데이트하고 요청이 중단되면 따로 에러 처리!
- cleanup함수는 컴포넌트가 언마운트 되거나 userId가 변경될때 호출 되는데 이때 controller.abort를 호출하여 현재 진행중인 요청을 취소할 수 있다.
- 만약 응답이 도착하기 전에 컴포넌트가 언마운트 되거나 userId가 변경된 이후에도 비동기 작업이 상태를 업데이트 하지 않도록 한다.
- AbortController를 사용하여 안전하게 관리할 수 있으며, isMounted와 같이 따로 변수를 통해 플래그를 사용하지 않고도 race condition를 해결할 수 있다.
 
 

AbortController - Web API | MDN

AbortController 인터페이스는 하나 이상의 웹 요청을 취소할 수 있게 해준다.

developer.mozilla.org


3. Suspense 사용

import React, { Suspense } from 'react';

// 비동기 데이터를 처리하는 함수
function fetchData() {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve('Data loaded');
    }, 2000);
  });
}

// 데이터를 감싸는 함수
function wrapPromise(promise) {
  let status = 'pending';
  let result;

  let suspender = promise.then(
    (r) => {
      status = 'success';
      result = r;
    },
    (e) => {
      status = 'error';
      result = e;
    }
  );

  return {
    read() {
      if (status === 'pending') {
        throw suspender;
      } else if (status === 'error') {
        throw result;
      } else if (status === 'success') {
        return result;
      }
    },
  };
}

const resource = wrapPromise(fetchData());

// 비동기 데이터를 표시하는 컴포넌트
function DataComponent() {
  const data = resource.read();
  return <div>{data}</div>;
}

// 앱 컴포넌트
export default function App() {
  return (
    <div>
      <h1>Suspense Example</h1>
      <Suspense fallback={<p>Loading...</p>}>
        <DataComponent />
      </Suspense>
    </div>
  );
}

- 비동기 요청이 완료되기 전에 컴포넌트는 로딩 상태를 표시한다.
- 데이터를 받기 전까지 fallback UI를 보여주며 데이터를 기다린다.
- 데이터가 성공적으로 로드되면 로딩 상태를 종료하고 데이터가 준비된 상태로 컴포넌트를 렌더링 한다.
- Suspense는 Concurrent Mode와 함께 사용되며, 여러 비동기 작업이 동시에 실행되더라도 상태를 일관되게 유지 한다. 데이터가 완료되기 전까지는 로딩 상태를 보여주고 데이터가 준비되면 그때 최신상태로 업데이트 한다.
- 데이터 요청이 중복되거나 Race Condition이 발생한다면, 마지막으로 완료된 요청의 결과만 실제로 적용된다.
- Suspense가 최신 상태의 데이터를 항상 렌더링하기 때문에 자연스럽게 해결된다.
 
 
 

Suspense를 사용해서 Race Condition, Waterfall 문제 해결하기

React에서 Suspense가 트렌드가 된 건 아무래도 React 18버전부터 아닐까 생각됩니다. 이번시간에는 Suspense를 사용해서 Race Condition, Waterfall 문제를 해결하는 방법에 대해 알아보도록 하겠습니다.

velog.io

 

React Query와 함께 Concurrent UI Pattern을 도입하는 방법 | 카카오페이 기술 블로그

카카오페이에서 React Query를 활용하여 Concurrent UI Pattern을 도입한 사례에 대해 소개합니다. 이 글은 연작 중 2편에 해당합니다. 1편: 카카오페이 프론트엔드 개발자들이 React Query를 선택한 이유, 2

tech.kakaopay.com