Chrome Extension의 Service Worker에서 스크린샷을 찍고 특정 영역만 잘라내야 하는 상황이 있었다. 문제는 Service Worker에는 DOM이 없어서 일반적인 Canvas API를 쓸 수 없다는 점이다.
문제 정의
상황
- Chrome Extension의 Service Worker에서 탭 스크린샷 캡처
- 전체 화면이 아닌 특정 영역만 필요 (iframe 영역)
- devicePixelRatio를 고려한 고해상도 크롭 필요
Service Worker의 제약
// Service Worker에서
const canvas = document.createElement('canvas');
// → Uncaught ReferenceError: document is not defined
Service Worker는 DOM이 없다. document도 없고 HTMLCanvasElement도 없다. 하지만 OffscreenCanvas는 된다.
선택지 분석
| 방식 | 장점 | 한계 | 적합한 상황 |
|---|---|---|---|
| Content Script에서 처리 | DOM 사용 가능 | 메인 스레드 블로킹 | 작은 이미지 |
| OffscreenCanvas (SW) | 메인 스레드 독립, DOM 불필요 | API 차이 있음 | Service Worker |
| 외부 서버 처리 | 클라이언트 부담 없음 | 네트워크 비용 | 대규모 처리 |
| WebAssembly | 고성능 | 복잡도 높음 | 무거운 연산 |
이미지 크롭은 단순 연산이라 OffscreenCanvas로 충분하다.
선택 근거
OffscreenCanvas를 선택한 이유:
- Service Worker에서 바로 사용 가능
- 메인 스레드와 완전히 독립적
- Canvas API와 거의 동일한 인터페이스
- Chrome Extension 환경에서 안정적 지원
구현
전체 흐름
captureVisibleTab() → dataUrl
↓
fetch(dataUrl) → Blob
↓
createImageBitmap(blob) → ImageBitmap
↓
OffscreenCanvas에 crop 영역만 drawImage
↓
convertToBlob() → Blob
↓
FileReader.readAsDataURL() → base64
핵심 코드
export async function captureAndCrop(
tabId: number,
bounds: Bounds,
devicePixelRatio: number,
viewport: Viewport
): Promise<CaptureResult> {
return new Promise((resolve) => {
// 1. 탭 스크린샷 캡처
chrome.tabs.captureVisibleTab({ format: 'png' }, async (dataUrl) => {
if (chrome.runtime.lastError) {
resolve({ success: false, error: chrome.runtime.lastError.message });
return;
}
try {
// 2. dataUrl → ImageBitmap 변환
const res = await fetch(dataUrl);
const blob = await res.blob();
const imageBitmap = await createImageBitmap(blob);
// 3. devicePixelRatio 적용
const dpr = devicePixelRatio;
const cropX = bounds.x * dpr;
const cropY = bounds.y * dpr;
const cropWidth = bounds.width * dpr;
const cropHeight = bounds.height * dpr;
// 4. OffscreenCanvas 생성
const canvas = new OffscreenCanvas(cropWidth, cropHeight);
const ctx = canvas.getContext('2d');
// 5. crop 영역만 그리기
ctx.drawImage(
imageBitmap,
cropX, cropY, cropWidth, cropHeight, // source
0, 0, cropWidth, cropHeight // destination
);
// 6. Blob으로 변환 (OffscreenCanvas 전용 메서드)
const croppedBlob = await canvas.convertToBlob({ type: 'image/png' });
// 7. data URL로 변환
const reader = new FileReader();
reader.onloadend = () => {
resolve({
success: true,
imageData: reader.result as string,
viewport,
});
};
reader.readAsDataURL(croppedBlob);
} catch (error) {
resolve({ success: false, error: error.message });
}
});
});
}
HTMLCanvasElement vs OffscreenCanvas
두 API는 비슷하지만 다른 점이 있다.
| 기능 | HTMLCanvasElement | OffscreenCanvas |
|---|---|---|
| DOM 필요 | O | X |
| Service Worker | X | O |
| Blob 변환 | toBlob(callback) |
convertToBlob() (Promise) |
| DataURL 변환 | toDataURL() |
직접 지원 안 함 |
OffscreenCanvas는 toDataURL()을 지원하지 않는다. convertToBlob()으로 Blob을 만든 후 FileReader로 data URL을 얻어야 한다.
// HTMLCanvasElement
const dataUrl = canvas.toDataURL('image/png');
// OffscreenCanvas
const blob = await canvas.convertToBlob({ type: 'image/png' });
const reader = new FileReader();
reader.readAsDataURL(blob);
// reader.result → data URL
devicePixelRatio 처리
Retina 디스플레이에서 captureVisibleTab은 실제 픽셀 크기로 캡처한다.
// CSS 좌표
bounds = { x: 100, y: 50, width: 300, height: 200 }
// devicePixelRatio = 2인 경우 실제 픽셀
cropX = 100 * 2 = 200
cropY = 50 * 2 = 100
cropWidth = 300 * 2 = 600
cropHeight = 200 * 2 = 400
이 변환을 빼먹으면 원하는 영역의 왼쪽 위 1/4만 잘리게 된다.
ImageBitmap을 쓰는 이유
Image 객체 대신 ImageBitmap을 쓰는 이유가 있다.
// Image 객체 - Service Worker에서 불가
const img = new Image(); // DOM API
// ImageBitmap - Service Worker에서 사용 가능
const bitmap = await createImageBitmap(blob);
Image는 DOM 객체라 Service Worker에서 쓸 수 없다. createImageBitmap()은 Web Worker/Service Worker에서도 사용 가능한 비동기 API다.
Service Worker에서 이미지를 처리해야 한다면 OffscreenCanvas가 가장 나은선택인듯 해보인다.
| 단계 | 메서드 | 설명 |
|---|---|---|
| 이미지 로드 | createImageBitmap() |
DOM 없이 이미지 디코딩 |
| 캔버스 생성 | new OffscreenCanvas() |
DOM 없이 캔버스 생성 |
| 그리기 | ctx.drawImage() |
일반 Canvas와 동일 |
| 내보내기 | convertToBlob() |
Promise 기반 (toBlob이 아님) |
주의할 점은 toDataURL()이 없다는 것과 devicePixelRatio 변환이다. 이 두 가지만 알면 일반 Canvas와 크게 다르지 않다.
프런트엔드 엔지니어, QA 엔지니어 그리고 디자이너를 위한
" ALL IN ONE " QA 서비스
https://pixeldiff.turtle-tail.com
'FrontEnd' 카테고리의 다른 글
| next-intl 없이 i18n 직접 만들기 (0) | 2026.02.07 |
|---|---|
| iframe 키 이벤트 재합성으로 Cross-Origin 우회하기 (0) | 2026.02.05 |
| Sharp + WebP로 이미지 30-50% 용량 절감하기 (0) | 2026.01.27 |
| S3 Presigned URL로 클라이언트 직접 업로드 (0) | 2026.01.22 |
| 풀페이지 스크린샷 타일 스티칭 구현 (0) | 2026.01.20 |
