AppBar에 메뉴 아이콘을 그렸다는 사실과 Drawer가 열린다는 사실은 다릅니다. Drawer를 닫는 코드만 넣은 메뉴도 화면을 바꾸지는 않습니다. 이 글은 Scaffold에 상단 바와 옆 메뉴를 연결한 뒤, 버튼 동작·context 위치·본문 교체·상세 화면 이동을 각각 확인하는 예제를 다룹니다.
2026년 10월 9일 Windows의 Flutter 3.47.5·Dart 3.13.4 격리 프로젝트에서 네 위젯 테스트를 실행했습니다. flutter analyze는 오류 없이 끝났고 네 테스트가 통과했습니다. 빈 콜백, 잘못된 context, 같은 화면에서 본문 교체, 새 페이지에서 뒤로가기를 따로 확인했으며 실기기 캡처나 Material 3 NavigationDrawer 테스트는 아닙니다.
leading을 비워 둔 기본 연결부터 확인
Scaffold(
appBar: AppBar(title: const Text('Home')),
drawer: const Drawer(child: Text('Menu')),
body: const Text('home-body'),
)
이번 테스트에서는 기본 AppBar에 DrawerButton이 한 개 나타났습니다. 버튼을 누르고 애니메이션이 끝난 뒤 ScaffoldState.isDrawerOpen이 true가 됐습니다. 메뉴 선택 후에는 닫힌 상태까지 검사했습니다. 버튼의 모양만 찾거나 Drawer 위젯이 트리에 있다는 사실만 확인하면 실제 열림을 놓칠 수 있습니다.
leading이 null이면 automaticallyImplyLeading이 자동 버튼 추론을 제어합니다. false로 바꾼 테스트에서는 자동 DrawerButton이 없었지만 Scaffold에는 Drawer가 그대로 있었고 openDrawer를 직접 호출하면 열렸습니다. 이 설정은 Drawer 자체를 없애거나 기능을 비활성화하는 옵션이 아닙니다.

직접 만든 메뉴 아이콘이 반응하지 않는 두 원인
| 테스트 조건 | 실행 관찰 | 수정 방향 |
|---|---|---|
| leading 지정, onPressed는 빈 함수 | 아이콘을 눌러도 isDrawerOpen=false | 자동 동작을 기대하지 말고 실제 openDrawer 연결 |
| Scaffold를 반환하는 build의 context 사용 | FlutterError | Scaffold 아래의 context 사용 |
| leading 안 Builder의 context 사용 | Drawer가 열리고 추가 예외 없음 | 같은 조건으로 정상 동작 재검증 |
기존처럼 IconButton(onPressed: () {}, icon: Icon(Icons.menu))를 넣으면 버튼은 보이지만 콜백이 아무 일도 하지 않습니다. non-null leading을 직접 주면 automaticallyImplyLeading 옵션이 그 버튼의 행동을 만들어 주지 않습니다. 장식용 예제를 실제 메뉴 동작처럼 읽지 않도록 콜백까지 포함합니다.
두 번째 실패에서는 현재 State의 build(context)가 새 Scaffold를 반환하는 구조였습니다. 그 context의 조상에는 반환할 Scaffold가 아직 포함되지 않으므로 Scaffold.of(context)가 찾지 못했습니다. 실제 오류의 첫 줄은 Scaffold.of() called with a context that does not contain a Scaffold.였습니다. 테스트는 의도한 오류를 수집한 뒤 Builder 버전에서 예외가 없는지 확인했습니다.
leading: Builder(
builder: (inner) => IconButton(
tooltip: 'Open menu',
icon: const Icon(Icons.menu),
onPressed: () => Scaffold.of(inner).openDrawer(),
),
),
Builder는 UI를 꾸미는 용도가 아니라 해당 위치의 하위 BuildContext를 얻는 데 사용했습니다. context라는 변수명을 inner로 바꾼다고 해결되는 것이 아니라 실제 트리 위치가 달라져야 합니다. 중첩 Scaffold에서는 가장 가까운 조상 Scaffold가 선택되므로 “찾기만 하면 올바른 메뉴다”라고 가정하지 않습니다.
최상위 메뉴는 본문 교체인지 새 route인지 먼저 정한다
저장 목록 같은 최상위 목적지로 바꿀 때는 이번 예제에서 선택 인덱스를 State 필드에 저장하고 본문을 교체했습니다. 메뉴를 누른 뒤 selected=1, saved-body 표시, Drawer 닫힘을 확인했습니다. Navigator.canPop은 false여서 새 상세 route가 쌓이지 않았습니다. 단순히 Navigator.pop만 호출한 기존 예제와 달리 실제 내용이 바뀌는지를 검사했습니다.
// Drawer 아래 Builder에서 받은 drawerContext
onTap: () {
Scaffold.of(drawerContext).closeDrawer();
setState(() => selected = 1);
},
closeDrawer는 메뉴 패널 닫기, setState는 선택 상태와 본문 갱신이라는 별도 역할입니다. 선택 항목 강조, AppBar 제목, body가 같은 상태를 사용하도록 합니다. 화면마다 페이지를 계속 push하면 최상위 메뉴를 몇 번 고른 뒤 뒤로가기가 메뉴 선택 이력을 따라가는 다른 UX가 됩니다. 반대로 사용자가 이전 상세 화면으로 돌아가야 한다면 단순 인덱스 교체는 그 요구를 만족하지 않습니다.
상세 화면은 Drawer를 닫고 push한 뒤 돌아오는 것까지 테스트
onTap: () {
final navigator = Navigator.of(drawerContext);
Scaffold.of(drawerContext).closeDrawer();
navigator.push(MaterialPageRoute<void>(
builder: (_) => Scaffold(
appBar: AppBar(title: const Text('Detail')),
body: const Text('detail-body'),
),
));
},
이 테스트는 하나의 MaterialApp Navigator만 사용했습니다. 상세 body와 BackButton이 나타났고, 뒤로가기 버튼을 누르면 home-body가 다시 보였습니다. 돌아온 Scaffold의 isDrawerOpen은 false였습니다. 열린 메뉴 위에서 새 route만 push해 뒤로갔을 때 이전 메뉴가 다시 드러나는 상태를 막고 싶은 경우 이 확인이 필요합니다.
Navigator 참조는 메뉴 context에서 먼저 얻고, 그 뒤 메뉴를 닫았습니다. 이 코드에는 await가 없으며 화면 전환 전에 서버 요청을 기다리는 코드는 테스트하지 않았습니다. 비동기 작업 뒤 context를 쓰는 경우에는 mounted와 취소 조건을 별도로 검토합니다. 중첩 Navigator나 선언형 라우터를 쓰면 어느 stack을 대상으로 이동할지도 명확히 해야 합니다.
버튼·패널·내용·뒤로가기 네 가지 합격선을 남긴다
| 관찰 지점 | 이번 합격 결과 | 빠뜨리기 쉬운 실패 |
|---|---|---|
| 메뉴 버튼 | 기본 버튼 1개, 직접 버튼은 콜백 연결 | 아이콘은 있지만 빈 함수 |
| 패널 상태 | 열기 true, 선택 후 false | 다른 Scaffold의 메뉴 제어 |
| 목적지 | 저장 본문 또는 상세 body 확인 | 닫기만 하고 실제 이동 없음 |
| 되돌아오기 | 홈 복귀와 닫힌 Drawer 확인 | 메뉴 선택마다 route 중복 적재 |
테스트에서는 pumpAndSettle 뒤 상태를 읽어 애니메이션 도중 값과 최종 값을 구분했습니다. 탭 직후 스크린샷 한 장만으로 닫힘을 판단하지 않습니다. 반복 탭 방지·키보드 및 스크린리더 접근성·폭이 좁은 화면·긴 메뉴 스크롤은 이번 네 테스트에 포함하지 않았고 실제 앱의 추가 합격 조건입니다. 메뉴 버튼에는 의미 있는 tooltip을 제공하고 접근성 검증을 별도로 수행합니다.
재현 범위와 Material 3 적용 경계
flutter analyze
flutter test --reporter expanded
// No issues found
// 4 tests passed: automatic, hidden/inert, context, push/back
기본 Drawer 동작을 검증했다고 NavigationDrawer의 선택 상태와 목적지 처리까지 같다고 보지 않습니다. 공식 Drawer 문서는 Material 3 구성에서 NavigationDrawer를 선호한다고 안내하지만, 전환 시에는 목적지 이벤트와 selectedIndex 처리를 해당 API로 다시 검증해야 합니다. 이번 결과는 Flutter 3.47.5 위젯 테스트와 한 Navigator 구조에만 해당하며 실기기·endDrawer·중첩 stack·상태 복원을 실행하지 않았습니다.
본문 선택값을 어디에 저장해야 하는지는 State 유지와 재빌드 실험에서 이어집니다. 하단 탭 UI와는 메뉴 열기·닫기 UX가 다르므로 이 글의 route 검증을 그대로 복사하지 않습니다.
실험 동작은 2026년 10월 9일 자동 leading 조건, Scaffold.of의 context 탐색, closeDrawer, Drawer와 NavigationDrawer 차이 공식 API와 대조했습니다.