안녕하세요 트리플랩입니다.
오늘은 ReactJS를 사용하면서 네트워크 요청하면서 버그처럼 보였던 상황을 공유하고, 공식문서에서는 어떻게 해결하는지 살펴보겠습니다.
공식문서에 있는 예시 입니다.
아래 예시는 ReactJS만 사용하는 경우에 Effect를 이용한 데이터 페칭 하는 과정에서 비동기(네트워크) 요청에 버그가 생길수 있는데
다음과 같이 해결할수 할수 있다라는 예시 입니다.
문제(버그) 상황
위에 select 박스를 연속으로 변경해서 데이터를 불러온다고 가정하면 이거 버그인데 라고 인지 할수 있을것 입니다.
만약 프레임워크를 사용하고 있다면 프레임워크의 데이터 페칭 메커니즘을 이용하는 것이 Effect를 직접 작성하는 것보다 더 효율적일 것입니다. 그리고 또 좋은 대안도 설명하고 있습니다.[참고링크]
외부라이브러리 사용하지 않고, 오직 ReactJS와, fetch함수만 사용한다면 공식문서에서도 설명 했듯이 다음과 같은 해결방법도 있습니다.
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(() => {
async function startFetching() {
setBio(null);
const result = await fetchBio(person);
if (!ignore) { // 컴포넌트가 언마운트되기 전에 ignore를 true로 설정하여 fetchBio 호출 후의 setBio 호출을 막을 수 있습니다.
setBio(result);
} else {
console.log("오래된 요청이라 반영하지 안 했습니다.")
}
}
let ignore = false;
startFetching();
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>
</>
);
}
결론
결과적으로, 컴포넌트가 언마운트된 이후에는 오래된 네트워크 요청 처리에는 더 이상 상태 업데이트가 발생하지 않으므로 안전하게 비동기 작업을 처리할 수 있습니다.
커스텀 Hook
이제 위에서 작업한 예시를 (비동기 네트워크 요청)을 커스텀 Hook으로 만들어서 사용해보도록 하겠습니다.
커스텀 Hook을 추출하는 것은 데이터의 흐름을 명확하게 해줍니다. url을 입력하고 data를 받습니다. useData안의 Effect를 “숨김으로써” 다른 사람이 ShippingForm(부모) 컴포넌트에 불필요한 의존성을 추가하는 것을 막을 수 있습니다.
시간이 지나면 앱의 대부분 Effect들은 커스텀 Hook 안에 있을 겁니다.
import { useState, useEffect } from 'react';
export function useData(url) {
const [data, setData] = useState(null);
const [error, setError] = useState(null); // 에러 상태 추가
useEffect(() => {
if (!url) return;
let ignore = false;
// 비동기 작업을 수행하는 내부 함수
const fetchData = async () => {
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error('Network response was not ok');
}
const json = await response.json();
if (!ignore) {
setData(json);
}
} catch (err) {
if (!ignore) {
setError(err.message);
}
}
};
fetchData();
return () => {
ignore = true;
};
}, [url]);
return { data, error }; // 에러와 데이터를 모두 반환
}
import React, { useState } from 'react';
import { useData } from './api.js';
export default function Page() {
const [url, setUrl] = useState('https://api.example.com/data');
const { data, error } = useData(url);
const handleChangeUrl = (event) => {
setUrl(event.target.value);
};
return (
<div>
<select value={url} onChange={handleChangeUrl}>
<option value="https://api.example.com/data">Default Data</option>
<option value="https://api.example.com/other">Other Data</option>
</select>
<hr />
{error && <p>Error: {error}</p>}
{data ? (
<pre>{JSON.stringify(data, null, 2)}</pre>
) : (
<p>Loading...</p>
)}
</div>
);
}
추가설명
추가로 "Effect 의존성 제거하기"설명하고 있는 글 입니다.
리액트가 아직 미숙하거나, 처음사용하는 분들이라면 해당 부분의 글을 살펴보기시 바랍니다.👇
- 이 코드의 문제점은 서로 관련이 없는 두 가지를 동기화하고 있다는 것입니다.
- country props를 기반으로 cities State를 네트워크에 동기화하려고 합니다.
- city State를 기반으로 areas State를 네트워크에 동기화하려고 합니다.
로직을 두 개의 Effect로 분할하고, 각 Effect는 동기화해야 하는 props즈에 반응합니다.