자주 사용되지 않는 정적 이미지 에셋을 WebP 포맷으로 변환하고 뷰포트에 바로 노출되지 않는 이미지는 lazy loading을 적용했습니다. 또한 Sharp 라이브러리를 도입해 프로젝트에서 비중이 큰 이미지를 최적화하여 로딩 속도를 개선했습니다.
변환 전
변환 후
사용자의 스크롤 위치를 지속적으로 계산하여 CPU부하가 발생할 수 있는 브라우저의 스크롤 이벤트 대신 요소가 뷰포트에 들어올 때만 데이터를 가져오는Intersection Observer를 활용해 이벤트 리스너 추가 및 제거 관리 없이 효율적으로 데이터를 불러올 수 있도록 개선하였습니다.
useInfinityQuery를 사용하여 데이터를 가져옴(스크롤 이벤트, intersection observer 동일 사용
const row = 10;
const {
data: reviewsData,
fetchNextPage,
hasNextPage,
isFetchingNextPage,
isFetchingPreviousPage,
} = useInfiniteQuery({
queryKey: ['reviews'],
queryFn: ({ pageParam = 0 }) => getReview({ pageParam, row }),
getNextPageParam: (lastPage, allPages) =>
lastPage.length === row ? allPages.length * row : undefined
});
개선 전 코드 (useInfinity Query + 스크롤 이벤트를 사용한 구현)
useEffect(() => {
const handleScroll = () => {
const { scrollTop, scrollHeight, clientHeight } = document.documentElement;
if (scrollTop + clientHeight >= scrollHeight - 10 && hasNextPage && !isFetchingNextPage) {
fetchNextPage(); // 사용자의 스크롤을 계산하여 useInfinityQuery의 fetchNextPage()를 호출하여 데이터를 가져옴
}
};
window.addEventListener('scroll', handleScroll);
return () => window.removeEventListener('scroll', handleScroll);
}, [hasNextPage, isFetchingNextPage, fetchNextPage]);
개선 후 코드 (useInfinity Query + Intersection Observer API를 사용한 구현)
const { data: reviewsData, isLoading, error, fetchNextPage, hasNextPage, isFetchingNextPage } = useReviewInfinite();
const observerRef = useRef<IntersectionObserver | null>(null);
const lastReviewRef = useCallback(
(node: HTMLDivElement | null) => {
if (isFetchingNextPage) return;
if (observerRef.current) observerRef.current.disconnect();
// 뷰포트 감지요소를 설정하고 현재 뷰포트에 요소가 들어온다면
observerRef.current = new IntersectionObserver((entries) => {
if (entries[0].isIntersecting && hasNextPage) {
fetchNextPage(); // useInfiniteQuery에 데이터를 요청
}
});
if (node) observerRef.current.observe(node);
},
[isFetchingNextPage, fetchNextPage, hasNextPage],
);
return (
<div className='flex flex-col w-full p-4'>
<h1>후기</h1>
<ReviewImage />
<div>
<ReviewCard reviews={reviews} />
{reviews.length > 0 && (
<div
ref={lastReviewRef} // 뷰포트 감지요소
className='h-1'
/>
)}
</div>
{isFetchingNextPage && <div>더 불러오는 중...</div>}
</div>
);
};
청첩장 폼 데이터의 자동 임시저장 기능에서 debounce
를 활용해 변경된 값에 대한 처리를 일정 시간 동안 지연시킴으로써, 불필요한 이벤트 호출과 네트워크 요청을 효과적으로 방지했습니다. 이를 통해 저장 요청 빈도를 줄이고, 사용자 경험을 개선하는 동시에 서버 부담을 경감시켰습니다.
개선 전 코드 (setInterval 사용)
const AUTO_SAVE_INTERVAL: number = 120000;
const [prevFormData, setPrevFormData] = useState(methods.getValues());
const interval = setInterval(async () => {
const { data: user } = await browserClient.auth.getUser();
const formData = methods.getValues();
const isInvitationModified = JSON.stringify(formData) !== JSON.stringify(prevFormData);
if (isInvitationModified) {
if (!user.user) {
sessionStorage.setItem('invitationFormData', JSON.stringify(formData));
} else {
if (existingInvitation === null) {
insertInvitation(formData);
} else {
updateInvitation(formData);
}
}
setPrevFormData(formData);
}
}, AUTO_SAVE_INTERVAL);
return () => clearInterval(interval);
}, [existingInvitation, methods]);
위의 코드는 setInterval을 사용하여 일정 시간마다 폼 데이터를 확인하고 API 호출을 하게 되지만 이 방식은 불필요한 호출이 자주 발생할 수 있어 성능에 문제가 될 수 있습니다.
개선된 코드 (디바운싱 적용)
const SAVE_DELAY_TIME: number = 3000;
const prevFormDataRef = useRef(""); // 이전 폼 데이터를 저장
// 디바운싱된 저장 함수
const handleDebouncedSave = debounce(async () => {
const { data: user } = await browserClient.auth.getUser();
const formData = methods.getValues();
const isInvitationModified = JSON.stringify(formData) !== prevFormDataRef.current;
if (isInvitationModified) {
if (!user.user) {
sessionStorage.setItem('invitationFormData', JSON.stringify(formData)); // 비회원일 경우 세션스토리지에 저장
} else {
if (existingInvitation === null) {
insertInvitation(formData); // 회원일 경우 Supabase에 데이터 삽입
} else {
updateInvitation(formData); // 기존 데이터가 있으면 업데이트
}
}
prevFormDataRef.current = JSON.stringify(formData); // 이전 데이터를 현재로 업데이트
}
}, SAVE_DELAY_TIME); // 일정 시간 이후 저장
// 폼 값 변화 감지 후 디바운싱 함수 실행
const subscribeEveryValues = () => {
const subscription = methods.watch((value) => {
if (value) {
handleDebouncedSave(); // 값이 변경될 때마다 디바운싱 함수 호출
}
queryClient.invalidateQueries({ queryKey: QUERY_KEYS.invitation()});
return () => subscription.unsubscribe(); // 구독 해제
});
};
// useEffect 훅을 사용하여 컴포넌트가 마운트될 때 구독 시작
useEffect(() => {
subscribeEveryValues();
}, [methods]); // methods가 변경될 때마다 useEffect가 실행됨
개선 후 동작