View in English

  • Apple Developer
    • 시작하기

    시작하기 탐색

    • 개요
    • 알아보기
    • Apple Developer Program

    알림 받기

    • 최신 뉴스
    • Hello Developer
    • 플랫폼

    플랫폼 탐색

    • Apple 플랫폼
    • iOS
    • iPadOS
    • macOS
    • tvOS
    • visionOS
    • watchOS
    • App Store

    피처링

    • 디자인
    • 배포
    • 게임
    • 액세서리
    • 웹
    • 홈
    • CarPlay
    • 기술

    기술 탐색

    • 개요
    • Xcode
    • Swift
    • SwiftUI

    피처링

    • 손쉬운 사용
    • 앱 인텐트
    • Apple Intelligence
    • 게임
    • 머신 러닝 및 AI
    • 보안
    • Xcode Cloud
    • 커뮤니티

    커뮤니티 탐색

    • 개요
    • Apple과의 만남 이벤트
    • 커뮤니티 주도 이벤트
    • 개발자 포럼
    • 오픈 소스

    피처링

    • WWDC
    • Swift Student Challenge
    • 개발자 이야기
    • App Store 어워드
    • Apple 디자인 어워드
    • 문서

    문서 탐색

    • 문서 라이브러리
    • 기술 개요
    • 샘플 코드
    • 휴먼 인터페이스 가이드라인
    • 비디오

    릴리즈 노트

    • 피처링 업데이트
    • iOS
    • iPadOS
    • macOS
    • watchOS
    • visionOS
    • tvOS
    • Xcode
    • 다운로드

    다운로드 탐색

    • 모든 다운로드
    • 운영 체제
    • 애플리케이션
    • 디자인 리소스

    피처링

    • Xcode
    • TestFlight
    • 서체
    • SF Symbols
    • Icon Composer
    • 지원

    지원 탐색

    • 개요
    • 도움말
    • 개발자 포럼
    • 피드백 지원
    • 문의하기

    피처링

    • 계정 도움말
    • 앱 심사 지침
    • App Store Connect 도움말
    • 새로 추가될 요구 사항
    • 계약 및 지침
    • 시스템 상태
  • 빠른 링크

    • 이벤트
    • 뉴스
    • 포럼
    • 샘플 코드
    • 비디오
 

비디오

메뉴 열기 메뉴 닫기
  • 컬렉션
  • 전체 비디오
  • 소개

더 많은 비디오

  • 소개
  • 요약
  • 자막 전문
  • 코드
  • Safari용 웹 확장 프로그램 생성하기

    Xcode 없이 처음부터 빌드하고 테스트하는 것으로 Safari 웹 확장 프로그램을 시작해 보세요. 콘텐츠 차단, 페이지 수정, 네이티브 메시지, 권한 모드를 함께 사용하여 다양한 플랫폼 전반에 걸쳐 강력하고 개인정보를 보호하는 브라우징 경험을 선사하는 방법을 살펴보세요.

    챕터

    • 0:00 - Introduction
    • 3:23 - Get started
    • 7:23 - Block content
    • 14:40 - Modify webpages
    • 19:53 - Package and distribute
    • 22:33 - Communicate with your app
    • 26:04 - Next steps

    리소스

    • w3.org — W3C WebExtensions Community Group
    • Packaging and distributing Safari Web Extensions with App Store Connect
    • WebKit.org – Report issues to the WebKit open-source project
    • Submit feedback
    • MDN Web Docs - Web Extensions API
      • HD 비디오
      • SD 비디오

    관련 비디오

    WWDC26

    • Safari 27용 WebKit의 새로운 기능
  • 비디오 검색…

    안녕하세요!

    저는 Kiara이고, Safari 팀의 엔지니어예요 Safari에 추가하고 싶은 기능에 대한 아이디어가 있었다면 그 아이디어를 현실로 만들 수 있어요 이 세션이 바로 여러분을 위한 거예요 익스텐션을 빌드하고 배포하는 데 필요한 모든 내용을 알려드릴게요 Safari용 웹 익스텐션을요 다룰 내용이 많으니 자유롭게 쉬거나 유용한 섹션으로 건너뛰어도 돼요 가장 도움이 되는 섹션으로요 Safari 웹 익스텐션은 앱 안에 패키징돼요 솔직히 App Store에서 제가 가장 좋아하는 것들 중 하나예요 광고 차단이나 맞춤 새 탭 페이지 빌드 또는 즐겨 찾는 스트리밍 사이트의 재생 경험 향상 같은 거예요 작지만 웹 경험을 의미 있게 개선할 수 있어요 Apple은 W3C 웹 익스텐션 작업 그룹에서 다른 브라우저들과 협력하고 있어요 브라우저 간 웹 익스텐션 빌드에 사용되는 API를 표준화하기 위해서예요 다른 브라우저용 익스텐션을 만든 적이 있다면 Safari로 가져올 수 있어요 패키징 및 배포 섹션으로 이동하면 방법을 알려드릴게요 App Store Connect를 사용해 익스텐션을 배포하는 방법이에요 오늘은 모든 내용을 다루기 위해 웹 익스텐션을 빌드할 거예요 처음부터요 익스텐션 개발 과정을 살펴보고 핵심 내용을 소개할게요 Safari에서 맞춤형 경험을 제공하는 데 사용할 수 있는 주요 API와 기능이에요 Safari에서 웹 익스텐션을 테스트하는 방법도 알려드릴게요 TestFlight를 사용해 베타 버전을 사용자와 공유하는 방법도요 세상과 공유할 준비가 되면 익스텐션을 제출할 거예요 App Store에요 시작하려면 이 세션의 샘플 코드 프로젝트를 다운로드하세요 오늘 빌드할 익스텐션의 모든 리소스가 들어 있어요 따라오실 수 있도록요 이 세션에서는 실제 익스텐션을 빌드하는 과정을 안내해 드릴게요 콘텐츠를 차단하고 웹 페이지를 수정하는 익스텐션이에요 그런 다음, 익스텐션을 패키징하는 몇 가지 옵션을 다룰게요 App Store에 배포하는 방법들이에요 익스텐션의 기능을 더욱 확장하려면 방법을 알려드릴게요 익스텐션이 포함된 앱과 함께 작동하는 방법이에요 마지막으로, 익스텐션은 iOS, iPadOS macOS의 Safari에서 작동할 거예요 visionOS에서도 동시에요 웹 익스텐션의 장점은 모두 다음으로 구성된다는 거예요 HTML, CSS, JavaScript로요 웹 개발을 해본 적이 있다면 필요한 것 대부분을 이미 알고 있어요 오늘 세션에서는 사람들이 차단할 수 있는 익스텐션을 빌드할 거예요 웹 브라우징 중 방해가 되는 사이트를요 알다시피, 브라우징 중에는 빠져들게 되는 것들이 많아요 webkit.org를 예로 들면요 수백 개의 글이 있고 쉽게 몇 시간을 보낼 수 있어요 WebKit의 새로운 소식을 읽다 보면요 그래서 이런 익스텐션이 필요해요 오늘 빌드할 거예요, 두 가지 차단 모드가 있어요 Light 모드는 사이트에서 최대 10분 브라우징을 허용해요 WebKit 글을 몇 개 읽기에 딱 맞아요 Full 모드는 사용자가 이동하려는 순간 리디렉션해요 해당 사이트로 이동하려 하면요 시작하려면 즐겨 쓰는 코드 에디터를 열게요 Xcode도 사용할 수 있지만 어떤 에디터든 익스텐션 빌드에 쓸 수 있어요 코드 에디터에서 익스텐션의 모든 파일을 담을 폴더를 만들 거예요 기초 작업을 위해 모든 익스텐션에 필요한 첫 번째 파일은 매니페스트예요 매니페스트는 JSON 형식의 파일로 브라우저에 익스텐션이 무엇인지 알려줘요 무엇을 할 수 있는지도요 매니페스트는 익스텐션의 신분증이라고 생각하세요 익스텐션 이름과 같은 정보를 포함해요 설명과 버전 번호도요 다음으로, 익스텐션 아이콘을 담을 이미지 폴더를 추가할 거예요 아이콘은 다양한 위치에 나타날 수 있어요 툴바처럼요 또는 익스텐션 설정에서요 나타나는 위치에 따라 다른 크기가 필요해요 그래서 아이콘을 svg로 추가할게요 Safari가 아이콘 크기를 완벽하게 처리해 줘서 중요한 것에 집중할 수 있어요 코드 에디터로 돌아가 실제로 어떻게 보이는지 살펴볼게요 매니페스트 파일에 익스텐션 아이콘을 추가했어요 아이콘은 이미지 폴더에 위치해 있어요 변경 사항을 저장하고 Safari로 이동해서 확인해 볼게요 익스텐션을 로드하러요 Safari에서 익스텐션을 로드하는 건 정말 쉬워요 Command+Comma로 Safari 설정을 열고 고급 설정 창을 클릭하면 돼요 "웹 개발자용 기능 보기"를 체크하면 돼요 개발자 창이 활성화될 거예요 거기서 임시 익스텐션을 추가할 수 있어요 코드 서명이 확인되지 않은 익스텐션이라서 서명되지 않은 익스텐션을 허용해야 해요 허용 후, 익스텐션 리소스가 담긴 폴더를 선택할게요 이렇게 하면 익스텐션이 Safari에 로드돼요! 대부분의 익스텐션은 사람들이 상호작용할 수 있는 UI가 있어요 제 익스텐션에는 사람들이 추가할 수 있는 방법이 필요해요 방해가 되는 사이트를 차단 목록에요 그래서 커스텀 UI를 추가해야 해요 이를 위한 몇 가지 방법이 있어요 한 가지 방법은 익스텐션의 액션 버튼이에요 Safari 툴바에 익스텐션용으로 추가된 버튼이에요 클릭하면 Safari가 정의한 UI로 팝업을 표시해요 팝업의 파일 이름이나 매니페스트에 정의한 리소스는 원하는 이름을 사용할 수 있어요 올바른 매니페스트 키와 연결되어 있으면요 Safari가 그것이 무엇인지 어떻게 사용해야 하는지 알 수 있도록요 이 예시에서 기본 팝업을 로드하는 파일은 "popup.html"로 설정돼 있어요 제 익스텐션의 경우 팝업에 차단 목록을 표시하면 UI가 좀 좁아 보일 수 있어요 그래서 대신 익스텐션의 옵션 페이지를 사용할게요 사용자가 익스텐션 설정을 지정할 수 있는 전체 페이지가 될 거예요 이제 코드 에디터로 돌아가서 이 변경 사항을 추가할게요 매니페스트에 옵션 페이지가 정의되어 있어요 익스텐션 폴더에 파일을 추가할게요 이 페이지의 전체 UI를 추가하기 전에 간단한 것부터 시작할게요 Hello World처럼요 Safari에서 익스텐션을 다시 로드하여 변경 사항을 테스트할 수 있어요 좋아요! 새로운 변경 사항이 적용됐어요 익스텐션에 설정 버튼이 생겼어요 버튼을 클릭하면 익스텐션의 옵션 페이지가 열려요! 이제 설정이 완료됐으니 익스텐션이 더 많은 일을 해야 해요 "Hello World"만 표시하는 것보다 더 유용하게요 그래서 사용자가 전환할 수 있는 페이지를 디자인했어요 Light 모드와 Full 모드 사이를요 이미 HTML, CSS, JavaScript를 사용해서 인터페이스를 작성했어요 예쁘고 인터랙티브하지만 연결해야 해요 이제 콘텐츠 차단을 시작하도록 익스텐션을 업그레이드하는 방법을 볼게요 declarative net request API를 사용할 거예요 이렇게 하면 익스텐션이 네트워크 요청을 차단, 수정 또는 리디렉션하는 기능을 가지게 돼요 이 API는 제가 가장 좋아하는 유형의 웹 익스텐션을 구동해요 이 기능으로 익스텐션은 광고 같은 웹 콘텐츠를 필터링할 수 있어요 사용자가 웹 브라우징 중에 타겟이 되는 트래커도요 하지만 익스텐션이 이 기능에 접근하려면 권한을 추가해야 해요 권한은 익스텐션이 Safari에 접근 권한이 필요한 것을 알리는 방법이에요 쿠키 접근 같은 것들이 있어요 저장소에 데이터 저장이나 클립보드에 쓰기도요 콘텐츠 차단을 위해 declarative net request 권한을 추가해야 해요 익스텐션의 매니페스트에서 추가할 수 있어요 이것을 설정하면 규칙 정의를 시작할 수 있어요 규칙에는 ID, 우선순위 그리고 조건이 충족될 때 발생할 액션 유형이 있어요 조건이 충족될 때요 이 규칙은 예를 들어 webkit.org로의 모든 탐색을 차단해요 규칙을 정의하는 방법은 두 가지예요 한 가지 옵션은 매니페스트에서 정의하는 거예요 이것들을 정적 규칙이라고 해요 사용하려는 규칙을 이미 알 때 좋아요 하지만 유연성이 필요하다면 런타임에 동적으로 규칙을 추가할 수 있어요 JavaScript를 사용해서요 동적 방식을 선택할게요 어떤 사이트를 차단할지 모르니까요 사용자가 목록에 추가할 때까지요 이 로직을 utilities 폴더 안에 rules.js라는 파일에 넣을게요 그런 다음 host.js 파일을 사용해서 규칙을 만들 거예요 사용자가 차단 목록에 사이트를 추가할 때요 코드로 돌아가서 연결해 볼게요 익스텐션의 매니페스트에 declarative net request 권한을 추가했어요 이전 옵션 페이지를 익스텐션을 위해 이미 만든 HTML, CSS, JavaScript 파일로 교체했어요 이미 익스텐션을 위해 만든 거예요 두 개의 새 파일이 있는 utilities 폴더도 추가했어요 규칙을 추가하려면 rules.js 파일로 가면 돼요 이 규칙들은 ID를 지정하므로 헬퍼 메서드를 추가했어요 사이트의 host를 고유한 정수 ID로 매핑하는 메서드예요 이제 ID, "block" 유형, urlFilter를 지정하는 규칙을 만들어요 사이트의 host에 매칭시키기 위해서요 그런 다음 declarative net request updateDynamicRules API를 사용해요 익스텐션에 규칙을 추가하기 위해서요 host 파일에서 사이트가 목록에 추가될 때 익스텐션이 규칙을 추가할 수 있어요 전체 차단 모드일 때요 Safari로 돌아가서 익스텐션을 업데이트하기 위해 다시 로드할게요

    옵션 페이지에서 webkit.org를 목록에 추가할게요

    그 사이트로 이동하면 차단됐어요! 익스텐션이 이제 목록에 추가된 사이트로의 탐색을 차단할 수 있어요 하지만 나타나는 오류 페이지가 별로 마음에 들지 않아요 사용자를 더 의도적인 곳으로 보내고 싶어요 익스텐션을 위해 디자인한 맞춤 페이지처럼요 익스텐션을 위해 디자인한 거예요 그래서 리디렉션 규칙이 필요해요 이 규칙은 이전 차단 규칙과 비슷하지만 유형이 redirect예요 사용자가 도달할 페이지를 위해 extensionPath를 지정할 수 있어요 이 변경을 하기 전에 익스텐션을 위한 host 권한을 추가해야 해요 네트워크 요청을 차단하려면 익스텐션이 페이지에 접근할 필요가 없어요 하지만 네트워크 요청을 리디렉션하려면 익스텐션이 접근해야 해요 그래서 익스텐션의 매니페스트에서 declarativeNetRequestWithHostAccess를 사용할게요 권한 대신이요 익스텐션이 어떤 사이트에도 미리 접근 권한을 요청할 필요가 없으므로 선택적 host 권한을 사용하고 런타임에 사이트 접근을 요청할 수 있어요 host 권한은 Safari에 익스텐션이 접근하려는 사이트를 알려줘요 매치 패턴의 배열로 설정할 수 있어요 각 패턴은 scheme, host 그리고 path로 구성돼요 어떤 사이트든 목록에 추가될 수 있으므로 모든 URL에 매칭할 수 있는 패턴을 사용할게요 익스텐션이 작동하기 위해 특정 사이트에 명시적 접근이 필요하다면 대신 host 권한을 사용하면 돼요 하지만 익스텐션은 자동으로 사이트 접근 권한을 얻지 않아요 익스텐션의 권한 모델은 사용자 프라이버시를 존중하도록 설계됐어요 사용자의 브라우징 경험은 개인 데이터를 노출할 수 있으므로 사용자가 통제권을 갖고 익스텐션이 접근할 수 있는 사이트를 결정해요 명시적으로 접근을 요청하면 Safari가 익스텐션의 액션 버튼에 배지를 표시해요 버튼을 클릭하면 알림이 나타나서 사용자에게 물어봐요 익스텐션에 페이지 접근 권한을 부여할지요 사용자가 접근을 허용하면 아이콘이 색조를 띠게 돼요 해당 페이지에서 익스텐션이 활성화됐음을 알리는 거예요 제 익스텐션은 어떤 사이트에도 미리 접근할 필요가 없어요 그래서 선택적 host 권한을 선택한 거예요 이렇게 하면 익스텐션이 필요할 때 어떤 사이트든 접근을 요청할 수 있어요 매니페스트에서 권한을 declarativeNetRequestWithHostAccess로 변경했어요 이제 익스텐션이 런타임에 어떤 사이트든 접근을 요청할 수 있어요 이제 rules 파일에서 리디렉션 규칙을 만들게요 이전 차단 규칙과 매우 비슷하지만 이제 유형이 redirect예요 맞춤 익스텐션 페이지로의 경로도 있어요 익스텐션 폴더에 페이지의 리소스를 추가했어요 이제 탐색을 차단하는 대신 페이지로 사용자를 리디렉션해요 익스텐션을 위해 디자인한 페이지로요 익스텐션이 사이트 접근이 필요하므로 접근을 요청할게요 도메인과 서브도메인에 대해 permissions.request API를 사용해서요 이 경험이 어떻게 보이는지 보려면 Safari에서 익스텐션을 업데이트할게요

    옵션 페이지에서 webkit.org를 추가할게요

    사이트가 추가되기 전에 익스텐션 접근 권한을 부여하라는 메시지가 나타나요 좋아요! 예상했던 그대로예요 접근을 허용하고 페이지를 새로 고칠게요

    놀라워요! 탐색이 맞춤 익스텐션 페이지로 리디렉션됐어요 익스텐션이 정말 잘 되고 있어요! 이제 방해가 되는 사이트로의 탐색을 리디렉션할 수 있어요 하지만 솔직히 말하면요 즐겨 찾는 사이트 중 일부와 완전히 연락을 끊는 건 어려울 수 있어요 그래서 최대 10분 브라우징을 허용하는 모드를 추가할 거예요 페이지 바로 위에 카운트다운 타이머와 함께요 그러려면 콘텐츠를 페이지에 직접 삽입할 방법이 필요해요 바로 콘텐츠 스크립트가 필요한 때예요 콘텐츠 스크립트는 익스텐션에 읽고 수정하는 기능을 제공해요 웹 페이지의 콘텐츠를요 스크립트는 정적일 수 있어요 매니페스트에 직접 선언되는 거예요 실행할 사이트의 파일과 매치 패턴과 함께요 타겟으로 삼을 사이트를 이미 알 때 잘 작동해요 하지만 제 경우에는 목록에 추가될 때까지 사이트를 알 수 없어요 그래서 registered content scripts API를 사용해서 즉석에서 추가할게요 정적 스크립트처럼 작동하지만 두 가지 추가 필드가 있어요 ID와 persistence 플래그예요 이것을 true로 설정하면 스크립트가 유지돼요 Safari를 다시 실행한 후에도요 이 API를 사용하려면 매니페스트에 scripting 권한을 추가할게요 그리고 utilities 폴더에 새 scripting.js 파일도요 여기서 콘텐츠 스크립트를 정의할게요 ID, 타이머용 JavaScript 파일, 스타일링을 위한 CSS를 지정할게요 그리고 사이트의 도메인과 모든 서브도메인을 포함하는 매치 패턴도요 그리고 이 플래그를 true로 설정할게요 그런 다음 register content scripts API를 사용해서 스크립트를 추가할게요 addHost 메서드로 돌아가서 이제 사용자가 차단 목록에 사이트를 추가하면 해당 사이트의 타이머를 보여주는 콘텐츠 스크립트를 추가할게요 익스텐션이 전체 차단 모드일 때 페이지가 로드되기 전에 리디렉션이 실행되므로 항상 스크립트를 등록할 수 있어요 이제 Light 차단 모드를 선택한 상태에서 webkit.org를 목록에 추가할게요 해당 사이트로 이동하면 페이지에 10분 타이머가 표시돼요

    익스텐션이 세상에 내놓고 싶은 것에 거의 다가왔어요 하지만 먼저 고치고 싶은 것이 있어요 눈치채셨겠지만 익스텐션을 다시 로드할 때마다 같은 사이트를 목록에 계속 다시 추가해야 해요 이것은 모든 정보를 메모리에 저장하고 있기 때문이에요 익스텐션이 다시 로드되면 그 상태가 사라져요

    storage API를 사용해서 이 데이터를 유지할 수 있어요 Safari는 두 가지 저장 영역을 지원해요 세션 저장소는 빠른 인메모리 작업에 적합해요 재시작 후 유지될 필요가 없는 것들이요 하지만 차단 목록은 유지되길 원해요 그래서 데이터를 디스크에 쓰는 로컬 저장소가 적합해요

    storage API를 사용하려면 매니페스트에 권한을 추가할게요 그런 다음 utilities 폴더에 새 파일을 추가할게요 이 파일에서 몇 가지 헬퍼 메서드를 정의할게요 저장소에서 host를 업데이트하고 가져오는 메서드예요 차단 모드를 저장하고 가져오는 메서드도요

    addHost 메서드로 돌아가서 이제 사용자가 새 사이트를 추가하면 업데이트할 수 있어요 저장소의 host 목록을요 저장된 목록을 사용해서 차단 목록을 표시할 수도 있어요 이 변경으로 차단 목록은 항상 렌더링될 거예요 추가된 모든 사이트의 전체 목록과 함께요! 마찬가지로 사용자가 두 모드 사이를 전환할 때 변경 사항을 저장소에 저장할게요 익스텐션이 전체 차단 모드라면 모든 사이트의 리디렉션 규칙을 만들 수 있어요 storage API 사용의 장점을 보려면 Safari에서 차단 모드를 "Full"로 변경할게요 그리고 목록에 사이트를 추가할게요

    이제 익스텐션을 다시 로드하면 방금 만든 변경 사항이 그대로 남아 있어요! 좋아요! storage API를 사용해서 익스텐션의 한 가지 지속성 문제를 해결했어요 하지만 고쳐야 할 것이 또 하나 있어요 등록된 콘텐츠 스크립트는 Safari 재시작 후에도 유지되지만 익스텐션 업데이트 후에는 그렇지 않아요 사용자가 익스텐션을 업데이트하면 콘텐츠 스크립트를 잃게 돼요 이를 해결하려면 익스텐션이 업데이트됐는지 알 방법이 필요해요 저장소에서 host를 읽고 스크립트를 다시 만들 수 있도록요 가장 적합한 장소는 백그라운드 페이지나 서비스 워커예요 둘 다 같은 일을 할 수 있어요 익스텐션의 라이프사이클을 관리하는 것처럼요 브라우저 이벤트를 수신하고 익스텐션의 여러 부분 사이에 메시지를 전달해요 Safari는 둘 다 지원하므로 정말 여러분의 선호에 달려 있어요! 저는 백그라운드 페이지가 DOM에 접근할 수 있어서 좋아요, 그걸 선택할게요 백그라운드 페이지를 추가하려면 매니페스트에 지정할게요 그런 다음 익스텐션 폴더에 파일을 추가할게요 여기서 onInstalled 이벤트에 등록할게요 이렇게 하면 익스텐션이 새 버전으로 업데이트됐음을 알 수 있어요 이 경우 저장소에서 host를 읽고 콘텐츠 스크립트를 다시 등록해요 익스텐션이 저장소에서 읽고 쓸 수 있도록 연결한 것은 정말 큰 개선이었어요 이제 App Store에 익스텐션을 올리는 방법을 살펴볼 때가 됐어요 한 가지 방법은 App Store Connect를 사용하는 거예요 App Store Connect에서 익스텐션을 업로드, 제출 및 관리할 수 있어요 방금 만든 것이든 아니면 가져오려는 기존 것이든요 Safari에 가져오려는 기존 것이든요 가장 좋은 점은요? Mac 없이 어떤 브라우저에서도 할 수 있다는 거예요 시작하려면 developer.apple.com으로 이동해서 등록할게요 Apple Developer Program에요 등록 후 appstoreconnect.apple.com으로 이동할게요 Safari 웹 익스텐션은 포함된 앱 안에 패키징되어야 하므로 App Store Connect를 사용해서 이 앱을 만들 수 있어요 앱을 만들 때 몇 가지를 지정해야 해요 익스텐션을 사용할 수 있게 하려는 플랫폼 같은 것들이에요 iOS와 macOS를 선택할게요 그러면 익스텐션이 iPhone, iPad Mac에서 사용 가능하게 돼요 그리고 Apple Vision Pro의 호환 앱으로도요 번들 식별자도 설정할게요 앱의 고유 ID예요 모든 세부 정보를 추가한 후 Xcode Cloud 탭으로 전환할게요 그리고 스크롤을 내려서 Safari Web Extension Packager로 이동해요 익스텐션 리소스를 업로드하기 위해서예요 업로드되면 Safari Web Extension Packager가 익스텐션을 몇 분 안에 패키징해 줄 거예요! 완료되면 문제를 확인하거나 익스텐션 테스트를 위한 다음 단계를 밟을 수 있어요 TestFlight를 사용해서요 TestFlight를 사용하면 베타 빌드를 배포할 수 있어요 지속적으로 개선하고 사용자 피드백을 구현할 수 있도록요 App Store에 익스텐션을 제출하기 전에요 제출할 준비가 되면 배포 탭으로 이동해서 마지막 마무리를 할게요 실제로 작동하는 익스텐션의 스크린샷처럼요 사용자가 이해할 수 있도록 도움이 되는 설명도요 익스텐션의 기능과 성능에 대한 설명이요 모든 세부 정보를 추가한 후 빌드를 선택하고 검토를 위해 제출할게요 App Store Connect를 사용해서 익스텐션을 배포하는 방법이에요! 이 세션에서 표준 WebExtension API와 기능이 어떻게 함께 작동하는지 보여드렸어요 맞춤형 브라우징 경험을 만들기 위해서요 하지만 익스텐션이 웹 플랫폼을 넘어설 수 있다고 하면 어떨까요 네이티브 메시징을 사용해서 플랫폼이 제공하는 기능에 접근하는 방법을 알려드릴게요 플랫폼이 제공하는 기능에요 여기서부터는 Xcode가 필요해요 가장 간단한 방법은 Safari Web Extension Packager 도구를 사용하는 거예요 Terminal에서 이 명령을 실행하면 Xcode 프로젝트가 만들어지고 실행돼요 앱과 웹 익스텐션이 포함될 거예요 거기서 메시지를 보내고 받을 수 있도록 연결할 수 있어요 이것을 네이티브 메시징이라고 해요 세 명이 쪽지를 전달하는 것처럼 생각하면 돼요 익스텐션의 JavaScript가 시작해요 중간의 App Extension이 그 메시지를 받아서 네이티브 앱에 전달해요 그러면 앱이 처리하고 같은 방식으로 결과를 돌려보내요 제 앱은 익스텐션이 설정을 보호하도록 도울 거예요 차단 목록을 변경하기 전에 생체 인증이 필요하도록요 시작하려면 nativeMessaging 권한을 추가할게요 익스텐션의 매니페스트에요 그런 다음 익스텐션의 백그라운드 페이지에서 메시지를 보내는 메서드를 추가할게요 익스텐션에서 네이티브 앱으로요 받은 메시지로 인증이 성공했는지 알 수 있어요 앱이 메시지를 받으려면 SafariWebExtensionHandler를 수정해야 해요 packager 도구가 만들어 준 파일에 있는 클래스예요 좋은 점은 이미 앱이 익스텐션으로부터 메시지를 받을 수 있도록 허용하는 템플릿이 포함되어 있어요 몇 가지 수정만 하면 돼요 requestBioAuth 키를 위한 메시지 파싱처럼요 그런 다음 시스템 API를 사용해서 생체 인증으로 사용자 인증을 요청할게요 사용자가 인증하면 앱이 결과와 함께 메시지를 돌려보내요 웹 익스텐션으로요 Xcode로 이동해서 익스텐션을 빌드하고 Safari에서 변경 사항을 테스트할게요 Xcode에서 익스텐션으로 메시지를 보내고 받기 위한 변경 사항을 적용했어요 Project Navigator에서 프로젝트에 웹 익스텐션의 모든 리소스가 있어요 Command+B 단축키를 사용해서 익스텐션을 빌드할게요 Safari에서 익스텐션을 활성화하고 옵션 페이지를 열게요 webkit.org를 목록에 추가할게요

    추가되기 전에 Touch ID로 인증하라는 메시지가 나타나요 인증 후 사이트가 목록에 추가돼요 네이티브 메시징을 사용해서 앱과 웹 익스텐션이 함께 작동하는 방법이에요 웹 익스텐션이 함께 작동하는 방법이요 이제 익스텐션이 배포 준비가 됐다고 자신 있게 말할 수 있어요 Xcode를 사용해서 할게요

    아카이브 빌드부터 시작할게요 이전에 App Store Connect를 사용해서 빌드를 만들었으므로 이 빌드 번호가 마지막 빌드보다 하나 높은지 확인해야 해요 Organizer 창에서 익스텐션을 배포할게요 처음부터 Safari 웹 익스텐션을 만들고 배포하는 방법이에요 처음부터요 오늘 많은 내용을 다뤘어요 막 시작하는 분이든 익스텐션을 더 발전시키려는 분이든 아이디어를 가지고 실제로 만들어 나갈 준비가 되셨으면 좋겠어요 여러분이 만드는 것이 기대돼요

    아직 안 하셨다면 샘플 코드 프로젝트를 다운로드하세요 오늘 소개한 API를 직접 사용해 보기 위해서요 크로스 브라우저 문서를 확인해서 더 자세히 알아볼 수 있어요 MDN의 웹 익스텐션 문서에서요 마지막으로 Feedback Assistant를 통해 피드백을 제공해 주세요 또는 bugs.webkit.org에 버그를 제출해 주세요 Safari 27에서 웹 익스텐션을 테스트하면서요 이 여정에 함께해 주셔서 감사해요 즐거운 WWDC 되세요!

    • 3:44 - Manifest file

      {
          "manifest_version": 3,
          "name": "Shiny OnTrack",
          "description": "Stay on track while you browse the web",
          "version": 1.0
      }
    • 4:29 - Adding an extension icon

      {
          "manifest_version": 3,
          "name": "Shiny OnTrack",
          "description": "Stay on track while you browse the web",
          "version": 1.0,
      
          "icons": {
              "512": "images/icon.svg"
          }
      }
    • 5:30 - Adding an action button

      {
          "manifest_version": 3,
          "name": "Shiny OnTrack",
          "description": "Stay on track while you browse the web",
          "version": 1.0,
      
          "action": {
              "default_popup": "popup.html"
          }
      }
    • 6:17 - Adding custom UI to your extension

      {
          "manifest_version": 3,
          "name": "Shiny OnTrack",
          "description": "Stay on track while you browse the web",
          "version": 1.0,
        
          "options_ui": {
              "page": "options.html"
          }
      }
    • 6:30 - Including the UI in the extension manifest

      {
          "manifest_version": 3,
          "name": "Shiny OnTrack",
          "description": "Stay on track while you browse the web",
          "version": 1.0,
      
          "icons": {
              "512": "images/icon.svg"
          },
      
          "options_ui": {
              "page": "options.html"
          }
      }
    • 6:40 - Hello World

      <!DOCTYPE html>
      <html>
          <body>
          <p>Hello World</p>
          </body>
      </html>
    • 8:18 - Adding declarativeNetRequest permission

      {
          "manifest_version": 3,
          "name": "Shiny OnTrack",
          "description": "Stay on track while you browse the web",
          "version": 1.0,
      
          "icons": {
              "512": "images/icon.svg"
          },
      
          "options_ui": {
              "page": "options.html"
          },
        
          "permissions": [ "declarativeNetRequest" ]
      }
    • 8:22 - Blocking network requests

      // block rule
      {
          id: 1,
          priority: 1,
          action: {
              type: "block"
          },
          condition: {
              urlFilter: "||webkit.org",
              resourceTypes: [ "main_frame" ]
          }
      }
    • 8:41 - Modifying network requests

      {
          "manifest_version": 3,
          "name": "Shiny OnTrack",
          "description": "Stay on track while you browse the web",
          "version": 1.0,
      
          "icons": {
              "512": "images/icon.svg"
          },
      
          "options_ui": {
              "page": "options.html"
          },
        
          "permissions": [ "declarativeNetRequest" ],
      
          "declarativeNetRequest": {
              "rule_resources": [
                  {
                      "id": "ruleset_id",
                      "enabled": true,
                      "path": "rules.json"
                  }
              ]
          }
      }
    • 8:50 - Updating dynamic rules

      await browser.declarativeNetRequest.updateDynamicRules({
          addRules: [ rule ]
      })
    • 9:19 - Wiring up the static declarativeNetRequest rules

      {
          "manifest_version": 3,
          "name": "Shiny OnTrack",
          "description": "Stay on track while you browse the web",
          "version": 1.0,
      
          "icons": {
              "512": "images/icon.svg"
          },
      
          "options_ui": {
              "page": "options.html"
          },
        
          "permissions": [ 
            "declarativeNetRequest" 
          ]
      }
    • 9:40 - Adding block rules dynamically

      // A helper function to map the host to the declarative net request rule ID.
      export function hostToRuleID(host) {
      	let hash = 0;
      	for (let i = 0; i < host.length; i++) {
      		hash = ((hash << 5) + hash) + host.charCodeAt(i);
      		hash |= 0;
      	}
      	return Math.abs(hash) || 1;
      }
      
      function createBlockRule(host) {
      	return {
      		id: hostToRuleID(host),
      		priority: 1,
      		action: {
      			type: "block"
      		},
      		condition: {
      			urlFilter: `||${host}`,
      			resourceTypes: ["main_frame"]
      		}
      	}
      }
      
      export async function createRules(hosts) {
      	try {
      		await browser.declarativeNetRequest.updateDynamicRules({
      			addRules: hosts.map(createBlockRule)
      		})
      	} catch {
      		console.log("Failed to create declarative net request rules")
      	}
      }
    • 10:10 - Handling adding hosts to the settings

      import { createRules, removeAllRules, removeRule } from './rules.js'
      
      export async function addHost(host, blockingMode) {
        if (!host)
          return
        
        if (blockingMode === "full")
          await createRules([host])
      }
    • 10:48 - Redirecting network requests

      {
          id: 1,
          priority: 1,
          action: {
              type: "redirect",
              redirect: {
                  extensionPath: "/blocked.html"
              }
          },
          condition: {
              urlFilter: "||webkit.org",
              resourceTypes: [ "main_frame" ]
          }
      }
    • 11:17 - Declaring optional host permissions

      {
          "manifest_version": 3,
          "name": "Shiny OnTrack",
          "description": "Stay on track while you browse the web",
          "version": 1.0,
      
          "icons": {
              "512": "images/icon.svg"
          },
      
          "options_ui": {
              "page": "options.html"
          },
        
          "permissions": [ "declarativeNetRequestWithHostAccess" ],
          "optional_host_permissions": [ "https://webkit.org/*" ]
      
      }
    • 11:54 - Declaring optional host permissions for all sites

      {
          "manifest_version": 3,
          "name": "Shiny OnTrack",
          "description": "Stay on track while you browse the web",
          "version": 1.0,
      
          "icons": {
              "512": "images/icon.svg"
          },
      
          "options_ui": {
              "page": "options.html"
          },
        
          "permissions": [ "declarativeNetRequestWithHostAccess" ],
          "optional_host_permissions": [ "*://*/*" ]
      
      }
    • 13:12 - Add the redirect rule

      // A helper function to map the host to the declarative net request rule ID.
      export function hostToRuleID(host) {
      	let hash = 0;
      	for (let i = 0; i < host.length; i++) {
      		hash = ((hash << 5) + hash) + host.charCodeAt(i);
      		hash |= 0;
      	}
      	return Math.abs(hash) || 1;
      }
      
      function createBlockRule(host) {
      	return {
      		id: hostToRuleID(host),
      		priority: 1,
      		action: {
      			type: "block"
      		},
      		condition: {
      			urlFilter: `||${host}`,
      			resourceTypes: ["main_frame"]
      		}
      	}
      }
      
      function createRedirectRule(host) {
      	return {
      		id: hostToRuleID(host),
      		priority: 1,
      		action: {
      			type: "redirect",
      			redirect: { extensionPath: "/blocked.html" }
      		},
      		condition: {
      			urlFilter: `||${host}`,
      			resourceTypes: ["main_frame"]
      		}
      	}
      }
      
      export async function createRules(hosts) {
      	try {
      		await browser.declarativeNetRequest.updateDynamicRules({
      			addRules: hosts.map(createRedirectRule)
      		})
      	} catch {
      		console.log("Failed to create declarative net request rules")
      	}
      }
    • 13:42 - Dynamically ask for host permissions

      import { createRules, removeAllRules, removeRule } from './rules.js'
      
      export async function addHost(host, blockingMode) {
        if (!host)
          return
        
        const granted = await browser.permissions.request({
          origins: [`*://${host}/*`, `*://*.${host}/*`]
        })
        if (!granted)
          return
        
        if (blockingMode === "full")
          await createRules([host])
      }
    • 14:55 - Defining content scripts

      {
          "manifest_version": 3,
          "name": "Shiny OnTrack",
          "description": "Stay on track while you browse the web",
          "version": 1.0,
      
          "icons": {
              "512": "images/icon.svg"
          },
      
          "options_ui": {
              "page": "options.html"
          },
        
          "permissions": [ "declarativeNetRequestWithHostAccess" ],
          "optional_host_permissions": [ "*://*/*" ],
        
          "content_scripts": [
              {
                  "js": [ "content.js" ],
                  "css": [ "content.css" ],
                  "matches": [ "*://*.webkit.org/*" ]
              }
          ]
      }
    • 15:13 - Dynamically registering content scripts

      let script = {
          id: "id",
          js: [ "content.js" ],
          css: [ "content.css" ],
          matches: [ "*://*.webkit.org/*" ],
          persistAcrossSessions: true
      }
      
      await browser.scripting.registerContentScripts([ script ])
    • 15:31 - Adding the scripting permission

      {
          "manifest_version": 3,
          "name": "Shiny OnTrack",
          "description": "Stay on track while you browse the web",
          "version": 1.0,
        
          "icons": {
              "512": "images/icon.svg"
          },
        
          "options_page": "options.html",
        
          "permissions": [
              "declarativeNetRequestWithHostAccess",
              "scripting"
          ],
        
          "optional_host_permissions": [ "*://*/*" ]
      }
    • 15:41 - Registering content scripts

      // scripting.js
      
      function contentScript(host) {
          return {
              id: `cs-${host}`,
              js: [ "content.js" ],
              css: [ "content.css" ],
              matches: [ `*://${host}/*`, `*://*.${host}/*` ],
              persistAcrossSessions: true
          }
      }
      
      export function registerScripts(hosts) {
          const scripts = hosts.map(contentScript)
          try {
              await browser.scripting.registerContentScripts(scripts)
          } catch {
              console.log("Failed to register content scripts")
          }
      }
    • 16:02 - Adding a host

      // host.js
      
      export async function addHost(host, blockMode) {
          if (!host)
              return
      
          const granted = await browser.permissions.request({
              origins: [`*://${host}/*`, `*://*.${host}/*`]
          })
      
          if (!granted)
              return
      
          if (blockingMode === "full")
              await createRules([ host ])
      
          await registerScripts([ host ])
      }
    • 17:06 - Web extensions storage APIs

      await browser.session.storage.set({
        key: value
      })
      
      await browser.local.storage.set({
        key: value
      })
    • 17:21 - Adding storage permission to the web extension manifest.json

      {
          "manifest_version": 3,
          "name": "Shiny OnTrack",
          "description": "Stay on track while you browse the web",
          "version": 1.0,
        
          "icons": {
              "512": "images/icon.svg"
          },
        
          "options_page": "options.html",
        
          "permissions": [
              "declarativeNetRequestWithHostAccess",
              "scripting",
              "storage"
          ],
        
          "optional_host_permissions": [ "*://*/*" ]
      }
    • 17:30 - Saving data with storage

      // storage.js
      
      export async function updateHosts(hosts) {
          await browser.storage.local.set({ hosts: hosts })
      }
      
      export async function getHosts() {
          const { hosts = [] } = await browser.storage.local.get("hosts")
          return hosts
      }
      
      export async function saveBlockMode(mode) {
          await browser.storage.local.set({ blockMode: mode })
      }
      
      export async function getBlockMode() {
          const { blockMode = "full" } = await browser.storage.local.get("blockMode")
          return blockMode
      }
    • 17:41 - Persisting hosts to storage

      // host.js
      
      export async function addHost(host, blockMode) {
          if (!host)
              return
      
          const granted = await browser.permissions.request({
              origins: [`*://${host}/*`, `*://*.${host}/*`]
          })
      
          if (!granted)
              return
      
          if (blockingMode === "full")
              await createRules([ host ])
      
          await registerScripts([ host ])
      
          let existingHosts = await getHosts()
          let updatedHosts = [ ...existingHosts, host ]
          await updateHosts(updatedHosts)
      }
    • 17:51 - Reading from storage

      // options.js
      
      let existingHosts = await getHosts()
      let blockMode = await getBlockMode()
      
      displayBlocklist(existingHosts)
    • 18:00 - Switching block modes

      // host.js
      
      export async function userDidSwitchMode(blockMode) {
          await saveBlockMode(blockMode)
      
          if (blockMode === "full") {
              let hosts = await getHosts()
              await createRules(hosts)
          } else
              await removeAllRules()
      }
    • 19:01 - Adding a background script

      {
          "manifest_version": 3,
          "name": "Shiny OnTrack",
          "description": "Stay on track while you browse the web",
          "version": 1.0,
        
          "icons": {
              "512": "images/icon.svg"
          },
        
          "options_page": "options.html",
        
          "permissions": [
              "declarativeNetRequestWithHostAccess",
              "scripting",
              "storage"
          ],
        
          "optional_host_permissions": [ "*://*/*" ],
        
          "background": {
              "scripts": [ "background.js" ],
              "type": "module"
          }
      }
    • 19:39 - Background script

      // background.js
      
      import { registerScripts } from "./utilities/scripting.js"
      import { getHosts } from "./utilities/storage.js"
      
      browser.runtime.onInstalled.addListener(async (details) => {
          if (details.reason !== "update")
              return
      
          const hosts = await getHosts()
          await registerScripts(hosts)
      })
    • 22:49 - Package your web extension into an app for Xcode

      xcrun safari-web-extension-packager --copy-resources /path/to/ShinyOnTrack
    • 23:32 - Adding the nativeMessaging permission

      {
          "manifest_version": 3,
          "name": "Shiny OnTrack",
          "description": "Stay on track while you browse the web",
          "version": 1.0,
        
          "icons": {
              "512": "images/icon.svg"
          },
        
          "options_page": "options.html",
        
          "permissions": [
              "declarativeNetRequestWithHostAccess",
              "scripting",
              "storage",
              "nativeMessaging"
          ],
        
          "optional_host_permissions": [ "*://*/*" ],
        
          "background": {
              "scripts": [ "background.js" ],
              "type": "module"
          }
      }
    • 23:40 - Sending a native message

      // background.js
      
      import { registerScripts } from "./utilities/scripting.js"
      import { getHosts } from "./utilities/storage.js"
      
      browser.runtime.onInstalled.addListener(async (details) => {
          if (details.reason !== "update")
              return
      
          const hosts = await getHosts()
          await registerScripts(hosts)
      })
      
      export async function requestBioAuth() {
          const message = { message: "requestBioAuth" }
          const response = await browser.runtime.sendNativeMessage(message)
          return response?.success
      }
    • 23:55 - Handling native messages

      // SafariWebExtensionHandler.swift
      
      import LocalAuthentication
      
      class SafariWebExtensionHandler: NSObject, NSExtensionRequestHandling {
          func beginRequest(with context: NSExtensionContext) {
              let request = context.inputItems.first as? NSExtensionItem
              let message = request?.userInfo?[SFExtensionMessageKey] as? [String: Any]
      
              if message?["message"] as? String == "requestBioAuth" {
                  let lAContext = LAContext()
                  Task {
                      do {
                          let success = try await lAContext.evaluatePolicy(
                              .deviceOwnerAuthenticationWithBiometrics,
                              localizedReason: "Authenticate to change blocked sites"
                          )
                          self.reply(context: context, success: success)
                      } catch {
                          self.reply(context: context, success: false)
                      }
                  }
              }
          }
      }
    • 24:25 - Replying to a native message

      // SafariWebExtensionHandler.swift
      
      import LocalAuthentication
      
      class SafariWebExtensionHandler: NSObject, NSExtensionRequestHandling {
          func beginRequest(with context: NSExtensionContext) {
              let request = context.inputItems.first as? NSExtensionItem
              let message = request?.userInfo?[SFExtensionMessageKey] as? [String: Any]
      
              if message?["message"] as? String == "requestBioAuth" {
                  let lAContext = LAContext()
                  Task {
                      do {
                          let success = try await lAContext.evaluatePolicy(
                              .deviceOwnerAuthenticationWithBiometrics,
                              localizedReason: "Authenticate to change blocked sites"
                          )
                          self.reply(context: context, success: success)
                      } catch {
                          self.reply(context: context, success: false)
                      }
                  }
              }
          }
      
          private func reply(context: NSExtensionContext, success: Bool) {
              let response = NSExtensionItem()
              response.userInfo = [SFExtensionMessageKey: ["success": success]]
              context.completeRequest(returningItems: [response], completionHandler: nil)
          }
      }
    • 0:00 - Introduction
    • Learn how Safari web extensions — built with HTML, CSS, and JavaScript and packaged inside an app — can run across iOS, iPadOS, macOS, and visionOS. Preview the distraction-blocker extension built throughout the session, which offers a 10-minute light mode and a full redirect mode.

    • 3:23 - Get started
    • Set up an extension from scratch by writing a manifest.json file, then add a popup UI so the extension is reachable from Safari's toolbar. The same project runs unchanged across every Apple platform that ships Safari.

    • 7:23 - Block content
    • Use the declarativeNetRequest API to block, modify, and redirect network requests, and declare the host permissions — including optional host permissions — that let users grant access on the sites where the extension should run.

    • 14:40 - Modify webpages
    • Inject content into pages with content scripts to render a countdown timer on distracting sites. Register scripts dynamically with the scripting API and persist user preferences and per-host state using the storage API and a background service worker.

    • 19:53 - Package and distribute
    • Submit a Safari web extension to the App Store using App Store Connect, and share beta builds with testers through TestFlight.

    • 22:33 - Communicate with your app
    • Generate an Xcode project with the Safari Web Extension Packager, then use native messaging to pass requests between the JavaScript extension and its containing app — unlocking platform features like Local Authentication that aren't available to web APIs.

    • 26:04 - Next steps
    • Download the sample project, explore the cross-browser WebExtensions documentation on MDN, and file feedback through Feedback Assistant or bugs.webkit.org.

Developer Footer

  • 비디오
  • WWDC26
  • Safari용 웹 확장 프로그램 생성하기
  • 메뉴 열기 메뉴 닫기
    • iOS
    • iPadOS
    • macOS
    • tvOS
    • visionOS
    • watchOS
    메뉴 열기 메뉴 닫기
    • Swift
    • SwiftUI
    • Swift Playground
    • TestFlight
    • Xcode
    • Xcode Cloud
    • SF Symbols
    메뉴 열기 메뉴 닫기
    • 손쉬운 사용
    • 액세서리
    • Apple Intelligence
    • 앱 확장 프로그램
    • App Store
    • 오디오 및 비디오(영문)
    • 증강 현실
    • 디자인
    • 배포
    • 교육
    • 서체(영문)
    • 게임
    • 건강 및 피트니스
    • 앱 내 구입
    • 현지화
    • 지도 및 위치
    • 머신 러닝 및 AI
    • 오픈 소스(영문)
    • 보안
    • Safari 및 웹(영문)
    메뉴 열기 메뉴 닫기
    • 문서(영문)
    • 튜토리얼
    • 다운로드
    • 포럼(영문)
    • 비디오
    메뉴 열기 메뉴 닫기
    • 지원 문서
    • 문의하기
    • 버그 보고
    • 시스템 상태(영문)
    메뉴 열기 메뉴 닫기
    • Apple Developer
    • App Store Connect
    • 인증서, 식별자 및 프로파일(영문)
    • 피드백 지원
    메뉴 열기 메뉴 닫기
    • Apple Developer Program
    • Apple Developer Enterprise Program
    • App Store Small Business Program
    • MFi Program(영문)
    • Mini Apps Partner Program
    • News Partner Program(영문)
    • Video Partner Program(영문)
    • Security Bounty Program(영문)
    • Security Research Device Program(영문)
    메뉴 열기 메뉴 닫기
    • Apple과의 만남
    • Apple Developer Center
    • App Store 어워드(영문)
    • Apple 디자인 어워드
    • Apple Developer Academy(영문)
    • WWDC
    최신 뉴스 읽기.
    Apple Developer 앱 받기.
    Copyright © 2026 Apple Inc. 모든 권리 보유.
    약관 개인정보 처리방침 계약 및 지침