-
post-frame, microtask, animation tick은 같은 큐가 아닙니다AI Agent/flutter 2026. 7. 6. 21:00728x90반응형

setState during build 예외를 쫓던 세 번의 시도 빨간 화면에 "setState() called during build"가 떴습니다.
리스트가 특정 조건에서 자동으로 새로고침되게 만들던 중이었습니다.
build 안에서 조건을 확인하고, 맞으면 provider를 다시 불렀는데, 바로 그 줄에서 예외가 났습니다.메시지 자체는 명확했습니다.
build 도중에 상태를 바꾸지 말라는 거였죠.
그럼 build가 끝난 다음에 부르면 될 텐데, Flutter에서 그 "끝난 다음"을 표현하는 방법이 하나가 아니라는 걸, 저는 그날 두 번 헛디디고 나서야 알았습니다.문제의 코드
상황은 이랬습니다.
어떤 조건이 맞으면 build 도중에 provider를 갱신하려고 했습니다.@override Widget build(BuildContext context) { if (shouldRefresh) { ref.read(dataProvider.notifier).reload(); // build 중에 상태 변경 → 예외 } return buildBody(); }build는 화면을 계산하는 중입니다.
그 계산 도중에 다시 상태를 바꾸면, 방금 그리던 걸 무효로 만드는 셈입니다.
Flutter가 그걸 막습니다.
그래서 "이 build가 끝난 다음에 해"라고 미뤄야 합니다.
문제는 "다음에"의 종류가 하나가 아니라는 거였습니다.첫 시도, microtask로 미뤄봤다
가장 먼저 손이 간 건 Future.microtask였습니다.
"지금 하던 일 끝나면 바로 해줘" 정도로 알고 있었거든요.
그렇게 감쌌더니 예외가 줄긴 했는데, 완전히 사라지진 않았습니다.
특정 흐름에서만 여전히 같은 빨간 화면이 떴습니다.
microtask는 지금 실행 중인 동기 코드가 끝나면 곧바로 실행되는데, build를 유발한 그 흐름이 아직 스택에 남아 있으면 microtask도 그 언저리에서 돌아 버립니다.
프레임이 끝나기를 기다려 주지 않는 거였습니다.
"나중"이긴 한데, 제가 원한 나중은 아니었습니다.두 번째, Duration.zero는 왜 됐을까
그다음 Future.delayed(Duration.zero)를 넣었습니다.
0초 뒤에 실행하라는 거니 프레임 하나쯤은 넘기겠지 싶었죠.
이건 예외가 안 났습니다.
그런데 찜찜했습니다.
Duration.zero는 콜백을 이벤트 큐에 넣는 건데, 이벤트 큐는 "이 프레임이 화면에 다 올라간 뒤"를 정확히 가리키지 않습니다.
그냥 다음 이벤트 루프 차례일 뿐입니다.
대충 프레임 하나 뒤라서 얻어걸린 거지, 프레임 경계에 맞춘 게 아니었습니다.
되긴 되는데 왜 되는지를 말로 설명 못 하는 코드는, 남겨두기가 불안합니다.결국 문서에서 찾은 자리
그래서 문서를 뒤져 addPostFrameCallback을 찾았습니다.
이건 이름 그대로, 지금 그리는 프레임이 화면에 다 올라간 다음 딱 한 번 불립니다.
build가 끝나고 레이아웃과 페인트까지 지나간 시점, 제가 처음부터 원했던 나중이 정확히 이거였습니다.WidgetsBinding.instance.addPostFrameCallback((_) { if (!mounted) return; ref.read(dataProvider.notifier).reload(); });microtask가 프레임을 안 기다린 것과 Duration.zero가 얻어걸린 것을 나란히 놓고 나서야, 셋이 왜 다른지 손에 잡혔습니다.
셋 다 "큐에 넣는" 건 맞는데 큐가 다릅니다.
microtask 큐는 프레임 앞쪽에, 이벤트 큐는 그 사이 어딘가에, post-frame 콜백은 프레임 끝에 붙습니다.
애니메이션을 위한 scheduleFrameCallback은 또 다른 자리인데, 다음 프레임을 그릴 타이밍에 맞춰 불리는 거라 매 프레임 값을 조금씩 바꾸는 애니메이션에나 어울립니다.
한 번 갱신하고 끝날 제 상황에 이걸 썼으면, 프레임만 괜히 하나 더 예약하는 셈이었습니다.post-frame에 일을 쌓았을 때
그런데 addPostFrameCallback도 만능은 아니었습니다.
편하다고 이것저것 다 post-frame으로 밀어 넣었더니, 스크롤할 때 살짝 버벅이는 느낌이 생겼습니다.
프레임을 다 그린 다음에 무거운 일을 몰아서 하니, 그 뒤 프레임이 밀린 겁니다.처음엔 리스트 위젯을 의심했습니다.
그런데 DevTools의 프레임 차트를 보니, 느린 프레임마다 제 post-frame 콜백이 꼬리처럼 붙어 있었습니다.
범인은 리스트가 아니라, 제가 프레임 끝에 몰아둔 잡일이었습니다.
build 예외를 피하려고 넣은 콜백이, 이번엔 프레임을 잡아먹고 있었습니다.post-frame은 "레이아웃이 끝난 크기를 읽어야 하는 일"처럼 프레임 뒤에 꼭 해야 하는 것만 넣는 자리였습니다.
무거운 계산이나 네트워크 호출은 거기 있을 이유가 없었습니다.
그건 그냥 async로 빼서 프레임과 분리하는 게 맞았습니다.지금은 "나중에 해야지" 싶을 때, 그 나중이 어느 나중인지를 먼저 정합니다.
build만 피하면 되는 건지, 프레임이 다 올라온 뒤여야 하는 건지, 매 프레임 갱신인지.
셋을 뭉뚱그려 "일단 미루면 되겠지"로 감싸던 시절보다, 빨간 화면을 확실히 덜 봅니다.그래도 가끔은 microtask랑 post-frame 사이에서 손이 잠깐 멈춥니다.
원문 기준
addPostFrameCallback API 문서: https://api.flutter.dev/flutter/scheduler/SchedulerBinding/addPostFrameCallback.html (확인일 2026-07-01).
세 콜백의 실행 순서를 말로 설명할 때 헷갈리기 쉬워, 올리기 전에 이 페이지로 순서만 다시 대조합니다.728x90반응형'AI Agent > flutter' 카테고리의 다른 글
같은 상태를 두 곳에서 쓰면 둘 중 하나가 거짓말합니다 (0) 2026.07.07 Flutter는 widget을 그리는 게 아니라 element/render tree를 갱신합니다 (1) 2026.07.07 Riverpod 3에서 await 중 ref가 죽는 순간 (0) 2026.07.06 로컬 상태는 서버 상태의 대체물이 아니라 임시 복사본입니다 (0) 2026.07.06 Flutter 라우팅은 화면 이동이 아니라 상태와 권한의 계약입니다 (0) 2026.07.05