문서 목차
이 페이지에서목차

/cc-inspector:bloc — 화면 상태 들여다보기

실행 중인 앱이 기록해 둔 BLoC/Cubit 이벤트·상태변화 로그를 꺼내 보여줍니다 — 이름별 필터·통계·초기화. 화면은 그대로인데 상태가 왜 안 바뀌는지 추적할 때.

항목내용
실행 명령/cc-inspector:bloc
분류개발
난이도●○○ 간단
MCP 서버flutter-inspector

한마디로#

앱이 지금 화면 안에서 "어떤 상태"인지(예: 로그인됨, 장바구니 비어 있음)를 실시간으로 들여다보는 돋보기입니다. 자동차 계기판처럼, 겉으로 보이는 화면 뒤에서 무슨 값이 켜지고 꺼졌는지 그대로 보여줍니다.

누가·언제 쓰나요#

  • 개발자나 QA 담당자가 앱이 실제로 돌아가는 중에 화면 동작을 점검할 때 사용합니다.
  • 특히 이런 상황에서 씁니다:
    • 버튼을 눌렀는데 화면이 바뀌지 않을 때 (상태가 안 바뀜)
    • 예상과 다른 화면이 떠 있을 때 (이상한 상태)
    • 화면이 깜빡이거나 불필요하게 자꾸 다시 그려질 때

무엇을 해주나요#

앱이 돌아가는 동안 기록해 둔 화면 상태 부품(BLoC/Cubit)의 이벤트·상태변화 로그를 꺼내서 보여줍니다.

  • 기록된 로그 전체 (개수 제한·종류 필터 가능)
  • 특정 부품 이름으로 걸러 본 로그 (예: HomeBloc 것만)
  • 통계 (어떤 부품이 몇 건이나 기록됐는지)
  • 기록 초기화 (재현 직전에 비우고 다시 재현하면 읽기 쉽습니다)

⚠️ "지금 살아 있는 부품 목록" 이나 "현재 상태 값" 을 직접 물어보는 기능은 없습니다. 관측자가 지나간 일을 쌓아 둘 뿐이라, 현재 값은 마지막 상태변화 로그에서 읽어야 합니다.

어떻게 쓰나요#

# 기록된 로그 전체 보기 (최근 것부터)
bloc_get_all_logs

# 특정 부품(HomeBloc)의 로그만 보기
bloc_get_by_name  blocName=HomeBloc

# 어떤 부품이 몇 건이나 기록됐는지 통계
bloc_get_stats

# 재현 직전에 기록 비우기
bloc_clear

bloc_get_all_logs 는 전체, bloc_get_by_name 은 이름으로 거른 것, bloc_get_stats 는 집계, bloc_clear 는 초기화입니다.

안에서 무슨 일이 벌어지나요#

문제 유형별로 정해진 순서대로 점검합니다.

  • 화면이 안 바뀔 때: 그 부품 로그가 아예 있는지 → 동작(이벤트)이 기록됐는지 → 상태변화가 뒤따랐는지 순서로 따라가며 어디서 끊겼는지 찾습니다.
  • 이상한 화면일 때: 마지막 상태변화 로그로 현재 값을 읽고, 그 앞의 기록을 되짚어 어떻게 그 상태까지 왔는지 봅니다.
  • 화면이 자꾸 다시 그려질 때: 통계로 동작이 너무 잦은지 보고, 다시 그리는 조건이 올바른지 살펴봅니다.

⚙️ 상세 옵션·실행 명세 (개발자 / AI 에이전트용)

Triggers#

  • State change tracking
  • BLoC event debugging
  • State inconsistency issues

MCP Tools#

⚠️ 등록된 도구는 아래 4종뿐이다 (package/mcp_inspector/lib/src/inspectors/ bloc_inspector.dart 의 registerBlocInspector()). 관측자는 MCPBlocObserver extends BlocObserver 라 onCreate/onEvent/onChange/onTransition/onError/onClose 를 상한 있는 Queue 에 적재할 뿐이므로, "현재 살아 있는 인스턴스 목록" 이나 "현재 상태값" 을 직접 돌려주는 도구는 존재하지 않는다. 현재 값은 마지막 onChange 로그에서 읽는다.

bloc_get_all_logs#

Returns recorded observer logs. Optional limit (max entries) and type (create / event / change / transition / error / close) filters.

Response shape:

{
  "logs": [
    {"type": "event", "blocName": "HomeBloc", "event": "LoadItems", "timestamp": "..."},
    {"type": "change", "blocName": "HomeBloc", "currentState": "...", "nextState": "..."}
  ],
  "totalLogs": 412,
  "returnedLogs": 2
}

bloc_get_by_name#

Returns logs for one container. blocName is required.

bloc_get_stats#

Returns per-container log counts and the observer's buffer occupancy.

bloc_clear#

Clears the recorded logs. Run it right before reproducing so the buffer holds only the repro.

Common Diagnostics#

State not updating#

1. bloc_clear                          -> Empty the buffer, then reproduce
2. bloc_get_by_name blocName=HomeBloc  -> No logs at all? See the bloc_signals caveat below
3. ... filter type=event               -> Was the event dispatched?
4. ... filter type=change              -> Did a state change follow it?

Unexpected state#

1. bloc_get_by_name blocName=HomeBloc  -> Last `change` entry is the current value
2. Walk the preceding entries          -> How did it get there?
3. Review event handler logic

Excessive BLoC rebuilds#

1. bloc_get_stats                      -> Which container dominates the buffer?
2. bloc_get_by_name + type=event       -> Event frequency
3. Review buildWhen conditions

Stale data resurrecting in state (zombie subscription)#

이미 떠난 화면·지운 항목의 데이터가 상태에 되살아나면, 무엇이 바뀌었는지가 아니라 누가 emit 했는지부터 찾는다. 이 문서가 위에 나열한 조회 도구로는 나오지 않는다 — bloc_signals 컨테이너(BlocSignal/CubitSignal)는 Bloc.observer 로 배선된 인스펙터 관측자(BlocObserver)의 대상이 아니라 로그가 아예 남지 않는다.

1. bloc_get_by_name -> 로그가 비어 있으면 bloc_signals 컨테이너다 (정상 — 2번부터 시작)
2. onChange + StackTrace.current 계측 -> 범위 밖 항목을 emit 한 '리스너' 까지만 특정된다
3. package/core 의 SwrBlocSignalMixin.emitSwrStream 에 구독 id·세대 로그 추가
   (등록 맵 _swrSubscriptions 가 private 이라 core 를 고쳐야 한다) -> '누가 열었나'
4. fmt_hot_restart_flutter 로 재현 -> 계측 이후에 생성된 구독만 id 를 갖는다
5. 그제야 로그를 읽어 **원장으로** 판정 -> `listen()` 수 − `cancel()` 수 − 등록 맵의 1 > 0
// 임시 계측 — 범위 밖 데이터를 상태에 넣는 코드를 스택으로 특정한다. 끝나면 지운다.
// ⚠️ 이 리포의 bloc_signals 소비 파일은 대개 `hide Change` 로 import 한다 — core 가
//    package:bloc 의 동명 `Change` 를 재수출해 ambiguous 이기 때문이다. 그 파일에
//    그대로 붙여넣으면 `Change` 가 bloc 쪽으로 해석돼 invalid_override 가 난다.
//    계측용 prefixed import 를 한 줄 더 얹는다.
import 'package:bloc_signals/bloc_signals.dart' as bs;

@override
void onChange(bs.Change<MyState> change) {
  super.onChange(change); // @mustCallSuper
  // 아래 술어는 각 BLoC 의 '현재 범위' 판정으로 바꿔 쓴다.
  final stale = change.nextState.items.where((it) => !_isInCurrentScope(it));
  if (stale.isNotEmpty) {
    Log.d('stale emit ${stale.length}건', stackTrace: StackTrace.current);
  }
}

⚠️ 핫 리로드로는 판정할 수 없다 — 좀비 구독은 계측을 넣기 전에 이미 열려 있어 구독 생성 시점 계측(구독 id 부여)을 거치지 않았다. 그래서 "로그 없는 emit" 으로 보인다. 핫 리스타트로 다시 재현해야 id 가 붙는다.

⚠️ 이벤트 이력만으로는 판정되지 않는다 — 겹침 자체는 같은 키의 호출 2회로 생기고 (실사고에서는 pageChanged 2건), 좀비가 생긴 뒤의 잔존 emit 에는 대응하는 이벤트가 아예 없다. 이벤트 수와 emit 수가 어긋나는 것이 신호다. bloc_signals 의 restartable() 도 superseded 핸들러의 emission 만 버릴 뿐 그 핸들러가 연 구독은 취소하지 않는다.

실사고: unibook #13475 — iPad 가로 양면 보기에서 페이지를 넘겨도 이전 스프레드의 노트가 남았다. emitSwrStream 이 이전 구독을 await cancel() 하는 동안 같은 키의 호출이 겹쳐 각자 구독을 열었고, 나중에 등록한 쪽이 앞의 것을 취소 없이 덮었다. 수정은 PR #13478 (키별 세대 토큰).

⚠️ 당시 관측 서명이던 registered=false 는 PR #13478 이전의 것이다. 현행 트리는 등록 직전에 세대를 재확인하고 남아 있는 다른 구독을 먼저 취소하므로 그 신호가 지속되지 않는다 — 겹침이 실제로 일어났다는 신호는 ② 세대 가드로 물러난 횟수(_isLatestSwrGeneration 분기) 하나뿐이다.

⚠️ "키당 살아 있는 구독 수 > 1" 로는 판정할 수 없다. 등록 맵이 Map<Object, StreamSubscription<Object?>> 라 키당 값이 하나뿐이어서, 맵만 보면 좀비는 원리적으로 보이지 않는다. 좀비의 정의는 맵 밖에 cancel 되지 않은 채 남은 구독 이고, 그것은 위 절차 3번이 만드는 원장(listen() 호출 수 − cancel() 호출 수 − 등록 맵의 1) 으로만 읽힌다.

정본 mixin 을 그대로 이식해 겹침 300회 × 키 1개로 계측하면 ② 물러남 600, ③ 등록 직전 재확인 0, 등록 시 non-identical 취소 0, onDone 의 non-identical 0, 원장상 좀비 0 이다 — ③ 이하는 #13478 이후의 방어층이라 순수 겹침만으로는 발화하지 않는 것이 정상이다.

Examples#

Read the whole buffer#

bloc_get_all_logs

Read one container's logs#

bloc_get_by_name  blocName=HomeBloc

See which container dominates#

bloc_get_stats

Clear before reproducing#

bloc_clear

References#

  • Detailed implementation: .claude/agents/flutter-inspector-bloc.md
  • Master inspector: .claude/commands/inspector.md
  • SWR stream 구독 불변식: cc-flutter:swr-pattern §9 · 정본 코드 package/core/lib/src/domain/usecase/mixins/swr_bloc_signal_mixin.dart

검색어를 입력하면 문서를 찾습니다.

↑ ↓ 이동 · Enter 열기Esc 닫기