본문 바로가기
Front-end

Next.js 14 SSR, Tanstack Query prefetchQuery 활용법 With LCP 단축으로 웹 성능 개선

by ghDev 2024. 7. 24.

 

기존 아트인포 는 CSR Suspense 방식으로 메인페이지가 구현되어 있었다.

성능 측정을 하였을때

before

 

LCP에서 아쉬운 결과가 나왔다.

Largest Contentful Paint(LCP)란?

LCP는 뷰포트 내에서 가장 큰 콘텐츠 요소의 로드 시간을 측정하는 성능 지표로, 
페이지가 사용자에게 시각적으로 준비되기까지 걸리는 시간을 나타냅니다. 
이는 빠르고 매력적인 사용자 경험을 만드는 데 중요한 요소입니다.

 

출처: https://web.dev/articles/optimize-lcp?hl=ko

 

최대 콘텐츠 렌더링 시간 최적화  |  Articles  |  web.dev

LCP 분석 및 개선이 필요한 주요 영역을 파악하는 방법에 관한 단계별 안내

web.dev

 

 

아트인포 특성상 이미지 위주의 컨텐츠로 이미지 포멧변환(Webp), 이미지 캐싱 등으로 최적화를 해둔 상태지만 그래도

 

아쉬운 결과에 다른 개선 방법을 찾아보았다.

떠오른 방법은 SSR을 활용하여 미리 데이터를 불러온다면 CSR에서 데이터를 불러오는 시간만큼 줄어들거고 그만큼 LCP 시간이 줄어들꺼라 생각하였다.

/page.tsx

메인 페이지 SSR환경에서 Tanstack query의 prefetchQuery를 활용하였다.
 const queryClient = GetQueryClient()

 try {
   await Promise.all([
     queryClient.prefetchQuery(queries.ads.list(AdvertisementType.CONCERT)),
     queryClient.prefetchQuery(queries.ads.list(AdvertisementType.EXHIBITION)),
     queryClient.prefetchQuery(
       queries.jobs.list({
         page: 1,
         size: 5,
         types: [JobType.ART_ORGANIZATION, JobType.LECTURER],
         professionalFields: [
           ProfessionalFieldTypes.CLASSIC,
           ProfessionalFieldTypes.POPULAR_MUSIC,
           ProfessionalFieldTypes.TRADITIONAL_MUSIC,
           ProfessionalFieldTypes.ADMINISTRATION,
         ],
       }),
     ),
     queryClient.prefetchQuery(queries.ads.list(AdvertisementType.BANNER)),
   ])
 } catch (error) {
   console.error("Failed to prefetch queries:", error)
 }

 const dehydratedState = dehydrate(queryClient)

 

메인페이지에선 4개의 쿼리 호출이 필요하여 Promise.all로 처리하였다.

 

/app/page

<HydrationBoundary state={dehydratedState}>
  <BannerContainer />
  <ArtContainer type={AdvertisementType.CONCERT} title="공연" />
  <MainJobsContainer />
  <ArtContainer type={AdvertisementType.EXHIBITION} title="전시" />
</HydrationBoundary>

 

@/components/BannerContainer

const { data: ads } = useQuery(queries.ads.list(AdvertisementType.BANNER))

@/components/ArtContainer

const { data: arts } = useQuery(queries.ads.list(type))


이런 방식으로 컴포넌트에서 prefetch한 query를 다시 호출해주었다. 

물론 해당 query는 prefetch 해왔기에 api 호출은 하지 않고 데이터를 꺼내쓰는 용도이다.

그리고 loading.tsx를 만들어 로딩처리(Skeleton UI)를 추가해주었다.

@/app/loading.tsx

const loading = () => {
  return (
    <div className="mx-auto h-full max-w-screen-lg px-4">
      <BannerSkeleton />
      <ArtSkeleton />
      <MainJobSkeleton />
      <ArtSkeleton />
    </div>
  )
}

export default loading

 

추가적으로 LCP(Largest Contentful Paint)를 줄이기 위해

 

가장 큰 크기의 컨텐츠인 BannerImage에 

Next/image

<FallbackImage
 src={ad.imageUrl}
 alt="banner_image"
 priority
 fill
 className="rounded-xl"
 sizes="100vw"
 />


priority(우선순위) 옵션을 추가해줬다.

다른 공연 등의 컨텐츠엔 false

이러한 설정으로 가장 큰 컨텐츠가 우선순위로 렌더링되며 더 큰 LCP 단축을 노려본다!


이후 가장 중요한 성능 측정!

 

 

LCP(Largest Contentful Paint)가 2.7초에서 0.8초로 약 70%가량감소 한걸 확인 할 수 있다.

덤으로 FCP(First Contentful Paint)도 0.7초에서 0.3초로 약 50%가량 감소된걸 확인 할 수 있다.

물론 SSR은 모든 서비스에 적용 할 수 있는건 아니라고 생각한다.

이유는 그만큼 브라우저에서 할 일을 Next.js 서버에 위임을 한 것이기 때문이고 아트인포는 서버의 부하를 고려 할 만한

트래픽을 가지지 않았기때문에 큰 고민없이 적용 할 수 있었다.