HTML img 태그 제대로 쓰기: srcset, sizes, loading까지
이미지 한 장 넣는 데 왜 이렇게 속성이 많을까요? <img src="..."> 한 줄이면 화면에 그림은 뜨지만, 실제 운영 중인 사이트의 <img> 태그를 열어보면 srcset, sizes, loading, width, height, fetchpriority까지 줄줄이 따라붙어 있는 모습을 자주 보게 됩니다.
이 속성들이 거추장스러워 보일 수 있지만 각각 다른 문제를 풀고 있는데요. 폴백 이미지를 정하고, 접근성을 챙기고, 레이아웃 시프트를 막고, 화면에 맞는 해상도를 고르고, 화면 밖 이미지는 늦게 받아오게 미루는 일을 한 태그 안에서 해냅니다. 이 글에서는 평범해 보이는 <img> 태그의 속성을 하나씩 풀어보면서, 언제 어떤 속성을 챙겨야 하는지 정리해 보겠습니다.
src와 alt부터
가장 기본이 되는 두 속성입니다. src는 이미지의 URL이고 alt는 이미지를 설명하는 텍스트인데요. 너무 당연해서 그냥 지나치기 쉽지만 한 가지 짚어둘 게 있습니다.
alt는 이미지가 안 보일 때 보여주는 “대체 텍스트”라기보다 이미지의 역할을 글로 옮긴 것에 가깝습니다. 스크린 리더 사용자에게는 이 텍스트가 곧 이미지 자체고, 검색 엔진도 이 값을 이미지의 의미로 받아들이거든요. 그래서 alt="이미지", alt="사진" 같이 형식적으로 채우는 건 거의 도움이 안 됩니다.
<!-- ❌ 의미 없는 alt -->
<img src="chart.png" alt="이미지" />
<!-- ✅ 이미지가 전달하는 정보를 글로 -->
<img
src="chart.png"
alt="2024년 분기별 매출 추이, 4분기에 전년 대비 35% 증가"
/>
반대로 순수하게 장식용인 이미지라면 alt=""로 비워두는 게 맞습니다. 스크린 리더가 의미 없는 정보를 읽고 지나가지 않도록 명시적으로 “읽을 게 없다”고 알려주는 방식이죠. alt 속성을 아예 빼버리면 스크린 리더가 파일명을 읽어버리는 경우가 있어서 빈 값이라도 적어두는 편이 안전합니다.
width와 height — 레이아웃 시프트 막기
width와 height 속성은 단순히 이미지 크기를 지정하는 게 아닙니다. 사실 요즘은 CSS로 너비를 조절하는 게 일반적이라 “어차피 CSS가 덮어쓸 텐데 왜 적지?”라는 생각이 들 수 있는데요. 이 두 속성의 진짜 역할은 이미지의 종횡비를 미리 알려주는 것입니다.
<img src="hero.jpg" alt="히어로 배너" width="800" height="450" />
브라우저가 이미지를 다 받기 전에 이 속성을 보면 “아, 800:450 비율이구나”를 알 수 있습니다. 그러면 이미지가 들어갈 자리를 미리 비워둘 수 있고, 이미지가 늦게 도착해도 페이지가 출렁이지 않습니다. 이걸 막지 못하면 Core Web Vitals의 CLS(Cumulative Layout Shift) 점수가 깎이는 건 물론이고, 사용자가 읽던 텍스트가 갑자기 아래로 밀려나는 짜증나는 경험이 됩니다.
CSS로 width: 100%를 주더라도 브라우저는 aspect-ratio: 800 / 450을 자동으로 계산해서 적용합니다. 그러니까 실제 표시 크기와 다르더라도 원본의 가로/세로 픽셀 값을 그대로 적어주면 됩니다.
img {
width: 100%;
height: auto;
}
height: auto를 함께 쓰면 종횡비를 유지하면서 너비에 맞춰 늘어납니다. 이 두 줄을 CSS에 한 번만 정의해 두고, HTML의 width/height에는 원본 크기를 적는 게 표준적인 패턴입니다.
srcset — 해상도에 맞는 이미지 고르기
같은 이미지를 4K 모니터와 모바일 화면에서 똑같은 크기로 내려보낼 이유는 없습니다. 모바일에는 작은 이미지를, 고해상도 화면에는 큰 이미지를 보내고 싶은데, 이걸 자바스크립트 없이 HTML 차원에서 해결하는 게 srcset 속성입니다.
srcset에는 두 가지 문법이 있는데요. 픽셀 밀도 서술자(1x, 2x)와 너비 서술자(400w, 800w)입니다. 둘은 비슷해 보여도 동작 방식이 꽤 다릅니다.
<!-- 픽셀 밀도 서술자 -->
<img src="logo.png" srcset="logo.png 1x, logo@2x.png 2x" alt="회사 로고" />
픽셀 밀도 방식은 “이건 일반 디스플레이용, 저건 고밀도 디스플레이용”을 직접 지정합니다. 로고처럼 표시 크기가 고정된 이미지에 잘 어울리는데요. Retina 디스플레이에서는 자동으로 2x 이미지를 받습니다.
<!-- 너비 서술자 -->
<img
src="hero-800.jpg"
srcset="hero-400.jpg 400w, hero-800.jpg 800w, hero-1200.jpg 1200w"
sizes="(max-width: 640px) 100vw, 400px"
alt="히어로 이미지"
/>
너비 서술자 방식은 각 파일의 실제 픽셀 너비를 알려주는 식입니다. 400w는 “이 파일이 400픽셀 너비”라는 뜻이고요. 브라우저는 이 정보와 sizes 속성을 조합해서 화면 크기, 픽셀 밀도, 네트워크 상태까지 종합해 최적의 후보를 골라냅니다.
여기서 헷갈리기 쉬운 점이 하나 있는데요. 너비 서술자를 쓸 때는 sizes 속성이 거의 필수입니다. sizes 없이 400w, 800w만 적어두면 브라우저가 이미지의 표시 너비를 추측할 수 없어서 가장 큰 이미지를 선택해버리는 경우가 많습니다. 두 문법은 섞어 쓰지 말고 상황에 따라 하나만 고르면 됩니다.
sizes — 브라우저에 레이아웃 힌트 주기
sizes 속성이야말로 처음 보면 가장 헷갈리는 속성입니다. CSS 셀렉터처럼 생겼지만 셀렉터가 아니고, 길이 단위가 들어가 있지만 CSS도 아닙니다. 정체는 미디어 조건 + 이미지가 차지할 너비의 쌍을 나열한 문법인데요.
sizes="(max-width: 640px) 100vw, 400px"
이건 “뷰포트 너비가 640px 이하면 이미지가 화면 전체 너비(100vw)를 차지하고, 그 외에는 400px를 차지한다”는 뜻입니다. 브라우저는 이 정보를 받아서 “그렇다면 이 화면에서는 600px짜리 이미지가 필요하겠군” 같은 계산을 하고, srcset 후보 중에서 가장 가까운 걸 고릅니다.
왜 이런 정보를 굳이 개발자가 알려줘야 할까요? 브라우저가 이미지를 결정해야 하는 시점은 HTML을 파싱하는 직후입니다. 그때는 아직 CSS가 적용되지 않았거나 레이아웃 계산이 끝나지 않은 상태라서, 이미지가 실제로 화면에서 얼마나 큰 영역을 차지할지 모릅니다. CSS로 width: 50%를 줬더라도 브라우저는 그 정보를 늦게 받죠. 그래서 미리 힌트를 받아두는 겁니다.
여러 조건을 나열할 때는 <picture>의 <source>처럼 위에서부터 순서대로 평가되니까 좁은 화면 조건을 먼저 두는 게 일반적입니다.
sizes=" (max-width: 480px) 100vw, (max-width: 1024px) 50vw, 400px "
모바일에서는 전체 너비, 태블릿에서는 절반 너비, 데스크톱에서는 400px 고정 — 이런 식으로 화면별 레이아웃을 그대로 옮겨 적으면 됩니다. 마지막 값은 조건 없이 적어서 폴백으로 둡니다.
loading과 decoding으로 미루기
화면 밖에 있는 이미지를 페이지 로드 시점에 한꺼번에 받아오는 건 낭비입니다. loading="lazy"를 붙이면 브라우저가 알아서 뷰포트에 가까워졌을 때 이미지를 받아오는데요.
<img src="far-below.jpg" alt="스크롤해야 보이는 이미지" loading="lazy" />
이걸 모든 이미지에 일괄 적용하면 안 됩니다. 페이지 첫 화면에 보이는 이미지(특히 LCP(Largest Contentful Paint) 후보)에 loading="lazy"를 붙이면 오히려 로딩이 늦어집니다. 화면 위쪽 이미지는 기본값(eager)으로 두고, 스크롤해야 보이는 이미지에만 lazy를 적용하는 게 맞습니다.
비슷한 맥락으로 decoding 속성도 있는데요. 이건 이미지를 디코딩하는 방식을 알려주는 힌트입니다.
<img src="hero.jpg" alt="히어로" decoding="sync" />
<img src="below-fold.jpg" alt="아래쪽" decoding="async" />
sync는 이미지 디코딩이 끝날 때까지 페이지 표시를 기다리고, async는 디코딩이 끝나는 대로 표시합니다. 대부분의 경우 브라우저 기본값(auto)에 맡겨도 충분하고, 명시적으로 지정해야 한다면 첫 화면 이미지는 sync, 나머지는 async를 쓰는 식으로 나눕니다.
fetchpriority로 우선순위 끌어올리기
loading="lazy"가 우선순위를 낮추는 거라면 fetchpriority="high"는 반대로 끌어올리는 속성입니다. 브라우저는 이미지의 중요도를 추측해서 다운로드 순서를 정하는데요. 히어로 이미지나 LCP 후보처럼 사용자에게 중요한 이미지는 명시적으로 우선순위를 알려줄 수 있습니다.
<img
src="hero.jpg"
alt="메인 배너"
width="1200"
height="600"
fetchpriority="high"
/>
반대로 별로 중요하지 않은 이미지(광고 배너, 사이드바 썸네일 등)에는 fetchpriority="low"를 줘서 다른 리소스에 양보하게 만들 수도 있습니다. 다만 모든 이미지에 high를 붙이면 의미가 없어지니까, 페이지당 한두 개의 핵심 이미지에만 신중하게 적용해야 합니다.
통합 예제
지금까지 살펴본 속성을 모두 합치면 이런 모양이 됩니다.
<img
src="https://img.example.com/hero-800.jpg"
srcset="
https://img.example.com/hero-400.jpg 400w,
https://img.example.com/hero-800.jpg 800w,
https://img.example.com/hero-1200.jpg 1200w
"
sizes="(max-width: 640px) 100vw, 400px"
alt="플랫폼 메인 화면 스크린샷"
width="800"
height="450"
loading="lazy"
/>
이 한 줄짜리(처럼 보이는) 태그가 하는 일을 풀어보면 이렇습니다. src에 적은 800px 이미지를 폴백으로 두고, srcset으로 400/800/1200 세 가지 크기를 후보로 제시합니다. sizes로 “모바일에서는 화면 전체, 그 외에는 400px 차지”라는 레이아웃 힌트를 주고, 브라우저는 이걸 종합해서 화면 크기와 픽셀 밀도에 맞는 이미지를 고릅니다. alt로 접근성을 챙기고, width/height로 종횡비를 미리 알려 CLS를 막고, loading="lazy"로 뷰포트에서 멀면 받아오지 않게 미룹니다.
CDN을 사용한다면 srcset의 후보 URL을 동일한 원본에서 너비만 다르게 생성하는 패턴이 흔한데요. Cloudflare Images Transformations나 Cloudinary처럼 URL 파라미터로 크기를 지정할 수 있는 서비스를 쓰면 원본 한 장으로 모든 후보를 만들 수 있습니다. Astro 같은 프레임워크를 쓰고 있다면 Astro의 이미지 최적화에서 <Image> 컴포넌트가 이런 속성들을 자동으로 채워주기도 합니다.
흔히 저지르는 실수
마지막으로 실무에서 자주 보는 실수 몇 가지를 짚고 가겠습니다.
우선 width와 height를 빼먹는 경우가 가장 흔한데요. CSS로 크기를 조절한다는 이유로 HTML에서 생략하면 레이아웃 시프트의 주범이 됩니다. CSS로 어떻게 크기를 바꾸든 HTML에는 원본 픽셀 값을 적어두세요.
또한 loading="lazy"를 첫 화면 이미지에 붙이는 경우도 자주 보입니다. 모든 이미지에 일괄 적용하기 쉬운 속성이라 그런데, 첫 화면의 LCP 이미지에 lazy를 붙이면 LCP가 눈에 띄게 느려집니다. 첫 화면 이미지는 명시적으로 빼두거나 빌드 도구에서 자동으로 처리하게 만드는 게 좋습니다.
마지막으로 srcset을 픽셀 밀도와 너비 서술자로 섞어 쓰는 경우인데요. srcset="hero.jpg 1x, hero@2x.jpg 800w" 같은 식의 혼용은 유효하지 않은 마크업입니다. 하나의 srcset 안에서는 한 가지 방식만 써야 합니다.
마치며
<img> 태그는 단순해 보이지만 속성 하나하나에 명확한 역할이 있습니다. src와 alt로 의미를 담고, width/height로 레이아웃을 안정시키고, srcset/sizes로 화면에 맞는 이미지를 고르고, loading/fetchpriority로 우선순위를 조절합니다. 각 속성이 서로 다른 자리를 맡고 있어서, 빼먹으면 그 자리만큼의 문제가 그대로 드러나는 셈이죠.
화면 크기에 따라 같은 이미지의 해상도만 바꾸는 거라면 <img> 태그의 srcset과 sizes만으로 충분하지만, 아예 다른 구도의 이미지를 보여주거나 AVIF/WebP 같은 포맷에 따라 분기해야 한다면 HTML picture 요소로 넘어가야 합니다. 두 도구를 상황에 맞게 골라 쓰면 됩니다.
각 속성의 정확한 스펙이 궁금하다면 MDN의 img 요소 문서에서 확인하실 수 있습니다.
This work is licensed under
CC BY 4.0