웹에서 화면을 이미지로 저장하기
웹에서 화면을 이미지로 저장하기 — html2canvas vs modern-screenshot
토스 앱인토스 미니앱에서 결과 카드를 이미지로 저장하는 기능을 구현하면서 겪은 삽질기
기술 스택
- React 19 + TypeScript
- Vite 5
- @apps-in-toss/web-framework 2.0.9 (saveBase64Data)
- html2canvas 1.x (최종 채택)
- modern-screenshot (시도 후 포기)
하고 싶었던 것
성격 테스트 결과 카드를 이미지로 저장하는 기능이 필요했어요. 사용자가 결과 화면에서 다운로드 버튼을 누르면, 캐릭터 일러스트 + 이름 + 육각형 스탯 차트가 포함된 카드를 PNG로 저장하는 거예요.
환경이 좀 특수했어요. 토스 앱 안의 WebView에서 동작해야 하고, 저장은 토스 SDK의 saveBase64Data를 써야 했어요.
두 라이브러리의 근본적 차이
html2canvas — DOM을 Canvas에 직접 다시 그림
DOM 읽기 → Canvas API로 한 땀 한 땀 다시 그림 → PNG 추출
html2canvas는 DOM 요소의 스타일을 읽고, Canvas 2D API(fillRect, drawImage, fillText 등)로 처음부터 다시 그려요. 브라우저의 렌더링 엔진을 쓰는 게 아니라, 자체적으로 CSS를 해석하고 canvas에 그리는 방식이에요.
import html2canvas from 'html2canvas';
const canvas = await html2canvas(element, {
backgroundColor: '#1a1a1a',
scale: 2,
useCORS: true,
});
const dataUrl = canvas.toDataURL('image/png');
modern-screenshot — DOM을 SVG로 변환 후 Canvas로 렌더링
DOM 복제 → SVG foreignObject로 변환 → Canvas에 SVG 렌더링 → PNG 추출
modern-screenshot은 DOM을 통째로 복제(clone)한 뒤, SVG의 <foreignObject> 안에 넣어요. 그 SVG를 canvas에 그려서 이미지를 만드는 방식이에요.
import { domToPng } from 'modern-screenshot';
const dataUrl = await domToPng(element, {
backgroundColor: '#1a1a1a',
scale: 2,
});
왜 modern-screenshot에서 이미지가 사라졌나
결론부터 말하면, SVG foreignObject의 보안 제한 때문이에요.
SVG foreignObject 안에서는 외부 리소스를 로드할 수 없어요. 이미지가 URL로 참조되면(<img src="/assets/character.png">) SVG 내부에서 접근이 차단돼요. 일반 브라우저에서는 same-origin이면 허용되기도 하지만, 토스 앱의 WebView에서는 더 엄격하게 막혔어요.
시도한 것들:
| 시도 | 결과 |
|---|---|
fetchFn 옵션으로 fetch → data URL 변환 |
토스 WebView에서 fetch 실패 |
canvas drawImage로 img를 data URL 변환 |
토스 WebView에서 canvas tainted |
XMLHttpRequest로 blob → data URL |
역시 실패 |
Vite ?inline으로 빌드타임 base64 변환 |
DOM clone 시 원본 src가 복사됨 |
| img src를 직접 setAttribute로 교체 | domToPng 내부 clone이 원본 참조 |
5번이나 다른 방법을 시도했지만, modern-screenshot이 내부적으로 DOM을 clone하는 과정에서 원본 이미지 경로를 사용하기 때문에, 아무리 런타임에 src를 바꿔도 소용이 없었어요.
html2canvas로 바꾸니 바로 해결
html2canvas는 SVG를 거치지 않아요. DOM을 읽고 Canvas에 직접 그리기 때문에, 이미 로드된 이미지를 drawImage로 바로 canvas에 그릴 수 있어요. 토스 WebView에서도 같은 origin의 이미지에 대한 canvas drawImage는 허용되더라고요.
// 이게 전부예요
const canvas = await html2canvas(element, {
backgroundColor: '#1a1a1a',
scale: 2,
useCORS: true,
});
그러면 modern-screenshot은 언제 쓰나
html2canvas에도 약점이 있어요. <canvas> 태그 내용을 읽지 못해요. 화면에 Chart.js, Three.js, rough.js 등으로 그린 <canvas> 요소가 있으면 html2canvas는 그 내용을 캡쳐할 수 없어요. 빈 영역으로 나와요.
modern-screenshot은 SVG 변환 방식이라 <canvas> 내용도 캡쳐할 수 있어요.
| 상황 | 추천 |
|---|---|
| 일반 HTML/CSS + 이미지 | html2canvas |
<canvas> 태그가 화면에 있음 |
modern-screenshot |
| 토스 앱인토스 WebView | html2canvas (SVG 보안 제한 회피) |
| SVG 차트 (recharts 등) | html2canvas (SVG는 <canvas>가 아님) |
추가로 주의할 점: 폰트
캡쳐 라이브러리 종류와 상관없이, 외부 폰트(Google Fonts 등)는 캡쳐 시 적용이 안 될 수 있어요. CORS 정책 때문에 폰트 파일을 읽지 못하거든요.
해결법: 폰트 파일을 프로젝트에 직접 넣고 @font-face로 로컬 경로를 지정하면 돼요. 다만 ait build(토스 빌드 도구) 환경에서는 경로 설정을 주의해야 해요.
토스 앱인토스에서 이미지 저장 전체 흐름
import html2canvas from 'html2canvas';
export async function captureAndSave(element: HTMLElement, fileName: string) {
// 1. Canvas에 화면 그리기
const canvas = await html2canvas(element, {
backgroundColor: '#1a1a1a',
scale: 2,
useCORS: true,
});
// 2. base64 추출
const dataUrl = canvas.toDataURL('image/png');
const base64 = dataUrl.replace(/^data:image\/png;base64,/, '');
// 3. 토스 SDK로 갤러리 저장
const { saveBase64Data } = await import('@apps-in-toss/web-framework');
await saveBase64Data({
data: base64,
fileName,
mimeType: 'image/png',
});
}
정리
| html2canvas | modern-screenshot | |
|---|---|---|
| 변환 경로 | DOM → Canvas 직접 | DOM → SVG → Canvas |
| 이미지 처리 | Canvas drawImage | SVG foreignObject embed |
<canvas> 캡쳐 |
❌ 못 함 | ✅ 가능 |
| 토스 WebView | ✅ 동작 | ❌ 이미지 누락 |
| 외부 폰트 | ⚠️ CORS 주의 | ⚠️ CORS 주의 |
"되던 프로젝트에서 뭘 썼더라"를 확인하는 게 가장 빠른 해결법이었어요. 같은 환경(토스 WebView)에서 검증된 html2canvas를 처음부터 썼으면 5번의 삽질은 없었을 거예요.