본 콘텐츠의 이미지 및 내용은 AI로 생성되었습니다.
본 콘텐츠의 이미지 및 내용을 무단으로 복제, 배포, 수정하여 사용할 경우 저작권법에 의해 법적 제재를 받을 수 있습니다.
이미지 로딩 중...
AI Generated
2025. 11. 30. · 12 Views
AsyncValue로 로딩/에러/데이터 상태 관리 완벽 가이드
Flutter Riverpod에서 비동기 상태를 우아하게 관리하는 AsyncValue의 모든 것을 알아봅니다. 로딩, 성공, 에러 세 가지 상태를 하나의 타입으로 처리하는 방법을 실무 예제와 함께 배워봅니다.
목차
- AsyncValue란?
- AsyncData - 성공 상태
- AsyncLoading - 로딩 상태
- AsyncError - 에러 상태
- switch 패턴으로 상태 분기
- when() 메서드 활용
- .value와 .valueOrNull
1. AsyncValue란?
김개발 씨는 오늘 API에서 데이터를 불러오는 화면을 만들고 있었습니다. 로딩 중일 때는 스피너를, 에러가 나면 에러 메시지를, 성공하면 데이터를 보여줘야 합니다.
"이 세 가지 상태를 어떻게 깔끔하게 관리하지?" 고민하던 그때, 선배가 다가와 말했습니다. "AsyncValue 써봤어요?"
AsyncValue는 Riverpod에서 제공하는 비동기 상태를 표현하는 sealed class입니다. 마치 택배 배송 상태처럼, 데이터가 "배송 중(로딩)", "배송 완료(성공)", "배송 실패(에러)" 중 어느 상태인지를 하나의 타입으로 표현합니다.
이것을 사용하면 비동기 처리의 모든 상태를 누락 없이 안전하게 다룰 수 있습니다.
다음 코드를 살펴봅시다.
// AsyncValue는 세 가지 상태 중 하나를 가집니다
import 'package:flutter_riverpod/flutter_riverpod.dart';
// 1. 로딩 상태
final loading = AsyncValue<String>.loading();
// 2. 성공 상태 (데이터 포함)
final success = AsyncValue<String>.data('안녕하세요!');
// 3. 에러 상태 (에러와 스택트레이스 포함)
final error = AsyncValue<String>.error(
Exception('네트워크 오류'),
StackTrace.current,
);
김개발 씨는 입사 4개월 차 Flutter 개발자입니다. 이번 주 미션은 서버에서 사용자 프로필을 불러와 화면에 표시하는 기능을 만드는 것이었습니다.
처음에는 간단해 보였습니다. API를 호출하고 데이터를 받아서 보여주면 되니까요.
하지만 막상 구현하려니 생각보다 복잡했습니다. 데이터를 불러오는 동안에는 로딩 스피너를 보여줘야 합니다.
성공하면 데이터를 화면에 그려야 합니다. 실패하면 에러 메시지와 재시도 버튼을 보여줘야 합니다.
이 세 가지 상태를 어떻게 관리해야 할까요? 처음에 김개발 씨는 isLoading, hasError, data 세 개의 변수를 따로 만들었습니다.
그런데 코드가 점점 지저분해졌습니다. isLoading이 true인데 data도 있으면 어떻게 해야 하지?
hasError가 true인데 isLoading도 true면? 상태 조합이 너무 많아져서 머리가 아팠습니다.
바로 이 문제를 해결하기 위해 AsyncValue가 등장했습니다. AsyncValue는 마치 택배 배송 추적 시스템과 같습니다.
택배는 "배송 준비 중", "배송 중", "배송 완료", "배송 실패" 중 하나의 상태만 가질 수 있습니다. 동시에 두 상태에 있을 수는 없습니다.
AsyncValue도 마찬가지입니다. AsyncLoading, AsyncData, AsyncError 세 가지 상태 중 정확히 하나만 가집니다.
Dart 3.0부터 도입된 sealed class 덕분에 이런 설계가 가능해졌습니다. sealed class는 상속받을 수 있는 클래스를 제한합니다.
AsyncValue를 상속받을 수 있는 것은 오직 AsyncLoading, AsyncData, AsyncError 세 가지뿐입니다. 컴파일러가 이를 알고 있기 때문에, 모든 케이스를 처리했는지 검사해줍니다.
위 코드를 살펴보겠습니다. 첫 번째로 AsyncValue.loading()은 아직 데이터를 불러오는 중임을 나타냅니다.
두 번째로 AsyncValue.data()는 성공적으로 데이터를 받았음을 나타내며, 괄호 안에 실제 데이터가 들어갑니다. 세 번째로 AsyncValue.error()는 에러가 발생했음을 나타내며, 에러 객체와 스택트레이스를 포함합니다.
실제로 Riverpod의 FutureProvider나 StreamProvider를 사용하면, 프레임워크가 자동으로 AsyncValue를 만들어줍니다. 개발자는 그저 이 값을 읽고 적절히 화면을 그리기만 하면 됩니다.
상태 전환도 Riverpod이 알아서 처리합니다. 김개발 씨는 AsyncValue를 알게 된 후 코드를 완전히 다시 작성했습니다.
세 개의 변수 대신 하나의 AsyncValue만 사용하니 코드가 훨씬 깔끔해졌습니다. 무엇보다 "이 상태 조합은 처리 안 해도 되나?"라는 걱정이 사라졌습니다.
컴파일러가 누락된 케이스를 알려주니까요.
실전 팁
💡 - AsyncValue는 제네릭 타입이므로 AsyncValue<User>, AsyncValue<List<Product>> 등 어떤 타입이든 감쌀 수 있습니다
- FutureProvider와 StreamProvider를 사용하면 AsyncValue가 자동으로 생성되므로 직접 만들 일은 드뭅니다
2. AsyncData - 성공 상태
API 호출이 성공했습니다! 서버에서 데이터가 무사히 도착했습니다.
이제 이 데이터를 화면에 보여줘야 합니다. AsyncValue의 세 가지 상태 중 가장 행복한 상태, AsyncData를 만나볼 시간입니다.
AsyncData는 비동기 작업이 성공적으로 완료되어 데이터를 가지고 있는 상태입니다. 마치 택배가 무사히 도착해서 상자 안에 물건이 들어있는 것과 같습니다.
value 속성을 통해 실제 데이터에 접근할 수 있으며, 이 데이터는 null이 아님이 보장됩니다.
다음 코드를 살펴봅시다.
// AsyncData 생성 및 사용
final userState = AsyncValue<User>.data(
User(id: 1, name: '김개발', email: 'kim@dev.com'),
);
// value로 데이터 접근 (null이 아님이 보장됨)
if (userState is AsyncData<User>) {
final user = userState.value;
print('사용자 이름: ${user.name}');
print('이메일: ${user.email}');
}
// hasValue로 데이터 존재 확인
print(userState.hasValue); // true
print(userState.isLoading); // false
print(userState.hasError); // false
드디어 기다리던 순간이 왔습니다. 서버에 요청을 보내고, 네트워크를 통해 데이터가 전송되고, 마침내 응답이 도착했습니다.
김개발 씨의 앱에 사용자 정보가 성공적으로 로드되었습니다. AsyncData는 이 행복한 순간을 표현하는 클래스입니다.
비유하자면, 온라인 쇼핑을 하고 택배를 기다리다가 마침내 "배송 완료" 알림을 받은 것과 같습니다. 상자를 열면 주문한 물건이 들어있습니다.
AsyncData도 마찬가지입니다. 안에 우리가 원하던 데이터가 들어있습니다.
AsyncData의 가장 중요한 속성은 value입니다. 이 속성을 통해 실제 데이터에 접근할 수 있습니다.
그리고 매우 중요한 점이 있습니다. value는 절대 null이 아닙니다.
AsyncData라는 것 자체가 "데이터가 있다"는 의미이기 때문입니다. 따라서 null 체크 없이 안전하게 사용할 수 있습니다.
위 코드를 살펴보겠습니다. AsyncValue.data() 생성자로 User 객체를 감싸서 AsyncData를 만들었습니다.
is AsyncData<User>로 타입 검사를 하면, 그 블록 안에서는 userState.value가 User 타입임이 보장됩니다. Dart의 스마트 캐스팅 덕분입니다.
AsyncValue에는 상태를 확인할 수 있는 편리한 속성들이 있습니다. hasValue는 데이터가 있는지를 boolean으로 알려줍니다.
AsyncData일 때 true입니다. isLoading은 로딩 중인지, hasError는 에러가 있는지 알려줍니다.
실무에서 AsyncData를 직접 만드는 경우는 많지 않습니다. 대부분 FutureProvider나 StreamProvider가 자동으로 만들어주기 때문입니다.
개발자가 해야 할 일은 이 상태를 받아서 적절한 UI를 그리는 것입니다. 한 가지 알아두면 좋은 점이 있습니다.
AsyncData는 이전 데이터를 기억할 수 있습니다. 예를 들어 새로고침을 할 때, 완전히 로딩 상태로 돌아가는 대신 이전 데이터를 보여주면서 동시에 로딩 인디케이터를 표시할 수 있습니다.
이를 "stale-while-revalidate" 패턴이라고 부릅니다. 사용자 경험을 크게 향상시키는 기법입니다.
김개발 씨는 이제 데이터가 성공적으로 로드됐을 때 어떻게 처리해야 하는지 알게 되었습니다. 하지만 항상 성공만 하면 좋겠지만, 현실은 그렇지 않습니다.
다음으로 로딩 상태를 살펴보겠습니다.
실전 팁
💡 - AsyncData의 value는 null이 아님이 보장되므로 null 체크가 필요 없습니다
- hasValue, isLoading, hasError 속성으로 간단히 상태를 확인할 수 있습니다
3. AsyncLoading - 로딩 상태
사용자가 버튼을 눌렀습니다. 서버에 요청이 날아갔습니다.
응답이 올 때까지 사용자에게 뭔가 보여줘야 합니다. "잠시만 기다려주세요, 데이터를 불러오고 있습니다." 이 대기 시간을 우아하게 처리하는 AsyncLoading을 알아봅시다.
AsyncLoading은 비동기 작업이 진행 중인 상태를 나타냅니다. 마치 식당에서 음식을 주문하고 기다리는 것과 같습니다.
아직 음식이 나오지 않았지만, 주방에서 열심히 요리하고 있다는 것을 알고 있습니다. 이 상태에서는 보통 로딩 스피너나 스켈레톤 UI를 보여줍니다.
다음 코드를 살펴봅시다.
// AsyncLoading 생성
final loadingState = AsyncValue<User>.loading();
// 로딩 상태 확인
print(loadingState.isLoading); // true
print(loadingState.hasValue); // false
print(loadingState.hasError); // false
// 새로고침 시 이전 데이터 유지하며 로딩
final previousUser = User(id: 1, name: '김개발');
final refreshingState = AsyncLoading<User>()
.copyWithPrevious(AsyncData(previousUser));
// 이전 데이터가 있으면 함께 표시 가능
print(refreshingState.isLoading); // true
print(refreshingState.hasValue); // true (이전 값 존재)
김개발 씨의 앱을 사용하는 사용자가 화면을 열었습니다. 앱은 서버에 데이터를 요청합니다.
하지만 네트워크를 통해 데이터가 오려면 시간이 걸립니다. 빠르면 0.1초, 느리면 몇 초가 걸릴 수도 있습니다.
이 시간 동안 사용자에게 아무것도 보여주지 않으면 앱이 멈춘 것처럼 보일 것입니다. AsyncLoading은 바로 이 "기다리는 중" 상태를 표현합니다.
음식점에서 주문하고 기다리는 상황을 생각해보세요. 점원이 "주문하신 음식 준비 중입니다"라고 말해주면 안심이 됩니다.
마찬가지로 앱에서도 "데이터 로딩 중"이라는 신호를 보내줘야 합니다. 보통은 동그란 스피너가 빙글빙글 도는 것으로 표현합니다.
AsyncLoading의 기본적인 사용법은 간단합니다. AsyncValue.loading()으로 생성하면 됩니다.
isLoading 속성이 true이고, hasValue와 hasError는 false입니다. 아직 데이터도 없고, 에러도 없으니까요.
그런데 여기서 재미있는 기능이 있습니다. 바로 이전 데이터 유지 기능입니다.
새로고침 상황을 생각해보세요. 사용자가 이미 데이터를 보고 있는데, 최신 데이터를 받아오기 위해 새로고침을 했습니다.
이때 화면을 완전히 비우고 로딩 스피너만 보여주면 좋지 않은 경험입니다. 대신 기존 데이터를 보여주면서 위에 살짝 로딩 인디케이터를 띄우는 게 훨씬 좋습니다.
AsyncLoading의 copyWithPrevious 메서드가 이를 가능하게 합니다. 이전의 AsyncData를 전달하면, 로딩 상태이면서 동시에 이전 데이터도 가지고 있는 상태가 됩니다.
isLoading도 true이고 hasValue도 true인 상태가 되는 것입니다. 이 패턴은 현대 앱에서 매우 중요합니다.
Instagram을 생각해보세요. 피드를 아래로 당겨 새로고침하면, 기존 게시물은 그대로 보이면서 위에 로딩 인디케이터가 나타납니다.
이것이 바로 "이전 데이터를 유지하면서 로딩"하는 패턴입니다. 실무에서 주의할 점이 있습니다.
처음 화면에 진입했을 때의 로딩과, 새로고침할 때의 로딩을 다르게 처리해야 할 수 있습니다. 처음에는 전체 화면 로딩을, 새로고침에는 작은 인디케이터를 보여주는 식입니다.
hasValue를 확인하면 이를 구분할 수 있습니다. 김개발 씨는 로딩 상태 처리의 중요성을 깨달았습니다.
사용자 경험을 위해 로딩 시간도 세심하게 관리해야 한다는 것을요. 하지만 모든 요청이 성공하지는 않습니다.
다음으로 에러 상태를 알아보겠습니다.
실전 팁
💡 - 처음 로딩과 새로고침 로딩을 구분하려면 hasValue를 확인하세요
- 로딩이 길어질 경우를 대비해 타임아웃 처리도 고려하세요
4. AsyncError - 에러 상태
네트워크가 끊겼습니다. 서버가 500 에러를 반환했습니다.
뭔가 잘못됐습니다. 하지만 앱이 그냥 멈춰버리면 안 됩니다.
사용자에게 무엇이 잘못됐는지 알려주고, 다시 시도할 수 있는 기회를 줘야 합니다. AsyncError가 바로 이 역할을 담당합니다.
AsyncError는 비동기 작업 중 에러가 발생한 상태를 나타냅니다. 마치 택배가 배송 중 분실되거나 파손된 상황과 같습니다.
error 속성으로 무엇이 잘못됐는지 확인할 수 있고, stackTrace로 어디서 문제가 발생했는지 추적할 수 있습니다.
다음 코드를 살펴봅시다.
// AsyncError 생성
final errorState = AsyncValue<User>.error(
Exception('서버 연결에 실패했습니다'),
StackTrace.current,
);
// 에러 정보 접근
if (errorState is AsyncError<User>) {
print('에러: ${errorState.error}');
print('스택트레이스: ${errorState.stackTrace}');
}
// 상태 확인
print(errorState.hasError); // true
print(errorState.isLoading); // false
print(errorState.hasValue); // false
// 에러 발생 후에도 이전 데이터 유지 가능
final previousData = AsyncData(User(id: 1, name: '김개발'));
final errorWithPrevious = AsyncError<User>(
Exception('새로고침 실패'),
StackTrace.current,
).copyWithPrevious(previousData);
완벽한 앱은 없습니다. 네트워크는 끊어지고, 서버는 다운되고, 예상치 못한 데이터가 들어옵니다.
김개발 씨도 이 사실을 뼈저리게 깨달았습니다. 개발 환경에서는 완벽하게 동작하던 앱이, 실제 사용자 환경에서는 온갖 에러를 만났습니다.
AsyncError는 "뭔가 잘못됐다"는 상태를 표현합니다. 택배 배송으로 비유하자면, "배송 실패"에 해당합니다.
단순히 실패했다는 사실만 알려주는 게 아니라, 왜 실패했는지도 함께 알려줍니다. AsyncError는 두 가지 중요한 정보를 담고 있습니다.
첫 번째는 error입니다. 어떤 에러가 발생했는지를 담고 있습니다.
Exception이나 Error 객체가 될 수 있습니다. 두 번째는 stackTrace입니다.
에러가 발생한 코드의 위치를 추적할 수 있는 정보입니다. 디버깅할 때 매우 유용합니다.
에러 처리에서 중요한 것은 사용자에게 적절한 메시지를 보여주는 것입니다. "Error: null"이라고 표시하면 사용자는 당황합니다.
대신 "인터넷 연결을 확인해주세요" 같은 친절한 메시지를 보여줘야 합니다. error 객체의 타입을 확인해서 적절한 메시지를 선택할 수 있습니다.
재미있는 점은 AsyncError도 이전 데이터를 유지할 수 있다는 것입니다. 새로고침하다가 에러가 발생했다고 해서 기존에 보여주던 데이터까지 없애버리면 사용자 입장에서 황당합니다.
copyWithPrevious를 사용하면 에러 상태이면서 동시에 이전 데이터도 보여줄 수 있습니다. 실무에서 흔히 보는 패턴은 스낵바나 토스트로 에러를 알리는 것입니다.
화면 전체를 에러 메시지로 덮는 대신, 기존 콘텐츠는 유지하면서 하단에 "새로고침 실패. 다시 시도해주세요"라는 메시지를 잠깐 띄우는 것입니다.
에러 처리에서 가장 중요한 것은 복구 방법을 제공하는 것입니다. "에러가 발생했습니다"로 끝나면 사용자는 막막합니다.
"다시 시도" 버튼을 제공하거나, 문제 해결 방법을 안내해야 합니다. 김개발 씨는 에러 화면을 섬세하게 디자인했습니다.
인터넷 연결 문제인지, 서버 문제인지, 인증 문제인지에 따라 다른 메시지를 보여주도록 했습니다. 덕분에 QA팀으로부터 "에러 처리가 잘 되어있네요"라는 칭찬을 받았습니다.
이제 세 가지 상태를 모두 알아보았습니다. 다음으로 이 상태들을 어떻게 분기 처리하는지 알아보겠습니다.
실전 팁
💡 - 에러 타입에 따라 다른 메시지를 보여주면 사용자 경험이 좋아집니다
- 항상 "다시 시도" 같은 복구 방법을 함께 제공하세요
5. switch 패턴으로 상태 분기
AsyncValue의 세 가지 상태를 모두 배웠습니다. 이제 이것들을 어떻게 구분해서 처리할까요?
Dart 3.0에서 도입된 switch 패턴을 사용하면 컴파일러가 모든 케이스를 처리했는지 검사해주어, 누락 없이 안전하게 상태를 분기할 수 있습니다.
switch 패턴은 Dart 3.0부터 지원하는 패턴 매칭 문법입니다. AsyncValue가 sealed class이기 때문에, switch문에서 모든 케이스를 처리하지 않으면 컴파일 에러가 발생합니다.
마치 체크리스트를 하나씩 확인하듯, 빠뜨린 상태가 없는지 컴파일러가 검사해줍니다.
다음 코드를 살펴봅시다.
// switch 표현식으로 상태별 Widget 반환
Widget build(BuildContext context, WidgetRef ref) {
final userAsync = ref.watch(userProvider);
// switch 표현식 - 모든 케이스 처리 필수
return switch (userAsync) {
AsyncData(:final value) => UserProfile(user: value),
AsyncLoading() => const CircularProgressIndicator(),
AsyncError(:final error) => ErrorWidget(
message: error.toString(),
onRetry: () => ref.invalidate(userProvider),
),
};
}
// 구조 분해로 값 추출
final message = switch (userAsync) {
AsyncData(value: final user) => '안녕하세요, ${user.name}님!',
AsyncLoading() => '로딩 중...',
AsyncError(error: final e) => '오류: $e',
};
박시니어 씨가 코드 리뷰에서 김개발 씨에게 질문했습니다. "여기 if-else로 상태를 분기했는데, AsyncError 케이스는 어디 갔어요?" 김개발 씨는 깜짝 놀랐습니다.
isLoading과 hasValue만 확인하고 에러 케이스를 완전히 빠뜨린 것입니다. "switch 패턴을 사용했으면 이런 실수를 안 했을 거예요." 박시니어 씨가 말했습니다.
switch 패턴은 Dart 3.0에서 도입된 강력한 기능입니다. 기존의 if-else 체인이나 삼항 연산자 대신, 더 명확하고 안전하게 상태를 분기할 수 있습니다.
무엇보다 모든 케이스를 처리했는지 컴파일러가 검사해줍니다. 체크리스트를 생각해보세요.
"로딩 처리 완료, 성공 처리 완료, 에러 처리..." 에러 처리를 깜빡하면 리스트가 완성되지 않습니다. switch 패턴도 마찬가지입니다.
세 가지 상태 중 하나라도 빠뜨리면 빨간 밑줄이 그어지며 컴파일이 되지 않습니다. 위 코드를 살펴보겠습니다.
switch (userAsync) 다음에 중괄호 대신 화살표가 나옵니다. 이것이 switch 표현식입니다.
각 케이스에서 값을 반환하고, 그 값이 바로 변수에 할당되거나 return 됩니다. **구조 분해(destructuring)**도 주목해주세요.
AsyncData(:final value)라고 쓰면, AsyncData의 value 속성을 바로 value라는 이름으로 꺼내 쓸 수 있습니다. AsyncData(value: final user)처럼 다른 이름을 지정할 수도 있습니다.
if-else와 비교하면 차이가 명확합니다. if (userAsync.isLoading) else if (userAsync.hasError) else...
이런 식으로 작성하면 조건 순서를 잘못 배치하거나, 케이스를 빠뜨리기 쉽습니다. switch 패턴은 이런 실수를 원천 차단합니다.
Widget을 반환하는 build 메서드에서 switch 표현식은 특히 유용합니다. 상태별로 완전히 다른 Widget을 반환해야 할 때, 한눈에 어떤 상태에서 어떤 UI가 나오는지 파악할 수 있습니다.
김개발 씨는 기존 코드를 모두 switch 패턴으로 바꿨습니다. 처음에는 문법이 낯설었지만, 금방 익숙해졌습니다.
무엇보다 "혹시 뭔가 빠뜨리지 않았을까?"라는 불안감이 사라졌습니다. 컴파일러가 지켜보고 있으니까요.
실전 팁
💡 - switch 표현식은 모든 케이스가 값을 반환해야 합니다. 하나라도 빠지면 컴파일 에러입니다
- 구조 분해를 활용하면 코드가 더 간결해집니다
6. when() 메서드 활용
switch 패턴이 좋긴 한데, 매번 길게 쓰기 귀찮습니다. 특히 간단한 분기 처리에는 더 짧은 방법이 필요합니다.
AsyncValue에는 when() 메서드가 내장되어 있어서, 함수형 스타일로 깔끔하게 상태를 분기할 수 있습니다.
when() 메서드는 AsyncValue의 세 가지 상태를 콜백 함수로 처리하는 편의 메서드입니다. data, loading, error 세 개의 named parameter를 받아서, 현재 상태에 맞는 콜백을 실행합니다.
switch 패턴과 마찬가지로 세 개의 콜백을 모두 제공해야 합니다.
다음 코드를 살펴봅시다.
// when() 메서드로 상태 분기
Widget build(BuildContext context, WidgetRef ref) {
final userAsync = ref.watch(userProvider);
return userAsync.when(
data: (user) => UserProfile(user: user),
loading: () => const CircularProgressIndicator(),
error: (error, stackTrace) => ErrorWidget(
message: error.toString(),
onRetry: () => ref.invalidate(userProvider),
),
);
}
// when()으로 문자열 생성
final statusMessage = userAsync.when(
data: (user) => '환영합니다, ${user.name}님!',
loading: () => '사용자 정보를 불러오는 중...',
error: (e, st) => '오류가 발생했습니다: $e',
);
김개발 씨는 switch 패턴에 익숙해졌지만, 한 가지 아쉬운 점이 있었습니다. 간단한 분기 처리에도 꽤 많은 코드를 작성해야 한다는 것이었습니다.
그때 박시니어 씨가 말했습니다. "when() 메서드 써봤어요?" when() 메서드는 AsyncValue에 내장된 편의 기능입니다.
함수형 프로그래밍에서 자주 보는 패턴입니다. fold나 map처럼, 여러 케이스를 함수로 처리하는 방식입니다.
사용법은 간단합니다. userAsync.when()을 호출하고, data, loading, error 세 개의 콜백을 전달합니다.
data 콜백은 실제 데이터를 인자로 받습니다. loading 콜백은 인자가 없습니다.
error 콜백은 에러 객체와 스택트레이스를 인자로 받습니다. switch 패턴과 비교하면 어떤 차이가 있을까요?
문법적으로 when()이 더 간결합니다. 특히 Widget을 반환할 때, return userAsync.when(...) 한 줄로 시작할 수 있어서 깔끔합니다.
반면 switch는 return switch (userAsync) { ... }처럼 약간 더 장황합니다.
하지만 when()도 세 가지 콜백을 모두 제공해야 합니다. 하나라도 빠뜨리면 컴파일 에러가 발생합니다.
이 점은 switch 패턴과 동일합니다. 안전성은 똑같이 보장됩니다.
error 콜백에서 stackTrace를 사용하지 않는 경우가 많습니다. 그럴 때는 언더스코어를 사용해 무시할 수 있습니다.
error: (e, _) => ... 이런 식으로요.
실무에서는 개인 취향이나 팀 컨벤션에 따라 선택하면 됩니다. 어떤 팀은 when()을 선호하고, 어떤 팀은 switch 패턴을 선호합니다.
둘 다 동일한 안전성을 제공하므로 기능적 차이는 없습니다. 김개발 씨는 간단한 분기에는 when()을, 복잡한 로직이 필요할 때는 switch 패턴을 사용하기로 했습니다.
상황에 맞는 도구를 선택하는 것이 중요하다는 것을 배웠습니다. 한 가지 더 알아두면 좋은 것이 있습니다.
when() 외에도 whenOrNull(), maybeWhen() 같은 변형이 있습니다. 모든 케이스를 처리하고 싶지 않을 때 유용합니다.
하지만 가능하면 when()으로 모든 케이스를 명시적으로 처리하는 것이 안전합니다.
실전 팁
💡 - when()과 switch 패턴 중 팀 컨벤션에 맞는 것을 선택하세요
- error 콜백에서 stackTrace가 필요 없으면 언더스코어(_)로 무시할 수 있습니다
7. .value와 .valueOrNull
때로는 복잡한 분기 처리 없이, 그냥 데이터만 빠르게 꺼내고 싶을 때가 있습니다. "로딩이든 에러든 상관없어, 값이 있으면 주고 없으면 말고." 이럴 때 사용하는 것이 .value와 .valueOrNull입니다.
.valueOrNull은 AsyncValue에 데이터가 있으면 그 값을 반환하고, 없으면 null을 반환합니다. 로딩 중이거나 에러 상태일 때 이전 데이터가 있다면 그 값을 반환합니다.
.value는 비슷하지만, 값이 없을 때 에러를 throw합니다. 따라서 값이 확실히 있을 때만 사용해야 합니다.
다음 코드를 살펴봅시다.
// valueOrNull - 값이 있으면 반환, 없으면 null
final userAsync = ref.watch(userProvider);
final userName = userAsync.valueOrNull?.name ?? '게스트';
// 조건부 UI에서 활용
if (userAsync.valueOrNull != null) {
print('현재 사용자: ${userAsync.valueOrNull!.name}');
}
// value - 값이 없으면 에러 발생 (주의!)
try {
final user = userAsync.value; // 로딩 중이면 에러!
print(user.name);
} catch (e) {
print('값이 없습니다');
}
// 안전한 패턴: hasValue 체크 후 value 사용
if (userAsync.hasValue) {
final user = userAsync.value;
print('사용자 이름: ${user.name}');
}
김개발 씨는 복잡한 화면을 만들고 있었습니다. 여러 개의 Provider에서 데이터를 불러와서 조합해야 했습니다.
매번 when()이나 switch로 분기하자니 코드가 너무 길어졌습니다. 그때 박시니어 씨가 힌트를 줬습니다.
"꼭 모든 상태를 처리해야 하는 건 아니에요." valueOrNull은 "값이 있으면 주고, 없으면 null"이라는 간단한 로직을 수행합니다. AsyncData 상태면 그 안의 값을 반환합니다.
AsyncLoading이나 AsyncError 상태면 null을 반환합니다. 단, 로딩이나 에러 상태에서도 이전 데이터가 유지되고 있다면 그 값을 반환합니다.
이것은 "낙관적" 접근 방식입니다. 일단 값이 있다고 가정하고 진행하는 것입니다.
값이 없으면 null이니까 적절히 처리하면 됩니다. null 병합 연산자(??)와 함께 사용하면 매우 유용합니다.
위 코드를 보세요. userAsync.valueOrNull?.name ??
'게스트'는 한 줄로 "사용자 이름이 있으면 보여주고, 없으면 '게스트'로 표시"를 구현합니다. when()이나 switch로 작성했다면 훨씬 길어졌을 것입니다.
반면 .value는 조심해서 사용해야 합니다. 값이 없을 때 null 대신 에러를 throw합니다.
AsyncLoading 상태에서 .value에 접근하면 앱이 크래시납니다. 따라서 값이 확실히 있다는 것을 알 때만 사용해야 합니다.
언제 value를 안전하게 사용할 수 있을까요? hasValue로 먼저 확인한 후에 사용하면 됩니다.
if (userAsync.hasValue) 블록 안에서는 value가 존재함이 보장되므로 안전합니다. 실무에서는 valueOrNull을 먼저 고려하는 것이 좋습니다.
대부분의 상황에서 더 안전하고 유연합니다. value는 정말 값이 있어야만 하는 상황, 예를 들어 이미 데이터가 로드된 후의 처리 로직에서만 사용하세요.
한 가지 주의점이 있습니다. valueOrNull이 null이 아니라고 해서 로딩이나 에러 상태가 아닌 것은 아닙니다.
새로고침 중일 때, 로딩 상태이면서 동시에 이전 값이 있을 수 있습니다. 상태를 정확히 알고 싶다면 isLoading, hasError와 함께 확인해야 합니다.
김개발 씨는 이제 상황에 맞게 도구를 선택할 수 있게 되었습니다. 정확한 상태 분기가 필요하면 switch나 when(), 간단히 값만 필요하면 valueOrNull을 사용합니다.
적재적소에 맞는 도구를 쓰는 것이 좋은 코드의 비결입니다.
실전 팁
💡 - valueOrNull이 value보다 안전합니다. 먼저 valueOrNull 사용을 고려하세요
- value를 쓸 때는 반드시 hasValue로 먼저 확인하거나, try-catch로 감싸세요
이상으로 학습을 마칩니다. 위 내용을 직접 코드로 작성해보면서 익혀보세요!
댓글 (0)
함께 보면 좋은 카드 뉴스
반응형 마이크로서비스 완벽 가이드
Spring WebFlux와 Reactor를 활용한 반응형 마이크로서비스 구축 방법을 초급 개발자를 위해 쉽게 풀어냅니다. 실무에서 바로 적용할 수 있는 패턴과 팁을 담았습니다.
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을 활용한 실무 수준의 쇼핑 앱 개발 과정을 단계별로 학습합니다. 상품 목록, 장바구니, 주문, 인증, 검색 기능까지 모든 핵심 기능을 구현하며 상태 관리의 실전 노하우를 익힙니다.