답변:
네, 저희 앱의 소셜 로그인은 Firebase Authentication을 핵심 백엔드로 활용하여 Google, Apple, Kakao 로그인을 구현했습니다. 전체적인 흐름은 다음과 같습니다.
1. 클라이언트 중심의 OAuth 인증:
사용자가 로그인 화면에서 특정 소셜 로그인 버튼(예: Google)을 누르면, google_sign_in 같은 각 플랫폼별 전용 Flutter 패키지를 사용하여 클라이언트(앱)에서 직접 OAuth 인증 절차를 시작합니다. 이 패키지가 OS 레벨의 로그인 UI를 호출하여 사용자 인증을 처리하고, 성공 시 ID 토큰(idToken)과 액세스 토큰(accessToken)을 앱으로 반환해 줍니다.
2. Firebase를 통한 인증 위임:
앱은 전달받은 이 토큰들을 사용하여 Firebase가 제공하는 Credential 객체(예: GoogleAuthProvider.credential)를 생성합니다. 그리고 이 Credential 객체를 Firebase Auth의 signInWithCredential 메소드에 전달합니다.
이 방식의 가장 큰 장점은, 저희 서버가 직접 소셜 서버와 통신하며 복잡한 '인증 코드-액세스 토큰 교환' 과정을 처리할 필요 없이, Firebase가 이 모든 검증 과정을 안전하게 대행해준다는 점입니다. Firebase 백엔드가 받은 토큰의 유효성을 직접 해당 소셜 서비스(Google, Apple 등)에 확인하고, 검증이 완료되면 저희 앱을 위한 고유한 Firebase 사용자(User)를 생성하거나 기존 사용자를 로그인 처리한 후, Firebase 전용 JWT를 앱에 돌려줍니다.
3. Firestore를 이용한 유저 프로필 관리:
Firebase 인증이 성공적으로 완료되면, 반환된 사용자의 고유 ID(uid)를 사용합니다. 이 uid를 문서 ID로 하여 Cloud Firestore 데이터베이스의 users 컬렉션에서 해당 유저의 프로필 정보가 있는지 확인합니다.
UserModel 객체를 생성하고 Firestore에 새로운 문서를 만들어 저장합니다. 이때 신규 유저를 위한 예제 데이터도 함께 생성해주는 로직을 추가했습니다.4. 세션 관리 (상태 관리):
로그인이 완료되면, 사용자의 uid와 로그인 방식(provider)을 shared_preferences를 통해 디바이스에 안전하게 저장하여 앱을 재시작해도 로그인 상태가 유지되도록 했습니다. 또한, 앱이 실행되는 동안에는 Riverpod 프로바이더를 통해 사용자 ID와 프로필 정보를 전역적으로 관리하여, 앱 내 어느 화면에서든 사용자 데이터에 일관되게 접근할 수 있도록 설계했습니다.
이러한 아키텍처를 통해 클라이언트 코드는 간결하게 유지하면서도, Firebase라는 강력한 BaaS(Backend-as-a-Service)를 활용하여 안전하고 확장성 있는 인증 시스템을 구축할 수 있었습니다.
A. Google, Kakao 등 여러 소셜 로그인을 iOS 환경에 구현하면서 몇 가지 어려움을 겪었습니다.
첫째, Google 로그인 후 화면 전환 문제가 있었습니다. 분명 로그인은 성공했는데, 홈 화면으로 자동으로 넘어가지 않는 현상이 있었습니다. 원인을 파악해보니, 비동기 코드의 타이밍 문제와 프레임 렌더링 콜백(addPostFrameCallback) 내에서 화면 전환 로직(Navigator.push)을 잘못 사용한 것이 문제였습니다. 사용자의 인증 상태를 직접 확인하고 즉시 화면을 전환하는 방식으로 해결했습니다.
둘째, Kakao 자동 로그인 실패 현상이 있었습니다. 앱을 재시작할 때마다 로그인을 다시 요구하는 문제였는데, 로컬에 저장된 토큰이 만료되었기 때문이었습니다. API를 통해 토큰의 유효성을 먼저 검증하고, 유효하지 않을 경우 수동 로그인을 유도하는 로직을 추가하여 해결했습니다.
셋째, 소셜 로그인 별 사용자 데이터 처리 방식의 차이를 이해하고 적용하는 것이 중요했습니다. 예를 들어, Kakao 로그인은 Firebase Auth와 직접 연동되지 않기 때문에, 사용자 정보를 수동으로 가져와 Firestore에 저장하는 별도의 구현이 필요했습니다.
A. 해당 문제의 원인은 비동기 작업의 타이밍 이슈와 addPostFrameCallback의 부적절한 사용이었습니다.
문제 분석: FirebaseAuth.instance.authStateChanges()를 통해 사용자 인증 상태 변화를 감지하고, addPostFrameCallback 콜백 안에서 Navigator.push를 호출하여 홈 화면으로 이동시키는 구조였습니다. 하지만 이 방식은 화면 렌더링이 끝난 후에야 화면 전환을 시도하기 때문에, 타이밍이 맞지 않아 전환이 누락되는 경우가 발생했습니다.
해결 전략: 콜백에 의존하는 대신, 사용자의 인증 상태를 더 직접적이고 즉각적으로 확인하는 방식으로 변경했습니다.
구체적인 해결 방법: 로그인 성공 후 addPostFrameCallback을 사용하는 대신, main 함수나 스플래시 화면 등 앱의 시작점에서 FirebaseAuth.instance.currentUser를 통해 현재 로그인된 사용자가 있는지 직접 확인했습니다. 사용자가 존재하면 즉시 홈 화면으로 이동시키고, 그렇지 않으면 로그인 화면을 보여주는 방식으로 로직을 변경하여 문제를 해결했습니다.
A. Kakao 자동 로그인이 실패한 근본적인 원인은 로컬에 저장된 인증 토큰이 만료되었기 때문입니다.
원인: 사용자가 이전에 로그인하여 기기에 토큰 정보가 남아있더라도, 그 토큰이 서버에서 이미 만료되었다면 유효하지 않습니다. 앱에서는 토큰의 존재 여부만으로 로그인 상태를 판단했기 때문에, 만료된 토큰을 가지고 자동 로그인을 시도하다가 실패하고 다시 로그인 화면을 보여주게 된 것입니다.
해결 방법: 앱이 시작될 때, 로컬에 저장된 토큰의 유효성을 Kakao API를 통해 먼저 확인하는 절차를 추가했습니다.
UserApi.instance.me())를 호출합니다.A. Kakao 로그인은 Firebase Auth와 직접 연동되지 않기 때문에, 사용자 정보를 수동으로 가져와 Firestore 데이터베이스에 저장하고 관리하는 방식을 사용했습니다.
사용자 정보 동의 및 획득: Kakao 로그인 시, 사용자에게 '프로필 정보(닉네임/프로필 사진)' 제공에 대한 동의를 받습니다. 동의 후 로그인에 성공하면, 발급된 액세스 토큰을 사용하여 Kakao API를 호출해 해당 사용자의 고유 ID, 닉네임, 프로필 이미지 URL 등의 정보를 가져옵니다.
Firestore에 사용자 정보 저장: 획득한 사용자 정보를 바탕으로 Firestore의 users 컬렉션에 새로운 문서를 생성하거나 기존 정보를 업데이트했습니다. 이때 Kakao에서 받은 사용자 고유 ID를 문서 ID나 필드로 사용하여 사용자를 식별했습니다.
앱 내 인증 상태 관리: Firestore에 사용자 정보가 성공적으로 저장되면, 앱 내부적으로 사용하는 상태 관리자(예: Provider, Riverpod)를 통해 '로그인된 상태'로 변경하고, 해당 사용자 정보를 앱 전역에서 사용할 수 있도록 했습니다. 이를 통해 앱 내에서는 자체적인 로그인 세션을 유지하는 것처럼 동작하게 만들었습니다.
A. 인증 정보(토큰)의 안전한 관리가 가장 중요합니다. 클라이언트(앱)에서 발급받은 액세스 토큰이나 인증 코드는 사용자를 대신하여 특정 작업을 수행할 수 있는 권한을 갖기 때문입니다.
토큰의 안전한 저장: 민감한 토큰 정보는 iOS의 Keychain, Android의 Keystore와 같이 안전한 저장소에 보관해야 합니다. SharedPreferences나 UserDefaults에 평문으로 저장하는 것은 피해야 합니다.
서버 사이드 검증: 클라이언트에서 받은 토큰은 반드시 백엔드 서버에서 해당 소셜 서비스(Google, Kakao 등)의 검증 API를 통해 유효성을 확인해야 합니다. 클라이언트가 보낸 정보만 믿고 인증 처리를 해서는 안 됩니다. 이는 탈취된 토큰을 이용한 비정상적인 접근을 막기 위함입니다.
HTTPS 통신: 클라이언트와 서버, 그리고 소셜 서비스 제공자와의 모든 통신은 HTTPS를 사용하여 중간자 공격(MITM)으로부터 데이터를 보호해야 합니다.
A. 추상화(Abstraction)를 통해 로그인 로직을 모듈화하는 것이 효율적입니다.
공통 인터페이스 정의: SocialLogin이라는 추상 클래스나 인터페이스를 정의하고, login(), logout(), getUserInfo() 와 같은 공통 메서드를 선언합니다.
구현체 작성: 각 소셜 로그인(Google, Kakao, Apple 등)에 대해 이 인터페이스를 구현하는 구체적인 클래스(GoogleLoginImpl, KakaoLoginImpl 등)를 작성합니다. 각 클래스는 해당 SDK를 사용하여 실제 로그인 로직을 처리합니다.
팩토리 패턴 또는 의존성 주입 사용: 사용자가 특정 소셜 로그인을 선택했을 때, 팩토리 패턴이나 의존성 주입(DI)을 사용하여 해당 구현체의 인스턴스를 생성하고 사용합니다.
이렇게 구현하면 로그인 UI 코드에서는 어떤 소셜 로그인 방식인지 신경 쓸 필요 없이 공통 인터페이스에만 의존하게 됩니다. 따라서 새로운 소셜 로그인을 추가하거나 기존 로직을 수정할 때 코드 변경을 최소화할 수 있어 유지보수성이 크게 향상됩니다.
A. 안정적인 사용자 경험을 위해 명확한 피드백과 대안 제공이 중요합니다.
타임아웃 설정: API 요청 시 적절한 타임아웃(예: 10-15초)을 설정하여 무한정 대기하는 상황을 방지합니다.
에러 메시지 안내: 타임아웃이 발생하거나 서버 에러(5xx) 응답을 받으면, 사용자에게 "현재 OOO 로그인이 원활하지 않습니다. 잠시 후 다시 시도해주세요." 와 같이 명확하고 친절한 메시지를 보여줍니다.
대안 로그인 옵션 제공: 만약 다른 소셜 로그인이나 이메일 로그인 같은 대안이 있다면, 해당 옵션을 사용하도록 유도하여 사용자가 서비스에 접근하지 못하는 상황을 최소화합니다.
재시도 버튼 제공: 단순히 에러 메시지만 보여주는 것보다, 사용자가 즉시 다시 시도해볼 수 있도록 '재시도' 버튼을 함께 제공하는 것이 좋은 UX입니다.
답변:
프로필 페이지 기능은 크게 데이터 조회와 데이터 수정으로 나누어 구현했으며, 두 기능 모두 Riverpod를 이용한 상태 관리와 Firestore와의 실시간 연동에 중점을 두었습니다.
1. 프로필 정보 조회:
프로필 페이지에 진입하면, userProfileProvider라는 StreamProvider가 현재 로그인된 사용자의 uid를 기반으로 Firestore의 users 컬렉션에 있는 해당 사용자 문서를 실시간으로 구독(snapshot)합니다.
StreamProvider를 사용함으로써 얻는 이점은, 만약 다른 기기에서 프로필 정보가 변경되거나 관리자에 의해 정보가 수정되었을 때, 별도의 새로고침 없이도 UI가 즉시 자동으로 업데이트된다는 것입니다. Firestore의 실시간 리스너가 데이터 변경을 감지하면, StreamProvider는 새로운 UserModel 객체를 생성하여 UI에 전달하고, ref.watch를 통해 이를 수신하는 위젯은 자동으로 리빌드됩니다.
2. 프로필 정보 수정:
사용자가 '수정' 버튼을 눌러 프로필 수정 페이지(UserProfileEditPage)로 이동하면, 현재 사용자 정보를 담은 UserModel 객체를 전달받아 TextEditingController에 초기화합니다.
사용자가 닉네임이나 이메일을 수정한 후 '완료' 버튼을 누르면, updateUserProfile 함수가 호출됩니다. 이 함수는 사용자의 uid와 수정된 데이터를 받아 Firestore 문서에 update 요청을 보냅니다. 이때 updatedAt 필드에는 FieldValue.serverTimestamp()를 사용하여 서버 시간을 기준으로 수정 시각을 정확히 기록하도록 했습니다.
수정이 완료되면 Navigator.pop(context, true)를 통해 변경이 있었음을 이전 페이지(UserProfilePage)에 알립니다. UserProfilePage에서는 이 반환값을 확인하고, ref.invalidate(userProfileProvider)를 호출하여 프로필 데이터 캐시를 무효화하고 스트림을 강제로 갱신합니다. 이를 통해 사용자는 수정된 최신 프로필 정보를 즉시 화면에서 확인할 수 있습니다.
프로필 이미지 수정의 경우, ImagePicker로 갤러리나 카메라에서 이미지를 선택하게 하고, 선택된 이미지는 Firebase Storage에 업로드합니다. 업로드가 완료되면 해당 이미지의 다운로드 URL을 받아, 이 URL을 Firestore의 사용자 문서에 있는 photoUrl 필드에 업데이트하는 방식으로 구현했습니다.
이처럼 Riverpod의 상태 관리와 Firestore의 실시간 데이터베이스 기능을 유기적으로 결합하여, 반응성이 뛰어나고 데이터 일관성이 보장되는 프로필 관리 기능을 구현했습니다.
답변:
PDF 내보내기 기능은 사용자가 작성한 노트의 서식과 내용을 그대로 유지하는 것이 중요했기 때문에, 클라이언트 사이드에서 직접 PDF를 생성하는 방식을 채택했습니다. 이를 위해 pdf와 flutter_quill 패키지를 핵심적으로 사용했습니다.
1. 데이터 구조 분석 (Delta):
저희 앱의 노트 편집기는 flutter_quill을 사용하는데, 이 편집기는 모든 텍스트와 서식, 이미지, 리스트 등의 정보를 Delta라는 JSON 기반의 데이터 구조로 관리합니다. Delta는 "어떤 텍스트를 삽입하고, 어떤 속성을 적용했는가"에 대한 작업(Operation)의 연속된 리스트입니다.
2. Delta를 PDF 위젯으로 변환:
PDF 생성 로직의 핵심은 _deltaToPdfWidgets 함수입니다. 이 함수는 Quill 컨트롤러로부터 얻은 Delta 객체를 순회하면서 각 Operation을 분석합니다.
pdf 패키지의 pw.TextStyle을 동적으로 생성하여 pw.TextSpan으로 만듭니다.pw.Container, pw.Padding, pw.Row 등의 위젯으로 감싸 해당 블록 스타일을 PDF 상에 구현합니다. 예를 들어, 'list' 속성이 있다면 순서에 맞춰 번호를 매기거나 불릿 포인트를 앞에 추가하는 pw.Row를 생성합니다.http 패키지로 이미지를 다운로드하고, 로컬 파일 경로이거나 Base64 데이터이면 바로 바이트 데이터로 변환합니다. 이 바이트 데이터를 pdf 패키지의 pw.MemoryImage로 만들어 PDF 문서에 삽입합니다.3. 폰트 및 파일 생성:
한글이 깨지지 않도록 rootBundle을 통해 앱 내에 포함된 Pretendard 폰트 파일을 직접 로드하여 pw.Font 객체를 만들고, 이를 PDF의 기본 폰트로 지정했습니다.
모든 Delta Operation이 pdf 위젯으로 변환되면, 이 위젯 리스트를 pw.MultiPage에 담아 pw.Document를 완성합니다. 마지막으로 path_provider를 사용해 앱의 임시 디렉토리 경로를 얻어와, 생성된 PDF 문서를 파일로 저장하고 그 File 객체를 반환하여 다른 앱으로 공유하거나 저장할 수 있도록 구현했습니다.
이 방식을 통해 서버 자원을 전혀 사용하지 않으면서도, 복잡한 서식을 포함한 노트 내용을 사용자가 보는 그대로 PDF 파일로 정확하게 변환할 수 있었습니다.
이 문서는 NotaNote Flutter 프로젝트의 소스 코드를 기반으로, MVVM (Model-View-ViewModel) 아키텍처 패턴과 Riverpod 상태관리 라이브러리가 어떻게 적용되었는지 분석합니다.
NotaNote 프로젝트는 폴더 구조를 통해 MVVM 패턴의 각 구성 요소를 명확하게 분리하고 있습니다. 이는 코드의 역할을 쉽게 파악하고 유지보수성을 높이는 좋은 설계입니다.
lib/
├── models/ # Model
├── viewmodels/ # ViewModel
├── pages/ # View (전체 화면)
├── views/ # View (화면의 일부)
├── widgets/ # View (재사용 가능한 작은 UI 조각)
├── services/ # Model (데이터 소스 및 비즈니스 로직)
├── providers/ # ViewModel과 View를 연결하는 Riverpod Provider
└── ...
models/: user_model.dart, note_model.dart 등 Firestore 데이터베이스의 문서 구조와 일치하는 순수한 데이터 객체(DTO)들이 위치합니다. 이 모델들은 fromJson, toJson 메소드를 포함하여 데이터 직렬화를 처리합니다.services/: firestore_service.dart, auth_service.dart, whisper_service.dart 등 외부 서비스(Firebase, Kakao, OpenAI API 등)와의 실제 통신을 담당하는 클래스들이 위치합니다. 이들은 데이터의 출처(Data Source) 역할을 수행합니다.pages/: login_page.dart, user_profile_page.dart, main_page.dart 등 앱의 전체 화면을 구성하는 스크린 위젯들이 위치합니다. 이 위젯들은 대부분 ConsumerWidget 또는 ConsumerStatefulWidget을 상속받아 Riverpod Provider와 상호작용합니다.views/ & widgets/: note_list_view.dart, profile_image_widget.dart 등 페이지 내에서 재사용되거나 특정 기능을 담당하는 더 작은 단위의 UI 조각들이 위치합니다.viewmodels/: google_auth_viewmodel.dart, user_profile_viewmodel.dart, note_viewmodel.dart 등 특정 화면이나 기능에 필요한 상태와 로직을 담고 있는 클래스들이 위치합니다. 이 프로젝트에서는 ViewModel의 역할을 Riverpod의 다양한 Provider들이 수행하고 있습니다.NotaNote는 Riverpod를 매우 효과적으로 사용하여 상태를 관리하고 의존성을 주입하고 있습니다. main.dart 파일의 runApp(const ProviderScope(child: MyApp())) 코드는 앱 전체에서 Riverpod를 사용할 수 있도록 설정하는 시작점입니다.
Provider (단순 의존성 주입)
auth_service.dart의 authServiceProviderAuthService 클래스의 인스턴스를 생성하여 앱의 다른 부분에 제공하는 가장 기본적인 형태의 Provider입니다. AuthService 자체는 상태를 가지지 않지만, 로그인/로그아웃과 같은 메소드를 제공합니다. ViewModel은 이 Provider를 통해 AuthService의 인스턴스를 얻고, 필요한 메소드를 호출합니다.FutureProvider (비동기 데이터 조회)
user_profile_viewmodel.dart의 userProfileProvider (실제로는 StreamProvider지만, FutureProvider와 유사한 비동기 데이터 처리 패턴을 보임)userProfileProvider는 Firestore로부터 사용자 정보를 비동기적으로 가져옵니다. UI에서는 ref.watch를 통해 이 Provider를 구독하고, when 메소드를 사용하여 로딩(loading), 데이터(data), 에러(error) 상태에 따라 각기 다른 UI를 손쉽게 보여줄 수 있습니다. 이는 비동기 코드의 상태 처리를 매우 선언적이고 깔끔하게 만들어 줍니다.StateNotifierProvider (사용자 상호작용에 의한 상태 관리)
note_viewmodel.dart (가상 예시, 실제 코드에서는 다른 방식으로 구현될 수 있음)NoteViewModel은 StateNotifier를 상속받아 노트 리스트(List<Note>)를 상태로 관리할 것입니다. addNote, deleteNote와 같은 메소드를 외부에 노출하고, UI에서는 이 메소드들을 호출하여 상태 변경을 요청합니다. StateNotifierProvider는 이처럼 사용자의 상호작용에 따라 변하는 복잡한 상태를 관리하고, 관련 비즈니스 로직을 UI와 분리하는 데 매우 효과적입니다..family 수정자 (동적 파라미터 전달)
page_viewmodel.dart의 pageViewModelProvider.family 수정자를 사용합니다. pageViewModelProvider는 groupId, noteId, pageId를 파라미터로 받아, 특정 페이지에 해당하는 ViewModel을 동적으로 생성합니다. 이를 통해 여러 페이지 인스턴스가 각자 독립적인 상태를 가질 수 있게 됩니다.login_page.dart): 사용자가 'Google로 로그인' 버튼을 누릅니다.onTap 콜백 함수는 ref.read(googleAuthViewModelProvider).signInWithGoogle()를 호출합니다.google_auth_viewmodel.dart):signInWithGoogle 메소드가 실행됩니다.AuthService나 GoogleSignIn 라이브러리를 직접 사용하여 실제 로그인 로직을 처리합니다.ref.read(userIdProvider.notifier).state = userId; 코드를 통해 전역 상태인 userIdProvider의 상태를 업데이트합니다.userIdProvider의 상태가 변경되면, 이 Provider를 watch하고 있던 다른 위젯들(예: MainPage)이 자동으로 리빌드되어 로그인된 사용자에게 맞는 화면을 보여줍니다.NotaNote 프로젝트는 MVVM 패턴을 통해 UI와 비즈니스 로직을 효과적으로 분리하고, Riverpod를 사용하여 각 계층 간의 의존성을 주입하고 상태를 관리하는 훌륭한 예시입니다. 이러한 아키텍처는 코드의 테스트 용이성, 유지보수성, 확장성을 크게 향상시켜, 안정적이고 품질 높은 애플리케이션을 만드는 데 기여합니다.
Riverpod의 주요 Provider 비교: FutureProvider, StreamProvider,
StateNotifierProvider
Riverpod는 다양한 상황에 맞는 최적의 상태 관리를 위해 여러 종류의
Provider를 제공합니다. 그중 가장 핵심적인 세 가지 Provider의 기능과
차이점은 다음과 같습니다.
핵심 특징:
사용 예시:
1 // Provider 정의
2 final userProvider =
FutureProvider.autoDispose.family<User, String>((ref,
userId) async {
3 return ref
.watch(userRepositoryProvider).fetchUser(userId);
4 });
5
6 // UI에서 사용
7 class UserProfilePage extends ConsumerWidget {
8 @override
9 Widget build(BuildContext context, WidgetRef ref) {
10 final userAsyncValue = ref.watch(userProvider('123'
));
11 return userAsyncValue.when(
12 loading: () => CircularProgressIndicator(),
13 error: (err, stack) => Text('Error: $err'),
14 data: (user) => Text('Name: ${user.name}'),
15 );
16 }
17 }
핵심 특징:
사용 예시:
1 // Provider 정의
2 final chatProvider =
StreamProvider.autoDispose<List<Message>>((ref) {
3 return ref
.watch(chatRepositoryProvider).getMessages();
4 });
5
6 // UI에서 사용
7 class ChatScreen extends ConsumerWidget {
8 @override
9 Widget build(BuildContext context, WidgetRef ref) {
10 final messagesAsyncValue = ref.watch(chatProvider);
11 return messagesAsyncValue.when(
12 loading: () => CircularProgressIndicator(),
13 error: (err, stack) => Text('Error: $err'),
14 data: (messages) => ListView.builder(
15 itemCount: messages.length,
16 itemBuilder: (context, index) => Text
(messages[index].text),
17 ),
18 );
19 }
20 }
핵심 특징:
사용 예시:
1 // StateNotifier 클래스 정의
2 class CounterNotifier extends StateNotifier<int> {
3 CounterNotifier() : super(0); // 초기 상태
4 void increment() => state++; // 상태 변경 메소드
5 }
6
7 // Provider 정의
8 final counterProvider = StateNotifierProvider<
CounterNotifier, int>((ref) {
9 return CounterNotifier();
10 });
11
12 // UI에서 사용
13 class CounterScreen extends ConsumerWidget {
14 @override
15 Widget build(BuildContext context, WidgetRef ref) {
16 final count = ref.watch(counterProvider); // 상태를
구독
17 final counterNotifier =
ref.read(counterProvider.notifier); // Notifier 인스턴스
접근
18
19 return Scaffold(
20 body: Center(child: Text('Count: $count')),
21 floatingActionButton: FloatingActionButton(
22 onPressed: () => counterNotifier.increment(),
// 메소드 호출
23 child: Icon(Icons.add),
24 ),
25 );
26 }
27 }
핵심 차이점 요약
┌───────┬──────────────────┬───────────────┬────────────────────┐
│ 구분 │ FutureProvider │ StreamPro... │ StateNotifierP... │
├───────┼──────────────────┼───────────────┼────────────────────┤
│ 주... │ 단발성 비동기... │ 지속적인 ... │ 사용자 상호작용... │
│ 데... │ Future (API ... │ Stream (Fi... │ StateNotifier ... │
│ 상... │ 외부 데이터 ... │ 외부 데이... │ 애플리케이션 내... │
│ UI... │ 데이터 소비(C... │ 데이터 소... │ 데이터
소비(Con... │
│ 적... │ "페이지 로드 ... │ "실시간 채... │ "장바구니에 상... │
└───────┴──────────────────┴───────────────┴────────────────────┘