안녕하세요! 안드로이드 파트 이유빈입니다:)
여러분은 평소에 앱을 어떻게 실행하시나요?
아마 대부분은 배경화면에 있는 앱 아이콘을 눌러 실행하실 겁니다.
하지만 이번 터닝 4차 스프린트에서는 알림, 카카오톡 공유 기능을 추가하면서 앱을 총 세 가지 경로로 실행할 수 있도록 설계해야 했습니다.
- 일반적으로 앱 아이콘을 눌러 실행
- 알림을 클릭하여 실행
- 카카오톡 공유 링크를 통해 실행
이처럼 앱을 다양한 방식으로 실행할 수 있게 되면서, 앱의 진입 구조를 어떻게 설계할지에 대한 고민이 깊어졌고, 그 과정에서 많은 것을 배울 수 있었습니다.
이번 글에서는 그 고민과 배움의 과정을 공유하고자 합니다!
터닝 구조
들어가기에 앞서, 터닝의 구조에 대해 먼저 설명해드리도록 하겠습니다.
터닝 안드로이드는 100% Jetpack Compose 기반 SingleActivity 구조입니다.
또한 Clean Architecture를 따르는 Multi-Module 설계를 적용하고 있습니다.
전체 모듈 의존성은 위와 같습니다. 다소 복잡하게 느껴지실 수 있지만,
각 기능은 Clean Architecture를 기반으로
presentation → domain ← data
방향으로 의존을 하고 있다고 이해하시면 됩니다.
앞으로 알림 및 카카오톡 공유 기능을 구현하기 위해 일부 모듈이 추가될 예정이며, 헷갈릴 수 있을 것 같아 간단히 정리하고 넘어가겠습니다.
<모듈 정리>
:core:firebase ➡️ 파이어베이스 관련 로직이 들어가는 모듈 (예: FCM)
ㄴ 이하 Firebase 모듈
:feature:main ➡️ 앱의 진입점인 MainAcitvity가 들어가는 모듈
ㄴ 이하 Main 모듈
:core:navigator ➡️ MainActivity의 호출용 인터페이스
ㄴ 이하 Navigator 모듈
딥링크 도입 이유
제가 딥링크를 도입하게 된 이유는 알림과 카카오톡 공유 기능을 구현하기 위해서였는데요,
각 기능별로 딥링크가 필요한 이유를 아래와 같이 설명드리겠습니다.
✅알림
알림을 클릭했을 때 홈, 탐색, 달력 중 하나의 화면으로 이동시켜야 했습니다.
일반적으로 알림 클릭 시에는 PendingIntent를 사용해 특정 화면으로 이동하지만,
터닝은 SingleActivity 기반 구조이기 때문에 Intent를 통한 화면 전환은 한 번만 가능합니다.
이 제한을 해결하기 위해 각 화면에 대한 딥링크를 등록하고,
딥링크 값에 따라 NavHost의 startDestination을 동적으로 설정하는 방식으로 문제를 해결했습니다.
또한 알림 기능은 Firebase의 FCM 기능을 활용하므로, 이를 처리할 별도의 Firebase 모듈을 만들어 적용했습니다.
최종 흐름 정리
1. Firebase 모듈에서 서버로부터 이동해야 할 화면의 타입을 전달받는다.(예: HOME, SEARCH, CALENDAR)
2. 해당 타입에 따라 딥링크 URI를 생성한다. (예: terning://home)
3. 딥링크를 Intent.data에 담아 MainActivity를 실행한다.
4. MainActivity에서는 전달받은 딥링크를 기준으로 NavHost의 시작 화면(startDestination)을 설정하여 해당 화면을 띄운다
✅카카오톡 공유하기
카카오톡 공유하기 기능에서는 딥링크가 필수적으로 요구됩니다.
카카오 디벨로퍼스에서 해당 기능을 설정할 때, 공유 메시지를 통해 앱을 열 수 있는 경로로 딥링크 URI를 등록하게 되어 있기 때문입니다.
따라서 이 기능을 구현하기 위해서는 자연스럽게 딥링크 도입이 필요했습니다.
구조 바꾸기
앞서 언급한 '딥링크 도입 이유 - 알림'에서도 알 수 있듯이, Firebase 모듈에서 생성한 딥링크는 Main 모듈에서 전달받아 사용해야 합니다.
이에 자연스럽게 "그럼 Firebase 모듈이 Main 모듈을 참조하면 되는 거 아닌가?" 라는 생각이 떠올랐습니다.
그러면 위 그래프 모듈에서 동그라미 친 부분처럼 의존성이 나오게 됩니다.
이렇게 될 경우 여러 문제점이 발생합니다.
- Firebase 모듈의 책임
- Firebase 모듈을 만든 이유는 파이어베이스 관련 코드들을 작성하기 위함이었습니다.
- 그런데 해당 모듈이 Main 모듈을 알게 되는 순간, UI 로직들도 알게 됩니다. 만약, MainActivity가 NewActivity로 바뀌게 된다면 Firebase 모듈도 같이 수정해줘야 하는 위험이 발생하는 것입니다.
- 순환참조의 위험성
- Firebase가 Main을 참조하고 있는 상황에서, 파이어베이스 기능을 활용하기 위해 Firebase 모듈에 있는 로직을 이용한다면 의존성 사이클이 생기게 되는 것입니다.
- 그 예시로 아래에서도 언급을 할 것이지만, 파이어베이스에서 제공하는 Service 기능을 활용하기 위해서는 메니페스트에 해당 클래스를 등록해줘야 합니다. 따라서 순환참조가 발생할 수밖에 없는 구조가 되는 것입니다.
따라서 Navigator 모듈에 MainActivity 호출용 인터페이스를 두고, 구현체는 Main 모듈에서 DI로 제공한 뒤 Firebase 모듈이 그 인터페이스를 주입받아 MainActivity를 실행하도록 구조를 재설계했습니다.
위 노트는 실제로 제가 안드 팀원들에게 생각한 구조를 설명하기 위해 사용한 사진입니다.
코드를 통해 한 번 더 설명해보겠습니다.
// Navigator 모듈
interface NavigatorProvider {
fun getMainActivityIntent(
deeplink: String?,
): Intent
}
딥링크를 매개변수로 받는 인터페이스를 Navigator 모듈에 작성해줍니다.
Firebase 모듈에서는 해당 인터페이스를 통해 생성된 딥링크를 보낼 수 있게 되는 것이죠.
// Main 모듈
class NavigatorProviderIntent @Inject constructor(
@ApplicationContext private val context: Context,
) : NavigatorProvider {
override fun getMainActivityIntent(deeplink: String?): Intent =
MainActivity.getIntent(context = context)
}
위 인터페이스의 구현체 부분입니다.
// Main 모듈 - DI 부분
@Module
@InstallIn(SingletonComponent::class)
interface NavigatorModule {
@Binds
@Singleton
fun bindNavigatorIntent(navigatorProviderIntent: NavigatorProviderIntent): NavigatorProvider
}
해당 인터페이스를 주입해주기 위한 모듈입니다.
최종 의존성 그래프는 위와 같습니다.
제가 구상한 대로
- Main 모듈이 Firebase 모듈을 참조해 파이어베이스 기능을 이용하고 있고,
- Firebase 모듈이 Navigator 모듈을 참조하여 생성한 딥링크만 전달하도록 구현했습니다.
안드로이드 4대 컴포넌트
딥링크 사용방법에 대해 알아보기 전에 안드로이드 4대 컴포넌트에 대해 먼저 짚고 넘어가겠습니다.
Activity, Service, Broadcast Receiver, Content Provider
이것이 바로 안드로이드의 4대 컴포넌트입니다.
이들이 이렇게 불리는 이유는 '안드로이드 앱을 들어올 수 있는 진입점'이기 때문입니다.
즉, 앱을 열 수 있는 방법이 총 4가지인 것입니다.
- Acitivty는 많은 분들이 아시는 바로 그 액티비티입니다. 터닝에서도 MainActivity를 통해 앱을 시작하게 됩니다.
- Service는 앱이 꺼져있는 상태에서도 백그라운드를 통해 처리를 할 수 있습니다.
- 바로 알림과 카카오톡 공유가 이 Service를 이용해 앱을 열게 되는 것입니다.
- 그러면 이러한 알림과 카카오톡 공유의 딥링크를 통해 앱을 열기 위해서는 메니페스트에 등록하여 알려주어야 합니다.
<!-- 메니페스트 알림 -->
<service
android:name="com.terning.core.firebase.messageservice.TerningMessagingService"
android:exported="false">
<intent-filter>
<action android:name="com.google.firebase.MESSAGING_EVENT" />
</intent-filter>
</service>
<!-- 메니페스트 카카오톡 공유 -->
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:scheme="kakao${NATIVE_APP_KEY}"
android:host="kakaolink" />
</intent-filter>
딥링크 사용 방법
마지막으로, 딥링크를 사용하는 방법에 대해 알려드리도록 하겠습니다.
딥링크의 형식은 다음과 같이 scheme과 host로 이루어져 있습니다.
{scheme}://{host}
터닝은 각 케이스에 대해 다음과 같이 분류하였습니다.
- 알림
- scheme: 터닝에서 고유하게 사용할 terning
- host: 처음 진입하는 화면에 대응되도록 함 (home, search, calendar)
- 카카오톡 공유
- scheme: 카카오 디벨로퍼스 계정의 native app key
- host: kakaolink로 고정
그럼 이렇게 두 가지 방식의 scheme과 host 등록이 가능한지 의문이 들 수 있는데요!
정답은 가능합니다:)
메니페스트에 딥링크 형식을 제대로 등록만 해준다면 알맞은 형식을 찾아 앱을 실행해주기 때문입니다!
fun NavGraphBuilder.splashNavGraph() {
composable<Splash>(
deepLinks = listOf(
navDeepLink<Splash>(
basePath = "terning://splash"
)
)
) {
SplashRoute()
}
}
해당 딥링크는 각 화면의 NavGraphBuilder에 정의된 composable 확장함수 안에 작성해주면, 해당 화면에 대한 딥링크 등록은 완료됩니다!
만약 딥링크가 아래와 같이 쿼리 파라미터 형식으로 전달되는 경우에는 MainActivity에서 해당 데이터를 파싱하여 대응해주면 됩니다:)
"terning://splash?redirect=home"
💡한 가지 더 알게된 점을 공유해보자면, 터미널에서 딥링크를 실행하기 위해서는 아래처럼 명령어를 입력하게 되는데요
adb shell am start -W -a android.intent.action.VIEW -d "terning://{딥링크 작성}" com.terning.point.debug
쉘에서는 &가 다른 의미로 인식이 되어 &를 넣고 싶으면 \&로 넣어주어야 합니다!!
마치며
이렇게 길었던 딥링크 구조 도입기가 끝났습니다!
딥링크를 구현하면서 안드로이드의 전반적인 구조에 대해 공부를 할 수 있었던 정말 뜻깊은 시간이었던 것 같아요:)
전체 코드가 궁금하시다면 터닝 안드로이드 레포지토리 구경하고 가세요! ㅎㅎ
GitHub - teamterning/Terning-Android: 💚 지금이 안드의 터닝포인트~ 💚
💚 지금이 안드의 터닝포인트~ 💚. Contribute to teamterning/Terning-Android development by creating an account on GitHub.
github.com
'Android' 카테고리의 다른 글
특명! 업데이트를 사용자에게 신속하게 전달하라! (2) | 2025.06.28 |
---|