내 컴퓨터에서는 멀쩡하던 화면이 변환 결과에서는 사진 자리가 비고 디자인이 통째로 빠진 맨 글씨로 나오는 일이 있습니다. 변환이 잘못된 것이 아니라 서버가 정말 그렇게 보이는 페이지를 캡처한 것이고, 원인은 거의 항상 하나로 모입니다.

1. 서버로 올라가는 것은 HTML 파일뿐입니다

파일 선택창에서 detail.html을 고르면 전송되는 것은 그 파일 하나입니다. 같은 폴더의 images 폴더도 style.css도 따라가지 않습니다. 브라우저는 고른 파일이 무엇을 참조하는지 들여다보지 않기 때문입니다.

서버는 받은 HTML을 임시 위치에 저장한 뒤 file:// 주소로 엽니다. 상대 경로는 원래 폴더가 아니라 그 임시 위치를 기준으로 해석되므로, src="images/a.jpg"는 임시 폴더 안에 없는 파일을 찾다 실패합니다.

게다가 이 실패는 조용합니다. 브라우저는 없는 이미지를 만나도 오류를 띄우지 않고 그 자리에 작은 깨진 이미지 아이콘만 남긴 채 나머지를 그리므로, 변환은 실패가 아니라 성공으로 끝나고 결과 PNG에는 빠진 상태가 그대로 담깁니다. 코드를 붙여넣는 방식도 조건이 같습니다. 애초에 업로드 창이 받아 주는 확장자가 .html.htm뿐이라, 이미지나 CSS 파일은 함께 고르는 것 자체가 되지 않습니다.

2. 되는 것과 안 되는 것

기준은 단순합니다. 파일 안에 들어 있거나 인터넷에서 그 자체로 내려받을 수 있으면 반영되고, 그 밖의 모든 참조는 실패합니다.

<!-- 서버에서 찾지 못합니다 -->
<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를 확대해서 확인하십시오.

3. CSS는 파일 안으로 옮깁니다

가장 흔하면서 가장 쉽게 해결되는 항목입니다. <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>의 순서 그대로 이어 붙여야 우선순위가 유지됩니다.

4. 이미지를 base64 data URI로 심기

data URI는 이미지 파일의 바이트를 글자로 바꿔 src 안에 직접 써 넣는 방식입니다. 외부 요청이 없으므로 네트워크 상태나 상대 서버의 정책과 무관하게 항상 성공하며, 형식은 data:MIME타입;base64,데이터 한 줄입니다.

MIME 타입은 원본 형식에 맞춰 적습니다. .pngimage/png, .jpgimage/jpeg, .gifimage/gif, .webpimage/webp, .svgimage/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>

용량이 약 3분의 1 늘어납니다

base64는 3바이트를 4글자로 바꾸므로 데이터가 약 4/3배, 즉 33% 정도 커집니다. 200KB짜리 사진은 약 267KB의 글자가 되어 HTML 안에 들어갑니다. 업로드 한도가 파일당 20MB이니, 심는 이미지의 원본 합계가 15MB를 넘기 시작하면 한도에 걸립니다. 코드를 붙여넣어 변환할 때는 한도가 더 낮아 한 번에 보내는 코드 전체가 10MB를 넘으면 변환 대신 서버 오류가 돌아오므로, base64를 크게 쓸 계획이라면 처음부터 .html 파일로 저장해 업로드하십시오.

그래서 심기 전에 최종 표시 크기의 2배 정도로 줄여 두십시오. 캡처 배율이 2배라 그 이상은 파일만 키울 뿐입니다. 로고와 아이콘, 배경 패턴에 특히 잘 맞고, 고화질 사진이 여러 장이면 다음 방법이 낫습니다.

SVG는 base64 대신 <svg> 코드를 그대로 붙여넣는 편이 작고 선명합니다.

5. 절대 https 주소 쓰기

이미 인터넷에 공개된 이미지라면 전체 주소를 그대로 적는 것이 가장 간단합니다. 조건은 하나, 로그인이나 권한 없이 누구나 그 주소로 파일을 받을 수 있어야 한다는 것입니다.

확인은 시크릿 창을 새로 열어 주소창에 URL만 붙여넣어 봅니다. 로그인 상태와 캐시가 빠진 조건이라 서버 환경에 가깝고, 이미지 한 장만 뜨면 통과입니다. 클라우드 드라이브의 공유 링크는 대부분 여기서 걸립니다. 이미지 파일이 아니라 미리보기 화면을 그리는 웹페이지 주소이기 때문입니다.

6. https 주소인데도 실패한다면

주소가 맞는데도 나오지 않는다면 원인은 세 갈래이고, 구분하는 방법이 각각 다릅니다.

① 핫링크 차단과 봇 차단

쇼핑몰과 블로그, CDN은 요청에 담긴 Referer나 브라우저 종류를 보고 자기 사이트 밖에서 온 요청을 거부합니다. 시크릿 창에서 URL 직접 열기는 되는데 내 HTML 안에서만 안 나온다면 이 경우이며, 상대 서버의 정책이라 우회할 수 없으니 이미지를 내려받아 base64로 심어야 합니다.

② 느린 응답

캡처는 네트워크 요청이 잠잠해진 시점을 기다렸다가 이루어지며, 이 대기에는 30초의 상한이 있습니다. 무거운 사진 수십 장을 느린 서버에서 끌어오면 그 안에 끝나지 못해 시간 초과가 나고, 이때는 일부만 빠진 이미지가 아니라 그 파일의 변환 자체가 실패로 끝납니다. 광고 태그나 통계 스크립트, 채팅 위젯처럼 끊임없이 통신하는 요소가 있어도 네트워크가 조용해지지 않아 같은 결과가 됩니다. 어차피 결과물은 움직이지 않는 한 장의 이미지이므로, 오래 걸리다 실패한다면 화면에 보이지 않는 외부 스크립트부터 지우는 것이 가장 빠릅니다.

③ 주소 자체의 문제

만료되는 임시 서명이 붙은 주소, 로그인 세션이 있어야 열리는 주소, 한글 파일명이 인코딩되지 않은 주소는 환경이 바뀌면 실패합니다. HTML 안에서 &&amp;로 쓰지 않아 쿼리가 중간에서 잘리는 경우도 흔합니다. 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>

이 조각이 잡아내는 것은 요청을 했다가 실패한 이미지뿐입니다. 요청 자체가 일어나지 않은 이미지는 오류가 발생하지 않아 빨간 상자도 생기지 않으니, 확인 후에도 빈 칸이 남아 있다면 다음 항목을 보십시오. 확인이 끝나면 이 조각은 지우고 다시 변환하십시오.

7. 지연 로딩(lazy loading)이 만드는 빈 칸

loading="lazy"는 이미지가 화면에 가까워질 때까지 내려받기를 미루는 속성으로, 웹페이지에서는 속도에 도움이 되지만 캡처에서는 정반대로 작동합니다.

렌더링은 높이 800 CSS px의 창에서 시작하고, 캡처는 스크롤 없이 문서 전체를 한 번에 담습니다. 즉 화면을 내리며 이미지를 깨우는 과정이 없습니다. 브라우저는 첫 화면에서 어느 정도 아래까지는 미리 받아 두므로 위쪽 몇 장은 멀쩡히 나오지만, 그 거리를 넘어선 이미지는 요청조차 되지 않은 채 남습니다. 긴 상세페이지에서 위쪽은 정상인데 아래쪽이 통째로 비어 있다면 거의 이 경우이며, 실패가 아니라 미요청이라 깨진 아이콘조차 보이지 않는 것이 특징입니다. 속성을 지우거나 loading="eager"로 바꾸면 해결됩니다.

자바스크립트로 만든 지연 로딩은 더 확실하게 실패합니다. 주소를 data-src에 넣고 스크롤할 때 src로 옮기는 방식이라, 스크롤이 없으면 src가 영영 비어 있습니다. 쇼핑몰 화면을 ‘다른 이름으로 저장’해 받은 HTML에서 흔하며, 상대 경로와 data-src가 뒤섞여 있어 필요한 부분만 새로 정리하는 편이 빠릅니다.

8. CSS 배경 이미지와 웹폰트

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를 써야 하며, 서버에 설치된 한글 폰트 문제는 한글 폰트가 깨지는 이유와 해결 방법에서 따로 다룹니다.

9. 증상으로 원인 찾기

결과 이미지에 나타난 모습으로 원인을 좁히는 표
증상원인해결
사진 자리에 깨진 이미지 아이콘 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 파일로 저장해 업로드

10. 업로드 전 점검 목록

첫 항목 하나만 지켜도 대부분의 문제가 업로드 전에 걸러집니다. 서버 환경을 내 컴퓨터에서 그대로 재현하는 방법이기 때문입니다.

  1. HTML 파일 하나만 복사해 빈 새 폴더에 넣고 더블클릭해 엽니다. 지금 보이는 화면이 곧 변환 결과이며, 여기서 빠진 것은 결과에서도 빠집니다.
  2. 시크릿 창으로 다시 엽니다. 캐시와 로그인 세션의 도움을 받고 있었는지 드러납니다.
  3. 개발자 도구(F12)의 콘솔과 네트워크 탭에 빨갛게 실패한 항목이 없는지 봅니다.
  4. 편집기에서 src="./, src="../, src="/, <link rel="stylesheet", data-src, loading="lazy", http://를 검색해 남은 것이 없는지 확인합니다.
  5. 외부 https 이미지는 주소를 시크릿 창에 붙여넣어 이미지 한 장만 뜨는지 확인합니다.
  6. 파일 용량이 20MB 아래인지 확인합니다. 붙여넣기로 변환한다면 한 번에 보내는 코드 전체가 10MB를 넘지 않아야 합니다.

원칙은 하나입니다. 이미지로 내보낼 HTML은 그 파일 하나만으로 완결되게 만드는 것입니다. 스타일은 <style> 안에, 작은 이미지는 data URI로, 큰 사진은 공개된 https 주소로 두면 언제 다시 변환해도 같은 결과가 나옵니다. 크기와 해상도 문제는 해상도와 크기 이해하기를, 다른 주제는 가이드 목록을 참고하십시오.