사건의 발단
이번에 진행한 사이드 프로젝트 '그돈이면'의 홈 화면에는 gif애니메이션이 들어가있다. 나는 당연히 Next에 내장된 Image 컴포넌트를 사용했고 별 다른 문제 없이 적용되었다 생각하며 넘어갔다.
주변 지인에게 프로젝트를 소개하면서 홈 화면에 이미지가 늦게 뜬다는 피드백을 받았는데 확인해보니 gif에 Image 컴포넌트를 사용했음에도 불구하고 WebP 포맷으로 변환되어 있지 않았다.
게다가 용량도 7.7MB라니... 굉장히 큰 사이즈의 이미지를 가져오고 있었다.
_log.warnOnce(`The requested resource "${href}" is an animated image so it will not be optimized. Consider adding the "unoptimized" property to the <Image>.`);
next모듈에서 이미지 컴포넌트가 있는 코드(/node_modules/next/dist/server/image-optimizer.js)를 살펴보니 GIF가 적용된 이미지 컴포넌트에서는 위와 같이 warning을 내려주고 있었다.(왜 나는 경고가 뜨지 않았는지 모르겠다)
그리고 받은 이미지를 그대로 리턴해주고 있어 이미지 태그를 사용하지 않는 것과 다름이 없었다.
이미 공식 문서에서도 Image 컴포넌트의 기본 로더가 애니메이션이 적용된 이미지에 대해서는 자동으로 우회해서 그대로 내려주고 있다고 쓰여 있었다.(공식 문서를 꼼꼼하게 읽자..) 만약 명시적으로 우회하려면 unoptimized 속성을 사용하라고 한다.
어찌됐든 Image 컴포넌트 최적화 기능을 사용하지 못하는 셈이니 계산을 한 번이라도 덜 하도록 써주는 것이 좋다고 생각하고 해당 이미지에 unoptimized props를 작성했다. 물론 이걸 작성한다고 해서 달라진 것은 없었다. 여전히 큰 용량의 이미지를 가져오고 있었고, 속도 또한 개선되지 않았다.
그럼 GIF는 어떻게 최적화해야 돼?
1. 비디오 포맷(MP4, WebM)으로 변경
검색하니 가장 많이 나오는 방법이었다. 그러나 내가 적용하려는 gif는 투명한 배경인데 이를 지원하지 않는 MP4는 불가능하다.
실제로 gif를 여러 변환 사이트에서 MP4로 변환해 본 결과, 흰색 백그라운드가 깔린 상태로 적용됐다. 반면에 WebM은 투명한 배경을 지원한다. 이번에는 WebM 포맷으로 변환해서 적용해봤다.
<video autoPlay loop>
<source src="/imgs/animation-wallet.webm" type="video/webm" />
<source src="/imgs/animation-wallet.mp4" type="video/mp4" />
</video>
<video> 안에 <source>를 넣어 브라우저가 지원하는 포맷에 따라 우선 순위를 지정할 수 있다. 위 코드의 경우, 브라우저에서 WebM이 지원되지 않으면 MP4 파일이 대신 사용하도록 한다.
caniuse를 살펴보니 IE를 제외한 대부분의 브라우저에서 WebM 포맷이 지원되는 것 같다. 안심하고 이제 적용해보자.
용량이 215KB로 매우 줄어들었다. 성공했다 생각했는데 사파리로 확인해보니 MP4 포맷과 마찬가지로 흰색 배경이 보인다. 분명 WebM 포맷으로 잘 나오고, 내 사파리 버전도 지원되는 버전에 속해있었는데 사파리에서는 투명 백그라운드로 뜨지 않는 문제가 생겼다.
검색해보니 사파리는 HEVC포맷으로 렌더링을 해줘야 투명한 배경으로 나온다는 것이었다.
source의 우선 순위 상 WebM이 제일 상단에 있고, 사파리 또한 WebM을 지원하다보니 HEVC포맷으로 변경된 source를 사용해도 사파리에는 여전히 WebM이 가장 먼저 뜰 것이다.
이를 해결하려면 하는 수 없이 자바스크립트 코드로 window.userAgent를 확인하고 사파리면 HEVC포맷으로, 아니면 WebM 포맷이 뜨도록 해줘야 한다는 것인데 이 과정이 너무 번거롭다고 생각했다.
결국, 비디오 포맷으로 변환하는 것이 아닌 다른 방법을 찾아보기로 했다.
2. APNG 포맷으로 변환
APNG란, PNG를 확장한 이미지 파일 포맷으로 애니메이션이 되는 PNG이다. GIF나 MP4보다 낮은 용량과 선명한 애니메이션 그리고 기존 PNG 파일과 하위호환성 유지 등의 장점이 있다.
용량은 7.7MB -> 5.7MB로 아주 조금 줄어들었으나 마찬가지로 Image 컴포넌트에서 최적화를 지원하지 않는 포맷이므로 패스한다. 게다가 사파리에서는 애니메이션이 움직이지 않으므로 사용할 수 없다.
3. 이미지 cdn을 통해 최적화된 이미지 사용 (Coludinary)
내가 자체적으로 gif를 최적화 시키기 힘들다면 서버에서 최적화된 이미지를 가져오면 어떨까 생각했다. 이미지를 가져올 서버로는 이전에 Next.js강의를 학습하며 우연히 사용해봤던 Cloudinary를 사용해보기로 했다.
public폴더에 있는 gif 이미지를 Cloudinary에 업로드하고 해당 이미지 주소를 가져와서 적용하는 것이다. 방법은 문서에도 잘 나와있고 간단하다.
Next.js에서 처리해줘야 할 것은 우선 외부 도메인 허용이다. next.config.mjs파일에 아래와 같이 클라우디너리 도메인을 허용해주자.
/** @type {import('next').NextConfig} */
const nextConfig = {
images: {
remotePatterns: [
{ protocol: "https", hostname: "res.cloudinary.com" },
],
},
};
export default nextConfig;
그 다음 클라우디너리에 로그인 한다.
좌측에 Assets를 클릭한 후 Folders에 들어가면 이미지를 업로드할 수 있는 폴더를 생성할 수 있다. 기본적으로 업로드하면 Home폴더에 생성되지만 나는 프로젝트 별로 파일을 관리하고 싶어 폴더를 생성해서 넣었다.
해당 폴더에 gif 이미지를 업로드하면 메뉴를 눌러 URL을 복사할 수 있는데 아래와 같은 형태의 URL을 <Image>의 src에 넣어주면 된다. https://res.cloudinary.com/<cloud_name>/<asset_type>/<delivery_type>/<transformations>/<version>/<public_id>.<extension>
특히 URL의 transformations 파라미터에 여러 최적화 옵션을 넣으면 최적화 된 이미지를 가져올 수 있다.
나는 위와 같이 품질과 포맷을 자동으로 최적화 시켜주는 q_auto와 f_auto를 넣어줬는데 이것만 해도 용량이 엄청 줄어들었다. 자동으로 WebP포맷으로 변환됐고, 시각적으로 보이는 화질 또한 그대로였다.
이대로 코드를 작성하려다 Next.js와 Cloudinary를 같이 사용하는 글을 발견했다. 이미지를 불러올 때 Next.js에서 외부 도메인을 허용해 사용하는 것은 권장되지 않는 방법이라는 것이다. Cloudinary CDN이 URL 요청 전에 Next.js에서 처리하고 전달하기 때문에 기능을 완벽하게 수행할 수 없기 때문이다.
next-cloudinary 패키지
권장 방법대로 패키지를 설치했다. 용량도 가벼운 편이고 공식 문서도 꽤 잘 정리되어 있다.
라이브러리를 사용하면 아래와 같은 장점들이 있다.
- Cloudinary의 다양한 기능(예: 자동 최적화, 포맷 변환 등)을 쉽게 활용할 수 있다.
- 이미지 URL을 직접 조작하지 않고 라이브러리에서 제공하는 메소드를 통해 간편하게 이미지를 다룰 수 있다.
- Next.js와의 통합이 잘 되어 있어 SSR(서버 사이드 렌더링)과 SSG(정적 사이트 생성)에 최적화된 이미지 처리가 가능하다.
"use client";
import { CldImage } from "next-cloudinary";
const HomeAnimation = () => {
return (
<CldImage
width={300}
height={60}
src="animation-wallet_fnoph2"
alt="home animation"
/>
);
};
export default HomeAnimation;
사용법은 간단하다. next-cloudinary에서 CldImage 컴포넌트를 불러오고 필수 속성인 width, height, src, alt를 작성한다.
next-cloudinary의 컴포넌트는 Next.js에서 제공하는 Image 컴포넌트를 래핑하여 기능을 확장했기 때문에 <Image> 쓰듯이 사용하면 된다.
다만, Next.js에서 앱 라우터를 사용중이라면 상단에 반드시 'use client'를 작성해야 한다. 나는 홈'/' 경로가 서버 컴포넌트로 작동하도록 두고 싶었기 때문에 따로 클라이언트 컴포넌트를 만들고, 페이지에 넣어주었다.
또, 반드시 해줘야 할 작업은 .env파일에 NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME을 작성하는 것이다. 해당 환경 변수를 패키지에서 사용하고 있기 때문에 써주지 않는다면 위와 같이 오류가 발생한다.
최종적으로 용량은 7.7MB -> 730KB, 속도는 331ms -> 171ms로 확연하게 줄어들었다.
다만 무료 요금제에서는 한계가 있으니 모든 이미지가 아닌 위와 같이 Image컴포넌트를 사용할 수 없는데 최적화가 필요한 이미지에 대해서만 적용하는 것이 좋겠다.
사파리에서는? 모바일에서는?
아쉽게도 사파리에서는 애니메이션이 움직이지 않았다. 클라우디너리에서 사파리에는 gif를 자동으로 최적화된 jxl 포맷으로 변환되어 이미지를 내려줬는데, 분명 사파리에서 애니메이션이 가능한 포맷임에도 불구하고 애니메이션이 전혀 동작하지 않았다.
결국, 포맷 자동 변환 기능이 아닌 직접 포맷을 webp나 avif로 설정하여 강제하도록 했다.
<CldImage
width={300}
height={60}
format="avif"
src="animation-wallet_fnoph2"
alt="home animation"
style={{ width: "auto" }}
/>
webp는 애니메이션이 깨져서 보였고, avif는 저작권과 관련된 pixcap과 함께 백그라운드가 생겼다.
해당 이슈는 웹 사파리와 모바일 사파리, 모바일 크롬에서 동일하게 발생하는 문제였다.
여러 시도 끝에 모든 브라우저에서 결국 잘 동작하는건 가장 효율이 좋지 않은 GIF라는 것을 깨달았다...
결론
일단 포맷을 gif로 강제하여 용량만 처음보다 약 2MB 줄였다.
디자이너 분이 로딩 애니메이션은 gif 추출이 안되어서 JSON파일로 전달해줬는데, 해당 홈 애니메이션도 JSON파일로 변환을 부탁드려야겠다...
며칠 동안 이 이미지 하나 때문에 여러 가지 변환을 시도하고 모바일도 계속 체크했지만 유의미한 결과는 얻지 못한 것 같다. 그래도 주변에 많이 물어보고 다시 시도해봐야겠다. !
Reference
'개발 > 프론트엔드' 카테고리의 다른 글
[JavaScript] 코어 자바스크립트 week2 -실행 컨텍스트 (0) | 2024.10.28 |
---|---|
[JavaScript] 코어 자바스크립트 week1 - 데이터 타입 (3) | 2024.10.22 |
[CI/CD]vercel에 무료로 organization레포지토리 배포하기 (0) | 2024.09.26 |
[NextJS] NextJS와 TailwindCSS로 폰트 설정하기 (0) | 2024.09.18 |
[Typescript] 타입스크립트는 왜 쓰는걸까? (4) | 2024.09.05 |