WebP 이미지 사용하기

HyeonSeok Yang
7 min readJun 20, 2020

구글에서 개발한 WebP는 이미지를 손실 압축하는 대신 용량을 획기적으로 줄여주는 이미지 포맷입니다. JPEG 포맷보다 압축률이 30% 정도 높다고 하지만, 제가 생각하는 최대의 장점은 PNG를 사용해야만 했던, 투명도가 있는 이미지도 압축할 수 있다는 점입니다.

다만 이런 WebP 포맷 사용의 단점은, IE와 Safari 브라우저에서 아직 지원하지 않고 있어, 일반 사용자를 대상으로 하는 웹페이지의 경우 무조건 Fallback을 지원해야 한다는 문제가 있습니다.

1.2MB (png) -> 193KB (webp, quality 90)

그러니 우선, 기존 이미지들을 WebP 이미지로 변환하는 방법에 대해 알아보고, Fallback을 지원하는 방법에 대해 소개하도록 하겠습니다.

WebP 변환하기

가장 손쉬운 방법은 convertio.co 를 사용하는 것입니다. 한 번에 두 개의 이미지씩, 하루에 열 개까지 변환이 가능합니다.

무릇 개발자란 손으로 하는 반복행동을 귀찮아해야하고, 자동화를 하는 것이 도리와도 같은 것이라. 열 개쯤 변환해서 테스트해본 후에, 자동화를 시작했습니다. 다행히 imagemin 라이브러리와 imagemin-webp 라이브러리가 있어 어렵지 않았습니다.

const fs = require("fs").promises;
const path = require("path");
const imagemin = require("imagemin");
const imageminWebp = require("imagemin-webp");
(async () => {
const dir = await fs.opendir("./public");
for await (const entry of dir) {
if (entry.isFile()) continue;
const target = path.join(dir.path, entry.name, "*.{jpg,png}");
await imagemin([target], {
destination: path.join(dir.path, entry.name, "webp");
plugins: [imageminWebp({ quality: 90 })],
});
}
})();

public 폴더 내의 서브디렉토리를 돌면서 그 안의 jpg/png 파일들을 압축해 webp 폴더에 출력합니다. 이 과정은 생각보다 빨라, 7.2MB 분량의 50개 파일을 압축하는 데에 1.x초 정도 걸렸습니다.

하지만 이 작업은 이미지를 추가할 때마다 돌려줘야 합니다. 이정도 규모에서 1초 정도라면, 더욱 이미지가 많아진다고 해도 매 빌드마다 실행해도 문제가 크진 않을거라고 생각합니다. 웹팩 플러그인으로 만들어봅니다.

new WebpConverterPlugin({ target: './public', destination: './webp' })

imagemin의 한계로, 압축할 이미지가 어느 폴더에, 어느 경로에 존재하건, 아웃풋 파일의 위치는 하나로 고정됩니다. 제 경우에는 이미지들을 분리해놓고 사용하기 때문에, 각각의 폴더별로 플러그인을 여러개 생성하여 사용했습니다.

WebP Fallback

앞서 말했듯, WebP 이미지를 단독으로 사용하기엔 무리가 있습니다. 사파리에서 볼 수 없기 때문입니다.

웹에서 이미지를 사용하는 방법이 여럿 있는 만큼, 대응해야 할 방식도 여러가지가 존재합니다.

<img src=”” />

picture 태그를 사용해야 할 때입니다.

<picture>
<source srcSet="/main/top-image.webp" type="image/webp" />
<img src="/main/top-image.png" />
</picture>

주의할 점은, picture 태그 안에는 필수적으로 img 태그가 들어가야 합니다. 해당 이미지를 fallback으로 사용하기 때문입니다.

CSS background-image

CSS의 background-image는 여러 개의 이미지를 동시에 얹는 것을 지원하지만, 이 방식을 사용하는것은 WebP 이미지와 png 이미지를 동시에 불러오는 결과를 낳습니다. 오히려 용량을 증가시키는 꼴입니다.

따라서 약간의 js 코드가 필요합니다.

export function detectWebpSupport() {
const image = new Image();
// 1px x 1px WebP 이미지
const webpdata = "data:image/webp;base64,UklGRiQAAABXRUJQVlA4IBgAAAAwAQCdASoBAAEAAwA0JaQAA3AA/vuUAAA=";
const callback = (event) => {
// if the event is from 'onload', check the see if the image's width is 1 pixel (which indicates support). otherwise, it fails
const result = event?.type === "load" && image.width === 1;
if (result) {
document.body.classList.add("webp");
}
else {
document.body.classList.add("no-webp");
}
};
image.onerror = callback;
image.onload = callback;
image.src = webpdata;
}

WebP를 지원하는 브라우저라면, <body>에 webp라는 클래스를, 지원하지 않는다면 no-webp라는 클래스를 추가합니다. 이제 CSS는 아래와 같이 작성할 수 있습니다.

.webp .bg {
background-image: url("/main/top-image.webp");
}
.no-webp .bg {
background-image: url("/main/top-image.png");
}

inline-style background-image

React 개발 중에는 JSX 코드를 사용하면서, inline style을 사용해 background-image를 설정할 일이 종종 생깁니다.

이 때는 아래와 같은 코드를 사용했습니다.

const resolveWebp = (webpSupported, img, fallbackExt) => {
const ext = img.split(".").pop();
// oh this is problem
if (!webpSupported && ext === "webp") {
return img.replace(".webp", `.${fallbackExt}`);
}
return img;
};

첫 번째 인자는 사용 방식에 따라 달라질 수 있습니다.
위에서 설명한 detectWebpSupport 함수를 사용하셨다면, body의 class를 확인할 수도 있고, 저 함수를 변형한 결과를 Context API를 통해 내려받을 수도 있습니다.

<div
className="image"
style={{ backgroundImage: `url(${resolveWebp(webp, "/main/top-image.webp", "png")})` }}
/>

마무리

처음엔 Chrome Lighthouse 점수를 높여보기 위한 의도로 WebP를 도입해보았습니다. 하지만 막상 도입하고 나니 이미지가 많던 페이지가 상당히 가벼워진 것을 느낄 수 있었습니다. WebP를 사용하기 위해서는 조금 고난한 과정을 거쳐야 하지만, 약간의 설정을 거치고 나면 생각보다 편하게 사용할 수 있게 됩니다. 이 글이 WebP를 도입하고자 하는 분들에게 도움이 되었으면 좋겠습니다.

--

--