블루투스 기기와 통신하는 iOS 앱을 만들려면 CoreBluetooth를 써야 해요.
그런데 막상 문서를 열어보면 Service, Characteristic, GATT... 생소한 용어들이 쏟아지죠.
이 글에서는 CoreBluetooth를 제대로 이해하기 위한 기초 지식을 다룰 거예요.
BLE가 왜 탄생했고, 어떻게 동작하는지 알고 나면 CoreBluetooth를 다루는데 쉬울거라고 생각합니다:)
Classic Bluetooth: 어떤 한계가 있었나?

초기 블루투스는 "케이블을 없애자"라는 목표로 탄생했습니다. 지속적인 연결과 높은 데이터 전송률에 최적화됐죠.
하지만 시대가 변했습니다. 에어팟, 혈당 측정기, 스마트워치 같은 센서 기기들이 폭발적으로 늘어났어요. 이런 기기들은 한 번 충전으로 며칠, 심지어 몇 달을 버텨야 했습니다.
문제는 기존 블루투스로는 이게 불가능했다는 겁니다. 왜일까요?
클래식 블루투스는 "연결을 계속 유지"하는 방식이었습니다.
마치 전화 통화처럼요. 통화 중에는 양쪽 다 계속 깨어 있어야 합니다. 이게 배터리를 잡아먹는 주범이었죠.
BLE 등장 🎬

2010년 Bluetooth 4.0과 함께 "BLE"가 등장했습니다
연결을 최소화하고, 필요할 때만 데이터를 빠르게 주고받고, 다시 잠들자!
이를 위해 BLE는 클래식 블루투스와 완전히 다른 프로토콜을 새로 만들었습니다. 같은 2.4GHz 주파수를 쓸 뿐, 내부 구조는 별개예요.
클래식 블루투스가 "전화 통화"라면, BLE는 "카카오톡 메시지"입니다.
Central과 Peripheral
BLE에서는 두 가지 역할이 있어요:
- Central (중앙 장치): 데이터를 요청하고 연결을 주도하는 쪽. 주로 스마트폰이 이 역할
- Peripheral (주변 장치): 데이터를 제공하고 광고하는 쪽. 센서 기기, 웨어러블 등이 이 역할
iOS 앱 개발에서는 대부분 Central 역할을 구현하게 됩니다 아이폰이 심박계를 읽거나, 스마트 전구를 제어하는 식으로.
CoreBluetooth에서는 이렇게 매핑되죠:
- Central → CBCentralManager
- Peripheral → CBPeripheral
간단하게 이 개념을 알고 뒷 내용을 읽으면 더 이해가 잘가요!
GATT(Generic Attribute Profile)
자, 이제 Central(아이폰)과 Peripheral(센서 기기)이 연결됐다고 칠게요. 그러면 어떻게 데이터를 주고 받을까요?
여기서 GATT가 등장하죠
쉽게 말해 BLE 기기들이 서로 "어떤 종류의 정보를 가지고 있고, 어떻게 주고받을까?"를 약속한 공통 규칙입니다
왜 이런 규칙이 필요했을까요?
라떼는 삼성 폰은 마이크로 USB, 아이폰은 라이트닝, LG는 또 다른 규격... 여행 갈 때 충전기를 여러 개 들고 다녀야 했습니다.
친구 폰 충전기를 빌릴 수도 없었죠.
USB-C가 표준이 되면서 상황이 바뀌었습니다. 이제는 어떤 브랜드 폰이든 같은 충전기로 충전할 수 있어요.
"모든 기기는 이 규격을 따라라"라는 약속 덕분입니다.(사실 제 아이폰은 14라....아직 C타입 안되긴 해요..ㅠ)
BLE도 마찬가지였습니다. 애플, 삼성, 샤오미, 각종 바이오 회사들이 자기만의 BLE 기기를 쏟아냈어요.
만약 각 회사가 "우리는 데이터를 이렇게 보낼게요"라고 제각각 정하면 어떻게 될까요?
앱 개발자는 기기마다 연동 코드를 새로 짜야 합니다. 삼성 혈당계용 코드, 애플 워치용 코드, 샤오미 밴드용 코드... 지옥이죠.
GATT는 이 문제를 해결합니다.
"모든 BLE 기기는 이 구조로 데이터를 정리해라!"
덕분에📱입장에서는 이렇게 됩니다:
"누가 만들었든, 어떤 기기든 상관없어. 나는 GATT 규칙대로만 읽을게."
GATT의 계층 구조
GATT는 데이터를 계층적으로 정리합니다:

Peripheral (BLE 기기)
└── Service (기능 단위)
└── Characteristic (실제 데이터)
└── Descriptor (메타 정보)
동네 편의점을 떠올려보세요:
- Peripheral = 편의점 건물 자체
- Service = 매대 구역 (음료 코너, 과자 코너, 도시락 코너)
- Characteristic = 각 구역의 특정 상품 (콜라, 새우깡, 도시락)
- Descriptor = 상품에 붙은 정보 (가격표, 유통기한, 칼로리)
편의점에서 제로 콜라를 사려면 어떻게 하나요?
"음료 코너 → 탄산음료 칸 → 콜라" 순서로 찾아갑니다. 굳이 전체 매장을 다 뒤질 필요 없죠.
GATT도 똑같습니다. 심박수를 읽고 싶다면?
"Heart Rate Service → Heart Rate Measurement Characteristic"으로 바로 접근합니다.
추측인 애플워치 예시
애플 워치 (Peripheral)
├── Heart Rate Service
│ ├── Heart Rate Measurement → 현재 심박수: 72 BPM
│ └── Body Sensor Location → 센서 위치: 손목
│
├── Battery Service
│ └── Battery Level → 배터리: 85%
│
└── Device Information Service
├── Manufacturer Name → "Apple"
└── Model Number → "Sunho-0803"
하나의 기기가 여러 Service를 가질 수 있습니다. 애플 워치 하나로 심박수도 알려주고, 배터리 정보도 제공하고, 기기 정보도 담고 있죠.
그리고 각 Service와 Characteristic에는 고유한 UUID가 붙어 있어요
왜 UUID를 붙여놨을까?
1. 재사용성을 위한 표준화
새 스마트워치를 만든다고 생각해볼게요:(
심박수 기능이 필요하면? Heart Rate Service를 처음부터 설계할 필요 없습니다.
이미 표준으로 정의된 걸 가져다 쓰면 돼요.
배터리 표시? 마찬가지로 Battery Service 표준을 쓰면 됩니다.
이렇게 표준화된 Service들을 전부 조립하다 보니, 자연스럽게 전 세계 공통 UUID가 생겼습니다:
- Heart Rate Service: 0x180D
- Battery Service: 0x180F
- Heart Rate Measurement: 0x2A37
제조사가 삼성이든 애플이든 샤오미든, 심박수 서비스는 무조건 0x180D입니다.
2. 표준 UUID로 빠른 접근이 가능해짐
아이폰은 이 표준 UUID들을 이미 알고 있어요.
그래서 연결 후 Service Discovery 과정에서 "무엇을 찾아야 할지" 명확히 알 수 있어지죠
3. 빠른 접근이 전력 효율을 좋게 만듬
탐색 시간이 짧아지면 뭐가 좋을까? 기기가 깨어 있는 시간이 줄어들죠
- 연결한다 (수십 밀리초)
- "0x180D 서비스 가지고 있어?" (바로 요청)
- "응, 그 안에 0x2A37 있어" (바로 응답)
- "그럼 0x2A37 값 줘!" (값 읽기)
- 연결 끊는다
- 다시 잠든다 💤
불필요한 탐색 시간이 없으니까 전체 과정이 순식간에 끝납니다.
결국 GATT의 계층 구조는 처음부터 "전력을 아끼자"라는 하나의 목표를 향해 설계된 겁니다.
재사용 가능한 블록으로 만들고 → 표준 주소를 붙이고 → 빠르게 찾아가게 해서 → 연결 시간을 최소화한다.
Service Discovery: 실제 탐색 과정
위에서 "연결하자마자 바로 요청한다"고 했지만, 실제 동작은 한 단계가 더 필요해요
Central(아이폰)은 Peripheral에 연결된 후, Service Discovery라는 탐색 과정을 거쳐야 해요
- Central: "네가 가진 모든 Service의 UUID 목록을 줘." (또는 "혹시 0x180D Service 가지고 있어?")
- Peripheral: "응, 나 0x180D Service랑 0x180F Service 있어."
- Central: "좋아, 그럼 0x180D Service 안에 있는 모든 Characteristic의 UUID 목록을 줘."
- Peripheral: "응, 그 안에는 0x2A37이랑 0x2A38 Characteristic이 있어."
- Central: (이제야 0x2A37의 실제 위치를 알게 됨) "오케이, 그럼 이제 그 0x2A37 Characteristic의 값을 읽을게!"
표준 UUID를 안다는 것은 "지름길"이라기보다는, 탐색 과정에서 "무엇을 찾아야 할지" 명확히 아는 것이죠
편의점 비유로 돌아가면, '콜라'를 사러 갈 때 '음료 코너'라는 표준을 아니까 다른 코너를 탐색할 필요 없이 바로 '음료 코너'로 직진하는데
그래도 일단 '음료 코너'로 걸어가서 '콜라'를 집는(탐색 및 발견) 행위는 필요하잖아요?
이 탐색 과정이 있기 때문에 연결 직후에 약간의 딜레이(수십~수백 ms)가 발생하게 돼죠..
그리고 iOS 개발 시 didDiscoverServices나 didDiscoverCharacteristics 같은 델리게이트 메서드가 호출되는 이유가 바로 이것!
Characteristic 동작: Read, Write, Notify, Indicate
Characteristic은 그저 값을 담고 있는 게 아닙니다. "이 값을 어떻게 다룰 수 있는지"도 정의되어 있어요.
Central(아이폰)이 Peripheral(워치)의 데이터를 어떻게 다루는지 살펴볼게요.
- Read (읽기)
- Central이 요청하면 Peripheral이 현재 값을 알려줍니다.
- 예시: 스마트폰 앱이 워치에게 "지금 심박수 몇임?"라고 물으면, 워치가 "72요"라고 대답하는거죠
- Write (쓰기)
- Central이 Peripheral의 값을 변경합니다.
- 예시: 스마트폰 앱으로 스마트 전구에 "빨간색으로 바꿔"라고 명령하면, 전구가 빨간색으로 변하는 거 있죠??
- Notify (알림)
- Central이 "앞으로 심박수 바뀔 때마다 나한테 알려줘"라고 구독(Subscribe)을 신청해둡니다.
그러면 Peripheral은 값이 바뀔 때만 알아서 Central에게 데이터를 "쏴줍니다".
-> Central은 가만히 기다리면 됩니다.
이때 Peripheral는 Central이 데이터를 잘 받았는지 확인하지 않는다. "보내고 땡" 🛎️
- Central이 "앞으로 심박수 바뀔 때마다 나한테 알려줘"라고 구독(Subscribe)을 신청해둡니다.
- Indicate (확인 알림)
- Notify와 비슷하지만, Central로부터 "데이터 잘 받았다!"라는 확인(ACK) 신호를 받아야만 다음 데이터를 보내요
- 절대 값이 유실되면 안될때! 속도 < 신뢰성 일때 사용하면 좋겟네요
- 왜 Notify/Indicate가 중요할까?
- Central이 주기적으로 "지금 심박수 뭐야?"라고 계속 물어보면(Polling 방식), 둘 다 계속 깨어 있어야 합니다.
하지만 Notify는 변화가 있을 때만 깨어나 필요한 통신을 하고, 나머지 시간은 둘 다 깊이 잠들어 있을 수 있죠!!! - 이게 BLE의 궁극적인 저전력 비결입니다.
- Central이 주기적으로 "지금 심박수 뭐야?"라고 계속 물어보면(Polling 방식), 둘 다 계속 깨어 있어야 합니다.
Advertising: 연결 없이 데이터 전달
GATT는 Central과 Peripheral이 '연결된 후'의 데이터 교환 방식을 담당합니다.
그런데 BLE는 아예 연결하기 전에도 데이터를 전달할 수 있어요!
이게 바로 Advertising입니다.
어떻게 작동하나요?
BLE 기기는 마치 라디오 방송처럼, 주기적으로 "나 여기 있어! 나 심박계야!" 같은 신호를 주변에 뿌립니다.
- Bluetooth 4.x: 최대 31바이트
- Bluetooth 5.0+: Extended Advertising으로 최대 255바이트 (심지어 체이닝으로 1650바이트까지!)
주변의 Central(아이폰)은 이 신호를 감지하고 데이터를 받아볼 수 있습니다.
왜 필요할까요?
기기와 기기가 직접 '연결'하는 것은 생각보다 비용이 큽니다:
- 시간: 연결을 수립하는 데만 수십~수백 밀리초가 소요됩니다.
- 전력: 연결을 유지하는 것 자체가 배터리를 소모합니다.
- 복잡도: 연결 끊김 처리, 재연결 등 관리할 것이 많아집니다.
그래서 아주 간단한 정보는 굳이 복잡한 연결 과정 없이 Advertising만으로 전달하는 것이 훨씬 효율적입니다.
실제 예시: Apple AirTag와 Find My 네트워크
- AirTag는 주기적으로 BLE Advertising 신호를 뿌립니다
- 근처를 지나가는 모든 Apple 기기가 이 신호를 감지하죠
- 각 기기는 AirTag의 식별자와 자신의 위치 정보를 Apple 서버로 전송합니다
- AirTag 소유자는 "나의 찾기" 앱에서 위치를 확인할 수 있죠
연결 없이, 수억 대의 Apple 기기를 활용해서, 전 세계 어디서든 분실물을 찾을 수 있어지죠!
Bluetooth의 발전 과정
Bluetooth는 계속 발전해왔지만, iOS 개발자로서 특히 주목할 만한 핵심 업데이트 몇 가지만 짚어볼게요.
- Bluetooth 5.0 (2016년): Advertising 강화 가장 큰 변화는 Extended Advertising입니다.
Advertising 섹션에서 배운 31바이트의 제한이 최대 255바이트로 늘어났습니다.
덕분에 연결 없이도 훨씬 더 많은 데이터를 브로드캐스트할 수 있게 되었죠. - Bluetooth 5.1 (2019년): 위치 추적 혁신 방향 탐지(Direction Finding) 기능이 도입됐습니다.
기존에는 "가깝다/멀다" 정도만 알 수 있었다면, 5.1부터는 "어느 방향에 있는지"까지 알 수 있게 됐어요.
AirTag의 '정확한 찾기'나 실내 내비게이션 앱 개발의 기반이 되는 기술입니다 - Bluetooth 5.2 (2020년): 오디오 혁명 LE Audio가 도입되어, 드디어 BLE만으로도 고품질 오디오를 스트리밍할 수 있게 됐습니다. Auracast™(하나의 장치에서 여러 헤드폰으로 동시 전송) 같은 기능이 가능해졌죠.
오디오 관련 앱을 만든다면 알아둘 만한 큰 변화입니다. - 이후 5.3, 6.0 등 최신 버전들은 주로 전력 효율 극대화, 대규모 IoT 네트워크 관리, 정밀 거리 측정 등을 다룹니다.
BLE는 저전력을 위해 설계됐고, 그래서 모든 구조(GATT, Service Discovery, Notify, Advertising)가 "최소한의 연결, 최소한의 통신"을 지향하죠
이 원리를 이해하면, CoreBluetooth의 비동기적인 delegate 패턴이 왜 그렇게 설계되었는지 자연스럽게 이해가 될 거예요.
다음 글에서는 실제 CoreBluetooth 코드를 작성해볼게요!!
'SWIFT개발일지' 카테고리의 다른 글
| ProtoBuf - 이게 뭔데 사람들은 환호성을 지를까? (1) | 2025.11.29 |
|---|---|
| 아 지겹다 복붙! Xcode 커스텀 템플릿 만들기 (1) | 2025.11.25 |
| 이미지 URL 저장 시 마주하는 함정 문제들 (0) | 2025.09.11 |
| Metal3편 - 메모리 사용량 급증 버그 수정 (0) | 2025.04.20 |
| 이미지 최적화 3탄(kingFisher를 삭제하고 Custom) (0) | 2025.04.16 |