raect-query 적용 전 posts
posts 페이지는 무한 스크롤로 추가적인 포스트를 더 요청해오는 역할을 담당하고 있습니다.
- fetchMorePosts
offset 이 변할 때마다 무한 스크롤에서 추가적인 포스트들을 불러옵니다.
- fetchNewPosts
채널 선택시 (채널 아이디가 변할 때마다) 새로운 포스트들을 불러옵니다.
const fetchMorePosts = useCallback(async () => { if (offset > 0 && postsData.length >= 10) { const data = await getPosts(channelId, offset); const editedData = editPostData(data.data); setPostsData([...postsData, ...editedData]); } }, [offset]); const fetchNewChannel = useCallback(async () => { const data = await getPosts(channelId, 0); const reformedData = editPostData(data.data); setPostsData(reformedData); setOffset(0); }, [channelId]);
useEffect(() => { fetchNewChannel(); }, [channelId]); useEffect(() => { fetchMorePosts(); }, [offset]);
이제 이 비동기 처리들을 혜성님께서 알려주신 react-query 로 바꿔보려고 합니다.
react-query 소개
우선 react-query 를 사용하기 위해 프로젝트 최상단 컴포넌트에 다음과 같이 QueryClientProvider 를 설정해줍니다.
// App.tsx import { QueryClient, QueryClientProvier } from 'react-query'; const queryClient = new QueryClient(); return ( <QueryClientProvider client={queryClient} > <App /> </QueryClientProvider> )
posts 를 받아오는 경우 GET 요청을 보내므로 useQuery 를 사용하게 됩니다.
useQuery 는 다음과 같이 사용할 수 있습니다.
useQuery(key, callback, options)
다음 두가지 방법으로 사용할 수 있습니다.
const { data, isLoading, ... } = useQuery(queryKey, queryFn, { }); const result = useQuery({ queryKey, queryFn, });
매개변수
- key
받아올 데이터를 식별하는 문자열 혹은 배열 값입니다.
배열의 경우 첫번째 인자로는 데이터를 식별하는 문자열을, 나머지 인자로는 콜백함수 내부로 전달할 매개변수를 받아올 수 있습니다.
- callback
비동기 처리 함수가 들어갑니다. 여기 API 호출을 넣으면 됩니다.
- options
API 요청시 다양한 옵션을 설정할 수 있습니다. 해당 옵션은 여기서 사용하지 않으므로 생략합니다!
반환값
다음과 같은 반환값을 가져올 수 있습니다.
const { isLoading, isError, data, error } = useQuery( // ... )
- isLoading
캐싱된 데이터가 없는 경우 로딩 여부에 따라
true
혹은 false
의 값을 가집니다.캐싱된 데이터가 있는 경우
false
의 값을 가집니다.- isError
에러가 발생한 경우
true
가 됩니다.- data
요청에 성공한 경우 받아올 데이터입니다.
- error
쿼리 함수에 오류가 발생한 경우 오류 객체를 받습니다.
- refetch
여기서 추가적으로
refetch
를 받아올 수 있는데, refetch 는 수동으로 해당 쿼리를 다시 요청합니다.react-query 적용
위 방법 중 두번째 방법을 사용하여 posts 코드에 적용하면 다음과 같습니다.
- key 값은
getChannelPosts
로 설정해주었다. 그리고 channelId 와 offset 을 함수 매개변수로 넘겨줍니다.
- 콜백함수로 사용할 비동기 함수를 다음과 같이 지정합니다.
const { data } = useQuery({ queryKey: ['getChannelPosts', channelId, offset], queryFn: async () => { await getPosts(channelId, offset) }, })
channelId 와 offset 을 인자로 전달해주었으므로 두 값이 바뀔 때마다 자동으로 refetch 가 실행됩니다.
따라서 기존의 긴 코드를 다음과 같이 수정해줄 수 있습니다.
수정 전
const fetchMorePosts = useCallback(async () => { if (offset > 0 && postsData.length >= 10) { const data = await getPosts(channelId, offset); const editedData = editPostData(data.data); setPostsData([...postsData, ...editedData]); } }, [offset]); const fetchNewChannel = useCallback(async () => { const data = await getPosts(channelId, 0); const reformedData = editPostData(data.data); setPostsData(reformedData); setOffset(0); }, [channelId]); useEffect(() => { fetchNewChannel(); }, [channelId]); useEffect(() => { fetchMorePosts(); }, [offset]);
수정 후
const { data: postsData } = useQuery({ queryKey: ['getChannelPosts', channelId, offset], queryFn: async () => { const data = await getPosts(channelId, offset); const editedData = editPostData(data); return offset === 0 ? editedData : [...data, ...editedData]; } });
추가 문제
포스트 10개 미만일 시 오프셋 적용 안 하기
- 마지막 자식 요소를 관찰하면 observer 가 offset 을 증가시킵니다. 여기서 더 불러올 포스트가 없음에도, 즉 포스트가 10개 미만인 경우에도 마지막 요소가 가시성이 true 가 되며 offset 이 증가하게 됩니다.
그리고 offset 이 변하게 됨으로써 추가적인 포스트를 불러오게 됩니다. 더 작성된 포스트가 없으니 postsData 가 undefined 가 되어 빈화면이 출력됩니다.
그래서 아래와 같이 childNode 가 10 이상인 경우에만 가시성을 관찰하도록 수정하였습니다.
useEffect(() => { if ( postsRef.current && postsRef.current.childNodes.length >= 10 ) { const { lastChild } = postsRef.current; lastChild && observe(lastChild); } }, [postsData]);
- 무한 스크롤이 에러가 나는 문제가 생겨서 console 을 출력해보니 react-query 로 받아오는 데이터를 postsData 에 넣어놓고, 새로운 데이터를 추가적으로 넣어주려고 했는데 이전 데이터가 undefined 가 되면서 spread 연산자가 적용되지 않는 문제가 생겼다.
결국 postsData 를 state 로 따로 분리하고, react-query 로 받아오는 데이터가 업데이트 될 때마다 postsData 를 변경하는 로직으로 변경했다.
const [postsData, usePostsData] = useState<EditedPost[]>([]); const { data } = useQuery({ queryKey: ['getChannelPosts', channelId, offset], queryFn: async () => { const data = await getPosts(channelId, offset); const editedData = editPostData(data); return editedData; } }); useEffect(() => { if (data) { setPostsData(offset === 0 ? data : [...postsData, ...data]); } }, []);