-
Notifications
You must be signed in to change notification settings - Fork 0
[기술 검토] 리액트 쿼리 도입 이유
펀잇의 경우 많은 data fetching 작업이 있습니다. 메인 페이지의 경우 6개의 data를 가져오고 있습니다. 서버 데이터를 관리하기 위해 수많은 상태가 생길 것입니다.
그리고 데이터를 가져오기 위한 부수 효과 처리가 많아지고, 업데이트에 따른 데이터 refetching도 많습니다.
상품 목록, 상품 상세, 상품 리뷰, 꿀조합, 랭킹 등 많은 UI 구성도 많았기 때문에 많은 요청에 대한 로딩과 예외 처리까지 컴포넌트에서 처리한다면 컴포넌트는 복잡해지고, 코드 가독성과 프로젝트 진행에 차질이 생길 것입니다.
리액트 쿼리를 도입하면 이러한 상황에서 아래의 장점을 가질 수 있습니다.
- Suspense와 ErrorBoundary 사용
- 두 컴포넌트를 사용하지 않으면 컴포넌트에서 로딩과 에러 상황에 따른 분기 처리를 해야 합니다.
- 기존 data fetching 과정에서 두 컴포넌트를 사용하기 위해서 Promise 또는 Error를 throw 해야합니다. 이를 위한 코드를 작성해야 합니다.
- 리액트 쿼리는 옵션 설정으로 Suspense와 ErrorBoundary를 사용할 수 있고, 컴포넌트 코드를 줄일 수 있습니다.
- 업데이트 이후 작업
- 리액트 쿼리의 mutate 함수는 onSuccess, onError, onSettled 와 같은 콜백을 제공합니다.
- 이를 사용해 성공, 실패 등에 상황에 맞게 후처리를 편리하게 할 수 있습니다.
- 서버 상태를 전역으로 사용 가능
- 펀잇은 UI 구성이 많아 컴포넌트의 수도 많습니다. 따라서 부모에서 데이터를 가져와 상태로 저장하면 자식 컴포넌트로 전달하기까지 깊게는 4뎁스까지 들어와야 했습니다.
- 리액트 쿼리를 사용해 해당 도메인에 대한 쿼리를 작성하면 그 데이터를 사용하는 컴포넌트에서 바로 사용 가능할 수 있습니다.
펀잇의 경우 무한 스크롤을 각종 페이지에서 사용하고 있습니다. (상품 목록, 리뷰, 레시피 조회)
무한 스크롤을 구현할 때 페이지 번호나 로딩 상태를 직접 관리해야 하는 불편함이 있었습니다.
특히 탭이 여러 개여서 탭을 이동할 때 이전 탭에서 불러온 페이지 번호를 초기화하고 새로운 탭의 데이터를 불러와야 했습니다.
이때 페이지 번호 초기화와 데이터를 가져오는 순서가 보장되지 않는 문제가 있었습니다.
이를 해결하기 위해서는 복잡한 로직이 필요했는데 리액트 쿼리를 사용하면 탭을 이동할 때 데이터를 무효화 할 수 있어 한 줄의 코드로 훨씬 간단하게 해결할 수 있었습니다.
리액트 쿼리 도입 전에는 data fetching과 관련한 로직을 custom hook로 작성하였습니다.
새로운 데이터가 필요할 때마다 해당 훅을 호출하며 사용하였습니다.
patch나 post 요청 후에는 데이터를 갱신하는 과정이 필요했는데, custom hook의 경우 함수 내부에서 사용할 수 없었고 로직 그대로 함수 내에 작성해 코드가 중복되고, 복잡해지는 문제가 있었습니다.
반면, 리액트 쿼리를 사용하면 invalidateQueries 함수를 이용해 데이터를 갱신할 수 있었습니다.
invalidateQueries를 사용하면 갱신 로직을 fetching 함수 내에 작성할 수 있어, UI 로직과 분리 작성할 수 있었습니다.
- 변경 전
const handleSubmit: React.FormEventHandler<HTMLFormElement> = async (event) => {
event.preventDefault();
await request(formData);
const reviewResponse = await productApi.get({
params: `/${product.id}/reviews`,
queries: `?sort=${REVIEW_SORT_OPTIONS[0].value}&page=0`,
credentials: true,
});
const { reviews } = await reviewResponse.json();
setProductReviews(reviews);
resetPage();
setReviewPreviewImage('');
resetReviewFormValue();
closeReviewDialog();
};
- 변경 후
const handleSubmit: FormEventHandler<HTMLFormElement> = async (event) => {
event.preventDefault();
mutate(formData, {
onSuccess: () => {
resetAndCloseForm();
scrollToPosition(targetRef);
},
onError: (error) => {
resetAndCloseForm();
if (error instanceof Error) {
alert(error.message);
return;
}
alert('리뷰 등록을 다시 시도해주세요');
},
});
};
- 📚 프론트엔드 개발 문서
- 🌏 브라우저 지원 범위
- 🧪 프론트엔드 테스트 전략
- [웹 접근성] a tag와 button의 차이는 무엇일까?
- multipart
- SvgSprite 컴포넌트 사용하기
- [INFRA] 프론트엔드 CI/CD 구축
- [기술 검토] 리액트 쿼리 도입 이유
- [기술] 로그인 기능 도입기
- 🐛 S3 배포 캐싱 오류
- 이미지를 위한 S3와 Cloudfront 설정하기
- 📓 성능리포트 ‐ 펀잇 서비스 최적화하기
- 펀잇 SEO 개선하기
- 📚 백엔드 개발 문서
- intellij에서 private DB 연결하기
- [INFRA 0] 전체 infra 구조 - ver1
- [INFRA 1] infra 서버 세팅
- [INFRA 2] 백엔드 CI/CD 구축
- [INFRA 3] 백엔드 DB 연결
- [INFRA 4] 깃허브 PR 라벨을 기준으로 젠킨스 빌드하기
- [LOG] 로그 세팅
- [Trouble Shooting] 일관된 테스트 격리 적용하기
- [Trouble Shooting] 프록시로 동작하는 @Transactional, 전파 옵션 관리