플러터(Flutter) 애플리케이션을 개발하다 보면 사용자의 특정 행동에 반응하여 화면을 스크롤해야 하는 경우가 빈번하게 발생합니다. 예를 들어, 긴 فرم(form)에서 유효성 검사에 실패한 입력 필드로 사용자를 안내하거나, 채팅 앱에서 새로운 메시지가 도착했을 때 화면 하단으로 자동으로 스크롤하는 기능, 혹은 사용자가 목차의 항목을 탭했을 때 해당 콘텐츠 영역으로 부드럽게 이동시키는 기능 등이 대표적입니다. 이처럼 프로그래매틱(programmatic) 스크롤은 사용자 경험(UX)을 크게 향상시키는 핵심적인 요소입니다.
플러터는 이러한 요구사항을 해결하기 위해 강력하고 유연한 여러 메커니즘을 제공합니다. 가장 대표적인 두 가지 방법은 GlobalKey
와 Scrollable.ensureVisible()
을 함께 사용하는 방식과, ScrollController
를 이용해 스크롤 위치를 직접 제어하는 방식입니다. 이 두 가지 방법은 각각의 장단점과 적합한 사용 사례를 가지고 있으며, 이를 정확히 이해하고 적용하는 것이 중요합니다.
이 글에서는 이 두 가지 핵심적인 스크롤 제어 기법을 심도 있게 다룹니다. 먼저 GlobalKey
의 본질과 Scrollable.ensureVisible()
메서드의 다양한 옵션을 활용하여 SingleChildScrollView
내의 특정 위젯으로 이동하는 방법을 상세히 알아봅니다. 이후 ListView
와 같이 동적으로 생성되는 긴 목록에서 왜 GlobalKey
방식이 비효율적인지를 살펴보고, 이에 대한 대안으로 ScrollController
를 사용하는 효과적인 방법을 구체적인 예제 코드와 함께 설명합니다. 마지막으로, 실제 프로젝트에서 마주할 수 있는 다양한 시나리오와 잠재적인 문제점, 그리고 해결 방안까지 제시하여 플러터에서 스크롤을 자유자재로 다룰 수 있는 견고한 기반을 마련해 드릴 것입니다.
1. GlobalKey와 Scrollable.ensureVisible: 정적 콘텐츠 스크롤의 기본
화면에 표시되는 위젯의 수가 많지 않고, 그 구조가 동적으로 변하지 않는 정적인 화면(예: 설정 페이지, 회원가입 폼)에서는 GlobalKey
와 Scrollable.ensureVisible()
의 조합이 가장 직관적이고 효과적인 해결책입니다. 이 방식의 핵심은 이동하고자 하는 목표 위젯에 고유한 식별자인 GlobalKey
를 부여하고, 필요할 때 이 키를 사용해 해당 위젯의 위치 정보를 얻어와 화면에 보이도록 스크롤하는 것입니다.
GlobalKey의 이해: 단순한 식별자를 넘어서
플러터에서 'Key'는 위젯을 식별하고, 위젯 트리가 재구성될 때 상태를 보존하는 데 사용되는 중요한 개념입니다. Key에는 여러 종류가 있지만, 프로그래매틱 스크롤과 관련하여 가장 중요한 것은 GlobalKey
입니다.
LocalKey
(예: ValueKey
, ObjectKey
, UniqueKey
)가 동일한 부모 위젯 내에서 형제 위젯들 사이의 식별을 위해 사용되는 반면, GlobalKey
는 이름 그대로 애플리케이션 전체에서 유일한 키입니다. 이 고유성 덕분에 위젯 트리의 어느 위치에서든 특정 GlobalKey
를 가진 위젯의 정보에 접근할 수 있습니다. GlobalKey
는 단순히 위젯을 식별하는 것을 넘어, 해당 위젯과 관련된 중요한 세 가지 정보, 즉 currentWidget
, currentState
, currentContext
에 대한 참조를 제공합니다.
- currentWidget: 현재 위젯 트리에 마운트된 위젯 인스턴스 자체에 접근합니다.
- currentState:
StatefulWidget
의 경우, 그 위젯의State
객체에 접근할 수 있게 해줍니다. 이를 통해 외부에서 해당 위젯의 내부 상태를 변경하거나 메서드를 호출하는 등의 상호작용이 가능합니다. - currentContext: 위젯 트리에서 해당 위젯의 위치와 관련된 정보를 담고 있는
BuildContext
에 접근합니다.Scrollable.ensureVisible()
은 바로 이BuildContext
를 사용하여 위젯의 렌더링된 위치와 크기를 파악합니다.
이러한 특성 때문에 GlobalKey
는 강력하지만, 남용될 경우 애플리케이션의 구조를 복잡하게 만들고 성능에 영향을 줄 수 있으므로 꼭 필요한 경우에만 신중하게 사용해야 합니다.
구현 단계별 상세 가이드
이제 GlobalKey
와 Scrollable.ensureVisible()
을 사용하여 스크롤을 구현하는 과정을 단계별로 살펴보겠습니다.
1단계: GlobalKey 인스턴스 생성
먼저, 스크롤의 대상이 될 위젯을 참조하기 위한 GlobalKey
인스턴스를 생성합니다. 보통 StatefulWidget
의 State
클래스 내에 멤버 변수로 선언합니다.
class _MyScrollPageState extends State<MyScrollPage> {
// 특정 위젯을 참조하기 위한 GlobalKey 생성
final GlobalKey _targetKey = GlobalKey();
// ... 나머지 코드
}
2단계: 목표 위젯에 GlobalKey 할당
생성한 GlobalKey
를 스크롤하고자 하는 목표 위젯의 key
속성에 할당합니다. 이로써 _targetKey
는 해당 Container
위젯과 연결됩니다.
// ... 위젯 빌드 메서드 내부
SingleChildScrollView(
child: Column(
children: [
// ... 다른 위젯들
Container(
key: _targetKey, // 생성한 키를 위젯에 할당
height: 300,
color: Colors.redAccent,
alignment: Alignment.center,
child: Text('여기가 목표 지점입니다!', style: TextStyle(color: Colors.white, fontSize: 20)),
),
// ... 다른 위젯들
],
),
)
3단계: Scrollable.ensureVisible() 호출
마지막으로, 스크롤을 실행할 트리거(예: 버튼 클릭)가 발생했을 때 Scrollable.ensureVisible()
메서드를 호출합니다. 이 메서드는 인자로 받은 BuildContext
에 해당하는 위젯이 뷰포트(viewport, 화면에 보이는 영역)에 완전히 보이도록 스크롤 위치를 조정합니다.
void _scrollToTarget() {
// GlobalKey를 통해 현재 BuildContext에 접근합니다.
final context = _targetKey.currentContext;
// context가 null이 아닌지 확인하는 것이 중요합니다.
// 위젯이 아직 렌더링되지 않았거나 화면에서 사라진 경우 null일 수 있습니다.
if (context != null) {
Scrollable.ensureVisible(context);
}
}
// ...
ElevatedButton(
onPressed: _scrollToTarget,
child: Text('목표 지점으로 스크롤'),
)
Scrollable.ensureVisible()의 고급 옵션 활용하기
Scrollable.ensureVisible()
는 단순히 위젯을 화면에 보이게 하는 것 이상의 세밀한 제어를 위한 여러 옵셔널 파라미터를 제공합니다. 이를 활용하면 훨씬 더 정교하고 부드러운 사용자 경험을 만들 수 있습니다.
duration
: 스크롤 애니메이션이 진행될 시간을 지정합니다. 기본값은Duration.zero
로, 즉시 이동합니다. 부드러운 이동을 원한다면Duration(milliseconds: 500)
과 같이 값을 지정할 수 있습니다.curve
: 스크롤 애니메이션의 속도 변화를 제어하는 곡선입니다.Curves
클래스에 정의된 다양한 프리셋(예:Curves.easeInOut
,Curves.bounceOut
)을 사용하여 다채로운 애니메이션 효과를 줄 수 있습니다.alignment
: 목표 위젯이 뷰포트 내 어느 위치에 표시될지를 결정합니다.0.0
: 뷰포트의 시작 부분(세로 스크롤의 경우 상단)에 위치시킵니다.0.5
: 뷰포트의 중앙에 위치시킵니다. (기본값)1.0
: 뷰포트의 끝 부분(세로 스크롤의 경우 하단)에 위치시킵니다.
이 옵션들을 적용한 예제는 다음과 같습니다.
void _scrollToTargetWithAnimation() {
final context = _targetKey.currentContext;
if (context != null) {
Scrollable.ensureVisible(
context,
duration: const Duration(seconds: 1), // 1초 동안 애니메이션
curve: Curves.easeInOutCubic, // 부드럽게 가속하고 감속하는 커브
alignment: 0.0, // 뷰포트 상단에 맞춤
);
}
}
이처럼 옵션을 조합하면, "버튼을 누르면 1초 동안 부드럽게 스크롤하여 목표 위젯이 화면 상단에 정확히 위치하도록" 하는 복합적인 동작을 손쉽게 구현할 수 있습니다.
전체 예제 코드: GlobalKey 방식
지금까지 설명한 내용을 종합한 전체 예제 코드는 다음과 같습니다.
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Programmatic Scroll Demo',
theme: ThemeData(primarySwatch: Colors.blue),
home: const ScrollDemoScreen(),
);
}
}
class ScrollDemoScreen extends StatefulWidget {
const ScrollDemoScreen({Key? key}) : super(key: key);
@override
State<ScrollDemoScreen> createState() => _ScrollDemoScreenState();
}
class _ScrollDemoScreenState extends State<ScrollDemoScreen> {
final GlobalKey _targetKey = GlobalKey();
// 스크롤을 실행하는 함수
void _scrollToTarget() {
final targetContext = _targetKey.currentContext;
if (targetContext != null) {
Scrollable.ensureVisible(
targetContext,
duration: const Duration(milliseconds: 600),
curve: Curves.easeInOut,
alignment: 0.5, // 뷰포트 중앙에 위치
);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Scroll to Widget with GlobalKey'),
),
body: SingleChildScrollView(
padding: const EdgeInsets.all(16.0),
child: Column(
children: [
const Text(
'아래 버튼을 눌러 특정 위젯으로 스크롤하세요.',
style: TextStyle(fontSize: 18),
textAlign: TextAlign.center,
),
const SizedBox(height: 20),
ElevatedButton(
onPressed: _scrollToTarget,
child: const Text('빨간색 컨테이너로 스크롤'),
),
const SizedBox(height: 400),
Container(
height: 200,
color: Colors.blue[200],
alignment: Alignment.center,
child: const Text('위젯 1', style: TextStyle(fontSize: 24)),
),
const SizedBox(height: 400),
// 스크롤의 목표가 되는 위젯
Container(
key: _targetKey,
height: 200,
color: Colors.red[400],
alignment: Alignment.center,
child: const Text(
'목표 지점!',
style: TextStyle(fontSize: 24, color: Colors.white),
),
),
const SizedBox(height: 400),
Container(
height: 200,
color: Colors.green[200],
alignment: Alignment.center,
child: const Text('위젯 3', style: TextStyle(fontSize: 24)),
),
const SizedBox(height: 400),
],
),
),
);
}
}
이 코드를 실행하고 버튼을 누르면, 화면이 부드럽게 스크롤되어 빨간색 컨테이너가 화면 중앙에 나타나는 것을 확인할 수 있습니다. 이것이 GlobalKey
를 이용한 스크롤 제어의 기본 원리입니다.
2. ScrollController: 동적 목록 스크롤의 전문가
GlobalKey
방식은 SingleChildScrollView
와 같이 모든 자식 위젯을 한 번에 빌드하는 경우에는 훌륭하게 작동합니다. 하지만 ListView.builder
나 GridView.builder
와 같이 화면에 보이지 않는 위젯은 메모리에서 해제하고, 필요할 때 다시 빌드하는 '지연 로딩(lazy loading)' 방식을 사용하는 위젯에서는 심각한 문제를 야기할 수 있습니다.
ListView.builder에서 GlobalKey가 부적합한 이유
수백, 수천 개의 아이템을 가진 리스트를 상상해 봅시다. 만약 각 아이템에 GlobalKey
를 할당하려고 하면 두 가지 큰 문제에 부딪힙니다.
- 키 충돌 문제:
ListView.builder
는 화면을 스크롤할 때 위젯을 재활용합니다. 즉, 화면 밖으로 사라진 위젯 객체를 새로운 데이터를 담아 다시 화면에 표시합니다. 만약 100개의 아이템에 100개의GlobalKey
를 담은 리스트를 만들고 이를itemBuilder
에서 사용한다면, 위젯이 재활용되는 순간 이전에 사용되었던GlobalKey
가 새로운 위젯에 다시 할당되면서 "multiple widgets used the same GlobalKey"라는 치명적인 오류가 발생할 수 있습니다. - 메모리 및 성능 문제:
GlobalKey
는 일반 위젯보다 더 많은 리소스를 소비합니다. 수많은 아이템에 각각GlobalKey
를 할당하는 것은 그 자체로 상당한 메모리 부담을 유발하며, 플러터의 위젯 트리 관리 성능을 저하시킬 수 있습니다.
또한, 아직 빌드되지 않은(화면에 보이지 않는) 위젯은 BuildContext
를 가지고 있지 않으므로, GlobalKey
를 통해 접근하더라도 currentContext
가 null
이 되어 Scrollable.ensureVisible()
을 호출할 수 없습니다.
이러한 문제들을 해결하기 위해 동적 목록 스크롤에는 ScrollController
가 사용됩니다.
ScrollController의 핵심 원리
ScrollController
는 이름 그대로 스크롤 가능한 위젯의 스크롤 상태를 제어하고 감시하는 객체입니다. GlobalKey
가 특정 '위젯'에 대한 참조를 제공한다면, ScrollController
는 '스크롤 위치(offset)'에 대한 직접적인 제어권을 제공합니다.
ScrollController
의 주요 메서드는 다음과 같습니다.
jumpTo(double offset)
: 아무런 애니메이션 없이 즉시 지정된 픽셀 오프셋으로 스크롤 위치를 이동시킵니다.animateTo(double offset, {required Duration duration, required Curve curve})
: 지정된 시간과 애니메이션 커브를 사용하여 부드럽게 목표 오프셋으로 스크롤합니다. 이는Scrollable.ensureVisible
의 애니메이션 기능과 유사합니다.
이를 사용하면 특정 인덱스의 아이템으로 이동하는 로직을 "해당 인덱스의 아이템이 시작되는 픽셀 오프셋을 계산하여 그 위치로 이동"하는 방식으로 구현할 수 있습니다.
구현 단계별 상세 가이드
1단계: ScrollController 인스턴스 생성 및 연결
State
클래스 내에 ScrollController
인스턴스를 생성하고, dispose()
메서드에서 반드시 컨트롤러를 해제하여 메모리 누수를 방지해야 합니다. 그리고 이 컨트롤러를 ListView.builder
의 controller
속성에 연결합니다.
class _MyListPageState extends State<MyListPage> {
final ScrollController _scrollController = ScrollController();
final int _itemCount = 100;
final double _itemHeight = 80.0;
@override
void dispose() {
_scrollController.dispose(); // 컨트롤러를 반드시 dispose해야 합니다.
super.dispose();
}
// ...
@override
Widget build(BuildContext context) {
return Scaffold(
// ...
body: ListView.builder(
controller: _scrollController, // 컨트롤러 연결
itemCount: _itemCount,
itemExtent: _itemHeight, // 아이템 높이가 고정된 경우 itemExtent 사용이 성능에 유리
itemBuilder: (context, index) {
// ...
},
),
);
}
}
2단계: 목표 오프셋 계산 및 스크롤 실행
특정 인덱스로 이동하기 위한 함수를 작성합니다. 가장 간단한 경우는 모든 아이템의 높이가 동일할 때입니다. 이때 목표 오프셋은 `(인덱스) * (아이템 높이)`로 쉽게 계산할 수 있습니다.
void _scrollToIndex(int index) {
// 인덱스가 유효한 범위 내에 있는지 확인
if (index < 0 || index >= _itemCount) return;
// 목표 오프셋 계산
final double targetOffset = index * _itemHeight;
_scrollController.animateTo(
targetOffset,
duration: const Duration(milliseconds: 500),
curve: Curves.easeOut,
);
}
만약 스크롤 가능한 최대 범위를 넘어서는 오프셋을 지정하면, 컨트롤러는 자동으로 스크롤 가능한 최대 위치(리스트의 가장 끝)로 이동시켜 줍니다.
전체 예제 코드: ScrollController 방식
다음은 ScrollController
를 사용하여 ListView.builder
의 특정 인덱스로 스크롤하는 전체 예제입니다.
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'ScrollController Demo',
home: ListScrollScreen(),
);
}
}
class ListScrollScreen extends StatefulWidget {
@override
_ListScrollScreenState createState() => _ListScrollScreenState();
}
class _ListScrollScreenState extends State<ListScrollScreen> {
final ScrollController _scrollController = ScrollController();
final int _itemCount = 100;
final double _itemHeight = 60.0;
final TextEditingController _textController = TextEditingController();
@override
void dispose() {
_scrollController.dispose();
_textController.dispose();
super.dispose();
}
void _scrollToIndex() {
final index = int.tryParse(_textController.text);
if (index != null && index >= 0 && index < _itemCount) {
_scrollController.animateTo(
index * _itemHeight,
duration: const Duration(seconds: 1),
curve: Curves.easeInOutBack,
);
} else {
// 사용자에게 유효하지 않은 입력임을 알림
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('0부터 ${_itemCount - 1} 사이의 숫자를 입력해주세요.'),
duration: const Duration(seconds: 2),
),
);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Scroll to Index with ScrollController'),
),
body: Column(
children: [
Padding(
padding: const EdgeInsets.all(8.0),
child: Row(
children: [
Expanded(
child: TextField(
controller: _textController,
keyboardType: TextInputType.number,
decoration: const InputDecoration(
labelText: '이동할 인덱스 입력',
border: OutlineInputBorder(),
),
),
),
const SizedBox(width: 8),
ElevatedButton(
onPressed: _scrollToIndex,
child: const Text('이동'),
),
],
),
),
Expanded(
child: ListView.builder(
controller: _scrollController,
itemCount: _itemCount,
itemExtent: _itemHeight, // 모든 아이템 높이가 동일함을 명시
itemBuilder: (context, index) {
return Card(
color: index % 2 == 0 ? Colors.grey[200] : Colors.white,
child: ListTile(
leading: CircleAvatar(child: Text('$index')),
title: Text('아이템 #$index'),
),
);
},
),
),
],
),
);
}
}
이 예제는 사용자가 입력 필드에 인덱스를 입력하고 '이동' 버튼을 누르면, 해당 인덱스의 아이템으로 부드럽게 스크롤되는 기능을 보여줍니다. 이는 GlobalKey
를 사용하지 않고도 동적 목록에서 효율적으로 특정 위치로 이동할 수 있음을 증명합니다.
고급 주제: 가변 높이 아이템 스크롤하기
실제 애플리케이션에서는 모든 리스트 아이템의 높이가 동일하지 않은 경우가 많습니다. 이 경우 `(인덱스) * (아이템 높이)`와 같은 단순한 계산은 불가능합니다. 이 문제를 해결하기 위한 몇 가지 접근법이 있습니다.
- 오프셋 미리 계산하기: 리스트 데이터를 로드할 때 각 아이템의 높이를 미리 계산하고, 각 인덱스까지의 누적 오프셋을 저장해두는 방식입니다. 이는 데이터가 많지 않고 아이템의 높이가 내용에 따라 결정될 때 유용할 수 있지만, 복잡하고 부정확할 수 있습니다.
- 패키지 사용하기: 이 복잡한 문제를 해결하기 위해 커뮤니티에서 개발한 훌륭한 패키지들이 있습니다. 대표적으로
scrollable_positioned_list
는 가변 높이를 가진 아이템 리스트에서도 특정 인덱스로 정확하게 스크롤하는 기능을 매우 간단하게 구현할 수 있도록 도와줍니다. 내부적으로 복잡한 계산을 추상화하여 개발자가itemScrollController.scrollTo(index: 50)
와 같이 직관적인 코드를 작성할 수 있게 해줍니다.
3. 올바른 선택: SingleChildScrollView vs. ListView
지금까지 두 가지 주요 스크롤 기법을 살펴보았습니다. 어떤 상황에서 어떤 방법을 선택해야 할까요? 이는 결국 어떤 스크롤 위젯을 사용하느냐에 따라 결정됩니다.
SingleChildScrollView
핵심 원리: 자식 위젯을 모두 한 번에 빌드하고 렌더링합니다. 뷰포트는 이 거대한 위젯의 일부를 보여주는 창 역할을 합니다.
장점:
- 구현이 간단하고 직관적입니다.
- 자식 위젯의 구조가 복잡하거나 서로 다른 종류의 위젯이 섞여 있을 때 유용합니다.
GlobalKey
를 사용하여 특정 위젯으로 이동하는 것이 매우 안정적입니다.
단점:
- 자식 위젯의 수가 많아지면 초기 빌드 시간이 길어지고 메모리 사용량이 급격히 증가합니다.
- 성능 저하의 주된 원인이 될 수 있습니다.
주요 사용 사례:
- 회원가입, 로그인 등 필드 수가 제한적인 폼 페이지
- 앱 설정 화면
- 내용의 양이 많지 않은 상세 정보 페이지
ListView (특히 ListView.builder)
핵심 원리: 화면에 보이는 아이템과 그 주변의 일부 아이템(캐시 영역)만 빌드합니다. 스크롤 시 위젯을 재활용하여 효율성을 극대화합니다.
장점:
- 아이템 수가 거의 무한대에 가까워도 일정한 메모리 사용량과 빠른 성능을 유지합니다.
- 대용량 데이터를 처리하는 데 최적화되어 있습니다.
단점:
GlobalKey
를 사용하는 방식이 부적합하며,ScrollController
를 사용해야 합니다.- 아이템 높이가 가변적일 경우 특정 인덱스로 스크롤하는 로직이 복잡해질 수 있습니다.
주요 사용 사례:
- 채팅 앱의 메시지 목록
- SNS의 피드
- 연락처, 상품 목록 등 동적으로 로드되는 긴 리스트
결론적으로, 화면에 표시될 콘텐츠의 양이 명확하게 제한되어 있고 예측 가능하다면 SingleChildScrollView
와 GlobalKey
가 좋은 선택입니다. 반면, 데이터의 양이 많거나 가변적이라면 반드시 ListView.builder
와 ScrollController
를 사용하여 애플리케이션의 성능과 안정성을 확보해야 합니다.
4. 실제 적용과 주의사항
이론을 배웠으니 이제 실제 프로젝트에서 발생할 수 있는 몇 가지 중요한 문제와 해결책을 짚어보겠습니다.
주의사항 1: 위젯이 렌더링되기 전에 스크롤 호출하기
initState()
메서드 안에서 Scrollable.ensureVisible()
이나 _scrollController.jumpTo()
를 호출하면 어떻게 될까요? 대부분의 경우 아무 일도 일어나지 않거나 오류가 발생합니다. 왜냐하면 initState()
가 호출되는 시점에는 위젯 트리가 아직 완전히 구성되지 않았고, 화면에 렌더링되지 않았기 때문입니다. 따라서 스크롤할 대상 위젯의 위치나 스크롤 뷰의 전체 크기를 알 수 없습니다.
이 문제를 해결하기 위해 WidgetsBinding.instance.addPostFrameCallback
을 사용합니다. 이 콜백은 플러터 엔진이 현재 프레임의 빌드와 렌더링을 모두 마친 직후에 실행되도록 보장해줍니다.
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
// 이 시점에서는 위젯의 크기와 위치 계산이 완료되었습니다.
_scrollToTarget();
});
}
이 패턴은 화면이 처음 로드될 때 특정 위치로 스크롤해야 하는 요구사항(예: 특정 아이템을 강조하며 화면 시작)을 구현할 때 필수적입니다.
주의사항 2: `_globalKey.currentContext`가 null인 경우
Scrollable.ensureVisible(_globalKey.currentContext)
를 호출할 때 _globalKey.currentContext
가 null
이어서 NullPointerException
이 발생하는 경우가 있습니다. 이는 다음과 같은 상황에서 발생할 수 있습니다.
GlobalKey
가 할당된 위젯이 아직 위젯 트리에 추가되지 않았을 때 (위 `initState` 문제와 유사)GlobalKey
가 할당된 위젯이 조건부 렌더링(예: `if (someCondition) ...`)에 의해 현재 위젯 트리에 포함되지 않았을 때ListView.builder
와 같이 위젯이 재활용되면서 화면 밖으로 나가 현재 트리에 존재하지 않을 때
따라서 Scrollable.ensureVisible
를 호출하기 전에는 항상 if (_globalKey.currentContext != null)
과 같이 null 체크를 해주는 것이 안정적인 코드를 작성하는 좋은 습관입니다.
플러터에서 프로그래밍 방식으로 스크롤을 제어하는 것은 사용자 경험을 한 차원 높이는 강력한 도구입니다. 정적인 콘텐츠에는 GlobalKey
와 Scrollable.ensureVisible
를, 동적이고 긴 목록에는 ScrollController
를 사용하는 기본 원칙을 기억하고, 위젯의 생명주기를 고려하여 적절한 시점에 스크롤 메서드를 호출한다면, 어떤 복잡한 스크롤 요구사항도 자신 있게 구현할 수 있을 것입니다.
0 개의 댓글:
Post a Comment