이 글은 누구를 위한 것인가
- iOS 앱에서 NFC 태그를 읽고 쓰려는 개발자
- Apple Pay 결제를 앱에 통합하려는 팀
- 사원증, 쿠폰, 티켓 NFC 리더를 구현하려는 개발자
들어가며
NFC(Near Field Communication)는 근거리 무선 통신으로 교통카드, 사원증, 결제 단말기에 사용된다. iOS 11부터 CoreNFC로 NFC 태그를 읽을 수 있고, iOS 13부터는 쓰기도 지원한다. Apple Pay는 PassKit으로 통합한다.
이 글은 bluefoxdev.kr의 iOS CoreNFC 비접촉 결제 가이드 를 참고하여 작성했습니다.
1. iOS NFC 아키텍처
[CoreNFC 지원 범위]
iOS 11+: NDEF 태그 읽기
iOS 13+: NDEF 태그 쓰기, ISO 15693
iOS 14+: 백그라운드 NFC 태그 읽기
지원 태그: NFC-A/B/F/V, ISO 15693, ISO 7816
[Info.plist 설정]
NFCReaderUsageDescription: "NFC 사용 설명"
com.apple.developer.nfc.readersession.formats:
- NDEF (일반 태그)
- TAG (특수 태그)
[Apple Pay 구성]
MerchantID: Certificates에서 발급
PassKit Entitlement 추가
결제 기능: PKPaymentAuthorizationViewController
카드 추가: PKAddPaymentPassViewController
[NFC 결제 vs Apple Pay]
NFC 읽기: 사원증, 쿠폰, 멤버십 확인
Apple Pay: 실제 결제 처리 (Stripe, PG사)
2. CoreNFC 구현
import CoreNFC
import PassKit
import SwiftUI
// NFC 태그 읽기
class NFCReader: NSObject, NFCNDEFReaderSessionDelegate {
private var session: NFCNDEFReaderSession?
private var onRead: ((String) -> Void)?
func startReading(completion: @escaping (String) -> Void) {
guard NFCNDEFReaderSession.readingAvailable else {
print("NFC 미지원 기기")
return
}
onRead = completion
session = NFCNDEFReaderSession(
delegate: self,
queue: nil,
invalidateAfterFirstRead: true
)
session?.alertMessage = "NFC 태그를 기기 상단에 가져다 대세요"
session?.begin()
}
func readerSession(_ session: NFCNDEFReaderSession, didDetectNDEFs messages: [NFCNDEFMessage]) {
for message in messages {
for record in message.records {
if let string = String(data: record.payload, encoding: .utf8) {
DispatchQueue.main.async {
self.onRead?(string)
}
}
}
}
}
func readerSession(_ session: NFCNDEFReaderSession, didInvalidateWithError error: Error) {
if (error as? NFCReaderError)?.code != .readerSessionInvalidationErrorFirstNDEFTagRead {
print("NFC 오류: \(error.localizedDescription)")
}
}
}
// NFC 태그 쓰기 (iOS 13+)
class NFCWriter: NSObject, NFCNDEFReaderSessionDelegate {
private var session: NFCNDEFReaderSession?
private var messageToWrite: NFCNDEFMessage?
func writeTag(content: String) {
let payload = NFCNDEFPayload(
format: .nfcWellKnown,
type: Data("T".utf8),
identifier: Data(),
payload: Data(content.utf8)
)
messageToWrite = NFCNDEFMessage(records: [payload])
session = NFCNDEFReaderSession(delegate: self, queue: nil, invalidateAfterFirstRead: false)
session?.alertMessage = "태그에 기기를 가져다 대세요"
session?.begin()
}
func readerSession(_ session: NFCNDEFReaderSession, didDetect tags: [NFCNDEFTag]) {
guard let tag = tags.first, let message = messageToWrite else { return }
session.connect(to: tag) { error in
guard error == nil else { return }
tag.queryNDEFStatus { status, capacity, error in
guard status == .readWrite else {
session.invalidate(errorMessage: "쓰기 불가 태그")
return
}
tag.writeNDEF(message) { error in
if let error = error {
session.invalidate(errorMessage: "쓰기 실패: \(error.localizedDescription)")
} else {
session.alertMessage = "쓰기 완료!"
session.invalidate()
}
}
}
}
}
func readerSession(_ session: NFCNDEFReaderSession, didInvalidateWithError error: Error) {}
func readerSession(_ session: NFCNDEFReaderSession, didDetectNDEFs messages: [NFCNDEFMessage]) {}
}
// Apple Pay 통합
struct ApplePayButton: View {
let amount: Decimal
let merchantId: String
var body: some View {
PaymentButton(action: handlePayment)
.frame(height: 50)
}
func handlePayment() {
let request = PKPaymentRequest()
request.merchantIdentifier = merchantId
request.supportedNetworks = [.visa, .masterCard, .amex]
request.merchantCapabilities = .threeDSecure
request.countryCode = "KR"
request.currencyCode = "KRW"
request.paymentSummaryItems = [
PKPaymentSummaryItem(label: "결제 금액", amount: NSDecimalNumber(decimal: amount))
]
guard PKPaymentAuthorizationController.canMakePayments(usingNetworks: request.supportedNetworks) else {
print("Apple Pay 미지원")
return
}
let controller = PKPaymentAuthorizationController(paymentRequest: request)
controller.delegate = ApplePayDelegate.shared
controller.present(completion: nil)
}
}
class ApplePayDelegate: NSObject, PKPaymentAuthorizationControllerDelegate {
static let shared = ApplePayDelegate()
func paymentAuthorizationController(
_ controller: PKPaymentAuthorizationController,
didAuthorizePayment payment: PKPayment,
handler completion: @escaping (PKPaymentAuthorizationResult) -> Void
) {
// 서버에 payment.token 전송하여 PG사 처리
Task {
do {
let result = try await processPaymentOnServer(token: payment.token.paymentData)
completion(PKPaymentAuthorizationResult(status: .success, errors: nil))
} catch {
completion(PKPaymentAuthorizationResult(status: .failure, errors: [error]))
}
}
}
func paymentAuthorizationControllerDidFinish(_ controller: PKPaymentAuthorizationController) {
controller.dismiss(completion: nil)
}
func processPaymentOnServer(token: Data) async throws -> Bool { return true }
}
마무리
CoreNFC의 핵심은 세션 관리다. NFCNDEFReaderSession은 인증된 앱에서만 동작하며, Info.plist에 사용 설명을 반드시 추가해야 한다. 쓰기는 iOS 13+ 전용이고 태그가 쓰기 가능 상태인지 먼저 확인해야 한다. Apple Pay는 PKPaymentRequest로 결제 정보를 구성하고, 서버에서 payment token을 PG사 API로 처리한다.