ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 불필요한 렌더링으로 인한 UI 버벅임 개선하기
    프론트엔드 2025. 10. 10. 22:56

    ⚠️ 문제 상황 

     

    • 부트캠프 프로젝트에서 습관 형성을 돕는 히트맵 기반 챌린지 UI를 구현하던 중, 사용자가 여러 챌린지를 비교하며 확인할 수 있도록 Swiper를 활용한 슬라이드 구조를 적용했다.
    • 하지만 실제로 UI를 사용해보는 과정에서 슬라이드를 전환할 때마다 이미지가 반복적으로 렌더링되며 화면 전환 시 버벅임이 발생하는 문제를 확인했다.
    • React DevTools를 통해 분석한 결과, Swiper 슬라이드 이동 시 각 이미지 컴포넌트가 불필요하게 재렌더링되고 있었다.
      → 이는 사용자 입장에서 UI 반응성이 떨어지고, 체감 성능이 저하되는 문제로 이어질거라고 예상이 되어 빠른 처리가 필요해보였다

     

    🔍 원인 분석

    • 프로젝트를 구현하면서 렌더링 최적화에 대해서 고민을 하던 중, Swiper.js의 slideItem렌더링의 문제점이 생겼다. 
    • 개발자라면 익숙할 히트맵 UI를 구현하던 중 문제가 생겼다 

    동영상 서비스가 종료되어 해당 콘텐츠를 재생할 수 없습니다.

     

    - 위 영상을 보면 스와이프 될 때마다 21/66개 이미지가 각각 재렌더링 되는 것을 볼 수 있다. 

     

    어떤 것이 문제일까..?

    문제: 슬라이드 전환 시 이미지 아이콘들이 불필요하게 재렌더링되어 버벅임이 발생했다.
    원인: Swiper의 내부 상태 변경으로 상위 컴포넌트가 리렌더링되면서, props 변화가 없는 자식 컴포넌트까지 함께 렌더링되는 구조였다.

     

    해결 방안

    • 슬라이드 전환 시 불필요한 렌더링을 줄이기 위해 처음에는 데이터 연산 비용을 줄이는 방향으로 접근했다.
    • 챌린지 진행도 계산 로직은 기간 계산, 루틴 매칭, 일별 집계 등 연산량이 많아질 수 있는 부분이었기 때문에 불필요한 재계산을 방지하기 위해 useMemo를 적용했다.
     const slidesData = useMemo(() => {
        return challenge.map(challenge => {
          const start = new Date(challenge.createdAt);
          const end = new Date(challenge.endAt);
          const days = Math.round((end.getTime() - start.getTime()) / (1000 * 60 * 60 * 24));
          const { dailyCompletions } = calculateSingleChallengeProgress(
            challenge,
            routines,
            routineCompletions,
            days
          );
          const category = CATEGORY_CONFIG.find(
            challengeCategory => challengeCategory.id === challenge.categoryId
          );
          return {
            id: challenge.id,
            name: challenge.name,
            src: category?.src,
            color: category?.color,
            dailyCompletions,
            categoryName: category?.name,
          };
        });
      }, [challenge, routines, routineCompletions]);

     

     

     

    하지만 렌더링 문제는 해결되지 않았다.

    → 원인을 다시 분석해보니, 문제는 연산 비용이 아니라 컴포넌트 리렌더링 구조에 있었다.

    useMemo는 연산 결과를 캐싱하는 역할만 할 뿐 컴포넌트의 렌더링 자체를 제어하지는 못한다는 점을 확인했다.

    다만 해당 계산 로직은 여전히 비용이 발생할 수 있는 부분이기 때문에 useMemo는 유지하고 렌더링 문제는 별도로 해결하기로 했다.

     {slidesData.map(slide => (
         <SwiperSlide key={slide.id}>
           <SlideContent
              name={slide.name}
              categorySrc={slide.src}
              categoryColor={slide.color}
              dailyCompletions={slide.dailyCompletions}
              categoryName={slide.categoryName}
            />
          </SwiperSlide>
     ))}

    → SlideContent를 React.memo로 감싸 props가 변경되지 않는 경우에는 재렌더링이 발생하지 않도록 최적화했다.

     

     

    • 그 결과, 부모 컴포넌트가 리렌더링되더라도 자식 컴포넌트는 불필요하게 다시 렌더링되지 않도록 개선할 수 있었다.
    • 특히 슬라이드 전환 시 이미지 아이콘이 반복적으로 다시 그려지는 문제를 줄이고 화면 전환 시 체감 성능을 개선할 수 있었다.

    동영상 서비스가 종료되어 해당 콘텐츠를 재생할 수 없습니다.

     

    • 또한 크롬 extension React Developer Tools를 사용해 렌더링 시간을 측정해봤다 
    • React.memo의 사용여부에 따라 Render duration이 4초 정도 차이를 평균 0.8~1.4ms로 감소시켰다. 

     

    📘 정리 

    이번 경험을 통해, 연산 최적화(useMemo)와 렌더링 최적화(React.memo)는 서로 다른 문제를 해결하는 도구라는 점을 확인했다.

    • useMemo → 연산 비용 최적화
    • React.memo → 컴포넌트 렌더링 제어

    → 상황에 맞게 두 가지를 구분하여 적용하는 것이 중요했다.

Designed by Tistory.