📌 앱 소개: 언어 학습자들이 서로 연결되어 게시물을 공유하고 상호작용하는 SNS 앱
🕒 기간: 2025.05.16 ~ 2025.05.27 (2주)
📱 플랫폼: Flutter 크로스 플랫폼 앱 (Android, iOS)
👥 개발 인원: 4명
💼 역할: 팀 리더, CI/CD 파이프라인 구축, 인증 시스템, 프로필 관리, 피드 필터링, Google Maps 연동, 온보딩 플로우 개발
🛠️ 주요 사용 기술: Flutter
Firebase
Riverpod
Clean Architecture
Google OAuth
Firestore
Cloud Functions
GitHub Actions
VWorld API
🔗 GitHub: zero-to-one-flutter/flutter-share-lingo
🔗 Play Store: ShareLingo on Play Store
├── app/ # 앱 전체 설정 및 공통 상수, 테마 등
│ ├── constants/ # 앱 상수 정의
│ │ ├── app_colors.dart # 색상 정의
│ │ ├── app_constants.dart # 상수 값 정의 (언어 목록, 태그 등)
│ │ └── app_styles.dart # 스타일 정의
│ └── theme.dart # 앱 테마 설정
├── core/ # 앱 전체에서 사용되는 핵심 기능 및 유틸리티
│ ├── exceptions/ # 앱 전체에서 사용되는 예외 클래스
│ ├── extensions/ # 확장 메서드 정의
│ ├── providers/ # 공통 프로바이더
│ ├── ui_validators/ # UI 유효성 검사기
│ └── utils/ # 유틸리티 함수
│ ├── dialogue_util.dart # 다이얼로그 관련 유틸리티
│ ├── format_time_ago.dart # 시간 포맷팅 유틸리티
│ ├── general_utils.dart # 일반 유틸리티 함수
│ ├── geolocator_util.dart # 위치 관련 유틸리티
│ ├── logger.dart # 로깅 유틸리티
│ ├── map_url_util.dart # 지도 URL 생성 유틸리티
│ ├── navigation_util.dart # 네비게이션 관련 유틸리티
│ ├── snackbar_util.dart # 스낵바 관련 유틸리티
│ └── throttler_util.dart # 스로틀링 유틸리티
├── data/ # 데이터 관련 클래스 및 데이터 액세스 계층
│ ├── data_source/ # 데이터 소스 클래스
│ ├── dto/ # 데이터 전송 객체
│ └── repository/ # 리포지토리 구현체
├── domain/ # 비즈니스 로직 및 엔티티 정의
│ ├── entity/ # 도메인 엔티티
│ ├── repository/ # 리포지토리 인터페이스
│ └── usecase/ # 유스케이스
├── presentation/ # UI 관련 코드
│ ├── pages/ # 앱 화면
│ │ ├── home/ # 홈 화면 (예시)
│ │ │ ├── home_page.dart # 홈 페이지
│ │ │ ├── home_view_model.dart # 홈 뷰모델
│ │ │ └── widgets/ # 홈 화면 관련 위젯들
│ ├── widgets/ # 공통 위젯
│ └── user_global_view_model.dart # 전역 사용자 뷰모델
├── main.dart # 앱 진입점
Play Store
출시까지 성공적으로 완료Clean Architecture
패턴 도입 및 전체 프로젝트 구조 설계flutter analyze
코드 품질 검증 시스템 도입으로 수동 검수 시간 50% 단축test-apk
브랜치로의 푸시 시 자동 APK 빌드 및 GitHub Artifacts
업로드로 QA 테스트 프로세스 간소화Firebase
설정 파일의 Base64 인코딩 후 GitHub Secrets
저장을 통한 보안 관리로 민감한 정보 보호와 CI 환경 안정성 확보Google Sign-In
과 Firebase Authentication
연동을 통한 원클릭 로그인 시스템 구축Google Maps Static API
를 활용하여 사용자 위치 중심의 개인화된 지도를 프로필 배경으로 적용VWorld API
연동으로 정확한 지역 정보 제공전체
탭: 모든 게시물을 시간순으로 표시하는 기본 피드추천
탭: 내 학습 언어를 모국어로 하고 내 모국어를 학습하는 사용자들의 게시물 필터링으로 상호 언어 교환 가능한 사용자 매칭동급생
탭: 나와 동일한 모국어와 학습 언어 조합을 가진 사용자들의 게시물 표시로 학습 동기 부여근처
탭: 동일한 읍면동에 거주하는 사용자들의 게시물만 표시하여 오프라인 모임 연결 지원Firestore Security Rules
를 통한 사용자별 데이터 소유권 검증 시스템 구현SharedPreferences
를 활용한 동의 상태 영구 저장으로 재동의 요구 방지URL Launcher
를 활용한 인앱 브라우저로 이용약관 및 개인정보처리방침 제공 및 설정 페이지에서도 접근 가능하도록 구현Riverpod
을 활용한 의존성 주입 패턴으로 모든 클래스가 생성자를 통해 필요한 의존성을 받아 테스트 시 Mock 객체 주입 가능Repository
패턴으로 데이터 접근 추상화를 통해 Firebase
에서 다른 백엔드로 전환 시에도 비즈니스 로직 변경 없이 대응 가능Mocktail
을 활용한 외부 의존성 격리로 Firebase
나 Google Sign-In
없이도 독립적인 테스트 실행 환경 구축Firebase Crashlytics
연동으로 실시간 오류 모니터링 체계 구축1. 클린 아키텍처와 의존성 주입 패턴 도입
요구 사항
팀 개발 시 복잡한 기능을 체계적으로 관리하고 코드 충돌을 최소화하며, 향후 확장과 유지보수를 고려한 확장 가능한 아키텍처 필요
의사 결정
Clean Architecture
패턴과 Riverpod
기반 의존성 주입 시스템 구축을 결정
// Repository 인터페이스 (Domain Layer)
abstract class AuthRepository {
Future<AppUser?> signInWithGoogle();
Future<void> signOut();
Stream<String?> authStateChanges();
}
// Repository 구현체 (Data Layer)
class AuthRepositoryImpl implements AuthRepository {
final GoogleSignInDataSource _googleSignIn;
final FirebaseAuthDataSource _firebaseAuth;
final UserDataSource _userDataSource;
AuthRepositoryImpl(this._googleSignIn, this._firebaseAuth, this._userDataSource);
// 구현...
}
// 의존성 주입 설정
final authRepositoryProvider = Provider<AuthRepository>(
(ref) => AuthRepositoryImpl(
ref.read(googleSignInDataSourceProvider),
ref.read(firebaseAuthDataSourceProvider),
ref.read(userFirestoreDataSourceProvider),
),
);
2. 사용자 간 거리 계산을 위한 GeoPoint Extension 설계
요구 사항 사용자들이 실제로 얼마나 가까이 있는지 직관적으로 표시 필요
의사 결정
GeoPoint Extension
방식으로 거리 계산 로직을 구현하기로 결정
GeoPoint
객체에 직접 메서드를 추가하여 어디서든 geoPoint.distanceFrom(otherPoint)
형태로 간단히 사용 가능distanceBetween
메서드 활용으로 지구 곡률을 고려한 정확한 거리 계산"3.2 km"
)은 ViewModel/UI 계층에서 처리하여 로직과 표현을 분리extension GeoPointExtensions on GeoPoint {
double distanceFrom(GeoPoint other) {
final distanceInMeters = Geolocator.distanceBetween(
latitude, longitude, other.latitude, other.longitude,
);
return distanceInMeters / 1000;
}
}
// UserGlobalViewModel에서 활용
String? calculateDistanceFrom(GeoPoint? otherLocation) {
final userLocation = state?.location;
if (userLocation == null || otherLocation == null) return null;
final distanceKm = userLocation.distanceFrom(otherLocation);
return '${distanceKm.toStringAsFixed(1)} km';
}
1. 온보딩 사용자 경험 최적화
문제 상황
위치 권한 거부 시 앱 사용이 제한되거나 오류가 발생하여 사용자가 앱을 이탈하는 문제 발생. 특히 일시적 거부와 영구적 거부를 구분하지 않아 적절한 안내 메시지 제공 불가
success
: 정상적인 위치 정보 획득deniedTemporarily
: 일시적 거부 - 권한 재요청 안내deniedForever
: 영구적 거부 - 설정 앱 이동 안내error
: 기술적 오류 - 재시도 또는 위치 없이 진행 안내enum LocationStatus { success, deniedTemporarily, deniedForever, error }
Future<(LocationStatus, Position?)> getPosition() async {
try {
final permission = await Geolocator.checkPermission();
if (permission == LocationPermission.denied ||
permission == LocationPermission.deniedForever) {
final requested = await Geolocator.requestPermission();
if (requested == LocationPermission.denied) {
return (LocationStatus.deniedTemporarily, null);
}
if (requested == LocationPermission.deniedForever) {
return (LocationStatus.deniedForever, null);
}
}
final position = await Geolocator.getCurrentPosition(
locationSettings: const LocationSettings(
accuracy: LocationAccuracy.high,
distanceFilter: 100,
),
);
return (LocationStatus.success, position);
} catch (e) {
return (LocationStatus.error, null);
}
}
2. GitHub Actions에서 Firebase 설정 파일 부재 문제
문제 상황
GitHub Actions 워크플로우에서 firebase_options.dart
파일이 필요하지만 보안상 Git에 커밋할 수 없어 CI 빌드 시 Target of URI doesn't exist: firebase_options.dart
오류 발생
문제 분석
GitHub Secrets는 멀티라인과 특수문자 처리가 불안정하며, iOS/Android 플랫폼별로 다른 형식의 설정 파일이 필요함을 확인
해결 방안 도출:
Base64 인코딩을 통해 바이너리 안전 문자열로 변환하면 한 줄로 저장 가능하고 디코딩 시 원본 복원됨을 검증
- name: Decode firebase_options.dart
run: |
mkdir -p lib
echo "$" | base64 --decode > lib/firebase_options.dart
3. 프로필 수정 시 데이터 동기화 최적화
문제 상황
사용자가 프로필 정보를 수정할 때 해당 사용자의 모든 게시물과 댓글에 반영되어야 하는데, 클라이언트에서 직접 처리하면 네트워크 오류나 앱 종료로 인한 부분 업데이트 실패로 데이터 불일치 가능성 존재
collectionGroup
쿼리로 모든 서브컬렉션의 댓글을 효율적으로 조회 및 업데이트