정의
블로그 본문에 넣은 세로로 긴 스크린샷이 위아래가 잘려 보인 적이 있다면, 혹은 이미지가 로드되는 순간 읽던 글이 아래로 밀린 적이 있다면 이 개념이 필요하다.
고유 치수(intrinsic dimensions) 는 이미지 파일 자체가 가진 원본 픽셀 크기다. 마크다운이나 CMS 본문에서 만들어진 <img>에는 대개 width/height가 없어 브라우저가 로드 전에 자리를 예약하지 못한다. 빌드 시점에 로컬 이미지 파일을 열어 실제 치수를 읽고 속성으로 주입하면 자리 예약과 원본 비율 렌더가 동시에 해결된다.
왜 필요한가
치수가 없는 이미지는 로드 전 높이가 0이다. 늦게 도착하면 아래 내용이 밀리고, 이는 누적 레이아웃 이동(CLS) 으로 측정돼 사용자 경험과 검색 순위 양쪽에 불리하다.
흔한 대응은 컨테이너에 aspect-ratio: 16 / 9를 주는 것이다. 흔들림은 막지만 대가가 있다 — 세로 이미지, 긴 스크린샷, 정사각 도표가 잘려 나간다. 비율을 추측하는 방식의 한계다. 근본 해법은 추측을 그만두고 원본에서 알아내는 것이고, 로컬 자산이라면 빌드 시점에 파일을 읽으면 되므로 런타임 비용도 네트워크 요청도 0이다.
동작 원리
- 마크다운이 HTML 트리(hast)로 변환된다.
- 플러그인이 트리를 순회하며
img노드를 찾는다. src가 로컬 자산 경로(/images/...등)이면public/아래 실제 파일 경로로 해석한다.- 이미지 헤더에서 폭·높이를 읽는다(전체 디코딩 불필요).
width/height속성을 노드에 주입한다.- 브라우저는 두 속성으로
aspect-ratio를 자동 계산해 로드 전에 정확한 자리를 예약한다. - CSS는 치수가 있는 이미지만
height: auto로 두어 원본 비율대로 그리고, 없는 이미지는 기존 폴백을 유지한다.
| 상태 | 자리 예약 | 비율 | 결과 |
|---|---|---|---|
| 치수 없음 | 안 됨 | 미정 | 로드 시 레이아웃 이동 |
고정 aspect-ratio 폴백 | 됨 | 강제 16:9 | 흔들림 없음, 세로 이미지 잘림 |
| 고유 치수 주입 | 됨 | 원본 | 흔들림도 잘림도 없음 |
실무 적용
// rehype 플러그인 (빌드 타임)
import { visit } from "unist-util-visit";
import imageSize from "image-size";
import { readFileSync } from "node:fs";
import path from "node:path";
export function rehypeImageSize({ publicDir }: { publicDir: string }) {
return (tree: Root) => {
visit(tree, "element", (node) => {
if (node.tagName !== "img") return;
const src = String(node.properties?.src ?? "");
if (!src.startsWith("/")) return; // 외부 URL은 건드리지 않는다
if (node.properties?.width) return; // 이미 지정됐으면 존중
try {
const file = readFileSync(path.join(publicDir, src));
const { width, height } = imageSize(file);
if (width && height) Object.assign(node.properties!, { width, height });
} catch {
// 파일이 없으면 폴백에 맡긴다 — 빌드를 깨뜨리지 않는다
}
});
};
}
/* 치수가 있는 이미지만 원본 비율, 나머지는 기존 크롭 폴백 유지 */
.prose img[width][height] {
height: auto;
max-width: 100%;
}
.prose img:not([width]) {
aspect-ratio: 16 / 9;
object-fit: cover;
}
단위 테스트는 "로컬 이미지에는 치수가 붙고 외부 URL은 그대로다"를 고정한다.
트레이드오프
- 빌드 타임 주입: 런타임 비용 0, 사용자 기기 부담 0. 대신 빌드 시간이 이미지 수에 비례해 늘고, 로컬 자산에만 적용된다.
- 런타임 측정(
onLoad에서 비율 반영): 외부 URL도 되지만 이미 늦었다 — 레이아웃 이동은 그 시점에 이미 발생했다. - 고정 비율 폴백: 구현이 가장 싸지만 콘텐츠를 훼손한다. 치수를 못 얻는 경우의 안전망으로만 남긴다.
- 이미지 CDN 파라미터(
?w=1200&h=800): 치수를 URL에서 알 수 있어 편하지만 벤더에 묶인다.
사용하면 안 되는 경우
- 본문 이미지가 전부 외부 URL인 경우 — 빌드 시 파일을 못 읽으므로 다른 전략(CDN 메타데이터, 업로드 시점 기록)이 필요하다.
- 이미지가 의도적으로 고정 비율 썸네일 그리드에 들어가는 경우 — 여기서는 크롭이 버그가 아니라 디자인이다.
흔한 실수
- 폴백을 지운다. 치수를 못 얻는 이미지는 언제나 남는다. 새 경로가 실패해도 예전 동작으로 안전하게 떨어져야 한다.
width/height를 CSS 픽셀로 오해한다. 이 속성은 비율 계산용 원본 치수이며, 실제 표시 크기는 CSS가 정한다.height: auto를 빠뜨린다.width: 100%만 주고 높이를 두면 이미지가 눌린다.- 외부 URL까지 빌드에서 받아오려 한다. 빌드가 네트워크에 의존하고 느려지며 CI에서 간헐 실패한다.
- 모든 이미지에
loading="lazy"를 건다. 첫 화면 이미지는 오히려 늦어진다 — 히어로만fetchpriority="high".
관련 개념
- cls-skeleton-layout-reservation — 자리 예약으로 레이아웃 이동 막기
- lqip-blur-placeholder-ssr — 로드 전 자리를 저해상 미리보기로 채우기
- carousel-viewport-image-deferral — 뷰포트 밖 이미지 로딩 지연
- server-image-proxy-transcoding-cache — 서버 측 이미지 변환·캐시