본 콘텐츠의 이미지 및 내용은 AI로 생성되었습니다.
본 콘텐츠의 이미지 및 내용을 무단으로 복제, 배포, 수정하여 사용할 경우 저작권법에 의해 법적 제재를 받을 수 있습니다.
AI Generated
2026. 1. 31. · 12 Views
macOS iOS Android 앱 통합 완벽 가이드
하나의 데스크톱 앱을 macOS, iOS, Android 모바일 앱과 연동하는 방법을 배웁니다. WebSocket 프로토콜 설계부터 Bonjour 자동 페어링까지, 크로스 플랫폼 앱 통합의 모든 것을 다룹니다.
목차
1. 네이티브 앱의 필요성
김개발 씨는 회사에서 macOS용 데스크톱 앱을 개발하고 있었습니다. 어느 날 기획팀에서 요청이 들어왔습니다.
"이 앱을 스마트폰에서도 제어할 수 있으면 좋겠어요. 웹으로 만들면 안 되나요?"
네이티브 앱이란 특정 운영체제에 최적화된 프로그래밍 언어로 개발된 앱을 말합니다. 마치 모국어로 대화하는 것처럼, 네이티브 앱은 해당 OS와 가장 자연스럽게 소통합니다.
웹 앱으로는 구현하기 어려운 시스템 깊은 곳의 기능에 접근할 수 있다는 것이 가장 큰 장점입니다.
다음 코드를 살펴봅시다.
// 웹 앱의 한계 - 시스템 레벨 접근 불가
// JavaScript로는 이런 기능을 구현할 수 없습니다
navigator.bluetooth.requestDevice() // 제한적
window.localStorage // 샌드박스 내에서만
// 네이티브 앱의 강점 - Swift (macOS/iOS)
import CoreBluetooth
import Network
class NativeAppManager {
// 시스템 블루투스에 직접 접근
let centralManager = CBCentralManager()
// 로컬 네트워크 디바이스 검색
let browser = NWBrowser(for: .bonjour(
type: "_myapp._tcp",
domain: nil
), using: .tcp)
}
김개발 씨는 입사 2년 차 개발자입니다. macOS용 생산성 앱을 혼자서 꽤 잘 만들어왔습니다.
그런데 오늘 기획팀 회의에서 예상치 못한 요청을 받았습니다. "김개발 씨, 이 앱 정말 좋은데요.
스마트폰에서도 쓸 수 있으면 좋겠어요. 요즘 다들 폰으로 일하잖아요.
웹으로 만들면 금방 되지 않나요?" 김개발 씨는 잠시 고민에 빠졌습니다. 웹으로 만들면 분명 빠르긴 할 것입니다.
하지만 지금 앱이 가진 핵심 기능들, 예를 들어 블루투스 기기 연결이나 로컬 네트워크 디바이스 검색 같은 기능은 웹으로는 구현이 어렵습니다. 그렇다면 네이티브 앱이란 정확히 무엇일까요?
쉽게 비유하자면, 네이티브 앱은 마치 그 나라 언어를 모국어로 쓰는 사람과 같습니다. 한국에서 한국어로 대화하면 뉘앙스까지 정확히 전달되듯이, iOS에서 Swift로 개발하면 iOS의 모든 기능을 온전히 활용할 수 있습니다.
반면 웹 앱은 통역사를 거쳐 대화하는 것과 비슷합니다. 기본적인 소통은 가능하지만, 섬세한 부분에서는 한계가 있습니다.
웹 앱의 한계는 분명합니다. 첫째, 시스템 API 접근이 제한됩니다.
블루투스, NFC, 로컬 네트워크 검색 같은 기능은 브라우저의 샌드박스를 벗어나야 합니다. 둘째, 백그라운드 실행이 어렵습니다.
브라우저 탭을 닫으면 모든 것이 멈춥니다. 셋째, 성능 최적화에 한계가 있습니다.
그래픽 집약적인 작업이나 실시간 처리에서 네이티브 앱과 비교할 수 없습니다. 반면 네이티브 앱은 이 모든 것이 가능합니다.
macOS의 Swift, iOS의 Swift, Android의 Kotlin은 각 운영체제의 모든 문을 열 수 있는 열쇠입니다. 시스템 설정을 변경하고, 다른 앱과 통신하며, 백그라운드에서 조용히 작업을 수행할 수 있습니다.
위의 코드를 살펴보겠습니다. JavaScript에서는 navigator.bluetooth.requestDevice()로 블루투스에 접근할 수 있지만, 사용자 동의가 필수이고 접근할 수 있는 디바이스도 제한적입니다.
하지만 Swift의 CBCentralManager는 시스템 블루투스에 직접 접근하여 모든 디바이스를 검색하고 연결할 수 있습니다. 실제 현업에서 이런 차이가 왜 중요할까요?
예를 들어 스마트홈 앱을 개발한다고 가정해봅시다. 집 안의 IoT 기기를 자동으로 검색하고 페어링하는 기능이 필요합니다.
웹 앱으로는 사용자가 매번 수동으로 기기를 연결해야 하지만, 네이티브 앱은 Bonjour나 mDNS 같은 프로토콜을 활용해 자동으로 기기를 발견할 수 있습니다. 물론 네이티브 앱에도 단점은 있습니다.
각 플랫폼별로 별도의 코드를 작성해야 하므로 개발 비용이 증가합니다. 하지만 오늘 우리가 배울 내용은 바로 이 문제를 해결하는 방법입니다.
공통된 프로토콜을 정의하고, 각 플랫폼의 강점을 살리면서도 일관된 사용자 경험을 제공하는 것입니다. 다시 김개발 씨의 이야기로 돌아가 봅시다.
기획팀의 요청을 받은 김개발 씨는 결심했습니다. "네이티브 앱으로 가겠습니다.
시간은 좀 더 걸리겠지만, 사용자 경험이 완전히 다를 거예요."
실전 팁
💡 - 시스템 깊은 통합이 필요하면 네이티브, 정보 표시 위주라면 웹을 고려하세요
- Flutter나 React Native 같은 크로스 플랫폼 도구도 좋은 선택지입니다
2. WebSocket 프로토콜 정의
이제 모바일 앱을 만들기로 결정했습니다. 그런데 문제가 있습니다.
macOS 앱과 iOS 앱, Android 앱이 어떻게 서로 대화할 수 있을까요? 김개발 씨는 선배 박시니어 씨에게 조언을 구했습니다.
"앱끼리 통신하려면 어떤 방법이 좋을까요?"
WebSocket은 클라이언트와 서버 사이에 지속적인 양방향 통신 채널을 제공하는 프로토콜입니다. 마치 전화 통화처럼, 한번 연결되면 양쪽에서 언제든 메시지를 주고받을 수 있습니다.
HTTP의 요청-응답 방식과 달리 실시간 통신에 최적화되어 있습니다.
다음 코드를 살펴봅시다.
// 앱 간 통신을 위한 WebSocket 메시지 프로토콜 정의
// JSON 기반으로 모든 플랫폼에서 파싱 가능
// 메시지 타입 정의
enum MessageType: String, Codable {
case command // 명령 전송 (재생, 정지 등)
case status // 상태 동기화
case handshake // 초기 연결 확인
case heartbeat // 연결 유지 확인
}
// 공통 메시지 구조
struct AppMessage: Codable {
let type: MessageType
let payload: [String: AnyCodable]
let timestamp: Date
let deviceId: String
}
// 실제 메시지 예시
let playCommand = AppMessage(
type: .command,
payload: ["action": "play", "trackId": "12345"],
timestamp: Date(),
deviceId: "iphone-abc123"
)
박시니어 씨는 화이트보드 앞에 섰습니다. "김개발 씨, 앱끼리 통신하는 방법은 여러 가지가 있어요.
하나씩 살펴볼까요?" 첫 번째 방법은 REST API입니다. 익숙하고 구현하기 쉽습니다.
하지만 문제가 있습니다. REST는 클라이언트가 먼저 요청해야만 서버가 응답할 수 있습니다.
macOS 앱에서 상태가 변경되었을 때, 모바일 앱이 그걸 알려면 계속 "뭐 바뀐 거 없어요?" 하고 물어봐야 합니다. 이것을 폴링이라고 하는데, 배터리도 많이 소모하고 비효율적입니다.
두 번째 방법이 바로 WebSocket입니다. WebSocket은 마치 전화 통화와 같습니다.
일반 HTTP가 편지를 주고받는 것이라면, WebSocket은 전화선을 연결해두고 필요할 때마다 바로 대화하는 것입니다. 한번 연결되면 양쪽 모두 언제든지 메시지를 보낼 수 있습니다.
그렇다면 앱끼리 어떤 형식으로 메시지를 주고받아야 할까요? 여기서 프로토콜 설계가 중요해집니다.
프로토콜이란 쉽게 말해 "우리 이런 규칙으로 대화하자"라고 약속하는 것입니다. 한국어를 쓸지 영어를 쓸지, 존댓말을 쓸지 반말을 쓸지 미리 정해두는 것과 같습니다.
우리 프로토콜의 핵심 구성요소를 살펴보겠습니다. 첫째, 메시지 타입입니다.
command는 "재생해", "멈춰" 같은 명령입니다. status는 "지금 재생 중이야", "일시정지 상태야" 같은 상태 정보입니다.
handshake는 처음 연결할 때 "안녕, 나 iPhone이야"라고 인사하는 것입니다. heartbeat는 "나 아직 살아있어"라고 주기적으로 확인하는 것입니다.
둘째, 페이로드입니다. 실제 전달하고자 하는 데이터가 담깁니다.
JSON 형식을 사용하면 Swift에서 만든 메시지를 Kotlin에서도 똑같이 파싱할 수 있습니다. 플랫폼 독립적이라는 것이 JSON의 큰 장점입니다.
셋째, 타임스탬프와 디바이스 ID입니다. 언제 보낸 메시지인지, 누가 보낸 메시지인지 명확히 해야 합니다.
여러 기기가 동시에 연결되어 있을 때 특히 중요합니다. 위 코드에서 AppMessage 구조체를 보면, 모든 메시지가 동일한 형태를 갖습니다.
이렇게 일관된 구조를 유지하면 각 플랫폼에서 메시지를 처리하는 로직도 단순해집니다. 실무에서 자주 하는 실수가 있습니다.
프로토콜을 너무 복잡하게 설계하는 것입니다. 처음에는 단순하게 시작하세요.
나중에 필요한 기능이 생기면 그때 확장하면 됩니다. 또한 버전 관리를 빼먹는 경우도 많습니다.
프로토콜이 변경되면 구버전 앱과 호환성 문제가 생길 수 있으므로, 메시지에 version 필드를 추가해두는 것이 좋습니다. 김개발 씨는 고개를 끄덕였습니다.
"결국 WebSocket으로 연결하고, JSON으로 메시지를 주고받으면 되는 거군요!" 박시니어 씨가 웃으며 말했습니다. "그렇지.
이제 실제 코드를 작성해볼까?"
실전 팁
💡 - 메시지 타입은 enum으로 정의하여 타입 안전성을 확보하세요
- 프로토콜 버전 필드를 추가해 향후 호환성 문제에 대비하세요
- heartbeat 주기는 30초 정도가 적당합니다
3. macOS Swift 코드 분석
이제 실제 코드를 살펴볼 차례입니다. macOS 앱이 WebSocket 서버 역할을 하고, 모바일 앱들이 클라이언트로 접속하는 구조입니다.
김개발 씨는 apps/macos 폴더를 열고 Swift 코드를 분석하기 시작했습니다.
macOS 앱은 Network 프레임워크를 사용하여 WebSocket 서버를 구현합니다. NWListener로 연결을 대기하고, NWConnection으로 각 클라이언트와 통신합니다.
Apple의 네이티브 프레임워크를 사용하면 별도의 라이브러리 없이도 안정적인 네트워크 통신이 가능합니다.
다음 코드를 살펴봅시다.
import Network
class WebSocketServer {
private var listener: NWListener?
private var connections: [UUID: NWConnection] = [:]
func start(port: UInt16) throws {
// WebSocket 프로토콜 설정
let parameters = NWParameters.tcp
let wsOptions = NWProtocolWebSocket.Options()
parameters.defaultProtocolStack
.applicationProtocols
.insert(wsOptions, at: 0)
// 리스너 생성 및 시작
listener = try NWListener(using: parameters,
on: NWEndpoint.Port(rawValue: port)!)
listener?.newConnectionHandler = { [weak self] connection in
self?.handleNewConnection(connection)
}
listener?.start(queue: .main)
print("WebSocket 서버 시작: 포트 \(port)")
}
private func handleNewConnection(_ connection: NWConnection) {
let id = UUID()
connections[id] = connection
connection.start(queue: .main)
receiveMessage(from: connection, id: id)
}
}
김개발 씨는 apps/macos 폴더 안의 WebSocketServer.swift 파일을 열었습니다. 처음 보는 코드였지만, 하나씩 뜯어보기로 했습니다.
먼저 import Network 부분입니다. 이것은 Apple이 iOS 12, macOS 10.14부터 제공하는 Network 프레임워크입니다.
예전에는 소켓 프로그래밍을 하려면 BSD 소켓이나 CFSocket 같은 저수준 API를 사용해야 했습니다. 코드도 복잡하고 에러 처리도 까다로웠습니다.
Network 프레임워크는 이런 복잡함을 감싸서 현대적인 Swift API로 제공합니다. NWListener가 핵심입니다.
이것은 마치 식당 입구에 서 있는 안내 직원과 같습니다. 손님이 오면 "어서오세요"라고 맞이하고, 자리로 안내해주는 역할입니다.
NWListener는 특정 포트에서 연결 요청을 기다리다가, 새로운 클라이언트가 접속하면 newConnectionHandler를 호출합니다. WebSocket 프로토콜 설정 부분을 보겠습니다.
NWProtocolWebSocket.Options()를 생성하여 TCP 파라미터에 추가합니다. 이 한 줄로 일반 TCP 연결이 WebSocket 연결로 업그레이드됩니다.
HTTP 핸드셰이크나 프레임 파싱 같은 복잡한 작업을 프레임워크가 알아서 처리해줍니다. connections 딕셔너리가 왜 필요할까요?
여러 모바일 기기가 동시에 접속할 수 있습니다. iPhone도 연결하고, iPad도 연결하고, Android 폰도 연결할 수 있습니다.
각 연결을 UUID로 구분하여 관리하면, 특정 기기에만 메시지를 보내거나, 연결이 끊어졌을 때 해당 기기만 정리할 수 있습니다. handleNewConnection 메서드를 자세히 보겠습니다.
새 연결이 들어오면 UUID를 생성하여 고유 식별자로 사용합니다. connection.start(queue: .main)으로 연결을 활성화하고, receiveMessage를 호출하여 메시지 수신 대기 상태로 들어갑니다.
이것이 바로 양방향 통신의 시작점입니다. 한 가지 주의할 점이 있습니다.
[weak self]를 사용하는 것을 보셨나요? 클로저 안에서 self를 강하게 참조하면 메모리 누수가 발생할 수 있습니다.
특히 네트워크 코드에서는 연결이 유지되는 동안 클로저도 계속 살아있으므로, 반드시 약한 참조를 사용해야 합니다. 실제 운영 환경에서는 추가적인 고려사항이 있습니다.
에러 처리, 재연결 로직, 타임아웃 설정 등이 필요합니다. 하지만 기본 구조는 위 코드와 동일합니다.
복잡해 보이는 네트워크 서버도 결국 리스너 생성, 연결 수락, 메시지 송수신이라는 세 단계로 이루어집니다. 김개발 씨는 코드를 읽으며 감탄했습니다.
"생각보다 깔끔하네요. Apple이 프레임워크를 잘 만들어놨구나."
실전 팁
💡 - Network 프레임워크는 iOS 12+ / macOS 10.14+ 에서 사용 가능합니다
- 연결당 UUID를 부여하여 멀티 클라이언트 환경을 관리하세요
- 반드시 weak self를 사용하여 메모리 누수를 방지하세요
4. iOS와 Android 구조
macOS 서버 코드를 이해한 김개발 씨는 이제 클라이언트 쪽을 살펴보기로 했습니다. apps/ios와 apps/android 폴더에는 각각 Swift와 Kotlin으로 작성된 모바일 앱 코드가 있습니다.
두 플랫폼이 같은 프로토콜을 어떻게 구현하는지 비교해보겠습니다.
iOS는 URLSessionWebSocketTask를, Android는 OkHttp의 WebSocket을 사용하여 클라이언트를 구현합니다. 언어와 API는 다르지만, 연결-송신-수신-종료라는 기본 흐름은 동일합니다.
각 플랫폼의 관용적인 패턴을 따르면서도 일관된 프로토콜을 유지하는 것이 핵심입니다.
다음 코드를 살펴봅시다.
// iOS 클라이언트 (Swift)
class WebSocketClient {
private var webSocket: URLSessionWebSocketTask?
func connect(to url: URL) {
let session = URLSession(configuration: .default)
webSocket = session.webSocketTask(with: url)
webSocket?.resume()
receiveMessage()
}
func send(_ message: AppMessage) {
let data = try? JSONEncoder().encode(message)
let string = String(data: data!, encoding: .utf8)!
webSocket?.send(.string(string)) { error in
if let error = error {
print("전송 실패: \(error)")
}
}
}
}
// Android 클라이언트 (Kotlin)
class WebSocketClient(private val serverUrl: String) {
private val client = OkHttpClient()
private var webSocket: WebSocket? = null
fun connect() {
val request = Request.Builder().url(serverUrl).build()
webSocket = client.newWebSocket(request, object : WebSocketListener() {
override fun onMessage(webSocket: WebSocket, text: String) {
val message = Gson().fromJson(text, AppMessage::class.java)
handleMessage(message)
}
})
}
}
박시니어 씨가 두 개의 모니터를 나란히 놓고 말했습니다. "왼쪽이 iOS 코드, 오른쪽이 Android 코드야.
같이 비교해보자." 먼저 iOS 클라이언트부터 살펴보겠습니다. iOS 13부터 URLSession에 WebSocket 지원이 추가되었습니다.
별도의 라이브러리 없이 네이티브 API만으로 WebSocket 통신이 가능해진 것입니다. URLSessionWebSocketTask는 마치 택배 기사와 같습니다.
연결을 맺고, 패키지를 보내고, 패키지를 받는 일을 담당합니다. connect 메서드를 보면, URLSession에서 webSocketTask를 생성하고 resume()으로 연결을 시작합니다.
resume()을 호출하기 전까지는 아무 일도 일어나지 않는다는 점에 주의하세요. 마치 전화기를 들고 번호를 누른 후 통화 버튼을 눌러야 연결되는 것과 같습니다.
send 메서드에서는 AppMessage를 JSON 문자열로 변환하여 전송합니다. 앞서 정의한 프로토콜이 여기서 사용됩니다.
JSONEncoder는 Swift 구조체를 JSON으로 자동 변환해주는 편리한 도구입니다. 이제 Android 클라이언트를 보겠습니다.
Android에서는 OkHttp 라이브러리를 사용합니다. Square에서 만든 이 라이브러리는 Android 개발의 사실상 표준입니다.
WebSocket 지원도 훌륭하고, 사용법도 직관적입니다. Kotlin의 object 표현식이 눈에 띕니다.
Java의 익명 클래스와 비슷한데, 더 간결합니다. WebSocketListener를 상속받아 onMessage, onFailure 같은 콜백을 구현합니다.
메시지가 도착하면 Gson으로 JSON을 파싱하여 AppMessage 객체로 변환합니다. 두 코드를 비교하면 흥미로운 점이 보입니다.
문법은 다르지만 구조는 같습니다. 연결 생성, 메시지 송신, 메시지 수신이라는 세 가지 핵심 기능이 동일한 패턴으로 구현되어 있습니다.
이것이 바로 프로토콜을 먼저 정의하는 것의 장점입니다. 각 플랫폼에서 어떻게 구현하든, 결국 같은 JSON 메시지를 주고받으면 됩니다.
앱 구조 측면에서도 살펴보겠습니다. apps/ios 폴더에는 Swift 프로젝트가, apps/android 폴더에는 Kotlin 프로젝트가 있습니다.
각각 Xcode와 Android Studio에서 열어 빌드할 수 있습니다. 공통된 비즈니스 로직은 가능한 한 비슷하게 유지하되, UI는 각 플랫폼의 가이드라인을 따릅니다.
iOS는 SwiftUI, Android는 Jetpack Compose를 사용하는 것이 요즘 트렌드입니다. 김개발 씨가 질문했습니다.
"두 플랫폼 코드를 동시에 관리하려면 어떻게 해야 하나요?" 박시니어 씨가 답했습니다. "프로토콜 변경이 있을 때마다 양쪽 모두 업데이트해야 해.
그래서 프로토콜 문서를 잘 관리하는 게 중요하고, 가능하면 자동화 테스트로 호환성을 검증하는 게 좋아."
실전 팁
💡 - iOS는 URLSessionWebSocketTask(네이티브), Android는 OkHttp(라이브러리)를 표준으로 사용하세요
- 프로토콜 변경 시 양 플랫폼 동시 업데이트를 잊지 마세요
- JSON 파싱 라이브러리는 각 플랫폼 표준을 따르세요 (Swift: Codable, Kotlin: Gson/Moshi)
5. Bonjour 페어링 메커니즘
WebSocket 통신 구조는 갖춰졌습니다. 그런데 한 가지 문제가 남았습니다.
모바일 앱이 macOS 앱에 접속하려면 IP 주소와 포트를 알아야 합니다. 사용자에게 "192.168.0.15:8080을 입력하세요"라고 하면 너무 불편하지 않을까요?
Bonjour는 Apple이 개발한 제로 구성 네트워킹 프로토콜입니다. 마치 블루투스 페어링처럼, 같은 네트워크에 있는 기기를 자동으로 발견할 수 있습니다.
IP 주소를 직접 입력할 필요 없이 "내 Mac"이라는 이름만 보고 연결할 수 있어 사용자 경험이 크게 향상됩니다.
다음 코드를 살펴봅시다.
import Network
class BonjourAdvertiser {
private var listener: NWListener?
func startAdvertising(name: String, port: UInt16) throws {
let parameters = NWParameters.tcp
// Bonjour 서비스 등록
listener = try NWListener(using: parameters,
on: NWEndpoint.Port(rawValue: port)!)
// 서비스 타입: _myapp._tcp (사용자 정의)
listener?.service = NWListener.Service(
name: name, // "김개발의 MacBook"
type: "_myapp._tcp" // 서비스 타입 식별자
)
listener?.serviceRegistrationUpdateHandler = { change in
switch change {
case .add(let endpoint):
print("Bonjour 서비스 등록됨: \(endpoint)")
case .remove(let endpoint):
print("Bonjour 서비스 해제됨: \(endpoint)")
@unknown default:
break
}
}
listener?.start(queue: .main)
}
}
class BonjourBrowser {
private var browser: NWBrowser?
func startBrowsing(onFound: @escaping (String, NWEndpoint) -> Void) {
// 같은 타입의 서비스 검색
browser = NWBrowser(for: .bonjour(type: "_myapp._tcp", domain: nil),
using: .tcp)
browser?.browseResultsChangedHandler = { results, _ in
for result in results {
if case .service(let name, _, _, _) = result.endpoint {
onFound(name, result.endpoint)
}
}
}
browser?.start(queue: .main)
}
}
김개발 씨는 테스트를 해보았습니다. macOS 앱에서 WebSocket 서버를 시작하고, iPhone에서 연결을 시도했습니다.
그런데 막막했습니다. "내 Mac의 IP 주소가 뭐지?
매번 바뀌는 것 같은데..." 이런 상황은 실제 사용자도 똑같이 겪게 됩니다. 집에서는 잘 되던 연결이 카페에 가면 안 되고, 회사에서도 또 설정을 바꿔야 합니다.
사용자에게 IP 주소를 입력하라고 하는 것은 최악의 UX입니다. 바로 이 문제를 해결하기 위해 Bonjour가 있습니다.
Bonjour는 Apple이 만든 기술이지만, 실제로는 mDNS(Multicast DNS)와 DNS-SD(DNS Service Discovery)라는 표준 프로토콜을 기반으로 합니다. 쉽게 말해, 같은 네트워크에 "나 여기 있어요!"라고 광고하고, 다른 기기들이 그 광고를 보고 찾아오는 방식입니다.
마치 식당가에서 호객 행위를 하는 것과 비슷합니다. macOS 앱이 "저희 맛집이에요, 포트 8080에서 영업 중입니다!"라고 외치면, iPhone 앱이 그 소리를 듣고 찾아가는 것입니다.
코드를 살펴보겠습니다. BonjourAdvertiser 클래스가 광고자 역할입니다.
NWListener.Service를 설정할 때 두 가지 정보를 제공합니다. name은 사람이 읽을 수 있는 이름으로 "김개발의 MacBook" 같은 친근한 이름을 사용합니다.
type은 서비스 종류를 나타내는 식별자로 "_myapp._tcp" 형식을 따릅니다. 밑줄로 시작하는 것이 규칙입니다.
BonjourBrowser 클래스가 탐색자 역할입니다. NWBrowser를 생성할 때 찾고자 하는 서비스 타입을 지정합니다.
같은 타입의 서비스가 발견되면 browseResultsChangedHandler가 호출됩니다. 여기서 서비스 이름과 엔드포인트 정보를 얻을 수 있습니다.
Android에서는 어떻게 할까요? Android에서는 NSD(Network Service Discovery)라는 API를 사용합니다. Bonjour와 호환되므로, macOS에서 광고한 서비스를 Android 앱에서도 발견할 수 있습니다.
NsdManager.DiscoveryListener를 구현하여 서비스 발견 이벤트를 처리합니다. 페어링 과정을 정리하면 이렇습니다.
첫째, macOS 앱이 시작되면 Bonjour 서비스를 광고합니다. 둘째, 모바일 앱이 같은 네트워크에서 서비스를 검색합니다.
셋째, 발견된 서비스 목록을 사용자에게 보여줍니다. 넷째, 사용자가 연결할 Mac을 선택하면 자동으로 IP와 포트 정보를 얻어 WebSocket 연결을 시작합니다.
이 모든 과정에서 사용자가 입력해야 할 것은 아무것도 없습니다. 주의할 점도 있습니다.
Bonjour는 같은 서브넷 내에서만 작동합니다. Wi-Fi와 유선 LAN이 다른 서브넷이면 서로 발견할 수 없습니다.
또한 일부 공용 Wi-Fi에서는 mDNS 패킷이 차단되어 있을 수 있습니다. 김개발 씨는 다시 테스트했습니다.
이번에는 iPhone 앱을 열자마자 "김개발의 MacBook Pro"라는 이름이 자동으로 나타났습니다. 탭 한 번으로 연결 완료.
"이게 바로 사용자가 원하는 경험이구나!"
실전 팁
💡 - 서비스 타입은 고유하게 정의하세요 (예: _mycompany-myapp._tcp)
- Android에서는 NsdManager를 사용하여 동일한 기능을 구현합니다
- 연결 실패 시 수동 IP 입력 옵션도 제공하면 좋습니다
6. 실전 모바일 앱 빌드하기
이론은 충분합니다. 이제 직접 앱을 빌드해볼 시간입니다.
김개발 씨는 터미널을 열고 apps/ios 폴더로 이동했습니다. "Xcode 없이 커맨드 라인으로도 빌드할 수 있다던데..."
iOS 앱은 xcodebuild 명령어로, Android 앱은 Gradle로 빌드합니다. 각 플랫폼의 빌드 시스템을 이해하면 CI/CD 파이프라인 구축이 가능하고, 자동화된 배포가 가능해집니다.
실제 기기에서 테스트하려면 인증서와 프로비저닝 프로파일 설정이 필요합니다.
다음 코드를 살펴봅시다.
# iOS 앱 빌드 (macOS에서만 가능)
cd apps/ios
# 의존성 설치 (CocoaPods 사용 시)
pod install
# 시뮬레이터용 빌드
xcodebuild -workspace MyApp.xcworkspace \
-scheme MyApp \
-sdk iphonesimulator \
-configuration Debug \
build
# 실제 기기용 아카이브 생성
xcodebuild -workspace MyApp.xcworkspace \
-scheme MyApp \
-sdk iphoneos \
-configuration Release \
archive -archivePath ./build/MyApp.xcarchive
# Android 앱 빌드
cd apps/android
# 디버그 APK 빌드
./gradlew assembleDebug
# 릴리즈 APK 빌드 (서명 필요)
./gradlew assembleRelease
# 빌드된 APK 위치
# app/build/outputs/apk/debug/app-debug.apk
# app/build/outputs/apk/release/app-release.apk
박시니어 씨가 김개발 씨 옆에 앉았습니다. "자, 이제 실제로 빌드해보자.
IDE에서 버튼 클릭하는 것도 좋지만, 커맨드 라인 빌드를 알아두면 나중에 자동화할 때 도움이 돼." 먼저 iOS 빌드부터 시작합니다. iOS 앱은 macOS에서만 빌드할 수 있습니다.
이것은 Apple의 정책입니다. xcodebuild는 Xcode에 포함된 커맨드 라인 도구로, Xcode를 열지 않고도 빌드할 수 있게 해줍니다.
apps/ios 폴더에서 작업을 시작합니다. 만약 CocoaPods로 의존성을 관리한다면 먼저 pod install을 실행해야 합니다.
이 명령어는 Podfile에 정의된 라이브러리들을 다운로드하고 프로젝트에 연결합니다. 시뮬레이터용 빌드와 실제 기기용 빌드는 다릅니다.
iphonesimulator SDK는 Mac의 CPU 아키텍처(Intel 또는 Apple Silicon)에 맞춰 빌드합니다. iphoneos SDK는 실제 iPhone의 ARM 프로세서에 맞춰 빌드합니다.
배포용 앱은 반드시 iphoneos로 빌드해야 합니다. 실제 기기에 설치하려면 인증서와 프로비저닝 프로파일이 필요합니다.
Apple Developer Program에 가입하고, Xcode에서 계정을 연결하면 자동으로 관리됩니다. 처음에는 복잡해 보이지만, 한번 설정해두면 이후에는 수월합니다.
이제 Android 빌드를 살펴보겠습니다. Android는 Gradle이라는 빌드 시스템을 사용합니다.
apps/android 폴더에 gradlew라는 래퍼 스크립트가 있습니다. 이것을 사용하면 Gradle이 설치되어 있지 않아도 빌드할 수 있습니다.
assembleDebug는 디버그 빌드를, assembleRelease는 릴리즈 빌드를 생성합니다. 디버그 빌드는 디버깅이 가능하고 서명 없이도 설치할 수 있습니다.
릴리즈 빌드는 최적화가 적용되고, Play 스토어 배포를 위해 서명이 필요합니다. 빌드된 APK 파일은 app/build/outputs/apk 폴더에 생성됩니다.
adb install 명령어로 연결된 Android 기기에 직접 설치할 수 있습니다. CI/CD 파이프라인에서 이 명령어들이 어떻게 사용될까요?
GitHub Actions나 Jenkins 같은 CI 도구에서 코드가 푸시될 때마다 자동으로 빌드를 실행할 수 있습니다. iOS 빌드는 macOS 러너가 필요하고, Android 빌드는 Linux 러너에서도 가능합니다.
성공적으로 빌드되면 테스트 기기에 자동 배포하거나, 스토어에 업로드하는 것까지 자동화할 수 있습니다. 자주 발생하는 문제와 해결책도 알아두면 좋습니다.
iOS에서 "No signing certificate found" 에러가 나면 Xcode에서 계정과 인증서를 확인해야 합니다. Android에서 "SDK location not found" 에러가 나면 local.properties 파일에 SDK 경로를 지정해야 합니다.
김개발 씨는 터미널에서 빌드 명령어를 실행했습니다. 잠시 후 "BUILD SUCCESSFUL" 메시지가 나타났습니다.
처음으로 모바일 앱을 빌드한 순간이었습니다. 박시니어 씨가 격려했습니다.
"축하해. 이제 네가 만든 앱이 세 개 플랫폼에서 돌아가게 됐어.
macOS에서 서버를 실행하고, iPhone과 Android에서 연결해봐." 김개발 씨는 macOS 앱을 시작하고, iPhone 시뮬레이터에서 앱을 열었습니다. Bonjour가 자동으로 Mac을 발견했고, 탭 한 번으로 연결이 완료되었습니다.
메시지를 보내니 macOS 앱에서 즉시 수신되었습니다. "드디어 완성이다!"
실전 팁
💡 - iOS 빌드는 macOS 필수, Android는 Windows/Linux/macOS 모두 가능합니다
- 첫 빌드 전 Xcode 라이선스 동의 필요: sudo xcodebuild -license accept
- 빌드 시간을 단축하려면 ccache(iOS)나 Gradle 빌드 캐시(Android)를 활용하세요
이상으로 학습을 마칩니다. 위 내용을 직접 코드로 작성해보면서 익혀보세요!
댓글 (0)
함께 보면 좋은 카드 뉴스
Flame 게임 플랫폼별 최적화 완벽 가이드
Flutter Flame 게임 엔진으로 개발한 게임을 iOS, Android, 웹 등 다양한 플랫폼에서 최적의 성능으로 실행하기 위한 최적화 기법을 다룹니다. 플랫폼 감지부터 Metal, Vulkan 활용, 해상도 스케일링까지 실무에서 바로 적용할 수 있는 내용을 담았습니다.
Parallelization Workflow 완벽 가이드
AI 에이전트 개발에서 핵심이 되는 병렬화 워크플로우를 다룹니다. Coroutine을 활용한 동시 처리부터 부분 실패 복구, 성능 최적화까지 실무에서 바로 적용할 수 있는 기법을 배웁니다.
Discord 봇 개발 완벽 가이드
discord.js로 Discord 봇을 만들어봅시다. 실시간 채팅 연동부터 슬래시 커맨드까지, 실무 코드로 배우는 Discord 통합 가이드입니다.
WhatsApp 통합 완벽 가이드 Baileys로 시작하기
Node.js 환경에서 Baileys 라이브러리를 활용하여 WhatsApp Web 프로토콜을 통합하는 완벽 가이드입니다. QR 인증부터 메시지 송수신, 미디어 처리까지 실전 예제와 함께 배웁니다.
Gateway 아키텍처 완벽 가이드
마이크로서비스 환경에서 WebSocket 기반 Gateway 패턴을 이해하고 실전에서 활용하는 방법을 배웁니다. 제어 플레인과 세션 관리 메커니즘을 Node.js로 구현하며, 실무에서 바로 적용할 수 있는 노하우를 제공합니다.