프로젝트를 마무리하고 성능 개선을 좀 더 하고싶어 리팩토링을 했다. 이미지 최적화를 통해 좀 더 로딩 속도를 빠르게 하기를 원했고, prefetch 적용 전과 후를 비교함으로써 어떠한 차이가 있는지 좀 더 딥하게 공부를 해보았다.

Next.js에서 Image 컴포넌트를 사용했기 때문에 기본적인 최적화 기능이 갖추어져있다. 예를들면 lazy loading, webp 형식으로의 변환, 캐싱 등이있다. 이미지를 요청하게 되면 최적화된 이미지가 cache/images 폴더에 생성이 된다.
또한 Image 태그에는 width와 height를 지정해야 next.js의 Image 컴포넌트를 잘 사용했다라고 할 수 있다. 근데 이미지 크기가 동적으로 변해야 한다면?
<div>
<Image
src={src}
alt={alt}
fill
/>
</div>
width와 height 대신 fill 속성을 사용하면 된다. width와 height는 Image 컴포넌트를 감싸준 div태그에! 그러면 이미지가 부모 요소를 채우도록 동작하게 된다.
Image 컴포넌트를 사용해서 기본적인 최적화 기능은 갖추고 있었지만, 아쉬운 점들이 좀 보였다. 용량이 큰 사진일 수록 업로드 속도가 느리다는 것이었다. 당연한 얘기이지만 욕심이 생겨서 좀 더 시간을 단축시켜 보기로 했다.
1. 개선 전 측정한 시간(캐시 적용X, 3g slow 적용)

개선을 하기 전 시간을 보면 가장 큰 용량의 이미지는 227kB이며, 오래 걸린 이미지는 약 21.6s 소요된 것을 볼 수 있다. 아무리 3g 환경에서 측정했다하지만, 너무 오래걸린다는 느낌을 받았다.
2. size 속성 추가
첫 번째 개선 방법으로 Image 컴포넌트에 sizes 속성을 추가하는 것이었다. 이유는 Image 태그의 sizes속성 기본값이 100vw이다.
한 마디로 layout:fill을 주게 될 경우 무조건 뷰포트 크기에 따라 이미지를 요청하기 때문에 이를 방지하고자 sizes 속성을 지정해준 것.
const sizesConfig: Record<ImageProps['type'], string> = {
product: '(max-width: 768px) 50vw, (max-width: 1024px) 50vw, 20vw',
profile: '(max-width: 768px) 36px, (max-width: 1024px) 36px, 33vw',
review: '(max-width: 768px) 60px, (max-width: 1024px) 80px, 33vw',
detail: '(max-width: 768px) 335px, (max-width: 1024px) 280px, 355px',
};
<Image
draggable={false}
src={src}
fill
alt={alt}
sizes={sizesConfig[type]}
priority
/>

sizes 속성만 추가했는데도, 이미지 크기 및 시간이 단축된 것을 볼 수 있었다.
3. imageSizes, deviceSizes
next.js에서 이미지는 따로 설정을 안 해주면 아래와 같은 기본값을 갖게 된다.
imageSizes: [16, 32, 48, 64, 96, 128, 256, 384],
deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048, 3840],

개발자 도구에서 이미지를 자세히 보면 기본값대로 모두 가져오게 되는 것이다. 불필요한 값들도 불러오니 좀 더 효율적으로 개선시켜보려고 했다.
imagesSizes는 Image컴포넌트에서 fill 속성을 사용하여 동적인 이미지 사이즈를 보여주고 싶을 때 사용된다. 앞서 fill 속성을 사용했기 때문에 imageSizes를 nextConfig에 추가했다.
imageSizes: [32, 48, 128, 280, 384],
deviceSizes: [640]
동작원리는 다음과 같다.
product: '(max-width: 768px) 50vw, (max-width: 1024px) 50vw, 20vw',
브라우저 최대 너비가 768px일때 50vw로 지정을 했다. 이는 브라우저 너비의 1/2를 뜻한다. 즉, 브라우저 너비가 700px이라면 절반인 350px의 이미지 크기를 뜻하는데, 해당 값을 imageSizes와 비교해보자. 280과 385 사이이므로 350 이상의 값인 384의 값으로 이미지를 다운로드하게 된다.

다음은 tanstack-query의 prefetch 기능이다. prefetch는 특정 페이지를 들어가기 전에 데이터들을 미리 가져와 캐싱하여 미리 로드한다. 그 결과 페이지 로딩이 빨라지게 된다.
const queryClient = getQueryClient();
await Promise.all([
queryClient.prefetchInfiniteQuery(
followerOptions(Number(userId), 'follower'),
),
queryClient.prefetchInfiniteQuery(
followeeOptions(Number(userId), 'followee'),
),
queryClient.prefetchInfiniteQuery(
productOptions(
Number(userId),
TAB_NAMES_ORIGIN.createdProduct,
getUserCreatedProducts,
),
),
queryClient.prefetchInfiniteQuery(
productOptions(
Number(userId),
TAB_NAMES_ORIGIN.favoriteProduct,
getUserFavoriteProducts,
),
),
queryClient.prefetchInfiniteQuery(
productOptions(
Number(userId),
TAB_NAMES_ORIGIN.reviewedProduct,
getUserReviewedProducts,
),
),

프로필 페이지로 이동하면 리뷰 남긴 상품, 등록된 상품, 찜한 상품 이렇게 3개의 메뉴가 보이게 된다. 나는 이 세 메뉴에 대한 데이터들을 모두 미리 받아오고 싶었던 것이다. 그래서 prefetch를 적용했을 때와 안했을 때의 차이는 얼마나 날 지 분석을 해보았다.(3g slow 기준)


첫 번째 이미지는 prefetch 적용을 안했을 때이다. 네모 표시한 api 요청이 있는 것을 볼 수 있다. 이 부분에서 차이가 났다. 약 2초 정도이긴 하지만, 2초면 크게 느껴질 수 있는 시간이라고 생각한다. useQuery는 prefetch 중인 api를 중복 호출하지 않음!
추가적으로 프로필 내의 팔로워, 팔로잉 유저 리스트들도 prefetch를 사용하여 미리 데이터를 캐싱하고 싶었다. 위 방법과 동일하게 팔로워, 팔로잉 데이터들을 불러오는 동일한 함수와 쿼리키로 prefetch를 시도하였고, 그 결과 3g slow기준 약 2초 정도 로딩 속도를 단축할 수 있었다.



![]() | ![]() |
|---|
프로필 편집을 하고 나면 느리게 업로드 됐던 이미지 이슈도 위에서 했던 이미지 최적화 방식으로 어느정도 해결이 되었다. 4~5MB인 이미지 기준으로 0.7s 정도 단축시켰다.