[ 안드로이드 ] 앱 내에 개발자 모드 추가하기
개발자 모드가 필요한 이유 개발자 모드를 통해서 서버 모드(상용, QA, Test)를 앱 내에서 변경할 수도 있고, 에러 발생 시 로그를 기록해줄 수도 있습니다. 또한 토큰 정보나 디바이스 정보 등과
dongx2.tistory.com
Intro
이전 글에서 개발자 모드를 추가하는 방법에 대해서 간단히 작성해보았습니다.
아직 개발자 모드를 추가하지 않았다면 이전 글을 먼저 보고 와주시면 도움이 될 것 같습니다.
이번 게시글에선 개발자 모드 내에 각종 정보들을 포함하는 방법에 대해 작성해보고자 합니다.
해당 글을 참고하여 각자의 프로젝트에 맞게 응용 해보시면 좋을 것 같습니다.
이 글은 헤이딜러의 Ted Park님의 게시글을 보고 작성하게 되었습니다.
해당 내용을 기반으로 응용한 내용들을 작성한 것이니 같이 읽어보시는 것도 좋을 것 같습니다.
앱안의 [개발자 모드]로 기획자/QA팀 과 편하게 일하기
앱 안에 [개발자 모드]를 만들어서 앱안의 여러 정보를 바로 확인하고 제어할 수 있습니다
medium.com
1. JWT 토큰 추가하기
개발자 모드를 추가하게 된 가장 큰 이유이기도 합니다.
서버 담당자 분들이 토큰을 요청했을 때 기존엔 직접 빌드를 해서 추출 했었기에, 안드 팀원들이 밖에 있으면 서버 분들은 무작정 기다릴 수 밖에 없었습니다.
이런 과정들이 너무 비효율적이라고 느껴졌고, 앱만 설치 되어있다면 바로 토큰을 전달할 수 있도록 개발자 모드를 추가하기로 다짐하였습니다.
구현 방법
토큰을 개발자 모드에 추가하는 것은 크게 어렵지 않았습니다.
저희 프로젝트에선 토큰을 Preferences에 저장하고 있었는데요, 이걸 간단히 출력해주기만 하면 끝이었습니다.
private fun initUserInfo() {
val ctx: Context = context ?: return
val accessToken = PreferenceManager.getString(ctx, TokenAuthenticator.TOKEN_KEY_ACCESS) ?: ""
val refreshToken = PreferenceManager.getString(ctx, TokenAuthenticator.TOKEN_KEY_REFRESH) ?: ""
setPreferenceSummary("dev_pref_key_access_token", accessToken)
setPreferenceSummary("dev_pref_key_refresh_token", refreshToken)
}
실행 결과
이렇게 토큰이 바로 보여지게 되고, 클릭하면 바로 클립보드에 복사되게 됩니다.
이로 인해 밖에 있어 노트북을 쓸 수 없는 상황이더라도 서버 분들에게 토큰을 바로 전달 할 수 있게 되었습니다.

2. 디바이스 정보 추가하기
현재 진행 중인 사이드 프로젝트에 QA 팀은 없지만 각자가 QA를 한다는 마음으로 임하고 있습니다.
저 같은 경우에는 여러 버전의 디바이스에서 테스트를 해보는 편인데, 이슈가 생겼을 때 버전과 모델명을 기록해두면 원인 파악에 조금 더 도움이 되는 것 같았습니다.
특히, 특정 디바이스에서만 발생하는 이슈를 파악할 때 가장 유용한 것 같아요
디바이스 정보를 개발자 모드에 추가 해두면 좋겠다 싶었고, 정말 간단하게 추가 할 수 있어서 필요 없더라도 추가해두는 것을 추천드립니다.
구현 방법
디바이스 정보를 추가하는 방법은 정말 간단합니다.
안드로이드 버전 : Build.VERSION.RELEASE
모델 명 : Build.BRAND, Build.MODEL
SDK 버전 : Build.VERSION.SDK_INT
private fun initDeviceInfo() {
setPreferenceSummary("dev_pref_android_version", Build.VERSION.RELEASE)
setPreferenceSummary("dev_pref_model_name", "${Build.BRAND} ${Build.MODEL}")
setPreferenceSummary("dev_pref_sdk_version", "${Build.VERSION.SDK_INT}")
}
실행 결과
코드 3줄만으로 디바이스 정보가 추가 되었습니다.
모델 명 말고 실제 기기 이름 (ex. Galaxy S22+)와 같이 출력하는 것도 봤는데, 이 부분은 어떻게 하는 지 조금 더 찾아봐야 할 것 같습니다,
(찾으면 글 수정하러 올게요)
3. 화면 정보 추가하기
저는 UI를 구현하게 되면 필수로 여러 해상도의 기기에서 테스를 해보는 편입니다.
안드로이드는 특히 폴드와 같은 이상한 해상도를 가진 휴대폰이 많기 때문에.. 웬만한 기기에서는 잘 나와도 폴드에서는 깨지는 경우가 많았습니다.
폴드 뿐만 아니라 구현을 어떻게 하느냐에 따라 화면 크기 별로 문제가 되는 경우가 많더라구요
그럴 때 기기의 화면 정보를 알아두면 UI 구현에 있어 조금 더 신경 쓸 수 있는 부분이 늘어나는 것 같았습니다.
구현 방법
화면 정보를 추가하는 것도 displayMetrics를 이용하면 어렵지 않게 추가할 수 있습니다.
단, 여기서 주의해야 할 점이 있습니다.
metrics에서 hegihtPixels을 구하면 상단바와 네비게이션 바의 높이는 제외된 화면의 높이가 측정 됩니다.
따라서 상단바와 네비게이션 바의 높이는 별도로 구하여 더해주어야 합니다.
화면 정보 코드
private fun initDisplayInfo() {
val metrics = activity?.resources?.displayMetrics ?: return
val windowManager = activity?.windowManager ?: return
val statusBarHeight = getStatusBarHeight(windowManager)
val naviBarHeight = getNaviBarHeight(windowManager)
with(metrics) {
setPreferenceSummary("dev_pref_display_ratio", "$widthPixels x ${heightPixels + statusBarHeight + naviBarHeight}")
setPreferenceSummary("dev_pref_display_density", "${densityDpi}dp")
setPreferenceSummary("dev_pref_display_resource_bucket", getDeviceResourseBucket(this))
}
}
글이 길어질 것 같으니 상단바와 네비게이션 바 높이 구하는 코드는 접어 놓도록 하겠습니다.
상단바 높이 구하기
private fun getStatusBarHeight(windowManager: WindowManager): Int {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
val windowMetrics = windowManager.currentWindowMetrics
val insets = windowMetrics.windowInsets.getInsetsIgnoringVisibility(WindowInsets.Type.statusBars())
insets.top
} else {
val context = context ?: return 0
val resourceId = context.resources.getIdentifier("status_bar_height", "dimen", "android")
if (resourceId > 0) {
context.resources.getDimensionPixelSize(resourceId)
} else {
0
}
}
}
네비게이션 바 높이 구하기
private fun getNaviBarHeight(windowManager: WindowManager): Int {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
val windowMetrics = windowManager.currentWindowMetrics
val insets = windowMetrics.windowInsets.getInsetsIgnoringVisibility(WindowInsets.Type.navigationBars())
insets.bottom
} else {
val context = context ?: return 0
val resourceId = context.resources.getIdentifier("navigation_bar_height", "dimen", "android")
if (resourceId > 0) {
context.resources.getDimensionPixelSize(resourceId)
} else {
0
}
}
}
실행 결과
4. 서버 모드 변경
이번에 추가한 개발자 모드에서 가장 핵심적인 기능 중 하나입니다.
현재 진행하는 사이드 프로젝트에는 3가지 종류의 서버가 운영되고 있습니다.
1. 상용 서버
2. 테스트 서버
3. 새로운(?) 상용 서버
상용 서버가 NodeJs로 되어 있는데, Spring Boot로 바꾼 후 이걸 상용 서버로 쓸 예정입니다.
그래서 현재는 3개의 서버가 운영되고 있는데요
기존에는 서버를 바꿀 때마다 local.properties를 직접 수정하여 재빌드를 해야 했습니다.
이런 부분도 너무 비효율적이라는 생각이 들었고, 서버 모드를 바꿀 수 있는 기능도 추가하게 되었습니다.
구현 방법
- 서버 모드를 Enum으로 정의 합니다.
- ListPreference에 서버 모드를 추가합니다.
- 서버를 선택하면 Preferences에 저장하고 앱을 종료(+ 로그아웃)합니다.
- 서버가 바뀌게 되므로 재로그인을 하도록 해주는게 좋습니다.
- 현재 선택된 서버의 주소를 Retrofit에 할당해줍니다.
서버 모드 Enum 정의
먼저 서버 모드들을 Enum으로 정의해줍니다.
해당 Enum 내에 현재 선택된 서버 모드를 가져오는 메소드도 추가해줍니다.
추후 BaseUrl을 지정할 때 사용하게 됩니다.
enum class ApiMode {
NODE,
TEST,
JAVA;
companion object {
private fun asValue(mode: String): ApiMode = when(mode.uppercase()) {
"NODE" -> NODE
"JAVA" -> JAVA
else -> TEST
}
fun getCurrentApiMode(context: Context): ApiMode {
return asValue(
PreferenceManager.getString(context, ApplicationClass.API_MODE) ?: ""
)
}
}
}
ListPreference 세팅
리스트 메뉴로 서버 모드를 추가하는 코드입니다.
서버 선택시 선택한 서버를 저장하고 앱 종료와 동시에 로그아웃 시킵니다.
private fun initApiMode() {
val ctx:Context = context ?: ApplicationClass.appContext
val currentApi = ApiMode.getCurrentApiMode(ctx)
findPreference<ListPreference>("dev_pref_key_api_mode")?.apply {
val entries = ApiMode.values().map { it.name }.toTypedArray()
val selectIndex = ApiMode.values().indexOf(currentApi)
this.entries = entries
this.entryValues = entries
title = currentApi.name
setValueIndex(selectIndex)
setOnPreferenceChangeListener { preference, newValue ->
val selectItem = newValue.toString()
this.title = selectItem
PreferenceManager.apply {
setString(ctx, ApplicationClass.API_MODE, selectItem)
setString(ctx, MySettingAccountInfoFragment.TOKEN_KEY_ACCESS, "none")
setString(ctx, MySettingAccountInfoFragment.TOKEN_KEY_REFRESH, "none")
}
destroyApp(ctx)
true
}
}
}
BaseUrl 세팅
선택한 서버에 따라 BaseUrl을 다르게 반환해주어야 합니다.
각 서버의 Url들은 BuildConfig를 통해서 관리하고 있습니다.
선택한 서버에 맞게 Url을 반환해주도록 구현하였고, 릴리즈 모드이거나 context가 초기화 되어있지 않은 경우 상용 서버를 반환하도록 해주었습니다.
private fun initApiMode() {
val currentApi = ApiMode.getCurrentApiMode(appContext)
PreferenceManager.setString(appContext, API_MODE, currentApi.name)
}
companion object {
lateinit var appContext: Context
const val API_MODE = "API_MODE"
fun getBaseUrl(): String {
return when {
!BuildConfig.DEBUG || !::appContext.isInitialized -> {
BuildConfig.RUNNECT_NODE_URL // 추후 Prod 서버로 변경
}
else -> {
val mode = ApiMode.getCurrentApiMode(appContext)
when(mode) {
ApiMode.JAVA -> BuildConfig.RUNNECT_PROD_URL
ApiMode.TEST -> BuildConfig.RUNNECT_DEV_URL
else -> BuildConfig.RUNNECT_NODE_URL
}
}
}
}
}
Retrofit에 BaseUrl 할당
마지막으로 Retrofit을 초기화할 때 getBaseUrl() 메소드를 이용해주면 됩니다.
이렇게 되면 선택한 서버 모드에 맞는 Url로 Api 통신이 이루어지게 됩니다.
@OptIn(ExperimentalSerializationApi::class, InternalCoroutinesApi::class)
@Provides
@Singleton
@Runnect
fun provideRunnectRetrofit(json: Json, client: OkHttpClient): Retrofit {
kotlinx.coroutines.internal.synchronized(this) {
val baseUrl = ApplicationClass.getBaseUrl()
val retrofit = Retrofit.Builder().baseUrl(baseUrl).client(client)
.addConverterFactory(json.asConverterFactory("application/json".toMediaType()))
.build()
return retrofit ?: throw RuntimeException("Retrofit creation failed.")
}
}
실행 결과
이제 원하는 서버를 선택하고 앱이 재실행 되면 앱은 해당 서버를 바라보게 됩니다.

마치며
이번 게시글에선 개발자 모드에 여러 기능을 추가하는 방법에 대해서 작성해보았습니다.
에러 로그를 기록하거나 서버 주소를 동적으로 설정하는 등 기능을 더 추가하고 개선하고 싶지만 아직까지는 오버스펙이라는 느낌이 없지 않아 있네요
이러한 기능들이 필요하다고 느껴질 때 추가해도 늦지 않을 것 같습니다.
메모용으로 글을 작성하긴 했는데 해당 내용을 활용해서 더 좋은 기능을 추가 해보는 것도 좋을 것 같습니다.
'안드로이드 > 이론' 카테고리의 다른 글
[ 안드로이드 ] 액티비티 배경 투명하게 설정 (+ 투명 배경 유지 안되는 이슈 해결) (0) | 2024.03.25 |
---|---|
[ 안드로이드 ] Multi ViewType RecyclerView ViewHolder 순서 고정하기 (0) | 2024.01.14 |
[ 안드로이드 ] 개발자의 실수를 줄여주는 어노테이션 (0) | 2023.12.12 |
[ 안드로이드 ] 앱 내에 개발자 모드 추가하기 (0) | 2023.12.08 |
[ 안드로이드 ] style.xml을 이용하여 공통 속성 정의하기 (0) | 2023.10.10 |