내 컴퓨터에서는 멀쩡하던 화면이 변환 결과에서는 사진 자리가 비고 디자인이 통째로 빠진 맨 글씨로 나오는 일이 있습니다. 변환이 잘못된 것이 아니라 서버가 정말 그렇게 보이는 페이지를 캡처한 것이고, 원인은 거의 항상 하나로 모입니다.
파일 선택창에서 detail.html을 고르면 전송되는 것은 그 파일 하나입니다. 같은 폴더의 images 폴더도 style.css도 따라가지 않습니다. 브라우저는 고른 파일이 무엇을 참조하는지 들여다보지 않기 때문입니다.
서버는 받은 HTML을 임시 위치에 저장한 뒤 file:// 주소로 엽니다. 상대 경로는 원래 폴더가 아니라 그 임시 위치를 기준으로 해석되므로, src="images/a.jpg"는 임시 폴더 안에 없는 파일을 찾다 실패합니다.
게다가 이 실패는 조용합니다. 브라우저는 없는 이미지를 만나도 오류를 띄우지 않고 그 자리에 작은 깨진 이미지 아이콘만 남긴 채 나머지를 그리므로, 변환은 실패가 아니라 성공으로 끝나고 결과 PNG에는 빠진 상태가 그대로 담깁니다. 코드를 붙여넣는 방식도 조건이 같습니다. 애초에 업로드 창이 받아 주는 확장자가 .html과 .htm뿐이라, 이미지나 CSS 파일은 함께 고르는 것 자체가 되지 않습니다.
기준은 단순합니다. 파일 안에 들어 있거나 인터넷에서 그 자체로 내려받을 수 있으면 반영되고, 그 밖의 모든 참조는 실패합니다.
<!-- 서버에서 찾지 못합니다 -->
<img src="images/photo.jpg">
<img src="./logo.png">
<img src="../assets/bg.jpg">
<img src="/static/icon.svg">
<link rel="stylesheet" href="style.css">
<script src="app.js"></script>
<style>.hero { background-image: url('bg.png'); }</style>
<!-- 그대로 반영됩니다 -->
<img src="https://example.com/photo.jpg">
<img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAA...">
<style>.hero { padding: 40px; background: #111; }</style>
<div style="color: #333">인라인 스타일</div>
<svg viewBox="0 0 24 24"><path d="M4 12h16" stroke="#333"/></svg>슬래시로 시작하는 /static/icon.svg도 안 됩니다. 웹 서버에서는 사이트 최상위를 뜻하지만 file://에서는 파일 시스템의 최상위를 가리키기 때문입니다. 내 PC의 경로를 file:///C:/Users/…로 적는 것도 소용없습니다. 그 경로는 내 컴퓨터에만 있고, 렌더링은 서버에서 일어나기 때문입니다.
빈자리의 모양이 원인을 반쯤 알려 줍니다. 가져오려다 실패한 이미지는 깨진 이미지 아이콘이 남고, 애초에 가져오려 하지도 않은 이미지는 아이콘 없이 완전히 빈 칸으로 남습니다. 앞은 주소가 틀렸거나 상대 서버가 거부한 경우이고, 뒤는 7번에서 다룰 지연 로딩처럼 요청 자체가 일어나지 않은 경우입니다. 너비와 높이를 지정하지 않은 <img>는 깨진 아이콘이 글자만 하게 줄어들어 눈에 잘 띄지 않으니, 결과 PNG를 확대해서 확인하십시오.
가장 흔하면서 가장 쉽게 해결되는 항목입니다. <link>로 불러오던 스타일시트를 열어 내용을 통째로 <style> 블록에 옮기면 됩니다.
<!-- 이전: 함께 올라가지 않는 파일을 가리킵니다 -->
<link rel="stylesheet" href="css/style.css">
<!-- 이후: style.css 의 내용을 그대로 옮겨 넣습니다 -->
<style>
body { margin: 0; font-family: "Noto Sans CJK KR", sans-serif; }
.hero { padding: 40px; background: #111; color: #fff; }
</style>CSS 안의 @import "reset.css";도 같은 이유로 실패하니 함께 옮기고, 스타일시트가 여러 개였다면 <link>의 순서 그대로 이어 붙여야 우선순위가 유지됩니다.
data URI는 이미지 파일의 바이트를 글자로 바꿔 src 안에 직접 써 넣는 방식입니다. 외부 요청이 없으므로 네트워크 상태나 상대 서버의 정책과 무관하게 항상 성공하며, 형식은 data:MIME타입;base64,데이터 한 줄입니다.
MIME 타입은 원본 형식에 맞춰 적습니다. .png는 image/png, .jpg는 image/jpeg, .gif는 image/gif, .webp는 image/webp, .svg는 image/svg+xml입니다. PNG·JPEG·GIF·WebP 같은 사진 계열은 브라우저가 라벨보다 실제 바이트를 먼저 보기 때문에 타입을 서로 바꿔 적어도 대개 그려집니다. 다만 SVG는 예외라서 image/svg+xml이 아니면 표시되지 않고, 반대로 PNG 데이터에 image/svg+xml을 붙여도 실패합니다. 관대한 쪽에 기대지 말고 형식에 맞춰 적는 편이 안전합니다.
# 리눅스(GNU coreutils): -w0 으로 줄바꿈 없이 한 줄로 출력
base64 -w0 logo.png > logo.b64
# macOS 기본 base64 에는 -w 옵션이 없습니다. 아래는 양쪽에서 모두 동작합니다.
base64 < logo.png | tr -d '\n' > logo.b64
# img 태그까지 한 번에 만들어 두기
printf '<img src="data:image/png;base64,%s" alt="">' "$(base64 -w0 logo.png)" > tag.txt
# Windows PowerShell (ReadAllBytes 는 상대 경로를 다르게 해석할 수 있어 Resolve-Path 로 넘깁니다)
[Convert]::ToBase64String([IO.File]::ReadAllBytes((Resolve-Path .\logo.png).Path)) | Set-Clipboard명령줄이 익숙하지 않다면 아래를 convert.html로 저장해 열고 이미지를 고르면 됩니다. FileReader가 완성된 <img> 태그를 만들어 주며, 모든 처리가 브라우저 안에서 끝나 어디에도 업로드되지 않습니다.
<input type="file" id="pick" accept="image/*">
<textarea id="out" rows="8" style="width: 100%"></textarea>
<script>
document.getElementById('pick').addEventListener('change', function (e) {
var file = e.target.files[0];
if (!file) return;
var reader = new FileReader();
reader.onload = function () {
// reader.result 는 이미 data:image/png;base64,... 형태의 완성된 문자열입니다
document.getElementById('out').value =
'<img src="' + reader.result + '" alt="">';
};
reader.readAsDataURL(file);
});
</script>base64는 3바이트를 4글자로 바꾸므로 데이터가 약 4/3배, 즉 33% 정도 커집니다. 200KB짜리 사진은 약 267KB의 글자가 되어 HTML 안에 들어갑니다. 업로드 한도가 파일당 20MB이니, 심는 이미지의 원본 합계가 15MB를 넘기 시작하면 한도에 걸립니다. 코드를 붙여넣어 변환할 때는 한도가 더 낮아 한 번에 보내는 코드 전체가 10MB를 넘으면 변환 대신 서버 오류가 돌아오므로, base64를 크게 쓸 계획이라면 처음부터 .html 파일로 저장해 업로드하십시오.
그래서 심기 전에 최종 표시 크기의 2배 정도로 줄여 두십시오. 캡처 배율이 2배라 그 이상은 파일만 키울 뿐입니다. 로고와 아이콘, 배경 패턴에 특히 잘 맞고, 고화질 사진이 여러 장이면 다음 방법이 낫습니다.
SVG는 base64 대신 <svg> 코드를 그대로 붙여넣는 편이 작고 선명합니다.
이미 인터넷에 공개된 이미지라면 전체 주소를 그대로 적는 것이 가장 간단합니다. 조건은 하나, 로그인이나 권한 없이 누구나 그 주소로 파일을 받을 수 있어야 한다는 것입니다.
확인은 시크릿 창을 새로 열어 주소창에 URL만 붙여넣어 봅니다. 로그인 상태와 캐시가 빠진 조건이라 서버 환경에 가깝고, 이미지 한 장만 뜨면 통과입니다. 클라우드 드라이브의 공유 링크는 대부분 여기서 걸립니다. 이미지 파일이 아니라 미리보기 화면을 그리는 웹페이지 주소이기 때문입니다.
주소가 맞는데도 나오지 않는다면 원인은 세 갈래이고, 구분하는 방법이 각각 다릅니다.
쇼핑몰과 블로그, CDN은 요청에 담긴 Referer나 브라우저 종류를 보고 자기 사이트 밖에서 온 요청을 거부합니다. 시크릿 창에서 URL 직접 열기는 되는데 내 HTML 안에서만 안 나온다면 이 경우이며, 상대 서버의 정책이라 우회할 수 없으니 이미지를 내려받아 base64로 심어야 합니다.
캡처는 네트워크 요청이 잠잠해진 시점을 기다렸다가 이루어지며, 이 대기에는 30초의 상한이 있습니다. 무거운 사진 수십 장을 느린 서버에서 끌어오면 그 안에 끝나지 못해 시간 초과가 나고, 이때는 일부만 빠진 이미지가 아니라 그 파일의 변환 자체가 실패로 끝납니다. 광고 태그나 통계 스크립트, 채팅 위젯처럼 끊임없이 통신하는 요소가 있어도 네트워크가 조용해지지 않아 같은 결과가 됩니다. 어차피 결과물은 움직이지 않는 한 장의 이미지이므로, 오래 걸리다 실패한다면 화면에 보이지 않는 외부 스크립트부터 지우는 것이 가장 빠릅니다.
만료되는 임시 서명이 붙은 주소, 로그인 세션이 있어야 열리는 주소, 한글 파일명이 인코딩되지 않은 주소는 환경이 바뀌면 실패합니다. HTML 안에서 &를 &로 쓰지 않아 쿼리가 중간에서 잘리는 경우도 흔합니다. http://로 시작하는 주소도 여기에 넣어 두는 편이 좋습니다. 요즘은 서버 쪽에서 http 요청을 https로 넘기거나 아예 응답하지 않는 일이 많아, 주소는 살아 있는데 이미지만 오지 않는 상황이 생깁니다. s를 붙여 다시 시도해 보고, 그래도 안 되면 내려받아 base64로 심는 편이 빠릅니다.
깨진 아이콘은 무엇이 실패했는지까지는 알려 주지 않습니다. 아래 조각을 </body> 직전에 넣으면 실패한 자리마다 그 주소가 빨간 상자로 찍혀 나와, 결과 PNG만 보고도 문제의 주소를 특정할 수 있습니다.
<script>
document.querySelectorAll('img').forEach(function (img) {
function fail() {
var box = document.createElement('div');
box.textContent = '로드 실패: ' + img.getAttribute('src');
box.style.cssText = 'padding:8px;margin:4px 0;background:#c62828;color:#fff;'
+ 'font:12px/1.5 monospace;word-break:break-all';
img.replaceWith(box);
}
// 이미 로드가 끝난 경우와 아직 진행 중인 경우를 모두 처리합니다
if (img.complete) {
if (img.naturalWidth === 0) fail();
} else {
img.addEventListener('error', fail);
}
});
</script>이 조각이 잡아내는 것은 요청을 했다가 실패한 이미지뿐입니다. 요청 자체가 일어나지 않은 이미지는 오류가 발생하지 않아 빨간 상자도 생기지 않으니, 확인 후에도 빈 칸이 남아 있다면 다음 항목을 보십시오. 확인이 끝나면 이 조각은 지우고 다시 변환하십시오.
loading="lazy"는 이미지가 화면에 가까워질 때까지 내려받기를 미루는 속성으로, 웹페이지에서는 속도에 도움이 되지만 캡처에서는 정반대로 작동합니다.
렌더링은 높이 800 CSS px의 창에서 시작하고, 캡처는 스크롤 없이 문서 전체를 한 번에 담습니다. 즉 화면을 내리며 이미지를 깨우는 과정이 없습니다. 브라우저는 첫 화면에서 어느 정도 아래까지는 미리 받아 두므로 위쪽 몇 장은 멀쩡히 나오지만, 그 거리를 넘어선 이미지는 요청조차 되지 않은 채 남습니다. 긴 상세페이지에서 위쪽은 정상인데 아래쪽이 통째로 비어 있다면 거의 이 경우이며, 실패가 아니라 미요청이라 깨진 아이콘조차 보이지 않는 것이 특징입니다. 속성을 지우거나 loading="eager"로 바꾸면 해결됩니다.
자바스크립트로 만든 지연 로딩은 더 확실하게 실패합니다. 주소를 data-src에 넣고 스크롤할 때 src로 옮기는 방식이라, 스크롤이 없으면 src가 영영 비어 있습니다. 쇼핑몰 화면을 ‘다른 이름으로 저장’해 받은 HTML에서 흔하며, 상대 경로와 data-src가 뒤섞여 있어 필요한 부분만 새로 정리하는 편이 빠릅니다.
background-image: url(...)도 <img>와 같은 규칙을 따릅니다. url() 안의 상대 경로는 그 CSS가 놓인 위치, 곧 HTML 파일의 위치를 기준으로 해석되므로 실패합니다.
다만 증상이 다릅니다. 배경 이미지는 실패해도 빈 칸이 아니라 background-color만 남아 그 영역이 단색으로 보입니다. 이유 없이 밋밋한 블록이 있다면 배경 이미지를 의심하십시오.
<style>
/* 상대 경로 — 실패하고 배경색만 남습니다 */
.card { background-image: url('img/bg.png'); background-color: #eee; }
/* data URI — 항상 성공. 따옴표로 감싸는 편이 안전합니다 */
.card { background-image: url('data:image/png;base64,iVBORw0KGgoAAA...'); }
/* SVG 를 직접 넣을 때: < > 는 %3C %3E 로, 색상의 # 은 %23 으로 인코딩합니다 */
.dot {
background-image: url('data:image/svg+xml,%3Csvg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 8 8"%3E%3Ccircle cx="4" cy="4" r="4" fill="%23ff3b30"/%3E%3C/svg%3E');
}
</style>#을 그대로 두면 브라우저가 그 뒤를 주소의 조각(fragment)으로 잘라내 도형이 사라집니다. @font-face 안의 src: url(...)도 같아서 절대 https 주소나 data URI를 써야 하며, 서버에 설치된 한글 폰트 문제는 한글 폰트가 깨지는 이유와 해결 방법에서 따로 다룹니다.
| 증상 | 원인 | 해결 |
|---|---|---|
| 사진 자리에 깨진 이미지 아이콘 | src가 상대 경로이거나 주소가 잘못됨 |
base64로 심거나 절대 https 주소로 교체 |
| 흰 배경에 검은 글씨만, 디자인이 전부 사라짐 | 외부 스타일시트를 <link>로 참조 |
CSS 내용을 <style> 블록으로 이동 |
| 특정 영역만 이미지 없이 단색 | background-image의 상대 경로 |
data URI 또는 절대 https 주소 |
| 위쪽 이미지는 정상, 아래쪽은 아이콘도 없이 빈 칸 | loading="lazy" |
속성 제거 또는 loading="eager" |
이미지 자리가 모두 빈 칸이고 원본 태그에 src가 없음 |
data-src 방식의 자바스크립트 지연 로딩 |
data-src 값을 src로 옮기기 |
| 브라우저에서는 보이는 외부 이미지가 결과에서만 실패 | 핫링크 차단, 봇 차단, 만료된 주소 | 이미지를 내려받아 base64로 심기 |
| 변환이 오래 걸리다 실패 | 느린 외부 리소스, 계속 통신하는 스크립트 | 외부 스크립트 제거, 이미지를 파일 안으로 |
http://로 시작하는 주소만 실패 |
서버가 http 응답을 중단했거나 https로 넘김 |
https://로 교체하거나 base64 |
| data URI를 넣었는데 SVG만 표시되지 않음 | MIME 타입이 image/svg+xml이 아님 |
타입 문자열 교정, 또는 <svg> 코드를 직접 삽입 |
| SVG 배경만 안 보임 | data URI 안의 #이 인코딩되지 않음 |
#을 %23으로 변경 |
| 업로드 단계에서 파일이 거부됨 | 20MB 초과 (base64가 약 33% 키움) | 이미지를 축소한 뒤 다시 심기 |
| 붙여넣기 변환에서만 서버 오류 | 한 번에 보낸 코드 전체가 10MB 초과 | .html 파일로 저장해 업로드 |
첫 항목 하나만 지켜도 대부분의 문제가 업로드 전에 걸러집니다. 서버 환경을 내 컴퓨터에서 그대로 재현하는 방법이기 때문입니다.
src="./, src="../, src="/, <link rel="stylesheet", data-src, loading="lazy", http://를 검색해 남은 것이 없는지 확인합니다.원칙은 하나입니다. 이미지로 내보낼 HTML은 그 파일 하나만으로 완결되게 만드는 것입니다. 스타일은 <style> 안에, 작은 이미지는 data URI로, 큰 사진은 공개된 https 주소로 두면 언제 다시 변환해도 같은 결과가 나옵니다. 크기와 해상도 문제는 해상도와 크기 이해하기를, 다른 주제는 가이드 목록을 참고하십시오.