<번역> Mastering Scrollable in Flutter

2025. 2. 20. 09:06·Flutter/하루 한 입 플러터

출처: 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");

이제 화면을 스크롤하며 데이터를 확인해봅시다.

  1. Android 기기에서 실행하면 scrollDelta와 dragDetails.y 값이 거의 동일하지만 부호가 반대인 것을 볼 수 있습니다.
  2. iPhone에서 실행하면 dragDetails 값은 비슷하지만, scrollDelta 값이 다릅니다.

이 차이는 Flutter에서 기본적으로 iOS에는 BouncingScrollPhysics,

Android에는 ClampingScrollPhysics가 적용되기 때문입니다.


5. ScrollPhysics(스크롤 물리 효과)는 어떻게 동작할까?

동작 원리:

  1. 드래그 정보를 받고, 스크롤 이동 거리를 계산함
  2. 물리 시뮬레이션을 통해 이동 값을 조정
  3. 필요하면 스크롤 한계를 적용
  4. 최종 스크롤 위치 및 속도를 결정
  5. 계산된 값을 알림(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
'Flutter/하루 한 입 플러터' 카테고리의 다른 글
  • <번역/정리> 11가지 플러터 람다 기법으로 코드가 200% 깔끔해지는 방법
복복씨
복복씨
개발자여, 사고하라 !
  • 복복씨
    정리노트
    복복씨
  • 전체
    오늘
    어제
    • 분류 전체보기 (118)
      • 개발새발자 (21)
        • 의 삶 (7)
        • 의 회고 (9)
        • 의 낙서장 (5)
        • 영어 (0)
      • FrontEnd (1)
        • React (1)
      • Flutter (38)
        • 새싹 (5)
        • Dart (8)
        • Flutter (14)
        • iOS 에서 Flutter 로 전환하며 (2)
        • 챗지피티랑놀.기 (3)
        • 하루 한 입 플러터 (2)
      • CS (7)
        • 짤막지식 (6)
      • IOS (6)
        • Swift (0)
        • UIKit (1)
        • SwitUI (4)
      • 머신러닝-딥러닝 (28)
        • 논문리뷰 (3)
        • study (16)
        • Kaggle (9)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    플러터
    사그널링서버
    flutter 애니메이션
    플러터 di
    FLUTTER
    futurerecord2
    부트캠프
    핫 리로드
    expando
    Flutter Lifecycle
    initState()
    용산캠
    새싹 플러터
    새싹 용산
    한주 회고
    테킷 앱스쿨
    코드 결합도
    멋쟁이 사자처럼
    dart
    unawaited
    swiftui 플러터
    깊은참조
    시그널링데이터
    flutter lottie
    getit
    새싹
    schedulemicrotask
    runzonedguarded
    IOS
    플러터 새싹
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.0
복복씨
<번역> Mastering Scrollable in Flutter
상단으로

티스토리툴바