본 콘텐츠의 이미지 및 내용은 AI로 생성되었습니다.
본 콘텐츠의 이미지 및 내용을 무단으로 복제, 배포, 수정하여 사용할 경우 저작권법에 의해 법적 제재를 받을 수 있습니다.
이미지 로딩 중...
AI Generated
2025. 12. 11. · 11 Views
Riverpod autoDispose 메모리 관리 완벽 가이드
Riverpod의 autoDispose로 메모리 누수를 방지하는 방법을 배워봅니다. 실무에서 자주 겪는 메모리 문제를 스토리텔링으로 쉽게 풀어냅니다. 초급 개발자도 바로 적용할 수 있는 실전 예제가 가득합니다.
목차
1. autoDispose 동작 원리
어느 날 김개발 씨가 Flutter 앱을 개발하던 중, 앱이 점점 느려지는 현상을 발견했습니다. 메모리 사용량을 확인해보니 이미 사용하지 않는 화면의 데이터가 계속 메모리에 남아있었습니다.
선배 개발자 박시니어 씨가 다가와 말했습니다. "autoDispose를 사용해보셨나요?"
autoDispose는 Riverpod Provider가 더 이상 사용되지 않을 때 자동으로 상태를 정리하는 기능입니다. 마치 도서관에서 책을 다 읽으면 자동으로 반납되는 시스템과 같습니다.
이 기능을 사용하면 메모리 누수를 방지하고 앱의 성능을 유지할 수 있습니다. 특히 많은 화면을 오가는 앱에서 필수적인 기능입니다.
다음 코드를 살펴봅시다.
// autoDispose를 사용한 Provider 선언
final userProvider = FutureProvider.autoDispose<User>((ref) async {
// API 호출로 사용자 정보 가져오기
final user = await fetchUser();
// Provider가 dispose될 때 자동으로 정리됨
ref.onDispose(() {
print('userProvider가 정리되었습니다');
});
return user;
});
// autoDispose 없이 사용하는 경우 (메모리에 계속 유지됨)
final permanentProvider = FutureProvider<User>((ref) async {
return await fetchUser();
});
[도입 - 실무 상황 스토리] 김개발 씨는 입사 3개월 차 Flutter 개발자입니다. 오늘도 열심히 앱을 개발하던 중, QA팀에서 이상한 버그 리포트가 올라왔습니다.
"앱을 오래 사용하면 점점 느려져요." 디버깅 도구를 켜고 메모리 사용량을 확인해보니 놀라운 사실을 발견했습니다. 이미 닫은 화면의 데이터가 메모리에 그대로 남아있었던 것입니다.
화면을 10개 열었다 닫으면, 10개 화면의 데이터가 모두 메모리를 차지하고 있었습니다. 김개발 씨는 당황했습니다.
선배 개발자 박시니어 씨가 코드를 살펴보더니 고개를 끄덕였습니다. "아, Provider에 autoDispose를 붙이지 않으셨네요.
그래서 메모리가 정리되지 않은 거예요." [개념 설명 - 비유로 쉽게] 그렇다면 autoDispose란 정확히 무엇일까요? 쉽게 비유하자면, autoDispose는 마치 식당의 테이블 정리 시스템과 같습니다.
손님이 식사를 마치고 나가면 직원이 자동으로 테이블을 치우고 다음 손님을 위해 준비하는 것처럼, autoDispose도 화면이 닫히면 자동으로 해당 화면의 데이터를 정리합니다. 만약 테이블을 정리하지 않으면 어떻게 될까요?
식당에 쓸 수 있는 테이블이 점점 줄어들고, 결국 새로운 손님을 받을 수 없게 됩니다. 앱의 메모리도 마찬가지입니다.
정리하지 않으면 사용 가능한 메모리가 줄어들고, 결국 앱이 느려지거나 심하면 종료됩니다. [왜 필요한가 - 문제 상황] autoDispose가 없던 시절, 아니 정확히는 autoDispose를 사용하지 않으면 어떤 일이 벌어질까요?
개발자들은 Provider의 생명주기를 직접 관리해야 했습니다. 화면이 닫힐 때 수동으로 상태를 정리하는 코드를 작성해야 했죠.
문제는 이걸 깜빡하기 쉽다는 것입니다. 특히 프로젝트가 커지고 화면이 많아질수록 어떤 Provider를 정리해야 하는지 추적하기가 어려워집니다.
더 큰 문제는 메모리 누수였습니다. 사용자가 화면을 10번, 20번 열었다 닫으면 그만큼 메모리에 데이터가 쌓입니다.
API 응답 데이터, 이미지, 컨트롤러 등이 메모리를 계속 차지하게 되죠. 결국 앱이 느려지고 사용자 경험이 나빠집니다.
[해결책 - 개념의 등장] 바로 이런 문제를 해결하기 위해 Riverpod은 autoDispose 기능을 제공합니다. autoDispose를 사용하면 Provider가 더 이상 사용되지 않을 때 자동으로 정리됩니다.
개발자가 별도로 정리 코드를 작성할 필요가 없습니다. 또한 메모리 누수 걱정 없이 안심하고 개발할 수 있습니다.
무엇보다 앱의 성능을 일정하게 유지할 수 있다는 큰 이점이 있습니다. 사용자가 앱을 아무리 오래 사용해도 메모리가 효율적으로 관리되기 때문입니다.
[코드 분석 - 단계별 설명] 위의 코드를 한 줄씩 살펴보겠습니다. 먼저 첫 번째 줄의 FutureProvider.autoDispose를 보세요.
일반 FutureProvider 대신 autoDispose가 붙었습니다. 이것만으로도 자동 정리 기능이 활성화됩니다.
다음으로 ref.onDispose 콜백에서는 Provider가 정리될 때 실행할 코드를 작성할 수 있습니다. 일반 Provider와의 차이를 확실히 느낄 수 있을 겁니다.
autoDispose가 없는 permanentProvider는 앱이 종료될 때까지 메모리에 계속 남아있습니다. [실무 활용 사례] 실제 현업에서는 어떻게 활용할까요?
예를 들어 쇼핑몰 앱을 개발한다고 가정해봅시다. 사용자가 상품 상세 페이지를 열면 상품 정보를 API로 가져옵니다.
사용자가 페이지를 나가면 그 상품 정보는 더 이상 필요 없습니다. 이때 autoDispose를 사용하면 자동으로 정리됩니다.
많은 스타트업과 대기업에서 이 패턴을 적극 활용하고 있습니다. 특히 뉴스 앱, SNS 앱처럼 화면 전환이 잦은 앱에서는 필수적입니다.
[동작 원리] 그렇다면 autoDispose는 내부적으로 어떻게 동작할까요? Riverpod은 각 Provider의 리스너 개수를 추적합니다.
Widget이 Provider를 사용하면 리스너가 증가하고, Widget이 dispose되면 리스너가 감소합니다. 리스너 개수가 0이 되면 Provider도 자동으로 정리됩니다.
마치 카운터처럼 동작한다고 생각하면 이해하기 쉽습니다. 누군가 사용하고 있으면 유지되고, 아무도 사용하지 않으면 정리되는 것이죠.
[주의사항] 하지만 주의할 점도 있습니다. 초보 개발자들이 흔히 하는 실수 중 하나는 모든 Provider에 무조건 autoDispose를 붙이는 것입니다.
앱 전체에서 계속 사용되는 설정 정보나 사용자 인증 상태 같은 데이터는 autoDispose를 사용하면 안 됩니다. 화면을 벗어날 때마다 로그아웃되는 황당한 상황이 벌어질 수 있습니다.
따라서 "이 데이터가 특정 화면에서만 사용되는가?"를 먼저 생각해보고 autoDispose를 적용해야 합니다. [정리] 다시 김개발 씨의 이야기로 돌아가 봅시다.
박시니어 씨의 설명을 들은 김개발 씨는 모든 화면별 Provider에 autoDispose를 추가했습니다. 앱을 다시 실행해보니 메모리 사용량이 안정적으로 유지되었습니다.
autoDispose를 제대로 이해하면 메모리 효율적인 앱을 만들 수 있습니다. 여러분도 오늘 배운 내용을 실제 프로젝트에 적용해 보세요.
실전 팁
💡 - 화면별로 사용되는 데이터는 항상 autoDispose 사용
- 앱 전체에서 사용되는 전역 상태는 autoDispose 제외
- ref.onDispose로 정리 과정을 로깅하면 디버깅에 유용
2. 페이지별 상태 관리
김개발 씨는 autoDispose의 기본 개념을 이해했습니다. 하지만 실제 프로젝트에 적용하려니 새로운 고민이 생겼습니다.
"어떤 Provider에 autoDispose를 붙여야 하고, 어떤 Provider는 붙이지 말아야 할까요?" 박시니어 씨가 웃으며 말했습니다. "페이지별로 생각해보세요."
페이지별 상태 관리는 각 화면에서만 사용되는 데이터를 해당 화면의 생명주기와 함께 관리하는 것입니다. 마치 호텔에서 투숙객마다 개별 룸키를 주는 것처럼, 각 화면마다 독립적인 상태를 관리합니다.
이렇게 하면 화면 간 데이터 충돌을 방지하고 메모리를 효율적으로 사용할 수 있습니다. autoDispose는 이런 페이지별 상태 관리에 최적화되어 있습니다.
다음 코드를 살펴봅시다.
// 상품 상세 페이지용 Provider (autoDispose 사용)
final productDetailProvider = FutureProvider.autoDispose.family<Product, String>(
(ref, productId) async {
// 특정 상품 정보 로드
return await fetchProduct(productId);
},
);
// 장바구니 Provider (autoDispose 미사용 - 전역 상태)
final cartProvider = StateNotifierProvider<CartNotifier, List<CartItem>>(
(ref) => CartNotifier(),
);
// 사용 예시
class ProductDetailPage extends ConsumerWidget {
final String productId;
@override
Widget build(BuildContext context, WidgetRef ref) {
final product = ref.watch(productDetailProvider(productId));
// 페이지를 나가면 자동으로 정리됨
return product.when(...);
}
}
[도입 - 실무 상황 스토리] 김개발 씨는 쇼핑몰 앱을 개발하고 있었습니다. 앱에는 여러 화면이 있었습니다.
홈 화면, 상품 목록, 상품 상세, 장바구니, 마이페이지 등 최소 10개 이상의 화면이었습니다. 각 화면마다 필요한 데이터가 달랐습니다.
상품 상세 화면에서는 해당 상품의 정보가 필요했고, 장바구니에서는 담긴 상품 목록이 필요했습니다. 김개발 씨는 고민에 빠졌습니다.
"어떤 데이터를 autoDispose로 관리하고, 어떤 데이터를 계속 유지해야 할까?" [개념 설명 - 비유로 쉽게] 페이지별 상태 관리를 이해하기 위해 호텔을 떠올려 봅시다. 호텔에는 두 종류의 공간이 있습니다.
하나는 투숙객의 개인 방이고, 다른 하나는 로비나 레스토랑 같은 공용 공간입니다. 개인 방의 물건들은 투숙객이 체크아웃하면 정리됩니다.
하지만 로비의 소파나 안내 데스크는 계속 그 자리에 있어야 합니다. 앱의 데이터도 마찬가지입니다.
특정 화면에서만 필요한 데이터는 개인 방처럼 화면이 닫히면 정리되어야 합니다. 하지만 앱 전체에서 사용되는 데이터는 공용 공간처럼 계속 유지되어야 합니다.
[구분 기준] 그렇다면 어떤 기준으로 구분해야 할까요? autoDispose를 사용해야 하는 경우를 먼저 살펴봅시다.
상품 상세 정보처럼 특정 화면에서만 보는 데이터가 대표적입니다. 검색 결과, 필터 설정, 페이지네이션 상태도 해당 화면에서만 필요합니다.
사용자가 화면을 나가면 이런 데이터는 더 이상 의미가 없습니다. 반대로 autoDispose를 사용하지 말아야 하는 경우도 있습니다.
사용자 로그인 정보, 앱 설정, 테마 설정처럼 앱 전체에서 계속 사용되는 데이터입니다. 장바구니 내용도 여러 화면을 오가면서 유지되어야 하므로 autoDispose를 사용하면 안 됩니다.
[실전 패턴] 실무에서 자주 사용되는 패턴을 소개합니다. 박시니어 씨는 간단한 규칙을 알려주었습니다.
"화면 이름이 Provider 이름에 들어가면 autoDispose를 사용하세요. 예를 들어 ProductDetailProvider, SearchPageProvider 같은 것들이죠." 반대로 UserProvider, SettingsProvider, ThemeProvider처럼 화면 이름이 없는 Provider는 전역 상태이므로 autoDispose를 사용하지 않습니다.
이 간단한 규칙만 기억해도 대부분의 경우를 올바르게 처리할 수 있습니다. [Family modifier 활용] family modifier를 함께 사용하면 더욱 강력합니다.
위 코드의 productDetailProvider를 보세요. family를 사용해 productId를 매개변수로 받습니다.
이렇게 하면 상품마다 독립적인 Provider 인스턴스가 생성됩니다. 사용자가 상품 A를 보다가 상품 B로 이동하면, 상품 A의 Provider는 자동으로 정리됩니다.
이는 메모리 효율성을 극대화하는 패턴입니다. 수백 개의 상품이 있어도 현재 보고 있는 상품의 데이터만 메모리에 유지됩니다.
[화면 전환 시나리오] 실제 사용자의 여정을 따라가 봅시다. 사용자가 홈 화면에서 시작합니다.
상품 목록을 보다가 마음에 드는 상품을 클릭합니다. 이때 productDetailProvider가 생성되고 상품 정보를 불러옵니다.
사용자가 상품을 장바구니에 담고 뒤로가기를 누릅니다. 이 순간 productDetailProvider는 자동으로 정리됩니다.
하지만 cartProvider는 그대로 유지됩니다. 사용자가 다른 상품을 보고 또 담아도 장바구니는 계속 데이터가 쌓입니다.
이것이 바로 페이지별 상태 관리와 전역 상태 관리의 조화입니다. [주의사항] 흔한 실수를 알아봅시다.
초보 개발자들은 종종 장바구니 Provider에 autoDispose를 붙입니다. 그러면 어떻게 될까요?
사용자가 상품을 담고 다른 화면으로 이동했다가 장바구니를 열면 텅 비어있습니다. 장바구니 화면이 닫히는 순간 Provider가 정리되어버렸기 때문입니다.
또 다른 실수는 상세 화면 Provider에 autoDispose를 안 붙이는 것입니다. 사용자가 100개 상품을 둘러보면 100개 상품 데이터가 모두 메모리에 남아있습니다.
앱이 느려질 수밖에 없습니다. [정리] 김개발 씨는 이제 확실히 이해했습니다.
"화면별 데이터는 autoDispose, 앱 전체 데이터는 일반 Provider. 생각보다 간단하네요!" 박시니어 씨가 웃으며 답했습니다.
"네, 원칙은 간단해요. 하지만 실제로는 경험이 쌓여야 직관적으로 판단할 수 있게 됩니다." 여러분도 각 화면의 데이터 특성을 고려해서 적절한 Provider 타입을 선택해보세요.
몇 번 해보면 자연스럽게 익숙해질 겁니다.
실전 팁
💡 - Provider 이름에 화면 이름이 들어가면 autoDispose 사용
- 장바구니, 로그인 정보처럼 여러 화면에서 쓰는 데이터는 제외
- family modifier와 함께 사용하면 매개변수별로 독립 관리 가능
3. ref.keepAlive() 사용법
김개발 씨가 autoDispose를 적용하고 테스트하던 중 새로운 요구사항이 생겼습니다. "검색 결과는 사용자가 다시 돌아왔을 때도 유지되었으면 좋겠어요." 하지만 autoDispose를 사용하면 화면을 나가는 순간 검색 결과가 사라집니다.
박시니어 씨가 해결책을 알려주었습니다. "ref.keepAlive()를 사용해보세요."
**ref.keepAlive()**는 autoDispose Provider의 자동 정리를 선택적으로 방지하는 기능입니다. 마치 호텔에서 레이트 체크아웃을 신청하는 것과 같습니다.
기본적으로는 autoDispose를 사용하지만, 특정 조건에서는 데이터를 유지할 수 있습니다. 이를 통해 자동 정리의 이점과 데이터 유지의 필요성을 동시에 충족할 수 있습니다.
다음 코드를 살펴봅시다.
// 검색 결과 Provider - 조건부 유지
final searchResultProvider = FutureProvider.autoDispose<List<Product>>((ref) async {
final keyword = ref.watch(searchKeywordProvider);
final results = await searchProducts(keyword);
// 검색 결과가 있으면 계속 유지
if (results.isNotEmpty) {
final link = ref.keepAlive();
// 5분 후 자동 정리 (선택사항)
Timer(Duration(minutes: 5), () {
link.close();
});
}
return results;
});
// 수동으로 정리를 트리거할 수도 있음
class SearchPage extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
return IconButton(
icon: Icon(Icons.refresh),
onPressed: () {
// 수동으로 Provider 무효화
ref.invalidate(searchResultProvider);
},
);
}
}
[도입 - 실무 상황 스토리] 김개발 씨는 검색 기능을 구현했습니다. 사용자가 "노트북"을 검색하면 관련 상품 목록이 나타납니다.
그런데 문제가 생겼습니다. 사용자가 상품 하나를 클릭해서 상세 페이지를 보고, 뒤로가기를 누르면 검색 결과가 사라져 있었습니다.
다시 검색 결과를 불러오려면 API를 또 호출해야 했습니다. 네트워크 비용도 비용이지만, 사용자는 로딩 화면을 또 봐야 했습니다.
사용자 경험이 좋지 않았습니다. 김개발 씨는 고민했습니다.
"autoDispose를 빼면 메모리 누수가 생기고, 그대로 두면 사용자 경험이 나빠진다. 어떻게 해야 하지?" [개념 설명 - 비유로 쉽게] **ref.keepAlive()**는 마치 호텔의 레이트 체크아웃 서비스와 같습니다.
보통은 체크아웃 시간이 되면 방을 비워야 합니다. 하지만 레이트 체크아웃을 신청하면 추가 시간 동안 방을 계속 사용할 수 있습니다.
필요할 때까지만 유지하고, 더 이상 필요 없으면 나가면 됩니다. ref.keepAlive()도 똑같습니다.
기본적으로는 화면을 나가면 정리되지만, keepAlive를 호출하면 화면을 나가도 데이터가 유지됩니다. 개발자가 직접 정리 시점을 제어할 수 있는 것이죠.
[사용 시나리오] 어떤 경우에 keepAlive를 사용해야 할까요? 가장 흔한 경우는 검색 결과 캐싱입니다.
사용자가 검색하고, 상세 페이지를 보고, 다시 돌아올 때 검색 결과가 그대로 있으면 좋습니다. 또 다른 예시는 페이지네이션 상태입니다.
사용자가 20페이지까지 스크롤했다면, 잠깐 다른 화면 갔다 와도 20페이지 위치를 기억하고 있어야 합니다. 양식 작성 중인 데이터도 좋은 사례입니다.
사용자가 긴 양식을 작성하다가 잠깐 다른 화면을 보고 돌아왔을 때, 작성 중이던 내용이 날아가면 안 됩니다. [코드 분석] 위 코드를 자세히 살펴봅시다.
keepAlive() 메서드는 KeepAliveLink 객체를 반환합니다. 이 객체가 존재하는 한 Provider는 정리되지 않습니다.
조건문을 보세요. 검색 결과가 비어있지 않을 때만 keepAlive를 호출합니다.
빈 결과는 굳이 유지할 필요가 없으니까요. Timer를 사용한 부분이 흥미롭습니다.
link.close()를 호출하면 keepAlive가 해제되고, 그 순간 아무도 Provider를 사용하지 않으면 자동으로 정리됩니다. 이렇게 하면 5분 동안만 검색 결과를 캐싱하고, 그 이후에는 메모리를 절약할 수 있습니다.
[자동 정리 타이밍] 중요한 개념을 짚고 넘어가겠습니다. keepAlive를 사용해도 Provider는 언젠가는 정리됩니다.
link.close()를 명시적으로 호출하거나, ref.invalidate()로 무효화하면 정리됩니다. 또한 앱이 종료되면 당연히 모든 것이 정리됩니다.
이것이 일반 Provider와의 차이점입니다. 일반 Provider는 앱이 종료될 때까지 절대 정리되지 않지만, keepAlive를 사용한 autoDispose Provider는 개발자가 원하는 시점에 정리할 수 있습니다.
[실전 패턴] 실무에서 자주 쓰는 패턴을 소개합니다. 많은 팀에서 조건부 keepAlive 패턴을 사용합니다.
데이터가 중요하거나, 비용이 많이 들거나, 사용자 경험에 영향을 주는 경우에만 keepAlive를 적용합니다. 모든 Provider에 무분별하게 사용하면 autoDispose의 의미가 없어집니다.
또 다른 패턴은 타임아웃 기반 정리입니다. 위 코드처럼 일정 시간 후 자동으로 정리되게 만듭니다.
이렇게 하면 최근 데이터는 빠르게 접근하면서도 오래된 데이터는 메모리에서 제거할 수 있습니다. [주의사항] keepAlive 사용 시 주의할 점이 있습니다.
모든 Provider에 keepAlive를 사용하면 안 됩니다. 그러면 autoDispose를 쓰는 의미가 없어집니다. 정말 필요한 곳에만 선택적으로 사용해야 합니다.
또한 link.close()를 잊지 마세요. Timer를 사용하든, 특정 이벤트에서 호출하든, 반드시 정리할 방법을 마련해야 합니다. 그렇지 않으면 결국 메모리 누수로 이어집니다.
[실제 사례] 김개발 씨는 검색 기능에 keepAlive를 적용했습니다. 검색 결과가 있으면 5분간 유지하고, 사용자가 새로 검색하거나 새로고침 버튼을 누르면 invalidate로 기존 결과를 정리했습니다.
테스트 결과는 완벽했습니다. 사용자가 상세 페이지를 보고 돌아와도 검색 결과가 그대로 있었습니다.
하지만 5분 후에는 자동으로 정리되어 메모리도 효율적으로 관리되었습니다. [정리] 박시니어 씨가 칭찬했습니다.
"완벽해요! keepAlive는 autoDispose의 유연성을 극대화하는 도구입니다.
자동 정리의 안전성과 데이터 유지의 편의성을 동시에 얻을 수 있죠." 여러분도 캐싱이 필요한 화면에서 keepAlive를 적극 활용해보세요. 사용자 경험과 성능 두 마리 토끼를 잡을 수 있습니다.
실전 팁
💡 - 검색 결과, 페이지네이션 상태처럼 임시 캐싱이 필요한 곳에 사용
- Timer로 자동 정리 시간을 설정하면 메모리 효율성 유지
- ref.invalidate()로 수동 정리도 가능하니 새로고침 버튼에 활용
4. onDispose 콜백 활용
김개발 씨는 autoDispose의 기본기를 마스터했습니다. 하지만 실무에서는 Provider가 정리될 때 추가 작업이 필요한 경우가 많았습니다.
스트림 구독 취소, 타이머 정리, 로그 기록 등이었습니다. 박시니어 씨가 onDispose 콜백에 대해 설명하기 시작했습니다.
onDispose 콜백은 Provider가 정리되기 직전에 실행되는 함수입니다. 마치 퇴근할 때 불을 끄고 문을 잠그는 것처럼, 정리 작업을 수행할 수 있습니다.
스트림 구독 해제, 컨트롤러 dispose, 타이머 취소 등 리소스 정리 작업에 필수적입니다. 메모리 누수를 완벽하게 방지하는 마지막 안전장치입니다.
다음 코드를 살펴봅시다.
// 실시간 알림을 받는 Provider
final notificationProvider = StreamProvider.autoDispose<Notification>((ref) {
// WebSocket 연결 생성
final websocket = WebSocketChannel.connect(
Uri.parse('wss://api.example.com/notifications'),
);
// Provider 정리 시 WebSocket 연결 종료
ref.onDispose(() {
print('WebSocket 연결 종료');
websocket.sink.close();
});
return websocket.stream.map((data) => Notification.fromJson(data));
});
// 타이머를 사용하는 Provider
final autoRefreshProvider = StateNotifierProvider.autoDispose<TimerNotifier, int>((ref) {
final notifier = TimerNotifier();
// 타이머 정리
ref.onDispose(() {
print('타이머 정리');
notifier.dispose();
});
return notifier;
});
[도입 - 실무 상황 스토리] 김개발 씨는 실시간 알림 기능을 개발하고 있었습니다. WebSocket으로 서버와 연결해서 새로운 알림이 오면 화면에 표시하는 기능이었습니다.
개발은 순조롭게 진행되었고, 기능도 잘 작동했습니다. 그런데 QA 테스트에서 문제가 발견되었습니다.
알림 화면을 열었다 닫았다를 반복하면 WebSocket 연결이 계속 쌓인다는 것이었습니다. 서버 로그를 보니 동일한 사용자의 연결이 10개, 20개씩 생성되어 있었습니다.
박시니어 씨가 코드를 보더니 한숨을 쉬었습니다. "WebSocket 연결을 종료하지 않았네요.
Provider가 정리될 때 연결도 함께 정리해야 합니다." [개념 설명 - 비유로 쉽게] onDispose 콜백을 이해하기 위해 사무실 퇴근을 생각해봅시다. 퇴근할 때 우리는 여러 정리 작업을 합니다.
컴퓨터를 끄고, 불을 끄고, 창문을 닫고, 문을 잠급니다. 그냥 가방만 들고 나가면 안 됩니다.
에어컨이 켜진 채로 밤새 돌아가거나, 중요한 서류가 책상 위에 방치될 수 있습니다. Provider도 마찬가지입니다.
단순히 메모리에서 제거되는 것만으로는 부족합니다. WebSocket 연결, 타이머, 스트림 구독처럼 명시적으로 종료해야 하는 리소스들이 있습니다.
onDispose는 이런 정리 작업을 수행하는 곳입니다. [필요한 이유] 왜 onDispose가 필요할까요?
Dart의 가비지 컬렉터는 메모리는 자동으로 정리하지만, 외부 리소스는 정리하지 못합니다. WebSocket 연결, 파일 핸들, 데이터베이스 커넥션 같은 것들은 명시적으로 close()를 호출해야 합니다.
만약 정리하지 않으면 어떻게 될까요? 연결은 계속 열려있고, 서버는 계속 데이터를 보내고, 배터리는 계속 소모됩니다.
사용자는 이미 화면을 나갔는데 말이죠. 이것이 바로 리소스 누수입니다.
[코드 분석] 위 코드를 단계별로 살펴봅시다. 첫 번째 예제의 WebSocket Provider를 보세요.
WebSocketChannel을 생성한 직후, ref.onDispose에 정리 코드를 등록합니다. Provider가 정리될 때 websocket.sink.close()가 자동으로 호출됩니다.
두 번째 예제는 타이머를 사용하는 경우입니다. StateNotifier 내부에서 타이머를 실행하고 있다면, Provider가 정리될 때 notifier.dispose()를 호출해야 합니다.
그렇지 않으면 타이머가 백그라운드에서 계속 돌아갑니다. [실행 순서] onDispose는 정확히 언제 실행될까요?
Provider의 마지막 리스너가 제거되면 즉시 onDispose 콜백들이 실행됩니다. 여러 개의 onDispose를 등록했다면 등록한 순서대로 실행됩니다.
모든 콜백이 완료된 후 Provider의 상태가 메모리에서 제거됩니다. 중요한 점은 동기적으로 실행된다는 것입니다.
async 함수를 사용할 수 없습니다. 정리 작업은 빠르고 확실하게 동기적으로 처리되어야 합니다.
[다양한 활용 사례] onDispose는 다양한 상황에서 활용됩니다. 스트림 구독 취소가 가장 흔합니다.
Stream.listen()으로 구독을 시작했다면, onDispose에서 subscription.cancel()을 호출해야 합니다. 애니메이션 컨트롤러도 마찬가지입니다.
AnimationController를 생성했다면 dispose()를 호출해야 합니다. 로그 기록에도 유용합니다.
Provider가 얼마나 자주 생성되고 정리되는지 추적하면 성능 최적화에 도움이 됩니다. 디버깅할 때도 "어떤 Provider가 왜 정리되지 않는가?"를 파악하는 데 onDispose 로그가 큰 도움이 됩니다.
[주의사항] onDispose 사용 시 주의할 점이 있습니다. 절대 async를 사용하지 마세요. onDispose는 동기 함수여야 합니다.
만약 비동기 정리가 필요하다면 Provider 설계를 다시 고민해봐야 합니다. 또한 예외 처리를 신중하게 하세요.
onDispose에서 예외가 발생하면 다른 정리 작업이 실행되지 않을 수 있습니다. try-catch로 감싸서 하나의 정리 실패가 전체에 영향을 주지 않도록 해야 합니다.
[실제 사례] 김개발 씨는 WebSocket Provider에 onDispose를 추가했습니다. 화면을 열고 닫을 때마다 콘솔에 "WebSocket 연결 종료" 메시지가 출력되었습니다.
서버 로그를 확인해보니 연결이 깔끔하게 정리되고 있었습니다. 더 이상 좀비 연결은 없었습니다.
메모리 사용량도 안정적이었고, 배터리 소모도 줄어들었습니다. QA팀에서 다시 테스트했고, 문제없이 통과했습니다.
[정리] 박시니어 씨가 만족스러워했습니다. "이제 완벽한 리소스 관리를 하고 있어요.
autoDispose로 자동 정리하고, onDispose로 외부 리소스까지 정리하니 메모리 누수 걱정이 없습니다." 여러분도 WebSocket, 타이머, 스트림을 사용할 때는 반드시 onDispose에서 정리 작업을 수행하세요. 이것이 프로페셔널한 개발자의 자세입니다.
실전 팁
💡 - WebSocket, 스트림 구독은 반드시 onDispose에서 정리
- 로그를 추가해서 Provider 생명주기 추적하면 디버깅에 유용
- try-catch로 감싸서 하나의 정리 실패가 전체에 영향 주지 않도록
5. 타이머/컨트롤러 정리
김개발 씨는 자동 새로고침 기능을 개발하고 있었습니다. 30초마다 서버에서 최신 데이터를 가져오는 기능이었습니다.
Timer.periodic을 사용해서 구현했는데, 화면을 나가도 타이머가 계속 돌아가는 문제가 있었습니다. "타이머도 정리해야 하는군요." 김개발 씨는 깨달았습니다.
타이머와 컨트롤러 정리는 시간 기반 작업과 애니메이션 리소스를 올바르게 해제하는 것입니다. 마치 알람을 설정했다면 필요 없을 때 꺼야 하는 것과 같습니다.
Timer, AnimationController, TextEditingController 등은 명시적으로 cancel() 또는 dispose()를 호출해야 합니다. 이를 통해 불필요한 CPU 사용과 메모리 누수를 방지할 수 있습니다.
다음 코드를 살펴봅시다.
// 자동 새로고침 Provider
final autoRefreshProvider = StateNotifierProvider.autoDispose<RefreshNotifier, DateTime>(
(ref) {
Timer? timer;
// 30초마다 새로고침
timer = Timer.periodic(Duration(seconds: 30), (t) {
ref.notifier.refresh();
});
// 타이머 정리
ref.onDispose(() {
timer?.cancel();
print('자동 새로고침 타이머 정리');
});
return RefreshNotifier();
},
);
// 애니메이션 컨트롤러를 사용하는 Provider
final animationProvider = Provider.autoDispose<AnimationController>((ref) {
final vsync = ref.watch(tickerProviderProvider);
final controller = AnimationController(
duration: Duration(milliseconds: 300),
vsync: vsync,
);
// 컨트롤러 정리
ref.onDispose(() {
controller.dispose();
print('애니메이션 컨트롤러 정리');
});
return controller;
});
[도입 - 실무 상황 스토리] 김개발 씨는 대시보드 화면을 개발했습니다. 실시간으로 서버 상태를 모니터링하는 화면이었습니다.
30초마다 자동으로 최신 데이터를 가져와서 차트를 업데이트했습니다. 개발이 완료되고 테스트를 시작했습니다.
화면은 잘 작동했습니다. 그런데 이상한 현상을 발견했습니다.
대시보드를 나가서 다른 화면에 있어도 네트워크 요청이 계속 발생했습니다. 로그를 보니 타이머가 계속 실행되고 있었습니다.
박시니어 씨가 설명했습니다. "Timer는 일반 객체와 다릅니다.
명시적으로 cancel()을 호출하지 않으면 계속 실행됩니다. 심지어 화면이 닫혀도요." [개념 설명 - 비유로 쉽게] 타이머를 이해하기 위해 알람시계를 떠올려 봅시다.
아침 6시에 울리도록 알람을 설정했습니다. 그런데 주말이라 일찍 일어날 필요가 없어졌습니다.
알람을 끄지 않으면 어떻게 될까요? 당연히 6시에 알람이 울립니다.
알람시계는 여러분이 주말이라는 걸 알지 못합니다. 명시적으로 꺼야만 멈춥니다.
Dart의 Timer도 똑같습니다. 한번 시작하면 cancel()을 호출할 때까지 계속 실행됩니다.
화면이 닫혔는지, 사용자가 다른 곳으로 이동했는지 알지 못합니다. 개발자가 직접 정리해야 합니다.
[Timer의 종류] Dart에는 두 종류의 타이머가 있습니다. Timer.periodic은 일정 간격으로 반복 실행됩니다.
위 코드처럼 30초마다 새로고침하는 경우에 사용합니다. Timer (일반)는 한 번만 실행됩니다.
예를 들어 5초 후 팝업을 자동으로 닫는 경우에 사용하죠. 두 종류 모두 cancel() 메서드로 정리해야 합니다.
특히 periodic 타이머는 계속 반복되므로 정리하지 않으면 심각한 성능 문제를 일으킵니다. [Controller 정리] Flutter에는 다양한 컨트롤러가 있습니다.
AnimationController는 애니메이션을 제어합니다. TextEditingController는 텍스트 입력을 관리합니다.
ScrollController는 스크롤 위치를 추적합니다. 이들은 모두 dispose() 메서드를 가지고 있습니다.
왜 dispose()가 필요할까요? 컨트롤러는 내부적으로 리스너를 관리합니다.
화면이 리빌드될 때마다 컨트롤러의 상태를 확인하기 위해 리스너를 등록합니다. dispose()를 호출하지 않으면 이 리스너들이 메모리에 계속 남아있습니다.
[코드 분석] 위 코드를 자세히 살펴봅시다. 첫 번째 예제에서는 Timer? 타입으로 선언했습니다.
nullable로 선언한 이유는 onDispose에서 접근하기 위해서입니다. Timer.periodic으로 타이머를 생성하고, ref.onDispose에서 timer?.cancel()을 호출합니다.
두 번째 예제의 AnimationController를 보세요. vsync는 애니메이션의 프레임 타이밍을 제어하는 중요한 파라미터입니다.
컨트롤러를 생성한 후, 반드시 onDispose에서 controller.dispose()를 호출합니다. [실행 흐름] 실제로 어떻게 동작하는지 흐름을 따라가 봅시다.
사용자가 대시보드 화면에 진입합니다. autoRefreshProvider가 생성되고 타이머가 시작됩니다.
30초마다 refresh() 메서드가 호출되어 서버에서 데이터를 가져옵니다. 차트가 업데이트됩니다.
사용자가 뒤로가기를 누릅니다. 화면이 dispose되고, Provider의 마지막 리스너가 제거됩니다.
즉시 onDispose 콜백이 실행되고, timer.cancel()이 호출됩니다. 타이머가 멈춥니다.
이제 네트워크 요청도 멈춥니다. [흔한 실수] 초보 개발자들이 자주 하는 실수를 알아봅시다.
가장 흔한 실수는 타이머 변수를 저장하지 않는 것입니다. Timer.periodic()을 호출만 하고 반환값을 저장하지 않으면 나중에 cancel()을 호출할 방법이 없습니다.
반드시 변수에 저장해야 합니다. 또 다른 실수는 dispose를 두 번 호출하는 것입니다.
이미 dispose된 컨트롤러를 다시 dispose하면 에러가 발생합니다. 플래그 변수나 nullable로 관리해서 중복 호출을 방지해야 합니다.
[성능 영향] 타이머를 정리하지 않으면 어떤 영향이 있을까요? CPU 사용률이 계속 증가합니다.
타이머가 10개, 20개 쌓이면 백그라운드에서 계속 콜백을 실행합니다. 배터리도 빠르게 소모됩니다.
불필요한 네트워크 요청으로 데이터 비용도 낭비됩니다. 무엇보다 사용자 경험이 나빠집니다.
앱이 느려지고, 때로는 예상치 못한 화면 업데이트가 발생합니다. 이미 닫은 화면의 데이터가 갑자기 나타나는 이상한 버그도 생길 수 있습니다.
[실제 사례] 김개발 씨는 모든 타이머와 컨트롤러에 정리 코드를 추가했습니다. 테스트해보니 화면을 나가는 순간 타이머가 깔끔하게 정리되었습니다.
네트워크 탭을 보니 불필요한 요청이 사라졌습니다. 배터리 프로파일러로 확인해보니 배터리 소모도 크게 줄었습니다.
화면을 여러 번 오가는 스트레스 테스트에서도 성능이 일정하게 유지되었습니다. [정리] 박시니어 씨가 고개를 끄덕였습니다.
"완벽합니다. Timer와 Controller는 특히 주의해야 해요.
정리하지 않으면 눈에 보이지 않게 앱을 좀먹습니다." 여러분도 타이머나 컨트롤러를 사용할 때는 반드시 정리 코드를 함께 작성하세요. onDispose와 함께라면 어렵지 않습니다.
실전 팁
💡 - Timer.periodic 사용 시 반드시 변수에 저장해서 나중에 cancel() 호출
- AnimationController는 생성과 동시에 onDispose에 정리 코드 작성
- 로그를 추가해서 타이머가 제대로 정리되는지 확인
6. 메모리 사용량 비교
김개발 씨는 모든 개선 작업을 완료했습니다. 하지만 정말 효과가 있는지 궁금했습니다.
"숫자로 확인해보고 싶어요." 박시니어 씨가 DevTools를 열며 말했습니다. "좋아요, 직접 비교해봅시다."
메모리 사용량 비교는 autoDispose 적용 전후의 실제 성능 차이를 측정하는 것입니다. 마치 다이어트 전후 체중을 재는 것처럼, 구체적인 수치로 개선 효과를 확인합니다.
Flutter DevTools의 Memory 탭을 사용하면 실시간으로 메모리 사용량을 추적할 수 있습니다. 이를 통해 메모리 누수를 발견하고 최적화 효과를 검증할 수 있습니다.
다음 코드를 살펴봅시다.
// 개선 전 - autoDispose 없음
final oldProductProvider = FutureProvider.family<Product, String>(
(ref, id) async => await fetchProduct(id),
);
// 개선 후 - autoDispose 사용
final newProductProvider = FutureProvider.autoDispose.family<Product, String>(
(ref, id) async {
final product = await fetchProduct(id);
ref.onDispose(() {
print('상품 $id 데이터 정리됨');
});
return product;
},
);
// 메모리 사용량 테스트 헬퍼
class MemoryTestHelper {
static Future<void> stressTest() async {
// 100개 상품을 순차적으로 열었다 닫기
for (int i = 0; i < 100; i++) {
// 상품 페이지 열기
await navigateToProduct(i.toString());
await Future.delayed(Duration(seconds: 1));
// 뒤로가기
navigateBack();
await Future.delayed(Duration(milliseconds: 500));
if (i % 10 == 0) {
print('진행: $i/100');
}
}
}
}
[도입 - 실무 상황 스토리] 김개발 씨는 한 달 동안 열심히 autoDispose를 적용했습니다. 모든 화면별 Provider에 autoDispose를 추가하고, 타이머와 컨트롤러도 정리했습니다.
onDispose 콜백도 빠짐없이 작성했습니다. 하지만 의문이 들었습니다.
"정말 효과가 있을까? 실제로 메모리가 줄어들었을까?" 코드는 깔끔해 보였지만, 구체적인 수치로 확인하고 싶었습니다.
박시니어 씨가 Flutter DevTools를 열었습니다. "직접 측정해봅시다.
개선 전 버전과 개선 후 버전을 비교해보면 확실히 알 수 있어요." [측정 준비] 메모리 사용량을 비교하려면 동일한 조건이 필요합니다. 먼저 테스트 시나리오를 정했습니다.
상품 페이지를 100번 열었다 닫는 것입니다. 사용자가 쇼핑몰에서 여러 상품을 둘러보는 상황을 시뮬레이션하는 것이죠.
각 상품 페이지는 약 2MB 정도의 데이터를 로드합니다. 개선 전 버전은 autoDispose 없이 일반 Provider만 사용합니다.
개선 후 버전은 모든 화면별 Provider에 autoDispose를 적용한 버전입니다. 동일한 디바이스에서 동일한 시나리오로 테스트합니다.
[DevTools 사용법] Flutter DevTools의 Memory 탭을 활용합니다. 앱을 실행하고 DevTools에 연결합니다.
Memory 탭을 열면 실시간 메모리 그래프가 보입니다. Memory 그래프는 전체 메모리 사용량을, Dart Heap 그래프는 Dart 객체가 사용하는 메모리를 보여줍니다.
Snapshot 버튼으로 특정 시점의 메모리 상태를 저장할 수 있습니다. 테스트 전과 후의 스냅샷을 비교하면 어떤 객체가 메모리에 남아있는지 확인할 수 있습니다.
[개선 전 측정] 먼저 autoDispose 없는 버전을 테스트했습니다. 시작 시 메모리 사용량은 150MB였습니다.
상품 페이지를 10개 열었다 닫으니 170MB로 증가했습니다. 50개 후에는 230MB, 100개 후에는 무려 350MB까지 올라갔습니다.
그래프를 보니 계단식으로 올라가기만 했습니다. 절대 내려가지 않았습니다.
각 상품의 데이터가 메모리에 그대로 남아있는 것이 분명했습니다. 스냅샷을 분석하니 100개 상품의 Product 객체가 모두 메모리에 있었습니다.
[개선 후 측정] 이제 autoDispose를 적용한 버전을 테스트했습니다. 시작 시 메모리는 동일하게 150MB였습니다.
상품 페이지를 10개 열었다 닫으니 154MB였습니다. 약간 증가했지만 큰 차이는 없었습니다.
50개 후에는 158MB, 100개 후에도 165MB에 불과했습니다. 그래프 모양이 완전히 달랐습니다.
톱니바퀴처럼 올라갔다 내려갔다를 반복했습니다. 상품 페이지를 열 때 메모리가 올라가고, 닫을 때 내려갔습니다.
스냅샷을 보니 현재 화면의 Product 객체만 메모리에 있었습니다. [수치 비교] 결과를 표로 정리했습니다.
개선 전: 시작 150MB → 100번 후 350MB (200MB 증가) 개선 후: 시작 150MB → 100번 후 165MB (15MB 증가) 메모리 절감 효과는 무려 185MB였습니다. 동일한 작업을 수행했는데 메모리 사용량이 절반 이하로 줄어들었습니다.
이것이 autoDispose의 실제 효과입니다. [장기 실행 테스트] 더 극단적인 테스트도 해봤습니다.
1000개 상품을 테스트했습니다. 개선 전 버전은 500개쯤에서 앱이 느려지기 시작했고, 800개를 넘어가니 크래시가 발생했습니다.
Out of Memory 에러였습니다. 개선 후 버전은 1000개를 모두 완주했습니다.
메모리 사용량은 여전히 170MB 정도에서 안정적으로 유지되었습니다. 앱 속도도 처음과 동일하게 빠릿빠릿했습니다.
[실제 사용자 시나리오] 실제 사용자는 어떤 경험을 할까요? 일반 사용자는 하루에 20-30개 정도의 상품을 둘러봅니다.
개선 전에는 앱을 오래 사용할수록 느려졌습니다. 30분 사용 후에는 화면 전환이 버벅거렸습니다.
개선 후에는 하루 종일 사용해도 성능이 동일했습니다. 메모리가 효율적으로 관리되어 항상 깨끗한 상태를 유지했습니다.
사용자는 이런 기술적 세부사항을 모르지만, "앱이 빠르고 부드럽다"고 느낍니다. [기타 개선 효과] 메모리 외에도 다른 이점이 있었습니다.
앱 시작 속도가 빨라졌습니다. 백그라운드에서 돌아올 때도 더 빠르게 재개되었습니다.
배터리 소모도 줄어들었습니다. 불필요한 데이터 처리가 없으니 CPU 사용률이 낮아진 것이죠.
무엇보다 안정성이 크게 향상되었습니다. 메모리 부족으로 인한 크래시가 사라졌습니다.
QA 테스트에서 통과율이 95%에서 100%로 올라갔습니다. [모니터링] 실제 프로덕션 환경에서도 모니터링을 계속했습니다.
Firebase Performance Monitoring을 사용해 실사용자의 메모리 사용량을 추적했습니다. autoDispose 적용 전에는 평균 메모리 사용량이 280MB였습니다.
적용 후에는 180MB로 감소했습니다. 특히 메모리 경고(Memory Warning) 발생 횟수가 90% 감소했습니다.
저사양 디바이스에서 앱이 종료되는 비율도 크게 줄어들었습니다. [정리] 김개발 씨는 숫자를 보고 감탄했습니다.
"와, 이렇게까지 차이가 나는군요!" 박시니어 씨가 웃으며 답했습니다. "네, 코드 몇 줄의 차이가 사용자 경험에는 엄청난 차이를 만듭니다.
autoDispose는 작은 노력으로 큰 효과를 얻을 수 있는 최고의 투자입니다." 여러분도 DevTools로 메모리 사용량을 측정해보세요. 숫자로 확인하면 최적화의 중요성을 더욱 실감할 수 있습니다.
실전 팁
💡 - Flutter DevTools Memory 탭으로 실시간 메모리 추적
- Snapshot으로 어떤 객체가 메모리에 남아있는지 분석
- Firebase Performance Monitoring으로 실사용자 데이터 수집
이상으로 학습을 마칩니다. 위 내용을 직접 코드로 작성해보면서 익혀보세요!
댓글 (0)
함께 보면 좋은 카드 뉴스
LangGraph Fault Tolerance 장애 복구 완벽 가이드
LangGraph 애플리케이션의 장애 복구 메커니즘을 실무 중심으로 배웁니다. Durable Execution부터 Graph Migrations까지 체크포인트 기반 복구 시스템을 스토리텔링으로 쉽게 이해할 수 있습니다.
LangGraph Time Travel 완벽 가이드
LangGraph의 Time Travel 기능으로 특정 시점으로 돌아가고, 상태를 포크하여 분기하고, 대안 경로를 탐색하는 방법을 배웁니다. 실무에서 디버깅과 실험에 활용하는 실전 가이드입니다.
LangGraph Persistence 완벽 가이드
LangGraph의 Persistence 기능을 활용하여 대화 상태를 저장하고 관리하는 방법을 배웁니다. Thread와 Checkpoint를 이해하고, 상태 조회 및 수정 방법을 실무 중심으로 학습합니다.
Riverpod 3.0 쇼핑 앱 종합 프로젝트 완벽 가이드
Flutter와 Riverpod 3.0을 활용한 실무 수준의 쇼핑 앱 개발 과정을 단계별로 학습합니다. 상품 목록, 장바구니, 주문, 인증, 검색 기능까지 모든 핵심 기능을 구현하며 상태 관리의 실전 노하우를 익힙니다.
Riverpod 3.0 Retry 자동 재시도 완벽 가이드
Riverpod 3.0에 새로 추가된 Retry 기능을 활용하여 네트워크 오류나 일시적인 실패 상황에서 자동으로 재시도하는 방법을 배웁니다. 초급 개발자도 쉽게 따라할 수 있도록 실무 예제와 함께 설명합니다.