import { useState, useEffect } from 'react';
import { fetchBio } from './api.js';
export default function Page() {
const [person, setPerson] = useState('Alice');
const [bio, setBio] = useState(null);
useEffect(() => {
setBio(null);
fetchBio(person).then(result => {
setBio(result);
});
}, [person]);
return (
<>
<select value={person} onChange={e => {
setPerson(e.target.value);
}}>
<option value="Alice">Alice</option>
<option value="Bob">Bob</option>
<option value="Taylor">Taylor</option>
</select>
<hr />
<p><i>{bio ?? 'Loading...'}</i></p>
</>
);
}
위와같이 데이터를 fetch 하여 그 결과를 상태를 업데이트하고 UI에 보여주는 간단한 코드가 있습니다.
근데 이 코드는 버그가 있어요.
select 에는 "Alice" "Bob" "Taylor" 3가지 옵션이 있는데 빠르게 옵션을 변경하면 fetch가 두번 일어나게 됩니다.
예를 들어 Alice에서 Bob으로 변경했다고 가정해볼게요.
그러면 fetchBio 함수는 두번 실행이 될거고 요청도 2번 보내게 될거에요.
근데 이 각각의 요청이 끝나는 시점은 불분명하다는거에요.
Alice 요청이 먼저 끝나고 Bob 요청이 끝난다면, UI에는 정상적으로 표시가 될거에요.
그러나 Bob 요청이 먼저 끝나고 Alice 요청이 먼저 끝나게되면 select 는 Bob을 선택된 상태지만, bio 상태는 "Alice"가 저장되어 UI 에서는 bio를 잘못표기하는 상황이 나타나게되요.
이렇게 두개의 비동기 작업이 경쟁하며 작업 완료의 순서를 예상할 수 없는것을 "경쟁 조건(race condition) 이라고합니다.
이를 수정한 코드는 다음과 같습니다.
import { useState, useEffect } from 'react';
import { fetchBio } from './api.js';
export default function Page() {
const [person, setPerson] = useState('Alice');
const [bio, setBio] = useState(null);
useEffect(() => {
let ignore = false;
setBio(null);
fetchBio(person).then(result => {
if (!ignore) {
setBio(result);
}
});
return () => {
ignore = true;
}
}, [person]);
return (
<>
<select value={person} onChange={e => {
setPerson(e.target.value);
}}>
<option value="Alice">Alice</option>
<option value="Bob">Bob</option>
<option value="Taylor">Taylor</option>
</select>
<hr />
<p><i>{bio ?? 'Loading...'}</i></p>
</>
);
}
useEffect 에 ignore 변수를 추가해주었고, effect 를 클린업하는 부분에 ignore 변수를 true로 할당하는 코드를 추가했어요.
이 패턴을 통해 경쟁 조건을 해결 할 수 있어요.
어떤 원리로 UI 버그가 안일어나게 하는지 살펴볼게요.
우선 각각의 effect는 고유한 ignore 변수를 가지게되요. (클로저)
만약 person을 두 번 바꿔서 effect가 2번 실행됬다고 해볼게요.
1. 첫번째 이펙트에서 fetch
2. person 변경 (첫번째 이펙트 cleanUp 실행: ignore = true, 추 후 첫번째 이펙트의 fetch가 끝나더라도 상태를 업데이트 하지 않습니다.)
3. 두번째 이펙트에서 fetch
4. 두번째 이펙트 fetch 종료 후 setBio
여기서 ignore의 중요한 역할은 fetch의 결과를 state에 설정하는 것을 막는 것이에요. ( 요청 자체를 막는건 아님. )