iOS NFC와 비접촉 결제: CoreNFC로 카드 읽기부터 결제까지

iOS

iOSNFCCoreNFCApple Pay결제

이 글은 누구를 위한 것인가

  • 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로 처리한다.