Daehan Lim

Header Project Icon

ShareLingo - 언어교류 SNS 앱

📝 개요

📌 앱 소개: 언어 학습자들이 서로 연결되어 게시물을 공유하고 상호작용하는 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

1 login screen 7 my profile screen 8 my profile posts screen 9 edit profile screen 2 onboarding date screen 3 onboarding name screen 4 onboarding language screen 5 onboarding language selection screen 11 self intro screen 6 onboarding location screen 10 settings screen

📖 프로젝트 배경

🛠️ Tech Stack

Flutter Dart Riverpod Clean Architecture Firebase Firestore Cloud Functions Google OAuth Crashlytics GitHub Actions Google Maps MVVM Firebase Auth VWorld API Geolocator Cached Network Image Dio Mocktail Firebase Storage Image Picker SharedPreferences URL Launcher

📋 프로젝트 구조

├── 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                          # 앱 진입점

🌟 수행 내용 및 성과

프로젝트 리더십 및 전체 개발 프로세스 관리

CI/CD 파이프라인 구축

Google OAuth 인증 및 사용자 관리 시스템

다단계 온보딩 및 위치 기반 매칭 시스템

언어 학습 맞춤형 4가지 피드 탭 구현 및 필터링 로직

Firebase 백엔드 자동화 및 보안 시스템

이용약관 및 개인정보처리방침

아키텍처 및 테스트, 예외 처리 시스템

🧭 기술적 의사결정

1. 클린 아키텍처와 의존성 주입 패턴 도입

// 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 설계

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. 온보딩 사용자 경험 최적화

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 설정 파일 부재 문제

- name: Decode firebase_options.dart  
  run: |  
    mkdir -p lib  
    echo "$" | base64 --decode > lib/firebase_options.dart

3. 프로필 수정 시 데이터 동기화 최적화

🎞️ 시연 영상

Watch the Video