본문 바로가기
  • 하고 싶은 일을 하자
개발

[React Hook 공식 문서 읽기] useEffect (Race Condtions 해결 방법)

by 박빵떡 2023. 11. 22.
반응형

React 개발을 하는데 있어 useEffect 는 필수이다.

정말 많이 쓴다.

그런데 공식 문서를 읽다보니 몰랐던 것을 알게되어 정리 해보겠다.

 

먼저 useEffect 에 대한 정의부터 예상과 달랐다.

나는 컴포넌트가 마운트/언마운트 되었을 때 실행할 로직을 정의하는 Hook 이다 라고 생각하고 있었는데

공식 문서의 정의에는

"외부 시스템을 사용하는 컴포넌트를 동기화하는 Hook 이다." 라고 설명하고 있다.

 

외부 시스템은 무엇이고, 동기화는 무엇일까?

 

External System

external system 이란 React 로 control 하지 않는 코드를 의미한다.

이에 대한 예시로 setInterval(), clearInterval(), window.addEventListener(), window.removeEventListener(), animation.start(), animation.reset() 등이 있다.

 

동기화

useEffect 는 componentDidMount, componentDidUpdate, componentWillUnmount 가 처리하던 side effect를 보다 더 읽기 쉽고, 유지보수하기 용이하도록 만든 hook 이다. 그래서 useEffect 라고 부른다.

 

여기서 side effect 는 부작용이 아니라, 외부에서 일어난 변화로 내부의 state를 바꾸는 행위를 의미한다. 즉, 외부와 내부를 동기화 시키는 것이 useEffect가 하는 syncrhonize 작업이다.

 

import { useEffect } from 'react';
import { createConnection } from './chat.js';

function ChatRoom({ roomId }) {
  const [serverUrl, setServerUrl] = useState('https://localhost:1234');

  useEffect(() => {
  	const connection = createConnection(serverUrl, roomId);
    connection.connect();
  	return () => {
      connection.disconnect();
  	};
  }, [serverUrl, roomId]);
  // ...
}

코드 출처: React.dev 공식 문서

 

Chatroom 컴포넌트가 마운트 되면 채팅 서버에 연결하고

Chatroom 컴포넌트가 언마운트 되면 채팅 서버의 연결을 끊고 있다.

채팅 서버라는 external system 을 사용자 웹과 동기화 시키는 것이다.

 

cleanup 코드 (return 문)는 언마운트 할 때만 실행되는 게 아니라

dependency 가 변경될 때도 실행된다.

 

개발 모드에서는 stress-test 를 위해 setup 코드 실행 전에 setup가 cleanup 코드도 실행된다.

 

공식 문서에 useEffect 내부에 fetch를 사용하면 race condition 이 생길 수 있는데, 이를 해결하는 방법이 정말 신박했다.

 

Race Condition 이란 무엇인가?

race condition은 대학생 때 운영체제 수업에서 배운 기억을 더듬어 보자면

두 개 이상의 프로세스가 필요로 하는 공유 자원을 서로 경쟁하는 상황을 의미한다.

이 상황에서 조치를 취하지 않으면 문제가 발생할 수 있다.

 

예를 들어 파일 A를 수정하려는 두 개의 프로세스 a와 b가 있다.

a 프로세스가 파일 A를 수정하고 있었다.

b 프로세스가 수정 전의 파일 A를 읽어 수정을 한다.

a 프로세스가 파일 A를 수정하여 저장했다.

b 프로세스도 파일 A를 수정하여 저장할 경우

a 프로세스의 작업 내역을 사라지게 된다.

이를 막기 위해서 공유 자원읜 파일 A를 프로세스 a 가 수정하고 있을 경우

프로세스 b는 파일 A를 수정하지 못하도록 막으면 해결된다.

운영체제에서는 세마포어나 뮤텍스를 이용해 해결한다.

 

useEffect 내부에 fetch를 사용할 경우 발생할 수 있는 race condition

https://maxrozen.com/race-conditions-fetching-data-react-with-useeffect

 

Fixing Race Conditions in React with useEffect - Max Rozen

If you're using useEffect to fetch data, chances are you've either run into a race condition, or have one without realising it. Let's learn how to fix them in this article.

maxrozen.com

출처: https://maxrozen.com/race-conditions-fetching-data-react-with-useeffect

https://codesandbox.io/s/beating-async-race-conditions-in-react-7759f?file=/src/DataDisplayer.js

 

Beating Async Race Conditions in React - CodeSandbox

Beating Async Race Conditions in React by rozenmd using react, react-dom, react-scripts

codesandbox.io

위의 예시 코드에서 Fetch Data 를 여러번 누르면

가장 마지막의 숫자가 출력되는 게 아니라

이전 숫자가 출력될 수 있다.

fetchData가 여러개 실행되기 때문이다.

 

이 코드가 의도하는 바는 가장 마지막에 실행한 fetch data 를 보여주는 것인데

fetchData가 랜덤한 timeout 딜레이로 실행되기 때문에

의도치 않게 가장 마지막에 실행하지 않은 fetch data를 보여주게 된다.

 

여러개의 fetchData가 실행되며 원치 않는 결과값을 얻는 것이

멀티 프로세스가 공유 자원을 경쟁하는 모습과 같다. 즉, race condition인 것이다.

 

출처: https://maxrozen.com/race-conditions-fetching-data-react-with-useeffect

이런 식으로 boolean flag를 이용하면 race condtion 을 피할 수 있다.

fetch data를 여러번 클릭할 경우

fetchData가 딜레이 된 후 실행될 때 active는 cleanup 함수에 의해 false가 되었기 때문에

처음에 실행하던 fetchData 에서는 setFetchedId와 setData가 실행되지 않는다.

 

즉 원하는 코드 (setFetchedId와 setData) 는

여러번 실행되더라도

마지막에 단 한번만 실행되게 하여 race condition을 피하게 된다.

 

위에서 사용한 boolean flag가 세마포어나 뮤텍스와 똑같은 역할이다.

(사실 세마포어 뮤텍스를 배운지 오래되어 정확한 얘기는 아닐 수 있으니 양해 부탁드립니다.)

fetch data를 여러번 클릭해 useEffect 내의 함수가 여러번 실행되고

boolean flag에 의해 한 번만 실행되는 모습이

멀티 프로세스, 멀티 스레드 와 비슷한 것 같아서 재밌는 공부였다.

 

참고로 위의 방법은 fetch 자체는 막지 못한다.

10번 클릭하면 서버에게 10번 요청한다.

출처 : https://maxrozen.com/race-conditions-fetching-data-react-with-useeffect

서버에 요청하는 것도 중단하기 위해서는 abortController 를 사용하면 된다.

회사 코드에 적용하기

회사 코드에 useEffect 가 없을 리는 없고, 겁나 많이 쓰고 있었다.

이런 식으로 dependency array 에 react value 가 없을 경우

lint 에서 문제가 있다고 알려준다.

그런데 eslint 에서 useEffect 관련 경고나 에러를 단 한 번도 본 적이 없었다는 게 생각났다.

그래서 확인해보니... 따흑

우리 회사 프로젝트의 lint 는 이 에러를 표시하고 있지 않았다 ㅠㅠ

 

create next app 으로 next.js 프로젝트를 시작하면

위의 next/core-web-vitals rule 이 적용되어 있는데

우리 회사 프로젝트는 react만 쓰다가 도중에 next.js 를 적용해서 그런지 저 rule 이 없었다.

그래서 useEffect 내에 문제가 있을 경우 lint 가 찾아주지 못했다.

 

해결 방법은 가장 좋은 방법은

위의 next/core-web-vitals 를 적용하면 되는 것인데

당장 해결해야 하는 많은 warning 과 error 들이 나와서 (눈물)

추후에 해결하기로 하고

 

일단 useEffect의 dependency array 에 문제가 있을 경우 알려주는

react-hooks/exhaustive-deps 를 eslint 가 warn 으로 표시하도록 적용하였다.

 

next/core-web-vitals 를 적용하지 않아도 당장에 큰 문제가 되지 않지만

react 규칙을 어기고 있는게 꽤 있어서 최대한 빠르게 적용해야 할 것 같다.

반응형

댓글