합동 세미나를 시작하며 챌린징 요소로 이전에는 사용해보지 않았던 react-query를 사용해보자는 의견이 나왔고 이에 동의했다. 리액트 쿼리를 처음 사용하는 것이라서, 단순히 API를 호출하는 방법을 배우는 것보다 그 사용 이유를 이해하는 것이 더 큰 챌린징이었다.
그래서 평소 많이 사용하는 useEffect내의 데이터 패칭 방식이 아닌 리액트 쿼리를 권장하는 이유가 무엇인지를 이해하고자 했다.
리액트 문서에서 제시하는 9가지 권장사항 중 1가지.
데이터를 패칭해야 하는 경우, useEffect보다 라이브러리를 사용하는 것이 좋다. 라는 내용이다.
// 🛑 WRONG
const [items, setItems] = useState();
useEffect(() => {
api.loadItems().then(newItems => setItems(newItems));
}, []);
// 🟢 RIGHT (one library option)
import {useQuery} from '@tanstack/react-query';
const { data: items } = useQuery(['items'], () => api.loadItems());
위의 내용을 발견하고 내가 애용하던 useEffect 내부에서 데이터 패칭은 왜 지양은 이해가 가질 않았고 이에 대해 찾아보았다.
관련 문서를 읽고 가장 크게 와닿은 useEffect 패칭 지양의 이유는 경쟁 상태의 발생으로 인한 예상치 못한 동작이었다.
경쟁 상태(Race Condition)는 여러 프로세스나 스레드가 공유된 자원에 동시에 접근하거나 수정하려고 할 때 발생하는 상태를 나타낸다.
function Bookmarks({ category }) {
const [data, setData] = useState([])
const [error, setError] = useState()
useEffect(() => {
fetch(`${endpoint}/${category}`)
.then(res => res.json())
.then(d => setData(d))
.catch(e => setError(e))
}, [category])
}
effect는 의존성이 변경될 때마다 다시 fetch 하는 방식으로 설정되어 있다. 하지만 네트워크 응답은 요청한 순서와 다르게 도착할 수 있다(경쟁 상태 발생). 즉 예상하지 않은 데이터를 보여줄 수도 있다는 것이다.
React 문서에 따르면 클린업 함수와 ignore라는 값을 사용해 이 문제를 해결할 수 있다고 하지만 코드가 복잡해진다. 더해서 우리가 예상한 대로 작동시키기 위해 로딩, 예외와 상태 관리를 고려할 시 더욱 복잡한 코드가 되어버린다.
function Bookmarks({ category }) {
const [isLoading, setIsLoading] = useState(true)
const [data, setData] = useState()
const [error, setError] = useState()
useEffect(() => {
let ignore = false
setIsLoading(true)
fetch(`${endpoint}/${category}`)
> .then(res => {
> if (!res.ok) {
> throw new Error('Failed to fetch')
> }
> return res.json()
})
.then(d => {
if (!ignore) {
setData(d)
setError(undefined)
}
.catch(e => {
if (!ignore) {
setError(e)
setData(undefined)
}
})
.finally(() => {
if (!ignore) {
setIsLoading(false)
}
})
return () => {
ignore = true
}
}, [category])
}
결국 데이터 패칭 자체는 간단할지라도 비동기 상태의 관리가 어렵기 때문에 코드가 복잡해진다는 사실을 알 수 있다.
나는 이점이 react-query를 사용하는 확실한 이유임을 깨달을 수 있었다.
function Bookmarks({ category }) {
const { isLoading, data, error } = useQuery({
queryKey: ['bookmarks', category],
queryFn: () =>
fetch(`${endpoint}/${category}`).then((res) => {
if (!res.ok) {
throw new Error('Failed to fetch')
}
return res.json()
}),
})
}
📌 react-query는 데이터 패칭 라이브러리가 아니라 비동기 상태 관리자이다.
react-query를 사용하는 이유는 앱에서 우리가 예상한 대로 비동기 상태가 동작하게 하는 걸 최대한 쉽게 하기 위해서이다.
이전에는 react-query가 단순히 데이터 패칭을 좀 더 편하게 해주기에 사용하면 편리하다라고만 이해하고 있었는데 이번 공부를 통해서 비동기 상태 관리자라는 중요한 맹점에 대해서 알게 되었다. 앞으로도 왜 사용하는지에 집중하며 공부해야겠음을 다시 한 번 깨달았다.
이 외에도 useEffect 데이터 패칭을 지양에는 더 자세한 많은 이유들이 존재하니 이에 대해 더 깊게 파는 것도 큰 공부가 될 것 같다.