[개발스토리] 랜딩페이지 성능 개선

ActionPower
12 min readMar 15, 2023

안녕하세요 👋 액션파워 플랫폼그룹 프론트엔드 개발팀입니다. 오늘은 다글로 랜딩 페이지를 개발하며 웹 성능을 개선한 경험을 공유해보려 합니다.

문제 상황

개발 환경에서는 성능 문제를 느끼지 못했으나, staging 배포 후 확인을 해보니 초기 로딩 속도가 11초로 매우 느린 이슈가 발생하였습니다.

개선 전 lighthouse 점수

원인

1. 이미지 파일의 위치

첫 번째 성능 저하 원인은 이미지 파일이 public 폴더 안에 위치하지 않았기 때문이었습니다.

Next.js에서 public 폴더에 포함된 정적 파일들은 빌드 타임에 번들에 포함되어 브라우저에서 요청할 수 있도록 만들어줍니다.

이미지 파일 경로는 Next.js에서 public 폴더에서부터 찾게 되며, 이미지 파일이 public 폴더에 없으면 해당 파일을 제공할 수 없으므로 404 오류가 발생하거나 이미지가 제대로 표시되지 않을 수 있습니다.

Next.js 문서에서도 이미지와 같은 정적 자산을 public 폴더에 배치하는 것을 권장하고 있습니다.

💡 Static File Serving.
Next.js can serve static files, like images, under a folder called public in the root directory. (출처)

루트 폴더/assets 밑에 존재하던 이미지 파일들을 public/assets 파일 안으로 이동하였습니다.

2. next/image 컴포넌트

성능 점수는 크게 개선되었지만 스테이징 환경에 설정되어있는 타임아웃을 초과하여 500에러를 뱉으며 일부 이미지 파일을 불러오지 못하는 문제는 여전히 발생하였습니다.

문제 해결을 위해 시도해본 것

1. sharp 라이브러리 설치

next/image 컴포넌트는 개발 환경에서 기본 로더로 squoosh를 사용하고 있습니다. 하지만 next start를 사용하는 프로덕션 환경에서는 sharp를 설치하는 것을 권장하고 있습니다.

💡 If your project uses Image Optimization with the default loader, you must install sharp as a dependency. (출처)

2. Minimum Cache 유효시간 늘리기

기본 로더를 사용할 경우 이미지는 요청 시 동적으로 최적화되고 <distDir>/cache/images 디렉토리에 저장됩니다. 최적화된 이미지 파일은 만료될 때까지 후속 요청에 제공됩니다. 캐시 된 이미지와 일치하지만 만료된 파일에 대한 요청이 발생하면 만료된 이미지가 즉시 제공됩니다. 그런 다음 이미지는 다시 백그라운드에서 최적화되고(재유효성 검사 또는 재검사라고도 함) 새로운 만료일과 함께 캐시에 저장됩니다.

최적화된 이미지에 대해 캐시 유효시간을 초 단위로 구성할 수 있습니다. 캐시 유효기간을 최대 시간인 1년으로 설정했습니다.

module.exports = {
images: {
minimumCacheTTL: 31536000,
},
}

두 가지 방법을 모두 시도해보았고 캐시가 적용된 이미지에 대해서는 로딩이 매우 빨랐지만 캐시되지 않은 이미지들은 여전히 느리게 로딩되었습니다.

Next.js의 next/image 컴포넌트는 자동으로 이미지를 최적화하여 최신 이미지 포맷(WebP 또는 AVIF)으로 변환하고, 원본 이미지 크기와 브라우저에서 이미지를 표시하는 영역의 크기를 비교하여 적절한 사이즈의 이미지를 제공합니다.

이러한 이미지 최적화는 상당한 CPU 리소스를 필요로 하는 프로세스입니다. 다글로 프론트엔드 코드는 서버리스 (Firebase Function, 256MB 메모리, .167 vCPU) 로 구축되어 있습니다. 서버의 CPU 처리 능력과 메모리가 충분하지 않은 환경에서 용량이 큰 이미지를 처리하여 많은 시간이 소요된 것 입니다. 이처럼 서버리스 환경에서 next/image 컴포넌트를 사용할 때는 서버의 리소스 제약을 고려하여 이미지 최적화 작업이 느리거나 실패할 가능성이 있다는 점을 유의해야 합니다.

이 같은 문제는 이미지 최적화 작업을 수행하는 CDN 서버를 구축하거나, 호스팅 서버의 메모리 성능을 높여서 해결할 수 있습니다. 하지만 저희는 아래의 3가지 이유로 당분간 랜딩 페이지 이미지들에 next/image 컴포넌트를 쓰지 않는 것으로 결정하였습니다.

  • 현재 우리 서비스에서 이미지를 적극적으로 활용하는 부분은 많지 않음
  • 이미지가 클수록 컨버팅하다가 서버가 500에러가 발생하는 경우가 많음
  • 몇개의 랜딩 페이지 이미지 최적화를 위해 CDN을 구축하거나 고사양의 서버 인프라를 구축하기에 리소스 및 관리 측면에서 비효율적이라고 판단 됨

next/image 컴포넌트에서 자동으로 해주던 최적화 몇가지를 직접 구현 적용해보면서 LCP 점수를 개선해보았습니다.

Next/image 컴포넌트 사용하지 않고 LCP를 개선하는 방법

LCP(Largest Contentful Paint)는 페이지의 주요 콘텐츠 중에서 가장 큰 요소가 화면에 표시되는 시점을 측정합니다. 일반적으로 LCP가 빠를수록 페이지의 로드 속도와 사용자 경험을 향상시킬 수 있습니다. 랜딩 페이지에서는 서비스 이미지가 최대 요소가 됩니다. LCP 점수를 개선하기 위해 파란색 테두리로 표시된 서비스 예시 이미지에 최적화를 진행하였습니다.

1.반응형 이미지

next/image 컴포넌트를 사용할 때 config에 디바이스 endpoint를 미리 지정해두면, next/image 컴포넌트에서 sizes prop을 사용할 때, 자동으로 디바이스 크기에 맞는 적절한 이미지를 제공합니다.

module.exports = {
images: {
deviceSizes: [640, 1024, 1440],
},
}

next/image 컴포넌트를 걷어낸 후 img 태그로 변경하면서 모바일에서 데스크탑 크기의 이미지를 그대로 불러와 사용하면서 모바일에서 성능이 크게 저하되었습니다.

브라우저 크기에 맞는 적절한 반응형 이미지를 제공하기 위해 picture 태그를 사용하였습니다. picture는 다양한 환경에서 이미지를 효과적으로 제공하기 위해 사용하는 태그입니다. 미디어 쿼리를 사용해 각 URL의 로딩 조건을 구체적으로 정의할 수 있으며 정의된 조건에 맞는 이미지만 사용하도록 강제할 수 있어 조건에 맞지 않는 이미지는 로드 하지 않습니다.

<picture>
<source
srcSet='/image_desktop.png'
media={`(min-width:${SCREEN_SIZE.TABLET}px)`}
/>
<source
srcSet='/image_tablet.png'
media={`(min-width:${SCREEN_SIZE.MOBILE}px)`}
/>
<img src='/image_mobile.png' alt={alt} />
</picture>

<picture> 요소는 0개 이상의 <source> 요소와 하나의 <img> 요소로 구성됩니다.

<source> 태그에서 srcSet 속성은 이미지 파일 경로를 지정합니다. 브라우저 크기에 따라 다른 이미지를 사용하기 때문에, 각 이미지 크기에 맞게 적절한 파일 경로를 지정해야 합니다.

media 속성은 미디어 조건을 지정하는 것으로, 브라우저 크기가 [TABLET]px 이상인 경우image_desktop.png 이미지가, [MOBILE]px 이상인 경우 image_tablet.png 이미지가, [MOBILE]px 이하인 경우 image_mobile.png 가 적용됩니다.

<img> 요소는 <picture> 요소의 자식 요소 중에서 가장 마지막에 위치해야 합니다. <picture> 요소를 지원하지 않는 브라우저의 하위 호환성을 위해 사용되거나 명시된 <source> 요소가 모두 조건을 만족하지 못할 경우 사용됩니다.

적용 결과

디바이스에 적합한 크기의 이미지를 제공함으로써 모바일 환경에서의 LCP가 약 5.6초로 단축되었습니다.

2. preload

preload는 js 코드를 로드함과 동시에 이미지 요소를 실제로 발견하기 전에 미리 요청하도록하여 웹 페이지 성능(LCP점수)를 향상시키는데 중요한 역할을 합니다.

With/without image preload, image 출처 lighthouse

next/image 컴포넌트를 사용할 경우 priority 속성을 추가하여 간단하게 preload를 구현할 수 있습니다. priority 속성을 추가하고 개발자 도구로 확인해보면 head 태그 안에 link 태그로 이미지를 preload 하는 코드를 발견할 수 있습니다. next/image 컴포넌트를 사용하지 않고 link 태그를 이용하여 직접 preload를 구현하는 방법을 알아보겠습니다.

<Head>
<link
rel="preload"
as="image"
href={sources.mobile}
media={`(max-width:${SCREEN_SIZE.MOBILE}px)`}
/>

<link
rel="preload"
as="image"
href={sources.tablet}
media={`(min-width:${SCREEN_SIZE.MOBILE + 1}px) and (max-width:${
SCREEN_SIZE.TABLET
}px)`}
/>
<link
rel="preload"
as="image"
href={sources.desktop}
media={`(min-width:${SCREEN_SIZE.TABLET + 1}px)`}
/>
</Head>

next/head 컴포넌트는 Next.js에서 제공하는 컴포넌트로, HTML <head> 요소를 수정하고, head에 들어갈 내용을 추가하는 데 사용됩니다.

<link> 요소는 HTML 문서 내부에서 다른 리소스를 참조하는 데 사용됩니다. rel 속성을 사용하여 미리 로드 할 리소스를 지정합니다. as 속성은 리소스의 유형을 나타내며, href 속성은 리소스의 URL을 지정합니다.

media 속성을 이용하여 브라우저가 현재 장치의 화면 크기를 확인하고, 해당 크기에 해당하는 이미지 리소스를 로드합니다.

이미지 리소스를 preload 함으로써 약 450밀리초(ms) 로딩 시간이 개선되었습니다.

prefetch false로 LCP 개선하기

Preload Largest Contentful Paint Image경고는 사라졌지만 LCP는 여전히 권장 시간인 2.5초를 초과하였습니다.

Next.js 어플리케이션에서 next/link 컴포넌트를 사용할 경우 기본적으로 viewport 내의 모든 링크 페이지가 기본적으로 prefetch 됩니다. 랜딩 페이지의 경우 헤더에 요금제 페이지와 로그인 페이지가 prefetch 됩니다. prefetch는 웹 성능 향상에 도움을 주지만 때에 따라 시스템 메모리와 CPU 리소스를 소모하여 실행 중인 다른 작업의 속도를 떨어뜨릴 수 있습니다. 또한 사용자가 사전에 가져온 페이지를 방문하지 않는다면 해당 페이지에 대한 리소스가 불필요하게 낭비될 수 있습니다. prefetch false를 적용하여도 호버시에 prefetch를 진행하고 페이지 전환에 큰 무리가 없다고 판단되어 로그인 페이지와 요금제 페이지에 prefetch false를 설정 하였습니다.

마무리

22점에서 97점으로 성능이 개선되었습니다.

개발환경에서는 보통 개발자의 로컬 컴퓨터에서 작업을 수행하며 좋은 성능의 pc를 사용하고 있을 확률이 높습니다. 반면 배포환경에서는 서버의 사양, 인프라의 제약, 네트워크 속도 등이 모두 로딩 속도에 영향을 미치기 때문에 개발환경에서는 빠르던 웹사이트가 배포환경에서는 느리게 로딩 될 수 있습니다. 웹사이트를 개발할 때 가능한 실제 배포환경과 유사한 환경에서 다시 확인해보는 것이 중요하다는 것을 알게 되었습니다.

웹 사이트 성능 최적화는 사용자 경험, 검색 엔진 최적화(SEO), 비용 절감, 매출 증대 등 다양한 측면에서 기업의 경쟁력을 높이는 데 중요한 역할을 합니다.

추후에는 어떤 작업으로 인해 웹 페이지가 느려졌는지 파악하고, 성능 이슈를 해결하여 웹 페이지의 사용자 경험을 신속하게 개선하기 위해 Lighthouse CI를 도입해보고 싶습니다.

앞으로도 성능을 개선하는 데 도움이 되는 기법들을 계속해서 적용하며 다양한 경험들을 공유드리고 싶습니다. 감사합니다.

Reference

Basic Features: Static File Serving | Next.js
next/link | Next.js
Largest Contentful Paint (LCP)
Image loading is super slow with next/image · vercel/next.js · Discussion #21294
Preload key requests — Chrome Developers
웹에 날개를 달아주는 웹 성능 최적화 기법

--

--

ActionPower

Cutting-edge AI for the Benefit of the World. Unlock the potential of AI for a better tomorrow! For more details: actionpower.kr/en