WebP 이미지 사용하기

구글에서 개발한 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를 도입하고자 하는 분들에게 도움이 되었으면 좋겠습니다.

--

--

--

Frontend Developer, mainly using React.js

Love podcasts or audiobooks? Learn on the go with our new app.

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store
HyeonSeok Yang

HyeonSeok Yang

Frontend Developer, mainly using React.js

More from Medium

Save Yourself Time On Common Typos

What causes resolutions to fail?

Taking risks resolutions to fail

Healing your maternal lineage