안녕하세요. 올리브영에서 프론트엔드 개발 업무를 담당하는 코난입니다.
올리브영 프론트엔드는 NEXT.JS 프레임워크를 사용하여 웹 페이지를 개발하고 있습니다.
이전 글인 라이브관 프론트엔드 입장에서 바라보기👀에서 언급했던 Next/image
컴포넌트를 사용했을 때, NEXT.JS에서 어떻게 최적화하는지 정리해 보려고 합니다. 🙇♂️
글을 작성한 이유
NEXT.JS는 개발자의 고민을 어떻게 해결해 주고 있는지가 궁금했고, 우리가 쓰는 기술을 이해하고 쓰고 싶어서 공부하던 중에 이거 정리해서 공유하면 괜찮겠는데?
라는 생각에, 이번 글을 작성했습니다.
문서는 아래와 같은 순서로 정리했습니다.
- NEXT.JS의 이미지 최적화
- sharp VS Squoosh
NEXT.JS의 이미지 최적화
Next/image에서 제공하는 기능
NEXT.JS에서 기본적으로 제공하는 기능은 아래와 같습니다.
- 장치의 크기에 맞춘 적절한 이미지 사이즈와 최신 이미지 포맷 지원
- Web Vitals의 CLS 발생을 방지
- 레이지 로드를 기본적으로 사용하고 있기 때문에, 뷰포트에 노출됐을 때 이미지 로드. 선택적으로 블러링 처리한 이미지를 먼저 노출하는 기능
- 이미지 리사이징. 외부 이미지도 리사이징 가능
img VS Next/image
HTML 기본 이미지 태그인 <img>와 Next/image 컴포넌트의 동작을 비교해 보겠습니다.
위 스크린샷은 크롬 브라우저에서 네트워크 쓰로틀링을 3G 환경으로 제약을 걸어서 테스트했을 때의 결과입니다.
- 빨간 사각형은 일반 <img> 태그를 사용했을 때, 이미지를 내려받는데
1.9MB, 11.5초
- 녹색 사각형은 Next/image 컴포넌트를 사용했을 때, 이미지를 내려받는데
16.9KB, 862ms
실제 결과를 봤을 때 확실한 차이가 있습니다. 그렇다면, NEXT.JS는 어떻게 이러한 차이를 만들 수 있었을까요?
이제 이 부분에 대해서 살펴보도록 하겠습니다.
Next/image 사용 예시와 렌더링 결과
먼저 우리가 에디터에서 작성한 모습입니다. Next/image
컴포넌트를 사용하는 모습이고, 일반적인 <img> 태그를 쓸 때와 비슷하게 src, width, height를 사용하는 모습입니다.
다음은 브라우저에서 렌더링했을 때의 모습입니다. 동일한 이미지이지만 이미지 경로가 우리가 코드를 작성할 때와 다르게 나오는 것을 확인할 수 있습니다.
우리가 선택한 이미지 경로가 아니라, /_next/image path 뒤에 쿼리스트링으로 url
에 우리가 지정한 이미지 경로를, w
값에 828을, q
값으로 75를 전달하고 있습니다.
src가 변하는 이유는 Next/image 컴포넌트의 loader 속성에 의해 변하는 것인데요.
Next/image
컴포넌트 코드의 일부 입니다. NEXT.JS 서버는 동작 시, /_next/image
라는 이미지 최적화를 위한 라우트를 만들고, 내부에서 이미지 최적화 모듈을 사용하여 이미지를 최적화합니다. NEXT.JS 서버의 이미지 최적화 요청 URL인 _next/image를 호출할 때 필요한 변환을 여기서 처리하고 있습니다. 그래서 기본 로더를 수정하지 않았다면 위와 같은 주소로 이미지 요청 주소를 변경하고 이미지 최적화를 처리하게 됩니다.
w는 우리가 지정한 이미지의 너비에 따라 next.config.js
파일 내에서 지정한 너비 브레이크 포인트에 맞춘 값을 설정하고, q는 next/image 컴포넌트의 quality props에서 지정한 값을 전달하는데, 값을 지정하지 않았기에 기본값인 75를 전달하고 있습니다.
이미지 최적화 시점과 이미지 재사용
NEXT.JS는 요청이 들어왔을 때, dist 폴더 밑에 cache/images
폴더에 최적화한 이미지를 동적으로 만들고, 이후에 동일한 요청에 대해서는 이미 만들어 놓은 최적화한 이미지를 캐시로서 재사용합니다.
왼쪽은 NEXT.JS 서버를 처음 실행했을 때의 모습입니다. 기본 dist 폴더인 .next 폴더 아래의 cache 폴더 아래에 이미지 관련 폴더가 없습니다.
오른쪽은 브라우저에서 실제로 유저가 이미지를 요청한 이후의 모습이며, 유저가 이미지를 요청했을 때 최적화한 이미지를 생성하는 것을 확인할 수 있습니다.
그리고, NEXT.JS의 서버가 동작한 뒤에 첫 요청이 들어온 경우에는 이미지를 최적화하는 로직이 있기 때문에 시간이 조금 더 오래 걸립니다. 첫 번째 요청이 끝난 후에 다시 동일한 이미지를 요청하는 경우에는 이미 최적화되어 있는 이미지를 재사용하기 때문에 좀 더 빠르게 응답하는 모습을 볼 수 있습니다.
아래 그림에서 볼 수 있듯이 첫 번째 요청은 64ms
가 걸렸지만, 두 번째 요청은 최적화한 이미지를 재사용했기에 5ms
로 빠르게 응답을 줬습니다.
최적화한 이미지를 재사용했는지 여부는 NEXT.JS에서 추가로 전달하는 응답 헤더를 살펴보면 알 수 있습니다.
이미지가 캐시가 되어 있지 않았다면, X-Nextjs-Cache 헤더에 MISS
를 이미지가 캐시되어 있었다면, HIT
를 응답으로 전달하기 때문에 이 값을 보고 이미지의 캐시 여부에 대한 판단이 가능합니다.
모든 이미지를 최적화하는가?
NEXT.JS가 이미지 최적화를 지원한다고 모든 이미지를 최적화 해줄까요? 아쉽게도 NEXT.JS도 만능은 아니기에 모든 이미지를 최적화하진 않습니다.
아래 이미지는 NEXT.JS의 이미지 최적화 모듈의 코드 일부분입니다.
동적으로 최적화를 해야 하므로 이미지 최적화가 필요 없는 SVG와 같은 vector 이미지, 그리고 GIF와 같은 상대적으로 복잡하고 최적화에 오래 걸리는 애니메이션 이미지의 경우에서는 코드 레벨에서 최적화를 진행하지 않고 바로 응답으로 내려주게 되어 있습니다.
그렇기에, 일부 이미지에 대해서는 의도적으로 lodaer 옵션을 오버라이드해서, 직접적인 URL을 요청하도록 코드를 수정하거나, Next/image 컴포넌트의 props인 unoptimized
를 사용하는 것에 대한 고민이 필요합니다.
sharp VS Squoosh
우리가 NEXT.JS를 사용하다 보면 문서를 보거나 서버를 실행했을 때 NEXT.JS는 운영 환경에서는 sharp
라이브러리를 사용할 것은 권장하고 있습니다.
sharp를 사용하는 것을 매우 추천하고 있는데, 왜 그럴까요? 🤔 이 부분을 알아보고자 합니다.
NEXT.JS는 어떻게 sharp의 유무를 판단할까?
시작하기에 앞서, NEXT.JS는 어떻게 sharp가 설치되어 있는지를 검사하는지 궁금하여 코드를 살펴봤습니다.
이미지 최적화 모듈을 초기화할 때, sharp를 import 함으로써 sharp의 설치 여부를 확인하고 이후에 동작하는 로직에서 sharp 변수를 기준으로 동작하는 방식으로 코드가 작성되어 있었습니다.
sharp? Squoosh?
NEXT.JS는 Squoosh를 기본 이미지 최적화 모듈로 사용하고 있고, Squoosh는 빠르게 설치할 수 있고 개발 환경에 적합하다고 합니다. 그런데, 운영 환경에서는 sharp를 사용하는 것을 매우 강력하게 권장하고 있습니다.
sharp
Squoosh
sharp와 Squoosh 성능 비교
코드는 동일하고 sharp 라이브러리를 추가해서 NEXT.JS 서버를 구동한 뒤 설치 전/후 이미지 최적화 결과를 비교해 봤습니다.
PNG -> WebP 변환
webp 파일로 변환했을 때의 모습입니다. 원본은 1.9MB의 이미지 파일입니다.
이미지 크기를 비교했을 때, Squoosh는 17.1KB
로, sharp는 16.9KB
로 크기를 감소했습니다.
크기를 비교했을 때는 큰 차이가 없지만, 응답속도를 비교한다면 Squoosh는 228ms
, sharp는 64ms
입니다. sharp를 사용했을 때 약 3~4배
정도로 빠르게 응답을 줬습니다.
PNG -> AVIF 변환
AVIF 파일로 변환했을 때의 모습입니다. 동일하게 원본은 1.9MB의 이미지 파일입니다.
이미지 크기를 비교했을 때, Squoosh는 10.8KB
로, sharp는 13.1KB
로 크기를 감소했습니다.
이번에도 크기를 비교했을 때는 큰 차이가 없지만, 응답속도를 비교한다면 Squoosh는 1.24s
, sharp는 202ms
입니다. sharp를 사용했을 때 약 6배
정도로 빠르게 응답을 줬습니다.
이 정도의 속도 차이가 난다면, sharp를 사용하지 않을 이유가 없을 것 같습니다.
AVIF에 대한 추가적인 내용은 이전 글인 웹사이트 최적화 방법 - 이미지 파트 문서를 참고하시면 됩니다.
지금 AVIF를 사용해도 괜찮을까?
AVIF는 요즘 대부분의 브라우저에서 지원하지만 일부 지원하지 않는 경우도 있으니 사용할 때 주의해야 합니다.
아래 그림은 caniuse.com에서 AVIF를 검색했을 때의 결과입니다.
보이는 것처럼, Edge 브라우저는 AVIF 포맷을 지원하지 않고 있습니다.
그럼 AVIF를 사용하면 안 될까요?
아닙니다.🙅 사용하셔도 됩니다.🙆
브라우저에서는 이미지 파일을 요청할 때, Accept 헤더에 본인이 사용할 수 있는 이미지 포맷에 대한 정보를 함께 전달합니다.
아래 이미지는 Edge 브라우저의 요청 헤더입니다. webp, apng, svg, 일반적인 이미지 포맷을 사용할 수 있다고 알려주고 있습니다.
아래 이미지는 Chrome 브라우저의 요청 헤더입니다. 추가로 AVIF를 사용할 수 있음을 같이 보내주고 있습니다.
NEXT.JS의 이미지 최적화 모듈의 캡쳐입니다.
NEXT.JS 서버의 이미지 최적화 모듈은 요청 헤더에 있는 Accept 헤더를 읽고, 브라우저에게 내려줄 mimeType
을 결정하고 요청한 브라우저에서 처리할 수 있는 이미지 형식으로 최적화하여 내려주게 됩니다.
그렇기 때문에, NEXT.JS 설정에서 AVIF를 사용하겠다고 설정하더라도, AVIF를 지원하지 않는 Edge 브라우저에게는 Webp
포맷을, AVIF를 지원하는 Chrome 브라우저에서는 AVIF
포맷을 응답으로 내려주기 때문에 사용하셔도 됩니다.
요약
- NEXT.JS는 모던 브라우저에서 고려해야 할 대부분의 최적화를
자체적으로 지원
하고 있어 개발자의 부담을 덜어줄 수 있다. - 이미지 최적화는 빌드 타임이 아닌
런타임에 요청이 들어왔을 때 최적화를 진행
한다. 그렇기에 최초 1회 요청은 응답이 느릴 수 있다. - sharp를 사용하면 Squoosh를 사용할 때와 비교하면 WebP 포맷으로 최적화할 때는
3~4배
, AVIF 포맷으로 최적화할 때는3~6배
정도의 성능 개선이 있다. - 브라우저의 요청에 맞춰서 최적화한 이미지를 응답으로 내려주기에
브라우저 호환성에 대한 고민을 덜
할 수 있다.
NOTE
내용을 기반으로 한 발표 영상과 자료가 있어 함께 전달드립니다.