새 프로젝트 개발에 참여하게 됐다
Ingridea는 친구가 만든 iOS 앱인데, 내가 독서를 하며 마음에 드는 글귀를 저장할 때 사용해온 앱이다. 추가되었으면 하는 기능은 있었지만 바쁜 제작자에게 부담을 주고 싶지 않아 말하지 않았었다.
우연한 기회로 나도 개발자로 참여하게 되어서 이 과정을 블로그를 통해 연재하고자 한다. Flutter와 앱 환경을 새롭게 경험해 보면서 많은 것을 배우고자 한다.
프로젝트 파악하기
새로운 코드베이스에 들어가면 먼저 폴더부터 펼쳐 보게 된다. 하지만 파일이 많을수록 디렉터리만 읽어서는 실제 사용자가 거치는 흐름을 알기 어렵다. 사용 중인 코드와 과거에 작성했지만 지금은 연결되지 않은 코드도 섞여 있기 때문이다.
그래서 Ingridea를 파악할 때는 다음 순서로 살펴봤다.
- 사용자가 어떤 문제를 해결하는 서비스인지 확인한다.
- 앱 실행부터 핵심 기능까지 사용자 흐름을 따라간다.
- 각 단계에서 Flutter, iOS 네이티브 코드, Firebase가 맡는 역할을 연결한다.
- 실제로 호출되는 경로와 레거시·미연결 경로를 구분한다.
- 문서의 설명을 그대로 믿지 않고 코드와 배포 상태를 대조한다.
이번 글은 이 과정에서 파악한 Ingridea의 서비스와 전체 구조를 먼저 정리한다.
Ingridea는 어떤 서비스인가
Ingridea는 책의 문장을 디지털로 옮기고 정리하는 앱이다. 책 페이지를 카메라로 스캔해 텍스트를 저장하고, 저장한 문장을 폴더별로 모아 보거나 원하는 부분에 색상 하이라이트를 적용할 수 있다.
사용자 입장에서 보면 흐름은 단순하다.
| |
현재 핵심 제품 경로는 iOS 16 이상의 VisionKit 스캔과 App Store 구독이다. 저장소에는 Android와 Web 프로젝트도 있지만 Android의 핵심 스캔·결제 경로는 아직 구현되지 않았다. Web은 앱 전체를 제공하기보다 이메일 링크 인증을 연결하는 landing 역할이 중심이다.
즉 Flutter 프로젝트라고 해서 모든 플랫폼이 같은 수준으로 동작한다고 가정하면 안 된다. 현재 Ingridea를 이해하려면 Flutter 앱이 중심에 있고, iOS 네이티브 기능과 Firebase가 핵심 경로를 나눠 맡는 구조로 보는 것이 정확하다.
전체 구조 한눈에 보기
Ingridea의 주요 구성은 다음과 같다.
| |
코드 디렉터리도 이 역할을 따라간다.
| |
처음에는 lib/만 보면 Flutter 앱의 대부분을 이해할 수 있을 것 같았다. 실제로는 스캔,
딥링크, 결제처럼 중요한 기능이 ios/와 functions/까지 이어져 있었다. 한 기능을
파악할 때 파일 하나가 아니라 경계 사이의 데이터 흐름을 따라가야 했다.
Flutter가 맡는 역할
Flutter는 Dart로 하나의 UI 코드를 작성해 여러 플랫폼에서 실행할 수 있게 하는 프레임워크다. Ingridea에서는 로그인 화면, 홈 화면, 폴더·페이지 목록, 텍스트 편집, 하이라이트와 설정 UI를 담당한다.
앱은 main.dart에서 시작한다. 실행 환경에 맞는 Firebase 설정을 고르고,
SharedPreferences와 Mixpanel을 초기화한 뒤 Riverpod의 ProviderScope로 앱을 감싼다.
그 다음 인증 상태와 최초 계정 설정 상태에 따라 로그인 화면 또는 Home 화면을 보여준다.
Riverpod으로 상태와 화면 연결하기
Flutter의 Widget은 화면을 그리는 단위다. 그런데 로그인 사용자, 선택한 폴더, 페이지 목록, 구독 상태처럼 여러 화면이 공유하는 값은 Widget 안에만 둘 수 없다. Ingridea는 이 상태를 Riverpod provider로 관리한다.
예를 들어 Firestore의 페이지 목록이 바뀌면 provider가 새 데이터를 받고 Home 화면이 다시 그려진다. 문장을 편집할 때는 변경 내용을 화면에 먼저 반영하고, 잠시 입력이 멈춘 뒤 Firestore에 저장하는 autosave 흐름도 있다.
여기서 알게 된 것은 상태 관리 코드를 볼 때 provider 이름만 따라가면 부족하다는 점이다. 실시간 Firestore listener가 언제 시작되고 로그아웃할 때 언제 해제되는지, 화면의 임시 상태와 저장된 상태가 어디서 합쳐지는지도 함께 봐야 한다.
MethodChannel로 Swift 기능 호출하기
VisionKit과 StoreKit처럼 iOS에 특화된 기능은 Flutter만으로 처리하지 않는다.
Flutter와 Swift 사이의 MethodChannel이 다리 역할을 한다.
대표적인 예가 책 스캔이다.
| |
현재 주 스캔 경로는 iOS 16 이상에서 VisionKit을 사용한다. Swift 화면에서 카메라와 텍스트 선택 UI를 띄우고, 사용자가 확인한 문장을 Firebase에 저장한다. Flutter와 iOS가 하나의 기능을 나눠 처리하므로 저장 결과와 화면 상태가 양쪽에서 일관되게 연결되는지를 함께 살펴봐야 하는 구조다.
Firebase가 맡는 역할
Firebase는 인증, 데이터베이스, 서버 함수, 정적 웹 호스팅 등을 제공하는 서비스다. 직접 서버 한 대를 운영하지 않아도 앱의 백엔드 기능을 구성할 수 있지만, 각각의 제품이 어떤 데이터를 읽고 쓰는지 경계를 명확히 해야 한다.
Auth: 이메일 링크 로그인
Ingridea는 비밀번호나 소셜 로그인이 아니라 Firebase Auth의 이메일 링크 로그인을 사용한다. 사용자가 이메일로 받은 링크를 누르면 Firebase Hosting의 인증 페이지와 iOS 딥링크를 거쳐 앱으로 돌아온다. 앱은 링크를 검증해 로그인하고, 최초 사용자라면 사용자 문서와 기본 폴더를 만든다.
| |
Flutter, Web, Swift, Firebase Auth가 한 번의 로그인에 모두 관여한다. 인증 문제를 확인할 때 앱 화면만 봐서는 원인을 찾기 어려운 이유다.
Firestore: 사용자 콘텐츠 저장
Cloud Firestore는 문서 단위로 데이터를 저장하는 NoSQL 데이터베이스다. Ingridea는 별도로 구성한 database에 사용자 콘텐츠와 서비스 상태를 저장한다.
핵심 데이터의 관계를 개념적으로 표현하면 다음과 같다. 실제 collection 이름과 세부 필드는 private 저장소와 운영 환경에만 남긴다.
| 데이터 | 저장하는 내용 | 관계 |
|---|---|---|
| 사용자 | 프로필·구독 상태 | 인증된 사용자 기준 |
| 폴더 | 폴더 이름·페이지 수 | 사용자에게 소속 |
| 페이지 | 스캔한 문장·하이라이트 정보 | 폴더에 소속 |
| 레거시 하이라이트 | 과거 형식의 하이라이트 정보 | 페이지에 종속 |
현재 편집 경로는 텍스트와 하이라이트 정보를 함께 관리한다. 기존에 저장된 데이터와의 호환도 고려해야 하므로 화면의 Dart class만이 아니라 iOS 저장 경로와 Firestore Rules까지 같은 기준으로 살펴봐야 한다.
Functions: 앱이 직접 결정하면 안 되는 일
Cloud Functions는 서버에서 실행되는 코드다. Ingridea의 Functions는 Node.js 20과 TypeScript로 작성돼 있으며 다음 역할을 맡는다.
- App Store 영수증 검증
- App Store Server Notifications 처리
- 이미지 OCR과 텍스트 교정
결제 결과나 외부 API 비밀키처럼 클라이언트만 믿고 처리할 수 없는 값은 서버 경계에 두어야 한다. 앱은 인증된 요청을 보내고, Function이 Apple 또는 OCR·AI 서비스와 통신한 뒤 필요한 결과를 Firestore에 반영한다.
Hosting: 작은 웹 경로도 앱 구조의 일부
Firebase Hosting은 이메일 인증 링크를 처리하는 landing page를 제공한다. Web이 앱의 주 플랫폼은 아니지만, 이메일 링크 로그인에서는 사용자가 앱으로 돌아오는 입구다. 따라서 Flutter 앱만 실행된다고 인증 흐름 전체가 검증된 것은 아니다.
핵심 기능을 따라가며 구조를 읽었다
전체 파일을 위에서 아래로 읽기보다 사용자 흐름을 하나씩 선택했다.
1. 앱이 어떻게 Home까지 가는가
main.dart의 초기화에서 시작해 인증 상태를 감시하는 provider와 화면 분기를 따라갔다.
그 과정에서 Firebase 초기화, 이메일 링크, 최초 계정 설정, 기본 폴더 생성이 하나의
로그인 흐름에 묶여 있다는 것을 알 수 있었다.
2. 스캔한 문장이 어디에 저장되는가
Home의 스캔 버튼에서 시작해 MethodChannel과 Swift의 VisionKit 화면, Firestore write를 따라갔다. 별도로 이미지 파일을 AI OCR에 보내는 경로도 있어, 같은 “스캔"이라는 이름 아래 실제 구현은 두 갈래라는 점을 구분했다.
3. 저장한 문장이 어떻게 편집되는가
페이지 문서는 단순 문자열만 저장하지 않는다. 편집기는 텍스트와 하이라이트 정보를 함께 관리하고, 화면의 임시 변경을 먼저 보여준 뒤 autosave한다. 기존 데이터와 현재 편집 경로가 함께 동작하는지도 실제 Widget의 호출부를 따라가며 확인했다.
4. Pro 권한은 어디서 결정되는가
Flutter의 결제 화면에서 StoreKit, 영수증 브리지, Firebase Function, Firestore 구독 문서까지 이어지는 흐름을 따라갔다. 결제 성공 UI와 실제 구독 권한은 같은 의미가 아니며, Apple 검증 결과를 누가 신뢰하고 저장하는지가 핵심이었다.
처음 파악하며 세운 기준
Ingridea는 Flutter, Swift, Firebase가 분리된 세 프로젝트가 아니라 하나의 사용자 행동을 나눠 처리하는 구조였다. 그래서 이후 개발에서는 다음을 기준으로 삼기로 했다.
- 기능 이름보다 실제 호출 경로를 기준으로 현재 동작을 판단한다.
- Flutter와 Swift 중 누가 Firestore를 쓰는지 함께 확인한다.
- 인증 정보와 사용자 콘텐츠의 소유 관계가 일관되는지 확인한다.
- 결제와 외부 API처럼 신뢰가 필요한 판단은 서버 경계를 살펴본다.
- Android와 Web의 존재를 곧바로 기능 지원으로 해석하지 않는다.
- 문서, 저장소 선언, 실제 배포 상태를 서로 다른 근거로 대조한다.
서비스의 큰 흐름을 이해하고 나니 바로 기능을 추가하기보다 먼저 정리해야 할 지점이 보였다. production과 분리된 로컬 실행 환경, 데이터 접근 경계, 결제 검증, 외부 API 호출 구조, OCR 비용 상한이 그 대상이었다.
이 기준으로 첫 번째 정비 작업을 진행하기로 했다.

댓글