본 콘텐츠의 이미지 및 내용은 AI로 생성되었습니다.
본 콘텐츠의 이미지 및 내용을 무단으로 복제, 배포, 수정하여 사용할 경우 저작권법에 의해 법적 제재를 받을 수 있습니다.
AI Generated
2025. 12. 11. · 50 Views
ref.listen으로 사이드 이펙트 처리 완벽 가이드
Riverpod에서 ref.listen을 사용하여 상태 변화에 따른 사이드 이펙트를 우아하게 처리하는 방법을 실무 중심으로 배워봅니다. 스낵바 표시, 페이지 이동, 에러 처리 등 실전 예제를 통해 완벽하게 이해할 수 있습니다.
목차
1. ref.listen 기본 사용법
어느 날 김개발 씨는 로그인 기능을 개발하고 있었습니다. 로그인이 성공하면 스낵바를 보여주고, 실패하면 에러 메시지를 띄워야 했습니다.
하지만 build 메서드 안에서 이런 작업을 하려니 계속 에러가 발생했습니다.
ref.listen은 Provider의 상태 변화를 감지하여 사이드 이펙트를 실행하는 메서드입니다. 마치 센서가 변화를 감지하고 경보를 울리는 것처럼, 상태 변화를 감지하여 원하는 동작을 수행합니다.
build 메서드가 아닌 initState나 생성자에서 호출하여 UI 렌더링과 분리된 작업을 처리할 수 있습니다.
다음 코드를 살펴봅시다.
class LoginScreen extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
// listen은 상태가 변할 때마다 콜백 실행
ref.listen<AsyncValue<User>>(
loginProvider,
(previous, next) {
// next는 새로운 상태값
next.when(
data: (user) => print('로그인 성공: ${user.name}'),
loading: () => print('로그인 중...'),
error: (err, stack) => print('로그인 실패: $err'),
);
},
);
return Scaffold(body: LoginForm());
}
}
김개발 씨는 입사 2개월 차 플러터 개발자입니다. 오늘은 회사 앱의 로그인 기능을 맡았습니다.
요구사항은 간단했습니다. 로그인이 성공하면 환영 메시지를 보여주고, 실패하면 에러를 표시하는 것이었습니다.
처음에 김개발 씨는 build 메서드 안에서 직접 스낵바를 띄우려고 했습니다. 하지만 이상한 에러가 계속 발생했습니다.
"setState() or markNeedsBuild() called during build"라는 메시지와 함께 앱이 멈춰버렸습니다. 선배 개발자 박시니어 씨가 다가와 코드를 살펴봅니다.
"아, 여기가 문제네요. ref.listen을 사용해야 해요." ref.listen이란 정확히 무엇일까요?
쉽게 비유하자면, ref.listen은 마치 택배 배송 추적 알림과 같습니다. 택배 상태가 변할 때마다 알림을 받는 것처럼, Provider의 상태가 변할 때마다 우리가 원하는 동작을 실행할 수 있습니다.
택배가 출발했을 때, 배송 중일 때, 도착했을 때 각각 다른 알림을 받듯이, 상태의 변화에 따라 다른 작업을 수행할 수 있습니다. ref.listen이 없던 시절에는 어땠을까요?
개발자들은 StatefulWidget을 만들고, didUpdateWidget 메서드에서 이전 상태와 새 상태를 비교하는 코드를 직접 작성해야 했습니다. 코드가 길어지고, 실수하기도 쉬웠습니다.
더 큰 문제는 UI 렌더링 중에 사이드 이펙트를 실행하려다 에러가 발생하는 경우였습니다. 프로젝트가 커질수록 이런 보일러플레이트 코드는 눈덩이처럼 불어났습니다.
바로 이런 문제를 해결하기 위해 ref.listen이 등장했습니다. ref.listen을 사용하면 상태 변화 감지가 자동화됩니다.
또한 UI 렌더링과 사이드 이펙트를 분리할 수 있습니다. 무엇보다 간결한 코드로 복잡한 로직을 처리할 수 있다는 큰 이점이 있습니다.
위의 코드를 한 줄씩 살펴보겠습니다. 먼저 ref.listen<AsyncValue<User>>(loginProvider, ...)를 보면 loginProvider의 상태 변화를 감시한다는 것을 알 수 있습니다.
이 부분이 핵심입니다. 다음으로 콜백 함수의 매개변수 (previous, next)에서는 이전 상태와 새로운 상태를 받아옵니다.
마지막으로 next.when(...)을 통해 상태에 따라 다른 동작을 수행합니다. 실제 현업에서는 어떻게 활용할까요?
예를 들어 쇼핑몰 앱을 개발한다고 가정해봅시다. 사용자가 상품을 장바구니에 담으면 "장바구니에 추가되었습니다"라는 스낵바를 보여주고, 결제가 완료되면 주문 완료 페이지로 이동해야 합니다.
ref.listen을 활용하면 이런 모든 사이드 이펙트를 깔끔하게 처리할 수 있습니다. 토스, 배달의민족, 무신사 같은 많은 기업에서 이런 패턴을 적극적으로 사용하고 있습니다.
하지만 주의할 점도 있습니다. 초보 개발자들이 흔히 하는 실수 중 하나는 listen 안에서 또 다른 ref.read를 호출하는 것입니다.
이렇게 하면 무한 루프가 발생할 수 있습니다. 따라서 콜백 안에서는 읽기만 하고, 쓰기는 신중하게 해야 합니다.
다시 김개발 씨의 이야기로 돌아가 봅시다. 박시니어 씨의 설명을 들은 김개발 씨는 고개를 끄덕였습니다.
"아, 그래서 빌드 중에 에러가 났던 거군요!" ref.listen을 제대로 이해하면 더 깔끔하고 유지보수하기 쉬운 코드를 작성할 수 있습니다. 여러분도 오늘 배운 내용을 실제 프로젝트에 적용해 보세요.
실전 팁
💡 - ref.listen은 build 메서드 안 어디서든 호출 가능하지만, 최상단에 배치하면 가독성이 좋습니다
- previous와 next를 비교하여 특정 조건에서만 사이드 이펙트를 실행할 수 있습니다
- AsyncValue.when 메서드로 로딩, 성공, 에러 상태를 한 번에 처리할 수 있습니다
2. 에러 발생 시 스낵바 표시
김개발 씨가 ref.listen을 적용하고 나니 또 다른 요구사항이 들어왔습니다. "로그인 실패하면 스낵바로 에러 메시지를 보여주세요." 하지만 listen 콜백 안에서 context를 사용해도 되는지 고민이 되었습니다.
ref.listen의 콜백 함수는 BuildContext를 자유롭게 사용할 수 있습니다. 마치 이벤트 핸들러처럼 동작하므로, 스낵바나 다이얼로그 같은 UI 작업을 안전하게 수행할 수 있습니다.
특히 에러 상태를 감지하여 사용자에게 즉시 피드백을 제공하는 용도로 많이 활용됩니다.
다음 코드를 살펴봅시다.
class LoginScreen extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
ref.listen<AsyncValue<User>>(
loginProvider,
(previous, next) {
// 에러 상태일 때만 스낵바 표시
if (next.hasError) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('로그인 실패: ${next.error}'),
backgroundColor: Colors.red,
duration: Duration(seconds: 3),
),
);
}
},
);
return Scaffold(body: LoginForm());
}
}
김개발 씨는 ref.listen의 기본 사용법을 익혔습니다. 이제 실무에서 가장 많이 쓰이는 패턴을 배워야 할 차례였습니다.
바로 에러가 발생했을 때 사용자에게 알려주는 것이었습니다. 요구사항 문서를 다시 살펴봅니다.
"비밀번호가 틀렸을 때, 네트워크가 끊겼을 때, 서버가 응답하지 않을 때 모두 사용자에게 친절하게 알려줘야 합니다." 김개발 씨는 고민에 빠졌습니다. "listen 콜백 안에서 context를 써도 괜찮을까?" 박시니어 씨가 옆에서 말합니다.
"걱정하지 마세요. listen 콜백은 이벤트 핸들러처럼 동작해서 context를 안전하게 쓸 수 있어요." listen 콜백에서 context 사용이란 정확히 무엇을 의미할까요?
쉽게 비유하자면, listen 콜백은 마치 버튼의 onPressed 핸들러와 같습니다. 버튼을 눌렀을 때 다이얼로그를 띄우거나 새 화면으로 이동하는 것처럼, 상태가 변했을 때 UI 작업을 수행할 수 있습니다.
이벤트가 발생한 시점에 실행되므로, 빌드 단계와 충돌하지 않습니다. context를 안전하게 사용하지 못하던 시절에는 어땠을까요?
개발자들은 GlobalKey를 만들어서 ScaffoldMessenger에 접근하거나, 별도의 상태 변수를 만들어 빌드 후에 체크하는 복잡한 방식을 사용해야 했습니다. 코드가 지저분해지고, 타이밍 문제로 인한 버그도 자주 발생했습니다.
더 큰 문제는 코드를 읽는 사람이 "이게 언제 실행되는 거지?"라고 혼란스러워한다는 점이었습니다. 바로 이런 문제를 해결하기 위해 ref.listen의 안전한 context 사용이 가능해졌습니다.
ref.listen을 사용하면 상태 변화 시점에 정확히 실행됩니다. 또한 코드가 직관적이어서 "에러가 나면 스낵바를 보여준다"는 의도가 명확히 드러납니다.
무엇보다 타이밍 문제 없이 안전하게 UI 작업을 수행할 수 있다는 큰 이점이 있습니다. 위의 코드를 한 줄씩 살펴보겠습니다.
먼저 if (next.hasError)를 보면 새로운 상태가 에러 상태인지 확인한다는 것을 알 수 있습니다. 이 부분이 핵심입니다.
다음으로 ScaffoldMessenger.of(context)에서는 현재 화면의 Scaffold에 접근하여 스낵바를 표시합니다. 마지막으로 SnackBar 위젯에 에러 메시지와 빨간색 배경을 설정하여 사용자가 즉시 알아볼 수 있도록 합니다.
실제 현업에서는 어떻게 활용할까요? 예를 들어 은행 앱을 개발한다고 가정해봅시다.
사용자가 계좌 이체를 시도했는데 잔액이 부족하거나, 비밀번호가 틀렸거나, 일일 한도를 초과한 경우 각각 다른 에러 메시지를 스낵바로 보여줘야 합니다. ref.listen을 활용하면 이런 모든 케이스를 하나의 콜백 안에서 깔끔하게 처리할 수 있습니다.
카카오뱅크, 토스 같은 핀테크 서비스에서 이런 패턴을 적극적으로 사용하고 있습니다. 하지만 주의할 점도 있습니다.
초보 개발자들이 흔히 하는 실수 중 하나는 모든 상태 변화마다 스낵바를 띄우는 것입니다. 이렇게 하면 로딩 중일 때도 스낵바가 나타나 사용자 경험이 나빠집니다.
따라서 hasError나 hasValue로 체크하여 필요한 경우에만 스낵바를 표시해야 합니다. 다시 김개발 씨의 이야기로 돌아가 봅시다.
코드를 수정하고 테스트를 돌려본 김개발 씨는 만족스러운 표정을 지었습니다. "이제 사용자가 에러를 바로 알 수 있겠네요!" 에러 처리는 사용자 경험의 핵심입니다.
ref.listen을 활용하면 에러를 우아하게 처리할 수 있습니다. 여러분도 오늘 배운 내용으로 사용자 친화적인 앱을 만들어 보세요.
실전 팁
💡 - hasError뿐만 아니라 next.error의 타입을 체크하여 에러 종류별로 다른 메시지를 표시할 수 있습니다
- SnackBar 대신 AlertDialog를 사용하면 더 중요한 에러를 강조할 수 있습니다
- duration을 조절하여 에러의 심각도에 따라 표시 시간을 다르게 설정할 수 있습니다
3. 성공 시 다른 페이지로 이동
로그인 화면의 에러 처리까지 완료한 김개발 씨는 다음 단계를 맞이했습니다. "로그인이 성공하면 홈 화면으로 자동으로 이동해야 해요." 하지만 페이지 이동을 어디서 해야 할지 고민이 되었습니다.
ref.listen은 상태 변화에 따른 네비게이션 처리에 최적화되어 있습니다. 마치 신호등이 파란불로 바뀌면 차가 출발하는 것처럼, 성공 상태가 되면 자동으로 다음 화면으로 이동할 수 있습니다.
Navigator를 사용한 페이지 전환을 사이드 이펙트로 처리하여 UI 로직과 분리할 수 있습니다.
다음 코드를 살펴봅시다.
class LoginScreen extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
ref.listen<AsyncValue<User>>(
loginProvider,
(previous, next) {
next.whenData((user) {
// 성공 시 홈 화면으로 이동
Navigator.of(context).pushReplacement(
MaterialPageRoute(
builder: (_) => HomeScreen(user: user),
),
);
});
},
);
return Scaffold(body: LoginForm());
}
}
김개발 씨는 에러 처리까지 성공적으로 완료했습니다. 이제 마지막 단계인 성공 시 페이지 이동만 남았습니다.
기획서를 다시 펼쳐봅니다. "로그인이 성공하면 메인 화면으로 이동하되, 뒤로가기 버튼을 눌러도 로그인 화면으로 돌아가면 안 됩니다." 김개발 씨는 처음에 LoginForm 위젯 안에서 직접 Navigator를 호출하려고 했습니다.
하지만 여러 곳에서 중복 코드가 발생했고, 로직이 분산되어 유지보수가 어려웠습니다. 박시니어 씨가 코드 리뷰를 하다가 조언을 했습니다.
"ref.listen에서 네비게이션까지 처리하면 훨씬 깔끔해요." ref.listen으로 네비게이션 처리란 정확히 어떤 의미일까요? 쉽게 비유하자면, ref.listen은 마치 공항의 탑승 안내 방송과 같습니다.
"탑승 준비가 완료되었습니다"라는 안내가 나오면 승객들이 게이트로 이동하듯이, "로그인이 성공했습니다"라는 상태 변화가 감지되면 자동으로 다음 화면으로 이동하는 것입니다. 중앙에서 일괄적으로 관리하므로 놓치는 경우가 없습니다.
네비게이션을 각 위젯에서 개별적으로 처리하던 시절에는 어땠을까요? 개발자들은 버튼 onPressed 핸들러마다 Navigator 코드를 작성해야 했습니다.
로그인 버튼, 자동 로그인, 소셜 로그인 등 여러 진입점에서 똑같은 코드를 반복했습니다. 더 큰 문제는 한 곳에서 수정하면 다른 곳도 모두 찾아서 수정해야 한다는 점이었습니다.
프로젝트가 커질수록 이런 중복 코드는 관리 악몽이 되었습니다. 바로 이런 문제를 해결하기 위해 ref.listen의 중앙집중식 네비게이션이 활용됩니다.
ref.listen을 사용하면 모든 성공 케이스를 한 곳에서 처리할 수 있습니다. 또한 비즈니스 로직과 네비게이션 로직이 분리됩니다.
무엇보다 코드 중복이 사라지고 유지보수가 쉬워진다는 큰 이점이 있습니다. 위의 코드를 한 줄씩 살펴보겠습니다.
먼저 next.whenData((user) {...})를 보면 데이터가 있을 때만 실행된다는 것을 알 수 있습니다. 이 부분이 핵심입니다.
다음으로 Navigator.of(context).pushReplacement에서는 현재 화면을 새 화면으로 대체합니다. 마지막으로 HomeScreen(user: user)에서 로그인한 사용자 정보를 다음 화면으로 전달합니다.
실제 현업에서는 어떻게 활용할까요? 예를 들어 전자상거래 앱을 개발한다고 가정해봅시다.
사용자가 상품을 장바구니에 담고, 주문서를 작성하고, 결제를 완료하는 전체 플로우가 있습니다. 각 단계마다 성공하면 자동으로 다음 화면으로 이동해야 합니다.
ref.listen을 활용하면 이런 복잡한 플로우를 각 화면마다 일관되게 처리할 수 있습니다. 쿠팡, 11번가 같은 이커머스 서비스에서 이런 패턴으로 매끄러운 사용자 경험을 제공합니다.
하지만 주의할 점도 있습니다. 초보 개발자들이 흔히 하는 실수 중 하나는 push와 pushReplacement를 혼동하는 것입니다.
push를 사용하면 뒤로가기로 로그인 화면에 다시 돌아갈 수 있어 사용자가 혼란스러워합니다. 따라서 로그인 같은 일회성 화면은 pushReplacement를 사용해야 합니다.
다시 김개발 씨의 이야기로 돌아가 봅시다. 코드를 수정하고 앱을 실행한 김개발 씨는 뿌듯함을 느꼈습니다.
"로그인하면 자동으로 홈으로 가네요. 깔끔해요!" 네비게이션은 앱의 흐름을 결정하는 중요한 요소입니다.
ref.listen을 활용하면 사용자가 자연스럽게 다음 단계로 이동할 수 있습니다. 여러분도 오늘 배운 내용으로 매끄러운 사용자 플로우를 만들어 보세요.
실전 팁
💡 - pushReplacement 대신 pushAndRemoveUntil을 사용하면 특정 화면까지만 스택을 제거할 수 있습니다
- GoRouter를 사용하는 경우 context.go나 context.replace를 활용할 수 있습니다
- 페이지 전환 전에 ref.invalidate로 이전 상태를 초기화하면 메모리 누수를 방지할 수 있습니다
4. 이전 값과 새 값 비교
김개발 씨는 이제 고급 기능을 구현해야 했습니다. "좋아요 개수가 100개를 넘으면 축하 메시지를 보여주세요.
단, 한 번만 표시되어야 합니다." 매번 상태가 변할 때마다 체크하면 중복 알림이 발생할 것 같았습니다.
ref.listen의 콜백은 previous와 next 두 매개변수를 제공하여 이전 상태와 새 상태를 비교할 수 있습니다. 마치 온도계가 특정 온도를 넘는 순간을 감지하는 것처럼, 값의 변화 시점을 정확히 포착할 수 있습니다.
이를 활용하면 중복 실행을 방지하고 특정 조건에서만 사이드 이펙트를 실행할 수 있습니다.
다음 코드를 살펴봅시다.
class PostScreen extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
ref.listen<int>(
likeCountProvider,
(previous, next) {
// 100을 넘는 순간만 감지 (중복 방지)
if (previous != null &&
previous < 100 &&
next >= 100) {
showDialog(
context: context,
builder: (_) => AlertDialog(
title: Text('축하합니다!'),
content: Text('좋아요 100개 달성!'),
),
);
}
},
);
return Scaffold(body: PostContent());
}
}
김개발 씨는 기본적인 ref.listen 사용법을 완벽히 익혔습니다. 이제 더 섬세한 제어가 필요한 상황을 만났습니다.
바로 값의 변화 자체가 아니라 특정 조건을 넘는 순간을 감지하는 것이었습니다. 기획팀에서 요청한 기능은 명확했습니다.
"사용자가 게시물에 좋아요를 누를 때마다 카운트가 올라갑니다. 그런데 100개를 달성하는 순간 축하 메시지를 딱 한 번만 보여주세요." 김개발 씨는 고민에 빠졌습니다.
"매번 체크하면 100일 때, 101일 때, 102일 때 계속 팝업이 뜰 텐데?" 박시니어 씨가 옆자리에서 힌트를 줍니다. "listen 콜백의 previous를 활용하면 돼요." previous와 next 비교란 정확히 무엇일까요?
쉽게 비유하자면, 이것은 마치 육상 경기의 결승선과 같습니다. 선수가 계속 달리고 있지만, 결승선을 통과하는 바로 그 순간만 기록됩니다.
90m, 95m, 99m를 지나가는 것은 중요하지 않고, 100m 결승선을 넘는 순간만 의미가 있습니다. 이처럼 previous < 100이고 next >= 100인 순간만 감지하면 됩니다.
값의 변화를 정확히 추적하지 못하던 시절에는 어땠을까요? 개발자들은 별도의 플래그 변수를 만들어 "이미 알림을 보여줬는지" 체크해야 했습니다.
코드가 복잡해지고, 상태 관리가 어려워졌습니다. 더 큰 문제는 화면을 나갔다 들어오면 플래그가 초기화되어 또 다시 알림이 나타나는 버그였습니다.
프로젝트가 커질수록 이런 엣지 케이스를 놓치기 쉬웠습니다. 바로 이런 문제를 해결하기 위해 previous와 next 비교 패턴이 활용됩니다.
이 패턴을 사용하면 정확한 변화 시점을 포착할 수 있습니다. 또한 별도의 상태 변수가 필요 없어 코드가 간결해집니다.
무엇보다 로직이 명확하여 버그가 줄어든다는 큰 이점이 있습니다. 위의 코드를 한 줄씩 살펴보겠습니다.
먼저 if (previous != null && ...)를 보면 최초 실행 시 previous가 null인 경우를 방지한다는 것을 알 수 있습니다. 이 부분이 핵심입니다.
다음으로 previous < 100 && next >= 100에서는 정확히 100을 넘는 순간만 감지합니다. 마지막으로 showDialog를 통해 축하 메시지를 한 번만 표시합니다.
실제 현업에서는 어떻게 활용할까요? 예를 들어 피트니스 앱을 개발한다고 가정해봅시다.
사용자가 하루 만 보를 걷는 순간, 연속 7일 운동을 달성하는 순간, 목표 체중에 도달하는 순간마다 성취 배지를 보여줘야 합니다. previous와 next를 비교하면 이런 마일스톤 달성 순간을 정확히 포착할 수 있습니다.
나이키 런클럽, 삼성헬스 같은 앱에서 이런 패턴으로 사용자 동기부여를 제공합니다. 하지만 주의할 점도 있습니다.
초보 개발자들이 흔히 하는 실수 중 하나는 previous가 null인 경우를 체크하지 않는 것입니다. 이렇게 하면 앱 실행 직후 크래시가 발생할 수 있습니다.
따라서 항상 previous != null을 먼저 확인해야 합니다. 다시 김개발 씨의 이야기로 돌아가 봅시다.
테스트를 돌려본 김개발 씨는 감탄했습니다. "딱 100개 될 때만 팝업이 뜨네요.
완벽해요!" 값의 변화 시점을 정확히 감지하는 것은 고급 기능의 핵심입니다. previous와 next 비교 패턴을 익히면 더욱 세밀한 제어가 가능합니다.
여러분도 오늘 배운 내용으로 사용자에게 특별한 순간을 선사해 보세요.
실전 팁
💡 - previous?.someProperty 형태로 null-safe하게 접근할 수 있습니다
- 여러 조건을 동시에 체크할 때는 early return 패턴으로 가독성을 높일 수 있습니다
- 복잡한 비교 로직은 별도 함수로 분리하면 테스트하기 쉽습니다
5. listenSelf 활용
김개발 씨는 Provider 내부에서도 다른 Provider를 감시할 필요가 생겼습니다. "사용자 로그인 상태가 변하면 자동으로 프로필을 다시 불러와야 해요." 하지만 Provider 안에서 ref.listen을 쓰니 자기 자신까지 감시하는 문제가 발생했습니다.
listenSelf는 Provider가 자기 자신의 상태 변화를 감지하는 메서드입니다. 마치 거울을 보며 자신의 변화를 확인하는 것처럼, Provider가 스스로의 상태를 모니터링할 수 있습니다.
위젯이 아닌 Provider 내부에서 사이드 이펙트를 처리할 때 유용하게 활용됩니다.
다음 코드를 살펴봅시다.
@riverpod
class ProfileNotifier extends _$ProfileNotifier {
@override
FutureOr<Profile> build() async {
// 자기 자신의 상태 변화를 감시
ref.listenSelf((previous, next) {
// 프로필 로딩 완료 시 로그 기록
if (next.hasValue) {
_logProfileLoaded(next.value!);
}
});
return await _fetchProfile();
}
void _logProfileLoaded(Profile profile) {
print('프로필 로드 완료: ${profile.name}');
}
}
김개발 씨는 이제 Provider 수준의 고급 기능을 다뤄야 할 시점이 왔습니다. 지금까지는 위젯에서 ref.listen을 사용했지만, 이번에는 Provider 내부에서 자기 자신을 감시해야 하는 상황이었습니다.
요구사항은 이랬습니다. "프로필 데이터를 불러오는 Provider가 있는데, 데이터 로딩이 완료될 때마다 분석 로그를 서버로 전송해야 합니다.
그런데 이 로직은 Provider 내부에 있어야 합니다." 김개발 씨는 처음에 ref.listen을 써보려 했지만, 다른 Provider를 감시하는 것이지 자기 자신을 감시할 수는 없었습니다. 박시니어 씨가 코드 리뷰 중에 새로운 방법을 알려줬습니다.
"listenSelf를 써보세요." listenSelf란 정확히 무엇일까요? 쉽게 비유하자면, listenSelf는 마치 자동차의 블랙박스와 같습니다.
운전자가 외부를 보는 것이 아니라, 차 자체의 상태를 기록합니다. 속도가 변하는 순간, 급정거하는 순간, 충돌하는 순간을 차 스스로 감지하고 기록하듯이, Provider가 자신의 상태 변화를 스스로 감시하고 필요한 작업을 수행합니다.
Provider가 자신을 모니터링할 수 없던 시절에는 어땠을까요? 개발자들은 외부 위젯에서 listen하여 콜백으로 Provider의 메서드를 호출하는 우회 방식을 써야 했습니다.
코드가 분산되고, Provider의 캡슐화가 깨졌습니다. 더 큰 문제는 여러 화면에서 같은 Provider를 사용하면 각 화면마다 listen 코드를 중복해야 한다는 점이었습니다.
프로젝트가 커질수록 이런 보일러플레이트는 관리 지옥이 되었습니다. 바로 이런 문제를 해결하기 위해 listenSelf가 등장했습니다.
listenSelf를 사용하면 Provider 내부에서 모든 로직을 완결할 수 있습니다. 또한 캡슐화가 유지되어 코드 구조가 깔끔해집니다.
무엇보다 한 곳에서만 작성하면 모든 곳에서 동작한다는 큰 이점이 있습니다. 위의 코드를 한 줄씩 살펴보겠습니다.
먼저 ref.listenSelf((previous, next) {...})를 보면 자기 자신의 상태 변화를 감시한다는 것을 알 수 있습니다. 이 부분이 핵심입니다.
다음으로 if (next.hasValue)에서는 데이터 로딩이 성공한 경우만 처리합니다. 마지막으로 _logProfileLoaded를 통해 내부 메서드를 호출하여 로그를 기록합니다.
실제 현업에서는 어떻게 활용할까요? 예를 들어 채팅 앱을 개발한다고 가정해봅시다.
메시지 리스트를 관리하는 Provider가 있고, 새 메시지가 추가될 때마다 읽지 않은 메시지 카운트를 업데이트하고, 푸시 알림을 예약하고, 분석 로그를 전송해야 합니다. listenSelf를 활용하면 이런 모든 부수 효과를 Provider 내부에서 일관되게 처리할 수 있습니다.
슬랙, 디스코드 같은 메신저 앱에서 이런 패턴을 사용합니다. 하지만 주의할 점도 있습니다.
초보 개발자들이 흔히 하는 실수 중 하나는 listenSelf 안에서 state를 직접 수정하는 것입니다. 이렇게 하면 무한 루프가 발생할 수 있습니다.
따라서 listenSelf는 읽기 전용 사이드 이펙트만 처리해야 합니다. 다시 김개발 씨의 이야기로 돌아가 봅시다.
listenSelf를 적용하고 나니 코드가 훨씬 깔끔해졌습니다. "이제 Provider가 알아서 로그를 보내네요.
어디서든 재사용할 수 있겠어요!" Provider의 캡슐화는 유지보수성의 핵심입니다. listenSelf를 활용하면 Provider가 자신의 책임을 스스로 완수할 수 있습니다.
여러분도 오늘 배운 내용으로 더 견고한 아키텍처를 만들어 보세요.
실전 팁
💡 - listenSelf는 build 메서드 안에서만 호출해야 합니다
- 로그 전송, 분석 이벤트, 캐시 업데이트 같은 읽기 전용 작업에 적합합니다
- UI 관련 작업(스낵바, 다이얼로그)은 위젯의 ref.listen에서 처리하는 것이 좋습니다
6. Provider 내부에서 listen
김개발 씨는 또 다른 고급 시나리오를 만났습니다. "인증 상태가 변하면 사용자 데이터를 자동으로 초기화해야 해요." 이번에는 다른 Provider의 변화를 감지해야 했습니다.
Provider 내부에서 ref.listen을 사용하면 다른 Provider의 상태 변화를 감지하여 자동으로 반응할 수 있습니다. 마치 도미노처럼 하나의 변화가 연쇄적으로 다른 변화를 일으키는 패턴입니다.
인증 상태, 언어 설정, 테마 같은 전역 상태가 변할 때 관련 Provider들을 자동으로 업데이트하는 데 유용합니다.
다음 코드를 살펴봅시다.
@riverpod
class UserDataNotifier extends _$UserDataNotifier {
@override
Future<UserData?> build() async {
// authProvider의 변화를 감시
ref.listen(
authProvider,
(previous, next) {
// 로그아웃 시 데이터 초기화
if (previous?.isAuthenticated == true &&
next.isAuthenticated == false) {
state = AsyncValue.data(null);
_clearCache();
}
},
);
final auth = ref.watch(authProvider);
if (!auth.isAuthenticated) return null;
return await _fetchUserData();
}
void _clearCache() {
print('캐시 초기화 완료');
}
}
김개발 씨는 이제 Riverpod의 고급 패턴 중 하나인 Provider 간 연동을 구현해야 했습니다. 지금까지는 위젯에서 listen하거나 Provider가 자기 자신을 listen했지만, 이번에는 A Provider가 B Provider를 감시하는 상황이었습니다.
기획서를 보니 요구사항이 명확했습니다. "사용자가 로그아웃하면 프로필 데이터, 설정 데이터, 캐시 데이터가 모두 자동으로 초기화되어야 합니다.
그런데 각 화면에서 개별적으로 처리하면 안 됩니다." 김개발 씨는 고민에 빠졌습니다. "어떻게 하면 한 곳에서 중앙 관리할 수 있을까?" 박시니어 씨가 아키텍처 설명을 해줬습니다.
"각 데이터 Provider가 authProvider를 listen하면 돼요." Provider 내부에서 다른 Provider listen이란 정확히 무엇일까요? 쉽게 비유하자면, 이것은 마치 스마트홈 시스템과 같습니다.
현관문이 잠기면 자동으로 모든 조명이 꺼지고, 에어컨이 절전 모드로 바뀌고, 보안 시스템이 활성화됩니다. 하나의 마스터 스위치(authProvider)를 감시하는 여러 슬레이브 장치들(다른 Provider들)이 자동으로 반응하는 것입니다.
Provider 간 연동을 수동으로 처리하던 시절에는 어땠을까요? 개발자들은 화면마다 onPressed 핸들러에서 여러 Provider를 직접 초기화해야 했습니다.
로그아웃 버튼이 있는 모든 화면에서 똑같은 코드를 반복했습니다. 더 큰 문제는 새로운 데이터 Provider가 추가될 때마다 모든 로그아웃 코드를 찾아서 수정해야 한다는 점이었습니다.
프로젝트가 커질수록 이런 의존성 관리는 악몽이 되었습니다. 바로 이런 문제를 해결하기 위해 Provider 내부 listen 패턴이 활용됩니다.
이 패턴을 사용하면 의존성이 명확히 드러납니다. 또한 자동으로 연쇄 반응이 일어나므로 수동 관리가 필요 없습니다.
무엇보다 새로운 Provider 추가 시 해당 Provider만 수정하면 된다는 큰 이점이 있습니다. 위의 코드를 한 줄씩 살펴보겠습니다.
먼저 ref.listen(authProvider, ...)를 보면 인증 Provider를 감시한다는 것을 알 수 있습니다. 이 부분이 핵심입니다.
다음으로 조건문에서는 로그인 상태에서 로그아웃 상태로 변하는 순간을 정확히 포착합니다. 마지막으로 state = AsyncValue.data(null)로 자신의 상태를 즉시 초기화하고 캐시를 삭제합니다.
실제 현업에서는 어떻게 활용할까요? 예를 들어 다국어 지원 앱을 개발한다고 가정해봅시다.
사용자가 언어를 한국어에서 영어로 변경하면, 뉴스 피드 Provider가 영어 콘텐츠를 다시 불러오고, 추천 시스템 Provider가 영어권 추천을 재계산하고, 캐시 Provider가 이전 데이터를 삭제해야 합니다. ref.listen을 활용하면 이런 복잡한 연쇄 반응을 각 Provider가 독립적으로 처리할 수 있습니다.
넷플릭스, 유튜브 같은 글로벌 서비스에서 이런 패턴을 사용합니다. 하지만 주의할 점도 있습니다.
초보 개발자들이 흔히 하는 실수 중 하나는 순환 참조를 만드는 것입니다. A가 B를 listen하고 B가 A를 listen하면 무한 루프가 발생합니다.
따라서 의존성 방향을 명확히 설계해야 합니다. 일반적으로 auth → data → ui 같은 단방향 흐름을 유지하는 것이 좋습니다.
다시 김개발 씨의 이야기로 돌아가 봅시다. 코드를 완성하고 테스트를 돌려본 김개발 씨는 놀라움을 감추지 못했습니다.
"로그아웃하니까 모든 데이터가 자동으로 사라지네요. 각 화면에서 아무것도 안 해도 되고요!" Provider 간 연동은 복잡한 앱을 우아하게 관리하는 핵심 패턴입니다.
ref.listen을 Provider 내부에서 활용하면 시스템이 스스로 조화롭게 동작합니다. 여러분도 오늘 배운 내용으로 견고하고 확장 가능한 아키텍처를 만들어 보세요.
실전 팁
💡 - 의존성 그래프를 그려보면 순환 참조를 미리 발견할 수 있습니다
- listen 콜백 안에서 ref.invalidate를 호출하면 관련 Provider를 재계산할 수 있습니다
- 너무 많은 Provider가 서로를 listen하면 디버깅이 어려우므로, 핵심 전역 상태만 감시하는 것이 좋습니다
이상으로 학습을 마칩니다. 위 내용을 직접 코드로 작성해보면서 익혀보세요!
댓글 (0)
함께 보면 좋은 카드 뉴스
AAA급 게임 프로젝트 완벽 가이드
Flutter와 Flame 엔진을 활용하여 AAA급 퀄리티의 모바일 게임을 개발하는 전체 과정을 다룹니다. 기획부터 앱 스토어 출시까지, 실무에서 필요한 모든 단계를 이북처럼 술술 읽히는 스타일로 설명합니다.
빌드와 배포 자동화 완벽 가이드
Flutter 앱 개발에서 GitHub Actions를 활용한 CI/CD 파이프라인 구축부터 앱 스토어 자동 배포까지, 초급 개발자도 쉽게 따라할 수 있는 빌드 자동화의 모든 것을 다룹니다.
게임 분석과 메트릭스 완벽 가이드
Flutter와 Flame으로 개발한 게임의 성공을 측정하고 개선하는 방법을 배웁니다. Firebase Analytics 연동부터 A/B 테스팅, 리텐션 분석까지 데이터 기반 게임 운영의 모든 것을 다룹니다.
게임 보안과 치팅 방지 완벽 가이드
Flutter와 Flame 게임 엔진에서 클라이언트 보안부터 서버 검증까지, 치터들로부터 게임을 보호하는 핵심 기법을 다룹니다. 초급 개발자도 쉽게 따라할 수 있는 실전 보안 코드와 함께 설명합니다.
애니메이션 시스템 커스터마이징 완벽 가이드
Flutter와 Flame 게임 엔진에서 고급 애니메이션 시스템을 구현하는 방법을 다룹니다. 스켈레탈 애니메이션부터 절차적 애니메이션까지, 게임 개발에 필요한 핵심 애니메이션 기법을 실무 예제와 함께 배워봅니다.