Flutter 개발

Platform Channel 완벽 가이드 2편: EventChannel과 BasicMessageChannel 실전 구현

실시간 데이터 스트리밍과 양방향 통신을 위한 고급 채널 활용법

John Doe
2025년 10월 8일
14분 읽기
41

지속적인 데이터 전송과 복잡한 통신 구조의 필요성

MethodChannel을 통해 네이티브 기능을 호출하고 결과를 받는 방식은 대부분의 상황에서 효과적이지만, 실시간으로 변화하는 데이터를 다루어야 하는 경우에는 한계가 명확합니다. 예를 들어 가속도 센서의 값을 1초에 수십 번씩 받아야 하는 상황에서 매번 MethodChannel로 호출하는 것은 비효율적이며, 코드의 복잡성을 불필요하게 증가시킵니다. 또한 GPS 위치 추적이나 배터리 상태 모니터링처럼 네이티브에서 발생하는 이벤트를 지속적으로 관찰해야 하는 경우, 단방향 요청-응답 패턴으로는 자연스러운 구현이 어렵습니다.
한편 네이티브와 Flutter 간에 양방향으로 메시지를 자유롭게 주고받아야 하는 복잡한 통신 구조도 존재합니다. 예를 들어 블루투스 통신에서는 Flutter에서 명령을 보내고 네이티브에서 응답을 받는 동시에, 네이티브에서도 연결 상태 변화나 데이터 수신 이벤트를 Flutter로 전달해야 합니다. 이러한 경우 MethodChannel만으로는 구조적으로 해결하기 어려우며, EventChannel이나 BasicMessageChannel과 같은 고급 채널의 활용이 필요합니다. 따라서 이번 글에서는 이 두 채널의 구현 방식과 실무 적용 사례를 구체적으로 다루어, 복잡한 네이티브 통합 시나리오를 효과적으로 해결할 수 있는 방법을 제시합니다.

EventChannel의 Stream 기반 통신 메커니즘

EventChannel은 네이티브에서 Flutter로 지속적으로 데이터를 전송하는 스트림 기반의 통신 채널입니다. 이는 Dart의 Stream API와 자연스럽게 통합되어 있으며, Flutter 측에서 구독을 시작하면 네이티브에서 데이터를 전송하기 시작하고, 구독을 취소하면 전송이 중단되는 구조를 가집니다. 이러한 방식은 옵저버 패턴과 유사하며, 네이티브에서 발생하는 이벤트를 Flutter가 수동적으로 수신하는 형태로 동작합니다.
EventChannel의 구현은 네이티브 측에서 EventSink를 통해 데이터를 전송하고, Flutter 측에서는 receiveBroadcastStream 메서드를 통해 Stream을 생성하여 listen하는 방식으로 이루어집니다. 이때 네이티브는 onListen과 onCancel 콜백을 구현하여 구독 시작과 종료 시점을 감지하고, 이에 따라 센서 등록이나 리소스 해제와 같은 작업을 수행할 수 있습니다. 결과적으로 EventChannel은 센서 데이터, 위치 정보, 네트워크 상태 변화 등 시간에 따라 변하는 값을 효율적으로 전달하는 데 최적화되어 있습니다.

BasicMessageChannel의 자유로운 양방향 통신 구조

BasicMessageChannel은 MethodChannel이나 EventChannel과 달리 특정 통신 패턴에 제약받지 않는 가장 유연한 형태의 채널입니다. 이는 Flutter와 네이티브 양쪽에서 모두 메시지를 보낼 수 있으며, 메서드 이름이나 이벤트 구조 없이 임의의 데이터를 전송할 수 있습니다. 따라서 개발자는 메시지의 포맷과 의미를 직접 정의해야 하며, 이는 높은 자유도를 제공하는 동시에 설계의 책임도 함께 부여합니다.
BasicMessageChannel은 MessageCodec을 사용하여 데이터를 직렬화하며, StandardMessageCodec, JSONMessageCodec, StringCodec 등 다양한 코덱을 선택할 수 있습니다. 이는 전송할 데이터의 형태에 따라 최적의 인코딩 방식을 선택할 수 있음을 의미하며, 복잡한 객체 구조나 바이너리 데이터를 전달해야 하는 경우에도 유연하게 대응할 수 있습니다. 그러나 이러한 유연성은 명확한 프로토콜 설계가 없으면 코드의 복잡성을 증가시킬 수 있으므로, 실무에서는 명확한 메시지 구조와 처리 로직을 먼저 설계한 후 구현하는 것이 권장됩니다.

각 채널의 구현 흐름과 적용 시나리오

EventChannel과 BasicMessageChannel은 각각의 통신 특성에 따라 구현 방식과 적용 범위가 명확히 구분됩니다. 아래 표는 두 채널의 핵심 구현 요소와 실무 활용 사례를 정리한 것입니다.
구분EventChannelBasicMessageChannel
통신 방향네이티브 → Flutter (단방향)양방향 자유 통신
데이터 형태Stream (연속적 이벤트)개별 메시지 단위
구독 관리onListen / onCancel핸들러 등록 방식
주요 사용 사례센서 데이터, 위치 추적, 배터리 모니터링블루투스 통신, 커스텀 프로토콜, 복잡한 상태 동기화
코덱 선택StandardMessageCodec 고정StandardMessageCodec, JSON, String 등 선택 가능
이러한 특성을 바탕으로 채널을 선택할 때는 먼저 데이터의 전송 빈도와 방향성을 고려해야 합니다. 네이티브에서 지속적으로 발생하는 이벤트를 Flutter에서 수신해야 한다면 EventChannel이 가장 적합하며, 양쪽에서 모두 메시지를 보내야 하거나 복잡한 통신 프로토콜을 구현해야 한다면 BasicMessageChannel을 사용하는 것이 효과적입니다. 또한 EventChannel은 Stream 기반이므로 Flutter의 StreamBuilder나 listen 메서드와 자연스럽게 통합되며, BasicMessageChannel은 메시지 핸들러를 직접 구현해야 하므로 상태 관리와의 연계를 신중히 설계해야 합니다.

EventChannel을 활용한 가속도 센서 데이터 스트리밍

EventChannel의 실제 구현 방식을 이해하기 위해, Android의 가속도 센서 데이터를 실시간으로 Flutter에 전달하는 예제를 살펴보겠습니다. 이 예제는 Flutter에서 센서 스트림을 구독하면 네이티브에서 센서 리스너를 등록하고, 센서 값이 변경될 때마다 Flutter로 데이터를 전송하는 전체 흐름을 보여줍니다.
Dart
// Flutter 측 코드
import 'package:flutter/services.dart';

class AccelerometerService {
  // EventChannel 인스턴스 생성
  static const eventChannel = EventChannel('com.example.app/accelerometer');

  // 센서 데이터 스트림 구독
  Stream<Map<String, double>> getAccelerometerStream() {
    return eventChannel.receiveBroadcastStream().map((event) {
      // 네이티브에서 전달된 데이터를 Map으로 변환
      final data = Map<String, double>.from(event as Map);
      return data;
    });
  }
}

// 사용 예시
class SensorWidget extends StatefulWidget {
  @override
  _SensorWidgetState createState() => _SensorWidgetState();
}

class _SensorWidgetState extends State<SensorWidget> {
  final _service = AccelerometerService();
  StreamSubscription? _subscription;

  @override
  void initState() {
    super.initState();
    // 센서 스트림 구독 시작
    _subscription = _service.getAccelerometerStream().listen((data) {
      print('X: ${data['x']}, Y: ${data['y']}, Z: ${data['z']}');
    });
  }

  @override
  void dispose() {
    // 구독 취소로 네이티브 리소스 해제
    _subscription?.cancel();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) => Container();
}

// Android 측 코드 (Kotlin)
import android.content.Context
import android.hardware.Sensor
import android.hardware.SensorEvent
import android.hardware.SensorEventListener
import android.hardware.SensorManager
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.EventChannel

class MainActivity: FlutterActivity() {
    private val CHANNEL = "com.example.app/accelerometer"
    private var sensorManager: SensorManager? = null
    private var accelerometer: Sensor? = null

    override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
        super.configureFlutterEngine(flutterEngine)
        
        sensorManager = getSystemService(Context.SENSOR_SERVICE) as SensorManager
        accelerometer = sensorManager?.getDefaultSensor(Sensor.TYPE_ACCELEROMETER)
        
        // EventChannel 설정 및 StreamHandler 등록
        EventChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL)
            .setStreamHandler(object : EventChannel.StreamHandler {
                private var sensorEventListener: SensorEventListener? = null
                
                // Flutter에서 구독 시작 시 호출
                override fun onListen(arguments: Any?, events: EventChannel.EventSink?) {
                    sensorEventListener = object : SensorEventListener {
                        override fun onSensorChanged(event: SensorEvent?) {
                            event?.let {
                                // 센서 데이터를 Map으로 변환하여 전송
                                val data = mapOf(
                                    "x" to it.values[0].toDouble(),
                                    "y" to it.values[1].toDouble(),
                                    "z" to it.values[2].toDouble()
                                )
                                events?.success(data)
                            }
                        }
                        
                        override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) {}
                    }
                    
                    // 센서 리스너 등록
                    sensorManager?.registerListener(
                        sensorEventListener,
                        accelerometer,
                        SensorManager.SENSOR_DELAY_NORMAL
                    )
                }
                
                // Flutter에서 구독 취소 시 호출
                override fun onCancel(arguments: Any?) {
                    // 센서 리스너 해제로 리소스 절약
                    sensorManager?.unregisterListener(sensorEventListener)
                    sensorEventListener = null
                }
            })
    }
}
위 코드에서 Flutter는 EventChannel을 통해 receiveBroadcastStream을 호출하여 Stream을 생성하고, listen 메서드로 구독을 시작합니다. 이때 네이티브의 onListen 콜백이 호출되어 센서 리스너가 등록되며, 센서 값이 변경될 때마다 EventSink의 success 메서드를 통해 데이터가 Flutter로 전송됩니다. Flutter에서 구독을 취소하면 onCancel이 호출되어 센서 리스너가 해제되므로, 불필요한 배터리 소모를 방지할 수 있습니다. 결과적으로 EventChannel은 지속적인 데이터 전송을 효율적으로 처리하며, Stream 기반의 반응형 프로그래밍과 자연스럽게 통합됩니다.

BasicMessageChannel을 활용한 양방향 메시지 통신

BasicMessageChannel의 양방향 통신 구조를 이해하기 위해, Flutter와 네이티브 간에 사용자 정의 메시지를 주고받는 예제를 살펴보겠습니다. 이 예제는 Flutter에서 네이티브로 명령을 보내고, 네이티브에서도 독립적으로 Flutter에 상태 업데이트를 전송할 수 있는 구조를 보여줍니다.
Dart
// Flutter 측 코드
import 'package:flutter/services.dart';

class CustomMessageService {
  // BasicMessageChannel 인스턴스 생성 (StandardMessageCodec 사용)
  static const channel = BasicMessageChannel<dynamic>(
    'com.example.app/custom',
    StandardMessageCodec(),
  );

  // 네이티브로 메시지 전송
  Future<void> sendMessage(Map<String, dynamic> message) async {
    await channel.send(message);
  }

  // 네이티브에서 오는 메시지 수신 핸들러 등록
  void setMessageHandler(Function(dynamic) handler) {
    channel.setMessageHandler((message) async {
      handler(message);
      return null;
    });
  }
}

// 사용 예시
void main() {
  final service = CustomMessageService();
  
  // 네이티브에서 오는 메시지 처리
  service.setMessageHandler((message) {
    print('Received from native: $message');
  });
  
  // 네이티브로 메시지 전송
  service.sendMessage({
    'action': 'start',
    'params': {'interval': 1000}
  });
}

// Android 측 코드 (Kotlin)
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.BasicMessageChannel
import io.flutter.plugin.common.StandardMessageCodec
import kotlinx.coroutines.*

class MainActivity: FlutterActivity() {
    private val CHANNEL = "com.example.app/custom"
    private var messageChannel: BasicMessageChannel<Any>? = null
    private var backgroundJob: Job? = null

    override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
        super.configureFlutterEngine(flutterEngine)
        
        // BasicMessageChannel 생성
        messageChannel = BasicMessageChannel(
            flutterEngine.dartExecutor.binaryMessenger,
            CHANNEL,
            StandardMessageCodec.INSTANCE
        )
        
        // Flutter에서 오는 메시지 처리
        messageChannel?.setMessageHandler { message, reply ->
            when (val data = message as? Map<*, *>) {
                null -> reply.reply(null)
                else -> {
                    val action = data["action"] as? String
                    when (action) {
                        "start" -> {
                            val interval = (data["params"] as? Map<*, *>)
                                ?.get("interval") as? Int ?: 1000
                            startBackgroundTask(interval.toLong())
                            reply.reply(mapOf("status" to "started"))
                        }
                        "stop" -> {
                            stopBackgroundTask()
                            reply.reply(mapOf("status" to "stopped"))
                        }
                        else -> reply.reply(mapOf("error" to "Unknown action"))
                    }
                }
            }
        }
    }

    // 백그라운드 작업 시작 (네이티브에서 Flutter로 주기적 메시지 전송)
    private fun startBackgroundTask(interval: Long) {
        backgroundJob = CoroutineScope(Dispatchers.Default).launch {
            while (isActive) {
                delay(interval)
                // 네이티브에서 Flutter로 메시지 전송
                runOnUiThread {
                    messageChannel?.send(mapOf(
                        "type" to "update",
                        "timestamp" to System.currentTimeMillis()
                    ))
                }
            }
        }
    }

    private fun stopBackgroundTask() {
        backgroundJob?.cancel()
        backgroundJob = null
    }

    override fun onDestroy() {
        stopBackgroundTask()
        super.onDestroy()
    }
}
위 코드에서 BasicMessageChannel은 Flutter와 네이티브 양쪽에 메시지 핸들러를 등록할 수 있으며, send 메서드를 통해 어느 쪽에서든 메시지를 전송할 수 있습니다. Flutter에서 start 액션을 보내면 네이티브는 백그라운드 작업을 시작하고, 주기적으로 update 메시지를 Flutter로 전송합니다. 이러한 구조는 MethodChannel의 단방향 호출이나 EventChannel의 구독 패턴으로는 구현하기 어려운 복잡한 통신 흐름을 자연스럽게 표현할 수 있습니다. 따라서 BasicMessageChannel은 양쪽에서 모두 주도권을 가져야 하는 복잡한 프로토콜이나, 기존 네이티브 시스템과의 통합이 필요한 경우에 효과적으로 활용할 수 있습니다.

고급 채널 활용의 핵심과 실무 선택 전략

EventChannel과 BasicMessageChannel은 각각 특정한 통신 패턴에 최적화되어 있으며, MethodChannel만으로는 해결하기 어려운 복잡한 시나리오를 효과적으로 처리할 수 있습니다. EventChannel은 네이티브에서 발생하는 지속적인 이벤트를 Stream 형태로 Flutter에 전달하는 데 최적화되어 있으며, 센서 데이터나 위치 추적과 같은 실시간 데이터 전송에 적합합니다. 이는 onListen과 onCancel 콜백을 통해 리소스를 효율적으로 관리할 수 있으며, Flutter의 Stream API와 자연스럽게 통합되어 반응형 프로그래밍 패턴을 쉽게 구현할 수 있습니다.
한편 BasicMessageChannel은 양방향 자유 통신이 필요한 복잡한 프로토콜 구현에 유용하며, 블루투스 통신이나 커스텀 네이티브 라이브러리와의 통합에서 강점을 발휘합니다. 다만 이러한 유연성은 명확한 메시지 구조와 처리 로직을 설계해야 하는 책임을 수반하므로, 실무에서는 프로토콜을 먼저 정의하고 문서화한 후 구현하는 것이 권장됩니다. 결과적으로 세 가지 Platform Channel을 모두 이해하고 상황에 맞게 선택한다면, Flutter 애플리케이션에서 네이티브 통합의 복잡성을 최소화하면서도 강력한 기능을 구현할 수 있습니다.
#Platform Channel
#EventChannel
#BasicMessageChannel
#Stream
#Native Integration