프로젝트 최적화

정소현·3일 전
0

팀프로젝트

목록 보기
50/50
post-thumbnail

webp, sharp를 이용한 이미지 최적화

자주 사용되지 않는 정적 이미지 에셋을 WebP 포맷으로 변환하고 뷰포트에 바로 노출되지 않는 이미지는 lazy loading을 적용했습니다. 또한 Sharp 라이브러리를 도입해 프로젝트에서 비중이 큰 이미지를 최적화하여 로딩 속도를 개선했습니다.

변환 전

변환 후

Intersection Observer를 활용한 스크롤 감지 방식 최적화

사용자의 스크롤 위치를 지속적으로 계산하여 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를 활용한 불필요한 이벤트 호출 방지

청첩장 폼 데이터의 자동 임시저장 기능에서 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가 실행됨

개선 후 동작

  • prevFormDataRef: useRef를 사용하여 이전 데이터를 저장하고, 데이터 변경 여부를 확인합니다.
  • handleDebouncedSave: 디바운싱된 저장 함수로, 데이터가 변경될 때만 저장을 실행합니다. 비회원은 세션스토리지에, 회원은 Supabase에 데이터를 저장합니다.
  • methods.watch: 폼 값이 변경될 때마다 handleDebouncedSave를 호출하여 디바운싱된 저장을 트리거합니다.202e0a17647/image.png)

0개의 댓글