순수 CSS와 LQIP로 만드는 번개처럼 빠른 이미지 미리보기
제 웹사이트는 꽤 빠릅니다.
어떤 기기에서, 어디서 접속하든 모든 페이지가 경쾌하게 로딩되기를 바랍니다. 놀랄 일도 아닙니다. 결국 일반 HTML과 CSS만 사용하고 있으니까요. JavaScript는 가능한 한 쓰지 않습니다.
그런데 아직도 정말 거슬리는 한 가지가 남아 있었습니다. 이미지가 로드된 뒤 레이아웃이 다시 밀리는 현상이었습니다.
문제는 텍스트를 표시할 준비가 되었을 때 아직 이미지 크기를 알 수 없다는 점입니다. 그 결과, 위쪽에 있는 이미지가 로드되는 순간 텍스트가 화면 아래로 밀려 내려갑니다.
또 이미지가 로딩되는 동안에는 미리보기도 없이 빈 공간만 남습니다. 느린 연결 환경에서는 이렇게 보입니다.

이미지의 너비와 높이를 하드코딩하면 해결할 수는 있지만, 번거롭고 오류가 생기기 쉽습니다. 게다가 미리보기도 제공되지 않습니다. 그래서 다른 사람들은 어떻게 하고 있을까 궁금해졌습니다. 🤔
작은 이미지 썸네일
어렴풋이 기억나기로는 Facebook이 모바일 앱에서 아주 작은 미리보기 썸네일을 사용한다는 것이었습니다. JPEG 헤더에서 양자화 테이블을 추출해 미리보기를 렌더링하는 방식입니다. 이 정보는 클라이언트에 저장되므로 매번 다운로드할 필요가 없습니다. 안타깝게도 이 방식은 이미지 인코더를 완전히 제어할 수 있어야 합니다. 앱에서는 통하지만 웹사이트에서는 활용하기 어렵습니다.
검색은 계속되었습니다.
그러다 동료인 Tobias Baldauf가 LQIP(Low-Quality Image Placeholders, 저품질 이미지 플레이스홀더)를 소개해 주었습니다.
아이디어는 이렇습니다.
- 인라인으로 삽입된 저품질 이미지 썸네일을 포함해 페이지를 로드합니다.
- 페이지가 완전히 로드된 뒤(예:
onload이벤트가 발생한 뒤) 고품질 이미지를 지연 로드합니다.
안타깝게도 이 기법은 JavaScript를 필요로 합니다. 그럼에도 아이디어 자체가 마음에 들어 다양한 이미지 크기와 포맷으로 실험을 시작했습니다. 목표는 표준 이미지 포맷을 사용해 가장 작은 썸네일을 만드는 것이었습니다.
벤치마크
다음은 서로 다른 파일 포맷으로 인코딩한 너비 15픽셀짜리 썸네일입니다.

썸네일을 만들기 위해 여러 도구를 사용했습니다. JPEG와 PNG 인코딩에는 svgexport를 사용했습니다.
svgexport img.svg img.png "svg{background:white;}" 15: 1%webp의 경우 cwebp를 사용했습니다.
cwebp img.png -o img.webpgif는 온라인 도구로 변환한 뒤 gifsicle로 최적화했습니다.
gifsicle -O3 < img.gif > img_mini.gif비교
WebP가 용량이 가장 작았지만 모든 브라우저에서 지원되지는 않습니다.
Gif가 그 다음으로 작았지만, 이미지를 리사이징하고 블러 필터를 적용한 결과가 마음에 들지 않았습니다.
결국 크기와 품질 사이에서 훌륭한 균형을 제공하는 PNG로 정했습니다. oxipng로 이미지를 한 번 더 최적화했는데, 이 도구는 zopfli 압축을 지원합니다. 그 결과 썸네일 크기는 300~400바이트 정도가 되었습니다.
썸네일 생성 과정을 블로그 빌드 툴체인에 통합했습니다. 이미지를 생성하는 실제 코드는 그다지 재미있지 않습니다. 정말 보고 싶으시다면 Github에 있습니다.
JavaScript 없이 구현하기
이미지 미리보기를 위한 기본 HTML 구조는 다음과 같습니다.
<figure>
<div class="loader">
<object data="image.svg" type="image/svg+xml"></object>
<img class="frozen" src="data:image/png;base64,..." />
</div>
</figure>핵심은 원본 이미지와 미리보기 이미지를 모두 loader div로 감싸고, 여기에 width: auto CSS 속성을 주는 것입니다.
.loader {
position: relative;
overflow: hidden;
width: auto;
}img 요소 대신 object 태그로 SVG를 감쌉니다. 이렇게 하면 SVG를 불러오지 못했을 때 플레이스홀더를 보여줄 수 있다는 장점이 있습니다. object는 loader div의 좌측 상단에 배치합니다.
.loader object {
position: absolute;
}
.loader img,
.loader object {
display: block;
top: 0;
left: 0;
width: 100%;
}플레이스홀더 핵 코드와 참고 링크는 다음과 같습니다.
/* https://stackoverflow.com/a/29111371/270334 */
/* https://stackoverflow.com/a/32928240/270334 */
object {
position: relative;
float: left;
display: block;
&:after {
position: absolute;
top: 0;
left: 0;
display: block;
width: 1000px;
height: 1000px;
content: "";
background: #efefef;
}
}마지막 부분은 썸네일을 다루는 방법입니다. 대부분의 다른 사이트처럼 저도 블러 필터를 적용하기로 했습니다. 일종의 이미지가 얼어붙은(frozen) 것처럼 보여서 CSS 선택자 이름도 그렇게 지었습니다. 또 선명한 테두리를 만들기 위해 스케일 변환도 적용했습니다.
.frozen {
-webkit-filter: blur(8px);
-moz-filter: blur(8px);
-o-filter: blur(8px);
-ms-filter: blur(8px);
filter: blur(8px);
transform: scale(1.04);
animation: 0.2s ease-in 0.4s 1 forwards fade;
width: 100%;
}
@keyframes fade {
0% {
opacity: 1;
}
100% {
opacity: 0;
}
}JavaScript 대신 CSS 애니메이션을 사용합니다.
애니메이션 지속 시간은 해당 페이지 모든 방문자의 로드 시간 중 95퍼센타일을 기준으로 정했습니다. 근사치이긴 하지만 대부분의 독자에게는 잘 동작할 것입니다.
결과
- JavaScript가 필요 없습니다
- 모든 최신 브라우저에서 동작합니다
- 메인 이미지를 불러오지 못할 경우를 위한 폴백을 지원합니다
- 오버헤드가 매우 작습니다
참고 자료
글을 무작위로 읽기