사이드프로젝트 LCP 지표 개선하기

_sw_·2025년 11월 6일
0

📌 문제 정의 (Before)

메인 페이지에서 CPU 성능을 낮춰 테스트를 해봤을 때 LCP 지표가 3.35s로 개선이 필요한 상태로 측정이 돠었다. 그래서 메인 페이지 컴포넌트 로직 수정을 통해 개선을 기획하게 되었다.

LCP는 페이지에서 가장 큰 영역을 차지하는 컨텐츠가 로드 되었을 때의 시점을 표기하는 Core Web Vital 지표이다.

현재 모멘티아 프로젝트의 메인 화면에서 보이는 케로셀의 이미지들이 효과적으로 랜더링 되지 않기 때문에 나쁘게 지표가 나오고 있는 것 같다.


🎯 개선 목표 및 방향

SSR 적용

    import LatestArtworkSection from '@/components/MainPage/LatestArtworkSection';
    import MonthlyBestArtistSection from '@/components/MainPage/MonthlyBestArtistSection';
    import MonthlyPopularArtworkSection from '@/components/MainPage/MonthlyPopularArtworkSection';
    
    const Home = () => {
      return (
        <div className='max-w-[1960px] w-full mx-auto my-[126px] flex flex-col gap-[126px] px-[32px] tablet:px-[140px]'>
          <MonthlyPopularArtworkSection />
          <LatestArtworkSection />
          <MonthlyBestArtistSection />
        </div>
      );
    };
    
    export default Home;

→ 메인 페이지는 크게 3개의 영역으로 구성되어 있다. 각 영역의 경우 현재 CSR로 랜더링 되어있는 상태이다. 현재 페이지는 메인 페이지로서 유저의 상호작용 보다 컨텐츠를 보여주는 역할이 더 큰 페이지이라고 생각한다.

→ LCP는 페이지 요청된 시점에서 가장 큰 컨텐츠 요소가 랜더링 될 때까지 걸리는 시간을 말한다. 따라서 중간에 데이터 요청을 기다리는 시간이 필요한 CSR에 비해 SSR은 서버에서 랜더링 된 화면을 바로 보여주면 되기 때문에 CSR → SSR로 개선하는 과정은 효과적인 방향이라는 생각이 들었다.


Next.js의 Image 태그의 priority 옵션 선택적 적용

    const ArtworkCard = ({
      mode = 'artwork-popular',
      rank,
      artworkInfo,
    }: ArtworkCardProps) => {
      ...
    
      return (
        <CardLayout onClick={clickArtworkCard} classname={modeClasses[mode]}>
          <Image
            src={postImage || '/images/defaultArtworkImage.png'}
            alt={postImage ? `artwork-${postId}` : 'default_image'}
            fill={true}
            sizes={modeClasses[mode] || '402px'}
            className={'object-cover'}
            priority
          />
    
          ...
        </CardLayout>
      );
    };
    
    export default ArtworkCard;
    

→ 현재 메인페이지에서 사용되는 ArtworkCard, ArtistProfileCard 컴포넌트에는 <Image/> 태그가 사용되는데 모두 priority 옵션이 적용되고 있다. priority 옵션을 사용하는 경우 이미지 리소스가 우선적으로 로드되는데 많은 이미지에 해당 옵션이 적용되고 있다보니 그만큼 랜더링에 필요한 다른 리소스들의 우선순위가 낮아져 LCP 지표에 영향을 주고 있다는 추측을 했다.

→ 케로셀 특성상 특정 순위 아래에 있는 이미지들은 빠르게 로드할 필요가 없기 때문에 특정 순위 이상인 경우 priority 옵션을 적용 하도록 수정할 계획이다.


✅ 리팩토링 결과 (After)

CSR → SSR

const MonthlyBestArtistSection = () => {
  const { cardsInfo, isLoading } = useGetMonthlyPopularArtists();

  if (isLoading) return <div>로딩중</div>;

  return (
    <div className='flex flex-col gap-[90px]'>
      <div className='flex flex-col gap-[30px]'>
        <p className='title-l'>이달의 예술가 TOP10</p>
        <p className='subtitle1'>
          지난 한 달간 가장 많은 작품 좋아요를 받은 작가들이에요.
        </p>
      </div>
      <ControlledCarousel
        slides={cardsInfo}
        renderSlide={(info: ArtistInfoType, index: number) => (
          <ArtistProfileCard artistInfo={{ ...info }} rank={index + 1} />
        )}
      />
    </div>
  );
};

export default MonthlyBestArtistSection;

→ 기존 코드의 경우 Tanstack Query를 통해 데이터를 요청하고, 데이터를 통해 반복적으로 컴포넌트를 랜더링하는 단순한 로직이었다.Tanstack Query를 사용하는 커스텀 훅을 활용하기 때문에 use client 옵션을 적용하게 되었다.

const MonthlyBestArtistSection = async () => {
  const users: ArtistInfoType[] = await getMonthlyPopularArtists();

  const slides = users.map((info: ArtistInfoType, index: number) => (
    <ArtistProfileCard
      artistInfo={{ ...info }}
      rank={index + 1}
      isPriority={index < 4}
    />
  ));

  return (
    <div className='flex flex-col gap-[90px]'>
      <div className='flex flex-col gap-[30px]'>
        <p className='title-l'>이달의 예술가 TOP10</p>
        <p className='subtitle1'>
          지난 한 달간 가장 많은 작품 좋아요를 받은 작가들이에요.
        </p>
      </div>
      <ControlledCarousel slides={slides} />
    </div>
  );
};

export default MonthlyBestArtistSection;

→ 개선 후에는 서버에서 활용하지 않는 Tanstack Query 관련한 로직들을 제거하여 데이터 fetching 하는 로직만 남겨두었다.



priority 옵션

    const MonthlyBestArtistSection = async () => {
    	...
    
      const slides = users.map((info: ArtistInfoType, index: number) => (
        <ArtistProfileCard
          artistInfo={{ ...info }}
          rank={index + 1}
          isPriority={index < 4}
        />
      ));
    
      return (
        <div className='flex flex-col gap-[90px]'>
          <div className='flex flex-col gap-[30px]'>
            <p className='title-l'>이달의 예술가 TOP10</p>
            <p className='subtitle1'>
              지난 한 달간 가장 많은 작품 좋아요를 받은 작가들이에요.
            </p>
          </div>
          <ControlledCarousel slides={slides} />
        </div>
      );
    };
    
    export default MonthlyBestArtistSection;
    const ArtworkCard = ({
      mode = 'artwork-popular',
      rank,
      artworkInfo,
      isPriority = false,
    }: ArtworkCardProps) => {
      ...
    
      return (
        <CardLayout onClick={clickArtworkCard} classname={modeClasses[mode]}>
          <Image
            src={postImage || '/images/defaultArtworkImage.png'}
            alt={postImage ? `artwork-${postId}` : 'default_image'}
            fill={true}
            sizes={modeClasses[mode] || '402px'}
            className={'object-cover'}
            priority={isPriority}
          />
    
          ...
        </CardLayout>
      );
    };
    
    export default ArtworkCard;
    

ArtistProfileCard , ArtworkCard 컴포넌트의 Props에 isPriority라는 props를 추가적으로 넘겨주고 받는 컴포넌트에서 해당 값을 활용하여 선택적으로 priority 옵션이 적용되도록 수정했다.


📈 리팩토링 효과

  • SSR 적용 후
  • priority 옵션 선택적 적용 후

→ 전반적으로 SSR을 적용했을 떄, priority 옵션을 적용했을 때 순차적으로 LCP 지표가 개선되는 것을 확인할 수 있었다.


💬 회고 및 배운 점

  • 이번 개선 작업에서 지표 측정을 lighthouse를 통해서 진행했었는데, 몇 회정도는 너무 지표가 좋게 나오거나 반대로 너무 나쁘게 지표가 나오는 경우가 있었다. 성능 측정 기준을 어떻게 잡아야 해당 지표를 신뢰할 수 있을지 고민이 되었다.
  • 두 개선 방안 모두 어쩌면 구현 단계에서 적용해 볼 수 있는 선택지들이었다는 생각도 들었다. 이런 부분들도 설계 과정에 포함하는 것과 이번처럼 구현 후 성능 개선을 하는 방향 중에 어떤 것이 더 효과적이고 잘한 방법인지도 고민이 되었다.

🧩 참고 자료

profile
나도 잘하고 싶다..!

0개의 댓글