← 모든 글

동의의 생애주기: 연령 확인 이후에 출시되는 것들

2025년 11월 4일, Apple은 App Store Server Notifications에 결제가 아니라 보호자의 결정에 반응해 발송되는 알림 유형을 추가했습니다. 바로 RESCIND_CONSENT이며, “부모 또는 보호자가 자녀의 앱 사용에 대한 동의를 철회했음”을 나타냅니다.12

이 서비스의 23개 유형 중에서 거래 정보 대신 appData 객체를 페이로드에 담는 유일한 유형이며, appData에는 “고객이 앱 내 구입을 전혀 하지 않아도” 존재하는 서명된 앱 트랜잭션이 들어 있습니다.319 따라서 지금까지 아무것도 판매한 적 없는 앱조차 알림 수신 엔드포인트를 운영해야 할 첫 번째 이유를 갖게 됩니다.

소셜 미디어 신고에서는 사용자의 연령 범위를 읽는 방법, 즉 자격(entitlement), 통과 조건, 반환된 경계값의 의미를 다뤘습니다. 그 내용은 이미 정리된 것으로 보고, 그다음 단계에서 이야기를 시작하겠습니다. 연령 확인은 그 사람이 몇 살인지에 답합니다. 여기서 다루는 세 개의 API는 누가 승인하는지, 이미 받아둔 승인이 앱의 변경 이후에도 유효한지, 그리고 보호자가 승인을 거둬들이면 무슨 일이 벌어지는지에 답합니다.21012

핵심 요약

  • 연령 확인의 하위 단계에 세 개의 API가 놓여 있고, Apple은 이를 하나의 묶음으로 제공합니다. PermissionKit의 Significant Change API, StoreKit의 AppStore.ageRatingCode, 그리고 RESCIND_CONSENT 서버 알림입니다.45
  • Apple은 이 네 가지 도구 목록을 텍사스 한 곳이 아니라 텍사스, 유타, 루이지애나에 함께 연결하며, 이 세 곳에 대해 제시한 날짜는 모두 이미 지났습니다. 그리고 Apple은 그 의무의 범위를 “법적으로 요구되는 특정 지역”으로 한정합니다.5815
  • 이 중 무엇이라도 실행할지를 결정하는 관문은 26.4에서 새로 도입된 AgeRangeService.requiredRegulatoryFeatures이며, 이 관문이 통제하는 흐름을 온전히 작성하려면 iOS 26.5가 필요합니다.736
  • Apple은 중대한 변경이 무엇인지 정의하기를 거부하며, 그 사실을 네 곳에서 반복해 밝히고 법률 자문으로 안내합니다. Apple이 공개한 단 하나의 구체적 사례조차 법률 조항에서 끌어온 것입니다. Apple은 텍사스 법이 앱의 연령 등급 변경을 그에 해당한다고 본다고 적었습니다.48
  • 개발자가 빌드를 올리지 않아도 등급은 바뀔 수 있습니다. Apple은 2026년 6월 18일 호주의 15+ 등급을 폐지하고 베트남에는 새로운 4단계 체계를 부여했는데, 어느 개발자에게도 무엇을 제출하라고 요구하지 않았습니다.937
  • 철회는 아무도 구현하지 않는 분기이고, Apple 문서도 사실상 마찬가지입니다. RESCIND_CONSENT는 서버 팀이 실제로 읽는 페이지의 생애주기 표 어디에도 등장하지 않습니다.2

부여, 재부여, 철회. Apple은 앞의 두 경로를 샘플 프로젝트와 샌드박스 테스트 매트릭스로 문서화했습니다. 세 번째에 주어진 것은 열거형 값 하나와 필드 네 개뿐인데, 알고 보니 제 코드에서 그 분기가 받은 관심의 양도 딱 그 정도였습니다.

Apple이 네 번에 걸쳐 발표한 일정

아래의 모든 날짜는 Apple의 개발자 뉴스에서 그대로 가져온 것이며, 개별 항목보다 그 순서가 더 중요합니다.

Apple은 2025년 10월 8일 텍사스 SB2420을 알리면서 “2026년 1월 1일부터”라는 날짜를 붙였고, 법 조문이 아니라 자사의 대응을 설명했습니다. 18세 미만 사용자를 위한 신규 Apple 계정은 가족 공유 그룹에 참여하게 되며, “부모 또는 보호자가 미성년자의 모든 App Store 다운로드, 앱 구입, 그리고 Apple의 앱 내 구입 시스템을 이용한 거래에 대해 동의를 제공해야 한다”는 내용이었습니다.13 11월 4일 Apple은 도구의 이름을 밝히고 26.2 베타에 이를 포함시켰습니다.4 12월 23일 계획은 멈췄습니다. “최근 지방법원이 발부한 가처분으로 텍사스주 법 SB2420의 집행이 정지되었습니다. … Apple은 앞서 발표한 구현 계획을 보류합니다.”14 그리고 2026년 6월 3일 다시 시작됐습니다. “최근 텍사스주 법 SB 2420에 대한 가처분을 해제한 법원 결정에 따라 … 이 변경 사항은 2026년 6월 4일부터 적용됩니다.”5

법 하나를 두고 8개월간 오갔지만, API는 단 한 번도 흔들리지 않았습니다. 26.2에서 출시됐고, 가처분 기간 내내 샌드박스 테스트가 가능했으며, 가처분이 풀렸을 때 이미 준비된 상태로 기다리고 있었습니다.14 12월의 보류를 미뤄도 된다는 허락으로 읽은 쪽은 다섯 달의 여유를 날린 셈입니다.

나머지 지도는 2026년 2월 24일에 도착했고, 텍사스 너머까지 뻗어 있습니다.

관할 지역 Apple이 적용된다고 밝힌 날짜 Apple이 적용된다고 밝힌 내용
호주, 브라질, 싱가포르 2026년 2월 24일 Apple은 “합리적인 방법으로 성인임이 확인되지 않는 한” 18+ 등급 앱의 다운로드를 차단합니다15
유타 2026년 5월 6일 요청 시 신규 Apple 계정의 연령 카테고리 공유15
텍사스 2026년 6월 4일 신규 Apple 계정이 “이제 해당 법의 적용을 받습니다”. 18세 미만 미성년자를 대신한 다운로드, 앱 내 구입, 중대한 변경에 대한 동의가 필요하며, 보호자는 이를 철회할 수 있습니다5
루이지애나 2026년 7월 1일 요청 시 신규 Apple 계정의 연령 카테고리 공유15

텍사스가 예외적인 지점은 기준 연령이지 도구가 아닙니다. Apple은 텍사스를 “18세 미만 미성년자를 대신한” 동의로 설명하고 카테고리를 “13세 미만, 13-15세, 16-17세, 18세 초과”로 공개하는데, 이는 해당 API가 만들어내는 결과와 정확히 일치합니다. “최대 세 개의 연령 기준점을 지정할 수 있으며, 이는 최대 네 개의 연령 범위를 만듭니다.”4529 유타와 루이지애나에 대해서는 요청 시 연령 카테고리 공유를 명시한 뒤, 네 가지 도구 모두가 “루이지애나와 유타의 규정 준수 의무를 개발자가 충족할 수 있도록 확장되었다”고 밝히면서 그중 하나로 Significant Change API를 지목합니다.15 도구는 텍사스 전용이 아닙니다. 의무 자체가 텍사스 전용인지에 대해서는 Apple이 답하기를 사양합니다. 의무의 범위를 “법적으로 요구되는 특정 지역”으로 한정하고, 그 질문은 법률 자문에 넘깁니다.8

이 표를 법률에 대한 진술이나 규정 준수 판단으로 읽지 마세요. 여기 기록된 것은 Apple이 발표한 내용과 Apple이 거기에 붙인 날짜뿐입니다. 저는 변호사가 아니라 엔지니어이며, 아래의 모든 내용은 API의 경계에서 멈춥니다.

규정 준수 관련 질문을 법률 자문으로 넘기는 Q&A에는 릴리스 계획 측면에서 반가운 소식이 하나 담겨 있습니다. 이로 인해 심사가 달라지느냐는 질문에 Apple은 “아니요, App Review 절차에는 변경 사항이 없습니다”라고 답합니다.8 의무가 발생하는 지점은 제출 시점이 아니라, 명시된 관할 지역에서의 런타임입니다.

그래서 구분은 깔끔합니다. 텍사스, 유타, 루이지애나에서 여러분의 앱에 동의 의무가 있는지는 그 지역 자격을 갖춘 변호사에게 물을 문제입니다. 반면 어떤 SDK를 기준으로 빌드할지는 그렇지 않습니다. Apple은 프레임워크에 접근하려면 “iOS 26.2 및 iPadOS 26.2 SDK 이상을 대상으로, Xcode 26.2(17C52) 이상에서 앱을 빌드해야 한다”고 명시하며, iOS 18 이하의 기존 계정은 “영향을 받지 않는다”고 밝힙니다.8

Apple은 “중대함”이 무엇인지 알려주지 않습니다

정의의 공백은 이 기능의 한복판에 놓여 있는데, Apple 문서 네 곳은 이를 채우는 대신 그대로 개발자에게 돌려보냅니다. 심볼 페이지(“적용되는 규제에 따라 무엇이 중대한 업데이트에 해당하는지는 개발자가 판단합니다”),12 두 건의 뉴스 게시물(“앱에 중대한 변경이 발생한 시점을 판단하는 것은 개발자의 책임입니다”),45 그리고 약관이나 개인정보 처리방침 변경이 해당하느냐는 직접적인 질문을 받은 Q&A(“경우에 따라 다릅니다. 적용되는 법률에 따라 무엇이 중대한 앱 업데이트인지는 개발자가 판단합니다”)가 그렇습니다.8

Apple이 제시하는 실제 사례는 정확히 하나뿐이며, 그것도 Apple이 아니라 법 조항에서 온 것입니다. “텍사스주 법은 앱의 연령 등급 변경을 중대한 변경으로 간주하므로, 개발자는 App Store Connect에서 연령 등급 선택을 최신 상태로 유지해야 합니다.”4

Apple이 정의해주는 것은 개발자가 작성할 문자열입니다. SignificantAppUpdateTopic은 좋은 예와 나쁜 예를 나란히 제시하는데, 심볼 페이지치고는 이례적으로 솔직합니다.

// Specific
let topic = SignificantAppUpdateTopic(
     description: "This update adds video calling and location sharing features."
)

// Vague
let topic = SignificantAppUpdateTopic(
    description: "We've made improvements to the app."
)

이 예제 주변에 붙은 Apple의 지시는 단도직입적입니다. “앱에서 무엇이 바뀌었는지 명확하게 설명하는 간결하고 이해하기 쉬운 표현을 사용하십시오. 부모와 보호자는 권한을 부여할지 결정할 때 이 설명을 봅니다.”12 이제 릴리스 노트의 상투적인 문구가, 자녀가 앱을 계속 쓸 수 있을지 판단하는 부모 앞에 놓입니다. 대부분의 앱이 앞으로 내보낼 변경 이력 문자열 중 가장 큰 것이 걸린 문장이 되는 셈입니다.

정의는 흐릿해도 거기에 딸린 의무는 전혀 흐릿하지 않습니다. Apple은 발동 조건에 대해서는 말을 아끼면서도, 개발자가 “필요한 경우 앱 또는 기능에 대한 접근을 차단할 책임”을 진다고 규정한 뒤 단호하게 덧붙입니다. “부모가 동의를 제공하기 전까지 자녀는 해당 중대한 업데이트에 접근하지 못하도록 차단되어야 하며, 여기에는 앱과 계정 데이터 전체 또는 특정 기능이 포함될 수 있습니다.”8 “앱과 계정 데이터 전체”라는 표현이 상당한 무게를 지고 있습니다. Apple은 차단 범위를 개발자에게 맡기면서, 그 상한선으로 계정 전체를 지목합니다.

상태 기계, 그리고 Apple 샘플이 새는 지점

Apple은 “Implementing age assurance and permissions”라는 샘플 프로젝트를 공개했고, 이것이 전체 흐름을 처음부터 끝까지 설명하는 유일한 자료입니다.6 이를 상태 기계로 읽으면 네 개의 분기와, 다시 쓸 만한 예제 하나가 드러납니다.

첫 번째 분기는 비용이 가장 적게 드는 쪽입니다. AgeRangeService.requiredRegulatoryFeaturesSet<AgeRangeService.RegulatoryFeature>를 반환하며, 구성원은 declaredAgeRangeRequired, significantAppChangeRequiresParentalConsent, significantAppChangeRequiresAdultNotification 세 가지입니다.7 Apple의 샘플은 이를 가장 먼저 확인하고, “두 기능 중 어느 것도 존재하지 않으면 앱은 흐름 전체를 건너뜁니다.”6 Apple은 이 속성이 사용자의 “지역 및 계정 설정”을 반영한다고 설명하는데, 저는 이를 명시된 관할 지역 밖의 사용자라면 빈 값이 돌아온다는 약속으로 읽습니다. 다만 Apple이 그런 종류의 보장을 한 적은 없습니다.7

나머지 분기는 나이와, 그 나이가 어떻게 확인됐는지에 따라 갈립니다.

대상 필요한 규제 기능 Apple 샘플의 동작
미성년자 significantAppChangeRequiresParentalConsent 보호자에게 PermissionQuestion을 전송6
성인, 확인된 수단 있음 significantAppChangeRequiresAdultNotification 시스템 확인 시트를 표시6
성인, 확인된 수단 없음 둘 중 하나 “설정에서 계정을 확인할 때까지” 접근 차단6
공유 거부 둘 중 하나 해당 케이스를 파싱하지만, 그에 대한 분기는 없음6

곱씹어볼 것은 세 번째 행입니다. Apple 계정에 결제 수단을 한 번도 등록하지 않은 성인은, 어린이를 보호하기 위해 만들어진 흐름에서 Apple의 레퍼런스 구현에 의해 차단됩니다. Apple은 대체 분기도, 우회 수단도 제공하지 않습니다.

공개된 문서들을 종합하면 분기 처리는 다음과 같은 모습이 됩니다. 성인과 미성년자의 구분은 Apple 샘플과 샌드박스 테스트 매트릭스를 따랐는데, 매트릭스에서 18세 이상 계정은 lowerBound가 18이고 상한이 없으며 ageRangeDeclarationconfirmed 또는 selfDeclared로 돌아옵니다. 사용 가능 버전의 하한선에 주의하세요. .confirmed를 읽으려면 iOS 26.5가 필요한데, 이는 이 흐름의 다른 모든 요소보다 한 릴리스 뒤입니다.61136

import DeclaredAgeRange
import PermissionKit
import SwiftUI

@available(iOS 26.5, *)
struct SignificantChangeGate: View {
    enum Phase {
        case checking
        case clear
        case awaitingGuardian(PermissionQuestion<SignificantAppUpdateTopic>)
        case blocked
    }

    let changeDescription: String
    @Environment(\.requestAgeRange) private var requestAgeRange
    @Environment(\.showSignificantUpdateAcknowledgment) private var acknowledge
    @State private var phase: Phase = .checking

    var body: some View {
        switch phase {
        case .checking:
            ProgressView().task { await resolve() }
        case .clear:
            ChangedFeatureView()
        case .awaitingGuardian(let question):
            PermissionButton(question: question) { Text("Ask a parent to approve") }
        case .blocked:
            AccountVerificationPrompt()
        }
    }

    private func resolve() async {
        let features = (try? await AgeRangeService.shared.requiredRegulatoryFeatures) ?? []
        guard !features.isEmpty else {
            phase = .clear                 // nothing applies to this person
            return
        }
        guard let response = try? await requestAgeRange(ageGates: 18),
              case let .sharing(range) = response else {
            phase = .blocked               // declined or unavailable: Apple documents no branch
            return
        }
        let isAdult = range.lowerBound == 18
        let isConfirmed = range.ageRangeDeclaration == .confirmed

        switch (isAdult, isConfirmed) {
        case (true, true) where features.contains(.significantAppChangeRequiresAdultNotification):
            try? await acknowledge(updateDescription: changeDescription)
            phase = .clear
        case (true, false):
            phase = .blocked               // adult with no confirmed method
        case (true, true):
            phase = .clear
        case (false, _):
            guard features.contains(.significantAppChangeRequiresParentalConsent) else {
                phase = .clear
                return
            }
            let topic = SignificantAppUpdateTopic(description: changeDescription)
            phase = .awaitingGuardian(PermissionQuestion(significantAppUpdateTopic: topic))
        }
    }
}

질문을 보내는 쪽은 쉬운 절반입니다. 답을 받는 쪽에서 Apple 샘플이 새고, 그 예제는 꼼꼼히 읽어볼 만큼 짧습니다.6

for await response in AskCenter.shared.responses(for: SignificantAppUpdateTopic.self) {
    guard response.choice.answer == .approval else {
        return
    }
    versionManager.handleAllChanges()
}

for await 안의 return은 시퀀스를 통째로 버립니다. 한 번 거부당하면 앱은 다음 실행 전까지 그 주제를 더 이상 관찰하지 않으므로, 거부를 누른 보호자가 1분 뒤 마음을 바꿔 승인해도 그 승인은 아무도 읽지 않는 스트림으로 흘러들어 갑니다. continue를 쓰고 거부 사실을 기록하세요. 이 예제를 의도가 아니라 결함으로 읽는 것은 저의 추론입니다. 어느 쪽이든 수정 비용은 키워드 하나입니다. 리스너는 백그라운드 실행이 없다는 전제로 설계하세요. 그것을 약속했던 것은 이 API가 대체한, 사용이 중단된 시퀀스뿐이었습니다.2021

대기는 하나의 상태이며, 개발자가 통제할 수 있는 만료 시간이 없습니다

생애주기의 중간 지점이 실제 설계 작업이 놓인 곳이고, 문서화된 다섯 가지 동작이 그 모양을 결정합니다. Apple이 절반만 답해준 것부터 보겠습니다. PermissionQuestion.expirationDateOptional<Date>이며 그 시점이 지나면 “질문을 받은 사람이 더 이상 응답할 수 없게” 되는데, Apple은 이에 대한 기본값을 공개하지 않았습니다.16

보호자가 질문을 보기도 전에 자녀가 취소할 수 있습니다. “요청 전송 흐름 중 어느 시점에서든 자녀는 요청을 취소할 수 있습니다. … 이 경우 시스템은 해당 질문에 대한 응답을 호출한 앱에 전달하지 않습니다.”17 응답도, 오류도, 콜백도 없습니다. 직접 만료 처리를 하지 않으면 대기 상태는 영원히 대기로 남습니다.

성인에게 물으면 예외가 발생합니다. 심볼 페이지가 비워둔 케이스를 샌드박스 문서가 설명합니다. “성인 사용자에 대해 AskCenter.ask(_:)를 호출하면 이 오류가 발생하는데, 성인은 보호자 권한 요청의 요건을 충족하지 않기 때문입니다.”11 AskError.notAvailable은 자체 페이지에 개요 설명이 없는 두 케이스 중 하나이자, 성인이 유발하는 바로 그 케이스입니다.18 예외를 잡아서 처리하지 말고, 묻기 전에 나이로 경로를 나누세요.

승인 상태는 앱 컨테이너가 아니라 iCloud에 두어야 합니다. Apple의 샘플은 확인된 각 변경 사항을 NSUbiquitousKeyValueStore에 기록하고 didChangeExternallyNotification을 관찰해 “다른 기기에서 같은 흐름을 다시 표시하지 않도록” 합니다.6 iPhone에서 승인한 부모가 iPad에서 두 번째 요청을 받아서는 안 되는데, PermissionKit은 그런 일을 대신 해주지 않습니다.

샘플에서 가장 훌륭한 대목은 대부분의 팀이 잘못 출시했을 문제를 해결합니다. 새로 설치한 사용자가 겪어본 적도 없는 변경에 동의할 필요는 없으므로, Apple은 AppTransaction.originalAppVersion을 읽어 “그 버전 이전 또는 그 버전에서 도입된 변경 사항은 자동으로 처리 완료로 표시”합니다.619 따라서 동의 추적은 변경별로, 그리고 사용자별로 이루어집니다. 불리언 하나가 아니라, 도입 버전이 붙은 변경 식별자가 필요합니다.

빌드 없이도 등급은 바뀝니다

AppStore.ageRatingCodeInt?를 비동기로 반환하는 static var이며, iOS, iPadOS, macOS, tvOS, visionOS, watchOS 전반에서 26.2부터 사용할 수 있습니다.10 Apple이 밝힌 용도는 해석이 아니라 비교입니다. “이 속성으로 앱의 연령 등급을 가져와 마지막으로 알고 있던 등급과 비교하여 변경 여부를 확인하십시오.”10 비교밖에 할 수 없는 이유는, Apple이 정수와 등급 단계 사이의 대응 관계를 문서 어디에도 공개하지 않았기 때문입니다. 지금 13+인지 물을 수는 없고, 지난번과 같은지만 물을 수 있습니다. 그러니 실질적인 계약은 이전 값을 직접 저장하고 첫 실행을 스스로 책임지는 것입니다.

앱이 왜 자기 등급을 감시해야 하는지는, 등급이 릴리스 없이 움직인다는 사실을 알아차리는 순간 분명해집니다. 2026년 5월 21일 Apple은 6월 18일부터 “호주 App Store에서 15+ 연령 등급을 더 이상 사용할 수 없게 된다”고 발표하면서, 베트남에는 기존 설문 응답에서 도출한 지역 전용 4단계 체계를 부여했습니다.9 둘 다 실제로 적용됐습니다. App Store Connect 도움말의 호주 표에는 이제 16+와 R 18+만 있고 15+ 행이 없으며, 베트남은 자체 표를 갖게 됐습니다.37 어느 변경도 제출이나 빌드, 개발자의 어떤 조치도 요구하지 않았고, Apple은 텍사스 법이 등급 변경을 중대한 변경으로 본다고 적었습니다.4

개발자가 직접 일으키는 등급 변경과 비교해 보세요. “개발자가 앱의 연령 등급을 업데이트하면, 해당 버전이 배포되는 즉시 모든 사용자 기기에서 등급이 갱신됩니다.”4 자신이 만든 변경은 계측할 수 있는 릴리스를 타고 갑니다. Apple의 변경은 그렇지 않고, 그 빈틈을 이 속성이 메웁니다.

테스트 문제는 빈틈 정도가 아닙니다. Apple 직원의 개발자 포럼 답변이 이를 직접 언급합니다. “앱을 Xcode 및 샌드박스 환경(TestFlight 포함)에서 빌드하고 실행하는 개발 과정에서는 0이 나오는 것이 정상입니다.”22 0nil이 아니므로, Apple 문서에 실린 예제조차 guard let을 통과시켜 0을 유효한 등급으로 넘겨줍니다. 그리고 그 스레드를 올린 개발자가 실기기에서 본 것이 정확히 그 상황이었습니다.1022

여기까지 따라가 봅시다. 0을 기준값으로 저장해두면, App Store에서 첫 실행될 때 실제 코드를 읽고 변경이 있었다고 판단해 아무것도 아닌 일에 대해 부모에게 재동의를 요청하게 됩니다. 0을 절대 기준값으로 기록하지 않는 센티널로 취급하라는 것은 Apple의 두 진술에서 끌어낸 저의 추론이며, 이것만은 넣지 않고는 출시하지 않을 방어 코드 한 줄입니다.

Apple의 제공 범위 메타데이터에는 어느 설명 문서에도 적혀 있지 않은 사실이 하나 더 있습니다. 이 네 가지 기능의 플랫폼 행은 서로 다른 자리에 구멍을 냅니다.

기능 iOS / iPadOS macOS Mac Catalyst visionOS tvOS / watchOS
AppStore.ageRatingCode10 26.2 26.2 행 없음 26.2 26.2
requiredRegulatoryFeatures7 26.4 26.4 26.4 행 없음 행 없음
SignificantAppUpdateTopic, PermissionButton1226 26.2 26.2 26.2 26.2 행 없음
showSignificantUpdateAcknowledgment27 26.4 행 없음 26.4 행 없음 행 없음

여기서 두 가지 비대칭이 드러납니다. 네이티브 macOS 앱은 어떤 사용자에게 significantAppChangeRequiresAdultNotification이 적용된다는 사실을 알 수 있지만, 그것을 충족시킬 API가 없습니다. showSignificantUpdateAcknowledgment(in:updateDescription:)UIWindowScene을 받으며 macOS 행을 공개하지 않고, SwiftUI의 SignificantUpdateAction도 같은 세 플랫폼에서 멈춥니다.27 Apple은 이웃한 requestAgeRange 호출에는 NSWindow 변형을 제공했으므로, 이 누락은 정책이라기보다 빈틈으로 읽힙니다. 이는 저의 추론입니다.28

두 번째로, 감지가 대응보다 멀리 뻗습니다. ageRatingCode는 tvOS와 watchOS 행을 공개하지만, 동의를 요청하는 어떤 것도 그렇지 않습니다.10 이 대상 플랫폼들은 등급 변경을 관찰할 수는 있어도 문서화된 대응 수단이 없는데, Return은 둘 다 출시합니다. tvOS 1.0.1은 App Store Connect에서 READY_FOR_DISTRIBUTION 상태이고, 배포된 iOS 앱은 watch 앱을 내장하고 있습니다.30 두 해석 모두 Apple의 메타데이터가 완전하다는 전제에 서 있는데, 정작 그 메타데이터가 그 전제를 흔듭니다. 이웃한 심볼들은 모두 공개하는 Mac Catalyst 행을 ageRatingCode만 공개하지 않습니다.10

Apple이 실행을 막은 뒤에도 서버가 필요한 이유

Apple은 철회 시의 동작을 한 문장으로 밝힙니다. “부모 또는 보호자가 자녀의 앱 접근에 대한 동의를 철회하면, Apple은 해당 앱의 실행을 차단합니다. 동의 철회를 처리하려면 notificationTypeRESCIND_CONSENT 값을 사용하십시오.”8

이 두 문장의 순서를 보세요. 시스템이 이미 접근을 차단하므로 RESCIND_CONSENT는 집행용 훅이 아니며, 그렇게 구현하면 이 알림을 허비하는 셈입니다. 이것은 여러분의 코드가 더 이상 실행될 수 없는 기기의 사용자에 대해 받을 수 있는 유일한 신호입니다. Apple은 이 이벤트를 어떻게 처리해야 하는지에 대해 아무 말도 하지 않으므로, 제가 찾은 가장 가까운 비유는 계정 삭제 요청입니다. 정산해야 할 구독, 동결해야 할 상태, 그리고 다시 나타날 수도 있는 가족이 있습니다.

페이로드는 이례적으로 빈약합니다. appData가 담는 필드는 appAppleId, bundleId, environment, signedAppTransactionInfo 네 개입니다.3 거래도, 구독도, 갱신 정보도, 여러분 쪽의 계정 식별자도 없습니다. 사람과 연결되는 고리는 서명된 앱 트랜잭션을 통하며, 그 appTransactionID는 App Store가 “앱을 다운로드한 각 Apple 계정마다, 그리고 가족 공유를 지원하는 앱의 경우 각 가족 그룹 구성원마다” 생성합니다.19 그러니 무료 앱에도 대조에 쓸 수 있는 계정별 영속 키가 존재하며, 그것을 한 번도 저장해두지 않은 앱은 누구의 것인지 알 수 없는 알림을 받게 됩니다.

전송 방식에는 놀랄 것이 없습니다. App Store Connect에서 환경별로 버전 2 URL을 등록하고, TLS 1.2 이상을 사용하며, 허용 목록에 17.0.0.0/8을 넣고, 성공은 200에서 206, 40x50x를 반환하면 프로덕션에 한해 1, 12, 24, 48, 72시간 간격으로 다섯 번의 재시도를 얻습니다.2324

RESCIND_CONSENT에는 서브타입도 없습니다. 공개된 19개 서브타입 값은 각각 특정 알림 유형에 한정되어 있고, RESCIND_CONSENT라는 문자열은 그 페이지 어디에도 등장하지 않습니다.25 더 시사적인 것은, Apple의 notificationType 페이지가 “앱 내 구입 생애주기 이벤트 처리 사례” 아래 여덟 개 표에 40개 이벤트를 정리해두었는데 RESCIND_CONSENT는 그중 어디에도 없다는 점입니다. 이 값은 가능한 값 목록에만 존재할 뿐, 그 페이지의 다른 어느 곳에도 나오지 않습니다.2 Apple은 이 알림을 연령 확인 자료에 문서화해놓고, 정작 서버 팀이 실제로 읽는 레퍼런스에는 연결해두지 않았습니다.

제 프로젝트들이 이미 잘못하고 있는 것

남의 코드를 논하기 전에 제 코드부터 뒤졌습니다. 대상 선정 규칙은 기계적이었는데, 버그는 바로 그 규칙에 있었습니다. 제 CLAUDE.md의 Active Projects 표에서 뒤에 Xcode 프로젝트가 있는 모든 행을 골랐고, 그 결과 7개 프로젝트, 모든 Swift 파일과 entitlements 파일, 속성 목록, project.pbxproj를 합쳐 508개 파일이 대상이 됐습니다.31 PermissionKit, AskCenter, SignificantAppUpdateTopic, FamilyControls, ManagedSettings, DeviceActivity, CKShare, sharedCloudDatabase, ageRatingCode 중 어느 것도 일치하는 결과가 없었습니다. 7개 중 5개는 App Store Connect 레코드를 갖고 있습니다. Water와 Yawara는 레코드가 없고 출시된 적도 없으니, 이 둘은 누군가의 손에 들어간 앱이 아니라 코드로 읽으시면 됩니다.31 미성년자 사용자가 가장 있을 법해 보였던 Ace Citizenship은 오히려 가능성이 가장 낮습니다. 동반 사이트가 N-400 자격을 “만 18세 이상”으로 명시하고 있고, 앱은 생년월일을 어디에서도 묻지 않습니다.31

그다음 ~/Projects 아래의 모든 저장소를 대상으로 동일한 패턴을 돌렸는데, 이것이 처음부터 했어야 할 스캔이었습니다. 같은 패턴이 21개 파일에 일치했고 그중 7개가 코드이며, 그 7개 중 6개가 레지스트리에 행조차 없었던 하나의 iOS 프로젝트에 몰려 있습니다.32

Randori는 주짓수 훈련 기록 앱이고, 이 패턴 목록이 존재하는 이유인 바로 그 동의 표면을 갖고 있습니다. ConnectionStore.swift, RandoriApp.swift, CKPostTransport.swift, ProfileCardView.swift, CloudShareSheet.swift, SocialContracts.swift에 걸쳐 CKSharesharedCloudDatabase가 20건 일치하며, 수락 처리는 앱이 이미 실행 중일 때는 userDidAcceptCloudKitShareWith를 통해, 콜드 런치에서는 connectionOptions.cloudKitShareMetadata를 통해 이뤄집니다.32 Randori는 남의 이름을 다는 문제에 대해 자체 동의 장치까지 갖추고 있습니다. TagConsentStore는 실패 시 차단(fail closed) 방식이라, 상대가 태깅 정책을 게시하지 않았다면 그 선수는 아예 태그될 수 없습니다.32

결국 실제 동의 표면을 가진 유일한 프로젝트는 자체 장부를 만들었고 Apple의 것은 하나도 채택하지 않았습니다. 이 프로젝트에는 제가 아직 구현하지 않은 세 가지 숙제가 있습니다. 계약 코드가 잠재워둔 사람 대 사람 기능을 켜는 것은 교과서적인 SignificantAppUpdateTopic 사례입니다. ageRatingCode는 저장된 기준값이 없고, 아직 출시되지 않은 1.0은 Xcode, 샌드박스, TestFlight에서만 실행됐는데 그곳이 바로 Apple이 이 속성은 0을 반환한다고 말한 환경입니다.22 철회 알림은 도착할 곳조차 없습니다. Randori는 아무것도 판매하지 않고 자체 서버도 운영하지 않습니다.32

더 흥미로운 발견은, 원래의 7개 프로젝트 중 셋에 이미 보호자 동의가 다른 이름으로 들어가 있다는 사실입니다.

구입 요청(Ask to Buy)은 같은 모양의 같은 메커니즘이고, Apple도 거의 같은 표현으로 설명합니다. “구입 요청을 사용하면, 자녀가 대상 구입이나 다운로드를 하려 할 때 시스템이 구입 요청을 부모 또는 보호자에게 전송합니다.”34 이는 Product.PurchaseResult.pending으로 드러나며, 승인은 호출 지점이 아니라 Transaction.updates를 통해 도착합니다. 그 시퀀스가 “구입 요청 거래와 같이 앱 외부에서 발생하는 거래”를 전달하기 때문입니다.35 거부하면 아무것도 오지 않습니다. “구입 요청을 거부했으므로 앱은 거래를 수신하지 않습니다.”34

7개 중 셋이 무언가를 판매하고, 각자 대기 상태를 다르게 처리합니다.33

상품 .pending 처리
ResumeGeni 월간 구독 이름이 붙은 .pending 결과와 문서화된 정산 경로
Reps 구독, 2개 등급 case .userCancelled, .pending:로 한 분기에 합침
Ace Citizenship 비소모성 별도의 case .pending:false를 반환, 취소와 동일

셋 중 둘이 보호자의 숙고를 사용자의 취소로 보고하며, 사용자가 실제로 겪는 일은 잘못된 라벨보다 더 나쁩니다. Reps는 purchasetrue를 반환할 때만 페이월을 닫고, Ace는 자체 호출이 성공을 반환할 때만 페이월을 넘어갑니다. .pending에서는 두 호출 모두 실행 가능한 값을 돌려주지 않으므로, 두 페이월 모두 오류도 토스트도 스피너도 없이 그대로 남아 있습니다. 눌러도 아무 일도 일어나지 않는 탭입니다.33 몇 분 뒤 부모가 승인하면, 그 거래는 요청이 있었다는 사실조차 화면에 드러낸 적 없는 리스너로 떨어집니다.

합쳐버린 분기는 PermissionKit이 더 큰 위험을 걸고 유도하는 바로 그 실패이며, Apple이 이 패턴에 두 번째 API를 부여하기 전에 저는 이미 그 실패를 두 번 출시했습니다. 대기는 특수한 분기가 아닙니다. 대기란 동의 흐름을 앱 안에서 본 모습 그 자체이며, 올바른 기본값은 반환하는 값이 아니라 화면에 그리는 상태입니다.

한 앱은 이미 서버 쪽 절반의 하류에 있는데, 덕분에 빠진 분기가 구체적으로 드러납니다. ResumeGeni의 버전 2 엔드포인트는 2026년 6월 26일 이후 최소 56건의 알림을 기록했고, 그중 55건이 샌드박스, 1건이 프로덕션에서 왔습니다. 두 환경 URL이 모두 등록되어 있음을 이렇게 알 수 있습니다. 이 앱의 핸들러는 13개 알림 유형의 이름을 갖고 있고 그중 8개를 실제로 분기 처리하며, 나머지 5개는 주석에만 등장합니다. RESCIND_CONSENT는 어느 쪽에도 없고, 저장소의 다른 어디에도 없습니다.33

자주 묻는 질문

보호자에게 동의를 요청하는 API와 성인에게 요청하는 API는 각각 무엇인가요?

프레임워크가 다르고, 거꾸로 알기 쉬운 구분입니다. 보호자 동의는 PermissionKit을 거칩니다. SignificantAppUpdateTopicPermissionQuestion(significantAppUpdateTopic:)으로 감싸 PermissionButton으로 보내고, AskCenter.shared.responses(for:)로 응답을 받습니다.12162126 성인의 변경 확인은 그 대신 Declared Age Range를 거치며, showSignificantUpdateAcknowledgment(in:updateDescription:)가 그 역할을 합니다.27 먼저 requiredRegulatoryFeatures를 확인하세요. PermissionKit으로 성인에게 물으면 AskError.notAvailable이 발생하기 때문입니다.711

실제 가족 계정 없이 동의 철회를 어떻게 테스트하나요?

개발자 모드를 켠 뒤 설정 → 개발자 → 샌드박스 Apple 계정에서 로그인하고, 계정을 선택한 다음 관리를 탭해 앱 동의 철회를 선택하세요. 번들 식별자를 입력하고 동의 철회를 탭하면 시스템이 “Notification Triggered”를 표시합니다.11 버전 2 URL을 설정해두었다면, 서버는 appData 객체가 담긴 RESCIND_CONSENT를 수신하며 그 안의 bundleIdenvironment 필드로 알림이 올바른 앱에 해당하는지 확인할 수 있습니다.311 샌드박스는 각 알림을 재시도 없이 한 번만 보내므로, 테스트 도중 50x를 반환한 엔드포인트에는 두 번째 기회가 없습니다.24

앱이 왜 런타임에 자기 연령 등급을 감시해야 하나요?

Apple이 개발자의 제출 없이도 스토어프런트에서 등급을 바꾸기 때문이고, Apple이 텍사스 법은 등급 변경을 중대한 변경으로 본다고 적은 뒤 보호자 동의를 요청하라며 Significant Change API를 가리키기 때문입니다.4 2026년 6월 18일이 그 실제 사례입니다. 호주는 15+ 등급을 잃었고 베트남은 4단계 체계를 얻었으며, 둘 다 기존 설문 응답을 바탕으로 기존 앱에 적용됐습니다.937 Apple은 이 속성의 정수와 등급 단계 사이의 대응표를 공개하지 않으므로, 직접 저장해둔 값과의 비교가 이 속성이 지원하는 유일한 연산입니다.10

시스템이 이미 차단하며, 대부분의 구현이 놓치는 지점이 바로 이것입니다. Apple은 보호자가 동의를 철회하면 “Apple이 앱 실행을 차단한다”고 밝힌 뒤, 이 알림은 집행이 아니라 이벤트 처리를 위한 것이라고 안내합니다.8 그러니 남은 일은 서버 쪽의 모양을 띱니다. 계정 상태를 동결하고, 구독이 있으면 정산하고, 여러분의 앱을 열 수 없는 기기로 푸시를 보내지 마세요. 실패한 인증 검사가 아니라 계정 삭제를 대하듯 다루시면 됩니다.

핵심 정리

iOS 개발자를 위해: - 오늘 당장 기존 Product.PurchaseResult switch 문을 점검하세요. 구입 요청은 이미 출시되어 있는 보호자 동의이고, .pending이 그것이 드러나는 방식이며, 그 케이스를 .userCancelled에 합치는 것이 PermissionKit이 더 큰 위험을 걸고 유도할 바로 그 결함입니다.3435 - 확인된 성인과 자가 선언한 성인을 구분하는 흐름이라면 26.4가 아니라 iOS 26.5를 기준으로 잡으세요.36 - Apple 샘플의 리스너를 복사하기 전에 return을 고치고, 저장해둔 ageRatingCode 기준값에 0이 절대 들어가지 않게 하세요.622

여러 Apple 플랫폼에 출시하는 팀을 위해: - 작업을 계획하기 전에 제공 범위 행부터 확인하세요. 네이티브 macOS 앱은 성인의 변경 확인이 필요하다는 것을 감지할 수 있지만 그것을 표시할 API가 없고, tvOS와 watchOS는 ageRatingCode를 읽을 수 있지만 그 뒤를 받쳐줄 동의 API가 없습니다.71027 - 확인 기록은 변경별 키로 NSUbiquitousKeyValueStore에 저장하고, AppTransaction.originalAppVersion을 사용해 새로 설치한 사용자를 그 이전의 변경에서 면제하세요.619

백엔드 및 릴리스 담당자를 위해: - 필요해지기 전에 계정별로 appTransactionID를 기록해두고, 무료 앱이라도 버전 2 엔드포인트를 세워두세요. 나중에 급히 만든 엔드포인트는 누구의 것인지 특정할 수 없는 철회 이벤트를 받게 됩니다.319 - 중대한 변경 설명 문구는 제품 카피를 책임지는 사람의 손을 거치게 하세요. 보호자가 여러분의 앱이 사용자를 계속 유지할지 판단하며 읽는 유일한 문자열입니다.12


이번 주기의 네 가지 시행 지점 중 셋은 개발자가 통제할 수 있는 것에서 발동합니다. SDK에 걸린 실행 화면 키, 툴체인에 걸린 @State 매크로, 제출에 걸린 소셜 미디어 신고가 그렇습니다. 보호자 동의는 법원의 사건 기록과 스토어프런트의 등급 표에서 발동합니다. 시리즈 전체는 Apple 생태계 시리즈에 모여 있습니다.

참고 문헌


  1. Apple, App Store Server Notifications changelog. 2025년 11월 4일 항목의 New features 아래에 “Updated the responseBodyV2DecodedPayload to include the new payload object, appData“와 “Added the notification type RESCIND_CONSENT to notificationType“가 있습니다. 변경 이력의 다음 두 항목은 2025년 12월 10일과 2026년 4월 27일이며, 어느 쪽도 동의 관련 내용은 아닙니다. HTML 페이지가 JavaScript를 통해 렌더링되므로 2026년 7월 26일 Apple 문서 JSON에서 확인했습니다. 

  2. Apple, notificationType, App Store Server Notifications. RESCIND_CONSENT 정의(“부모 또는 보호자가 자녀의 앱 사용에 대한 동의를 철회했음을 나타내는 알림 유형”)와 본문에서 사용한 개수의 출처입니다. 이 페이지는 23개의 가능한 값을 공개합니다(CONSUMPTION_REQUEST, DID_CHANGE_RENEWAL_PREF, DID_CHANGE_RENEWAL_STATUS, DID_FAIL_TO_RENEW, DID_RENEW, EXPIRED, EXTERNAL_PURCHASE_TOKEN, GRACE_PERIOD_EXPIRED, METADATA_UPDATE, MIGRATION, OFFER_REDEEMED, ONE_TIME_CHARGE, PRICE_CHANGE, PRICE_INCREASE, REFUND, REFUND_DECLINED, REFUND_REVERSED, RENEWAL_EXTENDED, RENEWAL_EXTENSION, RESCIND_CONSENT, REVOKE, SUBSCRIBED, TEST). 문자열 RESCIND_CONSENT는 페이지 페이로드에서 가능한 값 목록 안에 정확히 한 번 등장하며, “Handle use cases for In-App Purchase life-cycle events” 아래 여덟 개 표는 합쳐 40개의 이벤트 행(각 헤더 포함 4, 6, 7, 7, 8, 6, 6, 4행)을 담고 있지만 그중 어느 것도 이 값을 언급하지 않습니다. REVOKE는 동의 철회가 아니라 가족 공유 자격의 상실이라는 점에 유의하십시오. “고객이 가족 공유를 통해 이용할 수 있었던 앱 내 구입이 더 이상 공유로 제공되지 않습니다.” 2026년 7월 26일 Apple 문서 JSON에서 확인했습니다. 

  3. Apple, appData, App Store Server Notifications, 버전 2.19에서 도입. “The appData object is part of the responseBodyV2DecodedPayload. This object is present in the payload when the notificationType is RESCIND_CONSENT“와 네 개 속성의 출처입니다. appAppleId(“사용자가 App Store에서 다운로드하는 앱에 대해 제공되며, 샌드박스 환경에는 존재하지 않습니다”), bundleId, environment, signedAppTransactionInfo(JWSAppTransaction). responseBodyV2DecodedPayload 페이지는 이 배타성을 독립적으로 진술하며, appData를 “notificationTypeRESCIND_CONSENT일 때 나타나는” 필드로 설명하고 “The data, appData, summary, and externalPurchaseToken fields are mutually exclusive. The payload contains only one of these fields”를 덧붙입니다. RESCIND_CONSENT를 페이로드에 appData를 담는 유일한 알림 유형이라고 부른 근거가 이 두 페이지입니다. 문자열 appDatanotificationType 페이지 어디에도 나오지 않습니다. 

  4. Apple, Next steps for apps distributed in Texas, Apple Developer News, 2025년 11월 4일. 텍사스 연령 카테고리(“under 13, 13-15, 16-17, or over 18”), Apple이 사용하는 프레임워크 명칭(“the Significant Change API under the PermissionKit framework”), 개발자 책임 문장(“It’s the developer’s responsibility to determine when there’s a significant change to their app”), 연령 등급 사례(“Texas state law considers a change in the age rating of an app to be a significant change, and developers should keep their age rating selections current in App Store Connect. When a developer updates their app’s age rating, the rating is updated on all user devices once the version is live”), StoreKit 설명(“Developers can use a new property type in StoreKit to automatically check when their app’s age rating has changed on a user’s device and then use the Significant Change API to request parental consent”), 철회 동작(“A parent or guardian in Texas can withdraw consent for any app, which will block launching of the app on the child or teen’s device”), 그리고 네 항목짜리 “Next steps” 구현 목록의 출처입니다. 베타 제공 시점을 iOS 26.2와 iPadOS 26.2로 특정한 근거이기도 합니다. 2026년 7월 26일 확인. 

  5. Apple, Update for Apps Distributed in Texas, Apple Developer News, 2026년 6월 3일. 가처분 해제(“Due to a recent court ruling lifting an injunction on Texas law SB 2420, new Apple Accounts in Texas are now subject to the law”), 적용 범위(“age assurance and parent or guardian consent on behalf of minors under the age of 18 for downloads, Apple In-App Purchases, and significant changes associated with an app. Parents or guardians will also be able to revoke their consent for any app they previously approved for their child”), 시행일(“These changes will go into effect starting June 4, 2026”), 반복된 개발자 책임 안내, 그리고 동일한 네 항목 구현 목록의 출처입니다. 2026년 7월 26일 확인. 

  6. Apple, Implementing age assurance and permissions, Declared Age Range 샘플 코드. 제공 범위는 iOS 26.5, iPadOS 26.5, Mac Catalyst 26.5, Xcode 27.0 베타이며, 문서는 실행 전 “iOS 26.4 이상을 사용하는 기기에서” iCloud에 로그인하라고 안내합니다. 빈 집합 동작(“When neither feature is present, the app skips the flow entirely”), 18세 연령 기준점, 파싱되는 네 개 카테고리(.minor, “확인된 결제 수단이 있는 성인”으로 설명되는 .verifiedAdult, “계정 확인이 없는 성인”인 .unverifiedAdult, .declinedSharing), 미확인 성인의 결과(“the app sets the phase to .blocked and prevents access until the person verifies their account in Settings”), SignificantAppUpdateTopicPermissionQuestion을 만드는 미성년자 경로, PermissionButton 전송, 본문에 그대로 인용한 AskCenter.shared.responses(for:) 리스너 예제, 거부 동작(“When the parent denies the request, the app prevents the minor from using it”), didChangeExternallyNotification을 사용하는 NSUbiquitousKeyValueStore 확인 기록(“so other devices don’t present the same flow again”), 그리고 originalAppVersion 면제(“People who install the app when a significant change is already present don’t need to acknowledge it”)의 출처입니다. 이 프로젝트는 Declared Age Range 자격과 iCloud 키-값 저장소 서비스도 요구합니다. 2026년 7월 26일 Apple 문서 JSON에서 확인했습니다. 

  7. Apple, AgeRangeService.requiredRegulatoryFeaturesAgeRangeService.RegulatoryFeature, Declared Age Range. 둘 다 iOS 26.4, iPadOS 26.4, Mac Catalyst 26.4, macOS 26.4부터 제공되며 visionOS, tvOS, watchOS 행은 없습니다. 선언은 var requiredRegulatoryFeatures: Set<AgeRangeService.RegulatoryFeature> { get async throws }이며, “규제 기능의 서비스를 사용할 수 없는 경우” notAvailable을 던집니다. 열거형은 정확히 세 개의 케이스를 공개합니다. declaredAgeRangeRequired(“Indicates the person is required to share their age range with your app”), significantAppChangeRequiresAdultNotification(“Indicates that adult users must acknowledge your app’s significant change”), significantAppChangeRequiresParentalConsent(“Indicates a parent or guardian is required to acknowledge and consent to a significant app change”). 

  8. Apple, Age assurance frameworks Q&A, Apple Developer Support. SDK 하한선(“you must build your app against the iOS 26.2 and iPadOS 26.2 SDKs, or later, with Xcode 26.2 (17C52) or later”), 기존 계정 예외(“Existing Apple Accounts running iOS 18 and iPadOS 18, or earlier … won’t be affected”, 생략된 절은 “including adult and child accounts for kids and teens”를 명시합니다), 책임에 대한 답변(“Yes, developers are responsible for their own age restrictions” 및 “For questions about your compliance obligations, consult your legal counsel”), 본문에서 두 번 인용한 지역 한정(“In certain regions, where legally required, Apple uses age assurance methods to confirm an Apple Account holder’s age and shares age categories with you through the Declared Age Range API. In those regions, you must check the age of the people using your app”, 이후 “In regions where legally required, you need to check the age of the people using your app with the Declared Age Range API”로 재진술), 접근 차단 의무(“For significant app updates, you’re responsible for preventing access to your app or features when required, and for handling the response from the parent or guardian. Until the parent provides consent, the child must be prevented from accessing the significant update, which may include all app and account data or specific features”), 철회 동작(“When a parent or guardian revokes consent for their child to access an app, Apple will prevent the app from launching. To handle consent revocations, use the RESCIND_CONSENT value from notificationType”), App Review 답변(“No, there are no changes to the App Review process”), 그리고 약관·개인정보 답변(“It depends. You determine what constitutes a significant app update based on applicable laws”)의 출처입니다. 2026년 7월 26일 확인. 

  9. Apple, Upcoming changes to age ratings in Australia and Vietnam, Apple Developer News, 2026년 5월 21일. “Starting June 18, 2026, age ratings on the App Store will be updated in Australia and Vietnam”, 호주 변경(“The 15+ age rating will no longer be available on the App Store in Australia. Apps currently rated 15+ with the following content descriptors will be updated to 16+”)과 세 가지 콘텐츠 설명자(무제한 웹 접근, 빈번한 의료 또는 치료 정보, 루트 박스), 그리고 베트남 변경(“To align with Article 38 of Vietnam Decree 147, apps available on the App Store in Vietnam will require a region-specific age rating. Based on your age rating questionnaire responses in App Store Connect, your app will receive one of four ratings (00+(all ages), 12+, 16+, or 18+)”)의 출처입니다. 어느 변경도 개발자에게 빌드 제출을 요구하지 않습니다. 2026년 7월 26일 확인. 

  10. Apple, AppStore.ageRatingCode, StoreKit. 선언은 static var ageRatingCode: Int? { get async }이며 iOS 26.2, iPadOS 26.2, macOS 26.2, tvOS 26.2, visionOS 26.2, watchOS 26.2부터 제공되고 Mac Catalyst 행은 없습니다. 반환값은 “현재 연령 등급 코드를 나타내는 정수, 또는 연령 등급을 사용할 수 없는 경우 nil“입니다. 비교 설명(“Use this property to fetch the age rating for your app and compare it with the last known age rating to check if it has changed”)과 PermissionKit으로 이어지는 연결(“If your app’s age rating has changed, consider informing parents or guardians by using the Significant Change API”)의 출처이며, 후자의 문장에서 “Significant Change API”가 링크 텍스트이고 Apple의 SignificantAppUpdateTopic 페이지가 링크 대상입니다. 이 페이지에 실린 Apple 자체 예제는 이 속성을 감싼 guard let으로, 값을 사용할 수 없을 때 로그를 출력합니다. 2026년 7월 26일 이 정수와 등급 단계(4+, 9+, 13+, 16+, 18+) 사이의 대응 관계를 Apple 문서에서 검색했으나 이 페이지, AppStore 타입 페이지, App Store Connect 도움말의 연령 등급 레퍼런스 어디에서도 찾지 못했습니다. 

  11. Apple, Testing age assurance in sandbox, StoreKit. 기기에서의 경로(설정 → 개발자 → 샌드박스 Apple 계정 → 관리 → “Age Assurance or Revoke App Consent”), 18세 이상 행이 하한 18에 상한 없음, 연령 선언 selfDeclared 또는 confirmed를 반환하는 여섯 행짜리 테스트 매트릭스, 성인 요청 동작(“For 18+ test cases, PermissionKit throws AskError.notAvailable rather than returning a PermissionChoice. Calling AskCenter.ask(_:) for an adult user throws this error because they don’t meet the requirements for parental permission requests”), “Notification Triggered”라는 확인 메시지와 “A notification will be sent to the developer server soon”로 끝나는 철회 절차, 그리고 페이로드 설명(“your server receives a RESCIND_CONSENT notificationType. The notification payload includes an appData object with app metadata, including the bundleId and environment fields”)의 출처입니다. 매트릭스의 미성년자 세 행은 13세 미만 승인, 13-15세 승인, 16-17세 거부이며 모두 연령 선언이 guardianDeclared입니다. 

  12. Apple, SignificantAppUpdateTopic, PermissionKit. iOS 26.2, iPadOS 26.2, Mac Catalyst 26.2, macOS 26.2, visionOS 26.2부터 제공되며 struct SignificantAppUpdateTopic으로 선언되고 QuestionTopic을 준수하며 init(description: String)을 갖습니다. 정의의 위임(“You determine what constitutes a significant update based on applicable regulations”), 설명 작성 지침(“Use concise, understandable language that clearly explains what changed in your app. Parents and guardians see this description when deciding whether to grant permission”), 그리고 본문에 그대로 옮긴 Specific/Vague 예제의 두 주석 모두의 출처입니다. 

  13. Apple, New requirements for apps available in Texas, Apple Developer News, 2025년 10월 8일. 최초 발표(“Beginning January 1, 2026, a new state law in Texas … introduces age assurance requirements for app marketplaces and developers”, 생략된 부분이 SB2420을 명시합니다), 가족 공유 요건(“All new Apple Accounts for users under the age of 18 will be required to join a Family Sharing group, and parents or guardians will need to provide consent for all App Store downloads, app purchases, and transactions using Apple’s In-App Purchase system by the minor”), 그리고 유타와 루이지애나에 대한 사전 예고(“Similar requirements will come into effect later next year”)의 출처입니다. 2026년 7월 26일 확인. 

  14. Apple, Update on age requirements for apps distributed in Texas, Apple Developer News, 2025년 12월 23일. 가처분(“A recent injunction issued by a district court suspended enforcement of Texas state law SB2420 … In light of this ruling, Apple will pause previously announced implementation plans and monitor the ongoing legal process”), 네 가지 도구의 샌드박스 제공 지속, 그리고 유타와 루이지애나로의 확대의 출처입니다. 2026년 7월 26일 확인. 

  15. Apple, Age requirements for apps distributed in Brazil, Australia, Singapore, Utah, and Louisiana, Apple Developer News, 2026년 2월 24일. 18세 이상 다운로드 제한(“Starting February 24, 2026, Apple will block users in Australia, Brazil, and Singapore from downloading apps rated 18+ unless they have been confirmed to be adults through reasonable methods. The App Store will perform this confirmation automatically. However, developers may have separate obligations to independently confirm that their users are adults”), 유타와 루이지애나 날짜(“For users with new Apple Accounts in Utah as of May 6, 2026, and in Louisiana as of July 1, 2026, age categories will be shared with the developer’s app when requested through the Declared Age Range API”), 본문에 인용한 확장 문장과 그 뒤를 잇는 네 개의 링크(“The tools we previously announced have been expanded to help developers meet compliance obligations for Louisiana and Utah, including:” Declared Age Range API, PermissionKit의 Significant Change API, StoreKit의 새 연령 등급 속성 타입, App Store Server Notifications), 브라질 루트 박스 관련 결과, 그리고 Significant Update Action의 첫 공개 언급(“Developers can use the Declared Age Range API to present significant update notifications to adults in these states through the Significant Update Action, now in beta”)의 출처입니다. 2026년 7월 26일 확인. 

  16. Apple, PermissionQuestionexpirationDate, PermissionKit. final class PermissionQuestion<Topic> where Topic : QuestionTopic이며 iOS 26.0부터 제공되고 네 개의 이니셜라이저(init(handle:), init(handles:), init(communicationTopic:), init(significantAppUpdateTopic:))를 갖습니다. 마지막 것은 iOS 26.2에서 도입되었고 “중대한 업데이트 이후에도 앱을 계속 사용할 수 있도록 부모 또는 보호자에게 허가를 요청하는 권한 질문을 생성”한다고 설명됩니다. expirationDatefinal var expirationDate: Date?로 선언되며 “Once the date passes, the person that receives the question can no longer respond”라는 설명이 붙습니다. Apple은 이 속성의 기본값도, 중대한 업데이트 주제에 대한 설정 지침도 공개하지 않았습니다. 

  17. Apple, Creating a communication experience, PermissionKit. 본문에 인용한 취소 동작의 출처입니다. “At any point during the send request flow, the child has the option to cancel the request, and decide not to send the question to their parent or guardian. In this scenario, the system doesn’t deliver a response to the calling app for that specific question.” 또한 이 프레임워크의 iMessage 제약의 출처이기도 한데, PermissionKit 랜딩 페이지가 이를 Important 안내로 밝힙니다. “Communication experiences using the PermissionKit framework are only available using iMessage.” 이 문서는 CommunicationTopic 흐름만 다루며, Apple은 중대한 업데이트 흐름에 대한 동등한 문서를 공개하지 않았고, 이 문서의 코드 예제는 CommunicationLimits.current.permissionResponses를 호출하는데 이 심볼은 2026년 7월 26일 기준 Apple 문서에서 404를 반환합니다. 

  18. Apple, AskError, PermissionKit. enum AskErrorLocalizedError를 준수하며 여섯 개의 케이스를 갖습니다. 넷은 개요 설명이 있고 iOS 26.1에 도입되었습니다. unknown, communicationLimitsNotEnabled(“Indicates communication limits isn’t enabled to send permission requests”), contactSyncNotSetup, invalidQuestion. 둘은 자체 페이지에 개요가 없습니다. iOS 26.1의 systemError(underlyingError:)와 iOS 26.2에서 SignificantAppUpdateTopic과 함께 도입된 notAvailable입니다. notAvailable의 의미는 주석 11에서 인용한 샌드박스 테스트 문서에만 나옵니다. systemError는 적어도 시그니처에 원인이 드러납니다. communicationLimitsNotEnabledcontactSyncNotSetup이 중대한 업데이트 요청에서도 발생할 수 있는지는 언급되지 않았습니다. 

  19. Apple, AppTransactionappTransactionIDoriginalAppVersion, StoreKit. 식별자의 의미(“The App Store generates a single, globally unique appTransactionID for each Apple Account that downloads your app and for each family group member for apps that support Family Sharing”), 재다운로드·환불·재구매·스토어프런트 변경에도 유지되는 안정성, App Store Server Notifications 버전 2 페이로드에서의 존재, 그리고 무료 앱에 결정적인 문장(“The appTransactionID is available even if a customer makes no in-app purchases”)의 출처입니다. originalAppVersion은 “고객이 App Store에서 원래 구입한 앱 버전”이며 macOS에서는 CFBundleShortVersionString, 그 외에서는 CFBundleVersion을 담고, 샌드박스 환경에서는 항상 1.0입니다. 대응하는 서버 측 타입은 App Store Server API의 appTransactionId이며 버전 1.15에서 도입되었습니다. 

  20. Apple, CommunicationLimitsupdates, PermissionKit. updatesfinal var updates: some AsyncSequence<PermissionResponse<CommunicationTopic>, Never> { get }로 선언되며 “Registers the communication topic with the system, so your app can be launched on-demand in the background to receive permission updates”로 요약됩니다. Apple 문서는 updates와 두 개의 CommunicationLimits.ask(_:in:) 오버로드를 Deprecated API 항목 아래 묶어두었습니다. 클래스 자체는 isKnownHandle(_:)knownHandles(in:)에 대해서는 여전히 유효합니다. 대체 시퀀스는 그 약속을 버렸습니다. AskCenter.responses(for:)의 개요에도 설명에도 백그라운드 실행에 대한 언급이 전혀 없으며, 본문이 지적한 문서 공백이 바로 이것입니다. 

  21. Apple, AskCenterresponses(for:), PermissionKit. 둘 다 iOS 26.2, iPadOS 26.2, Mac Catalyst 26.2, macOS 26.2, visionOS 26.2부터 제공됩니다. AskCenterstatic let shared로 접근하는 final class이며, “질문을 적절한 가족 공유 채널로 라우팅”하고 “부모가 결정을 내리면 응답을 앱으로 전달”한다고 설명됩니다. responses(for:)final func responses<Topic>(for topicType: Topic.Type) -> some AsyncSequence<PermissionResponse<Topic>, Never> where Topic : QuestionTopic으로 선언되며 “Registers the topic type with the system and returns an asynchronous sequence of responses”로 요약될 뿐, 백그라운드 실행에 대한 언급은 없습니다. ask(_:in:) 오버로드는 네 개가 존재합니다. iOS, iPadOS, visionOS에서 UIViewController를 받는 둘과 macOS에서 NSWindow를 받는 둘이며, 주제 타입별로 하나씩입니다. PermissionResponsechoicequestion을 노출하고, PermissionChoice.Answerapprovaldenial 정확히 두 케이스만 공개합니다. 

  22. AppStore.ageRatingCode always returns 0 on real device에 달린 Apple 직원 답변, Apple Developer Forums. 최초 게시물은 2026년 4월 작성으로, iOS 26.4를 실행하는 실기기에서 샌드박스 계정으로 로그인하고 App Store Connect에 연령 등급을 설정한 상태에서 0이 반환된다고 보고합니다. Apple Staff로 표시된 작성자의 답변은 이렇게 말합니다. “The ageRatingCode API should be used to observe changes to your app’s age rating over time by comparing to its last known value. If your app’s age rating code has changed, consider informing parents or guardians by using the Significant Change API”, 그리고 “A value of 0 is expected during development of your app when it is built and run from Xcode and Sandbox environments (including TestFlight).” 2026년 7월 26일 두 차례 확인했고 문구는 동일했습니다. 포럼은 JavaScript를 통해 렌더링되며 답변의 작성 시점을 날짜가 아닌 “1w”라는 상대 시각으로 표시하므로 여기서는 게시일을 보고하지 않습니다. 개발자 포럼 답변은 문서 페이지보다 약한 근거이며, ageRatingCode에 대한 Apple 문서는 0 값을 전혀 언급하지 않습니다. 본문에서 끌어낸 결론, 즉 0을 기준값으로 저장하면 첫 App Store 빌드에서 거짓 변경이 만들어진다는 것은 그 답변과 Apple이 문서화한 비교 패턴에서 나온 저의 추론입니다. 

  23. Apple, Enabling App Store Server Notifications. TLS 하한선(“your server must support the Transport Layer Security (TLS) 1.2 protocol or later”), App Store Connect의 환경별 URL 설정, 포트 제약(443, 또는 1024 이상), 그리고 허용 목록 서브넷(“add the IP address subnet 17.0.0.0/8”, 이는 “샌드박스와 프로덕션 환경 모두에 적용”)의 출처입니다. 짝을 이루는 Receiving App Store Server Notifications 문서는 JWS로 서명된 signedPayload를 설명하며, 2026년 7월 26일 기준 data 객체만 언급하고 appData는 다루지 않습니다. 

  24. Apple, Responding to App Store Server Notifications. 성공 코드(“Send HTTP 200, or any HTTP code between 200 and 206”), 재시도 유발 조건(“Send HTTP 50x or 40x to have the App Store retry the notification”), 버전 2 재시도 일정(“it retries five times, at 1, 12, 24, 48, and 72 hours after the previous attempt”), 샌드박스 제약(“Retry notifications are available only in the production environment. In the sandbox environment, the App Store server attempts to send the notification one time”), 그리고 Get-Notification-History를 통한 복구 경로의 출처입니다. 

  25. Apple, subtype, App Store Server Notifications. 이 페이지는 19개의 가능한 값(ACCEPTED, ACTIVE_TOKEN_REMINDER, AUTO_RENEW_DISABLED, AUTO_RENEW_ENABLED, BILLING_RECOVERY, BILLING_RETRY, CREATED, DOWNGRADE, FAILURE, GRACE_PERIOD, INITIAL_BUY, PENDING, PRICE_INCREASE, PRODUCT_NOT_FOR_SALE, RESUBSCRIBE, SUMMARY, UPGRADE, UNREPORTED, VOLUNTARY)을 공개하며, 각각은 특정 알림 유형에 한정됩니다. 2026년 7월 26일 확인 기준, 문자열 RESCIND_CONSENT는 이 페이지 페이로드 어디에도 나타나지 않습니다. 

  26. Apple, PermissionButton, PermissionKit. @MainActor @preconcurrency struct PermissionButton<Topic, Label> where Topic : QuestionTopic, Label : View이며 iOS 26.2, iPadOS 26.2, Mac Catalyst 26.2, macOS 26.2, visionOS 26.2부터 제공되고, 각각 CommunicationTopicSignificantAppUpdateTopic으로 제약된 두 개의 init(question:label:) 오버로드를 갖습니다. 이는 CommunicationLimitsButton을 대체하며, Apple은 후자를 프레임워크 페이지의 Deprecated API 항목에 올려두었습니다. 

  27. Apple, AgeRangeService.showSignificantUpdateAcknowledgment(in:updateDescription:), SignificantUpdateAction, 그리고 SwiftUI 환경 값 showSignificantUpdateAcknowledgment. 이 메서드는 @MainActor func showSignificantUpdateAcknowledgment(in windowScene: UIWindowScene, updateDescription: String) async throws로 선언되며 iOS 26.4, iPadOS 26.4, Mac Catalyst 26.4를 공개하고 macOS 행은 없습니다. AgeRangeService는 “Displaying update acknowledgments” 아래에 이 오버로드 하나만 나열합니다. SignificantUpdateAction과 환경 값도 동일한 세 플랫폼을 공개합니다. 이 메서드에 대한 Apple의 Important 안내는 다음과 같습니다. “Before calling this function, check RegulatoryFeature to determine if a person must acknowledge your significant app change.” 환경 값 설명에는 이 액션을 “Button 또는 onAppear(perform:)에서 호출”하라는 내용이 덧붙습니다. 

  28. Apple, requestAgeRange(ageGates:::in:), Declared Age Range. Apple은 두 개의 오버로드를 공개합니다. iOS 26.0, iPadOS 26.0, Mac Catalyst 26.0에서 in viewController: UIViewController를 받는 것과 macOS 26.0에서 in window: NSWindow를 받는 것입니다. 연령 범위 요청에는 NSWindow 변형이 존재하는데 주석 27에서 인용한 확인 시트에는 없다는 점이, macOS의 누락을 정책적 결정이 아니라 빈틈으로 읽은 근거입니다. Apple은 어느 쪽으로도 밝힌 바가 없습니다. 

  29. Apple, Requesting people’s age range information in your app, Declared Age Range. 본문에 인용한 기준점 산술(“최대 세 개의 연령 기준점을 지정할 수 있으며, 이는 최대 네 개의 연령 범위를 만듭니다”), 간격 제약(“각 범위는 최소 2년 이상이어야 합니다”), 그리고 경계값의 의미(“lowerBound 값이 nil이면 그 사람은 여러분이 지정한 가장 낮은 연령 기준점 미만입니다” 및 “upperBoundnil이면 그 사람은 여러분이 지정한 가장 높은 연령 기준점 이상입니다”)의 출처입니다. 13, 16, 18에 기준점을 두면 Apple이 텍사스에 대해 공개한 네 개 범위가 정확히 반환되며, 경계가 있는 두 범위 모두 2년 최소 요건을 충족합니다. 13-15세는 3년, 16-17세는 2년입니다. 이 문서는 또한 사용자 계정이 속한 지역이 “시스템이 연령 범위를 반환하는 데 사용하는 연령 기준점을 결정하며, 이는 요청에서 지정한 기준점과 다를 수 있다”고 경고합니다. 2026년 7월 26일 Apple 문서 JSON에서 확인했습니다. 

  30. 저자 조사, 2026년 7월 26일: 플랫폼 관련 주장의 근거. 플랫폼 값은 각 프로젝트의 SUPPORTED_PLATFORMS 빌드 설정에서 읽었으며, Xcode가 대상과 무관하게 기록하는 배포 대상 키는 사용하지 않았습니다. Reps는 appletvos appletvsimulator iphoneos iphonesimulator macosx xros xrsimulator와 별도의 watchos watchsimulator 타깃을 선언하고, Ace Citizenship은 iphoneos iphonesimulator만 선언합니다. 이 방법은 플랫폼을 과소 보고하며, Return이 그 증거입니다. Return의 유일한 SUPPORTED_PLATFORMS 값은 iphoneos iphonesimulator macosx xros xrsimulator인 반면 TV와 watch 타깃은 대신 SDKROOT = appletvosSDKROOT = watchos를 갖고 있어, SUPPORTED_PLATFORMS 스윕으로는 둘 다 보이지 않습니다. 따라서 이 글의 “플랫폼 X에 출시한다”는 모든 주장은 빌드 설정이 아니라 App Store Connect에서 나온 것입니다. Return: TV_OS 1.0과 1.0.1 모두 READY_FOR_DISTRIBUTION, IOS와 MAC_OS 1.0.1도 마찬가지이며, iOS 타깃은 Embed Watch Content 단계를 통해 ReturnWatch Watch App(com.941apps.Return.watchkitapp)을 내장합니다. watch 앱이 손목에 도달하는 경로가 바로 이것입니다. Reps: IOS 1.1과 MAC_OS 1.1이 READY_FOR_DISTRIBUTION, TV_OS 버전은 그 상태에 도달한 적이 없고 TV_OS 1.2가 2026년 6월 2일부터 WAITING_FOR_REVIEW에 머물러 있으므로, Reps는 iOS와 macOS에 출시합니다. 

  31. 저자 조사, 2026년 7월 26일, macOS 26.5.2(빌드 25F84), Xcode 26.6(빌드 17F113), Swift 6.3.3 환경. 대상 선정 규칙은 범위를 신뢰가 아니라 검증과 재현으로 확인할 수 있도록 밝힙니다. 제 에이전트 설정 파일(~/.claude/CLAUDE.md)의 Active Projects 표에서 뒤에 Xcode 프로젝트가 있는 모든 행을 골랐고, 정확히 7개가 나옵니다. Reps, Return, Banana List(Get Bananas로 출시), Ace-Citizenship, Water, ResumeGeniApp, Yawara. 이 규칙이 곧 조사의 결함이기도 한데, Randori는 그 표에 행이 없고 정작 중요한 프로젝트가 Randori였기 때문입니다. 7개 중 5개는 App Store Connect 레코드가 있습니다(Reps 6776044339, Return 6756242021, Get Bananas 6756241534, Ace Citizenship 6532592671, ResumeGeni 6771154645). WaterYawara는 계정의 18개 앱 어디에도 없으므로 제출된 적이 없으며, 둘 중 어느 쪽도 앱이라고 부르면 과장입니다. 프로젝트별 Swift 파일 수는 77, 57, 55, 26, 34, 71, 143입니다. 동의 관련 스캔은 *.swift, *.entitlements, *.plist, project.pbxproj를 대상으로 했으며 각각 85, 65, 63, 34, 38, 74, 149개로 합계 508개입니다. 이 수치를 재현하려면 제외할 디렉터리 이름이 여섯 개가 아니라 여덟 개 필요합니다. build, DerivedData, .build, Pods, .git, worktrees, 그리고 Reps에만 있는 .venv(Reps/.venvReps/server/.venv에 걸친 142개 속성 목록)와 .xcode-state-backups(18개). 앞의 여섯 개만 제외하면 Reps는 245개로, 합계는 668개로 읽히므로 이 제외 목록은 결과를 좌우하며 여기 의존하는 모든 이름을 명시해두었습니다. 16개 패턴은 모두 대소문자를 구분합니다. PermissionKit, CommunicationLimits, AskPermission, AskCenter, SignificantAppUpdateTopic, SignificantUpdateAction, PermissionTopic, com.apple.developer.family-controls, FamilyControls, ManagedSettings, DeviceActivity, AuthorizationCenter, CKShare, sharedCloudDatabase, publicCloudDatabase, ageRatingCode. 7개 프로젝트 전부에서 일치 파일이 0개였으며 AskCenter도 포함됩니다. 동일한 파이프라인으로 돌린 두 개의 대조 패턴은 0이 아닌 결과를 냈고(StoreKit은 Reps에서 3개 파일, CKContainer|NSPersistentCloudKitContainer|SwiftData는 Banana List에서 17개 파일), 이로써 파이프라인이 조용히 실패한 것이 아니라 실제로 파일을 읽었음을 알 수 있습니다. Ace Citizenship의 온보딩(IntroCarouselView, WelcomeView, AddStateView, AddRepresentativeView)에는 나이나 생년월일 필드가 없고, PrivacyInfo.xcprivacy는 빈 NSPrivacyCollectedDataTypes를 선언합니다. “만 18세 이상” 자격 문구는 ~/Projects/acecitizenship.app/content/blog/n400-application-guide.md에서 나온 것입니다. 

  32. 저자 조사, 2026년 7월 26일: 확장 스캔과 Randori. 확장 스캔은 동일한 16개 패턴을 ~/Projects 아래 모든 저장소에 적용했고, 동일한 여덟 개 이름에 node_modules를 더해 제외했으며 gitignore된 경로도 제외했고, 21개 파일이 일치했습니다. 그중 7개가 코드입니다. Randori/Randori/ 아래 6개 Swift 파일과 _archive/Oishii-AI/Oishii AI/Services/CloudKitManager.swift(보관된 프로젝트의 publicCloudDatabase). 나머지 14개는 문서이거나 기계가 만든 상태 파일입니다. Randori 계획 및 설계 문서 9개, 이 사이트 자체의 content/blog/에 있는 게시물 3개(그중 하나는 이 글의 초안), Obsidian 인계 노트 1개, 그리고 사람이 아니라 스크립트가 쓰는 obsidian-signals/40-Projects/blakecrosley-com/wwdc-2026/.pulse_state.json. Randori는 com.wayofyawara.randori이며, App Store Connect 앱 6789693294에 버전 1.0이 PREPARE_FOR_SUBMISSION 상태입니다. CKSharesharedCloudDatabase는 6개 파일에 걸쳐 20개 라인에 일치합니다. ConnectionStore.swift(10), RandoriApp.swift(5), ProfileCardView.swift(2), 그리고 CKPostTransport.swift, CloudShareSheet.swift, SocialContracts.swift 각 1개. ConnectionStore는 헤더 주석에서 자신의 구조를 “하나의 존(ProfileCardZone), 하나의 레코드 타입(ConnectionCard), 하나의 공유”로 설명하며, privateCloudDatabasesharedCloudDatabase를 나란히 보유하고, 355번 줄에 accept(_ metadata: CKShare.Metadata), 547번 줄에 ensureOutboundShare(), 그리고 계정별 차단과 차단 해제를 구현합니다. RandoriApp.swift는 앱 델리게이트(73번 줄)와 윈도우 씬 델리게이트(97번 줄, “Warm: the app is running when the link is opened”라는 주석) 양쪽에 userDidAcceptCloudKitShareWith를 구현하며, RandoriSceneDelegate.scene(_:willConnectTo:options:)는 88번 줄에서 “Cold start: the invitation rides the connection options”라는 주석 아래 connectionOptions.cloudKitShareMetadata를 읽습니다. 본문이 콜드 런치를 수락 콜백이 아니라 연결 옵션에 할당한 근거가 이것입니다. TagConsentStore는 이 앱 자체의 동의 장부로 iCloud 계정별로 범위가 지정되며, 문서화된 기본 동작은 실패 시 차단입니다. 정책 레코드가 아직 도착하지 않은 상대는 태그될 수 없습니다. Randori/Randori.entitlements는 CloudKit, HealthKit, aps-environment를 요청합니다. 두 개의 CloudKit 공유 패턴을 제외한 모든 동의 패턴은 이 저장소 전체에서 0을 반환하며 StoreKit, signedPayload, notificationType도 마찬가지입니다. 따라서 이 앱은 아무것도 판매하지 않고 자체 서버도 운영하지 않습니다. docs/asc-metadata.md는 무료 가격과 4+ 연령 등급을 준비해두고 있습니다. 

  33. 저자 조사, 2026년 7월 26일: StoreKit 호출 지점과 서버 측 근거. StoreKit은 세 프로젝트에 등장합니다. Reps/Reps/Services/RepsProStore.swift(자동 갱신, 2개 등급), Ace Citizenship/StoreKitManager.swift(비소모성 1개), ResumeGeni/Subscription/(월간 구독 1개, 서버 측에서 접근 제어). 표에 인용한 .pending 처리는 RepsProStore.swift:134(case .userCancelled, .pending:), StoreKitManager.swift:69-73(별도의 case .pending:이며 그 자체의 return false는 73번 줄), SubscriptionStore.swift:209-212(이름이 붙은 대기 결과)에 있습니다. 사용자를 붙잡아두는 것은 호출 지점입니다. RepsProPaywallView.swift:359-361if await store.purchase(plan) 안에서만 화면을 닫고, ContentView.swift:484-485if success 안에서만 잠금을 해제하므로, 대기 중인 구매에 대해 반환된 false는 두 앱 모두에서 화면상 아무것도 바꾸지 않습니다. 반면 ResumeGeni는 PaywallView.swift:526에 도달하는데, 여기서 .pending 분기가 “Waiting for approval. You’ll get access once it’s approved.”라는 토스트를 띄웁니다. ResumeGeni의 App Store Server Notifications 엔드포인트는 ~/Projects/resumegeni/app/routers/appstore.pyPOST /api/appstore/notifications이며, {"signedPayload":"probe"}POST하면 HTTP 400 {"status":"invalid"}가 반환됩니다. 이는 활성화 플래그를 지나 Apple의 인증서 체인 검증기 안에서만 도달할 수 있고, GET은 405를 반환합니다. 두 환경 URL이 모두 등록되어 있으며, 그 근거는 운영 문서가 아니라 실제 전달입니다. 941-analytics D1 데이터베이스의 funnel_events 테이블에는 platform='ios'이고 메타데이터에 notification_type이 담긴 행이 56개 있으며, 기간은 2026-06-26T15:48:51Z부터 2026-07-25T16:40:11Z까지입니다. 이 중 55개는 environmentsandbox이고 1개는 production입니다(2026-07-21T18:08:21Z). 56은 하한으로 보시기 바랍니다. 이 서비스는 다섯 개의 이벤트 이름만 퍼널 행에 매핑하기 때문입니다. 실제로 관측된 네 가지 유형은 DID_RENEW(42), SUBSCRIBED(6), EXPIRED(5), DID_CHANGE_RENEWAL_STATUS(3)입니다. app/services/app_store_notification_service.py는 13개 알림 유형의 이름을 담고 있고, 그중 8개가 _funnel_event_name(75~89번 줄)에서 실행 가능한 분기에 도달합니다. SUBSCRIBED, OFFER_REDEEMED, DID_RENEW, REFUND, REVOKE, EXPIRED, DID_CHANGE_RENEWAL_STATUS, DID_FAIL_TO_RENEW. 나머지 5개는 주석 안에만 있습니다. 73번 줄의 PRICE_INCREASE, RENEWAL_EXTENDED, METADATA_UPDATE와 187번 줄의 EXTERNAL_PURCHASE_TOKEN, TEST. RESCIND_CONSENT, CONSUMPTION_REQUEST, REFUND_DECLINED, REFUND_REVERSED는 이 저장소 전체에서 일치하는 결과가 0입니다. 근거가 되지 못하는 것도 밝혀둡니다. docs/SUBSCRIPTION_GO_LIVE.md:83에 “프로덕션과 샌드박스 URL을 모두 설정하라”고 적혀 있긴 하지만, 이 줄은 해당 단계가 “재인증이 필요하다”고 언급한 제목 아래의 운영 지침이므로 완료된 등록이 아니라 의도를 기록한 것입니다. 따라서 위의 등록 주장은 이 문장이 아니라 실제로 전달된 알림에 근거합니다. 

  34. Apple, Testing Ask to Buy in Xcode, StoreKit. Apple이 이 메커니즘을 설명한 문장(“With Ask to Buy, when a child wants to make an eligible purchase or download, the system sends the purchase request to the parent or guardian”)과 거부 시 동작(“Your app doesn’t receive a transaction because you declined Ask to Buy”)의 출처입니다. 이 문서는 StoreKit 구성 편집기의 Purchase Options 아래 구입 요청 토글도 설명하며, 거래 관리자는 Pending Ask to Buy, Ask to Buy Approved, Ask to Buy Declined 상태를 표시합니다. 

  35. Apple, Product.PurchaseResult.pendingTransaction.updates, StoreKit. 둘 다 iOS 15.0부터 제공됩니다. 케이스 개요(“The purchase is pending, and requires action from the customer”), 해결 경로(“If a pending purchase succeeds, StoreKit delivers the resulting Transaction in the transaction updates”), 그리고 이 시퀀스의 목적(“This sequence receives transactions that occur outside of the app, such as Ask to Buy transactions, offer code redemptions, and purchases that customers make in the App Store”)의 출처입니다. Product.PurchaseResult 열거형은 success(_:), pending, userCancelled 세 케이스를 공개합니다. 이 열거형 페이지의 Apple 자체 예제는 대기 분기에 “The purchase requires action from the customer. If the transaction completes, it’s available through Transaction.updates.”라는 주석을 달아두었습니다. 

  36. 위 분기 처리 예제의 모든 심볼은 2026년 7월 26일 Apple 문서 JSON에서 확인했습니다. AgeRangeService.sharedstatic let shared: AgeRangeService입니다(iOS 26.0). SwiftUI 환경 값은 requestAgeRange, var requestAgeRange: DeclaredAgeRangeAction { get }(iOS 26.0)과 showSignificantUpdateAcknowledgment, var showSignificantUpdateAcknowledgment: SignificantUpdateAction { get }(iOS 26.4)입니다. 예제의 @available(iOS 26.5, *) 하한선은 이 둘 중 어느 쪽에서도 오지 않습니다. AgeRangeService.AgeRangeDeclaration.confirmed가 iOS 26.5, iPadOS 26.5, Mac Catalyst 26.5, macOS 26.5를 공개하는데 이는 확인 액션보다 한 릴리스 뒤이므로, 확인된 성인을 구분하는 코드는 모두 더 높은 하한선을 물려받습니다. Apple의 샘플 프로젝트도 동일한 26.5 제공 범위를 공개합니다.6 AgeRangeService.AgeRangelowerBound, upperBound, var ageRangeDeclaration: AgeRangeService.AgeRangeDeclaration?로 선언된 ageRangeDeclaration, 그리고 activeParentalControls를 노출합니다. 그 옵셔널에 대한 == .confirmed 비교가 유효한 이유는 AgeRangeDeclaration이 관계 섹션에 따라 EquatableHashable을 준수하기 때문입니다. PermissionButton의 이니셜라이저는 init(question: PermissionQuestion<Topic>, @ViewBuilder label: @escaping () -> Label)이며, 여기서 사용한 오버로드에서는 SignificantAppUpdateTopic으로 제약됩니다.26 ChangedFeatureViewAccountVerificationPrompt는 Apple 심볼이 아니라 여러분이 직접 만들 뷰의 자리 표시자입니다. 

  37. Apple, Age ratings values and definitions, App Store Connect Help, 2026년 7월 26일 확인. 주석 9에서 발표된 2026년 6월 18일 변경이 실제로 적용되었음을 확인해주는 출처입니다. “Australia age rating values” 아래 표는 이제 16+와 R 18+ 두 등급만 공개하며 15+ 행은 없습니다. 별도의 “Vietnam age rating values” 섹션이 새로 생겼고 “As required by Article 38 of Vietnam Decree 147”로 시작하며, 00+ 행은 “문제가 될 만한 자료를 포함하지 않으나 다음 콘텐츠의 사례를 포함할 수 있는” 앱으로 정의되고 보호자 통제, 연령 확인, 사용자 생성 콘텐츠, 메시지 및 채팅, 광고, 드문 콘테스트를 나열합니다. 호주 이전에 대한 Apple의 5월 21일 설명자 목록은 이 페이지의 현재 16+ 유발 목록과 일치하지 않는데, 이 불일치는 아직 해소하지 못했고 근거로 삼지도 않습니다. 여기서 끌어낸 주장은 15+ 등급이 사라졌다는 것과 베트남 표가 존재한다는 것뿐이기 때문입니다. 

관련 게시물

App Store의 소셜 미디어 체크박스, 그리고 그 대가

9월부터 시행되는 App Store 규칙에 따라 모든 제출물은 소셜 미디어 기능 여부를 신고해야 합니다. 13세 미만 예외 조항의 대가는 entitlement 하나, API 하나, 그리고 분기 처리 하나입니다.

20 분 소요

canOpenURL 지원 중단: 대신 무엇을 호출해야 하나

Apple은 세 문장으로 canOpenURL의 지원을 중단하면서 스킴 허용 목록 상한을 25개로 절반 줄였습니다. 대체 수단과, 그 대체 수단이 끝내 할 수 없는 검사 하나를 정리합니다.

19 분 소요