Safari Web Extension 배포 가이드
작성일: 2026-04-06 Last Updated: 2026-04-06 대상: Safari Web Extension (macOS + iOS) 개발, App Store 배포
공통 문서 참조: 크로스 브라우저 호환성은
cross-browser.md, Chrome 배포는chrome-store.md, Firefox 배포는firefox-addon.md참조
목차
- Safari Web Extension 개요
- 프로젝트 변환 및 설정
- API 호환성
- Apple Developer Program
- App Store 심사
- macOS + iOS 동시 지원
- 개발 및 디버깅
- CI/CD (Xcode Cloud / GitHub Actions)
- 비용 및 의사결정 가이드
- 체크리스트
1. Safari Web Extension 개요
Safari Web Extension이란
Safari 15+ (macOS Monterey, iOS 15)부터 Manifest V2/V3 기반 Web Extension API를 공식 지원합니다. 기존 Chrome/Firefox 확장을 Xcode 변환 도구를 통해 Safari 확장으로 변환할 수 있습니다.
핵심 특성
| 항목 | 내용 |
|---|---|
| 지원 시작 | Safari 15+ (2021), Manifest V3는 Safari 15.4+ |
| 배포 채널 | Mac App Store + iOS App Store (App Store 전용) |
| 개발 도구 | Xcode 필수 (macOS 전용) |
| 변환 도구 | safari-web-extension-converter (Xcode CLI Tools 포함) |
| 플랫폼 | macOS + iOS Safari 동시 지원 가능 |
| 비용 | Apple Developer Program $99/년 |
Chrome/Firefox와의 근본적 차이
| 항목 | Chrome/Firefox | Safari |
|---|---|---|
| 배포 방식 | 스토어에 ZIP 업로드 | 네이티브 앱으로 래핑 → App Store 제출 |
| 빌드 도구 | Node.js만으로 충분 | Xcode 필수 (macOS에서만 가능) |
| 심사 주체 | 각 스토어 자체 심사 | Apple App Review (앱 심사와 동일) |
| 코드 서명 | 불필요 | Apple 인증서 + 프로비저닝 프로파일 필수 |
| 확장 활성화 | 설치 즉시 활성 | 사용자가 Safari 설정에서 수동 활성화 |
Safari 버전별 Web Extension 지원
| Safari 버전 | macOS | iOS | 주요 추가 기능 |
|---|---|---|---|
| 14 | Big Sur | 14 | Safari App Extension (Legacy, 비 Web Extension) |
| 15 | Monterey | 15 | Web Extension 최초 지원 (MV2) |
| 15.4 | Monterey 12.3 | 15.4 | Manifest V3 지원 |
| 16 | Ventura | 16 | declarativeNetRequest, scripting 개선 |
| 16.4 | Ventura 13.3 | 16.4 | Service Worker 안정화, storage.session |
| 17 | Sonoma | 17 | userScripts, Profile별 확장, storage.session 개선 |
| 18 | Sequoia | 18 | Web Extension API 대폭 확대, sidePanel 논의 중 |
2. 프로젝트 변환 및 설정
기존 Chrome 확장을 Safari로 변환
Safari Web Extension은 기존 확장의 빌드 결과물을 Xcode 프로젝트로 감싸는 방식입니다.
변환 절차
# 1. Chrome용 빌드 생성
cd platform/client/browser-ext
npm run build -- --browser chrome
# 또는 Safari용 빌드가 지원되는 경우
npm run build -- --browser safari
# 2. Xcode 프로젝트로 변환
xcrun safari-web-extension-converter .output/chrome-mv3 \
--project-location ./safari-xcode \
--app-name "My Extension" \
--bundle-identifier com.example.myextension \
--swift \
--macos-only # iOS도 지원하려면 이 플래그 제거
safari-web-extension-converter 주요 옵션
| 옵션 | 설명 | 기본값 |
|---|---|---|
--project-location <path> | Xcode 프로젝트 출력 경로 | 현재 디렉토리 |
--app-name <name> | 앱 이름 | 확장 이름 |
--bundle-identifier <id> | 번들 식별자 | 자동 생성 |
--swift | Swift 기반 앱 프로젝트 | Obj-C |
--macos-only | macOS 전용 (iOS 제외) | 양쪽 모두 |
--ios-only | iOS 전용 (macOS 제외) | 양쪽 모두 |
--no-open | 변환 후 Xcode 자동 열기 방지 | 열기 |
--no-prompt | 대화형 프롬프트 비활성 | 대화형 |
--force | 기존 프로젝트 덮어쓰기 | 거부 |
--copy-resources | 리소스를 프로젝트에 복사 (참조 대신) | 참조 |
변환 후 Xcode 프로젝트 구조
safari-xcode/
+-- My Extension.xcodeproj # Xcode 프로젝트 파일
+-- My Extension/ # 컨테이너 앱 (App Store용 래퍼)
| +-- AppDelegate.swift # 앱 진입점
| +-- ViewController.swift # 확장 활성화 안내 UI
| +-- Main.storyboard # macOS UI
| +-- Assets.xcassets/ # 앱 아이콘
| +-- Info.plist # 앱 메타데이터
| +-- My_Extension.entitlements # 권한 (App Groups 등)
|
+-- Shared (Extension)/ # 실제 확장 코드 (공유)
| +-- Resources/ # 웹 확장 리소스 (여기에 빌드 결과물)
| | +-- manifest.json
| | +-- popup.html
| | +-- background.js
| | +-- _locales/
| | +-- icons/
| | +-- ...
| +-- SafariWebExtensionHandler.swift # 네이티브 메시징 핸들러
| +-- Info.plist
|
+-- My Extension (iOS)/ # iOS 컨테이너 앱 (양쪽 지원 시)
| +-- ...
+-- My Extension (macOS)/ # macOS 컨테이너 앱
+-- ...
WXT에서 Safari 빌드
WXT는 --browser safari 옵션으로 Safari 호환 빌드를 생성합니다.
# Safari용 빌드
wxt build --browser safari
# 출력: .output/safari-mv3/
// wxt.config.ts - Safari 관련 설정
import { defineConfig } from 'wxt';
export default defineConfig({
manifest: ({ browser }) => ({
name: '__MSG_extensionName__',
permissions: ['storage', 'activeTab'],
// Safari에서는 sidePanel 미지원이므로 제외
...(browser !== 'safari'
? {
side_panel: { default_path: 'sidepanel.html' },
permissions: ['storage', 'activeTab', 'sidePanel'],
}
: {}),
}),
});
# Safari 빌드 후 Xcode 변환 통합 스크립트
#!/bin/bash
set -euo pipefail
EXTENSION_NAME="My Extension"
BUNDLE_ID="com.example.myextension"
PROJECT_DIR="./safari-xcode"
# 1. Safari용 빌드
wxt build --browser safari
# 2. Xcode 프로젝트 변환 (기존 프로젝트가 있으면 리소스만 업데이트)
if [ -d "$PROJECT_DIR" ]; then
echo "Updating existing Xcode project resources..."
rsync -av --delete \
.output/safari-mv3/ \
"$PROJECT_DIR/Shared (Extension)/Resources/"
else
echo "Creating new Xcode project..."
xcrun safari-web-extension-converter .output/safari-mv3 \
--project-location "$PROJECT_DIR" \
--app-name "$EXTENSION_NAME" \
--bundle-identifier "$BUNDLE_ID" \
--swift \
--copy-resources \
--no-open
fi
echo "Done. Open $PROJECT_DIR in Xcode."
manifest.json Safari 호환성
Safari는 Manifest V3를 지원하되, Chrome/Firefox와 일부 차이가 있습니다.
지원하는 Manifest 키
| Manifest 키 | 지원 | 비고 |
|---|---|---|
manifest_version: 3 | O | 15.4+ |
action | O | popup, icon, badge |
background.service_worker | O | 16.4+ 안정화 |
background.scripts | O | Event Page 방식 (MV2 호환) |
content_scripts | O | |
permissions | O | 지원 범위 제한 있음 |
host_permissions | O | |
options_ui | O | open_in_tab: true 권장 |
web_accessible_resources | O | |
commands | O | 키보드 단축키 |
default_locale + _locales/ | O | 다국어 |
declarative_net_request | O | 16+ |
content_security_policy | O |
미지원 또는 제한적인 Manifest 키
| Manifest 키 | 상태 | 대안 |
|---|---|---|
side_panel | X | 별도 탭 또는 popover |
offscreen | X | Content Script에서 DOM API 사용 |
devtools_page | X | Safari Web Inspector 자체 사용 |
chrome_url_overrides | X | 없음 |
tab_groups | X | 없음 |
user_scripts | 제한적 (17+) | Content Script 대체 |
3. API 호환성
Safari 전용 제약사항
Safari는 Web Extension 표준을 지원하지만 보안/프라이버시 정책이 더 엄격합니다.
Service Worker (Background) 제한
| 항목 | Chrome | Safari |
|---|---|---|
| 지속 시간 | 비활성 30초 후 종료 | 비활성 수초 내 적극 종료 |
| Wake-up | 이벤트로 깨어남 | 이벤트로 깨어남 (지연 가능) |
| Persistent 상태 | storage.session | storage.session (16.4+), 이전 버전 미지원 |
| WebSocket | 지원 | 비활성 시 연결 끊김 |
| setTimeout/setInterval | 30초 내 실행 보장 | 보장하지 않음 (적극 종료) |
// Safari Service Worker 생존 전략
// ❌ 잘못된 패턴: 상태를 메모리에 유지
let cachedData: unknown = null; // Service Worker 종료 시 소멸
// ✅ 올바른 패턴: storage 활용
async function getData(): Promise<unknown> {
const result = await browser.storage.session.get('cachedData');
if (result.cachedData) return result.cachedData;
const fresh = await fetchFromServer();
await browser.storage.session.set({ cachedData: fresh });
return fresh;
}
// ✅ 올바른 패턴: alarms API로 주기 작업
browser.alarms.create('sync', { periodInMinutes: 5 });
browser.alarms.onAlarm.addListener(async (alarm) => {
if (alarm.name === 'sync') {
await performSync();
}
});
declarativeNetRequest 차이
| 항목 | Chrome | Safari |
|---|---|---|
| 정적 규칙 수 | 최대 330,000 | 최대 50,000 |
| 동적 규칙 수 | 최대 30,000 | 최대 30,000 |
| 세션 규칙 수 | 최대 5,000 | 최대 5,000 |
modifyHeaders 액션 | O | 제한적 (17+) |
redirect 액션 | O | O |
block 액션 | O | O |
| Regex 필터 | O (1,000개) | O (제한적) |
// Safari 호환 declarativeNetRequest 규칙 예시
{
"id": 1,
"priority": 1,
"action": { "type": "block" },
"condition": {
"urlFilter": "||tracker.example.com",
"resourceTypes": ["script", "image", "xmlhttprequest"]
}
}
storage API 차이
| 항목 | Chrome | Safari |
|---|---|---|
storage.local | O | O |
storage.sync | O (동기화) | O (동기화 미지원, local처럼 동작) |
storage.session | O | O (16.4+) |
storage.managed | O | X |
local 용량 | 10MB | 기본 5MB (Info.plist에서 증가 가능) |
// Safari storage.sync 대응
// Safari에서 storage.sync는 iCloud 동기화를 하지 않음
// Chrome/Firefox와 달리 로컬에만 저장됨
// 크로스 브라우저 안전한 패턴
async function saveSettings(settings: Record<string, unknown>): Promise<void> {
// storage.sync 사용 (Safari에서는 로컬 폴백)
await browser.storage.sync.set(settings);
}
// 용량 초과 방지: Safari의 낮은 quota 대응
async function saveWithQuotaCheck(
key: string,
value: unknown,
): Promise<void> {
try {
await browser.storage.local.set({ [key]: value });
} catch (error) {
if ((error as Error).message.includes('QUOTA_BYTES')) {
// 오래된 데이터 정리 후 재시도
await cleanupOldData();
await browser.storage.local.set({ [key]: value });
} else {
throw error;
}
}
}
webRequest 제한
| 항목 | Chrome (MV3) | Safari |
|---|---|---|
webRequest (관찰) | O | O (제한적) |
webRequest (차단/수정) | X (MV3에서 제거) | X |
webRequestBlocking | X | X |
declarativeNetRequest 대체 | O | O (규칙 수 제한) |
Safari는 Chrome MV3와 유사하게
webRequest의 차단/수정 기능을 제거했습니다.declarativeNetRequest를 사용하되, 규칙 수 제한에 유의하세요.
browser vs chrome 네임스페이스
Safari는 browser.* (Promise 기반)와 chrome.* (콜백 기반) 모두 지원합 니다.
| 네임스페이스 | Chrome | Firefox | Safari |
|---|---|---|---|
chrome.* | O (기본) | O (호환) | O |
browser.* | X (polyfill 필요) | O (기본) | O |
// Safari에서 권장: browser.* (Promise 기반)
const tabs = await browser.tabs.query({ active: true, currentWindow: true });
// WXT 사용 시 자동 처리
import { browser } from 'wxt/browser';
const data = await browser.storage.local.get('key');
미지원 API 전체 목록 및 대체 방안
| 미지원 API | 용도 | 대체 방안 |
|---|---|---|
chrome.sidePanel | 사이드 패널 UI | Popover + 별도 탭에서 확장 페이지 열기 |
chrome.offscreen | 백그라운드 DOM 접근 | Content Script에서 DOMParser 사용 |
chrome.tabGroups | 탭 그룹 관리 | 없음 (Safari에 해당 기능 자체 없음) |
chrome.debugger | 디버깅 API | 없음 (Safari Web Inspector 사용) |
chrome.enterprise.* | 기업 관리 | MDM 프로파일로 대체 |
chrome.gcm / chrome.instanceID | Google 푸시 알림 | APNs (Apple Push Notification) |
chrome.tts | 텍스트 음성 변환 | Web Speech API (speechSynthesis) |
chrome.printing | 인쇄 | window.print() |
chrome.downloads.open | 다운로드 파일 열 기 | 다운로드만 가능, 열기는 미지원 |
chrome.browsingData | 브라우징 데이터 삭제 | 없음 |
chrome.fontSettings | 폰트 설정 | 없음 |
chrome.proxy | 프록시 설정 | 없음 (시스템 설정 사용) |
크로스 브라우저 호환 래퍼 패턴
// lib/compat/safari-compat.ts
/**
* Safari에서 미지원 API에 대한 graceful 처리
*/
export function isSafari(): boolean {
return /^((?!chrome|android).)*safari/i.test(navigator.userAgent)
|| (typeof browser !== 'undefined'
&& browser.runtime.getURL('').startsWith('safari-web-extension://'));
}
/**
* Side Panel 대체: Safari에서는 새 탭으로 열기
*/
export async function openSideUI(path: string): Promise<void> {
if ('sidePanel' in chrome) {
const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
await chrome.sidePanel.open({ tabId: tab.id! });
} else {
// Safari fallback: 확장 내 페이지를 새 탭으로 열기
await browser.tabs.create({
url: browser.runtime.getURL(path),
});
}
}
/**
* Offscreen Document 대체: Safari에서는 직접 DOM API 사용
*/
export async function parseHTML(html: string): Promise<string> {
if ('offscreen' in chrome) {
// Chrome: Offscreen Document 경유
return await parseViaOffscreen(html);
}
// Safari/Firefox: DOMParser 직접 사용
const parser = new DOMParser();
const doc = parser.parseFromString(html, 'text/html');
return doc.body.textContent ?? '';
}
4. Apple Developer Program
등록 절차
| 단계 | 작업 | 비용 |
|---|---|---|
| 1 | Apple Developer 사이트 접속 | - |
| 2 | Apple ID로 로그인 (없으면 생성) | 무료 |
| 3 | Developer Program 등록 신청 | - |
| 4 | 연회비 결제 | $99/년 |
| 5 | 본인 확인 (개인: 신분증, 조직: D-U-N-S 번호) | - |
| 6 | 승인 대기 (개인: 24-48시간, 조직: 수 일-수 주) | - |
개인 vs 조직 계정
| 항목 | 개인 (Individual) | 조직 (Organization) |
|---|---|---|
| 비용 | $99/년 | $99/년 |
| 필요 서류 | 정부 발급 신분증 | D-U-N-S 번호 + 법인 확인 |
| 등록 소요 | 24-48시간 | 수 일-수 주 (D-U-N-S 발급 포함) |
| App Store 표시 | 개발자 이름 | 회사명 |
| 팀 관리 | 1인만 | 팀원 초대 가능 (역할: Admin, Developer, Marketing) |
| 앱 양도 | 제한적 | O |
| 추천 | 개인/사이드 프로젝트 | 상용 서비스, 기업 배포 |
D-U-N-S 번호: Dun & Bradstreet에서 발급하는 기업 식별 번호. Apple은 무료 발급 지원: D-U-N-S 번호 조회/신청 발급까지 5-10 영업일 소요.
인증서 및 프로비저닝 프로파일
Safari Extension은 네이티브 앱으로 래핑되므로 Apple의 코드 서명 체계를 따릅니다.