StoreKit 2 인터뷰: 구독 관리 및 영수증 검증
StoreKit 2, 구독 관리, 영수증 검증, 인앱 구매 구현에 관한 iOS 인터뷰 질문을 실용적인 Swift 코드 예제와 함께 마스터하십시오.

StoreKit 2는 iOS에서 인앱 구매가 처리되는 방식의 근본적인 변화를 나타냅니다. iOS 15와 함께 도입된 이 최신 프레임워크는 구독 관리 및 트랜잭션 검증에 필요한 코드를 극적으로 단순화합니다. iOS 18에서 StoreKit 1은 공식적으로 지원 중단되어, StoreKit 2가 유일한 지원 경로가 되었습니다. iOS 기술 인터뷰에서는 앱 수익화에 대한 개발자의 전문성을 평가하기 위해 이 주제를 정기적으로 다룹니다.
StoreKit 2는 async/await를 사용한 네이티브 Swift API를 활용하여 복잡한 콜백과 클라이언트 측 영수증 검증의 필요성을 제거합니다. 인터뷰어는 StoreKit 1과의 근본적인 차이점과 appTransactionID 및 SubscriptionOfferView와 같은 새로운 iOS 18.4 기능을 명확하게 설명할 수 있는 후보자를 높이 평가합니다.
StoreKit 2 아키텍처 개요
StoreKit 2는 Swift Concurrency를 최대한 활용하는 최신 아키텍처를 기반으로 구축되었습니다. StoreKit 1과 달리 모든 작업은 비동기적이며 네이티브 Swift 타입을 사용합니다.
사용 가능한 제품 가져오기
첫 번째 단계는 App Store Connect에 구성된 제품을 로드하는 것입니다. StoreKit 2는 선언적 API로 이 작업을 단순화합니다.
import StoreKit
actor ProductService {
// App Store Connect에 구성된 제품 식별자
private let productIdentifiers: Set<String> = [
"com.app.subscription.monthly",
"com.app.subscription.yearly",
"com.app.premium.lifetime"
]
// 반복적인 네트워크 호출을 피하기 위한 제품 캐시
private var cachedProducts: [Product] = []
func loadProducts() async throws -> [Product] {
// 사용 가능한 경우 캐시 반환
guard cachedProducts.isEmpty else {
return cachedProducts
}
// App Store에 비동기 요청
let products = try await Product.products(for: productIdentifiers)
cachedProducts = products
return products
}
func product(for identifier: String) async throws -> Product? {
let products = try await loadProducts()
return products.first { $0.id == identifier }
}
}actor를 사용하면 제품 캐시를 관리할 때 스레드 안전성이 보장됩니다.
StoreKit 2 제품 유형
StoreKit 2는 다양한 제품 유형을 명확하게 구분합니다. 이러한 구분을 이해하는 것은 인터뷰에 필수적입니다.
import StoreKit
extension Product.ProductType {
var displayName: String {
switch self {
case .consumable:
// 소비형 제품(크레딧, 라이프 등)
return "Consumable"
case .nonConsumable:
// 영구 구매(기능 잠금 해제)
return "Non-Consumable"
case .autoRenewable:
// 자동 갱신 구독
return "Auto-Renewable Subscription"
case .nonRenewable:
// 갱신 없는 구독(임시 패스)
return "Non-Renewable Subscription"
@unknown default:
return "Unknown"
}
}
}자동 갱신 구독은 현대 iOS 애플리케이션의 지배적인 비즈니스 모델을 나타냅니다.
구매 트랜잭션 처리
StoreKit 2의 구매 프로세스는 async/await 덕분에 선형적이 됩니다. StoreKit 1의 델리게이트와 중첩된 콜백은 더 이상 필요하지 않습니다.
구매 시작
iOS 18.2부터 구매에는 UI 컨텍스트 매개변수가 필요합니다. 이 변경으로 구매 시트가 앱의 뷰 계층 구조 내에서 올바르게 표시됩니다.
import StoreKit
import UIKit
actor PurchaseManager {
enum PurchaseError: Error {
case productNotFound
case purchaseCancelled
case purchasePending
case verificationFailed
case unknown
}
func purchase(_ product: Product, in scene: UIWindowScene) async throws -> Transaction {
// UI 컨텍스트로 구매 시작 (iOS 18.2+)
let result = try await product.purchase(confirmIn: scene)
switch result {
case .success(let verification):
// 트랜잭션 서명 검증
let transaction = try checkVerification(verification)
// 트랜잭션을 완료된 것으로 표시
await transaction.finish()
return transaction
case .userCancelled:
// 사용자가 구매 모달을 취소함
throw PurchaseError.purchaseCancelled
case .pending:
// 보류 중인 구매(부모 승인 등)
throw PurchaseError.purchasePending
@unknown default:
throw PurchaseError.unknown
}
}
private func checkVerification<T>(_ result: VerificationResult<T>) throws -> T {
switch result {
case .verified(let safe):
// App Store에서 서명되고 검증된 트랜잭션
return safe
case .unverified(_, let error):
// 잘못되었거나 손상된 서명
throw PurchaseError.verificationFailed
}
}
}finish() 메서드는 매우 중요합니다: 콘텐츠가 사용자에게 전달되었음을 App Store에 알립니다.
콘텐츠 전달 후 transaction.finish()를 호출하는 것을 절대 잊지 마십시오. 완료되지 않은 트랜잭션은 다음 앱 실행 시 복원되어 예기치 않은 동작을 일으킵니다.
백그라운드 트랜잭션 수신
StoreKit 2는 앱 외부에서 발생하는 트랜잭션(갱신, 가족 공유 구매 등)을 발행하는 비동기 스트림인 Transaction.updates를 도입합니다.
import StoreKit
actor TransactionObserver {
private var updateTask: Task<Void, Never>?
func startObserving() {
// 이전 관찰 취소
updateTask?.cancel()
updateTask = Task(priority: .background) {
// 트랜잭션 업데이트의 무한 스트림
for await result in Transaction.updates {
do {
let transaction = try self.checkVerification(result)
// 유형에 따라 트랜잭션 처리
await self.handleTransaction(transaction)
// 항상 트랜잭션 완료
await transaction.finish()
} catch {
// 디버깅을 위해 오류 기록
print("Transaction verification failed: (error)")
}
}
}
}
func stopObserving() {
updateTask?.cancel()
updateTask = nil
}
private func handleTransaction(_ transaction: Transaction) async {
switch transaction.productType {
case .autoRenewable:
// 구독 상태 업데이트
await updateSubscriptionStatus(transaction)
case .nonConsumable:
// 프리미엄 기능 잠금 해제
await unlockFeature(transaction.productID)
default:
break
}
}
private func checkVerification<T>(_ result: VerificationResult<T>) throws -> T {
switch result {
case .verified(let safe):
return safe
case .unverified:
throw PurchaseError.verificationFailed
}
}
private func updateSubscriptionStatus(_ transaction: Transaction) async {
// 상태 업데이트 구현
}
private func unlockFeature(_ productID: String) async {
// 기능 잠금 해제 구현
}
enum PurchaseError: Error {
case verificationFailed
}
}트랜잭션 관찰은 업데이트를 놓치지 않도록 앱이 시작되자마자 시작되어야 합니다.
StoreKit 2 구독에 대한 일반적인 인터뷰 질문
iOS 인터뷰에는 구독 관리에 관한 구체적인 질문이 자주 포함됩니다. 다음은 가장 일반적인 질문과 자세한 답변입니다. 더 많은 Swift 인터뷰 주제는 Swift 구조화된 동시성 가이드를 참조하십시오.
질문 1: 현재 구독 상태를 확인하는 방법은 무엇입니까?
StoreKit 2는 활성 권한에 대한 직접 액세스를 제공합니다. Transaction.currentEntitlement(for:)는 iOS 18.4에서 지원 중단되었습니다. 새로운 currentEntitlements(for:)는 여러 권한(예: 구독과 가족 공유 액세스)을 처리하기 위한 비동기 시퀀스를 반환합니다.
import StoreKit
struct SubscriptionStatusChecker {
struct SubscriptionStatus {
let isActive: Bool
let expirationDate: Date?
let willRenew: Bool
let productID: String?
let appTransactionID: UInt64? // iOS 18.4+
}
static func checkCurrentStatus(for productID: String) async -> SubscriptionStatus {
// 새로운 currentEntitlements(for:) API 사용 (iOS 18.4+)
var latestTransaction: Transaction?
for await result in Transaction.currentEntitlements(for: productID) {
if case .verified(let transaction) = result {
if transaction.productType == .autoRenewable {
if latestTransaction == nil ||
transaction.purchaseDate > latestTransaction!.purchaseDate {
latestTransaction = transaction
}
}
}
}
guard let transaction = latestTransaction else {
return SubscriptionStatus(
isActive: false,
expirationDate: nil,
willRenew: false,
productID: nil,
appTransactionID: nil
)
}
// 구독이 여전히 활성인지 확인
let isActive = transaction.expirationDate ?? Date.distantPast > Date()
// 갱신 정보 가져오기
let renewalInfo = await getRenewalInfo(for: transaction.productID)
return SubscriptionStatus(
isActive: isActive,
expirationDate: transaction.expirationDate,
willRenew: renewalInfo?.willAutoRenew ?? false,
productID: transaction.productID,
appTransactionID: transaction.appTransactionID
)
}
private static func getRenewalInfo(for productID: String) async -> Product.SubscriptionInfo.RenewalInfo? {
guard let product = try? await Product.products(for: [productID]).first,
let subscription = product.subscription else {
return nil
}
// 갱신 상태 가져오기
let status = try? await subscription.status.first
return status?.renewalInfo
}
}이 접근 방식은 서버 호출 없이 정확한 구독 상태를 제공합니다. iOS 18.4에 도입된 appTransactionID 필드는 서버 간 호출 없이 트랜잭션 간에 고객을 추적할 수 있게 합니다.
iOS 면접 준비가 되셨나요?
인터랙티브 시뮬레이터, flashcards, 기술 테스트로 연습하세요.
질문 2: 무료 평가판 및 프로모션 제공을 처리하는 방법은 무엇입니까?
StoreKit 2는 제공 정보에 대한 액세스를 단순화합니다. 인터뷰어는 종종 다양한 제공 유형에 대한 이해를 테스트합니다. iOS 18.4부터 App Store Server Library에서 사용 가능한 새로운 PromotionOfferV2SignatureCreator를 사용하여 JWS로 프로모션 제공에 서명할 수 있습니다.
import StoreKit
struct OfferManager {
enum OfferType {
case freeTrial(duration: String)
case payAsYouGo(periods: Int, price: Decimal)
case payUpFront(duration: String, price: Decimal)
case oneTime(price: Decimal) // iOS 17.2+: 모든 제품 유형의 제공 코드
case none
}
static func getIntroductoryOffer(for product: Product) -> OfferType {
guard let subscription = product.subscription,
let offer = subscription.introductoryOffer else {
return .none
}
switch offer.paymentMode {
case .freeTrial:
// 무료 평가판 기간
return .freeTrial(duration: formatPeriod(offer.period))
case .payAsYouGo:
// 여러 기간에 걸친 할인 가격
return .payAsYouGo(
periods: offer.periodCount,
price: offer.price
)
case .payUpFront:
// 기간에 대한 단일 결제
return .payUpFront(
duration: formatPeriod(offer.period),
price: offer.price
)
case .oneTime:
// 일회성 제공 코드(소비형, 비소비형, 비갱신형)
return .oneTime(price: offer.price)
@unknown default:
return .none
}
}
static func checkEligibility(for product: Product) async -> Bool {
// 사용자가 제공의 혜택을 받을 수 있는지 확인
guard let subscription = product.subscription else {
return false
}
// isEligibleForIntroOffer는 새로운 구독자인지를 나타냄
return await subscription.isEligibleForIntroOffer
}
private static func formatPeriod(_ period: Product.SubscriptionPeriod) -> String {
let value = period.value
switch period.unit {
case .day:
return "(value) day(value > 1 ? "s" : "")"
case .week:
return "(value) week(value > 1 ? "s" : "")"
case .month:
return "(value) month(value > 1 ? "s" : "")"
case .year:
return "(value) year(value > 1 ? "s" : "")"
@unknown default:
return ""
}
}
}isEligibleForIntroOffer 메서드는 사용할 수 없는 제공을 표시하는 것을 피하기 위해 필수적입니다. 이전에 자동 갱신 구독으로 제한되었던 제공 코드는 iOS 16.3부터 소비형, 비소비형, 비갱신형 구독도 지원합니다.
질문 3: 이전 구매를 복원하는 방법은 무엇입니까?
구매 복원은 App Store 심사 지침에 따라 필수 기능입니다.
import StoreKit
actor RestoreManager {
struct RestoreResult {
let restoredProducts: [String]
let activeSubscriptions: [String]
}
func restorePurchases() async throws -> RestoreResult {
var restoredProducts: [String] = []
var activeSubscriptions: [String] = []
// App Store와 동기화
try await AppStore.sync()
// 모든 사용자 트랜잭션을 반복
for await result in Transaction.currentEntitlements {
guard case .verified(let transaction) = result else {
continue
}
switch transaction.productType {
case .nonConsumable:
// 영구 구매 복원
restoredProducts.append(transaction.productID)
case .autoRenewable:
// 구독이 활성인지 확인
if let expiration = transaction.expirationDate,
expiration > Date() {
activeSubscriptions.append(transaction.productID)
}
default:
// 소비형은 복원할 수 없음
break
}
}
return RestoreResult(
restoredProducts: restoredProducts,
activeSubscriptions: activeSubscriptions
)
}
}AppStore.sync()는 Apple 서버와의 동기화를 강제하며 재설치 후에 유용합니다.
SubscriptionOfferView: 현대적인 구독 머천다이징
iOS 18.4에서 도입된 SubscriptionOfferView는 자동 제품 메타데이터 로딩을 통해 구독 옵션을 표시하는 SwiftUI 컴포넌트입니다. 구독 기반 앱을 구축하는 개발자에게 일반적인 인터뷰 주제입니다.
import SwiftUI
import StoreKit
struct SubscriptionSettingsView: View {
let subscriptionGroupID = "com.app.subscription.group"
var body: some View {
VStack(spacing: 16) {
// 현재 구독자에게 업그레이드 옵션 표시
SubscriptionOfferView(groupID: subscriptionGroupID)
.visibleRelationship(.upgrade)
.prefersPromotionalIcon(true)
.subscriptionOfferViewDetailAction { path in
path.append("subscriptionDetail")
}
// 현재 플랜 세부 정보 표시
SubscriptionOfferView(groupID: subscriptionGroupID)
.visibleRelationship(.current)
}
.padding()
}
}visibleRelationship 매개변수는 표시되는 구독 티어를 제어합니다: .upgrade는 한 단계 높은 플랜을 표시하고, .downgrade는 낮은 티어를, .crossgrade는 더 나은 가치의 동등한 티어를, .all은 그룹의 모든 플랜을 표시합니다.
서버 측 영수증 검증
중요한 구독이 있는 애플리케이션에는 서버 측 검증이 여전히 권장됩니다. StoreKit 2는 최신 검증을 위해 JWS(JSON Web Signatures)를 사용합니다. 지원 중단된 verifyReceipt 엔드포인트는 App Store Server API로 대체되었습니다.
서버용 트랜잭션 데이터 추출
import StoreKit
struct TransactionPayload: Codable {
let transactionID: String
let originalTransactionID: String
let productID: String
let purchaseDate: Date
let expirationDate: Date?
let jwsRepresentation: String
let appTransactionID: String? // iOS 18.4+
}
actor ServerValidationService {
private let apiEndpoint = "https://api.example.com/verify"
func validateWithServer(_ transaction: Transaction) async throws -> Bool {
// 서버용 페이로드 생성
let payload = TransactionPayload(
transactionID: String(transaction.id),
originalTransactionID: String(transaction.originalID),
productID: transaction.productID,
purchaseDate: transaction.purchaseDate,
expirationDate: transaction.expirationDate,
jwsRepresentation: transaction.jwsRepresentation ?? "",
appTransactionID: transaction.appTransactionID.map { String($0) }
)
// JSON으로 인코딩
let encoder = JSONEncoder()
encoder.dateEncodingStrategy = .iso8601
let jsonData = try encoder.encode(payload)
// 서버로 전송
var request = URLRequest(url: URL(string: apiEndpoint)!)
request.httpMethod = "POST"
request.httpBody = jsonData
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
let (data, response) = try await URLSession.shared.data(for: request)
guard let httpResponse = response as? HTTPURLResponse,
httpResponse.statusCode == 200 else {
return false
}
// 서버 응답 디코딩
let validationResponse = try JSONDecoder().decode(
ValidationResponse.self,
from: data
)
return validationResponse.isValid
}
}
struct ValidationResponse: Codable {
let isValid: Bool
let subscriptionStatus: String?
}jwsRepresentation에는 서버에서 검증할 수 있는 암호화 서명이 포함되어 있습니다. 새로운 appTransactionID는 추가 서버 라운드 트립 없이 정확한 고객 추적을 가능하게 합니다.
Apple의 App Store Server API는 JWS 토큰을 통한 서버 측 검증, 실시간 구독 이벤트를 위한 Server Notifications V2, 그리고 상태를 직접 쿼리하기 위해 트랜잭션 ID를 받아들이는 새로운 SubscriptionStatus API(iOS 18.4+)를 제공합니다.
서버 알림 구성
Server Notifications V2는 실시간 이벤트 수신을 가능하게 합니다.
enum NotificationType: String, Codable {
case subscribed = "SUBSCRIBED"
case didRenew = "DID_RENEW"
case didFailToRenew = "DID_FAIL_TO_RENEW"
case didChangeRenewalStatus = "DID_CHANGE_RENEWAL_STATUS"
case expired = "EXPIRED"
case gracePeriodExpired = "GRACE_PERIOD_EXPIRED"
case offerRedeemed = "OFFER_REDEEMED"
case priceIncrease = "PRICE_INCREASE"
case refund = "REFUND"
case renewalExtended = "RENEWAL_EXTENDED"
case revoke = "REVOKE"
}
struct ServerNotification: Codable {
let notificationType: NotificationType
let subtype: String?
let data: NotificationData
struct NotificationData: Codable {
let bundleId: String
let environment: String
let signedTransactionInfo: String
let signedRenewalInfo: String?
}
}이러한 알림을 통해 백엔드와 동기화된 구독 상태를 유지할 수 있습니다.
오류 처리 및 엣지 케이스
인터뷰는 종종 오류 시나리오와 적절한 처리에 대한 이해를 테스트합니다. iOS 아키텍처 패턴에 대한 자세한 내용은 iOS 시니어 인터뷰 아키텍처 가이드를 참조하십시오.
StoreKit 2의 일반적인 오류
import StoreKit
struct PurchaseErrorHandler {
enum UserFacingError {
case networkError
case paymentFailed
case notAuthorized
case productNotAvailable
case unknown
var message: String {
switch self {
case .networkError:
return "Connection failed. Please check your internet connection."
case .paymentFailed:
return "Payment failed. Please verify your payment information."
case .notAuthorized:
return "In-app purchases are disabled on this device."
case .productNotAvailable:
return "This product is not currently available."
case .unknown:
return "An unexpected error occurred."
}
}
}
static func handle(_ error: Error) -> UserFacingError {
// StoreKit 오류인지 확인
if let storeKitError = error as? StoreKitError {
switch storeKitError {
case .networkError:
return .networkError
case .userCancelled:
// 오류 표시 안 함, 사용자가 취소함
return .unknown
case .notAvailableInStorefront:
return .productNotAvailable
case .notEntitled:
return .notAuthorized
default:
return .unknown
}
}
// 구매 오류
if let purchaseError = error as? Product.PurchaseError {
switch purchaseError {
case .invalidQuantity:
return .unknown
case .productUnavailable:
return .productNotAvailable
case .purchaseNotAllowed:
return .notAuthorized
default:
return .unknown
}
}
return .unknown
}
}명확한 오류 처리는 사용자 경험을 개선하고 디버깅을 단순화합니다.
오프라인 모드 지원
import StoreKit
actor OfflineCapabilityManager {
private let userDefaults = UserDefaults.standard
private let entitlementsKey = "cached_entitlements"
func cacheCurrentEntitlements() async {
var entitlements: [String: Date] = [:]
for await result in Transaction.currentEntitlements {
if case .verified(let transaction) = result {
entitlements[transaction.productID] = transaction.expirationDate
}
}
// 로컬에 저장
if let data = try? JSONEncoder().encode(entitlements) {
userDefaults.set(data, forKey: entitlementsKey)
}
}
func getCachedEntitlements() -> [String: Date] {
guard let data = userDefaults.data(forKey: entitlementsKey),
let entitlements = try? JSONDecoder().decode(
[String: Date].self,
from: data
) else {
return [:]
}
// 만료된 권한 필터링
let now = Date()
return entitlements.filter { _, expiration in
expiration > now
}
}
func hasValidEntitlement(for productID: String) -> Bool {
let cached = getCachedEntitlements()
return cached[productID] != nil
}
}로컬 캐싱은 연결이 없어도 프리미엄 기능에 대한 액세스를 유지합니다.
프로덕션을 위한 모범 사례
권장 아키텍처
import StoreKit
import Combine
@MainActor
final class SubscriptionService: ObservableObject {
static let shared = SubscriptionService()
@Published private(set) var products: [Product] = []
@Published private(set) var purchasedProductIDs: Set<String> = []
@Published private(set) var subscriptionStatus: SubscriptionStatus = .none
private var transactionObserver: Task<Void, Never>?
enum SubscriptionStatus {
case none
case active(expirationDate: Date)
case expired
case inGracePeriod
}
private init() {
// 트랜잭션 관찰 시작
startTransactionObserver()
// 초기 상태 로드
Task {
await loadProducts()
await updateSubscriptionStatus()
}
}
private func startTransactionObserver() {
transactionObserver = Task.detached(priority: .background) {
for await result in Transaction.updates {
if case .verified(let transaction) = result {
await transaction.finish()
await self.updateSubscriptionStatus()
}
}
}
}
func loadProducts() async {
do {
let productIDs = [
"com.app.subscription.monthly",
"com.app.subscription.yearly"
]
products = try await Product.products(for: productIDs)
} catch {
print("Failed to load products: (error)")
}
}
func purchase(_ product: Product) async throws {
let result = try await product.purchase()
switch result {
case .success(let verification):
if case .verified(let transaction) = verification {
await transaction.finish()
await updateSubscriptionStatus()
}
case .userCancelled:
break
case .pending:
break
@unknown default:
break
}
}
func updateSubscriptionStatus() async {
var purchasedIDs: Set<String> = []
var latestSubscription: Transaction?
for await result in Transaction.currentEntitlements {
if case .verified(let transaction) = result {
purchasedIDs.insert(transaction.productID)
if transaction.productType == .autoRenewable {
if latestSubscription == nil ||
transaction.purchaseDate > latestSubscription!.purchaseDate {
latestSubscription = transaction
}
}
}
}
purchasedProductIDs = purchasedIDs
if let subscription = latestSubscription,
let expiration = subscription.expirationDate {
if expiration > Date() {
subscriptionStatus = .active(expirationDate: expiration)
} else {
subscriptionStatus = .expired
}
} else {
subscriptionStatus = .none
}
}
deinit {
transactionObserver?.cancel()
}
}이 중앙 집중식 아키텍처는 애플리케이션 전체에서 구매 상태 관리를 단순화합니다.
Sources
- What's new in StoreKit and In-App Purchase, WWDC25 -
appTransactionID및SubscriptionOfferView를 포함한 iOS 18.4 API 추가 - App Store Server API Documentation - JWS 및 Server Notifications V2를 통한 서버 측 검증
- StoreKit Documentation - StoreKit 2의 완전한 API 참조
StoreKit 2 인터뷰의 핵심 포인트
StoreKit 2는 인앱 구매 관리를 현대적이고 안전한 개발 경험으로 변환합니다. 인터뷰에서 기억해야 할 핵심 요점:
- 네이티브 async/await API가 iOS 18에서 지원 중단된 StoreKit 1의 복잡한 델리게이트와 콜백을 제거
VerificationResult를 통한 자동 검증이 암호화 트랜잭션 검증을 처리Transaction.updates가 갱신 및 가족 공유를 포함한 백그라운드 트랜잭션을 위한 비동기 스트림 제공Transaction.currentEntitlements(for:)가 여러 권한 처리를 위해 지원 중단된currentEntitlement(for:)를 대체appTransactionID필드(iOS 18.4+)가 서버 간 호출 없이 고객 추적 가능SubscriptionOfferView가 업그레이드, 다운그레이드, 크로스그레이드 옵션을 위한visibleRelationship을 갖춘 구독 머천다이징용 내장 UI 제공- 프로모션 제공은
PromotionOfferV2SignatureCreator를 사용하여 JWS로 서명 가능 - 제공 코드가 소비형, 비소비형, 비갱신형 구독 지원(iOS 16.3+)
- 서버 검증이 지원 중단된
verifyReceipt엔드포인트 대신 JWS 토큰을 사용한 App Store Server API 사용 - 예기치 않은 복원 동작을 방지하기 위해 콘텐츠 전달 후 항상
transaction.finish()호출 - 연결 없이 원활한 사용자 경험을 위해 오프라인 권한 캐싱 구현
연습을 시작하세요!
면접 시뮬레이터와 기술 테스트로 지식을 테스트하세요.
iOS 코드의 버그를 찾을 수 있나요
실제 코드 한 조각, 숨은 버그 하나, 하루 한 번. 계정 없이 바로 도전할 수 있습니다.

작성자
Anthony Fillion-MailletSharpSkill 창업자
10년 이상 풀스택 개발을 해왔습니다. SharpSkill을 운영하며 이곳에 게시되는 모든 내용에 책임을 집니다.
2026년 10월 20일 업데이트
태그
공유
관련 기사

Swift Testing Framework 면접 2026: #expect와 #require 매크로 vs XCTest
iOS 면접을 위한 새로운 Swift Testing Framework를 마스터합니다: #expect와 #require 매크로, XCTest 마이그레이션, 고급 패턴 및 흔한 함정.

iOS 푸시 알림 면접 2026: APNs, 토큰, 트러블슈팅
Push Notifications, APNs, 토큰 관리, 트러블슈팅에 관한 iOS 면접 준비 완벽 가이드입니다. 자주 묻는 질문에 상세한 답변을 함께 담았습니다.

iOS 시니어 면접 2026: 아키텍처와 디자인 패턴 질문
MVVM, VIPER, Clean Architecture, 디자인 패턴 핵심 질문으로 iOS 시니어 면접을 준비합니다. Swift 코드 예시가 포함된 완벽 가이드입니다.