👥 그룹 만들기 : 원하는 친구들과 그룹방을 만들어요
🗳️ 투표하기 : 함께 찍은 사진들을 다함께 투표해서 골라요
🏞️ 네컷 만들기 : 베스트 사진으로 네컷 사진이 만들어져요
[ 그룹 탈퇴 기능 추가 ] 그룹 생성 및 참가 후 원하면 그룹에서 나갈 수 있어요! |
[ 키워드 필터링 기능 추가 ] 모임 종류 별로 볼 수 있어요 |
[ 그룹 그리드 추가 ] 그룹을 편한 방법으로 보세요! |
- UI 구현: SwiftUI의 함수형 방식을 활용해 UI를 구성하고, TCA의 Action과 State를 사용해 Source of Truth를 체계적으로 관리
- 효율성 극대화: 각 모듈별 Scheme을 생성하여 독립적으로 SwiftUI 프리뷰를 활용하여 작업 속도와 효율성 향상
- 공유 상태 관리: 사용자 정보를 Shared State로 관리하여 여러 화면에서 일관된 접근과 활용을 보장
- 화면 전환 관리: TCA 기반 Coordinator를 사용해 화면 전환 로직을 명확히 분리하고 체계적으로 처리
- 모듈화 및 환경 전환: TCA DependencyKey를 통해 의존성을 모듈화하고, 환경별 동작을 손쉽게 전환 가능
- 효율적인 의존성 관리: 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을 관리하는 모듈
- 반복적 UI 요소의 체계화: 자주 사용되는 UI 요소(버튼, 텍스트 필드, 카드 등)를 표준화하여 디자인 시스템으로 정리 및 구축
- 일관된 UI/UX 제공: 모든 화면에서 일관된 스타일과 동작을 유지하도록 디자인 시스템을 활용하여 사용자 경험을 향상
- 생산성 향상: 재사용 가능한 UI 컴포넌트를 설계하여 개발 속도를 높이고, 중복된 작업을 최소화
- 유지보수 용이: 중앙에서 관리되는 디자인 시스템을 통해 변경 사항이 전체 앱에 일괄 적용되므로 유지보수가 간편
- 협업 강화: 디자이너와 개발자가 공통된 디자인 시스템을 사용하여 커뮤니케이션 비용을 줄이고 협업을 강화
Kakao 공식 문서 상 open URL을 구현하는 방법은 총 3가지입니다.
- 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 이전 버전에서 사용해야 합니다.
- 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에서 구현해야합니다.
- 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
에 해당 매서드를 구현하였습니다.
참고
실행 시 Kakao를 열 때 Thread 이슈가 발생하여 AuthController의 handleOpenUrl매서드를 호출하는 함수에 @MainActor 매크로를 사용하여 Thread 문제를 해결하고자 하였습니다. 하지만 해당 매서드는 URL경로를 등록하는 개념의 매서드이기 때문에 Thread 문제와는 무관하다는 것을 알게되었습니다. 하여 실제 화면 변화가 일어나는 Login 매서드에 @MaingActor를 사용하여 문제를 해결하였습니다.
Firebase의 등록 토큰을 수신하기 위해선 AppDelegate
에서 MessagingDelegate
를 채택하여 구현해야합니다.
하지만 MessagingDelegate
는 TCA
구조에 맞추어 생각했을 때, FirebaseClient
즉 Dependency
내부에서 관리하는 것이 맞다고 판단하였습니다.
하여 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))
}
}
}
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. 이렇게 제공되는 모듈이 많은데 말이죠.. 자세한 이유는 여기에서 보실 수 있습니다.
Tuist를 사용하면 Info.plist
와 Build Setting
을 코드로 간단히 설정할 수 있습니다.
하지만 Tuist를 사용하여 Xcode 프로젝트에서 App > Capabilities > Push Notifications
를 활성화를 하기 위해선 별도의 entitlements
파일이 필요하였습니다.
이는 Bundle Resources
의 Entitlements
, Information Property List
, Privacy manifest files
중 Push Notifications
는 service 혹은 technology의 사용허가를 담당하는 entitlements
에 해당되기 때문입니다.
Apple Push Notification service (APNs)
환경 사용 여부를 결정하는 APS Environment
키 값에 development
와 production
권한 값을 각각 설정하였습니다.
- DependencyClient 매크로 사용 시 throw되지 않고 void가 아닌 반환인 클로저가 있는 경우, 구현되지 않은 값을 생성할 수 있도록 기본값을 줘야합니다. 런타임 시 충돌 없이 테스트 및 SwiftUI preview에 해당 기본값을 사용하여 즉각 접근할 수 있기 때문입니다.
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.
이에 create
시 status
중 errSecDuplicateItem
의 경우 create
가 아닌 update
를 할 수 있도록 로직 수정하였습니다.