Skip to main content

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 참조


목차

  1. Safari Web Extension 개요
  2. 프로젝트 변환 및 설정
  3. API 호환성
  4. Apple Developer Program
  5. App Store 심사
  6. macOS + iOS 동시 지원
  7. 개발 및 디버깅
  8. CI/CD (Xcode Cloud / GitHub Actions)
  9. 비용 및 의사결정 가이드
  10. 체크리스트

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/FirefoxSafari
배포 방식스토어에 ZIP 업로드네이티브 앱으로 래핑 → App Store 제출
빌드 도구Node.js만으로 충분Xcode 필수 (macOS에서만 가능)
심사 주체각 스토어 자체 심사Apple App Review (앱 심사와 동일)
코드 서명불필요Apple 인증서 + 프로비저닝 프로파일 필수
확장 활성화설치 즉시 활성사용자가 Safari 설정에서 수동 활성화

Safari 버전별 Web Extension 지원

Safari 버전macOSiOS주요 추가 기능
14Big Sur14Safari App Extension (Legacy, 비 Web Extension)
15Monterey15Web Extension 최초 지원 (MV2)
15.4Monterey 12.315.4Manifest V3 지원
16Ventura16declarativeNetRequest, scripting 개선
16.4Ventura 13.316.4Service Worker 안정화, storage.session
17Sonoma17userScripts, Profile별 확장, storage.session 개선
18Sequoia18Web 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>번들 식별자자동 생성
--swiftSwift 기반 앱 프로젝트Obj-C
--macos-onlymacOS 전용 (iOS 제외)양쪽 모두
--ios-onlyiOS 전용 (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: 3O15.4+
actionOpopup, icon, badge
background.service_workerO16.4+ 안정화
background.scriptsOEvent Page 방식 (MV2 호환)
content_scriptsO
permissionsO지원 범위 제한 있음
host_permissionsO
options_uiOopen_in_tab: true 권장
web_accessible_resourcesO
commandsO키보드 단축키
default_locale + _locales/O다국어
declarative_net_requestO16+
content_security_policyO

미지원 또는 제한적인 Manifest 키

Manifest 키상태대안
side_panelX별도 탭 또는 popover
offscreenXContent Script에서 DOM API 사용
devtools_pageXSafari Web Inspector 자체 사용
chrome_url_overridesX없음
tab_groupsX없음
user_scripts제한적 (17+)Content Script 대체

3. API 호환성

Safari 전용 제약사항

Safari는 Web Extension 표준을 지원하지만 보안/프라이버시 정책이 더 엄격합니다.

Service Worker (Background) 제한

항목ChromeSafari
지속 시간비활성 30초 후 종료비활성 수초 내 적극 종료
Wake-up이벤트로 깨어남이벤트로 깨어남 (지연 가능)
Persistent 상태storage.sessionstorage.session (16.4+), 이전 버전 미지원
WebSocket지원비활성 시 연결 끊김
setTimeout/setInterval30초 내 실행 보장보장하지 않음 (적극 종료)
// 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 차이

항목ChromeSafari
정적 규칙 수최대 330,000최대 50,000
동적 규칙 수최대 30,000최대 30,000
세션 규칙 수최대 5,000최대 5,000
modifyHeaders 액션O제한적 (17+)
redirect 액션OO
block 액션OO
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 차이

항목ChromeSafari
storage.localOO
storage.syncO (동기화)O (동기화 미지원, local처럼 동작)
storage.sessionOO (16.4+)
storage.managedOX
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 (관찰)OO (제한적)
webRequest (차단/수정)X (MV3에서 제거)X
webRequestBlockingXX
declarativeNetRequest 대체OO (규칙 수 제한)

Safari는 Chrome MV3와 유사하게 webRequest의 차단/수정 기능을 제거했습니다. declarativeNetRequest를 사용하되, 규칙 수 제한에 유의하세요.

browser vs chrome 네임스페이스

Safari는 browser.* (Promise 기반)와 chrome.* (콜백 기반) 모두 지원합니다.

네임스페이스ChromeFirefoxSafari
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사이드 패널 UIPopover + 별도 탭에서 확장 페이지 열기
chrome.offscreen백그라운드 DOM 접근Content Script에서 DOMParser 사용
chrome.tabGroups탭 그룹 관리없음 (Safari에 해당 기능 자체 없음)
chrome.debugger디버깅 API없음 (Safari Web Inspector 사용)
chrome.enterprise.*기업 관리MDM 프로파일로 대체
chrome.gcm / chrome.instanceIDGoogle 푸시 알림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

등록 절차

단계작업비용
1Apple Developer 사이트 접속-
2Apple ID로 로그인 (없으면 생성)무료
3Developer 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의 코드 서명 체계를 따릅니다.

인증서 종류

인증서용도Xcode 자동 관리
Apple Development개발/테스트용 빌드 서명O
Apple DistributionApp Store / Ad Hoc 배포O
Mac App DistributionMac App Store 제출O
Mac Installer Distributionpkg 인스톨러 서명O
Developer ID ApplicationMac 앱 직접 배포 (Notarization)X (수동)

Xcode 자동 서명 (권장)

Xcode → Project → Signing & Capabilities

[x] Automatically manage signing
Team: [Your Team]
Bundle Identifier: com.example.myextension

→ Xcode가 인증서, App ID, 프로비저닝 프로파일을 자동 생성/관리

수동 서명 (CI/CD용)

# Keychain에 인증서 설치
security create-keychain -p "" build.keychain
security import distribution.p12 -k build.keychain -P "$CERT_PASSWORD" -T /usr/bin/codesign
security set-key-partition-list -S apple-tool:,apple: -s -k "" build.keychain

# 프로비저닝 프로파일 설치
mkdir -p ~/Library/MobileDevice/Provisioning\ Profiles
cp *.mobileprovision ~/Library/MobileDevice/Provisioning\ Profiles/
cp *.provisionprofile ~/Library/MobileDevice/Provisioning\ Profiles/

macOS + iOS 동시 배포 설정

하나의 Xcode 프로젝트에서 macOS 앱과 iOS 앱을 동시에 생성합니다.

Xcode → Project → Targets

+-- My Extension (macOS) # macOS 컨테이너 앱
+-- My Extension Extension (macOS) # macOS Safari Extension
+-- My Extension (iOS) # iOS 컨테이너 앱
+-- My Extension Extension (iOS) # iOS Safari Extension

각 타겟에 별도의 Bundle Identifier가 필요합니다:

타겟Bundle ID예시
macOS Appcom.example.myextension컨테이너 앱
macOS Extensioncom.example.myextension.extension확장
iOS Appcom.example.myextension.ios컨테이너 앱
iOS Extensioncom.example.myextension.ios.extension확장

5. App Store 심사 (Safari Extension)

Safari Extension의 배포 경로

Safari Extension은 독립 배포가 불가능합니다. 반드시 네이티브 앱(컨테이너 앱)에 포함하여 App Store를 통해 배포해야 합니다.

빌드된 웹 확장 코드
↓ (Xcode 프로젝트에 포함)
네이티브 앱 + Safari Extension 번들
↓ (Archive → Export)
App Store Connect 업로드
↓ (심사)
Mac App Store / iOS App Store 게시
↓ (사용자 다운로드)
Safari 환경설정에서 확장 활성화

App Store Review Guidelines 중 확장 관련 항목

가이드라인핵심 내용
4.0 Design확장 활성화 방법을 앱 내에서 명확히 안내해야 함
4.2 Minimum Functionality컨테이너 앱이 "확장 활성화 안내"만 있으면 거부 위험. 최소한의 독립 기능 필요
2.5.1 Software Requirementspublic API만 사용, private API 호출 금지
5.1.1 Data Collection수집하는 모든 데이터를 프라이버시 라벨에 명시
5.1.2 Data Use and Sharing추적 목적의 데이터 수집 시 ATT(App Tracking Transparency) 필수
2.3.1 Accurate Metadata스크린샷, 설명, 키워드가 실제 기능과 일치해야 함

거부 사유 Top 5 (Safari Extension 특화)

1. 컨테이너 앱 기능 부족 (Guideline 4.2)

❌ 거부: 컨테이너 앱이 "Safari에서 확장을 활성화하세요"라는 안내만 표시
✅ 대응: 컨테이너 앱에 독립적으로 유용한 기능 추가
- 확장 설정/대시보드
- 저장된 데이터 조회
- 온보딩 튜토리얼
- 확장 상태 모니터링
// ✅ 컨테이너 앱: 확장 활성화 상태 확인 + 설정 UI
import SafariServices

class ViewController: NSViewController {
override func viewDidLoad() {
super.viewDidLoad()
checkExtensionState()
}

func checkExtensionState() {
SFSafariExtensionManager.getStateOfSafariExtension(
withIdentifier: "com.example.myextension.extension"
) { state, error in
DispatchQueue.main.async {
if let state = state {
self.updateUI(isEnabled: state.isEnabled)
}
}
}
}
}

2. 프라이버시 라벨 불일치 (Guideline 5.1.1)

❌ 거부: 확장이 페이지 URL을 서버로 전송하지만 프라이버시 라벨에 미고지
✅ 대응: App Store Connect에서 프라이버시 라벨 정확히 작성
- 수집하는 모든 데이터 유형 체크
- 수집 목적 (기능 제공, 분석, 광고 등) 명시
- 추적 여부 정확히 표시

3. 콘텐츠 차단 규칙 (Content Blocker 관련)

❌ 거부: Content Blocker로 특정 회사/서비스를 차별적으로 차단
✅ 대응: 차단 규칙이 합리적이고 투명하게 작동
- 사용자가 차단 규칙 확인/수정 가능
- 카테고리 기반의 일반적 차단 규칙

4. 불완전한 기능 (Guideline 2.1)

❌ 거부: 특정 Safari 버전에서 크래시, 빈 팝업, 동작 안 하는 기능
✅ 대응: 최소 지원 버전에서 철저히 테스트
- macOS + iOS 모두 테스트
- Safari 환경설정에서 활성화/비활성화 반복 테스트
- 네트워크 오프라인 상태 처리

5. 메타데이터 부정확 (Guideline 2.3)

❌ 거부: 스크린샷이 실제 UI와 다름, 키워드에 경쟁사 이름 포함
✅ 대응:
- 실제 동작하는 화면의 스크린샷 사용
- 설명에 확장 기능 정확히 기술
- 관련 없는 키워드 사용 금지

프라이버시 라벨 작성

App Store Connect에서 프라이버시 라벨을 작성해야 합니다.

데이터 유형별 체크항목

데이터 유형예시수집 여부 확인
Contact Info이메일, 이름, 전화번호로그인/계정 기능이 있는 경우
Health & Fitness건강/운동 데이터해당 없음 (대부분)
Financial Info결제 정보인앱 결제/구독 시
Location위치 데이터GPS/IP 기반 기능 시
Sensitive Info인종, 성적 지향 등해당 없음 (대부분)
Contacts주소록주소록 접근 시
User Content이메일/문자/사진/비디오콘텐츠 접근 시
Browsing History방문 사이트 목록tabs 또는 history 권한 사용 시
Search History검색어검색 기능 시
IdentifiersDevice ID, User ID분석/추적 시
Usage Data앱 사용 패턴분석 기능 포함 시
Diagnostics크래시 로그, 성능 데이터크래시 리포팅 시

작성 원칙

각 데이터 유형에 대해:
1. 수집 여부 → Yes/No
2. 사용자 연결 여부 → Linked to User Identity / Not Linked
3. 추적 여부 → Used for Tracking / Not Used for Tracking
4. 수집 목적 → App Functionality / Analytics / Advertising / ...

스크린샷 규격

Mac App Store

디스플레이해상도필수
16인치 MacBook Pro2880x1800권장
13인치 MacBook Pro2560x1600O (최소 1장)
iMac (27인치)2560x1440선택

iOS App Store

디바이스해상도필수
iPhone 15 Pro Max (6.7")1290x2796O
iPhone 15 Pro (6.1")1179x2556O
iPad Pro 12.9" (6세대)2048x2732O (iPad 지원 시)
iPad Pro 11"1668x2388선택

스크린샷 가이드

✅ 권장:
- Safari 브라우저 내에서 확장이 동작하는 모습
- 팝업이 열린 상태의 스크린샷
- macOS: Safari 툴바의 확장 아이콘이 보이도록
- iOS: Safari의 확장 메뉴(aA → Extensions)에서 활성화하는 모습
- 주요 기능별 1장씩

❌ 회피:
- Chrome/Firefox에서 찍은 스크린샷
- 확장과 무관한 일반 웹페이지 스크린샷
- 모킹/가상 UI 이미지

6. macOS + iOS 동시 지원

Shared Extension 폴더 구조

safari-web-extension-converter는 macOS/iOS 공유 리소스를 Shared (Extension)/Resources/에 배치합니다.

safari-xcode/
+-- Shared (Extension)/ # macOS + iOS 공유 확장 코드
| +-- Resources/ # 웹 확장 리소스 (manifest.json, JS, HTML...)
| | +-- manifest.json
| | +-- background.js
| | +-- popup.html
| | +-- popup.js
| | +-- content-script.js
| | +-- icons/
| | +-- _locales/
| +-- SafariWebExtensionHandler.swift # 네이티브 메시징 (양쪽 공유)
|
+-- My Extension (macOS)/ # macOS 전용 컨테이너 앱
| +-- AppDelegate.swift
| +-- ViewController.swift # macOS: 확장 활성화 안내 UI
| +-- Main.storyboard
| +-- Assets.xcassets/ # macOS 앱 아이콘
|
+-- My Extension (iOS)/ # iOS 전용 컨테이너 앱
| +-- AppDelegate.swift
| +-- ViewController.swift # iOS: 확장 활성화 안내 UI
| +-- Main.storyboard
| +-- Assets.xcassets/ # iOS 앱 아이콘

iOS Safari 제약사항

iOS Safari의 Web Extension은 macOS에 비해 상당히 제한적입니다.

기능macOS SafariiOS Safari비고
Popup (toolbar icon 클릭)O제한적iOS는 주소창 aA 메뉴에서 접근
Toolbar 아이콘OX (iOS 16+에서 제한적)iOS는 aA 메뉴 내 표시
Background ScriptOO동일하게 적극 종료
Content ScriptOO
tabs APIO제한적일부 이벤트 미지원
windows APIOXiOS에 윈도우 개념 없음
contextMenusOXiOS에 우클릭 없음
commands (키보드 단축키)OXiOS에 키보드 단축키 제한적
devtoolsOX
확장 활성화Safari 환경설정설정 앱 → Safari → 확장경로가 다름
Badge TextO제한적iOS 16.4+

iOS 팝업 접근 경로

iOS Safari에서 확장 접근 방법:
1. 주소창 좌측 "aA" 버튼 탭
2. "확장 프로그램 관리" 선택 (최초)
3. 확장 활성화
4. 이후 "aA" → 확장 이름 탭 → 팝업 UI 표시

→ Chrome/Firefox의 툴바 아이콘 클릭과 달리 접근성이 낮음
→ 온보딩에서 이 과정을 명확히 안내해야 함

iOS 전용 대응 코드

// iOS Safari 감지
function isIOSSafari(): boolean {
return /iPad|iPhone|iPod/.test(navigator.userAgent)
&& /Safari/.test(navigator.userAgent)
&& !/Chrome|CriOS|FxiOS/.test(navigator.userAgent);
}

// iOS에서 미지원 기능 분기
async function setupExtension(): Promise<void> {
if (isIOSSafari()) {
// contextMenus 미지원: 팝업 내 메뉴로 대체
// commands 미지원: 팝업 내 버튼으로 대체
// windows API 미지원: tabs만 사용
setupIOSUI();
} else {
setupDesktopUI();
setupContextMenus();
setupKeyboardShortcuts();
}
}

// iOS 팝업 UI: 더 크고 터치 친화적으로
function setupIOSUI(): void {
document.documentElement.classList.add('ios-safari');
}
/* iOS Safari 팝업 최적화 */
.ios-safari {
/* 터치 타겟 44px 이상 (Apple HIG) */
--min-touch-target: 44px;
/* iOS 팝업은 전체 너비 */
--popup-width: 100vw;
--popup-max-width: 400px;
}

.ios-safari button,
.ios-safari a {
min-height: var(--min-touch-target);
min-width: var(--min-touch-target);
padding: 12px 16px;
}

네이티브 메시징 (SafariWebExtensionHandler)

Safari Extension의 고유 기능으로, JavaScript와 네이티브 Swift 코드 간 통신이 가능합니다.

// SafariWebExtensionHandler.swift
import SafariServices
import os.log

class SafariWebExtensionHandler: NSObject, NSExtensionRequestHandling {

func beginRequest(with context: NSExtensionContext) {
let item = context.inputItems[0] as! NSExtensionItem
let message = item.userInfo?[SFExtensionMessageKey] as? [String: Any]

guard let action = message?["action"] as? String else {
context.completeRequest(returningItems: nil, completionHandler: nil)
return
}

switch action {
case "getAppVersion":
let version = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String
let response = NSExtensionItem()
response.userInfo = [SFExtensionMessageKey: ["version": version ?? "unknown"]]
context.completeRequest(returningItems: [response], completionHandler: nil)

case "openApp":
// 컨테이너 앱 열기
// macOS: NSWorkspace, iOS: URL Scheme
let response = NSExtensionItem()
response.userInfo = [SFExtensionMessageKey: ["success": true]]
context.completeRequest(returningItems: [response], completionHandler: nil)

default:
context.completeRequest(returningItems: nil, completionHandler: nil)
}
}
}
// JavaScript에서 네이티브 메시징 호출
async function callNative(
message: Record<string, unknown>,
): Promise<unknown> {
return new Promise((resolve) => {
browser.runtime.sendNativeMessage(
'com.example.myextension',
message,
(response) => resolve(response),
);
});
}

// 사용 예시
const result = await callNative({ action: 'getAppVersion' });
console.log('App version:', result.version);

App Extension 라이프사이클

이벤트macOSiOS
앱 설치App Store에서 다운로드App Store에서 다운로드
확장 활성화Safari 환경설정 → 확장설정 → Safari → 확장
확장 비활성화Safari 환경설정에서 해제설정에서 해제
앱 삭제앱 삭제 시 확장도 제거앱 삭제 시 확장도 제거
앱 업데이트App Store 자동 업데이트App Store 자동 업데이트
백그라운드 종료메모리 압박 시적극적 종료 (iOS 더 빈번)

7. 개발 및 디버깅

Safari 개발자 도구 활성화

macOS:
1. Safari → Settings (⌘,) → Advanced
2. "Show features for web developers" 체크
3. 메뉴바에 "Develop" 메뉴 표시됨

확장 개발 모드 활성화

Safari → Develop → Allow Unsigned Extensions (매 실행마다 활성화 필요)
→ 서명되지 않은 로컬 빌드를 테스트할 수 있음

또는:
Safari → Settings → Advanced → "Show features for web developers"
→ Develop 메뉴에서 개발 중 확장 로드

이 옵션은 Safari를 닫았다 열 때마다 리셋됩니다. 매번 다시 활성화해야 합니다.

Xcode에서 확장 실행

Xcode:
1. Scheme에서 확장 앱 선택
2. ⌘R (Run) → 컨테이너 앱 실행 + Safari에 확장 로드
3. Safari → Settings → Extensions → 확장 활성화

Web Extension Background Page 디버깅

Safari → Develop → Web Extension Background Content
→ 확장의 Background Page (Service Worker) Inspector 열림

Inspector에서:
- Console: console.log 확인
- Network: 확장의 네트워크 요청 확인
- Sources: 브레이크포인트 설정
- Storage: extension storage 내용 확인

Content Script 디버깅

Safari → Develop → [현재 페이지]
→ Web Inspector에서 Content Script 소스 확인 가능

또는:
Develop → Web Extension Background Content
→ Content Script의 console.log는 여기에 표시
1. Safari 툴바에서 확장 아이콘 클릭 (팝업 열기)
2. 팝업이 열린 상태에서:
Develop → [확장 이름] → [popup.html]
3. Web Inspector 열림 → 팝업 UI 디버깅

iOS 시뮬레이터에서 확장 테스트

1. Xcode에서 iOS 타겟 선택 (Scheme)
2. Simulator 선택 (iPhone 15 Pro 등)
3. ⌘R → 시뮬레이터에서 컨테이너 앱 실행
4. 시뮬레이터에서 Safari 열기
5. 설정 → Safari → 확장 → 확장 활성화
6. Safari에서 테스트 페이지로 이동

디버깅:
- Mac의 Safari → Develop → Simulator → [페이지]
→ iOS Safari의 Web Inspector를 Mac에서 열 수 있음

실제 iOS 기기에서 테스트

1. iPhone/iPad를 USB로 Mac에 연결
2. iPhone → 설정 → Safari → 고급 → "Web Inspector" 활성화
3. Xcode에서 실제 기기 선택 → ⌘R
4. Mac Safari → Develop → [기기 이름] → [페이지]
→ 실제 기기의 Web Inspector를 Mac에서 열 수 있음

console.log 확인 위치 정리

컨텍스트확인 위치
Background (Service Worker)Develop → Web Extension Background Content
Content ScriptDevelop → [해당 웹페이지] (또는 Background Inspector에도 표시)
PopupDevelop → [확장 이름] → popup.html
Options PageDevelop → [확장 이름] → options.html
네이티브 핸들러 (Swift)Xcode Console (os.log 또는 print)

자주 발생하는 디버깅 이슈

증상원인해결
확장이 목록에 안 보임Allow Unsigned Extensions 미활성화Develop 메뉴에서 활성화
Content Script 미주입Safari가 확장 권한을 제한Safari 설정에서 "모든 웹사이트" 접근 허용
Background 즉시 종료Safari의 적극적 Service Worker 종료storage.session + alarms API 활용
팝업이 안 열림action.default_popup 경로 오류manifest.json 경로 확인
API 호출 실패CSP 또는 host_permissions 누락manifest.json 권한 확인

8. CI/CD (Xcode Cloud / GitHub Actions)

방법 1: Xcode Cloud

Apple이 제공하는 네이티브 CI/CD 서비스. App Store Connect와 직접 통합됩니다.

Xcode Cloud 설정

Xcode → Product → Xcode Cloud → Create Workflow

Workflow 구성:
1. Source: Git 리포지토리 연결
2. Environment: Xcode 15+, macOS 14+
3. Build Actions:
- Archive (macOS)
- Archive (iOS)
4. Post Actions:
- Upload to TestFlight
- (또는) Submit to App Review

트리거:
- Push to main branch
- Tag (v* 패턴)
- Manual

사전 빌드 스크립트 (ci_scripts/)

Xcode Cloud는 ci_scripts/ 폴더의 스크립트를 자동 실행합니다.

#!/bin/bash
# ci_scripts/ci_pre_xcodebuild.sh
# Xcode Cloud에서 빌드 전 실행

set -euo pipefail

# Node.js 설치 (웹 확장 빌드에 필요)
brew install node@20

# 의존성 설치 + Safari 빌드
cd "$CI_PRIMARY_REPOSITORY_PATH/platform/client/browser-ext"
npm ci
npm run build -- --browser safari

# Xcode 프로젝트의 Shared Extension 리소스 업데이트
rsync -av --delete \
.output/safari-mv3/ \
"$CI_PRIMARY_REPOSITORY_PATH/safari-xcode/Shared (Extension)/Resources/"

echo "Safari extension resources updated."
#!/bin/bash
# ci_scripts/ci_post_xcodebuild.sh
# 빌드 후 실행 (선택)

echo "Build completed. Archive: $CI_ARCHIVE_PATH"

Xcode Cloud 비용

티어컴퓨트 시간비용
Free25시간/월무료
Pay as you go초과분$0.08/분

방법 2: GitHub Actions + xcodebuild

macOS runner를 사용하여 GitHub Actions에서 직접 빌드합니다.

# .github/workflows/release-safari.yml
name: Release Safari Extension

on:
push:
tags:
- 'v*'

permissions:
contents: read

env:
XCODE_PROJECT: safari-xcode/My Extension.xcodeproj
MACOS_SCHEME: "My Extension (macOS)"
IOS_SCHEME: "My Extension (iOS)"

jobs:
build-web-extension:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
cache-dependency-path: platform/client/browser-ext/package-lock.json

- name: Install and build
run: |
cd platform/client/browser-ext
npm ci
npm run build -- --browser safari

- uses: actions/upload-artifact@v4
with:
name: safari-build
path: platform/client/browser-ext/.output/safari-mv3/

build-and-deploy:
needs: build-web-extension
runs-on: macos-14 # Apple Silicon runner (M1)
timeout-minutes: 30

steps:
- uses: actions/checkout@v4

- uses: actions/download-artifact@v4
with:
name: safari-build
path: safari-xcode/Shared (Extension)/Resources/

- name: Select Xcode version
run: sudo xcode-select -s /Applications/Xcode_15.4.app

- name: Install certificates
env:
DISTRIBUTION_CERT_BASE64: ${{ secrets.APPLE_DISTRIBUTION_CERT_BASE64 }}
DISTRIBUTION_CERT_PASSWORD: ${{ secrets.APPLE_DISTRIBUTION_CERT_PASSWORD }}
KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }}
run: |
# Keychain 생성
security create-keychain -p "$KEYCHAIN_PASSWORD" build.keychain
security default-keychain -s build.keychain
security unlock-keychain -p "$KEYCHAIN_PASSWORD" build.keychain
security set-keychain-settings -t 3600 build.keychain

# 인증서 설치
echo "$DISTRIBUTION_CERT_BASE64" | base64 --decode > cert.p12
security import cert.p12 -k build.keychain \
-P "$DISTRIBUTION_CERT_PASSWORD" \
-T /usr/bin/codesign \
-T /usr/bin/security
security set-key-partition-list -S apple-tool:,apple: \
-s -k "$KEYCHAIN_PASSWORD" build.keychain

rm cert.p12

- name: Install provisioning profiles
env:
MACOS_APP_PROFILE_BASE64: ${{ secrets.MACOS_APP_PROFILE_BASE64 }}
MACOS_EXT_PROFILE_BASE64: ${{ secrets.MACOS_EXT_PROFILE_BASE64 }}
IOS_APP_PROFILE_BASE64: ${{ secrets.IOS_APP_PROFILE_BASE64 }}
IOS_EXT_PROFILE_BASE64: ${{ secrets.IOS_EXT_PROFILE_BASE64 }}
run: |
mkdir -p ~/Library/MobileDevice/Provisioning\ Profiles
echo "$MACOS_APP_PROFILE_BASE64" | base64 --decode > ~/Library/MobileDevice/Provisioning\ Profiles/macos_app.provisionprofile
echo "$MACOS_EXT_PROFILE_BASE64" | base64 --decode > ~/Library/MobileDevice/Provisioning\ Profiles/macos_ext.provisionprofile
echo "$IOS_APP_PROFILE_BASE64" | base64 --decode > ~/Library/MobileDevice/Provisioning\ Profiles/ios_app.mobileprovision
echo "$IOS_EXT_PROFILE_BASE64" | base64 --decode > ~/Library/MobileDevice/Provisioning\ Profiles/ios_ext.mobileprovision

- name: Archive macOS
run: |
xcodebuild archive \
-project "$XCODE_PROJECT" \
-scheme "$MACOS_SCHEME" \
-archivePath build/macOS.xcarchive \
-destination "generic/platform=macOS" \
CODE_SIGN_STYLE=Manual \
DEVELOPMENT_TEAM=${{ secrets.APPLE_TEAM_ID }}

- name: Archive iOS
run: |
xcodebuild archive \
-project "$XCODE_PROJECT" \
-scheme "$IOS_SCHEME" \
-archivePath build/iOS.xcarchive \
-destination "generic/platform=iOS" \
CODE_SIGN_STYLE=Manual \
DEVELOPMENT_TEAM=${{ secrets.APPLE_TEAM_ID }}

- name: Export macOS archive
run: |
cat > ExportOptions-macOS.plist << 'PLIST'
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>method</key>
<string>app-store</string>
<key>teamID</key>
<string>${{ secrets.APPLE_TEAM_ID }}</string>
<key>uploadSymbols</key>
<true/>
</dict>
</plist>
PLIST

xcodebuild -exportArchive \
-archivePath build/macOS.xcarchive \
-exportOptionsPlist ExportOptions-macOS.plist \
-exportPath build/export-macOS

- name: Export iOS archive
run: |
cat > ExportOptions-iOS.plist << 'PLIST'
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>method</key>
<string>app-store</string>
<key>teamID</key>
<string>${{ secrets.APPLE_TEAM_ID }}</string>
<key>uploadSymbols</key>
<true/>
</dict>
</plist>
PLIST

xcodebuild -exportArchive \
-archivePath build/iOS.xcarchive \
-exportOptionsPlist ExportOptions-iOS.plist \
-exportPath build/export-iOS

- name: Upload to App Store Connect
env:
APP_STORE_CONNECT_API_KEY_ID: ${{ secrets.ASC_KEY_ID }}
APP_STORE_CONNECT_ISSUER_ID: ${{ secrets.ASC_ISSUER_ID }}
APP_STORE_CONNECT_API_KEY: ${{ secrets.ASC_PRIVATE_KEY }}
run: |
# API Key 파일 생성
mkdir -p ~/.appstoreconnect/private_keys
echo "$APP_STORE_CONNECT_API_KEY" > \
~/.appstoreconnect/private_keys/AuthKey_${APP_STORE_CONNECT_API_KEY_ID}.p8

# macOS 업로드
xcrun altool --upload-app \
-f build/export-macOS/*.pkg \
--type macos \
--apiKey "$APP_STORE_CONNECT_API_KEY_ID" \
--apiIssuer "$APP_STORE_CONNECT_ISSUER_ID"

# iOS 업로드
xcrun altool --upload-app \
-f build/export-iOS/*.ipa \
--type ios \
--apiKey "$APP_STORE_CONNECT_API_KEY_ID" \
--apiIssuer "$APP_STORE_CONNECT_ISSUER_ID"

- name: Cleanup keychain
if: always()
run: security delete-keychain build.keychain

GitHub Actions 필수 시크릿

시크릿설명발급 위치
APPLE_TEAM_IDApple Developer Team IDDeveloper Portal → Membership
APPLE_DISTRIBUTION_CERT_BASE64배포 인증서 (p12, base64 인코딩)Keychain Access → Export
APPLE_DISTRIBUTION_CERT_PASSWORDp12 파일 비밀번호인증서 내보내기 시 설정
KEYCHAIN_PASSWORD임시 Keychain 비밀번호임의 설정
MACOS_APP_PROFILE_BASE64macOS 앱 프로비저닝 프로파일Developer Portal
MACOS_EXT_PROFILE_BASE64macOS 확장 프로비저닝 프로파일Developer Portal
IOS_APP_PROFILE_BASE64iOS 앱 프로비저닝 프로파일Developer Portal
IOS_EXT_PROFILE_BASE64iOS 확장 프로비저닝 프로파일Developer Portal
ASC_KEY_IDApp Store Connect API Key IDASC → Users → Keys
ASC_ISSUER_IDApp Store Connect Issuer IDASC → Users → Keys
ASC_PRIVATE_KEYApp Store Connect API Private Key (.p8)ASC → Users → Keys (1회 다운로드)

방법 3: fastlane 활용

fastlane은 빌드, 서명, 배포를 자동화하는 Ruby 기반 도구입니다.

fastlane 설정

# safari-xcode/fastlane/Fastfile

default_platform(:mac)

platform :mac do
desc "Build and upload macOS Safari Extension to TestFlight"
lane :beta do
# 웹 확장 빌드 (Node.js)
sh("cd ../../platform/client/browser-ext && npm ci && npm run build -- --browser safari")
sh("rsync -av --delete ../../platform/client/browser-ext/.output/safari-mv3/ '../Shared (Extension)/Resources/'")

# 자동 인증서 관리
match(type: "appstore", app_identifier: [
"com.example.myextension",
"com.example.myextension.extension"
])

# 버전 번호 증가
increment_build_number

# 빌드
build_mac_app(
scheme: "My Extension (macOS)",
export_method: "app-store"
)

# TestFlight 업로드
upload_to_testflight(
skip_waiting_for_build_processing: true
)
end

desc "Submit to App Store Review"
lane :release do
deliver(
submit_for_review: true,
automatic_release: false,
force: true
)
end
end

platform :ios do
desc "Build and upload iOS Safari Extension to TestFlight"
lane :beta do
match(type: "appstore", app_identifier: [
"com.example.myextension.ios",
"com.example.myextension.ios.extension"
])

increment_build_number

build_app(
scheme: "My Extension (iOS)",
export_method: "app-store"
)

upload_to_testflight(
skip_waiting_for_build_processing: true
)
end
end
# safari-xcode/fastlane/Matchfile
git_url("https://github.com/your-org/certificates.git")
storage_mode("git")

type("appstore")

app_identifier([
"com.example.myextension",
"com.example.myextension.extension",
"com.example.myextension.ios",
"com.example.myextension.ios.extension"
])

team_id("YOUR_TEAM_ID")

fastlane + GitHub Actions

# .github/workflows/release-safari-fastlane.yml
name: Release Safari (fastlane)

on:
push:
tags:
- 'v*'

jobs:
release:
runs-on: macos-14
steps:
- uses: actions/checkout@v4

- uses: actions/setup-node@v4
with:
node-version: '20'

- uses: ruby/setup-ruby@v1
with:
ruby-version: '3.2'
bundler-cache: true
working-directory: safari-xcode

- name: Run fastlane
working-directory: safari-xcode
env:
MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }}
MATCH_GIT_BASIC_AUTHORIZATION: ${{ secrets.MATCH_GIT_TOKEN }}
APP_STORE_CONNECT_API_KEY_ID: ${{ secrets.ASC_KEY_ID }}
APP_STORE_CONNECT_API_ISSUER_ID: ${{ secrets.ASC_ISSUER_ID }}
APP_STORE_CONNECT_API_KEY_CONTENT: ${{ secrets.ASC_PRIVATE_KEY }}
run: |
bundle exec fastlane mac beta
bundle exec fastlane ios beta

TestFlight 베타 배포

TestFlight를 통해 앱 심사 전에 테스터에게 배포할 수 있습니다.

TestFlight 절차

단계작업자동화 가능
1Archive + ExportO (xcodebuild / fastlane)
2App Store Connect 업로드O (altool / fastlane)
3처리 대기 (5-30분)자동
4내부 테스터 자동 배포 (25명)자동
5외부 테스터 배포 (10,000명)수동 (Beta App Review 필요)
6테스터 피드백 수집App Store Connect 확인

내부 vs 외부 테스터

구분내부 테스터외부 테스터
인원최대 25명최대 10,000명
자격App Store Connect 팀원이메일 초대 / 공개 링크
심사불필요 (자동 배포)Beta App Review 필요 (1-2일)
빌드 유효기간90일90일

9. 비용 및 의사결정 가이드

Safari 추가 시 비용 요소

항목일회성연간 반복비고
Apple Developer Program-$99필수
Xcode 프로젝트 초기 설정8-16시간-변환 + 코드 서명 + CI 설정
Safari API 호환성 대응16-40시간-미지원 API 대체 구현
App Store 메타데이터4-8시간-스크린샷, 설명, 프라이버시 라벨
유지보수-월 4-8시간업데이트 빌드 + 심사 대응
macOS runner (GitHub Actions)-$0.08/분Linux runner보다 10배 비쌈

Safari 브라우저 점유율 (2026 기준, 글로벌)

플랫폼Safari 점유율비고
데스크탑 (글로벌)~15-18%macOS 사용자 중심
모바일 (글로벌)~25-27%iOS 사용자의 기본 브라우저
데스크탑 (미국)~20%Apple 생태계 비율 높음
모바일 (미국)~30%
데스크탑 (한국)~8-10%Mac 점유율 낮음
모바일 (한국)~25%iPhone 점유율 반영

핵심: B2C 서비스에서 모바일 사용자가 많다면 Safari iOS 비중이 상당함.

Chrome/Firefox만 vs Safari 추가: ROI 분석

시나리오Chrome+Firefox 커버리지Safari 추가 후 커버리지추가 비용
데스크탑 글로벌~80%~95-98%$99/년 + 개발 비용
모바일 글로벌70% (Chrome만)~95%iOS 사용자 대폭 확보
기업/교육 (미국)~75%~95%Apple 생태계 필수

의사결정 매트릭스

Safari 지원을 추가해야 하는 시점을 판단하기 위한 기준입니다.

판단 기준Safari 추가 권장Safari 보류
대상 시장미국, 일본, 서유럽 (Apple 점유율 높음)동남아, 인도 (Android 중심)
사용자 유형B2C, 일반 소비자B2B, 개발자 도구
플랫폼모바일 웹 중심데스크탑 중심
확장 기능기본 API (storage, tabs, scripting)고급 API (sidePanel, offscreen, tabGroups)
팀 규모3명+ (iOS 경험자 1인 이상)1-2명 풀스택
수익 모델구독/결제 (Apple IAP 가능)무료 도구
사용자 요청Safari 지원 요청 많음요청 거의 없음
경쟁사경쟁사가 Safari 지원경쟁사도 미지원

단계별 도입 전략

Phase 1: Chrome + Firefox (출시)
- 전체 사용자의 ~80% 커버
- 개발/유지보수 비용 최소화
- Edge는 Chrome 빌드 그대로 사용

Phase 2: Safari macOS 추가 (사용자 1,000+ 또는 요청 증가)
- 데스크탑 커버리지 ~95%
- macOS 사용자 확보
- Apple Developer 등록 + Xcode 설정

Phase 3: Safari iOS 추가 (모바일 사용자 중요)
- 모바일 커버리지 ~95%
- iOS 전용 UI 최적화
- App Store 심사 대응 체계

비용 대비 효과 계산 예시

연간 비용:
- Apple Developer: $99
- 개발 (초기): 40시간 × $50/hr = $2,000
- 유지보수: 6시간/월 × 12 × $50/hr = $3,600
- CI macOS runner: ~$200 (월 40분 × 12개월)
- 총 첫 해: ~$5,900
- 이후 연간: ~$3,900

기대 효과:
- 사용자 15-25% 추가 확보
- 기존 1,000명 → +150~250명
- 유료 전환 10%, 월 $5 → 추가 $75~125/월 ($900~1,500/년)
- MAU 5,000+ 에서 손익분기

10. 체크리스트

프로젝트 변환 전 체크리스트

환경:

  • macOS에서 작업 중 (Xcode는 macOS 전용)
  • Xcode 15+ 설치
  • Apple Developer Program 등록 완료
  • xcrun safari-web-extension-converter 명령 사용 가능

소스 호환성:

  • Chrome 확장 빌드가 정상 동작
  • chrome.sidePanel 사용 시 Safari 폴백 구현
  • chrome.offscreen 사용 시 Safari 폴백 구현
  • chrome.tabGroups 사용 시 Safari 폴백 구현
  • Service Worker 상태 관리가 storage.session 기반 (메모리 의존 X)
  • storage.sync 사용 시 동기화 미지원 인지

Xcode 프로젝트 설정 체크리스트

프로젝트:

  • Bundle Identifier 설정 (앱 + 확장, 각 플랫폼별)
  • Team 설정 (Signing & Capabilities)
  • Automatically manage signing 활성화 (또는 수동 프로파일 설정)
  • Deployment Target 설정 (macOS 12+, iOS 15+)
  • App Icons 설정 (모든 사이즈)

확장:

  • Shared (Extension)/Resources/ 에 빌드 결과물 복사됨
  • manifest.json의 Safari 미지원 키 제거
  • SafariWebExtensionHandler.swift 필요 시 네이티브 메시징 구현
  • Info.plistSFSafariWebExtensionConverterVersion 확인

컨테이너 앱:

  • 확장 활성화 안내 UI 구현
  • 확장 활성화 상태 체크 (SFSafariExtensionManager.getStateOfSafariExtension)
  • 최소한의 독립 기능 포함 (App Store 4.2 규정)

App Store 제출 전 체크리스트

기능:

  • macOS Safari에서 모든 기능 테스트 통과
  • iOS Safari에서 모든 기능 테스트 통과 (iOS 지원 시)
  • Safari 환경설정에서 확장 활성화/비활성화 정상
  • Background Script 종료 후 재활성화 시 정상 동작
  • 오프라인 상태 처리 (에러 메시지 표시)
  • 모든 UI (팝업, 옵션, 컨텐트 스크립트) 정상 렌더링

App Store 메타데이터:

  • Mac App Store 스크린샷 (최소 1장, 2560x1600)
  • iOS App Store 스크린샷 (6.7" + 6.1", iPad 지원 시 추가)
  • 앱 설명 (확장 기능 명확히 기술)
  • 카테고리 선택 (Utilities, Productivity 등)
  • Support URL
  • 프라이버시 정책 URL

프라이버시:

  • 프라이버시 라벨 모든 항목 정확히 체크
  • 추적 사용 시 ATT (App Tracking Transparency) 구현
  • 데이터 수집 목적 정확히 기술

코드 서명:

  • Distribution 인증서 유효 (만료일 확인)
  • 프로비저닝 프로파일 유효 (앱 + 확장, macOS + iOS)
  • Entitlements 올바름 (App Groups 등 필요 시)

심사 대응:

  • 테스트 계정 정보 (로그인 필요 시 App Review Notes에 기재)
  • 심사자를 위한 데모 영상 (복잡한 기능의 경우)
  • App Review Notes에 확장 활성화 방법 기재

CI/CD 체크리스트

  • macOS runner 사용 가능 (GitHub Actions macos-14 또는 Xcode Cloud)
  • 인증서/프로파일 CI에 등록 (base64 인코딩)
  • App Store Connect API Key 발급 및 CI 시크릿 등록
  • 웹 확장 빌드 → Xcode 리소스 복사 자동화
  • Archive + Export 자동화 (xcodebuild 또는 fastlane)
  • TestFlight 자동 업로드 설정
  • 버전 번호 자동 증가 (build number)

관련 문서

문서내용
chrome-store.mdChrome Web Store 배포
firefox-addon.mdFirefox Add-on 배포
cross-browser.md크로스 브라우저 호환성
architecture.mdManifest V3 아키텍처
../common/ci-cd.md공통 CI/CD 파이프라인
../common/licensing.md라이선스/트라이얼

외부 참조

자료URL
Safari Web Extensions 공식 문서https://developer.apple.com/documentation/safariservices/safari_web_extensions
safari-web-extension-converter 레퍼런스https://developer.apple.com/documentation/safariservices/safari_web_extensions/converting_a_web_extension_for_safari
App Store Review Guidelineshttps://developer.apple.com/app-store/review/guidelines/
Apple Developer Programhttps://developer.apple.com/programs/
Xcode Cloudhttps://developer.apple.com/xcode-cloud/
fastlane docs (match)https://docs.fastlane.tools/actions/match/

Last Updated: 2026-04-06