출처: https://medium.com/@pomis172/mastering-scrollable-in-flutter-4cbc5f42420e
Flutter에서 Scrollable 완벽 이해하기
저자: Roman Ismagilov
2025년 1월 12일
Scrollable은 ListView, CustomScrollView, SingleChildScrollView 등과 같은 여러 주요 위젯들의 슈퍼클래스(superclass)입니다. 이 글에서는 Scrollable이 내부적으로 어떻게 동작하는지 알아보겠습니다.
1. Notification(알림)이란?
Flutter에서는 스크롤, 크기 변경, 레이아웃 변경과 같은 이벤트가 발생할 때 알림(Notification)을 위젯 트리의 상위 위젯으로 보낼 수 있습니다. 즉, 무언가가 스크롤되거나 크기가 바뀌면, 상위 위젯들이 해당 정보를 알게 됩니다.
예제 코드: ScrollUpdateNotification 감지하기
NotificationListener<ScrollUpdateNotification>(
onNotification: (notification) {
return false; // 디버거 중단점 설정 가능
},
child: ListView(
children: [
const SizedBox(height: 1000),
],
),
)
위 코드를 보면 스크롤 이벤트가 발생할 때마다 알림이 전송됩니다.
scrollDelta란?
scrollDelta는 이전 상태와 비교하여 스크롤된 픽셀 수를 의미합니다.- 양수이면 사용자가 스크롤을 아래(또는 기본 방향)로 움직였다는 뜻이고,
- 음수이면 반대 방향으로 스크롤했다는 뜻입니다.
2. metrics 속성 살펴보기
notification.metrics 내부에는 유용한 데이터들이 많습니다.
이를 시각적으로 정리하면 다음과 같습니다.
| 속성명 | 설명 |
|---|---|
extentTotal |
Scrollable 콘텐츠의 총 높이 |
maxScrollExtent |
현재 뷰포트에 맞지 않는 초과 콘텐츠의 높이 |
scrollDelta |
이전 상태에서 변화된 스크롤 거리 |
extentBefore |
현재 스크롤 위치 이전에 남은 높이 |
extentAfter |
현재 스크롤 위치 이후에 남은 높이 |
viewPortDimension |
Scrollable이 포함된 위젯의 높이 |
3. 콘텐츠가 뷰포트보다 작으면?
예제에서 SizedBox(height: 1000)를 200으로 변경해 봅시다.
그러면 스크롤할 필요가 없으므로 알림이 전송되지 않습니다.
하지만 우리가 스크롤 정보를 얻고 싶다면 어떻게 해야 할까요?
ScrollController 사용하기
class _ScrollableExampleState extends State<ScrollableExample> {
final _controller = ScrollController(); // <-- ScrollController 추가
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
_controller.position; // <-- 디버그 중단점 설정 가능
});
}
...
ListView(
controller: _controller, // <-- 컨트롤러 연결
...
)
}
이제 maxScrollExtent 값이 0이 됩니다. (스크롤할 필요가 없기 때문)
4. DragDetails란?
알림(Notification) 객체 내부를 살펴보면 dragDetails 속성이 있습니다.
이는 GestureDetector에서 사용하는 DragUpdateDetails와 유사합니다.
print("$scrollDelta\t$dragDetails");
이제 화면을 스크롤하며 데이터를 확인해봅시다.
- Android 기기에서 실행하면
scrollDelta와dragDetails.y값이 거의 동일하지만 부호가 반대인 것을 볼 수 있습니다. - iPhone에서 실행하면
dragDetails값은 비슷하지만,scrollDelta값이 다릅니다.
이 차이는 Flutter에서 기본적으로 iOS에는 BouncingScrollPhysics,
Android에는 ClampingScrollPhysics가 적용되기 때문입니다.
5. ScrollPhysics(스크롤 물리 효과)는 어떻게 동작할까?
동작 원리:
- 드래그 정보를 받고, 스크롤 이동 거리를 계산함
- 물리 시뮬레이션을 통해 이동 값을 조정
- 필요하면 스크롤 한계를 적용
- 최종 스크롤 위치 및 속도를 결정
- 계산된 값을 알림(Notification)으로 리스너에게 전달
커스텀 ScrollPhysics 적용 예제
BouncingScrollPhysics(parent: AlwaysScrollableScrollPhysics())
- 먼저
BouncingScrollPhysics가 적용된 후, AlwaysScrollableScrollPhysics가 추가 적용됩니다.
6. ScrollExtent(총 스크롤 길이)는 항상 알 수 있을까?
아니요.
ListView.builder를 사용하면 아이템의 크기가 동적으로 변할 수 있기 때문입니다.
예를 들어, Text 위젯이 1줄이거나 2줄일 수 있는 경우, 전체 높이를 미리 계산할 수 없습니다.
이런 경우, Flutter는 현재 뷰포트에 보이는 아이템 + cacheExtent 만큼만 로드합니다.
(cacheExtent는 화면 밖에서도 일부 데이터를 미리 로드하는 기능)
7. 왜 Scrollable 내부에서 Spacer 또는 Flexible을 사용할 수 없을까?
SingleChildScrollView(
child: Column(
children: [
Text("Content"),
const Spacer(), // <- 이렇게 하면 안 됨
ElevatedButton(onPressed: () {}, child: Text("Button"))
],
),
)
문제점:
Spacer는 가능한 한 최대 공간을 차지하려고 함SingleChildScrollView는 자식들이 필요한 만큼 공간을 가질 수 있도록 함- 서로 충돌이 발생!
해결 방법: LayoutBuilder + ConstrainedBox + IntrinsicHeight
LayoutBuilder(builder: (context, constraints) {
return SingleChildScrollView(
child: ConstrainedBox(
constraints: BoxConstraints(
minHeight: constraints.maxHeight,
maxHeight: double.infinity,
),
child: IntrinsicHeight(
child: Column(
children: [
Text("Content"),
const Spacer(),
ElevatedButton(onPressed: () {}, child: Text("Button"))
],
),
),
),
);
})
이 코드의 역할:
LayoutBuilder: 부모 위젯의 크기 정보를 가져옴SingleChildScrollView: 스크롤을 가능하게 함ConstrainedBox: 최소 높이 = 부모 높이 설정IntrinsicHeight:Column이 올바른 높이로 계산되도록 함
이렇게 하면 Spacer가 Scrollable 안에서도 정상적으로 작동합니다.
8. Scrollable과 Transform 같이 사용하기
스크롤 시 AppBar의 버튼을 애니메이션 효과로 변화시키는 방법을 구현해봅시다.
1) ValueNotifier로 스크롤 위치 저장
class _ScrollableZoomerState extends State<ScrollableZoomer> {
ValueNotifier<double> scrollPosition = ValueNotifier(0.0);
}
- 0.0 = 처음 위치
- 1.0 = 스크롤 끝까지 이동
2) 스크롤 이벤트 감지
@override
Widget build(BuildContext context) {
return Scaffold(
body: NotificationListener<ScrollUpdateNotification>(
onNotification: (notification) {
scrollPosition.value = min(1, notification.metrics.pixels /
notification.metrics.maxScrollExtent);
return true;
},
child: widget.child,
),
);
}
3) 버튼 애니메이션 적용
appBar: AppBar(
actions: [
ValueListenableBuilder(
valueListenable: scrollPosition,
builder: (context, value, _) => Opacity(
opacity: value > 0.1 ? value : 0,
child: Transform.translate(
offset: Offset(0, 40 * (1 - value)),
child: TextButton(
onPressed: () {},
child: Text("Next"),
),
),
),
)
],
),
- 스크롤 정도에 따라 버튼의
Opacity(투명도)와 위치가 변경됨
마무리
이 글이 Flutter의 Scrollable 개념을 이해하는 데 도움이 되었길 바랍니다! 🚀
더 많은 정보가 있으면 업데이트할 예정이니, Twitter를 팔로우해주세요!
'Flutter > 하루 한 입 플러터' 카테고리의 다른 글
| <번역/정리> 11가지 플러터 람다 기법으로 코드가 200% 깔끔해지는 방법 (2) | 2025.05.21 |
|---|