NotaNote 나의 기술 면접

김동연·2025년 7월 8일

면접

목록 보기
1/2

기술 면접 대비 - NotaNote 앱 기능 분석

1. SNS 로그인 (소셜 로그인)

질문: 소셜 로그인 기능은 어떻게 구현하셨나요?

답변:

네, 저희 앱의 소셜 로그인은 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에 새로운 문서를 만들어 저장합니다. 이때 신규 유저를 위한 예제 데이터도 함께 생성해주는 로직을 추가했습니다.
  • 기존 사용자라면, Firestore 문서 업데이트 없이 로그인 절차를 완료합니다.

4. 세션 관리 (상태 관리):
로그인이 완료되면, 사용자의 uid와 로그인 방식(provider)을 shared_preferences를 통해 디바이스에 안전하게 저장하여 앱을 재시작해도 로그인 상태가 유지되도록 했습니다. 또한, 앱이 실행되는 동안에는 Riverpod 프로바이더를 통해 사용자 ID와 프로필 정보를 전역적으로 관리하여, 앱 내 어느 화면에서든 사용자 데이터에 일관되게 접근할 수 있도록 설계했습니다.

이러한 아키텍처를 통해 클라이언트 코드는 간결하게 유지하면서도, Firebase라는 강력한 BaaS(Backend-as-a-Service)를 활용하여 안전하고 확장성 있는 인증 시스템을 구축할 수 있었습니다.


소셜 로그인 구현 관련 기술 면접 Q&A

프로젝트 기반 질문

Q. 소셜 로그인을 구현하면서 겪었던 가장 큰 어려움은 무엇이었나요?

A. Google, Kakao 등 여러 소셜 로그인을 iOS 환경에 구현하면서 몇 가지 어려움을 겪었습니다.

첫째, Google 로그인 후 화면 전환 문제가 있었습니다. 분명 로그인은 성공했는데, 홈 화면으로 자동으로 넘어가지 않는 현상이 있었습니다. 원인을 파악해보니, 비동기 코드의 타이밍 문제와 프레임 렌더링 콜백(addPostFrameCallback) 내에서 화면 전환 로직(Navigator.push)을 잘못 사용한 것이 문제였습니다. 사용자의 인증 상태를 직접 확인하고 즉시 화면을 전환하는 방식으로 해결했습니다.

둘째, Kakao 자동 로그인 실패 현상이 있었습니다. 앱을 재시작할 때마다 로그인을 다시 요구하는 문제였는데, 로컬에 저장된 토큰이 만료되었기 때문이었습니다. API를 통해 토큰의 유효성을 먼저 검증하고, 유효하지 않을 경우 수동 로그인을 유도하는 로직을 추가하여 해결했습니다.

셋째, 소셜 로그인 별 사용자 데이터 처리 방식의 차이를 이해하고 적용하는 것이 중요했습니다. 예를 들어, Kakao 로그인은 Firebase Auth와 직접 연동되지 않기 때문에, 사용자 정보를 수동으로 가져와 Firestore에 저장하는 별도의 구현이 필요했습니다.

Q. Google 로그인 후 화면이 자동으로 넘어가지 않는 문제를 어떻게 해결했나요? 구체적인 해결 과정을 설명해주세요.

A. 해당 문제의 원인은 비동기 작업의 타이밍 이슈와 addPostFrameCallback의 부적절한 사용이었습니다.

  1. 문제 분석: FirebaseAuth.instance.authStateChanges()를 통해 사용자 인증 상태 변화를 감지하고, addPostFrameCallback 콜백 안에서 Navigator.push를 호출하여 홈 화면으로 이동시키는 구조였습니다. 하지만 이 방식은 화면 렌더링이 끝난 후에야 화면 전환을 시도하기 때문에, 타이밍이 맞지 않아 전환이 누락되는 경우가 발생했습니다.

  2. 해결 전략: 콜백에 의존하는 대신, 사용자의 인증 상태를 더 직접적이고 즉각적으로 확인하는 방식으로 변경했습니다.

  3. 구체적인 해결 방법: 로그인 성공 후 addPostFrameCallback을 사용하는 대신, main 함수나 스플래시 화면 등 앱의 시작점에서 FirebaseAuth.instance.currentUser를 통해 현재 로그인된 사용자가 있는지 직접 확인했습니다. 사용자가 존재하면 즉시 홈 화면으로 이동시키고, 그렇지 않으면 로그인 화면을 보여주는 방식으로 로직을 변경하여 문제를 해결했습니다.

Q. Kakao 자동 로그인이 실패했던 원인과 해결 방법은 무엇이었나요?

A. Kakao 자동 로그인이 실패한 근본적인 원인은 로컬에 저장된 인증 토큰이 만료되었기 때문입니다.

  1. 원인: 사용자가 이전에 로그인하여 기기에 토큰 정보가 남아있더라도, 그 토큰이 서버에서 이미 만료되었다면 유효하지 않습니다. 앱에서는 토큰의 존재 여부만으로 로그인 상태를 판단했기 때문에, 만료된 토큰을 가지고 자동 로그인을 시도하다가 실패하고 다시 로그인 화면을 보여주게 된 것입니다.

  2. 해결 방법: 앱이 시작될 때, 로컬에 저장된 토큰의 유효성을 Kakao API를 통해 먼저 확인하는 절차를 추가했습니다.

    • 토큰 유효성 검증: 앱 실행 시, 저장된 토큰으로 사용자 정보를 조회하는 API(UserApi.instance.me())를 호출합니다.
    • 분기 처리: API 호출이 성공하면 토큰이 유효한 것이므로 자동 로그인 처리하고 홈 화면으로 이동합니다. 만약 API 호출이 실패(예: 401 Unauthorized 에러)하면 토큰이 만료된 것이므로, 사용자에게 다시 로그인하도록 로그인 화면으로 안내했습니다.

Q. Kakao 로그인은 Firebase와 직접 연동되지 않는데, 사용자 정보는 어떻게 관리했나요?

A. Kakao 로그인은 Firebase Auth와 직접 연동되지 않기 때문에, 사용자 정보를 수동으로 가져와 Firestore 데이터베이스에 저장하고 관리하는 방식을 사용했습니다.

  1. 사용자 정보 동의 및 획득: Kakao 로그인 시, 사용자에게 '프로필 정보(닉네임/프로필 사진)' 제공에 대한 동의를 받습니다. 동의 후 로그인에 성공하면, 발급된 액세스 토큰을 사용하여 Kakao API를 호출해 해당 사용자의 고유 ID, 닉네임, 프로필 이미지 URL 등의 정보를 가져옵니다.

  2. Firestore에 사용자 정보 저장: 획득한 사용자 정보를 바탕으로 Firestore의 users 컬렉션에 새로운 문서를 생성하거나 기존 정보를 업데이트했습니다. 이때 Kakao에서 받은 사용자 고유 ID를 문서 ID나 필드로 사용하여 사용자를 식별했습니다.

  3. 앱 내 인증 상태 관리: Firestore에 사용자 정보가 성공적으로 저장되면, 앱 내부적으로 사용하는 상태 관리자(예: Provider, Riverpod)를 통해 '로그인된 상태'로 변경하고, 해당 사용자 정보를 앱 전역에서 사용할 수 있도록 했습니다. 이를 통해 앱 내에서는 자체적인 로그인 세션을 유지하는 것처럼 동작하게 만들었습니다.

일반/심화 질문

Q. 소셜 로그인을 구현할 때 보안상 가장 중요하게 고려해야 할 점은 무엇인가요?

A. 인증 정보(토큰)의 안전한 관리가 가장 중요합니다. 클라이언트(앱)에서 발급받은 액세스 토큰이나 인증 코드는 사용자를 대신하여 특정 작업을 수행할 수 있는 권한을 갖기 때문입니다.

  1. 토큰의 안전한 저장: 민감한 토큰 정보는 iOS의 Keychain, Android의 Keystore와 같이 안전한 저장소에 보관해야 합니다. SharedPreferencesUserDefaults에 평문으로 저장하는 것은 피해야 합니다.

  2. 서버 사이드 검증: 클라이언트에서 받은 토큰은 반드시 백엔드 서버에서 해당 소셜 서비스(Google, Kakao 등)의 검증 API를 통해 유효성을 확인해야 합니다. 클라이언트가 보낸 정보만 믿고 인증 처리를 해서는 안 됩니다. 이는 탈취된 토큰을 이용한 비정상적인 접근을 막기 위함입니다.

  3. HTTPS 통신: 클라이언트와 서버, 그리고 소셜 서비스 제공자와의 모든 통신은 HTTPS를 사용하여 중간자 공격(MITM)으로부터 데이터를 보호해야 합니다.

Q. 여러 소셜 로그인을 제공할 때, 아키텍처 측면에서 어떻게 구현하는 것이 효율적일까요?

A. 추상화(Abstraction)를 통해 로그인 로직을 모듈화하는 것이 효율적입니다.

  1. 공통 인터페이스 정의: SocialLogin이라는 추상 클래스나 인터페이스를 정의하고, login(), logout(), getUserInfo() 와 같은 공통 메서드를 선언합니다.

  2. 구현체 작성: 각 소셜 로그인(Google, Kakao, Apple 등)에 대해 이 인터페이스를 구현하는 구체적인 클래스(GoogleLoginImpl, KakaoLoginImpl 등)를 작성합니다. 각 클래스는 해당 SDK를 사용하여 실제 로그인 로직을 처리합니다.

  3. 팩토리 패턴 또는 의존성 주입 사용: 사용자가 특정 소셜 로그인을 선택했을 때, 팩토리 패턴이나 의존성 주입(DI)을 사용하여 해당 구현체의 인스턴스를 생성하고 사용합니다.

이렇게 구현하면 로그인 UI 코드에서는 어떤 소셜 로그인 방식인지 신경 쓸 필요 없이 공통 인터페이스에만 의존하게 됩니다. 따라서 새로운 소셜 로그인을 추가하거나 기존 로직을 수정할 때 코드 변경을 최소화할 수 있어 유지보수성이 크게 향상됩니다.

Q. 소셜 로그인 API가 일시적으로 다운되거나 응답이 없을 경우, 사용자 경험(UX)을 어떻게 처리하시겠어요?

A. 안정적인 사용자 경험을 위해 명확한 피드백대안 제공이 중요합니다.

  1. 타임아웃 설정: API 요청 시 적절한 타임아웃(예: 10-15초)을 설정하여 무한정 대기하는 상황을 방지합니다.

  2. 에러 메시지 안내: 타임아웃이 발생하거나 서버 에러(5xx) 응답을 받으면, 사용자에게 "현재 OOO 로그인이 원활하지 않습니다. 잠시 후 다시 시도해주세요." 와 같이 명확하고 친절한 메시지를 보여줍니다.

  3. 대안 로그인 옵션 제공: 만약 다른 소셜 로그인이나 이메일 로그인 같은 대안이 있다면, 해당 옵션을 사용하도록 유도하여 사용자가 서비스에 접근하지 못하는 상황을 최소화합니다.

  4. 재시도 버튼 제공: 단순히 에러 메시지만 보여주는 것보다, 사용자가 즉시 다시 시도해볼 수 있도록 '재시도' 버튼을 함께 제공하는 것이 좋은 UX입니다.


2. 프로필 페이지

질문: 프로필 조회 및 수정 기능은 어떻게 구현하셨나요?

답변:

프로필 페이지 기능은 크게 데이터 조회데이터 수정으로 나누어 구현했으며, 두 기능 모두 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의 실시간 데이터베이스 기능을 유기적으로 결합하여, 반응성이 뛰어나고 데이터 일관성이 보장되는 프로필 관리 기능을 구현했습니다.


3. PDF 내보내기

질문: 노트 내용을 PDF로 내보내는 기능은 어떻게 구현하셨나요?

답변:

PDF 내보내기 기능은 사용자가 작성한 노트의 서식과 내용을 그대로 유지하는 것이 중요했기 때문에, 클라이언트 사이드에서 직접 PDF를 생성하는 방식을 채택했습니다. 이를 위해 pdfflutter_quill 패키지를 핵심적으로 사용했습니다.

1. 데이터 구조 분석 (Delta):
저희 앱의 노트 편집기는 flutter_quill을 사용하는데, 이 편집기는 모든 텍스트와 서식, 이미지, 리스트 등의 정보를 Delta라는 JSON 기반의 데이터 구조로 관리합니다. Delta는 "어떤 텍스트를 삽입하고, 어떤 속성을 적용했는가"에 대한 작업(Operation)의 연속된 리스트입니다.

2. Delta를 PDF 위젯으로 변환:
PDF 생성 로직의 핵심은 _deltaToPdfWidgets 함수입니다. 이 함수는 Quill 컨트롤러로부터 얻은 Delta 객체를 순회하면서 각 Operation을 분석합니다.

  • 텍스트 처리: 텍스트 삽입(insert) Operation을 만나면, 여기에 적용된 속성(attributes) - 예를 들어 'bold', 'italic', 'header', 'color' 등 - 을 확인합니다. 그리고 이 속성에 매핑되는 pdf 패키지의 pw.TextStyle을 동적으로 생성하여 pw.TextSpan으로 만듭니다.
  • 블록 스타일 처리: 줄바꿈 문자를 기준으로 한 줄이 끝날 때, 해당 줄에 적용된 블록 속성(예: 'blockquote', 'code-block', 'list', 'align')을 확인합니다. 그리고 pw.Container, pw.Padding, pw.Row 등의 위젯으로 감싸 해당 블록 스타일을 PDF 상에 구현합니다. 예를 들어, 'list' 속성이 있다면 순서에 맞춰 번호를 매기거나 불릿 포인트를 앞에 추가하는 pw.Row를 생성합니다.
  • 이미지 처리: 이미지 Operation의 경우, 이미지 URL을 가져옵니다. URL이 네트워크 주소이면 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 프로젝트 아키텍처 분석: MVVM과 Riverpod 적용 사례

이 문서는 NotaNote Flutter 프로젝트의 소스 코드를 기반으로, MVVM (Model-View-ViewModel) 아키텍처 패턴과 Riverpod 상태관리 라이브러리가 어떻게 적용되었는지 분석합니다.

1. 프로젝트 구조와 MVVM 패턴 매핑

NotaNote 프로젝트는 폴더 구조를 통해 MVVM 패턴의 각 구성 요소를 명확하게 분리하고 있습니다. 이는 코드의 역할을 쉽게 파악하고 유지보수성을 높이는 좋은 설계입니다.

lib/
├── models/       # Model
├── viewmodels/   # ViewModel
├── pages/        # View (전체 화면)
├── views/        # View (화면의 일부)
├── widgets/      # View (재사용 가능한 작은 UI 조각)
├── services/     # Model (데이터 소스 및 비즈니스 로직)
├── providers/    # ViewModel과 View를 연결하는 Riverpod Provider
└── ...

Model (모델)

  • 역할: 앱의 데이터 구조(Data Structure)와 비즈니스 로직(Business Logic), 그리고 데이터 소스(Data Source)와의 통신을 담당합니다.
  • 구현:
    • 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) 역할을 수행합니다.

View (뷰)

  • 역할: 사용자에게 보여지는 UI를 구성하고, 사용자의 입력을 받아 ViewModel에 전달합니다. View 자체는 상태나 비즈니스 로직을 거의 가지지 않고, ViewModel이 제공하는 데이터를 화면에 그리는 역할에 집중합니다.
  • 구현:
    • 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 조각들이 위치합니다.

ViewModel (뷰모델)

  • 역할: View와 Model 사이의 중재자 역할을 합니다. View가 필요로 하는 상태(State)를 관리하고, View로부터 받은 요청을 기반으로 Model(Service)의 비즈니스 로직을 호출합니다. 그리고 Model로부터 받은 데이터를 View가 사용하기 쉬운 형태로 가공하여 제공합니다.
  • 구현:
    • viewmodels/: google_auth_viewmodel.dart, user_profile_viewmodel.dart, note_viewmodel.dart 등 특정 화면이나 기능에 필요한 상태와 로직을 담고 있는 클래스들이 위치합니다. 이 프로젝트에서는 ViewModel의 역할을 Riverpod의 다양한 Provider들이 수행하고 있습니다.

2. Riverpod 적용 방식 분석

NotaNote는 Riverpod를 매우 효과적으로 사용하여 상태를 관리하고 의존성을 주입하고 있습니다. main.dart 파일의 runApp(const ProviderScope(child: MyApp())) 코드는 앱 전체에서 Riverpod를 사용할 수 있도록 설정하는 시작점입니다.

Provider 종류별 적용 사례

  1. Provider (단순 의존성 주입)

    • 사례: auth_service.dartauthServiceProvider
    • 분석: AuthService 클래스의 인스턴스를 생성하여 앱의 다른 부분에 제공하는 가장 기본적인 형태의 Provider입니다. AuthService 자체는 상태를 가지지 않지만, 로그인/로그아웃과 같은 메소드를 제공합니다. ViewModel은 이 Provider를 통해 AuthService의 인스턴스를 얻고, 필요한 메소드를 호출합니다.
  2. FutureProvider (비동기 데이터 조회)

    • 사례: user_profile_viewmodel.dartuserProfileProvider (실제로는 StreamProvider지만, FutureProvider와 유사한 비동기 데이터 처리 패턴을 보임)
    • 분석: API 호출이나 데이터베이스 조회처럼 한 번 실행되는 비동기 작업의 결과를 관리하는 데 사용됩니다. userProfileProvider는 Firestore로부터 사용자 정보를 비동기적으로 가져옵니다. UI에서는 ref.watch를 통해 이 Provider를 구독하고, when 메소드를 사용하여 로딩(loading), 데이터(data), 에러(error) 상태에 따라 각기 다른 UI를 손쉽게 보여줄 수 있습니다. 이는 비동기 코드의 상태 처리를 매우 선언적이고 깔끔하게 만들어 줍니다.
  3. StateNotifierProvider (사용자 상호작용에 의한 상태 관리)

    • 사례: note_viewmodel.dart (가상 예시, 실제 코드에서는 다른 방식으로 구현될 수 있음)
    • 분석: 만약 노트 내용을 추가, 수정, 삭제하는 기능이 있다면, NoteViewModelStateNotifier를 상속받아 노트 리스트(List<Note>)를 상태로 관리할 것입니다. addNote, deleteNote와 같은 메소드를 외부에 노출하고, UI에서는 이 메소드들을 호출하여 상태 변경을 요청합니다. StateNotifierProvider는 이처럼 사용자의 상호작용에 따라 변하는 복잡한 상태를 관리하고, 관련 비즈니스 로직을 UI와 분리하는 데 매우 효과적입니다.
  4. .family 수정자 (동적 파라미터 전달)

    • 사례: page_viewmodel.dartpageViewModelProvider
    • 분석: Provider를 생성할 때 외부로부터 파라미터를 전달받아야 할 경우 .family 수정자를 사용합니다. pageViewModelProvidergroupId, noteId, pageId를 파라미터로 받아, 특정 페이지에 해당하는 ViewModel을 동적으로 생성합니다. 이를 통해 여러 페이지 인스턴스가 각자 독립적인 상태를 가질 수 있게 됩니다.

의존성 흐름 (로그인 예시)

  1. View (login_page.dart): 사용자가 'Google로 로그인' 버튼을 누릅니다.
  2. View → ViewModel: onTap 콜백 함수는 ref.read(googleAuthViewModelProvider).signInWithGoogle()를 호출합니다.
  3. ViewModel (google_auth_viewmodel.dart):
    • signInWithGoogle 메소드가 실행됩니다.
    • 이 메소드는 Model 계층의 AuthServiceGoogleSignIn 라이브러리를 직접 사용하여 실제 로그인 로직을 처리합니다.
    • 로그인 성공 후, Firestore에 사용자 정보를 저장하는 로직을 수행합니다.
    • 마지막으로, ref.read(userIdProvider.notifier).state = userId; 코드를 통해 전역 상태인 userIdProvider의 상태를 업데이트합니다.
  4. ViewModel → View: userIdProvider의 상태가 변경되면, 이 Provider를 watch하고 있던 다른 위젯들(예: MainPage)이 자동으로 리빌드되어 로그인된 사용자에게 맞는 화면을 보여줍니다.

결론

NotaNote 프로젝트는 MVVM 패턴을 통해 UI와 비즈니스 로직을 효과적으로 분리하고, Riverpod를 사용하여 각 계층 간의 의존성을 주입하고 상태를 관리하는 훌륭한 예시입니다. 이러한 아키텍처는 코드의 테스트 용이성, 유지보수성, 확장성을 크게 향상시켜, 안정적이고 품질 높은 애플리케이션을 만드는 데 기여합니다.

Riverpod의 주요 Provider 비교: FutureProvider, StreamProvider,
StateNotifierProvider

Riverpod는 다양한 상황에 맞는 최적의 상태 관리를 위해 여러 종류의
Provider를 제공합니다. 그중 가장 핵심적인 세 가지 Provider의 기능과
차이점은 다음과 같습니다.

1. FutureProvider

  • 기능 및 용도:
    한 번만 실행되는 비동기 작업을 처리하고 그 결과를 UI에 제공하는 데
    사용됩니다. 주로 서버로부터 데이터를 가져오는 API 호출과 같은 작업에
    이상적입니다.
  • 작동 방식:
    Future를 생성하는 함수를 제공받아 실행합니다. 이 Future가 완료될
    때까지의 상태(로딩, 데이터, 에러)를 관리하고 UI에 노출합니다.
  • 노출되는 상태:
    AsyncValue 객체를 통해 상태를 제공합니다. AsyncValue는 세 가지
    상태를 가집니다.
    AsyncValue.loading: Future가 아직 실행 중인 상태 (로딩 중)
    AsyncValue.data: Future가 성공적으로 완료되어 데이터를 반환한
    상태
    * AsyncValue.error: Future 실행 중 오류가 발생한 상태
  • 핵심 특징:

    • 캐싱(Caching): 기본적으로 Future의 결과를 캐싱합니다. 한번
      성공적으로 데이터를 가져오면, Provider가 다시 호출되어도 Future를
      재실행하지 않고 캐시된 값을 즉시 반환합니다. 데이터를
      새로고침하려면 ref.invalidate 또는 ref.refresh를 사용해야 합니다.
    • UI 코드 단순화: when 메소드를 사용하여 로딩, 데이터, 에러 상태에
      따라 각기 다른 위젯을 깔끔하게 렌더링할 수 있습니다.
  • 사용 예시:

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 }

2. StreamProvider

  • 기능 및 용도:
    지속적으로 데이터가 발생하는 비동기 스트림을 처리하는 데
    사용됩니다. Firebase Firestore의 실시간 업데이트, 웹소켓(WebSocket)
    통신, GPS 위치 추적 등 실시간 데이터 처리에 적합합니다.
  • 작동 방식:
    Stream을 생성하는 함수를 제공받아 해당 스트림을
    구독(listen)합니다. 스트림에서 새로운 값이 발생할 때마다 UI에 새로운
    상태를 전달합니다.
  • 노출되는 상태:
    FutureProvider와 동일하게 AsyncValue 객체를 사용합니다.
    스트림에서 새로운 데이터가 들어올 때마다 AsyncValue.data 상태가
    갱신됩니다.
  • 핵심 특징:

    • 자동 리소스 관리: Provider가 더 이상 사용되지 않으면(위젯
      트리에서 제거되면) 자동으로 스트림 구독을 취소하여 메모리 누수를
      방지합니다.
    • 실시간 반응: 외부 데이터 소스에서 변경이 발생하면 즉시 UI에
      반영할 수 있습니다.
  • 사용 예시:

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 }

3. StateNotifierProvider

  • 기능 및 용도:
    사용자 상호작용에 의해 변경되는 복잡한 상태를 관리하는 데
    사용됩니다. 단순한 비동기 데이터가 아닌, 여러 메소드를 통해 상태를
    변경해야 하는 경우(예: 장바구니, 폼 상태, 필터 설정)에 가장 적합한
    솔루션입니다.
  • 작동 방식:
    상태 로직을 캡슐화한 StateNotifier 클래스의 인스턴스를 생성하고
    제공합니다. UI는 이 Provider를 통해 StateNotifier의 메소드를 호출하여
    상태를 변경하고, 변경된 상태를 구독하여 UI를 갱신합니다.
  • 노출되는 상태:
    StateNotifier가 관리하는 상태 객체 그 자체를 노출합니다. (예:
    List, CartState)
  • 핵심 특징:

    • 로직과 UI의 분리: 상태 변경 로직을 StateNotifier 클래스 안에
      모아두어 UI 코드와 완벽하게 분리할 수 있습니다. 이는 테스트
      용이성과 코드의 유지보수성을 크게 향상시킵니다.
    • 불변성(Immutability): StateNotifier는 상태를 직접 수정하는 것이
      아니라, 새로운 상태 객체를 생성하여 교체하는 방식을 권장합니다.
      이를 통해 상태 변화를 예측 가능하고 안정적으로 만듭니다.
  • 사용 예시:

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... │
적... │ "페이지 로드 ... │ "실시간 채... │ "장바구니에 상... │
└───────┴──────────────────┴───────────────┴────────────────────┘

0개의 댓글