CBPeripheral은 iOS에서 원격 BLE 기기를 나타내는 Core Bluetooth 프레임워크의 클래스입니다. 각 CBPeripheral 객체는 연결된 BLE 기기의 UUID, 이름, RSSI 및 GATT 서비스 계층 구조를 캡슐화합니다. 개발자는 CBPeripheral을 통해서만 주변기기와 상호 작용합니다: 서비스 발견(discoverServices:), 특성 읽기(readValueForCharacteristic:), 데이터 쓰기(writeValue:forCharacteristic:type:) 및 알림 구독(setNotifyValue:forCharacteristic:). Apple Developer, 2026에 따르면, CBPeripheral은 모든 BLE 주변기기 작업의 중심 객체이며, 기기 발견 또는 연결 시 CBCentralManager에 의해 반환됩니다.
핵심 포인트
CBPeripheral은 iOS 애플리케이션에서 원격 BLE 기기를 나타내는 객체입니다. iPhone의 로컬 Bluetooth 어댑터를 관리하는 CBCentralManager와 달리, CBPeripheral은 외부 주변 기기(센서, 피트니스 트래커, 비콘 또는 의료 기기)를 모델링합니다. 각 CBPeripheral 인스턴스에는 연결 세션 간에 지속되는 고유 식별자(UUID)가 포함되어 있습니다. Apple은 시스템 본딩을 통해 UUID를 특정 기기에 연결합니다.
CBPeripheral은 init을 통해 직접 생성되지 않습니다. Core Bluetooth 프레임워크는 두 가지 시나리오에서 CBPeripheral 객체를 반환합니다: scanForPeripheralsWithServices:를 통해 기기가 발견될 때(델리게이트 didDiscoverPeripheral) 및 retrievePeripheralsWithIdentifiers:를 통해 이전에 알려진 기기에 연결할 때. 객체를 얻은 후, 개발자는 CBCentralManager에서 connectPeripheral:을 호출하며, 이후 CBPeripheral을 GATT 작업에 사용할 수 있습니다.
CBPeripheral 수명 주기에는 6가지 상태가 포함됩니다: 연결 끊김(초기), 연결 중(connect 호출 후), 연결됨(didConnectPeripheral 후), 발견 중(discoverServices 호출 중), 발견됨(서비스 수신 후), 연결 끊는 중(cancelPeripheralConnection 후). 각 상태는 CBPeripheralDelegate 프로토콜을 통해 추적됩니다. 이는 iOS에서 BLE 애플리케이션에 필수입니다.
CBPeripheral은 세 가지 수준으로 구성된 계층적 GATT 구조를 저장합니다. 루트 수준은 CBService(서비스)의 배열이며, 각 서비스에는 CBCharacteristic(특성)의 배열이 포함되고, 각 특성에는 CBDescriptor(디스크립터)의 배열이 포함됩니다. 이 모델은 Bluetooth GATT 사양을 완전히 준수합니다: 서비스는 기기 기능(예: “심박수 서비스”), 특성은 특정 값(맥박 72bpm), 디스크립터는 특성 메타데이터(측정 단위, 알림 구성)입니다.
| 수준 | Core Bluetooth 클래스 | 설명 |
|---|---|---|
| 서비스 | CBService | 관련 특성의 논리적 그룹, UUID(16비트, 32비트 또는 128비트)로 식별 |
| 특성 | CBCharacteristic | 특정 데이터 값, 읽기, 쓰기 및 알림 지원 |
| 디스크립터 | CBDescriptor | 특성 메타데이터: 클라이언트 구성 CCCD, 사용자 설명, 프레젠테이션 형식 |
표준 BLE 서비스는 Bluetooth SIG에 의해 등록됩니다: Heart Rate Service(UUID 180D), Battery Service(180F), Device Information(180A), Blood Pressure(1810). 사용자 정의 서비스에는 128비트 UUID(예: E20A39F4-73F5-4BC4-A12F-17D1AD07A961)가 사용됩니다. iOS는 표준 UUID를 자동으로 인식하고 사람이 읽을 수 있는 이름을 표시합니다. 사용자 정의 UUID는 16진수 형식으로 표시됩니다.
연결 후, CBPeripheral의 계층 구조는 비어 있습니다 — 서비스와 특성이 로드되지 않습니다. 개발자는 discoverServices:를 호출하여 서비스를 가져온 다음 각 서비스에 대해 discoverCharacteristics:forService:를 호출해야 합니다. 서비스에 포함된 서비스가 있는 경우, 추가로 discoverIncludedServices:forService:를 호출합니다. 계층 구조 발견이 완료된 후에만 CBPeripheral이 채워지고 읽기 및 쓰기에 사용할 수 있습니다.
발견 CBPeripheral의 GATT 구조 발견은 읽기 또는 쓰기 작업 전에 필수 단계입니다. discoverServices: 메서드는 모든 기기 서비스의 비동기 검색을 시작합니다. nil이 전달되면 모든 서비스가 발견되고, CBUUID 배열이 전달되면 지정된 UUID가 있는 서비스만 발견됩니다(시간 최적화). 결과는 델리게이트 peripheral:didDiscoverServices:에 도착합니다 — CBPeripheral 객체는 services 속성을 CBService 배열로 채웁니다.
서비스를 수신한 후, 각 CBService에 대해 discoverCharacteristics:forService:를 호출해야 합니다. 마찬가지로, nil — 모든 특성, CBUUID 배열 — 지정된 특성만. 결과: peripheral:didDiscoverCharacteristicsForService:error:. 이 단계에서 CBCharacteristic은 허용된 작업을 정의하는 속성(properties: .read, .write, .notify, .indicate)을 받습니다.
import CoreBluetooth
extension BLEViewController: CBPeripheralDelegate {
// 1. Service discovery
func peripheral(_ peripheral: CBPeripheral,
didDiscoverServices error: Error?) {
guard let services = peripheral.services else { return }
for service in services {
// Request characteristics for each service
peripheral.discoverCharacteristics(nil, for: service)
}
}
// 2. Characteristic discovery
func peripheral(_ peripheral: CBPeripheral,
didDiscoverCharacteristicsFor service: CBService,
error: Error?) {
guard let characteristics = service.characteristics else { return }
for characteristic in characteristics {
if characteristic.properties.contains(.read) {
peripheral.readValue(for: characteristic)
}
if characteristic.properties.contains(.notify) {
peripheral.setNotifyValue(true, for: characteristic)
}
}
}
// 3. Read value
func peripheral(_ peripheral: CBPeripheral,
didUpdateValueFor characteristic: CBCharacteristic,
error: Error?) {
guard let data = characteristic.value,
let value = String(data: data, encoding: .utf8)
else { return }
print("Characteristic value: \(value)")
}
}
예제에서 CBPeripheralDelegate는 세 가지 필수 발견 메서드를 구현합니다. didDiscoverServices는 발견된 모든 서비스를 반복하고 특성을 요청합니다. didDiscoverCharacteristicsForService는 각 특성의 속성을 확인합니다: .read의 경우 readValue를 호출하고, .notify의 경우 setNotifyValue(true)를 호출합니다. didUpdateValueForCharacteristic 메서드는 Data 형식으로 실제 값을 수신합니다.
값 읽기 CBCharacteristic의 값 읽기는 readValueForCharacteristic: 메서드를 사용하여 수행됩니다. 결과는 비동기적으로 peripheral:didUpdateValueForCharacteristic:error:에 도착합니다. 중요: 기기에 캐시된 값이 있을 수 있지만(발견 직후 characteristic.value 사용 가능), 현재 데이터를 얻으려면 readValue 호출이 필수입니다. iOS는 에너지 효율을 위해 값을 캐시할 수 있습니다 — readValue는 캐시를 새로 고칩니다.
값 쓰기는 writeValue:forCharacteristic:type: 메서드를 사용하여 수행됩니다. type 매개변수는 쓰기 유형을 결정합니다: .withResponse(CBCharacteristicWriteWithResponse) — 기기가 didWriteValueForCharacteristic을 통해 쓰기를 확인합니다. .withoutResponse(CBCharacteristicWriteWithoutResponse) — 확인 없는 쓰기, 최대 속도이지만 전달 보장은 없습니다. BLE 사양은 MTU(최대 전송 단위)를 제한합니다: BLE 4.0의 경우 최대 23바이트, BLE 5.0+의 경우 최대 251바이트. MTU보다 큰 데이터는 애플리케이션 수준에서 조각화가 필요합니다.
// CBPeripheral characteristic read and write
class BLEService {
private let peripheral: CBPeripheral
private let serviceUUID = CBUUID(string: "180D")
private let charUUID = CBUUID(string: "2A37")
init(peripheral: CBPeripheral) {
self.peripheral = peripheral
}
// Read with response
func readHeartRate() {
guard let service = peripheral.services?.first(where: { $0.uuid == serviceUUID }),
let characteristic = service.characteristics?.first(where: { $0.uuid == charUUID })
else { return }
peripheral.readValue(for: characteristic)
}
// Write with response (withResponse)
func writeWithResponse(data: Data) {
guard let characteristic = findCharacteristic() else { return }
peripheral.writeValue(data, for: characteristic,
type: .withResponse)
}
// Write without response (withoutResponse)
// Max throughput, no delivery guarantee
func writeWithoutResponse(data: Data) {
guard let characteristic = findCharacteristic() else { return }
peripheral.writeValue(data, for: characteristic,
type: .withoutResponse)
}
private func findCharacteristic() -> CBCharacteristic? {
return peripheral.services?
.flatMap { $0.characteristics ?? [] }
.first(where: { $0.uuid == charUUID })
}
}
쓰기 유형 withResponse 또는 withoutResponse의 선택은 신뢰성 요구 사항에 따라 다릅니다. 명령(불 켜기, 문 열기)에는 withResponse를 사용하십시오 — 전달 보장이 중요합니다. 스트리밍 데이터(맥박, 온도)에는 withoutResponse를 사용하십시오 — 하나의 패킷 손실은 중요하지 않습니다. BLE 기기는 하나의 쓰기 유형만 지원할 수 있습니다 — characteristic.properties.contains(.write) 및 .writeWithoutResponse 속성을 확인하십시오.
알림은 주변 기기가 중앙 기기로 특성 값을 비동기적으로 전송하는 BLE 메커니즘으로, 중앙 측의 지속적인 폴링이 없습니다. CBPeripheral은 setNotifyValue:forCharacteristic: 메서드를 통해 구독을 활성화합니다. 구독 활성화 후, iOS는 자동으로 주변 기기의 CCCD(클라이언트 특성 구성 디스크립터)에 쓰고, 값이 변경될 때마다 기기가 업데이트를 보내기 시작합니다.
표시(indication)와 달리, 알림은 중앙 기기의 확인이 필요하지 않습니다 — 패킷이 전송되고 잊혀집니다. 이는 최대 처리량을 제공하지만 패킷 손실 가능성이 있습니다. 표시는 프로토콜 수준(L2CAP)에서 확인이 필요합니다 — 더 안정적이지만 느립니다. CBCharacteristic의 properties 속성은 지원되는 모드(.notify, .indicate 또는 둘 다)를 정확히 나타냅니다.
CBPeripheral의 연결이 끊어지면(연결 해제, 범위 이탈), 모든 활성 구독이 자동으로 재설정됩니다. 재연결 시, 각 특성에 대해 다시 setNotifyValue:true를 호출해야 합니다. iOS는 애플리케이션이 포그라운드를 떠날 때도 구독을 잃습니다(백그라운드 모드가 활성화되지 않은 경우) — 백그라운드 작동을 위해 Info.plist에서 “Uses Bluetooth LE accessories” 기능을 활성화해야 합니다.
// CBPeripheral notification subscription management
class NotificationManager: NSObject {
private var peripheral: CBPeripheral?
private var subscribedCharacteristics: Set<CBUUID> = []
// Subscribe to notifications for all .notify characteristics
func subscribeToAllNotifications(peripheral: CBPeripheral) {
self.peripheral = peripheral
guard let services = peripheral.services else { return }
for service in services {
guard let characteristics = service.characteristics else { continue }
for characteristic in characteristics {
if characteristic.properties.contains(.notify)
|| characteristic.properties.contains(.indicate) {
peripheral.setNotifyValue(true, for: characteristic)
subscribedCharacteristics.insert(characteristic.uuid)
}
}
}
}
// Unsubscribe from all notifications
func unsubscribeFromAll() {
guard let peripheral = peripheral else { return }
guard let services = peripheral.services else { return }
for service in services {
guard let characteristics = service.characteristics else { continue }
for characteristic in characteristics {
if subscribedCharacteristics.contains(characteristic.uuid) {
peripheral.setNotifyValue(false, for: characteristic)
}
}
}
subscribedCharacteristics.removeAll()
}
// Notification handler
func peripheral(_ peripheral: CBPeripheral,
didUpdateNotificationStateFor characteristic: CBCharacteristic,
error: Error?) {
if characteristic.isNotifying {
print("Subscription active: \(characteristic.uuid)")
} else {
print("Subscription inactive: \(characteristic.uuid)")
}
}
}
NotificationManager는 CBPeripheral 알림의 올바른 처리를 보여줍니다. subscribeToAllNotifications는 모든 서비스와 특성을 반복하며 .notify와 .indicate를 활성화합니다. subscribedCharacteristics는 올바른 구독 취소를 위해 활성 구독을 추적합니다. didUpdateNotificationStateForCharacteristic은 characteristic.isNotifying 속성을 통해 성공적인 구독 상태 변경을 확인합니다.
완전한 워크플로우 CBPeripheral을 사용한 완전한 워크플로우에는 CBCentralManager에서 객체 가져오기, 연결, 발견, 읽기/쓰기, 알림 구독 및 연결 해제가 포함됩니다. 아래 예제는 최신 async/await API(iOS 15+)를 사용하여 Swift에서 완전한 BLE 주변기기 수명 주기를 관리하는 BLEConnection 클래스를 구현합니다.
import CoreBluetooth
// Full CBPeripheral management example with async/await
class BLEConnection: NSObject {
private let centralManager: CBCentralManager
private var peripheral: CBPeripheral?
private var continuation: CheckedContinuation<Void, Error>?
override init() {
centralManager = CBCentralManager(delegate: nil, queue: .main)
super.init()
centralManager.delegate = self
}
// 1. Connect to peripheral
func connect(to peripheral: CBPeripheral) async throws {
self.peripheral = peripheral
peripheral.delegate = self
centralManager.connect(peripheral, options: nil)
try await withCheckedThrowingContinuation { continuation in
self.continuation = continuation
}
}
// 2. Discovery
func discoverServices() async throws {
guard let peripheral = peripheral else {
throw BLEError.notConnected
}
peripheral.discoverServices(nil)
try await withCheckedThrowingContinuation { continuation in
self.continuation = continuation
}
}
}
// 3. CBCentralManager
extension BLEConnection: CBCentralManagerDelegate {
func centralManagerDidUpdateState(_ central: CBCentralManager) {
// Handle Bluetooth device state
}
func centralManager(_ central: CBCentralManager,
didConnect peripheral: CBPeripheral) {
continuation?.resume()
continuation = nil
}
func centralManager(_ central: CBCentralManager,
didFailToConnect peripheral: CBPeripheral,
error: Error?) {
continuation?.resume(throwing: error ?? BLEError.connectionFailed)
continuation = nil
}
}
enum BLEError: Error {
case notConnected
case connectionFailed
case serviceNotFound
case characteristicNotFound
}
BLEConnection 클래스는 CheckedContinuation을 통해 Swift Concurrency(async/await)를 사용합니다 — 이는 Core Bluetooth의 델리게이트 기반 API로 작업하기 위한 현대적인 패턴입니다. connect(to:)는 didConnectPeripheral을 통해 연결 확인을 기다리고, discoverServices()는 didDiscoverServices를 통해 기다립니다. 이 접근 방식은 중첩된 델리게이트를 제거하고 BLE 코드를 선형적이고 읽기 쉽게 만듭니다. BLEError를 통한 오류 처리는 모든 일반적인 BLE 연결 실패 시나리오를 다룹니다.
자주 묻는 질문
CBPeripheral은 이전에 연결된 기기의 경우 CBCentralManager의 retrievePeripheralsWithIdentifiers:를 통해 얻을 수 있습니다. 이전에 저장된 기기의 UUID(NSUUID) 배열을 전달하면 프레임워크가 시스템 BLE 본딩 데이터베이스의 기기에 대한 CBPeripheral 배열을 반환합니다. 이는 iPhone이 이전에 페어링한 기기에서만 작동합니다. 새 기기의 경우 스캔이 필요합니다.
일반적인 원인: 기기가 범위를 벗어남(RSSI가 임계값 미만), BLE 라디오가 꺼짐(CBCentralManager.state != .poweredOn), CBPeripheralDelegate가 설정되지 않음(peripheral.delegate = self), 또는 연결 전에 discoverServices가 호출됨. centralManager.state를 확인하고, connect 호출 전에 델리게이트가 설정되었는지 확인하며, 5–10초 타임아웃으로 재시도하십시오.
원인은 .writeWithoutResponse만 지원하는 특성에서 .withResponse를 사용하거나 그 반대입니다. 호출 전에 characteristic.properties를 확인하십시오. 또 다른 가능한 문제는 MTU입니다: 데이터가 20바이트(BLE 4.0 MTU)를 초과하는 경우, negotiateMTU 또는 조각화를 통한 MTU 협상이 필요합니다. 최대 패킷 크기를 결정하려면 peripheral.maximumWriteValueLength(for: .withResponse)를 사용하십시오.
범위 밖의 CBPeripheral은 즉시 연결이 끊어지지 않습니다 — iOS는 타임아웃(일반적으로 20–30초) 후 .disconnected 상태로 전환합니다. 모니터링을 위해 CBPeripheral에서 readRSSI를 사용하십시오 — 사용 불가능한 경우 CBError.connectionTimeout 코드로 오류를 반환합니다. 연결 손실을 적시에 감지하려면 centralManager:didDisconnectPeripheral:error:도 모니터링하십시오.
Core Bluetooth는 스레드 안전하지 않습니다 — 모든 CBPeripheral 호출은 동일한 큐(일반적으로 메인 큐 또는 CBCentralManager 초기화 시 지정된 직렬 큐)에서 이루어져야 합니다. 다른 스레드에서의 동시 호출은 경쟁 조건과 애플리케이션 충돌을 유발합니다. 모든 BLE 작업에는 DispatchQueue(label: “com.app.ble”)를, UI 업데이트에는 DispatchQueue.main.async를 사용하십시오.
요약
턴키 방식의 모바일 애플리케이션을 개발해 드립니다
IT Sectr는 2017년부터 스타트업과 기업을 위한 iOS 및 Android 애플리케이션을 만듭니다. 저희가 상담해 드리고 최적의 솔루션을 제안하겠습니다.