StreamBuilder: 개념, 작동 원리 및 Flutter에서의 활용

저자: IT Sectr 게시일: 2026-07-03 읽는 시간: 8 분

StreamBuilder는 비동기 스트림에서 새 데이터를 받을 때 자동으로 인터페이스를 재구성하는 Flutter 위젯입니다. 단일 결과를 처리하는 FutureBuilder와 달리, StreamBuilder는 Stream의 전체 라이프사이클 동안 지속적인 UI 업데이트를 지원합니다. 공식 Flutter 문서(2026)에 따르면, StreamBuilder는 실시간 애플리케이션(채팅, 뉴스 피드, 센서 모니터링, 금융 티커)에서 사용됩니다. 이는 UI가 수동적인 setState 호출 없이 데이터 상태를 반영하는 리액티브 프로그래밍의 핵심 도구입니다.

핵심 사항

  • StreamBuilder — 리액티브 UI 렌더링을 위해 Stream과 데이터 스냅샷을 받아들이는 위젯
  • 스냅샷에는 connectionState, data, error가 포함되어 스트림의 현재 상태를 정의합니다
  • ConnectionState는 none, waiting, active, done의 네 단계를 거칩니다
  • AsyncSnapshot — 모든 프레임에서 데이터 일관성을 보장하는 불변 객체
  • StreamController는 스트림을 관리합니다: 데이터 추가, 오류 처리, Stream 종료

StreamBuilder란

StreamBuilder는 Flutter SDK 패키지의 위젯으로, Stream을 구독하고 새로운 스트림 이벤트가 발생할 때마다 자식 요소를 재구성합니다. StreamBuilder는 Stream 객체를 받아들이고 스트림에서 수신한 최신 스냅샷을 기반으로 위젯을 반환합니다.

Flutter 아키텍처에서 StreamBuilder는 UI 구성을 데이터 상태와 분리하는 Builder 위젯 그룹에 속합니다. 상태 변경에 명시적인 setState 호출이 필요한 StatefulWidget과 달리, StreamBuilder는 비동기 이벤트에 자동으로 반응하여 코드를 단순화하고 동기화 오류 위험을 줄입니다.

단일 비동기 값을 처리하는 FutureBuilder와 달리, StreamBuilder는 지속적인 데이터 스트림을 위해 설계되었습니다. FutureBuilder는 첫 번째 결과를 받으면 종료되는 반면, StreamBuilder는 스트림을 계속 수신하고 새 이벤트마다 UI를 업데이트합니다.

StreamBuilder는 데이터가 지속적으로 도착하는 모든 시나리오에서 사용됩니다: WebSocket 연결, 센서 콜백, Firebase 알림, Bluetooth 이벤트 큐, BLoC를 통한 애플리케이션 상태 브로드캐스트. GitHub의 Flutter 프로젝트 분석(2025)에 따르면, StreamBuilder는 FutureBuilder 및 LayoutBuilder와 함께 가장 많이 사용되는 세 가지 Builder 위젯 중 하나입니다.

결론: UI가 지속적으로 변화하는 데이터를 반영해야 하는 곳에서는 StatefulWidget을 통한 수동 상태 관리를 피하고 StreamBuilder를 사용하세요.

StreamBuilder 작동 방식

StreamBuilder는 빌드 시점에 Stream을 구독하고 위젯이 소멸될 때 구독을 해지합니다. Stream이 이벤트를 발생시킬 때마다 StreamBuilder는 새 AsyncSnapshot을 받고 UI를 재구성하기 위해 builder 함수를 호출합니다.

프로세스는 세 단계로 구성됩니다. 첫째: StreamBuilder는 stream.listen 메서드를 통해 전달된 Stream에 대한 구독을 생성합니다. 둘째: 각 이벤트에서 StreamBuilder는 내부 AsyncSnapshot을 업데이트하고 위젯을 재구성이 필요함(dirty)으로 표시합니다. 셋째: 프레임워크가 새 스냅샷으로 builder 함수를 호출하고 UI가 현재 데이터를 표시합니다.

중요: StreamBuilder는 내부적으로 StreamSubscription을 사용합니다. Stream이 직접 전달되면 StreamBuilder는 초기화 중에 한 번 구독합니다. Stream이 변경되면(예: 부모 재구성 시) StreamBuilder는 이전 스트림에서 구독을 해지하고 새 스트림을 구독합니다. 이 동작은 initialData 및 buildWhen 매개변수로 제어되며, 재구성 횟수를 최적화할 수 있습니다.

결론: 구독 라이프사이클을 이해하는 것이 StreamBuilder를 올바르게 사용하는 기본입니다. 잘못된 스트림 관리는 메모리 누수 또는 UI의 오래된 데이터로 이어집니다.

ConnectionState: 스트림의 네 가지 상태

AsyncSnapshot 객체의 connectionState 속성은 StreamBuilder가 스트림 처리의 어떤 단계에 있는지 결정합니다. none, waiting, active, done의 네 가지 상태가 있습니다.

ConnectionState.none

None은 Stream이 아직 데이터 전송을 시작하지 않은 초기 상태입니다. 이 상태에서 snapshot.connectionState는 ConnectionState.none이고 snapshot.data는 null입니다. 일반적으로 이 상태에서는 플레이스홀더 또는 대기 표시기가 표시됩니다. Stream이 초기 데이터를 제공하지 않으면 StreamBuilder는 이 상태에서 시작됩니다.

ConnectionState.waiting

Waiting은 비동기 스트림에서 데이터를 기다리는 상태입니다. Stream은 활성 상태이지만 데이터가 아직 도착하지 않았습니다. 이 상태는 예를 들어 네트워크에서 데이터를 로드하거나 장기 연결을 열 때 발생합니다. 이 상태에서는 CircularProgressIndicator 또는 스켈레톤 로더를 표시하는 것이 일반적입니다.

ConnectionState.active

Active — 스트림이 데이터를 발생시키고 UI가 최신 정보를 표시합니다. 이 상태에서 snapshot.hasData는 true이고 snapshot.data에는 스트림의 최신 값이 포함됩니다. 스트림이 Broadcast Stream인 경우 활성 상태는 새 데이터 대기와 공존할 수 있습니다.

ConnectionState.done

Done — 스트림이 완료되어 새 데이터가 도착하지 않습니다. Snapshot.data에는 스트림이 닫히기 전에 전달된 마지막 값이 포함됩니다. 스트림이 성공적으로 완료되면 snapshot.hasError는 false입니다. 이 상태는 최종 결과를 표시하는 데 사용됩니다: “로딩 완료” 메시지 또는 다음 화면으로의 전환 등입니다.

결론: StreamBuilder를 통해 UI를 구축할 때는 인터페이스가 로딩, 데이터, 오류 및 완료를 올바르게 표시할 수 있도록 네 가지 상태를 모두 처리해야 합니다.

StreamController를 사용한 스트림 관리

StreamController는 dart:async 패키지의 클래스로, Stream을 생성하고 관리합니다. StreamController는 데이터 추가, 오류 처리, 스트림 종료 및 라이프사이클 제어를 가능하게 합니다.

StreamController에는 single-subscription(구독자 1명)과 broadcast(여러 구독자)의 두 가지 유형이 있습니다. Single-subscription 컨트롤러는 한 번에 한 명의 리스너만 허용합니다 — 두 번째 구독은 예외를 발생시킵니다. Broadcast 컨트롤러는 여러 StreamBuilder가 동시에 동일한 스트림을 수신할 수 있게 하며, BLoC 및 공유 애플리케이션 상태에 유용합니다.

StreamController<T>.broadcast()를 통해 StreamController를 생성할 때, 첫 번째 구독 전에 추가된 데이터는 새 구독자에게 재생되지 않습니다. 연결 시 최신 값을 얻으려면 마지막 이벤트를 캐시하는 rxdart 패키지의 BehaviourSubject를 사용하세요.

컨트롤러 작업을 마친 후에는 controller.close()를 호출해야 합니다. close를 호출하지 않으면 리소스 누수가 발생합니다: 스트림이 열린 상태로 유지되고, 구독자가 메모리에 남아 있으며, GC가 관련 객체를 해제하지 않습니다.

결론: StreamController는 명시적인 라이프사이클 관리와 함께 사용하세요. Single-subscription 스트림에는 표준 컨트롤러를, 공유 상태에는 broadcast 컨트롤러 또는 BehaviourSubject를 사용하세요.

StreamBuilder 코드 예제

예제 1은 StreamController와 StreamBuilder를 사용한 카운트다운 타이머를 보여줍니다.

dart
import 'dart:async';

class TimerWidget extends StatefulWidget {
  const TimerWidget({super.key});

  final StreamController<int> controller = StreamController<int>();

  void startTimer() {
    int count = 0;
    Timer.periodic(Duration(seconds: 1), (timer) {
      controller.sink.add(count++);
      if (count > 10) {
        controller.close();
        timer.cancel();
      }
    });
  }
}

예제에서는 1초 간격으로 0부터 10까지 숫자를 생성하는 컨트롤러가 생성됩니다. 10에 도달하면 close가 호출되고 스트림이 종료됩니다. 이 컨트롤러의 스트림을 구독한 StreamBuilder는 각각의 새 값을 표시합니다.

예제 2 — 여러 소스의 데이터를 표시하기 위해 Broadcast Stream과 함께 StreamBuilder 사용.

dart
final StreamController<String> broadcastController =
    StreamController<String>.broadcast();

StreamBuilder<String>(
  stream: broadcastController.stream,
  initialData: 'Waiting for data...',
  builder: (context, AsyncSnapshot<String> snapshot) {
    if (snapshot.connectionState == ConnectionState.waiting) {
      return const Center(
        child: CircularProgressIndicator(),
      );
    }
    if (snapshot.hasError) {
      return Text('오류: ${snapshot.error}');
    }
    return Text('데이터: ${snapshot.data}');
  },
)

두 번째 예제는 모든 상태 처리를 보여줍니다: 초기 표시를 위한 initialData, 로딩 표시기를 위한 waiting, 오류를 위한 hasError, 성공 결과를 위한 data. 이 패턴은 StreamBuilder를 사용하는 프로덕션 코드의 표준입니다.

결론: 처음에 빈 화면을 방지하기 위해 initialData를 사용하고, 사용자에게 오류를 올바르게 표시하기 위해 항상 hasError를 처리하세요.

StreamBuilder 사용 시 일반적인 실수

실수 1: 부모가 재구성될 때마다 새 Stream 생성. Stream이 빌드마다 새 객체를 생성하는 표현식을 통해 전달되면, StreamBuilder는 이전 스트림에서 구독을 해지하고 새 스트림을 구독하여 무한 재구성 루프가 발생합니다. 해결책: remembered 변수 또는 고정 Stream이 있는 StatefulWidget을 사용하세요.

실수 2: 오류 처리 부재. Stream은 controller.sink.addError를 통해 오류를 발생시킬 수 있으며, builder가 snapshot.hasError를 확인하지 않으면 사용자가 빈 화면이나 무한 로딩을 보게 됩니다. 해결책: 항상 hasError를 확인하고 명확한 메시지를 표시하세요.

실수 3: 닫히지 않은 StreamController로 인한 메모리 누수. 컨트롤러가 dispose에서 닫히지 않으면 스트림이 계속 존재하고 GC가 메모리를 해제하지 않습니다. 해결책: dispose에서 controller.close()를 호출하고 마무리 작업을 위해 done 이벤트를 수신하세요.

실수 4: 느린 builder 함수와 StreamBuilder 사용. builder가 모든 스트림 이벤트에서 호출되므로 내부의 무거운 계산은 프레임 드롭을 유발합니다. 해결책: 계산을 별도의 isolate로 이동하거나 데이터 변환에 Stream.map을 사용하세요.

결론: StreamBuilder는 강력하지만 까다로운 도구입니다. Stream 라이프사이클을 모니터링하고, 오류를 처리하며, builder에서 무거운 연산을 피하세요.

자주 묻는 질문

StreamBuilder와 FutureBuilder의 차이점은 무엇인가요?

FutureBuilder는 단일 비동기 결과를 위해 설계되었습니다: Future를 구독하고, 하나의 값을 받고, 종료됩니다. StreamBuilder는 시간에 따라 여러 값을 발생시킬 수 있는 Stream을 구독하고, 새 이벤트마다 UI를 재구성합니다.

StreamBuilder에서 AsyncSnapshot이란 무엇인가요?

AsyncSnapshot은 현재 구독 상태(connectionState), 마지막으로 수신된 값(data), 그리고 스트림이 예외를 발생시킨 경우 오류 객체(error)를 포함하는 불변 객체입니다.

StreamBuilder에서 오류를 처리하는 방법은?

오류는 builder 함수의 snapshot.hasError 및 snapshot.error 속성을 통해 처리됩니다. 스트림이 sink.addError를 통해 오류를 발생시키면 AsyncSnapshot이 오류를 받고, builder는 적절한 메시지 또는 폴백 UI를 표시해야 합니다.

하나의 Stream을 여러 StreamBuilder에서 사용할 수 있나요?

, Stream이 broadcast인 경우 가능합니다(StreamController.broadcast를 통해 생성). Single-subscription Stream은 구독자 한 명만 허용합니다. 여러 위젯 간에 하나의 스트림을 공유하려면 broadcast 컨트롤러 또는 BehaviourSubject가 포함된 rxdart 패키지를 사용하세요.

모든 이벤트에서 StreamBuilder 재구성을 방지하는 방법은?

UI 재구성을 트리거할 이벤트를 필터링하려면 buildWhen 매개변수를 사용하세요. 또한 StreamBuilder에 데이터를 전달하기 전에 필터링하기 위해 Stream.transformer 또는 Stream.where를 적용하세요.

요약

  • StreamBuilder — 비동기 데이터 스트림에서 리액티브 UI 구축을 위한 위젯, 지속적인 인터페이스 업데이트 지원
  • AsyncSnapshot에는 connectionState(none, waiting, active, done), data, error가 포함됨 — 모든 스트림 상태
  • StreamController는 스트림 라이프사이클 관리: 데이터 추가, 오류 처리, 스트림 종료
  • Broadcast Stream은 여러 StreamBuilder가 하나의 스트림을 구독할 수 있게 함, single-subscription은 하나만
  • 오류 처리는 필수: hasError 확인 없이 앱이 로딩 상태에 멈출 수 있음
  • 메모리 누수가 가장 일반적인 문제: 항상 dispose에서 StreamController를 닫을 것
  • 권장사항: 항상 initialData를 지정하고 원활한 UX를 위해 네 가지 connectionState 값을 모두 처리할 것

턴키 방식의 모바일 애플리케이션을 개발해 드립니다

IT Sectr는 2017년부터 스타트업과 기업을 위한 iOS 및 Android 애플리케이션을 만듭니다. 저희가 상담해 드리고 최적의 솔루션을 제안하겠습니다.

프로젝트 논의

더 읽어보기