Skip to content

Latest commit

 

History

History
282 lines (225 loc) · 19 KB

README.md

File metadata and controls

282 lines (225 loc) · 19 KB

PIC - 우리만의 기억, 네컷에 담아요

📸 그날의 추억을 기록하는 우리만의 네컷사진

👥 그룹 만들기 : 원하는 친구들과 그룹방을 만들어요

🗳️ 투표하기 : 함께 찍은 사진들을 다함께 투표해서 골라요

🏞️ 네컷 만들기 : 베스트 사진으로 네컷 사진이 만들어져요

🔗 PIC iOS 다운로드

🤖 프로젝트 설정 및 실행 방법

🙋‍♀️ 가자 빵집으로~ 🍞 iOS 팀원
🐷 송현아 🐸 양준혁
🙉 조찬우 🐨 최혜린

🌼 주요 기능

[ 간단한 회원가입! ]
Apple/Kakao로 간편하게
PIC의 회원이 되어 보세요!
[ 그룹으로 다같이! ]
✅ 초대코드 입력하기로
전달받은 그룹 코드로 참가해봐요
✅ 그룹 만들기로
네컷을 함께만들 그룹을 만들어요
[ 그룹 만들기 ]
초대코드가 없다면
그룹을 생성해보아요!
1️⃣그룹의 이름을 정해요
2️⃣이 그룹은 어떤 모임인가요?
3️⃣그룹의 대표 사진을 정해주세요!
완성된 그룹에 친구를 바로 초대해봐요
[ 다함께 만드는 네컷 사진! ]
그룹에서 네컷 생성하기를 누르고
그룹에 새로운 네컷을 만들어요
1️⃣네컷의 이름을 정해요
2️⃣사진을 찍은 날짜를 입력해요
3️⃣사진을 최대 4장 올려요
[ 친구도 사진을 올려요! ]
그룹 내 다른친구들도
멋진 사진을 올려주세요!
사진은 최대 4장까지 가능해요
[ 픽미픽미~ 사진 투표하기! ]
그룹원이 모두 사진을 올리면
투표하기를 눌러
각자 사진 좋아요를 투표해요
✅ 좌,우 슬라이드 혹은
✅ X,O 버튼을 눌러
최고의 네컷을 투표해봐요!
[ 82빨리 해달라고~ ]
쿡 찌르기를 눌러
게으른 그룹원에게 알림을 보내기!
사진 올리기, 투표하기 빨리해줘~
[ 우리의 네컷사진 완성! ]
추억 만들기 완료!
완성된 네컷사진을 공유해봐요
완성된 네컷사진은
그룹 내 하단에서 확인가능해요!
[ 그룹 관리하기 ]
그룹 내부 우측상단 👥을 눌러
참가한 그룹원을 확인해요!
그룹 초대 코드를 복사해서
친구를 더 초대해봐요!
[ 마이페이지 ]
✅ 사용자의 이름을 확인해보세요
✅ 현재 앱 버전을 체크해보세요
✅ 앱의 알림을 On/Off할 수 있어요
[ 로그아웃 하기 ]
현 기기에서 로그아웃할 수 있어요
[ PIC 탈퇴하기🥹 ]
PIC을 떠나실건가요...?

💪 업데이트된 기능

[ 그룹 탈퇴 기능 추가 ]
그룹 생성 및 참가 후 원하면
그룹에서 나갈 수 있어요!
[ 키워드 필터링 기능 추가 ]
모임 종류 별로 볼 수 있어요
[ 그룹 그리드 추가 ]
그룹을 편한 방법으로 보세요!

📚 개발 프로세스

1️⃣ SwiftUI와 TCA를 활용한 UI 구현 및 관리

  • UI 구현: SwiftUI의 함수형 방식을 활용해 UI를 구성하고, TCA의 Action과 State를 사용해 Source of Truth를 체계적으로 관리
  • 효율성 극대화: 각 모듈별 Scheme을 생성하여 독립적으로 SwiftUI 프리뷰를 활용하여 작업 속도와 효율성 향상
  • 공유 상태 관리: 사용자 정보를 Shared State로 관리하여 여러 화면에서 일관된 접근과 활용을 보장
  • 화면 전환 관리: TCA 기반 Coordinator를 사용해 화면 전환 로직을 명확히 분리하고 체계적으로 처리
  • 모듈화 및 환경 전환: TCA DependencyKey를 통해 의존성을 모듈화하고, 환경별 동작을 손쉽게 전환 가능

2️⃣ Tuist를 활용한 의존성 관리

스크린샷 2025-01-20 오후 2 44 23
  • 효율적인 의존성 관리: Tuist를 사용해 외부 의존성을 선언적으로 관리하여 프로젝트 설정 시간을 단축하고, 테스트와 코드 모듈화를 용이하게 개선
  • 환경별 구분 관리: Tuist의 설정을 활용해 개발 환경을 Dev와 Prod로 명확히 분리, 환경에 따라 필요한 설정과 의존성을 독립적으로 관리 가능
  • 모듈화된 구조 설계: 각 기능을 독립된 모듈로 구분하고, Tuist를 통해 모듈 간 의존성을 체계적으로 관리하여 유지보수성과 확장성을 극대화
  • 자동화와 일관성 보장: Xcode 프로젝트를 Tuist로 생성 및 관리하여 설정의 일관성을 유지하고, 팀 협업에서 충돌을 최소화
App: 앱 관련 최상위 파일들만 포함 (런치스크린, Scene/AppDelegate, main App 등)
Coordinator: 각 Feature를 기준으로 Coordinator 생성하여 화면 전환을 관리
Core: 모델, 서비스, 공통 익스텐션 등 핵심 기능 모듈
DesignSystem: 디자인 정의에 따른 컴포넌트와 디자인 관련 공통 익스텐션을 포함한 모듈
Common: Extension 및 Utilities를 포함한 모듈
Services: 외부라이브러리 Dependency 관리하는 모듈
Feature: 실제 기능을 구현한 모듈 / Scene: Feature의 실제 기능 화면을 구현한 모듈
LoveBug: 재사용 가능한 독립적인 Model과 DesignSystem을 관리하는 모듈

3️⃣ 디자인 시스템 구축하여 관리

  • 반복적 UI 요소의 체계화: 자주 사용되는 UI 요소(버튼, 텍스트 필드, 카드 등)를 표준화하여 디자인 시스템으로 정리 및 구축
  • 일관된 UI/UX 제공: 모든 화면에서 일관된 스타일과 동작을 유지하도록 디자인 시스템을 활용하여 사용자 경험을 향상
  • 생산성 향상: 재사용 가능한 UI 컴포넌트를 설계하여 개발 속도를 높이고, 중복된 작업을 최소화
  • 유지보수 용이: 중앙에서 관리되는 디자인 시스템을 통해 변경 사항이 전체 앱에 일괄 적용되므로 유지보수가 간편
  • 협업 강화: 디자이너와 개발자가 공통된 디자인 시스템을 사용하여 커뮤니케이션 비용을 줄이고 협업을 강화

🔥 트러블 슈팅

1️⃣ open URL 구현 위치

Kakao 공식 문서 상 open URL을 구현하는 방법은 총 3가지입니다.

  1. AppDelegate에 구현하는 방법
import KakaoSDKAuth
...
class AppDelegate: UIResponder, UIApplicationDelegate {
    ...
    func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any] = [:]) -> Bool {
        if (AuthApi.isKakaoTalkLoginUrl(url)) {
            return AuthController.handleOpenUrl(url: url)
        }
        return false
    }
}

해당 방법은 AppDelegate와 SceneDelegate가 분리전인 iOS 13 이전 버전에서 사용해야 합니다.

  1. SceneDelegate에 구현하는 방법
import KakaoSDKAuth
...
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
    ...
    func scene(_ scene: UIScene, openURLContexts URLContexts: Set<UIOpenURLContext>) {
        if let url = URLContexts.first?.url {
            if (AuthApi.isKakaoTalkLoginUrl(url)) {
                _ = AuthController.handleOpenUrl(url: url)
            }
        }
    }
}

iOS 13 이상으로 생성된 프로젝트라면 Info.plist 파일에 UIApplicationSceneManifest 설정이 추가되며, UISceneDelegate.swift를 기본으로 사용하도록 설정되기 때문에, 해당 매서드를 SceneDelegate에서 구현해야합니다.

  1. App에 구현하는 방법
import SwiftUI
import KakaoSDKCommon
import KakaoSDKAuth

@main
struct SwiftUI_testApp: App {
    var body: some Scene {
        WindowGroup {
            // onOpenURL()을 사용해 커스텀 URL 스킴 처리
            ContentView().onOpenURL(perform: { url in
                if (AuthApi.isKakaoTalkLoginUrl(url)) {
                    AuthController.handleOpenUrl(url: url)
                }
            })
        }
    }
    ...
}

✅ 저희 앱의 경우 AppDelegate를 살려놨기 때문에 AppDelegate에서 해당 매서드를 구현했습니다. 하지만 token이 저장되지 않는다는 에러가 발생하여 원인을 찾다보니, 저희 앱은 iOS 13 버전 이상을 타겟하는 앱이기 때문에 해당 매서드를 AppDelegate에서 구현하면 안된다는 것을 알게되었습니다. 하여 App에서 최초 ContentView에 진입 시 .onOpenURL 에 해당 매서드를 구현하였습니다. 참고

2️⃣ open URL 시 Main Thread Running

스크린샷 2024-06-17 오전 5 24 46 실행 시 Kakao를 열 때 Thread 이슈가 발생하여 AuthController의 handleOpenUrl매서드를 호출하는 함수에 @MainActor 매크로를 사용하여 Thread 문제를 해결하고자 하였습니다. 하지만 해당 매서드는 URL경로를 등록하는 개념의 매서드이기 때문에 Thread 문제와는 무관하다는 것을 알게되었습니다. 하여 실제 화면 변화가 일어나는 Login 매서드에 @MaingActor를 사용하여 문제를 해결하였습니다.

3️⃣ TCA의 구조에 맞추어 delegate를 Dependency 내부에서 설정

🔗 참고 자료: FCM 등록 토큰 액세스

Firebase의 등록 토큰을 수신하기 위해선 AppDelegate에서 MessagingDelegate를 채택하여 구현해야합니다. 하지만 MessagingDelegateTCA 구조에 맞추어 생각했을 때, FirebaseClientDependency 내부에서 관리하는 것이 맞다고 판단하였습니다. 하여 AppDelegate가 아닌 Dependency내부에서 대리자 설정을 하고자는 문제가 발생하였습니다.

TCA의 UsernotificationClient 예시를 보면 Delegate 객체를 생성하여 AsyncStream을 활용하면 Dependency 내부에서 대리자를 설정하는 방법을 참고할 수 있었습니다. 이를 참고하여 MessagingDelegate 객체를 Dependency 내부에서 생성하여 필요에 따라 호출되어 사용될 수 있도록 만들었습니다.

기존 MessagingDelegate 코드
class AppDelegate: UIResponder, UIApplicationDelegate, MessagingDelegate {
    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        Messaging.messaging().delegate = self

        return true
    }
    func messaging(_ messaging: Messaging, didReceiveRegistrationToken fcmToken: String?) {
        ...
    }
}
TCA Dependency내 MessagingDelegate 코드
@DependencyClient
public struct FirebaseClient: Sendable {
 public var delegate: @Sendable () -> AsyncStream<DelegateEvent> = { .finished }
 
 public enum DelegateEvent {
   case messaging(
     _ messaging: Messaging,
     fcmToken: String?
   )
 }
}

extension FirebaseClient: DependencyKey {
 public static var liveValue: FirebaseClient {
   return FirebaseClient(
     delegate: {
       AsyncStream { continuation in
         let delegate = MessageDelegate(continuation: continuation)
         Messaging.messaging().delegate = delegate
         continuation.onTermination = { _ in
           _ = delegate
         }
       }
     }
   )
 }

extension FirebaseClient {
 final class MessageDelegate: NSObject, MessagingDelegate, Sendable {
   let continuation: AsyncStream<DelegateEvent>.Continuation
   
   init(continuation: AsyncStream<DelegateEvent>.Continuation) {
     self.continuation = continuation
   }
   
   func messaging(
     _ messaging: Messaging,
     didReceiveRegistrationToken fcmToken: String?
   ) {
     continuation.yield(.messaging(messaging, fcmToken: fcmToken))
   }
 }
}

4️⃣ Firebase Library 충돌 문제

FirebaseClient로 Firebase Library와 Dependency를 생성하니 아래와 같은 에러가 발생하였습니다.

에러: 'NSInvalidArgumentException', reason: '-[FIRInstallationsItem registeredInstallationWithJSONData:date:error:]:

이는 정적 라이브러리와 Objective-C의 동적 특성 간의 충돌로 인해, 정적 라이브러리에 있는 카테고리 메서드가 앱에 링크되지 않기 때문에 에러가 발생한 케이스입니다. 해당 에러는 registeredInstallationWithJSONData:date:error:메서드가 호출 되었지만 해당 메서드가 FIRInstallationsItem 클래스 내부에 존재하지 않는 것을 뜻합니다. Objective-C는 메서드를 호출하기 전까지 메서드의 구현 코드를 결정하지 않으며, 메서드에 대한 링커 심볼을 정의하지 않고 클래스에 대한 심볼만 정의합니다. 카테고리는 메서드들의 모음이므로 카테고리의 메서드는 심볼을 생성하지 않습니다. 따라서 클래스가 이미 정의된 경우, 링커는 카테고리에 정의된 메서드를 로드하지 않습니다.

-ObjC 링커 플래그는 링커가 정적 라이브러리에 있는 모든 Objective-C 클래스와 카테고리를 로드하도록 합니다. 하여 FirebaseClient에 의존하는 Service 모듈의 setting에 "OTHER_LDFLAGS": "-ObjC"를 추가하여 해결하였습니다.

🧐 재밌는건 FirebaseAnalytics는 정적 프레임워크로만 배포된다는 점입니다. The 7.x update applies to all Firebase libraries except FirebaseAnalytics, which continues to be distributed as a binary static framework. 이렇게 제공되는 모듈이 많은데 말이죠.. 자세한 이유는 여기에서 보실 수 있습니다.

참고 🍎 Technical Q&A 🔗 블로그 링크 🔗 블로그 링크 🔗 Firebase git issues

5️⃣ Push Notification을 Tuist에서 설정

Tuist를 사용하면 Info.plistBuild Setting을 코드로 간단히 설정할 수 있습니다. 하지만 Tuist를 사용하여 Xcode 프로젝트에서 App > Capabilities > Push Notifications를 활성화를 하기 위해선 별도의 entitlements 파일이 필요하였습니다. 이는 Bundle ResourcesEntitlements, Information Property List, Privacy manifest filesPush Notifications는 service 혹은 technology의 사용허가를 담당하는 entitlements에 해당되기 때문입니다. Apple Push Notification service (APNs) 환경 사용 여부를 결정하는 APS Environment 키 값에 developmentproduction 권한 값을 각각 설정하였습니다.

참고 🍎 APS Environment Entitlement

6️⃣ DependencyClient unimplemented value

  • DependencyClient 매크로 사용 시 throw되지 않고 void가 아닌 반환인 클로저가 있는 경우, 구현되지 않은 값을 생성할 수 있도록 기본값을 줘야합니다. 런타임 시 충돌 없이 테스트 및 SwiftUI preview에 해당 기본값을 사용하여 즉각 접근할 수 있기 때문입니다.

참고 ⚙️ 소소한 Hotfix

7️⃣ KeyChain Create Error

KeyChain Create 시 이미 값이 존재하는 경우 아래 경고가 떴습니다.

An "Effect.run" returned from "KakaoLogin/LoginCore.swift:124" threw an unhandled error. …

    KeyChainClientError(
      userInfo: [:],
      code: .failToCreate,
      underlying: nil
    )

All non-cancellation errors must be explicitly handled via the "catch" parameter on "Effect.run", or via a "do" block.

이에 createstatuserrSecDuplicateItem의 경우 create가 아닌 update를 할 수 있도록 로직 수정하였습니다.

status 참고 링크