- 2026.07.29 Apps Script 프로젝트가 늘어나면 반드시 망가지는 것들
- 2026.07.16 매일 돌던 Apps Script 트리거가 갑자기 멈추는 이유
- 2026.07.15 브라우저 저장소에 고객정보 넣으면 생기는 문제
- 2026.07.07 브라우저가 내 데이터를 버릴 때: 초록 체크가 거짓말을 한다
- 2026.07.07 설치됨 ≠ 작동함: 백업 트리거가 한 번도 안 돌았다
- 2026.07.07 GAS엔 점진 배포가 없다: '맞는' 검사 하나가 전 사용자를 잠근다
- 2026.07.07 시트가 텍스트를 날짜로 바꾼다: '2026-06'이 완료를 영영 막을 때
- 2026.06.18 여러 사람이 한 시트에 쓰는 점검 앱: 동시성·중복·내보내기 설계
- 2026.06.12 셀을 한 칸씩 읽지 마라: 서비스 호출이 진짜 비용이다
- 2026.06.11 시트 셀에 JSON·이미지를 통째로 넣으면 터진다
- 2026.06.10 웹앱 배포 설정 하나가 보안 모델을 정한다: Execute as / 접근 권한
- 2026.06.09 GAS 6분 실행 한도: 멈추기 전에 끊어서 재개하라
- 2026.06.08 PropertiesService는 데이터베이스가 아니다: 9KB의 벽
- 2026.06.07 모바일에서 화면을 이미지로 저장할 때 멈추는 진짜 이유 지금 보는 글
- 2026.06.06 동시에 같은 시트를 쓰면 깨진다: LockService와 30 동시 실행 천장
- 2026.06.05 재배포할 때마다 주소가 바뀌면 안 된다: /exec URL 고정
- 2026.06.04 외부 호출은 가끔 실패한다: 지수 백오프와 재시도 예산
- 2026.06.03 HtmlService 앱은 껍데기 먼저: 비동기 로드와 모바일 뒤로가기
- 2026.06.02 GAS를 졸업해야 할 때: 한계 신호와 외부 DB 이전
- 2026.06.01 GAS에 지능과 엔진룸을 더하기: Vertex AI와 Cloud Run
모바일에서 화면을 이미지로 저장할 때 멈추는 진짜 이유
SVG foreignObject로 DOM을 캡처하면 모바일 Safari·GAS iframe에서 멈추거나 빈 이미지가 나온다. 직접 캔버스에 그리고, 다운로드는 Web Share로 받아라.
SVG foreignObject로 DOM을 통째로 캡처하는 방식은 모바일에서 신뢰할 수 없다. 모바일 Safari와 GAS의 iframe 환경에서 캡처가 멈추거나 빈(흰) 이미지가 나온다. 데스크톱에서 잘 되던 코드가 현장 폰에서 죽는다.
왜 중요한가
현장 앱의 핵심 결과물은 보통 점검표 PDF나 이미지 출력이다. 그게 폰에서 멈추면 업무가 거기서 끝난다. 사용자는 “저장 버튼이 안 먹는다”고만 느끼고, 원인(브라우저 렌더링 엔진 차이)은 보이지 않는다.
정답: 직접 캔버스에 그린다
DOM 캡처에 의존하지 말고 필요한 요소를 canvas에 직접 그린다. export 대상별로 캡처 방식을 분기하고, 무거운 작업 전에는 진행 표시 + 한 프레임 양보 + 타임아웃 가드를 둔다.
async function exportImage(drawFn) {
showProgress(); // 1초 안에 진행 표시
await nextFrame(); // 페인트 한 번 양보 (멈춤처럼 보이지 않게)
const canvas = document.createElement("canvas");
canvas.width = 1080;
canvas.height = 1527; // A4 비율
const ctx = canvas.getContext("2d");
drawFn(ctx); // foreignObject가 아니라 직접 드로잉
const blob = await new Promise((res) => canvas.toBlob(res, "image/png"));
await share(blob, "inspection.png");
hideProgress();
}
function nextFrame() {
return new Promise((r) => requestAnimationFrame(() => r()));
}
다운로드도 함정이다
<a download>는 파일명 힌트일 뿐, 모바일에서 다운로드를 보장하지 않는다. Web Share API를 먼저 쓰고, 실패하면 새 탭으로 열어 “길게 눌러 저장”을 안내한다.
async function share(blob, name) {
const file = new File([blob], name, { type: blob.type });
if (navigator.canShare?.({ files: [file] })) {
return navigator.share({ files: [file] }); // 클릭 핸들러 안에서만
}
window.open(URL.createObjectURL(blob), "_blank"); // fallback
}
navigator.share는 사용자 클릭 핸들러 안에서 호출해야 한다. 비동기 작업 뒤로 미루면 브라우저가 막는다.
깊이
- 고해상도 A4는 모바일 메모리를 많이 쓴다.
scale을 제한하고, 큰 출력은 페이지 단위로 그린 뒤 합성한다. - 사진 회전. iPhone 세로 사진은 EXIF orientation이 있어, canvas에 그릴 때 명시적으로 회전 보정하지 않으면 눕거나 뒤집힌다.
핵심 한 줄: 모바일 export는 DOM을 찍지 말고, 캔버스에 그려서 공유로 내보내라.
자주 묻는 질문
- 모바일 Safari에서 SVG foreignObject로 DOM을 캡처하면 왜 빈 이미지가 나오나요?
- SVG foreignObject 방식은 모바일 Safari와 GAS의 iframe 환경에서 신뢰할 수 없습니다. 캡처가 멈추거나 흰 이미지만 반환되는데, 브라우저 렌더링 엔진 차이 때문입니다.
- 모바일에서 이미지 export를 안정적으로 하려면 어떻게 해야 하나요?
- DOM 캡처에 의존하지 말고 필요한 요소를 canvas에 직접 그려야 합니다. 무거운 작업 전에는 진행 표시, 한 프레임 양보(requestAnimationFrame), 타임아웃 가드를 함께 써야 멈춤처럼 보이지 않습니다.
- 모바일에서 canvas 이미지를 저장할 때 a download 태그를 쓰면 안 되나요?
- a download는 파일명 힌트일 뿐, 모바일에서 다운로드를 보장하지 않습니다. Web Share API를 먼저 시도하고, 실패하면 새 탭으로 열어 길게 눌러 저장하도록 안내하는 방식이 권장됩니다.
- 2026.07.29 Apps Script 프로젝트가 늘어나면 반드시 망가지는 것들
- 2026.07.16 매일 돌던 Apps Script 트리거가 갑자기 멈추는 이유
- 2026.07.15 브라우저 저장소에 고객정보 넣으면 생기는 문제
- 2026.07.07 브라우저가 내 데이터를 버릴 때: 초록 체크가 거짓말을 한다
- 2026.07.07 설치됨 ≠ 작동함: 백업 트리거가 한 번도 안 돌았다
- 2026.07.07 GAS엔 점진 배포가 없다: '맞는' 검사 하나가 전 사용자를 잠근다
- 2026.07.07 시트가 텍스트를 날짜로 바꾼다: '2026-06'이 완료를 영영 막을 때
- 2026.06.18 여러 사람이 한 시트에 쓰는 점검 앱: 동시성·중복·내보내기 설계
- 2026.06.12 셀을 한 칸씩 읽지 마라: 서비스 호출이 진짜 비용이다
- 2026.06.11 시트 셀에 JSON·이미지를 통째로 넣으면 터진다
- 2026.06.10 웹앱 배포 설정 하나가 보안 모델을 정한다: Execute as / 접근 권한
- 2026.06.09 GAS 6분 실행 한도: 멈추기 전에 끊어서 재개하라
- 2026.06.08 PropertiesService는 데이터베이스가 아니다: 9KB의 벽
- 2026.06.07 모바일에서 화면을 이미지로 저장할 때 멈추는 진짜 이유 지금 보는 글
- 2026.06.06 동시에 같은 시트를 쓰면 깨진다: LockService와 30 동시 실행 천장
- 2026.06.05 재배포할 때마다 주소가 바뀌면 안 된다: /exec URL 고정
- 2026.06.04 외부 호출은 가끔 실패한다: 지수 백오프와 재시도 예산
- 2026.06.03 HtmlService 앱은 껍데기 먼저: 비동기 로드와 모바일 뒤로가기
- 2026.06.02 GAS를 졸업해야 할 때: 한계 신호와 외부 DB 이전
- 2026.06.01 GAS에 지능과 엔진룸을 더하기: Vertex AI와 Cloud Run