<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Sewon Ann</title>
    <description>The latest articles on DEV Community by Sewon Ann (@kingori).</description>
    <link>https://dev.to/kingori</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F312595%2Fe42b73dc-c619-46ee-b083-aa4873a81960.png</url>
      <title>DEV Community: Sewon Ann</title>
      <link>https://dev.to/kingori</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/kingori"/>
    <language>en</language>
    <item>
      <title>KMP에서 Google 계정으로 Firebase 로그인 구현하기</title>
      <dc:creator>Sewon Ann</dc:creator>
      <pubDate>Sun, 25 Jan 2026 11:20:34 +0000</pubDate>
      <link>https://dev.to/kingori/kmpeseo-google-gyejeongeuro-firebase-rogeuin-guhyeonhagi-138</link>
      <guid>https://dev.to/kingori/kmpeseo-google-gyejeongeuro-firebase-rogeuin-guhyeonhagi-138</guid>
      <description>&lt;p&gt;Kotlin Multiplatform 프로젝트에서 Android/iOS/JVM/JS 네 가지 플랫폼 모두 Google 계정으로 Firebase 로그인을 구현했다. 그 과정에서 겪은 문제들과 해결 방법을 정리한다.&lt;/p&gt;

&lt;h2&gt;
  
  
  배경
&lt;/h2&gt;

&lt;p&gt;Firebase는 js/iOS/Android SDK를 제공한다. 아직 KMP 를 지원하진 않아, &lt;a href="https://github.com/GitLiveApp/firebase-kotlin-sdk" rel="noopener noreferrer"&gt;GitLive Firebase SDK&lt;/a&gt;가 KMP 지원을 목적으로 개발되었다. 하지만 지원 기능이 아직 완전하지 않다. 특히 JVM 플랫폼에서는 인증 API가 제한적으로 구현되어 있어 Firebase REST API를 직접 호출해야 하는 등의 제약이 있다.&lt;/p&gt;

&lt;p&gt;각 플랫폼에서 Google 로그인을 구현한 방식은 다음과 같다:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Android&lt;/strong&gt;: Credential Manager API로 Google ID Token 획득 → GitLive SDK로 Firebase 인증&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;iOS&lt;/strong&gt;: Swift Google SDK로 Google ID Token 획득 → GitLive SDK로 Firebase 인증&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;JS&lt;/strong&gt;: Firebase Web SDK의 &lt;code&gt;signInWithPopup&lt;/code&gt;으로 한 번에 처리&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;JVM&lt;/strong&gt;: OAuth 2.0 REST API로 Google ID Token 획득 → Firebase REST API로 Firebase 인증&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  개념
&lt;/h2&gt;

&lt;h3&gt;
  
  
  인증 흐름
&lt;/h3&gt;

&lt;p&gt;Google 계정으로 Firebase 로그인하는 전체 흐름은 다음과 같다:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1. 사용자가 "Google로 로그인" 버튼 클릭
   ↓
2. 플랫폼별 Google 인증 수행
   - Android/iOS/JVM: Google ID Token 획득
   - JS: Firebase Web SDK로 Google 로그인 및 Firebase 인증 완료
   ↓
3. 인증 결과 반환
   - Android/iOS/JVM: GoogleSignInResult.Credential (idToken)
   - JS: GoogleSignInResult.SignedInUser (이미 로그인된 사용자 정보)
   ↓
4. Firebase 인증 처리
   - Android/iOS/JVM: GitLive SDK로 Firebase 인증 (signInWithCredential)
   - JS: 이미 완료됨 (signInWithPopup 내부에서 처리됨)
   ↓
5. 로컬 저장소에 세션 정보 생성
   ↓
6. UI에서 사용자 정보 표시
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  토큰
&lt;/h3&gt;

&lt;p&gt;인증 과정에서 사용하는 Google ID 토큰과 Firebase ID 토큰의 차이는 다음과 같다:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;특징&lt;/th&gt;
&lt;th&gt;Google ID Token&lt;/th&gt;
&lt;th&gt;Firebase ID Token&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;발급자&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Google OAuth 2.0&lt;/td&gt;
&lt;td&gt;Firebase Authentication&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;용도&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Firebase에 사용자 신원 증명&lt;/td&gt;
&lt;td&gt;Firebase 백엔드와의 통신&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;형식&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;JWT (Google 서명)&lt;/td&gt;
&lt;td&gt;JWT (Firebase 서명)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  플랫폼별 차이
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;플랫폼&lt;/th&gt;
&lt;th&gt;Google 인증 방법&lt;/th&gt;
&lt;th&gt;Firebase 인증 방법&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Android&lt;/td&gt;
&lt;td&gt;Credential Manager API&lt;/td&gt;
&lt;td&gt;GitLive SDK (signInWithCredential)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;iOS&lt;/td&gt;
&lt;td&gt;Swift Google SDK&lt;/td&gt;
&lt;td&gt;GitLive SDK (signInWithCredential)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Web&lt;/td&gt;
&lt;td&gt;Firebase Web SDK&lt;/td&gt;
&lt;td&gt;signInWithPopup 내부&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;JVM&lt;/td&gt;
&lt;td&gt;OAuth 2.0 REST API&lt;/td&gt;
&lt;td&gt;Firebase REST API + FirebasePlatform&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;토큰 관리&lt;/strong&gt;: Android/iOS/JS에서는 GitLive SDK가 토큰을 자동으로 관리한다. JVM에서는 GitLive SDK의 제약으로 FirebasePlatform에 직접 토큰을 설정해야 한다.&lt;/p&gt;

&lt;h2&gt;
  
  
  설정
&lt;/h2&gt;

&lt;h3&gt;
  
  
  공통
&lt;/h3&gt;

&lt;p&gt;모든 플랫폼에서 GitLive SDK를 사용한다:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="nf"&gt;implementation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"dev.gitlive:firebase-auth:1.12.0"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  플랫폼별 의존성
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Android:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="nf"&gt;implementation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"androidx.credentials:credentials:1.2.2"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;implementation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"androidx.credentials:credentials-play-services-auth:1.2.2"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;implementation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"com.google.android.libraries.identity.googleid:googleid:1.1.0"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;iOS (SPM):&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Firebase iOS SDK&lt;/li&gt;
&lt;li&gt;Google Sign-In SDK&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;JVM:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="nf"&gt;implementation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"io.ktor:ktor-client-core:2.3.7"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;implementation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"io.ktor:ktor-client-cio:2.3.7"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;implementation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"io.ktor:ktor-client-content-negotiation:2.3.7"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;implementation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.2"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  구현
&lt;/h2&gt;

&lt;p&gt;각 플랫폼에서 Google ID Token을 획득하는 방법이 다르므로, &lt;code&gt;GoogleAuthenticator&lt;/code&gt; 인터페이스로 추상화했다.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="kd"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;GoogleAuthenticator&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;suspend&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;signInWithGoogle&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nc"&gt;Result&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;GoogleSignInResult&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;sealed&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;GoogleSignInResult&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;data class&lt;/span&gt; &lt;span class="nc"&gt;Credential&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;idToken&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;accessToken&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;?)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;GoogleSignInResult&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="kd"&gt;data class&lt;/span&gt; &lt;span class="nc"&gt;SignedInUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;user&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;User&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;GoogleSignInResult&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Android
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Google ID Token 획득 (Credential Manager API):&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;googleIdOption&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;GetGoogleIdOption&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Builder&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setServerClientId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;YOUR_WEB_CLIENT_ID&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setFilterByAuthorizedAccounts&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;false&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;build&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;result&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;credentialManager&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getCredential&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;activity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;idToken&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;GoogleIdTokenCredential&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createFrom&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;credential&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;idToken&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Firebase 인증:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;credential&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;FirebaseGoogleAuthProvider&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;credential&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;idToken&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nc"&gt;FirebaseAuth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;signInWithCredential&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;credential&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  iOS
&lt;/h3&gt;

&lt;p&gt;Swift와 Kotlin 간 통신을 위해 &lt;code&gt;GoogleSignInProvider&lt;/code&gt; 인터페이스를 정의했다. Swift가 이 인터페이스를 구현하고 Kotlin에서 호출하는 방식이다.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Kotlin 인터페이스 정의:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Swift가 구현할 인터페이스&lt;/span&gt;
&lt;span class="kd"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;GoogleSignInProvider&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;getGoogleCredential&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;onSuccess&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;idToken&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;accessToken&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;?)&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;Unit&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;onFailure&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;Unit&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Swift 구현체를 저장할 변수&lt;/span&gt;
&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="py"&gt;googleSignInProvider&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;GoogleSignInProvider&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;

&lt;span class="c1"&gt;// Swift에서 호출하여 provider를 등록&lt;/span&gt;
&lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;setGoogleSignInProvider&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;GoogleSignInProvider&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;googleSignInProvider&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;provider&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// 등록된 provider 반환&lt;/span&gt;
&lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;getGoogleSignInProvider&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nc"&gt;GoogleSignInProvider&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;googleSignInProvider&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Swift 구현:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="kt"&gt;IOSGoogleSignInProvider&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;NSObject&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;GoogleSignInProvider&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;getGoogleCredential&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nv"&gt;onSuccess&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="p"&gt;?)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="kt"&gt;Void&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nv"&gt;onFailure&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="kt"&gt;Void&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;GIDSignIn&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sharedInstance&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;signIn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;withPresenting&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;topVC&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;error&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt;
            &lt;span class="k"&gt;guard&lt;/span&gt; &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;idToken&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;idToken&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tokenString&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="nf"&gt;onFailure&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Google ID Token 획득 실패"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="k"&gt;return&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="nf"&gt;onSuccess&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;idToken&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Swift 초기화 코드:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="kd"&gt;import&lt;/span&gt; &lt;span class="kt"&gt;Firebase&lt;/span&gt;
&lt;span class="kd"&gt;import&lt;/span&gt; &lt;span class="kt"&gt;GoogleSignIn&lt;/span&gt;

&lt;span class="c1"&gt;// 앱 시작 시 Firebase 초기화&lt;/span&gt;
&lt;span class="kt"&gt;FirebaseApp&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;configure&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="c1"&gt;// GoogleSignInProvider 등록&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;provider&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;IOSGoogleSignInProvider&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="nf"&gt;setGoogleSignInProvider&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Kotlin에서 Swift 코드 호출:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="c1"&gt;// IosGoogleAuthenticator.kt&lt;/span&gt;
&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;IosGoogleAuthenticator&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;GoogleAuthenticator&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="k"&gt;suspend&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;signInWithGoogle&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nc"&gt;Result&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;GoogleSignInResult&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Credential&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;suspendCancellableCoroutine&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;continuation&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt;
            &lt;span class="c1"&gt;// Swift 객체를 호출해서 idToken 획득&lt;/span&gt;
            &lt;span class="nf"&gt;getGoogleSignInProvider&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;getGoogleCredential&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="n"&gt;onSuccess&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;idToken&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;accessToken&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt;
                    &lt;span class="n"&gt;continuation&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;resume&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;success&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;GoogleSignInResult&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Credential&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;idToken&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;accessToken&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
                &lt;span class="p"&gt;},&lt;/span&gt;
                &lt;span class="n"&gt;onFailure&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;error&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt;
                    &lt;span class="n"&gt;continuation&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;resume&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;failure&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Exception&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
                &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Firebase 인증 (GitLive SDK):&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="c1"&gt;// FirebaseAuthRepository.ios.kt&lt;/span&gt;
&lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="k"&gt;suspend&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;signInWithCredential&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;idToken&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;accessToken&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;?,&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nc"&gt;Result&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;User&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;credential&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;FirebaseGoogleAuthProvider&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;credential&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;idToken&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;accessToken&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;authResult&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;FirebaseAuth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;signInWithCredential&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;credential&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;user&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;authResult&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toDomainUser&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="nc"&gt;Result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;success&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Exception&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;Result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;failure&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Web
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Firebase Web SDK의 &lt;code&gt;signInWithPopup&lt;/code&gt;:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;authModule&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;js&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"require('firebase/auth')"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;firebaseAuth&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;dynamic&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;authModule&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getAuth&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;provider&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;js&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"new authModule.GoogleAuthProvider()"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;resultPromise&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="err"&gt;dynamic&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;authModule&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;signInWithPopup&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;firebaseAuth&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;resultPromise&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;onFulfilled&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;dynamic&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt;
        &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;firebaseUser&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;dynamic&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;
        &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;user&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;User&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;uid&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;firebaseUser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;uid&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;email&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;firebaseUser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;displayName&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;firebaseUser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;displayName&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;photoUrl&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;firebaseUser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;photoURL&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;emailVerified&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;firebaseUser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;emailVerified&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nc"&gt;Boolean&lt;/span&gt; &lt;span class="o"&gt;?:&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;provider&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"google.com"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;createdAt&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;lastLoginAt&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="c1"&gt;// 이미 Firebase에 로그인됨&lt;/span&gt;
        &lt;span class="nc"&gt;Result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;success&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;GoogleSignInResult&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;SignedInUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  JVM
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;OAuth 2.0 Authorization Code Flow:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="c1"&gt;// 로컬 콜백 서버 시작&lt;/span&gt;
&lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;port&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;findAvailablePort&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;8080&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;callbackServer&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;OAuthCallbackServer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;// OAuth 2.0 인증 URL 생성 및 브라우저 열기&lt;/span&gt;
&lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;state&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;UUID&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;randomUUID&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;authUrl&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"https://accounts.google.com/o/oauth2/v2/auth?client_id=$CLIENT_ID&amp;amp;redirect_uri=http://127.0.0.1:$port/callback&amp;amp;response_type=code&amp;amp;scope=openid email profile&amp;amp;state=$state"&lt;/span&gt;
&lt;span class="nc"&gt;Desktop&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getDesktop&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;browse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;URI&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;authUrl&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

&lt;span class="c1"&gt;// 콜백 대기 (최대 5분)&lt;/span&gt;
&lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;authCode&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;withTimeoutOrNull&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;300_000L&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;callbackServer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;waitForCallback&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Authorization Code를 ID Token으로 교환&lt;/span&gt;
&lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;response&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;httpClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"https://oauth2.googleapis.com/token"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;parameter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"code"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;authCode&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;parameter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"client_id"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;CLIENT_ID&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;parameter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"redirect_uri"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"http://127.0.0.1:$port/callback"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;parameter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"grant_type"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"authorization_code"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;googleIdToken&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;TokenResponse&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;().&lt;/span&gt;&lt;span class="n"&gt;idToken&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Firebase REST API로 Firebase ID Token 교환:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;response&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;httpClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"https://identitytoolkit.googleapis.com/v1/accounts:signInWithIdToken"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;setBody&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="s"&gt;"""
        {
            "idToken": "$googleIdToken",
            "requestUri": "http://localhost",
            "returnSecureToken": true
        }
        """&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;trimIndent&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;firebaseToken&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;FirebaseTokenResponse&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;FirebasePlatform에 직접 설정:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;userJson&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"""
{
    "isAnonymous": false,
    "uid": "${userInfo.sub}",
    "idToken": "$firebaseToken",
    "email": ${userInfo.email?.let { "\"$it\"" } ?: "null"},
    "photoUrl": ${userInfo.picture?.let { "\"$it\"" } ?: "null"},
    "displayName": ${userInfo.name?.let { "\"$it\"" } ?: "null"}
}
"""&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;trimIndent&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;key&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"com.google.firebase.auth.FIREBASE_USER"&lt;/span&gt;
&lt;span class="n"&gt;platform&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;store&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;userJson&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;주의&lt;/strong&gt;: 이 방법은 GitLive SDK 내부 구현에 의존적이므로 SDK 업데이트 시 동작하지 않을 수 있다.&lt;/p&gt;




&lt;h2&gt;
  
  
  정리
&lt;/h2&gt;

&lt;p&gt;Firebase SDK, GitLive SDK, 각 플랫폼의 Google 인증 SDK를 조합하여 KMP 프로젝트에서 네 가지 플랫폼 모두 Firebase 인증을 구현했다. 하지만 JVM 플랫폼에서는 GitLive SDK의 제약으로 Firebase REST API를 직접 호출하고 FirebasePlatform에 토큰을 설정하는 등의 우회 방법이 필요했다.&lt;/p&gt;

&lt;p&gt;각 플랫폼별로 다른 SDK와 API를 사용하지만, &lt;code&gt;GoogleAuthenticator&lt;/code&gt; 인터페이스로 플랫폼별 구현을 추상화하여 공통 비즈니스 로직과 UI 계층을 Kotlin으로 공유할 수 있었다.&lt;/p&gt;

</description>
      <category>android</category>
      <category>ios</category>
      <category>kotlin</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Implementing Firebase Google Login in Kotlin Multiplatform (KMP)</title>
      <dc:creator>Sewon Ann</dc:creator>
      <pubDate>Sun, 25 Jan 2026 11:15:56 +0000</pubDate>
      <link>https://dev.to/kingori/implementing-firebase-google-login-in-kotlin-multiplatform-kmp-48hd</link>
      <guid>https://dev.to/kingori/implementing-firebase-google-login-in-kotlin-multiplatform-kmp-48hd</guid>
      <description>&lt;p&gt;In this post, I will share my experience implementing Firebase Google Login across four platforms—&lt;strong&gt;Android, iOS, JVM (Desktop), and JS&lt;/strong&gt;—in a Kotlin Multiplatform project, including the challenges I faced and how I overcame them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Background
&lt;/h2&gt;

&lt;p&gt;While Firebase provides native SDKs for JS, iOS, and Android, it does not yet officially support KMP. As a workaround, the community-led &lt;a href="https://github.com/GitLiveApp/firebase-kotlin-sdk" rel="noopener noreferrer"&gt;GitLive Firebase SDK&lt;/a&gt; is widely used for KMP support. However, its feature set is not yet complete. Specifically, the JVM platform has a very minimal implementation for the Auth API, forcing us to manually call the Firebase REST API.&lt;/p&gt;

&lt;p&gt;Here is a high-level look at the implementation strategy for each platform:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Android&lt;/strong&gt;: Acquire Google ID Token via &lt;strong&gt;Credential Manager API&lt;/strong&gt; → Authenticate with Firebase using GitLive SDK.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;iOS&lt;/strong&gt;: Acquire Google ID Token via &lt;strong&gt;Swift Google SDK&lt;/strong&gt; → Authenticate with Firebase using GitLive SDK.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;JS&lt;/strong&gt;: Handle everything at once using the Firebase Web SDK's &lt;code&gt;signInWithPopup&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;JVM&lt;/strong&gt;: Acquire Google ID Token via &lt;strong&gt;OAuth 2.0 REST API&lt;/strong&gt; → Authenticate via &lt;strong&gt;Firebase REST API&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Core Concepts
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Authentication Flow
&lt;/h3&gt;

&lt;p&gt;The overall flow for Firebase Google Login is as follows:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;User clicks "Sign in with Google"&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Perform Platform-Specific Google Auth&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Android/iOS/JVM&lt;/strong&gt;: Retrieve &lt;strong&gt;Google ID Token&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;JS&lt;/strong&gt;: Complete both Google and Firebase Auth using the Firebase Web SDK.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Return Auth Result&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Android/iOS/JVM&lt;/strong&gt;: &lt;code&gt;GoogleSignInResult.Credential&lt;/code&gt; (contains &lt;code&gt;idToken&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;JS&lt;/strong&gt;: &lt;code&gt;GoogleSignInResult.SignedInUser&lt;/code&gt; (contains Firebase user info).&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Firebase Authentication&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Android/iOS/JVM&lt;/strong&gt;: Call &lt;code&gt;signInWithCredential&lt;/code&gt; via GitLive SDK.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;JS&lt;/strong&gt;: Skip (already handled internally by &lt;code&gt;signInWithPopup&lt;/code&gt;).&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Session Creation&lt;/strong&gt;: Store session info in local storage.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;UI Update&lt;/strong&gt;: Display user information.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Tokens: Google vs. Firebase
&lt;/h3&gt;

&lt;p&gt;It is crucial to understand the difference between the two tokens used in this process:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature&lt;/th&gt;
&lt;th&gt;Google ID Token&lt;/th&gt;
&lt;th&gt;Firebase ID Token&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Issuer&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Google OAuth 2.0&lt;/td&gt;
&lt;td&gt;Firebase Authentication&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Purpose&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Proof of identity to Firebase&lt;/td&gt;
&lt;td&gt;Communication with Firebase backend&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Format&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;JWT (Signed by Google)&lt;/td&gt;
&lt;td&gt;JWT (Signed by Firebase)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Platform-Specific Breakdown
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Platform&lt;/th&gt;
&lt;th&gt;Google Auth Method&lt;/th&gt;
&lt;th&gt;Firebase Auth Method&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Android&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Credential Manager API&lt;/td&gt;
&lt;td&gt;GitLive SDK (&lt;code&gt;signInWithCredential&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;iOS&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Swift Google SDK&lt;/td&gt;
&lt;td&gt;GitLive SDK (&lt;code&gt;signInWithCredential&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Web&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Firebase Web SDK&lt;/td&gt;
&lt;td&gt;Internal to &lt;code&gt;signInWithPopup&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;JVM&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;OAuth 2.0 REST API&lt;/td&gt;
&lt;td&gt;Firebase REST API + Manual Injection&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Token Management&lt;/strong&gt;: While GitLive SDK automatically manages tokens on Android, iOS, and JS, the JVM implementation requires manually setting the token in the &lt;code&gt;FirebasePlatform&lt;/code&gt; due to SDK limitations.&lt;/p&gt;




&lt;h2&gt;
  
  
  Configuration
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Common Setup
&lt;/h3&gt;

&lt;p&gt;Add the GitLive SDK dependency to your &lt;code&gt;commonMain&lt;/code&gt; source set:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="nf"&gt;implementation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"dev.gitlive:firebase-auth:1.12.0"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Platform-Specific Dependencies
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Android:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="nf"&gt;implementation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"androidx.credentials:credentials:1.2.2"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;implementation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"androidx.credentials:credentials-play-services-auth:1.2.2"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;implementation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"com.google.android.libraries.identity.googleid:googleid:1.1.0"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;iOS (via Swift Package Manager):&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Firebase iOS SDK&lt;/li&gt;
&lt;li&gt;Google Sign-In SDK&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;JVM:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="nf"&gt;implementation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"io.ktor:ktor-client-core:2.3.7"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;implementation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"io.ktor:ktor-client-cio:2.3.7"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;implementation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"io.ktor:ktor-client-content-negotiation:2.3.7"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;implementation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.2"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Implementation
&lt;/h2&gt;

&lt;p&gt;Since the method for acquiring the Google ID Token varies by platform, I abstracted the logic using a &lt;code&gt;GoogleAuthenticator&lt;/code&gt; interface.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="kd"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;GoogleAuthenticator&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;suspend&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;signInWithGoogle&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nc"&gt;Result&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;GoogleSignInResult&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;sealed&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;GoogleSignInResult&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;data class&lt;/span&gt; &lt;span class="nc"&gt;Credential&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;idToken&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;accessToken&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;?)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;GoogleSignInResult&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="kd"&gt;data class&lt;/span&gt; &lt;span class="nc"&gt;SignedInUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;user&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;User&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;GoogleSignInResult&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Android Implementation
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Acquiring Google ID Token (Credential Manager API):&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;googleIdOption&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;GetGoogleIdOption&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Builder&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setServerClientId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;YOUR_WEB_CLIENT_ID&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setFilterByAuthorizedAccounts&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;false&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;build&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;request&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;GetCredentialRequest&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Builder&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addGoogleIdOption&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;googleIdOption&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;build&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;result&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;credentialManager&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getCredential&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;activity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;idToken&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;GoogleIdTokenCredential&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createFrom&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;credential&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;idToken&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Firebase Authentication:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;credential&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;FirebaseGoogleAuthProvider&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;credential&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;idToken&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nc"&gt;FirebaseAuth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;signInWithCredential&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;credential&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  iOS Implementation (Swift Interop)
&lt;/h3&gt;

&lt;p&gt;For iOS, I defined a &lt;code&gt;GoogleSignInProvider&lt;/code&gt; interface to bridge Swift and Kotlin. Swift implements this interface, and Kotlin calls it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Kotlin Interface Definition:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="kd"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;GoogleSignInProvider&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;getGoogleCredential&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;onSuccess&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;idToken&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;accessToken&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;?)&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;Unit&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;onFailure&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;Unit&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Global reference for the Swift implementation&lt;/span&gt;
&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="py"&gt;googleSignInProvider&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;GoogleSignInProvider&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;

&lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;setGoogleSignInProvider&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;GoogleSignInProvider&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;googleSignInProvider&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;provider&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Swift Implementation:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="kt"&gt;IOSGoogleSignInProvider&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;NSObject&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;GoogleSignInProvider&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;getGoogleCredential&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nv"&gt;onSuccess&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kd"&gt;@escaping&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="p"&gt;?)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="kt"&gt;Void&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nv"&gt;onFailure&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kd"&gt;@escaping&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="kt"&gt;Void&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;GIDSignIn&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sharedInstance&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;signIn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;withPresenting&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;topVC&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;error&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt;
            &lt;span class="k"&gt;guard&lt;/span&gt; &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;idToken&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;idToken&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tokenString&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="nf"&gt;onFailure&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Failed to retrieve Google ID Token"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="k"&gt;return&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="nf"&gt;onSuccess&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;idToken&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Calling Swift from Kotlin:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;IosGoogleAuthenticator&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;GoogleAuthenticator&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="k"&gt;suspend&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;signInWithGoogle&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nc"&gt;Result&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;GoogleSignInResult&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Credential&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;suspendCancellableCoroutine&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;continuation&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt;
            &lt;span class="nf"&gt;getGoogleSignInProvider&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;getGoogleCredential&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="n"&gt;onSuccess&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;idToken&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;accessToken&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt;
                    &lt;span class="n"&gt;continuation&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;resume&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;success&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;GoogleSignInResult&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Credential&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;idToken&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;accessToken&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
                &lt;span class="p"&gt;},&lt;/span&gt;
                &lt;span class="n"&gt;onFailure&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;error&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt;
                    &lt;span class="n"&gt;continuation&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;resume&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;failure&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Exception&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
                &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Web (JS) Implementation
&lt;/h3&gt;

&lt;p&gt;Leveraging the Firebase Web SDK's interoperability via &lt;code&gt;dynamic&lt;/code&gt; types:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;authModule&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;js&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"require('firebase/auth')"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;firebaseAuth&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;dynamic&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;authModule&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getAuth&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;provider&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;js&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"new authModule.GoogleAuthProvider()"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;resultPromise&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="err"&gt;dynamic&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;authModule&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;signInWithPopup&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;firebaseAuth&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;// Handle promise result and map to User domain model...&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  JVM (Desktop) Implementation
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;OAuth 2.0 Authorization Code Flow:&lt;/strong&gt;&lt;br&gt;
This involves starting a local callback server to intercept the auth code from the system browser.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="c1"&gt;// 1. Generate Auth URL and open system browser&lt;/span&gt;
&lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;authUrl&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"https://accounts.google.com/o/oauth2/v2/auth?..."&lt;/span&gt;
&lt;span class="nc"&gt;Desktop&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getDesktop&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;browse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;URI&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;authUrl&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

&lt;span class="c1"&gt;// 2. Wait for callback and exchange code for ID Token via Ktor&lt;/span&gt;
&lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;googleIdToken&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;httpClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"https://oauth2.googleapis.com/token"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="o"&gt;..&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt; &lt;span class="p"&gt;}.&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;TokenResponse&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;().&lt;/span&gt;&lt;span class="n"&gt;idToken&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Direct Injection into FirebasePlatform:&lt;/strong&gt;&lt;br&gt;
Since GitLive's JVM implementation is minimal, we exchange the Google ID Token for a Firebase Token via REST and manually store it in the SDK's internal key.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;key&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"com.google.firebase.auth.FIREBASE_USER"&lt;/span&gt;
&lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;userJson&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"""{ "uid": "$uid", "idToken": "$firebaseToken", ... }"""&lt;/span&gt;
&lt;span class="n"&gt;platform&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;store&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;userJson&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;⚠️ Warning&lt;/strong&gt;: This approach relies on the internal implementation of the GitLive SDK and may break with future updates.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;By combining the Firebase SDK, GitLive SDK, and platform-specific Google Auth SDKs, I successfully implemented Firebase Authentication for all four platforms in a KMP project. While the JVM platform required hacky workarounds due to SDK limitations, abstracting the logic with the &lt;code&gt;GoogleAuthenticator&lt;/code&gt; interface allowed me to keep the business logic and UI layer clean and shared in Kotlin.&lt;/p&gt;

</description>
      <category>android</category>
      <category>ios</category>
      <category>kotlin</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>KMP 밋업 202512 후기</title>
      <dc:creator>Sewon Ann</dc:creator>
      <pubDate>Mon, 22 Dec 2025 05:42:37 +0000</pubDate>
      <link>https://dev.to/kingori/kmp-miseob-202512-hugi-1fmj</link>
      <guid>https://dev.to/kingori/kmp-miseob-202512-hugi-1fmj</guid>
      <description>&lt;h1&gt;
  
  
  들어가며
&lt;/h1&gt;

&lt;p&gt;일요일(12/21), Kotlin User Group Seoul에서 주최한 &lt;a href="https://www.ticketa.co/events/66" rel="noopener noreferrer"&gt;KMP 밋업&lt;/a&gt;에 다녀왔다. 아들이 생긴 이후 정말 오랜만에 개발자 모임에 나갔다. 그나마 최근 참석했던 모임은 모두 GDG Android 주관 행사였는데, 아는 분이 없는 커뮤니티 행사에 가려니 약간의 설렘과 뻘쭘함이 공존했다.&lt;/p&gt;

&lt;p&gt;육아휴직 기간 동안 최근 KMP + CMP로 간단한 앱을 만들어볼까 싶어 이것저것 실험 중이다. 직접 코드를 짜는 게 귀찮아 대부분 Claude Code에 시키면서, Firebase를 이용한 인증이나 Metro를 이용한 DI 등을 테스트하고 있다. 마침 밋업 주제들이 내가 고민하던 지점들과 딱 맞아서 고민 없이 참석했다.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/Kotlin-User-Groups-Seoul/kotlin-multiplatform-meetup-2025" rel="noopener noreferrer"&gt;전체 발표 자료&lt;/a&gt;도 공유되었으니, KMP / CMP에 관심 있는 분들은 읽어보시면 큰 도움이 될 듯하다. 세션을 들으며 느꼈던 감상을 간단히 정리해본다.&lt;/p&gt;

&lt;h1&gt;
  
  
  세션별 정리
&lt;/h1&gt;

&lt;h2&gt;
  
  
  Compose Multiplatform 외부 의존성 아키텍처 설계부터 운영까지
&lt;/h2&gt;

&lt;p&gt;CMP로 내부 사용자용 앱을 iOS / Android로 배포하며 겪은 경험을 공유해 주셨다. &lt;code&gt;expect&lt;/code&gt; / &lt;code&gt;actual&lt;/code&gt;은 KMP의 언어적 특성인데, 뒤에 나올 Metro 같은 DI를 사용할 경우 인터페이스만 선언하고 각 플랫폼별 구현체를 주입할 수 있다. 생성자도 동일해야 하는 등의 제약이 있어, DI를 쓰지 않을 때나 유용한 기능이 아닐까 하는 생각이 들었다.&lt;/p&gt;

&lt;p&gt;모든 멀티플랫폼 기술은 플랫폼 특화 구현이 등장할 때 이를 얼마나 &lt;strong&gt;덜 고통스럽게&lt;/strong&gt; 처리해 주느냐가 관건이라 생각한다. 이 발표에서는 &lt;strong&gt;Swift에서 Kotlin 코드의 함수를 자연스럽게 호출하여 네이티브 쪽 변경사항을 Kotlin에 반영하는 예시&lt;/strong&gt;를 보여주었다. 또한 Kotlin에서 선언한 인터페이스의 구현체를 Swift로 작성해 제공하는 방법도 인상적이었다.&lt;/p&gt;

&lt;p&gt;Kotlin/Native는 JNI처럼 Java와 Native 세상이 따로 존재하며 중재하는 역할이 아니라, 아예 Kotlin 코드가 Native 코드로 컴파일되어 나온다. 그 덕분에 Swift와 Kotlin 간의 상호 호출이 매우 자연스러운데, 이 점은 볼 때마다 놀랍고 아직 적응이 잘 안 된다.&lt;/p&gt;

&lt;p&gt;다만, 멀티 모듈 상황에서 싱글톤 객체가 Swift와 Kotlin 영역에서 서로 다른 인스턴스로 존재할 수 있는 문제는 주의해야겠다. &lt;a href="https://kotlinlang.org/docs/multiplatform/multiplatform-project-configuration.html#several-shared-modules" rel="noopener noreferrer"&gt;관련 문서&lt;/a&gt;를 확인해 보니 여러 모듈을 묶은 &lt;code&gt;Umbrella Module&lt;/code&gt; 이라는 통합 Kotlin 모듈을 만들어 처리하라고 권장한다. Android로 치면 &lt;code&gt;app&lt;/code&gt; 모듈 아래에 모든 모듈이 모이는 &lt;code&gt;umbrella&lt;/code&gt; 모듈을 하나 더 두는 구조가 될 것 같다.&lt;/p&gt;

&lt;p&gt;Firebase 관련 내용도 언급되었다. 아직 Firebase는 공식 KMP SDK가 없어서 Android/iOS 버전을 따로 써야 한다. &lt;a href="https://github.com/GitLiveApp/firebase-kotlin-sdk" rel="noopener noreferrer"&gt;GitLive의 Firebase SDK&lt;/a&gt;가 대안이지만 모든 기능을 커버하진 못한다. 뒤에 나올 Supabase는 KMP를 잘 지원하는 것과 대조적이다. 개인적인 경험으로도 GitLive 연동은 그리 깔끔하지 않아 고통스러웠다.&lt;/p&gt;

&lt;p&gt;만약 프로젝트를 다시 설계한다면 이렇게 구성해보고 싶다.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;가입 및 사용자 인증 등 세션 기반 기능: &lt;strong&gt;Supabase&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;FCM, Analytics, Crashlytics 등 세션과 무관한 기능: 각 플랫폼의 &lt;strong&gt;Native Firebase SDK&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;발표에서는 CocoaPods 설치도 다뤘는데, 나는 어떻게든 SPM(Swift Package Manager)으로 해결하는 편이 낫다고 본다. CocoaPods는 Android 개발자 입장에서 이해하기 어려운 괴랄한 문제들이 자주 발생하기 때문이다. 또한 회사에서 KMP 도입 시 "CocoaPods를 써야 한다"고 하면 iOS 개발자분들에게 어떤 반응을 얻을지 눈에 선하다. (안드로이드 프로젝트에 갑자기 Java로만 코딩하라는 느낌 아닐까.)&lt;/p&gt;

&lt;h2&gt;
  
  
  KMP와 UIKit으로 iOS 네이티브 앱 만들기
&lt;/h2&gt;

&lt;p&gt;CMP로 앱을 출시해 본 분의 발표였는데 매우 흥미로웠다. KMP로 빌드할 때 Native Code가 만들어지는 컴파일 과정을 상세히 짚어주었고, Xcode 없이 손으로 하나하나 'Hello World' iOS 앱을 만드는 과정을 소개했다.&lt;/p&gt;

&lt;p&gt;발표 내용 중 UIView와 MetalView의 오버레이 옵션이나 Alert 구현 방법 등은 나중에 iOS용 CMP 앱을 만들 때 실무적인 팁이 될 것 같다. 특히 ARC(Automatic Reference Counting)가 CMP 앱 개발에 영향을 끼칠 수 있다는 점은 귀동냥으로만 듣던 지식이 실제 개발에 어떻게 연결되는지 깨닫게 해준 지점이었다. 역시 멀티플랫폼을 잘하려면 양쪽 플랫폼을 깊이 있게 알아야 한다.&lt;/p&gt;

&lt;h2&gt;
  
  
  KMP + Supabase: 1인 개발의 새로운 패러다임
&lt;/h2&gt;

&lt;p&gt;Firebase로 Google 로그인을 구현(Desktop/Android)하며 꽤 고통받았던 기억이 있는데, Supabase의 KMP SDK 발표를 보고 나니 다음엔 무조건 이걸 써야겠다는 생각이 들었다. Claude Code에게 잔소리하며 고생했던 것보다 라이브러리 차원의 지원이 훨씬 간편해 보였다. DB 쪽도 PostgreSQL 기반이라 SQL 사용이 자유롭고 API 생성도 직관적이었다.&lt;/p&gt;

&lt;h2&gt;
  
  
  Metro로 KMP에서 의존성 주입 사용하기
&lt;/h2&gt;

&lt;p&gt;마지막은 새로운 DI 솔루션인 &lt;a href="https://github.com/ZacSweers/metro" rel="noopener noreferrer"&gt;Metro&lt;/a&gt; 소개였다. 현재 토이 프로젝트에 Metro를 쓰고 있어 더 반가웠다. 과거 Dagger의 복잡함에 질려 Koin으로 갈아탔던 터라 '컴파일 기반 DI'에 선입견이 있었는데, Metro는 기억 속의 Dagger보다 훨씬 사용하기 편해 보였다. Anvil 같은 솔루션들의 장점을 잘 흡수한 듯하다.&lt;/p&gt;

&lt;p&gt;발표에서는 뒷단에서 벌어지는 동작 원리도 설명해 주었다. Dagger가 Java 코드를 생성하는 방식이라면, Metro는 Kotlin Compiler Plugin으로 동작하여 IR(Intermediate Representation) 단에서 정보를 활용하므로 생성된 코드가 지저분하지 않고 깔끔하다. 특히 에러 메시지가 친절해졌다는 점이 가장 마음에 든다. Dagger 시절, 빌드 실패를 보면 초콜릿만 &lt;strong&gt;주워 먹으며&lt;/strong&gt; 스트레스를 풀다 배만 나왔던 기억이 난다. 아직 1.0 버전은 아니지만, 토이 프로젝트에는 충분히 도입할 만한 수준이다.&lt;/p&gt;

&lt;h1&gt;
  
  
  마무리
&lt;/h1&gt;

&lt;p&gt;집에서 Claude Code와 씨름하며 CMP를 가지고 놀다가, 같은 고민을 하는 분들의 깊이 있는 이야기를 들으니 즐겁고 자극이 되었다. &lt;/p&gt;

&lt;p&gt;KMP가 앞으로 얼마나 더 잘 안착할지는 지켜봐야겠지만, 현재로선 Android와 Desktop 조합은 매우 깔끔해 보이고 iOS 쪽은 여전히 학습 곡선이 있어 보인다. UI를 제외한 로직 공유(KMP)라면 도입 가능성이 훨씬 높지 않을까 싶다. 이번 기회에 Kotlin User Group Seoul에도 가입했으니 앞으로 활발히 활동해봐야겠다. 멋진 행사를 준비해 주신 운영진과 발표자분들께 감사드린다.&lt;/p&gt;

</description>
      <category>community</category>
      <category>kotlin</category>
      <category>mobile</category>
    </item>
    <item>
      <title>안드로이드 개발자가 빠르게 적용할 수 있는 Flutter 프로젝트 구성</title>
      <dc:creator>Sewon Ann</dc:creator>
      <pubDate>Sun, 02 Nov 2025 14:07:29 +0000</pubDate>
      <link>https://dev.to/kingori/andeuroideu-gaebaljaga-bbareuge-jeogyonghal-su-issneun-flutter-peurojegteu-guseong-1n83</link>
      <guid>https://dev.to/kingori/andeuroideu-gaebaljaga-bbareuge-jeogyonghal-su-issneun-flutter-peurojegteu-guseong-1n83</guid>
      <description>&lt;h2&gt;
  
  
  시작
&lt;/h2&gt;

&lt;p&gt;나는 10년 넘게 안드로이드 앱만 개발해왔는데 Flutter 로 간단한 앱을 만들 일이 생겨 Flutter 작업을 처음 해 봤다. 작업을 시작할 때 안드로이드 개발과 익숙하지만, 그렇다고 Flutter의 관행을 벗어나지 않는 구성을 고민을 했었고, 어느정도 내 목표에 맞는 구성을 만들 수 있어 정리해본다.  물론 Flutter 를 진지하게 오래 해 온 분들이 봤을 땐 구식이거나 더 나은 대안이 있을 수 있겠으나, 안드로이드에 익숙한 개발자가 익숙한 방식으로 후딱 Flutter 에 뛰어들기엔 괜찮은 접근이 아닐까 싶다.&lt;/p&gt;

&lt;h2&gt;
  
  
  기술 선택
&lt;/h2&gt;

&lt;p&gt;안드로이드 프로젝트도 구성이 굉장히 다양한데, 내가 염두에 둔 안드로이드 프로젝트 구성은 다음과 같다.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;DI - hilt 까진 안가더라도 Koin 정도는 될, 하여간 DI 는 있어야 한다. Koin 이 DI 가 맞냐, service locator 냐 하는 논쟁이 있는데, 하여간 필요한 컴포넌트를 주입받을 수 있는 도구가 필요하다. 싱글턴 이런거 말구.&lt;/li&gt;
&lt;li&gt;Retrofit - 다양한 API 호출을 해야 하는데, 일일이 http 호출 함수를 직접 호출하지 않고 인터페이스 등으로 호출 스펙을 정의해서 사용하고 싶다.&lt;/li&gt;
&lt;li&gt;JSON &amp;lt;-&amp;gt; 객체 매퍼 : Kotlin Serialization 과 같은, JSON 과 객체 간 상호 매핑 도구를 이용한다.&lt;/li&gt;
&lt;li&gt;UI/로직 분리 + Single Ui State : UI 와 로직을 분리하고, 로직에서 단일 Ui State 를 View 쪽에 전달하면 View 는 이걸 가지고 그리는 역할만 한다. 이 때 안드로이드에서 익숙한 MVVM 이면 더 좋겠지만, 뭐가 되었건 상관없다.&lt;/li&gt;
&lt;li&gt;러닝 커브가 깊은 기술 배제(당장 대충 공부해서 제품 만들어야 함) / all-in-one 기술 배제 (대부분 커스터마이징해야 하는데 쉽지 않음)&lt;/li&gt;
&lt;li&gt;기왕이면 copilot 등 AI 에게 일 시키기 좋은 기술 선택 - 남들이 많이 쓰고, 변경의 범위가 좁아야 일 시키기가 좋다. 이런 측면에서 손이 좀 더 가더라도 변경이 직관적인 기술이 좋은데 (손은 내가 안쓰고 AI가 한다...), 복잡한 기술은 변경이 눈에 잘 안보여서 무식하지만 간단한 기술이 낫다.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;위 기준으로 내가 선택한 기술은 다음과 같다.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://pub.dev/packages/dio" rel="noopener noreferrer"&gt;dio&lt;/a&gt; - 안드로이드의 &lt;a href="https://square.github.io/okhttp/" rel="noopener noreferrer"&gt;OkHttp&lt;/a&gt; 를 생각하면 된다. Http 호출 처리를 돕는 도구이다. &lt;/li&gt;
&lt;li&gt;
&lt;a href="https://pub.dev/packages/retrofit" rel="noopener noreferrer"&gt;retrofit&lt;/a&gt; - 이름마저 같은, 안드로이드의 &lt;a href="https://square.github.io/retrofit/" rel="noopener noreferrer"&gt;Retrofit&lt;/a&gt; 역할의 도구이다. Api 스펙을 추상적으로 선언하면 구체적인 Api 호출 코드를 만드는 역할을 한다. 안드로이드 Retofit 이 OkHttp 를 호출 도구로 사용했듯, 플러터 Retrofit 은 dio 를 호출 도구로 사용한다. 안드로이드 Retrofit 은 런타임에 api 를 선언한 interface 클래스를 읽어들여 proxy 기술을 이용해 구체 클래스를 만들어내는 반면, 플러터 retrofit 은 build runner 를 이용해 dart 코드를 생성하는 차이가 있다.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://pub.dev/packages/json_serializable" rel="noopener noreferrer"&gt;json_serializable&lt;/a&gt; - JSON - 객체 매핑을 담당한다. 직접 호출할 일은 거의 없고, 아래 freezed 에 얹어서 사용한다. 매핑 도구들은 몇가지 더 있는데, freezed 와 궁합을 생각해서 이 도구를 선택했다.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://pub.dev/packages/freezed" rel="noopener noreferrer"&gt;freezed&lt;/a&gt; &amp;amp; &lt;a href="https://pub.dev/packages/freezed_annotation" rel="noopener noreferrer"&gt;freezed annotation&lt;/a&gt; - kotlin 의 data clsss 역할, 즉 불변 객체를 만들어주는 역할을 생각하면 된다. json serializable 과 엮으면 json 매핑 코드도 생성해준다.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://pub.dev/packages/go_router" rel="noopener noreferrer"&gt;go_router&lt;/a&gt; &amp;amp; &lt;a href="https://pub.dev/packages/go_router_builder" rel="noopener noreferrer"&gt;go_router builder&lt;/a&gt; - 앱 내 화면 전환을 담당한다. 안드로이드에선 Jetpack Navigation을 쓰거나 직접 Intent 를 날리는 반면, 플러터에선 직접 내비게이션을 관리해야 하기 때문에 go_router 를 써야 한다.  go_router builder 를 함께 사용할 경우, 내비게이션 이동을 위한 함수들이 자동생성되어 인자들을 안정적으로 호출할 수 있다. 참고로 난 지금도 activity 가 분리된 안드로이드 프로젝트라면 왜 복잡하게 Navigation을 쓰는지 모르겠다. &lt;/li&gt;
&lt;li&gt;
&lt;a href="https://pub.dev/packages/flutter_riverpod" rel="noopener noreferrer"&gt;flutter riverpod&lt;/a&gt; &amp;amp; &lt;a href="https://pub.dev/packages/hooks_riverpod" rel="noopener noreferrer"&gt;hooks riverpod&lt;/a&gt; &amp;amp; &lt;a href="https://pub.dev/packages/riverpod_annotation" rel="noopener noreferrer"&gt;riverpod_annotation&lt;/a&gt;  - DI 역할을 하는, 각종 상태 관리 프레임워크는 &lt;a href="https://riverpod.dev/" rel="noopener noreferrer"&gt;riverpod&lt;/a&gt; 를 사용했다. 프레임워크다 보니 앱 구성에 아마도 가장 큰 역할을 미칠텐데, 몇 가지 framework 를 비교하다 riverpod 를 선택했다. 전신인 &lt;a href="https://pub.dev/packages/provider" rel="noopener noreferrer"&gt;provider&lt;/a&gt; 는 너무 구식이고, &lt;a href="https://pub.dev/packages/get" rel="noopener noreferrer"&gt;GetX&lt;/a&gt; 는 이거저거 다 해주는 종합선물세트인데 러닝커브도 그렇고, 나에겐 과했다. 만약 내가 flutter 를 좀 더 진지하게 하겠다면 GetX 를 선택했을 지도 모르겠으나, 가볍게 접근하기엔 너무 복잡하여 결과적으로 rivderpod 를 선택했다. hooks riverpod 는 선택사항이지만, 아래에 소개한 flutter_hooks 와 함께 사용해서 코드량을 크게 줄여줄 수 있다. 공부해야 할 게 좀 늘어나지만, ai 도구들에게 일을 시켜보면 빠르게 감을 잡을 수 있다.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://pub.dev/packages/fpdart" rel="noopener noreferrer"&gt;fpdart&lt;/a&gt; -  dart 에 함수형 프로그래밍의 기분을 느낄 수 있게 해 준다. Either / Option 등 없으면 아쉬운 기능들을 제공해준다.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://pub.dev/packages/easy_localization" rel="noopener noreferrer"&gt;easy_localization&lt;/a&gt; - 다국어 지원을 위한 라이브러리이다. 안드로이드에서 문자열을 xml 리소스로 관리하듯, json 으로 문자열을 관리할 수 있다. 나의 경우엔 구글 시트에 문자열을 선언하고, 이걸 json 으로 내려받아 asset 디렉터리에 만들어주는 스크립트를 만들어서 사용했다.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://pub.dev/packages/flutter_hooks" rel="noopener noreferrer"&gt;flutter_hooks&lt;/a&gt; - React Hooks 에 영감을 받은 프로젝트라는데, 나는 React Hooks 도 모른다. 하지만 flutter 로 UI 작업을 조금만 하다보면 dispose 에서 리소스 해제한다거나 하는 과정 때문에 코드가 금새 더러워지는 걸 볼 수 있다. 이 경우 flutter hooks 를 사용하면 코드를 꽤 간결하게 유지할 수 있다. 뭔가 compose 의 &lt;code&gt;remember&lt;/code&gt; 나 &lt;code&gt;launchedEffect&lt;/code&gt; 를 호출하는 느낌이 든다. 추가로, 조금만 widget 이 복잡해져도 &lt;code&gt;StatelessWidget&lt;/code&gt; 과 쌍을 이루는 &lt;code&gt;StatefulWidget&lt;/code&gt; 를 선언해야 하는데, flutter hooks 를 사용하면 이 부분을 &lt;code&gt;HookWidget&lt;/code&gt; 하나로 해결할 수 있다. 어떤 hook 을 사용해야하는지 러닝커브가 있긴 한데, gemini 에게 물어보면 잘 알려주기 때문에 도움을 많이 받았다.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://pub.dev/packages/flutter_launcher_icons" rel="noopener noreferrer"&gt;flutter_launcher_icons&lt;/a&gt; - 멀티플랫폼이다보니 플랫폼 별 앱 아이콘을 관리하는 것도 일인데, 이 부분을 한결 쉽게 해 주는 빌드 도구이다. 안드로이드의 경우 adaptive icon 도 지원한다.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://firebase.google.com/docs/flutter/setup?hl=ko&amp;amp;platform=ios" rel="noopener noreferrer"&gt;firebase&lt;/a&gt; - 안드로이드와 동일하게 Crashlytics 나 Analytics 등의 기능은 Firebase 를 사용한다. 안드로이드와 다르게 firebase 명령줄 도구를 설치하고, 이 도구를 이용해 코드 생성 등의 밑작업을 해 줘야 해서 좀 더 복잡하긴 하다.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://pub.dev/packages/permission_handler" rel="noopener noreferrer"&gt;permission_handler&lt;/a&gt; - 안드로이드 뿐 아니라 iOS 쪽 런타임 권한까지 고민해야 한다. 내 프로젝트는 이미지나 파일 접근 권한 정도만 사용했는데, 위치 권한같이 복잡한 권한도 깔끔하게 잘 지원해줄지 모르겠다.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://pub.dev/packages/flutter_local_notifications" rel="noopener noreferrer"&gt;flutter_local_notifications&lt;/a&gt; - 알림 처리를 위한 패키지이다. 안드로이드와 iOS 의 알림 처리 방식이 완전히 달라 도움을 받았다.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  API 호출
&lt;/h2&gt;

&lt;p&gt;Http Api를 호출해서 응답을 받는 절차는 안드로이드와 거의 비슷하다. 안드로이드가 Retrofit 으로 선언한 interface 에서 coroutine 으로 특정 타입의 응답을 받는 것 처럼, 플러터에선 Retrofit 으로 선언한 abstract class 를 기반으로 생성된 함수를 이용해 Future 응답을 받는다.&lt;/p&gt;

&lt;p&gt;이 때 응답 DTO 는 freezed 로 받고, json 매핑은 json serializable 로 이뤄진다. 대강의 코드는 다음과 같다.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Riverpod&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;keepAlive:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;// riverpod 에서 singleton 으로 관리&lt;/span&gt;
&lt;span class="n"&gt;UserApi&lt;/span&gt; &lt;span class="nf"&gt;userApi&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Ref&lt;/span&gt; &lt;span class="n"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;  
  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;dio&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ref&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;watch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dioProvider&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;  
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;UserApi&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dio&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;  
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nd"&gt;@RestApi&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;baseUrl:&lt;/span&gt; &lt;span class="s"&gt;"user/"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nl"&gt;callAdapter:&lt;/span&gt; &lt;span class="n"&gt;EitherCallAdapter&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;abstract&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="err"&gt;&lt;/span&gt;&lt;span class="nc"&gt;UserApi&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;  
  &lt;span class="kd"&gt;factory&lt;/span&gt; &lt;span class="n"&gt;UserApi&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Dio&lt;/span&gt; &lt;span class="n"&gt;dio&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="kt"&gt;String&lt;/span&gt; &lt;span class="n"&gt;baseUrl&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_UserApi&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  

  &lt;span class="nd"&gt;@GET&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"me"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;// api 호출 후 json 응답을 dto 로 매핑하는 처리까지 retrofit 에서 담당&lt;/span&gt;
  &lt;span class="n"&gt;Future&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;ApiEither&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;UserDto&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;me&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// dto&lt;/span&gt;
&lt;span class="nd"&gt;@freezed&lt;/span&gt;  
&lt;span class="kd"&gt;abstract&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;UserDto&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;_$UserDto&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;  
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="kd"&gt;factory&lt;/span&gt; &lt;span class="n"&gt;UserDto&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;  
    &lt;span class="nd"&gt;@JsonKey&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;name:&lt;/span&gt; &lt;span class="s"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kd"&gt;required&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  
  &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_UserDto&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  

  &lt;span class="kd"&gt;factory&lt;/span&gt; &lt;span class="n"&gt;UserDto&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;fromJson&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Map&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kd"&gt;dynamic&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_$UserDtoFromJson&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;  
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// call adatper - api 호출 예외를 Either 로 추상화&lt;/span&gt;
&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;EitherCallAdapter&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="n"&gt;CallAdapter&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Future&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;,&lt;/span&gt; &lt;span class="n"&gt;Future&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Either&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;ApiException&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;  
  &lt;span class="nd"&gt;@override&lt;/span&gt;  
  &lt;span class="n"&gt;Future&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Either&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;ApiException&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;adapt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Future&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="kt"&gt;Function&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="n"&gt;call&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kd"&gt;async&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;  
    &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;  
      &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;call&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;  
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;Right&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;  
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="kd"&gt;on&lt;/span&gt; &lt;span class="n"&gt;DioException&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;  
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;Left&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ApiException&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;fromDioException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;  
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;  
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;Left&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;NonHttpException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;  
    &lt;span class="p"&gt;}&lt;/span&gt;  
  &lt;span class="p"&gt;}&lt;/span&gt;  
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;typedef&lt;/span&gt; &lt;span class="n"&gt;ApiEither&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Either&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;ApiException&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;;&lt;/span&gt;

&lt;span class="kd"&gt;sealed&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ApiException&lt;/span&gt; &lt;span class="kd"&gt;implements&lt;/span&gt; &lt;span class="n"&gt;Exception&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;DTO&lt;/code&gt; 클래스의 경우 관용적인 코드 패턴을 계속 작성해야 하므로 intelliJ 의 live template 등을 이용하거나, gemini 등에 프로젝트 표준을 잘 가르켜두면 좋다.&lt;/p&gt;

&lt;h1&gt;
  
  
  UI
&lt;/h1&gt;

&lt;p&gt;화면은 &lt;code&gt;HookConsumerWidget&lt;/code&gt; 을 확장해서 선언하면 지저분하게 &lt;code&gt;StatelessWidget&lt;/code&gt; 과 &lt;code&gt;StatefulWidget&lt;/code&gt; 두 벌 씩 관리하지 않아도 된다. &lt;/p&gt;

&lt;p&gt;물론 UI 를 구성할 때 state 가 변경될 때 전체 ui 를 다시 그리지 않게 하려면 select 함수 등을 이용해 ui 의 변경 범위를 최소화하도록 세심하게 구현해야 한다.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;
&lt;span class="c1"&gt;// repository &lt;/span&gt;
&lt;span class="nd"&gt;@Riverpod&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;keepAlive:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  
&lt;span class="n"&gt;UserRepository&lt;/span&gt; &lt;span class="nf"&gt;userRepository&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Ref&lt;/span&gt; &lt;span class="n"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;  
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;UserRepositoryImpl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="n"&gt;ref&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;read&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;userApiProvider&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;  
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;abstract&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;UserRepository&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;Future&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Either&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Exception&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;Profile&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;getProfile&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;UserRepositoryImpl&lt;/span&gt; &lt;span class="kd"&gt;implements&lt;/span&gt; &lt;span class="n"&gt;UserRepository&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;UserApi&lt;/span&gt; &lt;span class="n"&gt;_api&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="n"&gt;UserRepositoryImpl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;_api&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="nd"&gt;@override&lt;/span&gt;
  &lt;span class="n"&gt;Future&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Either&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Exception&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;Profile&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;getProfile&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="kd"&gt;async&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_api&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;me&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;mapToProfile&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// dto to profile&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// vm &amp;amp; state&lt;/span&gt;
&lt;span class="nd"&gt;@freezed&lt;/span&gt;  
&lt;span class="kd"&gt;abstract&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ProfileVmState&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;_$ProfileVmState&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;  
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="kd"&gt;factory&lt;/span&gt; &lt;span class="n"&gt;ProfileVmState&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="nd"&gt;@Default&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;AsyncResult&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;uninitialized&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="n"&gt;AsyncResult&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Profile&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nd"&gt;@Default&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;AsyncResult&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;uninitialized&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="n"&gt;AsyncResult&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Profile&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;update&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
  &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_ProfileVmState&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nd"&gt;@riverpod&lt;/span&gt;  
&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ProfileVm&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="n"&gt;_$ProfileVm&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;  
  &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="n"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="kd"&gt;async&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;  
    &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;userRepo&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ref&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;read&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;userRepositoryProvider&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;  

    &lt;span class="n"&gt;state&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;copyWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;fetch:&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;AsyncResult&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;loading&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;  
    &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;userRepo&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getProfile&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;  
    &lt;span class="n"&gt;state&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;copyWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;fetch:&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;toAsyncResult&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;  
  &lt;span class="p"&gt;}&lt;/span&gt;  

  &lt;span class="nd"&gt;@override&lt;/span&gt;  
  &lt;span class="n"&gt;ProfileVmState&lt;/span&gt; &lt;span class="n"&gt;build&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;  
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;ProfileVmState&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;  
  &lt;span class="p"&gt;}&lt;/span&gt;  
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// screen &lt;/span&gt;
&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ProfileScreen&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="n"&gt;HookConsumerWidget&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;ProfileScreen&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="k"&gt;super&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="nd"&gt;@override&lt;/span&gt;  
  &lt;span class="n"&gt;Widget&lt;/span&gt; &lt;span class="n"&gt;build&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BuildContext&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;WidgetRef&lt;/span&gt; &lt;span class="n"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;ref&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;listen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="n"&gt;profileVmProvider&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;select&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;updateResult&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prev&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;next&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
       &lt;span class="c1"&gt;// 프로필 업데이트 실패, 성공 처리&lt;/span&gt;
       &lt;span class="n"&gt;next&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;fold&lt;/span&gt;&lt;span class="p"&gt;(...);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;Scaffold&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="p"&gt;...);&lt;/span&gt; &lt;span class="c1"&gt;// ui state 에 따라 화면 구성 &lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  FCM
&lt;/h2&gt;

&lt;p&gt;FCM 수신 시 iOS 동작까지 고려해야하는데, iOS 플랫폼에 익숙치 않아 이 부분이 좀 헷갈렸다. 실험적으로 알아낸 결론은 다음과 같다. 참고로 FCM 에선 notification 필드 사용 방식이 아닌 data 필드 사용 방식을 택했다.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;안드로이드

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;FirebaseMessaging.onMessage.listen()&lt;/code&gt; 으로 payload 수신&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;FlutterLocalNotificationsPlugin.show()&lt;/code&gt; 로 알림센터에 등록&lt;/li&gt;
&lt;li&gt; 사용자가 알림을 누른 경우

&lt;ul&gt;
&lt;li&gt;앱 프로세스가 살아있는 경우: &lt;code&gt;FlutterLocalNotificationsPlugin.initialize()&lt;/code&gt; 의 &lt;code&gt;onDidReceiveNotificationResponse&lt;/code&gt; 호출&lt;/li&gt;
&lt;li&gt;앱 프로세스가 죽은 경우: 앱 실행 후 &lt;code&gt;FlutterLocalNotificationsPlugin().getNotificationAppLaunchDetails()&lt;/code&gt; 값으로 어떤 스킴으로 앱이 구동되었는지 확인한 후, 스킴 처리&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;/li&gt;

&lt;li&gt;iOS

&lt;ul&gt;
&lt;li&gt;알림은 시스템에서 자동으로 등록&lt;/li&gt;
&lt;li&gt;사용자가 알림을 누른 경우

&lt;ul&gt;
&lt;li&gt;앱 프로세스가 살아있는 경우: &lt;code&gt;FirebaseMessaging.onMessageOpenedApp.listen()&lt;/code&gt; 콜백 호출됨&lt;/li&gt;
&lt;li&gt;앱 프로세스가 죽은 경우: 앱 실행 후 &lt;code&gt;FirebaseMessaging.instance.getInitialMessage()&lt;/code&gt; 값으로  메시지 확인&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;/li&gt;

&lt;/ul&gt;

&lt;h2&gt;
  
  
  맺음말
&lt;/h2&gt;

&lt;p&gt;Flutter 의 라이브러리 생태계도 상당히 잘 되어있어 뒤져보는 재미가 있는데, 너무 방대하다보니 안드로이드 개발자가 선뜻 시작하다가 지쳐버릴 수 있다. 이 글에 소개한 구성이 Flutter 개발자 입장에서 구닥다리이거나, 더 나은 선택지들이 분명히 있겠지만 안드로이드 개발자가 친숙하게, 후딱 Flutter 개발을 시작하기엔 괜찮은 선택이 아닐까 싶다. 모쪼록 Flutter 를 시작해보려는 안드로이드 개발자에게 도움이 되었으면 한다. &lt;/p&gt;

&lt;p&gt;더 나아가 샘플 프로젝트라도 만들어 보면 좋겠지만 게을러서...&lt;/p&gt;

</description>
      <category>flutter</category>
      <category>android</category>
    </item>
    <item>
      <title>ActivityResultContract 안전하게 사용하기</title>
      <dc:creator>Sewon Ann</dc:creator>
      <pubDate>Fri, 29 Aug 2025 02:05:48 +0000</pubDate>
      <link>https://dev.to/kingori/activityresultcontract-anjeonhage-sayonghagi-34ga</link>
      <guid>https://dev.to/kingori/activityresultcontract-anjeonhage-sayonghagi-34ga</guid>
      <description>&lt;p&gt;안드로이드는 intent 를 이용해 다른 앱의 기능을 유연하게 호출하고, 결과를 받아올 수 있다. 이렇게 결과를 받아오는 데 사용하는 함수인 &lt;code&gt;startActivityForResult&lt;/code&gt;와 &lt;code&gt;onActivityResult&lt;/code&gt;는 편리해 보이지만, 여러 가지 함정을 가지고 있었다. 이 글에선 새로운 표준이 된 &lt;code&gt;Activity Result API&lt;/code&gt;와 &lt;code&gt;ActivityResultContract&lt;/code&gt;에 대해 깊이 파헤쳐 보고, 사용 시 주의할 점까지 함께 알아보겠다.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;code&gt;ActivityResultContract&lt;/code&gt; 소개
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:activity/activity/src/main/java/androidx/activity/result/contract/ActivityResultContract.kt" rel="noopener noreferrer"&gt;ActivityResultContract&lt;/a&gt; 는 다른 액티비티를 실행하고, 그로부터 결과를 받아오는 작업을 추상화한 클래스다. 이 클래스는 두 가지 타입, 즉 &lt;strong&gt;입력(Input)&lt;/strong&gt;과 &lt;strong&gt;출력(Output)&lt;/strong&gt;을 제네릭으로 받는다. &lt;code&gt;createIntent&lt;/code&gt; 메서드를 통해 입력을 바탕으로 인텐트를 생성하고, &lt;code&gt;parseResult&lt;/code&gt; 메서드를 통해 &lt;code&gt;onActivityResult&lt;/code&gt; 콜백으로부터 받은 결과를 출력 타입으로 변환하는 역할을 한다.&lt;/p&gt;

&lt;h2&gt;
  
  
  과거의 유산: &lt;code&gt;onActivityResult&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;ActivityResultContract&lt;/code&gt;가 등장하기 전에는 다른 액티비티로부터 결과를 받기 위해 &lt;code&gt;startActivityForResult&lt;/code&gt;와 &lt;code&gt;onActivityResult&lt;/code&gt;를 사용했다.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;Activity&lt;/code&gt;에서 &lt;code&gt;onActivityResult&lt;/code&gt; 사용하기
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;OldActivity&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;AppCompatActivity&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;REQUEST_CODE&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;123&lt;/span&gt;

    &lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;onCreate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;savedInstanceState&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Bundle&lt;/span&gt;&lt;span class="p"&gt;?)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;super&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;onCreate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;savedInstanceState&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="c1"&gt;// ...&lt;/span&gt;
        &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;intent&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Intent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;OtherActivity&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;java&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;startActivityForResult&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;intent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;REQUEST_CODE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;onActivityResult&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;requestCode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;resultCode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Intent&lt;/span&gt;&lt;span class="p"&gt;?)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;super&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;onActivityResult&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;requestCode&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;resultCode&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;requestCode&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="nc"&gt;REQUEST_CODE&lt;/span&gt; &lt;span class="p"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;resultCode&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="nc"&gt;Activity&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;RESULT_OK&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="c1"&gt;// 결과 처리&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;이 방식은 &lt;code&gt;requestCode&lt;/code&gt;를 사용해 여러 결과를 구분해야 했고, &lt;code&gt;onActivityResult&lt;/code&gt; 메서드 내부에 복잡한 조건문이 생길 수 있다는 단점이 있었다. 또한, &lt;strong&gt;&lt;code&gt;Activity&lt;/code&gt; 클래스에 직접 의존&lt;/strong&gt;하기 때문에 유닛 테스트가 어려워지는 문제도 있었다.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;Fragment&lt;/code&gt;에서 &lt;code&gt;onActivityResult&lt;/code&gt; 사용하기
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;Fragment&lt;/code&gt; 역시 &lt;code&gt;onActivityResult&lt;/code&gt;를 사용했다.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;OldFragment&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Fragment&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

    &lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;onStart&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;super&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;onStart&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;intent&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Intent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;requireContext&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="nc"&gt;OtherActivity&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;java&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;startActivityForResult&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;intent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;REQUEST_CODE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;onActivityResult&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;requestCode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;resultCode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Intent&lt;/span&gt;&lt;span class="p"&gt;?)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;super&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;onActivityResult&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;requestCode&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;resultCode&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;requestCode&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="nc"&gt;REQUEST_CODE&lt;/span&gt; &lt;span class="p"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;resultCode&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="nc"&gt;Activity&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;RESULT_OK&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="c1"&gt;// 결과 처리&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;Fragment&lt;/code&gt; 역시 &lt;code&gt;Activity&lt;/code&gt;와 마찬가지로 &lt;code&gt;onActivityResult&lt;/code&gt;를 오버라이드해야 했으며, 이는 비슷한 단점들을 야기했다.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;code&gt;ActivityResultContract&lt;/code&gt;의 내부 동작: &lt;code&gt;Registry&lt;/code&gt;를 통한 추상화
&lt;/h2&gt;

&lt;p&gt;많은 개발자가 &lt;code&gt;ActivityResultContract&lt;/code&gt;가 완전히 새로운 방식으로 동작한다고 생각하지만, 사실 &lt;strong&gt;내부적으로는 여전히 &lt;code&gt;startActivityForResult&lt;/code&gt;와 &lt;code&gt;onActivityResult&lt;/code&gt;를 사용&lt;/strong&gt;한다. &lt;code&gt;ActivityResultContract&lt;/code&gt;는 이 두 메서드를 개발자가 직접 호출하는 대신, &lt;a href="https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:activity/activity/src/main/java/androidx/activity/result/ActivityResultRegistry.kt" rel="noopener noreferrer"&gt;ActivityResultRegistry&lt;/a&gt; 를 통해 이를 추상화하고 관리한다.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;ActivityResultRegistry&lt;/code&gt;는 생명주기에 안전한 방식으로 결과를 전달하기 위해 &lt;strong&gt;내부적으로 리스너와 키를 사용&lt;/strong&gt;한다. &lt;code&gt;registerForActivityResult&lt;/code&gt;를 호출하면 다음 세 가지 과정이 진행된다.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;키 생성&lt;/strong&gt;: &lt;code&gt;ActivityResultRegistry&lt;/code&gt;는 내부적으로 고유한 키(예: &lt;code&gt;"activity_rq#1"&lt;/code&gt;)를 생성한다. 참고: &lt;a href="https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:activity/activity/src/main/java/androidx/activity/ComponentActivity.kt;l=811;bpv=1;bpt=1?q=componentactivity" rel="noopener noreferrer"&gt;ComponentActivity#registerForActivityResult&lt;/a&gt; &lt;/li&gt;
&lt;li&gt; &lt;strong&gt;리스너 등록&lt;/strong&gt;: 이 고유한 키와 함께, 개발자가 제공한 콜백(&lt;code&gt;ActivityResultCallback&lt;/code&gt;)이 &lt;code&gt;ActivityResultRegistry&lt;/code&gt;에 등록된다. 참고: &lt;a href="https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:activity/activity/src/main/java/androidx/activity/result/ActivityResultRegistry.kt;drc=63088af677e17befa57ea614a3d9a292096b676d;bpv=1;bpt=1;l=81" rel="noopener noreferrer"&gt;ActivityResultRegistry#register&lt;/a&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;이제 &lt;strong&gt;&lt;code&gt;ActivityResultLauncher&lt;/code&gt;의 &lt;code&gt;launch&lt;/code&gt; 메서드&lt;/strong&gt;가 호출되면, &lt;code&gt;ActivityResultRegistry&lt;/code&gt;는 준비된 인텐트를 &lt;strong&gt;&lt;code&gt;startActivityForResult&lt;/code&gt;를 이용해 실행&lt;/strong&gt;한다. ( 참고 : &lt;a href="https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:activity/activity/src/main/java/androidx/activity/ComponentActivity.kt;l=224;drc=63088af677e17befa57ea614a3d9a292096b676d;bpv=1;bpt=1" rel="noopener noreferrer"&gt;ComponentActivity.activityResultRegistry&lt;/a&gt;) 액티비티가 종료되어 결과가 반환되면, &lt;code&gt;onActivityResult&lt;/code&gt;를 통해 전달된 결과를 &lt;code&gt;ActivityResultRegistry&lt;/code&gt;가 가로챈다. (참고: &lt;a href="https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:activity/activity/src/main/java/androidx/activity/ComponentActivity.kt;l=775;drc=63088af677e17befa57ea614a3d9a292096b676d" rel="noopener noreferrer"&gt;ComponentActivity#onActivityResult&lt;/a&gt;)&lt;/p&gt;

&lt;p&gt;마지막으로, &lt;code&gt;ActivityResultRegistry&lt;/code&gt;는 &lt;strong&gt;처음 등록했던 고유한 키&lt;/strong&gt;를 이용해 등록된 콜백을 찾아 &lt;code&gt;onResult&lt;/code&gt;를 실행하는 방식으로 결과를 전달한다. 이 모든 과정이 &lt;code&gt;ActivityResultContract&lt;/code&gt; 뒤에 숨겨져 있어 개발자는 복잡한 콜백 처리를 직접 할 필요가 없다.&lt;/p&gt;

&lt;p&gt;이 과정을 sequence diagram 으로 표현하면 다음과 같다.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fedquj7pnnu82hxr6ap36.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fedquj7pnnu82hxr6ap36.png" alt=" " width="800" height="517"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;code&gt;ActivityResultContract&lt;/code&gt; 사용 시 주의할 점
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;ActivityResultContract&lt;/code&gt;는 분명 강력한 도구이지만, 잘못 사용하면 예상치 못한 버그를 유발할 수 있다. 가장 중요한 원칙은  &lt;code&gt;registerForActivityResult()&lt;/code&gt; 는 항상 같은 순서로 호출되어야 한다는 점이다. &lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;Activity&lt;/code&gt;에서 사용할 때
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;Activity&lt;/code&gt;에서 &lt;code&gt;registerForActivityResult&lt;/code&gt;를 호출할 때는 &lt;strong&gt;클래스 인스턴스가 생성되는 시점&lt;/strong&gt;에 멤버 변수로 등록하는 것을 권장한다.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;NewActivity&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;AppCompatActivity&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

    &lt;span class="c1"&gt;// 인스턴스가 생성될 때 초기화&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;someActivityResultLauncher&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;registerForActivityResult&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nc"&gt;ActivityResultContracts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;StartActivityForResult&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt;
        &lt;span class="c1"&gt;// 결과 처리&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;onCreate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;savedInstanceState&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Bundle&lt;/span&gt;&lt;span class="p"&gt;?)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;super&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;onCreate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;savedInstanceState&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="c1"&gt;// ...&lt;/span&gt;
        &lt;span class="n"&gt;someActivityResultLauncher&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;launch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;intent&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;만약 경우에 따라 호출 순서가 달라진다면 어떤 일이 일어날까? 이런 식으로 짜면 안되지만 하여간 다음 코드를 상상해보자.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;NewActivity&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;AppCompatActivity&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

    &lt;span class="k"&gt;lateinit&lt;/span&gt; &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="py"&gt;someActivityResultLauncher&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;ActivityResultLauncher&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Intent&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="k"&gt;lateinit&lt;/span&gt; &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="py"&gt;otherActivityResultLauncher&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;ActivityResultLauncher&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Intent&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;

     &lt;span class="nf"&gt;init&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
         &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;isAm&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;ZonedDateTime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="n"&gt;hour&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;12&lt;/span&gt;

         &lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="n"&gt;isAm&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
             &lt;span class="n"&gt;someActivityResultLauncher&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;registerForActivityResult&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;..&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
             &lt;span class="n"&gt;otherActivityResultLauncher&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;registerForActivityResult&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;..&lt;/span&gt;&lt;span class="p"&gt;.)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
         &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
             &lt;span class="n"&gt;otherActivityResultLauncher&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;registerForActivityResult&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;..&lt;/span&gt;&lt;span class="p"&gt;.)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
             &lt;span class="n"&gt;someActivityResultLauncher&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;registerForActivityResult&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;..&lt;/span&gt;&lt;span class="p"&gt;.)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
         &lt;span class="p"&gt;}&lt;/span&gt;
     &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;오전에 액티비티가 생성되어 some &amp;gt; other 순으로 register 되었다. 이 경우 some 의 내부 key는 &lt;code&gt;activity_req#0&lt;/code&gt;, other 는 &lt;code&gt;activity_req#1&lt;/code&gt; 이 된다. some 을 launch 해서 다른 OtherActivity 가 호출되었다. 그 사이에 오후가 되었다. 그 사이에 사용자가 화면을 회전해서 &lt;code&gt;NewActivity&lt;/code&gt; 는 재생성되어야 한다. 이제 OtherActivity가 재생성되면서 결과값이 전달된다. 그런데 이번엔 other 가 #0, some 이 #1 이 되어버렸다. 결국 some 의 callback 이 호출되어야 하는데, 엉뚱하게 other 의 callback 이 호출되어 버린다. 만약 두 callback 의 인자 타입이 다를 경우엔 class cast exception 이 날 수도 있고, 아니면 가져온 결과값으로 엉뚱한 일이 벌어질 수 있다.&lt;/p&gt;

&lt;p&gt;여기서 의문이 한가지 들 수 있다. &lt;code&gt;startActivityForResult()&lt;/code&gt; 에 전달되는 request code 는 랜덤하게 생성되는데, 액티비티가 재생성되었을 때 onActivityResult 에서 이전 request code 를 어떻게 알고 callback 을 호출하는걸까? 이 부분은 &lt;code&gt;ActivityResultRegistry&lt;/code&gt; 가 재생성 시 복구되면서 기존 내부 key 와 request code 간 매핑을 복구하기 때문에 가능핟.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;ActivityResultRegistry&lt;/code&gt; 는 내부적으로 launcher 들에 부여된 key 를 Int 타입인 request code 로 반환하는 테이블을 갖고 있으며 ( 참고: &lt;a href="https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:activity/activity/src/main/java/androidx/activity/result/ActivityResultRegistry.kt;l=383;drc=63088af677e17befa57ea614a3d9a292096b676d;bpv=1;bpt=1" rel="noopener noreferrer"&gt;ActivityResultRegistiry#bindRcKey&lt;/a&gt;) , 액티비티가 재생성될 때 이 테이블을 복구한다. 따라서 액티비티가 재생성되어도 이전에 호출했던 request code 를 가지고 어떤 콜백을 호출해야 할 지 잘 알아낼 수 있다. ( 참고 : &lt;a href="https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:activity/activity/src/main/java/androidx/activity/result/ActivityResultRegistry.kt;l=260;drc=63088af677e17befa57ea614a3d9a292096b676d;bpv=1;bpt=1" rel="noopener noreferrer"&gt;ActivityResultRegistry#onRestoreInstanceState&lt;/a&gt;)&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;Fragment&lt;/code&gt;에서 사용할 때
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;Fragment&lt;/code&gt; 역시 &lt;code&gt;Activity&lt;/code&gt;와 동일한 문제가 발생할 수 있다. 추가적으로 &lt;code&gt;Fragment&lt;/code&gt;가 &lt;code&gt;destroy&lt;/code&gt;된 후 다시 &lt;code&gt;create&lt;/code&gt;될 때, &lt;strong&gt;이전에 등록했던 콜백 정보가 사라져 콜백이 제대로 전달되지 않을 수 있다.&lt;/strong&gt; 이는 &lt;strong&gt;&lt;code&gt;Fragment&lt;/code&gt;가 &lt;code&gt;destroy&lt;/code&gt;될 때 &lt;code&gt;ActivityResultRegistry&lt;/code&gt;에 등록된 정보가 제거&lt;/strong&gt;되기 때문이다.&lt;/p&gt;

&lt;p&gt;lifecycleOwner 가 DESTROY 될 때 등록된 contract, callback 을 unregister 하는 동작은 어차피 &lt;code&gt;ActivityResultRegistry&lt;/code&gt;가 수행하기 때문에 &lt;code&gt;Activity&lt;/code&gt; 나 &lt;code&gt;Fragment&lt;/code&gt; 나 동일하다. 하지만 나는 한번 destroy 된 &lt;code&gt;Activity&lt;/code&gt; 인스턴스가 다시 create 되는 경우를 보지 못했는데, &lt;code&gt;Fragment&lt;/code&gt; 에선 이런 경우를 종종 봤다. 따라서, &lt;code&gt;Activity&lt;/code&gt; 에선 객체 초기화 시 register 를 권장했지만 &lt;code&gt;Fragment&lt;/code&gt; 에선 오히려 &lt;code&gt;onCreate&lt;/code&gt; 에서 register 를 권장한다. 이래야 destroy 후 re-create 시 다시 &lt;code&gt;ActivityResultRegistry&lt;/code&gt; 에 &lt;code&gt;Launcher&lt;/code&gt;가  등록되기 때문이다. &lt;/p&gt;

&lt;p&gt;따라서 &lt;code&gt;Fragment&lt;/code&gt; 인스턴스 초기화 시점보다는 &lt;strong&gt;&lt;code&gt;onCreate&lt;/code&gt; 콜백 내에서 &lt;code&gt;registerForActivityResult&lt;/code&gt;를 호출&lt;/strong&gt;하는 것이 더 안전하다.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;NewFragment&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Fragment&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;lateinit&lt;/span&gt; &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="py"&gt;someActivityResultLauncher&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;ActivityResultLauncher&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Intent&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;

    &lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;onCreate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;savedInstanceState&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Bundle&lt;/span&gt;&lt;span class="p"&gt;?)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;super&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;onCreate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;savedInstanceState&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="c1"&gt;// Fragment가 재생성될 때마다 콜백이 다시 등록되도록 보장&lt;/span&gt;
        &lt;span class="n"&gt;someActivityResultLauncher&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;registerForActivityResult&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="nc"&gt;ActivityResultContracts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;StartActivityForResult&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt;
            &lt;span class="c1"&gt;// 결과 처리&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;someButtonClick&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;someActivityResultLauncher&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;launch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Intent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;requireContext&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="nc"&gt;OtherActivity&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;java&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  &lt;code&gt;Composable&lt;/code&gt;에서 사용할 때
&lt;/h3&gt;

&lt;p&gt;Jetpack Compose에서는 &lt;a href="https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:activity/activity-compose/src/main/java/androidx/activity/compose/ActivityResultRegistry.kt;drc=63088af677e17befa57ea614a3d9a292096b676d;l=80" rel="noopener noreferrer"&gt;rememberLauncherForActivityResult&lt;/a&gt; 를 사용한다. 이 함수 역시 내부적으로 &lt;code&gt;activityResultRegistry.register&lt;/code&gt;를 사용하므로, &lt;code&gt;Activity&lt;/code&gt; 나 &lt;code&gt;Fragment&lt;/code&gt; 와 크게 다르지 않다.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Composable&lt;/span&gt;
&lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;MyScreen&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;someActivityResultLauncher&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;rememberLauncherForActivityResult&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;contract&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;ActivityResultContracts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;StartActivityForResult&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="n"&gt;onResult&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt;
            &lt;span class="c1"&gt;// 결과 처리&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="nc"&gt;Button&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;onClick&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;someActivityResultLauncher&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;launch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Intent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;OtherActivity&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;java&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Launch Activity"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;다만 내부 key 가 &lt;code&gt;UUID.randomUUID().toString()&lt;/code&gt; 라서, &lt;code&gt;Activity&lt;/code&gt; 에서 &lt;code&gt;activity_req#1&lt;/code&gt; 와 같은 형태로 등록 순서에 영향을 받는게 아닌, 랜덤한 고유값이 부여되는 부분이 다르다. &lt;/p&gt;

&lt;p&gt;&lt;code&gt;rememberLauncherForActivityResult&lt;/code&gt;도 마찬가지로 &lt;strong&gt;조건문 안에 등록하면 안 된다&lt;/strong&gt;. &lt;code&gt;remember&lt;/code&gt;가 있는 함수는 컴포저블의 &lt;strong&gt;재구성(recomposition)&lt;/strong&gt; 과정에서 &lt;code&gt;remember&lt;/code&gt;의 키가 변경되거나 조건에 따라 호출되지 않을 경우, 인스턴스가 &lt;strong&gt;새롭게 생성되거나 아예 생성되지 않을 수&lt;/strong&gt; 있기 때문이다.&lt;/p&gt;

&lt;p&gt;또한, &lt;code&gt;rememberLauncherForActivityResult&lt;/code&gt;는 가급적 &lt;strong&gt;최상위 컴포저블에서 사용하는 것을 권장&lt;/strong&gt;한다. 하위 컴포저블에서 사용할 경우, 상위 컴포저블의 상태 변화나 조건문에 따라 컴포저블의 위치 등이 바뀔 경우 액티비티 재생성 시 다른 composable 로 인식되어 이전 composable 의 복구가 이뤄지지 않는다면 결과 콜백이 호출되지 않기 때문이다. &lt;/p&gt;

&lt;p&gt;캡슐화 관점에서 기능을 사용하고자 하는 composable 내부에 &lt;code&gt;rememberLauncherForActivityResult&lt;/code&gt; 를 사용하면 참 편리하겠지만, 해당 composable 이 어떤 맥락에서, 어떤 경우에 호출되는지 통재하기가 어렵기 때문에 이렇게 구현하는 건 위험할 수 있다. 예를들어 게시판을 만드는데 댓글 composable 에 이미지 첨부를 위해 갤러리를 호출하고, 결과를 받아오는 기능까지 넣어두면 참 멋질 것 같다. 하지만 이 댓글 composable 이 어떤 식으로 호출될지 composable 작성자가 컨트롤 할 수 없고, 이러다보면 액티비티 재생성 시 호출 위치가 바뀌어버릴 경우 찾기 어려운 오류가 발생할 것이다. 반대로 composable 을 사용하는 개발자 입장에서도 이 composable을 사용하는데 어떤 주의사항이 있는지 일일이 파악하기가 쉽지 않아, 이런 경우 애초에 &lt;code&gt;rememberLauncherForActivityResult&lt;/code&gt; 를 안쓰는게 낫다.&lt;/p&gt;

&lt;p&gt;따라서 가급적 최상위 composable 에만 사용하는게 유지보수 관점에서 더 나은 선택이 될 것이라 생각한다.&lt;/p&gt;

&lt;h2&gt;
  
  
  맺음말
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;ActivityResultContract&lt;/code&gt;는 코드를 간결하게 만들고, 타입 안정성을 제공하며, 테스트 용이성을 높여준다.&lt;/strong&gt; 하지만 주의깊게 사용하지 않을 경우 찾기 어려운 이상동작이 발생할 수 있으므로 잘 알고 사용해야 한다.&lt;/p&gt;

</description>
      <category>android</category>
    </item>
    <item>
      <title>안드로이드 앱의 API 응답 캐싱</title>
      <dc:creator>Sewon Ann</dc:creator>
      <pubDate>Tue, 01 Apr 2025 02:29:59 +0000</pubDate>
      <link>https://dev.to/kingori/andeuroideu-aebyi-api-eungdab-kaesing-2l6l</link>
      <guid>https://dev.to/kingori/andeuroideu-aebyi-api-eungdab-kaesing-2l6l</guid>
      <description>&lt;p&gt;데이터를 캐시하는 목적은 다양할텐데, 모바일 앱에선 대게 빠르게 화면을 보여주기 위해 이전 응답을 저장해두었다가 보여주는 용도로 사용한다. 안드로이드 앱에서 API 응답을 캐싱하는 방법을 간단히 살펴보자. &lt;/p&gt;

&lt;p&gt;캐시는 어디에 정보를 저장하는지, 아키텍처의 어느 계층에서 캐시를 하는지에 따라 세분화 할 수 있다. 저장 위치에 따른 구분으론 앱이 살아있는 동안에만 캐시하는 메모리 캐시와 디스크 캐시로 나뉠 수 있고, 계층에 따른 구분으론 네트워크 캐시와 애플리케이션 캐시로 나뉠 수 있다. 참, 애플리케이션 캐시는 그냥 내가 지어봤다.&lt;/p&gt;

&lt;p&gt;나의 결론부터 얘기해보면 다음과 같다.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;네트워크 캐시보다는 애플리케이션 캐시를 사용하자. 네트워크 캐시는 용도가 꽤 한정적이다.&lt;/li&gt;
&lt;li&gt;디스크 캐시와 메모리 캐시는 용도에 따라 구분하면 되는데, 디스크 캐시의 내용을 다시 메모리 캐시할 필요는 거의 없다.&lt;/li&gt;
&lt;li&gt;메모리 캐시할 땐 DTO 객체를 캐시하자.&lt;/li&gt;
&lt;li&gt;캐시는 만료 처리를 잘 해야 한다.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  네트워크 캐시
&lt;/h2&gt;

&lt;p&gt;아마 대부분 &lt;a href="https://square.github.io/okhttp/" rel="noopener noreferrer"&gt;Okhttp&lt;/a&gt; 를 이용해 api 를 호출하고 응답을 받을 것이다. Okhttp도 &lt;a href="https://square.github.io/okhttp/features/caching/" rel="noopener noreferrer"&gt;Cache&lt;/a&gt; 기능이 있고, 이를 잘 활용할 수 있다면 앱 개발자 입장에선 가장 편리하게 캐시의 이점을 누릴 수 있다. 하지만 &lt;strong&gt;사용에 제약이 많아 도움이 되는 경우가 적다&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Okhttp 캐시는 &lt;code&gt;cache-control&lt;/code&gt; 헤더를 기반으로 동작한다. &lt;a href="https://square.github.io/okhttp/recipes/#response-caching-kt-java" rel="noopener noreferrer"&gt;참조 코드도 나와있는데&lt;/a&gt;, 많은 api 응답이 즉시성을 요구하기 때문에 잘 활용하지 않는다. 써 보고 싶다면 백엔드 개발자와 잘 협의를 해서, response 에도 제대로 된 cache header가 설정되어야 한다. 하지만 okhttp의 network interceptor를 이용해 response 의 cache header도 조작할 수 있으므로, 앱만 의지가 있다면 활용할 수 있다.&lt;/p&gt;

&lt;p&gt;같은 API라도 언제는 캐시하고, 언제는 캐시하지 않는 등의 조정이 필요하다. 기본적으론 캐시를 사용하는데, 특정 상황에서만 캐시를 사용하지 않도록 &lt;code&gt;cache-control&lt;/code&gt; 헤더를 제어하면 된다. retrofit 을 쓴다면 header를 인자로 받던지, 별개의 함수를 만들고, 함수에 커스텀한 처리를 넣어두면 된다.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="kd"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;BalanceService&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

   &lt;span class="c1"&gt;// case 1: cache control 헤더를 인자로 받기&lt;/span&gt;
   &lt;span class="nd"&gt;@GET&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;..&lt;/span&gt;&lt;span class="p"&gt;.)&lt;/span&gt;
   &lt;span class="k"&gt;suspend&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;fetchBalance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nd"&gt;@Header&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Cache-Control"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;cacheControlHeader&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;BalanceDto&lt;/span&gt;

   &lt;span class="c1"&gt;// case 2: custom annotation 등을 이용해 api 를 분리하기&lt;/span&gt;
   &lt;span class="nd"&gt;@GET&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;..&lt;/span&gt;&lt;span class="p"&gt;.)&lt;/span&gt;
   &lt;span class="nd"&gt;@Cache&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;// 이런 annotation 은 없다. 직접 만들어서 설정하자.&lt;/span&gt;
   &lt;span class="k"&gt;suspend&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;fetchBalanceWithCache&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;BalanceDto&lt;/span&gt;

   &lt;span class="nd"&gt;@GET&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;..&lt;/span&gt;&lt;span class="p"&gt;.)&lt;/span&gt;
   &lt;span class="nd"&gt;@Cache&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;false&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
   &lt;span class="k"&gt;suspend&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;fetchBalanceWithoutCache&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;BalanceDto&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;그런데 여기까지 오면 굳이 이렇게 네트워크 캐시를 써야 하나? 싶은 마음이 든다. 가끔 앱의 전역 설정같이 잘 바뀌지 않는 api 응답 같은 경우엔 활용해봄직 하지만, 전체 api 중에 그런 성격의 api 는 대부분 많지 않을것이라 생각한다. 그 얼마 안되는 경우를 해결하기 위해 이렇게 기술적으로 복잡하게 들어가느니, 차라리 그냥 애플리케이션 캐시로 직접 관리하는 게 편한 경우가 많다. 물론 굉장히 네트워크가 좋지 않은 환경까지 대응한다면 앱 전반적으로 api 캐싱을 고려해야 하니 이런 경우라면 필요하겠으나, 대한민국 사용자를 대상으로 하는 앱이라면 굳이 이렇게까지 복잡하게 만들 필요가 있을까 싶다.&lt;/p&gt;

&lt;p&gt;이 네트워크 캐시가 빛을 발할 만한 부분이 있는데, 네트워크 이미지 캐시이다. 대부분의 이미지의 URI는 고정되어 있기 때문에 해당 URI의 내용이 캐싱되어 있으면 잘 hit할 것이라 기대할 수 있다. 하지만 이 역시 많은 이미지 로더들이 자체적인 캐시 처리를 하기 때문에 별로 의미가 없다. &lt;a href="https://coil-kt.github.io/coil/network/#cache-control-support" rel="noopener noreferrer"&gt;coil 문서&lt;/a&gt; 를 보면 기본적으로 cache-control 헤더값과 무관하게 모든 응답을 디스크 캐시에 기록한다고 적혀있다. 또한 okhttp의 cache 설정이 아닌, &lt;a href="https://github.com/coil-kt/coil/blob/main/coil-network-core/src/commonMain/kotlin/coil3/network/CacheNetworkResponse.kt" rel="noopener noreferrer"&gt;자체 cache 구현&lt;/a&gt;을 활용한다. 따라서 위에서 언급한 네트워크 캐시를 안써도 coil 은 알아서 네트워크 이미지 캐시를 잘 한다.&lt;/p&gt;

&lt;p&gt;내 경험 상 네트워크 캐시는 별로 쓸 일이 없었다. 그 이유는 다음과 같다.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;api 응답 캐시 용도로는 개별 응답의 캐시 컨트롤이 쉽지 않아 사용하기 불편하다.&lt;/li&gt;
&lt;li&gt;이미지 캐시 용도로는 이미 이미지 로더가 자체적인 캐시를 만들어뒀기 때문에 안쓴다.&lt;/li&gt;
&lt;li&gt;캐시 관리할 대상 api 가 많지 않다면, 애플리케이션 캐시로 직접 관리하는게 프로젝트 유지보수하기 편하다. 알아야 할 내용(네트워크 캐시)이 줄어드니깐. &lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  애플리케이션 캐시
&lt;/h2&gt;

&lt;p&gt;한땀 한땀 손으로 제어하는 애플리케이션 캐시로 가 보자. 데이터를 어디에 저장하는지에 따라 메모리 캐시와 디스크 캐시로 구분할 수 있을텐데, &lt;/p&gt;

&lt;h3&gt;
  
  
  디스크 캐시
&lt;/h3&gt;

&lt;p&gt;디스크 캐시는 다시 &lt;a href="https://developer.android.com/topic/libraries/architecture/datastore?hl=ko&amp;amp;_gl=1*6olqdr*_up*MQ..*_ga*NDQxMzI4NDUuMTc0MzM5MjQ5Mw..*_ga_6HH9YJMN9M*MTc0MzM5MjQ5Mi4xLjAuMTc0MzM5MjQ5Mi4wLjAuNTE3NDIwODEw#prefs-vs-proto" rel="noopener noreferrer"&gt;DataStore&lt;/a&gt; 를 이용한 파일 저장과 &lt;a href="https://developer.android.com/training/data-storage/room?hl=ko" rel="noopener noreferrer"&gt;Room&lt;/a&gt; 을 이용한 DB 저장으로 구분할 수 있겠다. 예전엔 DB 쓰기가 좀 껄끄러웠는데 이젠 Room 덕분에 DB를 아주 편하게 사용할 수 있다. 나는 대게 갯수를 한정할 수 없는 데이터는 DB에, 한정된 개수의 데이터는 DataStore 에 저장한다. &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;디스크 캐시의 내용을 다시 메모리에 캐시해야 할까? 난 불필요하다고 생각한다.&lt;/strong&gt; 왠지 접근할 때 마다 IO가 발생할테니 이를 다시 메모리캐시하는게 좋을 것 같아 보인다. 하지만 DataStore는 자체적으로 읽어온 내용을 메모리에 캐시한다. Room 은 저채적인 메모리 캐시가 없지만, Room 이 접근하는 Sqlite 가 내부에 캐시를 한다. 따라서 괜히 복잡하게 메모리 캐시를 추가할 이유가 없다.&lt;/p&gt;

&lt;h3&gt;
  
  
  메모리 캐시
&lt;/h3&gt;

&lt;p&gt;가장 빈번하게 활용하는 형태가 api 의 응답을 메모리에 들고 있는 메모리 캐시일 것이다. 메모리 캐시에 뭘 저장할지도 결정을 해야 한다. 크게 3가지 선택지가 있을것이다.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;json (응답 자체) : 응답으로 받는 json 자체를 저장&lt;/li&gt;
&lt;li&gt;dto : json 을 객체로 매핑한 dto 객체를 저장&lt;/li&gt;
&lt;li&gt;도메인 객체: dto를 다시 앱 로직에 맞춰 매핑한 도메인 객체를 저장&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;일단 retrofit 등을 쓴다면 json 을 구경할 일이 없을거라, json은 탈락이다. 그럼 dto 를 저장할건지, 도메인 객체를 저장할 건지 고민이 된다.&lt;/p&gt;

&lt;p&gt;gemini 등에 물어봐도 이에 대한 완전한 결론은 잘 보이지 않는다. 하지만 앱에서 api 응답을 캐싱할 용도로는 dto 저장이 적합해보인다. 설계 측면에서 메모리 캐시는 api 응답을 담아두는 그릇일 뿐이며, 도메인보다는 데이터 레이어에서 해결할 내용에 가깝다. 도메인 객체를 만들어내기 위해 여러개의 dto 들을 조합해야 하는 경우엔 좀 고민이 되긴 하지만, 그런 경우에도 결국 개별 dto 들을 캐싱한 후 도메인 객체는 매번 새로 만들어내는 게 코드를 관리하기 더 좋다고 생각한다. 대부분 메모리 캐시는 간단한 맵의 형태일텐데, dto 를 저장할 경우엔 캐시 키를 구성하기도 훨씬 편하다. 그냥 api 요청 인자가 키가 되기 때문이다. ChatGPT선생님은 아래와 같이 말씀하신다.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4oo8wrlva7g08uaaw5il.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4oo8wrlva7g08uaaw5il.png" alt="ChatGPT 선생님 말씀" width="800" height="727"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  캐시와 네트워크 요청 간 교통정리
&lt;/h3&gt;

&lt;p&gt;대부분의 캐시 활용 유스케이스는 다음과 같다.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;캐시를 찔러본다.&lt;/li&gt;
&lt;li&gt;hit 했으면 캐시로 응답&lt;/li&gt;
&lt;li&gt;hit 하지 못했다면 네트워크 요청을 보내고, 응답으로 캐시를 갱신&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;이 과정을 실제 코딩한다면 어떻게 할지도 생각해볼 거리가 있다. retrofit 을 이용해 구현한 대부분의 안드로이드 앱은 대강 다음 형태로 구현되어 있을것이다. 계좌 잔고를 캐싱하는 예시를 생각해보자.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="kd"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;BalanceRepository&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
   &lt;span class="k"&gt;suspend&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;getBalance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Balance&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;BalanceRepositoryImpl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;balanceService&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;BalanceService&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;BalanceRepository&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
   &lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="k"&gt;suspend&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;getBalance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;service&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fetchBalance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;type&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toBalance&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;BalanceService&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nd"&gt;@GET&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;..&lt;/span&gt;&lt;span class="p"&gt;.)&lt;/span&gt;
  &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;fetchBalance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nd"&gt;@Query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;BalanceDto&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;여기 메모리 캐시를 집어넣으면 대강 다음과 같은 모습이 될 것이다.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;BalanceRepositoryImpl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;..&lt;/span&gt;&lt;span class="p"&gt;.)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;BalanceRepository&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
   &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;balanceCache&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;mutableMapOf&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nc"&gt;BalanceDto&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;()&lt;/span&gt;

   &lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="k"&gt;suspend&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;getBalance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;balanceCache&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getOrPut&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;type&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; 
        &lt;span class="n"&gt;service&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fetchBalance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;type&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
       &lt;span class="p"&gt;}.&lt;/span&gt;&lt;span class="nf"&gt;toBalance&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;귿이 이 코드를 더 복잡하게 할 필요는 없어보인다. 캐시 동기화 정도의 개선은 생각해 볼만 하겠다.&lt;/p&gt;

&lt;p&gt;디스크 캐시의 경우엔 몇 가지 고민이 더 생긴다. 잔고를 디스크에 넣는게 말이 안되지만 datastore 에 저장하겠다고 가정해보자. 위에 디스크캐시 부분에서 언급했듯이, 디스크캐시 내용을 다시 메모리캐시에 올릴 이유는 없다. datastore 에 shared preference 기반으로 json 전체를 string 으로 저장했다고 가정해본다. 그렴 코드가 좀 더 늘어난다.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="kd"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;BalanceStore&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="o"&gt;..&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;BalanceStoreImpl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;store&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;DataStore&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Preferences&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;BalanceStore&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;fetchBalance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="n"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Flow&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;BalanceDto&lt;/span&gt;&lt;span class="p"&gt;?&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;store&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;it&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;type&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;toDto&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="k"&gt;suspend&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;updateBalance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="n"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dto&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;BalanceDto&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;store&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;edit&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="n"&gt;it&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;type&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;dto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toJson&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;BanalceRepositoryImpl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;balanceStore&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;BalanceStore&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;balanaceService&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;BalanceService&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;BanalceRepository&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
   &lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="k"&gt;suspend&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;getBalance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Balance&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="c1"&gt;// 실제론 코드 더 잘 짜야함! &lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;runBlocking&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;balanceStore&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fetchBalance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;type&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;first&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;?:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;balance&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;baservice&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fetchBalance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;type&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;balanceStore&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;updateBalance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="n"&gt;type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;balance&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;balance&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;      
   &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;이름을 좀 더 추상화해서 &lt;code&gt;BalanceRemoteDataSource&lt;/code&gt;, &lt;code&gt;BalanceLocalDataSource&lt;/code&gt; 등으로 만들 수도 있겠다. 그런데 앱 전체적으로 데이터 캐싱하는 대상은 일부분일텐데, &lt;code&gt;RemoteDataSource&lt;/code&gt; 가 등장하는 순간, &lt;code&gt;LocalDataSource&lt;/code&gt; 가 없는 대부분의 api 들도 다 &lt;code&gt;RemoteDataSource&lt;/code&gt; 라고 이름붙여야 할까 고민이 된다. 이건 득실을 따져봐야겠지만, 나는 괜히 이름만 길어지는 것 같아 굳이 그렇게 까지 만든 필요는 없다고 생각한다.&lt;/p&gt;

&lt;h3&gt;
  
  
  캐시 만료/ 무시 처리
&lt;/h3&gt;

&lt;p&gt;캐시를 만드는 순간, 만료 혹은 무시(하고 네트워크 요청) 처리를 신경 써 줘야 한다. 두 기능이 다 필요한 경우도 있겠지만, 대부분은 둘 중 하나의 기능만 필요했다. 예를 들어 로그인한 사용자의 user id 는 대게 만료만 필요하고 (로그아웃), 계좌 잔액은 무시만 필요하다.&lt;/p&gt;

&lt;p&gt;무시는 아직까지 repository 함수에 forceUpdate 정도의 인자를 받는 것 외에 더 깔끔한 방법은 모르겠다. 메모리 캐시에 무시 처리를 한다면 이렇게 만들 수 있겠다.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;BalanceRepositoryImpl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;..&lt;/span&gt;&lt;span class="p"&gt;.)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;BalanceRepository&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
   &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;balanceCache&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;mutableMapOf&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nc"&gt;BalanceDto&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;

   &lt;span class="c1"&gt;// 실무에선 이렇게 암호같이 짜지 말자&lt;/span&gt;
   &lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="k"&gt;suspend&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;getBalance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;forceRefresh&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Boolean&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;
     &lt;span class="p"&gt;{&lt;/span&gt; 
       &lt;span class="n"&gt;balanceCache&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;type&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;takeUnless&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;forceRefresh&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;?:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
         &lt;span class="n"&gt;service&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fetchBalance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;type&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;also&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;balanceCache&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;type&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;it&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
       &lt;span class="p"&gt;}&lt;/span&gt;
     &lt;span class="p"&gt;}.&lt;/span&gt;&lt;span class="nf"&gt;toBalance&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;만료는 좀 더 복잡하다. 예를 들어 사용자가 로그아웃 할 때 모든 사용자 id 캐시를 디스크/ 메모리에서 삭제해야 한다면 어떻게 하면 좋을까? 이 경우 한군데라도 만료를 빼 먹으면 꽤 골치아픈 버그가 생긴다. 현재 내가 생각하는 가장 좋은 구현은 DI 도구등을 이용해 캐시를 가진 구현체들을 주입받아 캐시를 만료하는 방법이다.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="kd"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;UserIdAware&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;expireUserId&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;UserIdCache&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;UserIdAware&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="py"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;

    &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;expireUserId&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
       &lt;span class="n"&gt;userId&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; 

&lt;span class="c1"&gt;// DI로 주입받자&lt;/span&gt;
&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;LogoutUsecase&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;userIdAwares&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;UserIdAware&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;operator&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;invoke&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
       &lt;span class="o"&gt;..&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;
       &lt;span class="n"&gt;userIdAwares&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;it&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;expireUserId&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  정리
&lt;/h2&gt;

&lt;p&gt;안드로이드의 API 응답 캐싱에서, 일관적으로 모든 api 에 캐싱을 적용하겠다면 네트워크 캐싱을 적용해야겠지만, 일부 api 만 캐싱한다면 애플리케이션 레벨에서 캐싱을 하는 게 유지보수하기 더 좋다고 생각한다. 메모리캐시를 사용할 경우엔 DTO 객체를 저장하는 걸 고려해보자. &lt;/p&gt;

</description>
      <category>android</category>
    </item>
    <item>
      <title>2025년에 안드로이드 앱을 만들기</title>
      <dc:creator>Sewon Ann</dc:creator>
      <pubDate>Tue, 11 Mar 2025 02:33:14 +0000</pubDate>
      <link>https://dev.to/kingori/2025nyeone-andeuroideu-aebeul-mandeulgi-50df</link>
      <guid>https://dev.to/kingori/2025nyeone-andeuroideu-aebeul-mandeulgi-50df</guid>
      <description>&lt;p&gt;육아휴직 기념(?)으로 2025년 요즘 기준의 안드로이드 앱 개발 환경을 간단히 소개해본다.&lt;/p&gt;

&lt;h2&gt;
  
  
  빌드 : gradle
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://bazel.build/?hl=ko" rel="noopener noreferrer"&gt;bazel&lt;/a&gt; 얘기가 좀 나오고 있긴 하지면, 여전히 굳건한 대세는 &lt;a href="https://gradle.org/" rel="noopener noreferrer"&gt;gradle&lt;/a&gt; 이다. 대단히 크거나 복잡한 프로젝트가 아니라면 괜히 고생하지 말고 gradle 을 쓰자.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.linkedin.com/pulse/gradle-vs-bazel-what-spotifys-build-system-migration-means-musili-0fj1f/" rel="noopener noreferrer"&gt;Gradle vs. Bazel: What Spotify's Build System Migration Means for App Quality and User Experience&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  빌드 설정: convention plugin
&lt;/h3&gt;

&lt;p&gt;요즘은 하나의 모듈이 아닌, 하나의 앱 모듈과 여러 라이브러리 모듈로 구성하는게 일반적이다. 이러다보면 여러 모듈의 build.gradle 에 동일한 설정이 중복될 수 밖에 없다. 예전엔 그냥 &lt;code&gt;apply from: common.gradle&lt;/code&gt; 이런식으로 다른 gradle 파일을 직접 참고하기도 했지만, 요즘은 좀 더 우아하게 convention plugin 을 만들어서 공유한다.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.gradle.org/current/samples/sample_convention_plugins.html" rel="noopener noreferrer"&gt;Sharing build logic between subprojects Sample&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  의존 관리 : version catalog
&lt;/h3&gt;

&lt;p&gt;여러 모듈의 의존 라이브러리 버전을 잘 관리하기 위해 예전엔 build config 를 쓴다거나 이런 저런 꼼수를 썼으나, 이젠 &lt;a href="https://docs.gradle.org/current/userguide/version_catalogs.html" rel="noopener noreferrer"&gt;version catalog&lt;/a&gt; 가 표준이다. &lt;/p&gt;

&lt;p&gt;여러 모듈에서 동일한 의존들을 줄줄이 적어야한다면, convention plugin 이나, version catalog 의 bundle 기능을 사용하면 깔끔하개 정리할 수 있다. 예를 들어 compose 를 사용하는 모듈이라면 딸려오는 의존들이 매우 많은데, version catalog 에 compose bunlde 을 만들어서 딱 한줄로 해결하거나, compose convention plugin 을 만들어 의존은 plugin 다 넣어버릴 수도 있다. &lt;/p&gt;

&lt;h3&gt;
  
  
  build cache 도입
&lt;/h3&gt;

&lt;p&gt;gradle 은 빌드된 결과물을 캐싱한다. 이를 &lt;a href="https://docs.gradle.org/current/userguide/build_cache.html" rel="noopener noreferrer"&gt;build cache&lt;/a&gt; 라고 한다. 모듈 에 변경이 없을 경우엔 다음 번 빌드 시 해당 모듈을 다시 빌드하지 않고 캐시를 이용해 산출물을 가져오기 때문에 훨씬 빠르다. &lt;/p&gt;

&lt;p&gt;build cache 는 local cache 와 remote cache 로 나뉜다. 혼자 개발한다면야 local cache 만 있어도 충분하겠지만, 여럿이 개발을 한다면 remote cache 를 도입한다면 다른 팀원의 빌드 시간을 줄일 수 있다. remote cache 를 구성하려면 캐시 서버가 있어야 하는데, &lt;a href="https://hub.docker.com/r/gradle/build-cache-node/" rel="noopener noreferrer"&gt;build- cache-node&lt;/a&gt; 라는 도커 이미지를 제공하기 때문에 손쉽게 구성할 수 있다. 다만 당연히 도커 이미지의 스토리지는 어딘가에 영속적으로 남겨져 있어야 캐시로써 의미가 있을것이다.&lt;/p&gt;

&lt;p&gt;remote build cache 의 경우, 보수적인(?) 조직이라면 CI 서버에서만 cache server에 push 하도록 정책을 잡을 수도 있다. &lt;/p&gt;

&lt;h3&gt;
  
  
  빌드 성능 분석 : build-scan
&lt;/h3&gt;

&lt;p&gt;gradle의 &lt;a href="https://docs.gradle.org/current/userguide/build_scans.html" rel="noopener noreferrer"&gt;build scan&lt;/a&gt; 기능을 사용하면 빌드 시간을 더 줄일 수 있는 힌트를 얻을 수 있다. 안드로이드 스튜디오에도 빌드 분석 기능이 있긴 하지만, build scan 은 좀 더 세밀한 분석결과를 제공한다.&lt;/p&gt;

&lt;p&gt;문제는 build scan 의 결과가 로컬이 아닌 gradle 서버에서 제공한다는 것이다. 정확하게 gradle 서버에 뭘 전송하는지는 문서를 더 봐야겠지만, 보안문제가 생길 수 있다. 리포트 지우기 등의 기능도 제공하지만, 회사에서 쓰려면 보안 유지를 위해 문서를 잘 읽어보자. gradle 의 유료 솔루션인 &lt;a href="https://gradle.com/develocity/" rel="noopener noreferrer"&gt;develocity&lt;/a&gt; 를 쓰면 이런 문제를 해결할 수 있지만 비용이...&lt;/p&gt;

&lt;h2&gt;
  
  
  모듈 구성 : feature 별 분리
&lt;/h2&gt;

&lt;p&gt;모듈 구성은 layer 별로 한다 / feature 별로 한다 / 둘 다 한다 등의 논쟁이 많은데 일단 난 feature 파이다. 반박 시 이건 내 글이니 내 말이 맞다. 뭐 그 안에서도 공유되는 모델들도 있고 해서 복잡하겠지만. 여튼 난 feature 를 기준으로 모델을 분리한다.&lt;/p&gt;

&lt;p&gt;대표적인 모듈 분리의 이점은 다음과 같다.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;kotlin 의 internal 접근자를 이용해 내부 구현을 숨길 수 있다. &lt;/li&gt;
&lt;li&gt;build cache 등을 이용해 빌드 속도를 줄일 수 있다.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  네트워킹 - retrofit
&lt;/h2&gt;

&lt;p&gt;요즘은 KMP(kotlin multiplatform) 때문에 &lt;a href="https://ktor.io/" rel="noopener noreferrer"&gt;ktor&lt;/a&gt; 얘기도 나오는 것 같은데, 난 &lt;a href="https://square.github.io/retrofit/" rel="noopener noreferrer"&gt;retrofit&lt;/a&gt; 이 익숙해서 여전히 retrofit 에 한표를 던진다. 물론 KMP를 염두한다면 ktor 로 가보는 것도 좋겠다. 아무거나 상관없다고 생각한다.&lt;/p&gt;

&lt;p&gt;retrofit 을 쓴다면 이제는 당연히 coroutine 을 쓸 것이고, 한 걸음 더 나아가면 반환 결과를 &lt;code&gt;Result&lt;/code&gt; 로 받도록 만들고 싶다. 이건 &lt;a href="https://github.com/skydoves/retrofit-adapters" rel="noopener noreferrer"&gt;retrofit-adapters&lt;/a&gt; 를 적용하면 되는데, 결과적으로 이런 모양이 된다.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="nd"&gt;@GET&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;..&lt;/span&gt;&lt;span class="p"&gt;.)&lt;/span&gt;
&lt;span class="k"&gt;suspend&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;fetchUser&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Result&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;UserDto&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  json 매핑 - kotlinx serialization
&lt;/h2&gt;

&lt;p&gt;API 호출을 하다보면 필연적으로 JSON 과 객체 매핑을 해야하는데, &lt;a href="https://github.com/Kotlin/kotlinx.serialization" rel="noopener noreferrer"&gt;kotlinx serialization&lt;/a&gt; 을 쓰면 된다. kotlin 초창기엔 &lt;a href="https://github.com/square/moshi" rel="noopener noreferrer"&gt;moshi&lt;/a&gt; 등이 언급되었으나, 지금 시점에 moshi 를 선택할 이유는 없다고 본다. &lt;/p&gt;

&lt;p&gt;gson 쓰면 안되냐고 물어본다면 쓰지말라고 하고싶다. gson 의 경우엔 reflection 을 쓰기 때문에 kotlin 의 생성자 호출도 이뤄지지 않고, 이러다보면 몇가지 생각지 못한 문제가 생긴다. 예를 들어 다음과 같은 DTO 객체를 만들었다고 치자.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="kd"&gt;data class&lt;/span&gt; &lt;span class="nc"&gt;UserDto&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;first&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;last&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;full&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;first&lt;/span&gt; &lt;span class="p"&gt;+&lt;/span&gt; &lt;span class="n"&gt;last&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;gson 으로 &lt;code&gt;{ "first": "a", "last": "b" }&lt;/code&gt; 를 매핑한 UserDto를 가져와서 full 변수의 내용을 보면 &lt;code&gt;ab&lt;/code&gt; 가 아니라 null 이 들어있던가, 예외가 발생할거다. 생성자가 호출되지 않기 때문이다. 참고로 위의 내용은 좋은 예시는 아니긴 하다. 애초에 DTO 에 full 같은걸 넣지 말자.&lt;/p&gt;

&lt;h2&gt;
  
  
  영속적 데이터 저장 - jetpack datastore, room
&lt;/h2&gt;

&lt;p&gt;쭉쭉 늘어나는 데이터라면 DB 기반의 &lt;a href="https://developer.android.com/jetpack/androidx/releases/room?hl=ko" rel="noopener noreferrer"&gt;room&lt;/a&gt; 을 , 그렇지 않다면 &lt;a href="https://developer.android.com/topic/libraries/architecture/datastore?hl=ko" rel="noopener noreferrer"&gt;datastore&lt;/a&gt; 를 쓰면 된다. &lt;/p&gt;

&lt;p&gt;datastore 는 shared preference 기반의 구현과 protobuffer 기반의 구현을 &lt;a href="https://developer.android.com/topic/libraries/architecture/datastore#prefs-vs-proto" rel="noopener noreferrer"&gt;제공한다&lt;/a&gt;. protobuffer 가 좀 더 멋져보이긴 하지만, 나중에 migration 을 하다 골치아픈 일이 발생할 수도 있어서, 굳이 써야하나 싶다. 나라면 안쓰겠다.&lt;/p&gt;

&lt;h2&gt;
  
  
  DI - koin
&lt;/h2&gt;

&lt;p&gt;koin 이 DI 냐, DI가 필요하냐 등의 많은 논쟁이 있는데 난 DI는 필요하고 생각하고, &lt;a href="https://dagger.dev/hilt/" rel="noopener noreferrer"&gt;hilt&lt;/a&gt; 보단 &lt;a href="https://insert-koin.io/" rel="noopener noreferrer"&gt;koin&lt;/a&gt; 에 한표를 던진다. KMP 때문에 koin 이 더 힘을 받고 있기도 하고, dagger 시절부터 시작해서 hilt 로 내려온 이 복잡한 설정으로 늘어난 복잡도가 과연 필요한가 싶다. 내가 dagger 포기자이기도 하고(대포자...). 역시 반박 시 여긴 내 글이니 내 말이 맞...&lt;/p&gt;

&lt;p&gt;koin 은 초기엔 런타임에 모든 설정이 이뤄지기 때문에 런타임에 크래시가 날 위험이 있긴 한데, annotation 부터 시작해서 최근엔 아직 베타이긴 하지만 &lt;a href="https://plugins.jetbrains.com/plugin/26131-koin-dependency-injection-official-" rel="noopener noreferrer"&gt;intelliJ 플러그인&lt;/a&gt; 도 나와서 점점 이런 걱정들을 해소해주고 있다. 그리고 완전하진 않지만 단위테스트를 통해 검증하는 방법도 제공해 주고 있어, 쓰기 나름이라고 생각한다. &lt;/p&gt;

&lt;h2&gt;
  
  
  이미지 로더 - coil
&lt;/h2&gt;

&lt;p&gt;이젠 옛 유물이 된 &lt;a href="https://github.com/square/picasso" rel="noopener noreferrer"&gt;picasso&lt;/a&gt; 를 빼고나면, &lt;a href="https://github.com/bumptech/glide" rel="noopener noreferrer"&gt;glide&lt;/a&gt; 와 &lt;a href="https://coil-kt.github.io/coil/" rel="noopener noreferrer"&gt;coil&lt;/a&gt; 정도가 남지 않을까 싶은데 glide는 커스터마이징이 너무 어려워 이젠 그냥 coil 을 쓰는게 맞다고 생각한다.&lt;/p&gt;

&lt;h2&gt;
  
  
  UI - compose
&lt;/h2&gt;

&lt;p&gt;이젠 완전히 &lt;a href="https://developer.android.com/compose" rel="noopener noreferrer"&gt;compose&lt;/a&gt; 세상이 되었다. 하지만 지금도 뜻밖의 버그도 만나게 되고, 뭔가 미묘하게 문제가 발생해서 여전히 필요에 따라 군데군데 xml 기반의 뷰가 필요한 상황이 생긴다. 그래도 강력한 preview 덕분에 UI 개발이 훨씬 편해졌다.&lt;/p&gt;

&lt;h2&gt;
  
  
  View 와 ViewModel 간의 커뮤니케이션 - flow
&lt;/h2&gt;

&lt;p&gt;MVVM , MVI 등 아키텍쳐 이야기는 너무 의견이 다양해서 생략하고, compose 덕분에 이제는 ViewModel 에서 상태를 StateFlow 등으로 노출하고, View 에선 이 상태를 구독한 후 composable 에 던져 적절히 렌더링하는 게 대세이다. 물론 여기에도 토스트 같은 side-effect 를 상태에 포함시킬거냐 등의 수많은 설계 고민거리가 들어갈 수 있겠다. &lt;/p&gt;

&lt;p&gt;나는 &lt;a href="https://github.com/airbnb/mavericks" rel="noopener noreferrer"&gt;mavericks&lt;/a&gt; 를 몇년간 써 오고 있긴 한데, 새로운 프로젝트라면 굳이 mavericks가 필요할까 싶다.&lt;/p&gt;

&lt;h2&gt;
  
  
  코드 품질 관리 - ktlint , konsist
&lt;/h2&gt;

&lt;p&gt;여럿이 개발을 한다면 코딩 컨벤션 통일을 위해 &lt;a href="https://github.com/pinterest/ktlint" rel="noopener noreferrer"&gt;ktlint&lt;/a&gt; 정도는 도입을 해 주는게 좋다. git precommit hook 등에 걸어두자.&lt;/p&gt;

&lt;p&gt;써 보진 않았지만 프로젝트의 아키텍쳐 규칙 위반을 잡아내기 위한 &lt;a href="https://docs.konsist.lemonappdev.com/" rel="noopener noreferrer"&gt;konsist&lt;/a&gt; 도 좋아보인다.&lt;/p&gt;

&lt;h2&gt;
  
  
  단위 테스트 - junit 4
&lt;/h2&gt;

&lt;p&gt;단위 테스트는 여전히 junit 4 기반으로 작성한다. &lt;a href="https://robolectric.org/" rel="noopener noreferrer"&gt;robolectric&lt;/a&gt; 동작도 그렇고, 꼭 junit 5 여야 하는 이유도 크게 느끼지 못해서. &lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.yes24.com/Product/Goods/104084175" rel="noopener noreferrer"&gt;단위테스트&lt;/a&gt; 책을 읽은 후론 &lt;a href="https://dzone.com/articles/two-schools-of-unit-testing" rel="noopener noreferrer"&gt;고전파 단위테스트&lt;/a&gt; 를 지향하려고 한다. 고전파를 지향하려면 최대한 mock 등을 쓰지 않고 실제 구현체를 제공해야 하는데, 만약 구현체가 다른 모듈의 internal class 라면 어떻게 제공하지? 하는 문제가 생긴다. 다행히 AGP 8.5 정도에서부터 kotlin 기반의 test fixture 기능을 제공하기 때문에, 적절히 test fixture 를 만들어두면 테스트 코드끼리 fixture 를 서로 공유할 수 있다. 이를 위해선 &lt;code&gt;android.experimental.enableTestFixturesKotlinSupport=true&lt;/code&gt; 옵션을 build.gradle 에 넣어야하는데, 이 옵션에 대한 설명을 찾기가 정말 힘들다. android 개발 문서 사이트에서도 검색할 수 없고. 나는 대체 이걸 어디서 찾았지?&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://medium.com/@theilacker/implementing-test-fixtures-in-kotlin-android-project-say-goobdye-to-sharedtest-folder-ec51f396f56f" rel="noopener noreferrer"&gt;Test fixtures in Android with Kotlin: share code between unit and instrumentation tests&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;단위 테스트는 아니지만 그 외에 요즘은 스크린샷 테스트나 compose preview 기반의 테스트도 많이 이야기되고 있다. 이건 재밌어보여서 해보고싶다.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://developer.android.com/studio/preview/compose-screenshot-testing?hl=ko" rel="noopener noreferrer"&gt;Compose 미리보기 스크린샷 테스트&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>android</category>
      <category>development</category>
    </item>
    <item>
      <title>IntelliJ의 댜양한 검색기능 - Find 부터 커스텀 inspection rule 까지</title>
      <dc:creator>Sewon Ann</dc:creator>
      <pubDate>Sun, 09 Mar 2025 09:46:43 +0000</pubDate>
      <link>https://dev.to/kingori/intellijyi-dyayanghan-geomsaeggineung-find-buteo-keoseuteom-inspection-rule-ggaji-43o5</link>
      <guid>https://dev.to/kingori/intellijyi-dyayanghan-geomsaeggineung-find-buteo-keoseuteom-inspection-rule-ggaji-43o5</guid>
      <description>&lt;p&gt;코딩을 하다보면 기존 프로젝트의 코드를 찾고, 고쳐야 하는 경우도 많이 생긴다. 그래서 IDE들은 다양한 찾기 / 찾아서 바꾸기 기능을 제공한다. IntelliJ 도 댜앙한 찾기 / 바꾸기 기능을 제공하는데, &lt;code&gt;Find Usage&lt;/code&gt; 를 제외한 검색 기능은 범위와 수준에 따라 아래와 같이 구분할 수 있다.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;현재 파일 내부에서 찾기 / 바꾸기&lt;/li&gt;
&lt;li&gt;전체 파일들에서 찾기 / 바꾸기&lt;/li&gt;
&lt;li&gt;구조적 찾기 / 바꾸기&lt;/li&gt;
&lt;li&gt;커스팀 플러그인으로 inspection 을 만들어 찾기&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;각 기능을 간단히 소개해본다.&lt;/p&gt;

&lt;h1&gt;
  
  
  현재 파일 내부에서 찾기 / 바꾸기
&lt;/h1&gt;

&lt;p&gt;맥 기준으로 cmd + f / cmd + r 로 현재 선택된 파일 내부에서 문구를 찾고, 바꿀 수 있다. 졍규표현식도 사용할 수 있다. 단순한 기능이라 딱히 더 설명할 내용이 없다. &lt;/p&gt;

&lt;h1&gt;
  
  
  전체 파일들에서 찾기 / 바꾸기
&lt;/h1&gt;

&lt;p&gt;cmd + shift + f / cmd + shift + r 을 누르면 지정한 scope 내부에서 문구를 찾고, 바꿀 수 있다. 알아두면 좋을 유용한 scope 으로 &lt;code&gt;Files in previous search result&lt;/code&gt; 이 있다. 말 그대로 이전 검색 결과 내에서 다시 검색을 하는 기능이다. 예를 들어 &lt;strong&gt;Abc 라는 인터페이스의 구현체 중에서 &lt;code&gt;@GET&lt;/code&gt; 이 붙어있는 함수와 &lt;code&gt;@POST&lt;/code&gt; 가 붙어있는 함수가 모두 포함된 파일&lt;/strong&gt; 을 찾아야한다면, 간단히 Abc 인터페이스 구현체를 검색하고, 검색 결과를 가진 파일 중에서 &lt;code&gt;@GET&lt;/code&gt; 을 검색한 후, 다시 그 결과중에서 &lt;code&gt;@POST&lt;/code&gt; 를 검색한 결과 파일들만 뒤져보면 된다.&lt;/p&gt;

&lt;h1&gt;
  
  
  구조적 찾기 / 바꾸기 (Search Structurally /Replace Structurally)
&lt;/h1&gt;

&lt;p&gt;이 기능은 굉장히 강력한 만큼, 쓰기도 어렵다. &lt;a href="https://www.jetbrains.com/help/idea/structural-search-and-replace.html" rel="noopener noreferrer"&gt;IntelliJ 도움말 문서의 항목&lt;/a&gt; 을 봐도 하위 항목이 있을 정도로 복잡하다. 맨 땅에서 시작하긴 정말 어려운데, 다행히 기능을 열어보면 여러 템플릿들이 보인다. &lt;/p&gt;

&lt;p&gt;템플릿을 선택해서 적절히 modifier 를 조정하면 원하는 검색 결과를 얻을 수 있다. 예를 들어 클래스에 선언된 코틀린 함수 중 이름에 x 가 들어가고, 인자의 갯수가 1개에서 3개 사이이면서 반환 타입은 없는 함수를 찾는다면? 템플릿에서 Kotlin &amp;gt; Class-based &amp;gt; All methods of a class 를 선택한 다음, $ReturnalType$ 은 지우고 $Method$ 엔 Text modifier 를 추가하고, modifier 의 내용엔 정규표현식인 &lt;code&gt;.*x.*&lt;/code&gt; 를 넣으면 된다. &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F36dmsnylbucu8xmcirnv.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F36dmsnylbucu8xmcirnv.png" alt="Image description" width="800" height="443"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;이렇게 만든 템플릿은 inspection rule 로 저장할 수도 있다.&lt;/p&gt;

&lt;p&gt;modifier 에는 groovy &lt;a href="https://www.jetbrains.com/help/idea/search-templates.html#script_constraints" rel="noopener noreferrer"&gt;script&lt;/a&gt; 도 넣을 수 있다. script가 또 굉장히 강력해서 단순히 대소문자 치환 정도가 아니라 &lt;a href="https://plugins.jetbrains.com/docs/intellij/psi-elements.html" rel="noopener noreferrer"&gt;PSI Eleements&lt;/a&gt; 를 이용해 이 항목이 변수인지 상수인지 등의 복잡한 판단도 가능하다.&lt;/p&gt;

&lt;h2&gt;
  
  
  커스팀 플러그인으로 inspection 을 만들어 찾기
&lt;/h2&gt;

&lt;p&gt;구조적 찾기 / 바꾸기에 script modifier 까지 사용하면 다 될 것 같지만 여기에도 한계가 존재한다. 이번에 내가 하고 싶었던 작업은 &lt;strong&gt;모든 Koin.get() 함수 호출 중, 타입 인자가 특정 타입이면서 타입 인자가 생략된 경우를 찾고, 생략된 타입 인자를 추가하기&lt;/strong&gt; 였다. 일단 모든 Koin.get() 를 찾는 건 단순히 find usage 만으로도 가능하다. 그런데 생략된 타입 인자를 찾아서, 채워넣는 부분이 문제였다. &lt;/p&gt;

&lt;p&gt;구조적 바꾸기에서 PSI tree 를 이용하면 되지 않을까 싶었는데, 이 경우 타입추론으로 생략된 실제 T 타입을 알아내는 부분에서 막혔다. 실제 타입을 알아내기 위해선 &lt;code&gt;BindingContext&lt;/code&gt; 객체가 필요한데, 구조적 검색에선 여기까진 불가능했다. 아니면 단순히 내가 방법을 찾지 못했을 수도 있다. 이렇게 생략된 타입 인자까지 얻어내기 위해선 결국 커스텀 IntelliJ 플러그인을 만들고, 플러그인에서 커스텀 inspection rule 을 만들면 된다. &lt;/p&gt;

&lt;p&gt;커스텀 IntelliJ 플러그인을 만드는 것도 성가신 일이긴 한데, 일회성 플러그인이라면 &lt;a href="https://plugins.jetbrains.com/plugin/7282-liveplugin" rel="noopener noreferrer"&gt;LivePlugin&lt;/a&gt; 을 이용하면 간단히 추가할 수 있다. 이 플러그인은 내부에 간단한 플러그인을 만들어넣을 수 있는 플러그인이다. 플러그인 제공 플러그인이라고 해야 하나? 그리고 이 플러그인에 내장된 샘플 중 커스텀 inspection rule 플러그인도 제공하므로 빠르게 시작해 볼 수 있다. &lt;/p&gt;

&lt;p&gt;실제 플러그인은 copilot 을 이용해 만들어냈다. 대강 "Koin.get() 함수 중 타입 인자가 생략된 호출을 찾아낸 다음, 생략된 타입 인자를 명시적으로 추가해주는 IntelliJ 플러그인을 작성해줘" 라고 입력하니 잘 만들어줬다. 한번에 실행에 성공하진 못했고, import 문이나 몇 줄 정도는 수정해줬지만 잘 동작했다.&lt;/p&gt;

&lt;p&gt;LivePlugin 을 이용해 작성한 plugin 을 실행해서 등록한 후, IntelliJ 의 inspection 기능에서 추가된 inspection rule 을 활성화한 후 code inspection을 실행하니 원하는 검색 결과를 얻을 수 있었다.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgwx4fz5q2gx3hqisaytr.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgwx4fz5q2gx3hqisaytr.png" alt="Image description" width="800" height="556"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  구조적 찾기의 한계 - 언제 plugin 이 필요할까
&lt;/h1&gt;

&lt;p&gt;정확하진 않지만, 찾고자 하는 코드의 맥락만으로 판단할 수 있는 검색 조건이라면 구조적 찾기로 가능한 것 같고, 그렇지 않고 타입 추론 등이 필요한 더 복잡한 경우는 커스텀 플러그인까지 만들어야 하는 것 같다.&lt;/p&gt;

&lt;p&gt;또 한가지, 커스텀 플러그인의 경우 quick-fix 를 제공할 순 있지만 한번에 찾아서 바꾸는 건 안되는 것 같다. 일일이 inspection 결과에서 눌러서 수정해줘야 한다.&lt;/p&gt;

</description>
      <category>intellij</category>
    </item>
    <item>
      <title>Android Studio 에서 Compose 의 Preview 가 남긴 로그 보기</title>
      <dc:creator>Sewon Ann</dc:creator>
      <pubDate>Wed, 27 Nov 2024 02:45:28 +0000</pubDate>
      <link>https://dev.to/kingori/android-studio-eseo-compose-yi-preview-ga-namgin-rogeu-bogi-18jp</link>
      <guid>https://dev.to/kingori/android-studio-eseo-compose-yi-preview-ga-namgin-rogeu-bogi-18jp</guid>
      <description>&lt;p&gt;Compose의 프리뷰 기능은 매우 유용하다. 내가 짠 compose 코드를 수행해서 바로 확인할 수 있어 작업 능률을 크게 높여준다. 그렇다면 프리뷰를 실행하는 과정에서 로그를 남기고, 이걸 확인할 수도 있지 않을까? 가능하다.&lt;/p&gt;

&lt;p&gt;다만 프리뷰가 실행되는 환경이 안드로이드 상이 아니니, 일반적인 로깅 방법인 Timber 나 android Logger 를 쓸 수 없다. 대신 system out 으로 출력하는 &lt;code&gt;print()&lt;/code&gt; 를 쓰면 된다.&lt;/p&gt;

&lt;p&gt;출력된 내용은 IDE 의 system out 출력에 포함된다. 그럼 이 내용은 어디서 볼까? IDE의 기능인 &lt;code&gt;Show Log in Finder&lt;/code&gt; 로 볼 수 있다. &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fasrocbdwwtcde2or5ovz.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fasrocbdwwtcde2or5ovz.png" alt="Image description" width="800" height="220"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;이 기능을 실행하면 파인더에서 log 파일이 선택된다. 이 파일을 vscode 등의 텍스트 편집기로 열면 이 안에 내가 preview를 포함한 compose 코드에 print 함수로 남긴 로그가 보인다. &lt;/p&gt;

&lt;p&gt;확인을 다 마쳤다면 &lt;code&gt;print()&lt;/code&gt; 로 임시로 찍은 로그는 삭제해주자.&lt;/p&gt;

</description>
      <category>android</category>
    </item>
    <item>
      <title>import * 을 쓰지 말아야 할 또 하나의 이유</title>
      <dc:creator>Sewon Ann</dc:creator>
      <pubDate>Wed, 17 Mar 2021 01:28:45 +0000</pubDate>
      <link>https://dev.to/kingori/import-5531</link>
      <guid>https://dev.to/kingori/import-5531</guid>
      <description>&lt;p&gt;import * (wildcard import)는 가급적 쓰지 말라고 얘기한다. 일일이 import 하면 명확히 내가 뭘 갖다 쓰는지 알 수 있고, 엉겹결에 의도치 않은 녀석이 딸려와서 호출되는 잠재적인 오류도 막을 수 있다.&lt;/p&gt;

&lt;p&gt;그리고 어제, 순환 참조 문제를 바꾸기 위해 이 모듈에서 저 모듈로 클래스를 옮기는 과정에서 * 을 쓰지 말아야 할 또 하나의 이유를 찾았다. &lt;/p&gt;

&lt;p&gt;import * 을 해 버리면 replace all 로 내가 옮긴 클래스의 import 문만 기계적으로 바꿔버릴 수가 없다. 물론 대부분의 move 리펙터링은 IDE 를 이용해서 하기 때문에 문제가 없지만, 어제와 같이 대량의 move를 할 땐 하나하나 옮기는 것 보다 무식하게 그냥 cut &amp;amp; paste 해 버리고 import문만 replace all 하는게 훨씬 빠르다. 클래스 하나 옮길 때 마다 안드로이드 스튜디오의 import optimize가 도는데 너무 느려서 내가 돌아버릴 지경.&lt;/p&gt;

&lt;p&gt;import * 을 안했다면 replace all 기능을 이용해 내가 옮긴 클래스의 import 문을 기계적으로 바꿔버릴 수 있다. 예를 들어 &lt;code&gt;com.me.A&lt;/code&gt; 클래스를 &lt;code&gt;com.you.A&lt;/code&gt; 로 옮겼다면, &lt;code&gt;import com.me.A&lt;/code&gt; 를 싹 &lt;code&gt;import com.you.A&lt;/code&gt; 로 바꾸면 끝난다.&lt;/p&gt;

&lt;p&gt;하지만 import * 을 해서 해당 클래스를 사용하고 있었다면, replace 조건에 걸리지 않기 때문에 일일이 찾아다니면서 바꿔줘야 한다. 그런 클래스의 import 문에는 &lt;code&gt;import com.me.*&lt;/code&gt; 만 있지, &lt;code&gt;import com.me.A&lt;/code&gt; 가 존재하지 않기 때문이다.&lt;/p&gt;

&lt;p&gt;그러니 적어도 플랫폼쪽 클래스 같이 내가 바꿀 일이 거의 없는 클래스라면 wildcard import를 조금이라도 고려할 이유가 있겠지만, 내가 짜는 클래스라면 wildcard import를 하지 말자.&lt;/p&gt;

&lt;p&gt;안드로이드 스튜디오를 쓴다면, settings &amp;gt; code style &amp;gt; kotlin (java) 메뉴에서 간단히 기본 설정을 바꿀 수 있다.&lt;/p&gt;

</description>
      <category>android</category>
      <category>kotlin</category>
      <category>java</category>
    </item>
    <item>
      <title>안드로이드 개발팀 내 공통 기능 제공하는 방법에 대한 고민</title>
      <dc:creator>Sewon Ann</dc:creator>
      <pubDate>Tue, 12 Jan 2021 04:22:13 +0000</pubDate>
      <link>https://dev.to/kingori/-3lmp</link>
      <guid>https://dev.to/kingori/-3lmp</guid>
      <description>&lt;p&gt;나는 안드로이드 개발팀 내에서 공통 기능 제공을 맡고있다. 각 사업별 라이브러리 모듈이 필요로 하는 기능을 제공해야 한다.&lt;/p&gt;

&lt;p&gt;다른 모듈에 기능을 제공하는 방법은 정말 다양하다. 내가 생각한 기능 제공 방법과, 그 중에서 AAC ViewModel 로 기능을 제공하려다 난관에 봉착한 얘기를 해보겠다.&lt;/p&gt;

&lt;p&gt;생각나는대로 다른 팀, 모듈에 기능을 제공하는 방법을 생각해봤다.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;SDK : 가장 거창하고, 거대하고, 손이 많이 가는 방식이라고 볼 수 있겠다. SDK 라 하면 대부분 초기화 과정에서 온갖 옵션을 설정해서 SDK client 인스턴스를 만들어내고, 그 인스턴스가 제공하는 메서드들을 이용해 기능을 제공받는다. 그런데 같은 프로젝트 내부라면 SDK 는 너무 거창하고 불편하겠지.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;UseCase : SDK와 반대로, 생각해볼 수 있는 가장 작은 단위의 기능 제공 형태이다. 사용자는 Dagger 나 Koin 등의 DI 도구를 이용해 자신의 VM 등등에서 UseCase 를 주입받아 사용하고, UseCase 가 제공하는 단 하나의 펑션의 결과를 이용해 원하는 결과를 받는다. UI 관련 작업은 못하겠지.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Repository : UseCase가 개별 기능을 제공한다면, Repository 는 몇개의 기능을 묶어서 제공한다. UserRepository, CarRepository 등등. 내가 가장 많이 제공하는 형태이다. DI를 통해 사용측에서 가져다 쓴다.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;그럼 UI가 엮인 기능은 어떻게 제공할까? 커스텀 뷰 하나로 끝나는게 아니라 화면 전체를 제공해야 한다면 Activity 나 Fragment 를 제공해야 한다. 사용하는 측에선 startActivity 를 하거나, 자신의 Activity 에서 Fragment 를 add해서 활용한다. Activity의 경우엔 호출할 때 intent 에 몇가지 인자를 넣어서 입맞에 맞게 동작을 바꿀 순 있지만 제약이 많고, Fragment 의 경우엔 Fragment 의 함수를 호출할 순 있지만, 안드로이드 특성 상 시스템이 Fragment를 언제 재생성할 지 모르기 때문에 안전하게 만든다면 Activity와 마찬가지로 Fragment 의 argument 를 이용하는 수준에서 커스터마이징할 수 밖에 없다. &lt;/p&gt;

&lt;p&gt;굉장히 복잡한 로직이 담긴 다이얼로그를 제공하게 되었는데, 어떻게 제공할 수 있을까 고민했다. 쓰는 쪽에서도 내부 로직 일부를 커스텀해야 하기 때문에 단순 intent 나 argument bundle 수준으로 대응할 수 없었다. &lt;/p&gt;

&lt;p&gt;그럼 ViewModel 과 Fragment 를 함께 제공하면 어떨까? 라는 생각에 도달했다. Fragment 는 UI 를 제공하고, ViewModel 을 통해서 기능을 제공하고, 필요한 경우 로직을 커스터마이징 한다. ViewModel 은 사용하는 측의 Activity/ Fragment 에서 생성하고, 내가 만든 Fragment 는 그 ViewModel 을 그대로 사용하면 된다. activity 레벨의 ViewModel로 만들면 가능하다.&lt;/p&gt;

&lt;p&gt;로직을 커스터마이징해야 한다면, 인터페이스의 구현체를 직접 넣으라고 만들면 된다. 대충 이런 식이다. Koin 을 쓴다고 가정한다.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="c1"&gt;//사용측&lt;/span&gt;
&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CustomerActivity&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;featureViewModel&lt;/span&gt; &lt;span class="k"&gt;by&lt;/span&gt; &lt;span class="n"&gt;viewModel&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;FeatureViewModel&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nf"&gt;parametersOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nc"&gt;CustomLogicImpl&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nf"&gt;onCreate&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
     &lt;span class="n"&gt;featureViewModel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;someLiveData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;observe&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="o"&gt;..&lt;/span&gt;&lt;span class="p"&gt;.}&lt;/span&gt;

     &lt;span class="nc"&gt;FeatureDialogFragment&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;show&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;//기능 제공측&lt;/span&gt;
&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;FeatureDialogFragment&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;featureViewModel&lt;/span&gt; &lt;span class="k"&gt;by&lt;/span&gt; &lt;span class="n"&gt;sharedViewModel&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;FeatureViewModel&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;()&lt;/span&gt;

  &lt;span class="nf"&gt;onCreate&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="n"&gt;featureViewModel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;doSomething&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;  
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;이렇게 하니, 사용측에서 요구하는 로직 커스터마이징도 지원하고, UI도 제공할 수 있었다.&lt;/p&gt;

&lt;p&gt;하지만 위 사용측 코드는 자칫 잘못하면 메모리릭이 나기 쉽다. 예를 들어 위 코드를 아래와 같이 익명 내부 클래스로 짜게 되면, 액티비티 메모리릭이 날 수 있다. 그 이유는 액티비티보다 수명이 긴 뷰모델이 액티비티에 대한 참조를 들고 있기 때문이다.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CustomerActivity&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;featureViewModel&lt;/span&gt; &lt;span class="k"&gt;by&lt;/span&gt; &lt;span class="n"&gt;viewModel&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;FeatureViewModel&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nf"&gt;parametersOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="k"&gt;object&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;CustomLogic&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="o"&gt;..&lt;/span&gt;&lt;span class="p"&gt;.}&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;//이러면 못써!&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;어제부터 이 문제로 고민하고 있는데, 현재 내린 결론은 그냥 저런 방식으로 기능을 제공하면 잠재적인 문제가 발생할 소지가 많으니 하지 말자이다. 만약 위와 같이 UI 도 제공하고, 커스터마이징 가능한 로직도 제공해야 한다면 어떻게 해야 할까? 지금 드는 생각은 그냥 abstract Fragment 와 abstract ViewModel 을 제공해서, 사용측에서 알아서 커스터마이징 부분을 채워넣으라고 하는 방법 밖에 모르겠다. 라이프사이클이 얽혀들어가면 모든 문제가 백배 어려워지는 듯. ㅠㅠ&lt;/p&gt;

&lt;p&gt;참고로 하나의 DI 컨테이너 안에서 돌아가는 경우라면 scope을 잘 조정해서 원하는 ViewModel 구현체를 Fragment 에서 주입받도록 구현할 수도 있다. Koin의 경우 Qualifier를 사용하면 된다. 이 경우엔 ViewModel 을 abstract하게 만들어서 알아서 구현부분을 채워넣으라고 하고, 이 ViewModel을 Fragment에서 사용하면 된다. 하지만 내 프로젝트의 경우엔 한 프로젝트 내부에서 여러개의 Koin Application을 사용하기에, 단순한 Qualifier로도 해결이 되지 않아 이 방식도 쓰지 못한다. 어떻게 어떻게 해결을 한다손 치더라도 배보다 배꼽이 더 커질 것 같아 포기했다.&lt;/p&gt;

</description>
      <category>android</category>
    </item>
    <item>
      <title>과연 Android Gradle Plugin 4.1.0 에선 Desugaring 을 써도 되는걸까?</title>
      <dc:creator>Sewon Ann</dc:creator>
      <pubDate>Tue, 08 Dec 2020 02:19:52 +0000</pubDate>
      <link>https://dev.to/kingori/android-gradle-pluing-4-1-0-desugaring-pel</link>
      <guid>https://dev.to/kingori/android-gradle-pluing-4-1-0-desugaring-pel</guid>
      <description>&lt;p&gt;&lt;strong&gt;한줄 요약&lt;/strong&gt; : AGP(Android Gradle Plugin) 4.1.0 이상부턴 안심하고 java8 desugar 기능을 써도 될 것 같다.&lt;/p&gt;

&lt;h2&gt;
  
  
  Java8 Api Desugar
&lt;/h2&gt;

&lt;p&gt;안드로이드는 특정 OS 이상에서만 java 8을 제대로 지원한다. 예를 들어 &lt;code&gt;java.time&lt;/code&gt; 패키지의 &lt;a href="https://developer.android.com/reference/java/time/package-summary?hl=en"&gt;javadoc&lt;/a&gt; 을 보면, 최소 지원 버전이 API 26 ( Android 8.0 오레오 ) 인 것을 확인할 수 있다. 이러한 불편함을 해소하기 위해 빌드 도구에 &lt;a href="https://developer.android.com/studio/write/java8-support"&gt;Java 8+ Api desugaring 지원&lt;/a&gt; 기능을 추가하였다. 구형 OS의 sdk에 없는 java 8 관련 코드를 앱 빌드에 심어줘서 앱 입장에선 OS 구분 없이 API를 사용할 수 있게된다.&lt;/p&gt;

&lt;p&gt;Kotlin을 주력으로 쓰는 입장에서 다른 API는 별 흥미가 없지만, &lt;code&gt;java.time&lt;/code&gt; 은 꽤 매력적이다. 기존엔 이 API를 쓰려면 &lt;a href="https://github.com/JakeWharton/ThreeTenABP"&gt;ThreeTenABP&lt;/a&gt; 를 써야 하는데, desugaring의 은총을 입으면 별도 라이브러리 없이도 그냥 &lt;code&gt;java.time&lt;/code&gt; 을 쓸 수 있게 된다. &lt;/p&gt;

&lt;h2&gt;
  
  
  AGP 4.0.0 의 버그
&lt;/h2&gt;

&lt;p&gt;하지만 기존 AGP 4.0.0 버전에선 문제가 있었다. 마시멜로 이하 단말에서 특정 클래스에 접근할 때 &lt;code&gt;NoClassDefFoundError&lt;/code&gt; 가 발생한다. 이 이슈는 &lt;a href="https://issuetracker.google.com/issues/157681341?fbclid=IwAR2f2EJk-P4Se_cUPVfWLLM30QtZCbN4RzpQ2Zu-3K4jdF8p_ktCItjDBeU"&gt;구글 이슈 트래커&lt;/a&gt; 에 잘 정리되어 있다. 회사 프로젝트에서도 이 문제가 발생해서 들어내는 작업을 했었다.&lt;/p&gt;

&lt;p&gt;그럼 이젠 문제가 해결되었을까 궁금했다. 위 이슈엔 친절히 문제가 발생하는 repo의 링크가 걸려있어 실험을 해 봤다.&lt;/p&gt;

&lt;h2&gt;
  
  
  AGP 4.1.0 실험
&lt;/h2&gt;

&lt;p&gt;링크의 repo를 받아 그대로 빌드해서 실행해봤다. 예상대로 앱이 뜨자마자 크래시가 난다. 그럼 AGP 4.1.0 을 적용해보면 어떨까? 그런데 링크 repo의 기반 환경이 꽤 예전 것이라 AGP 4.1.0 을 바로 적용하기 어려워 삽질을 좀 한 끝에 아래 두 가지 환경에서 테스트 해 봤다.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;kotlin 1.4.20 / gradle 6.7.1 / desugar 1.0.9 / &lt;strong&gt;AGP 4.1.0-alpha09&lt;/strong&gt; : &lt;strong&gt;크래시 발생&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;kotlin 1.4.20 / gradle 6.7.1 / desugar 1.0.9 / &lt;strong&gt;AGP 4.1.0&lt;/strong&gt; : &lt;strong&gt;실행 성공&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  결론
&lt;/h2&gt;

&lt;p&gt;다른 부분은 desugaring 에 큰 영향을 미치지 않는다고 가정할 때, AGP 4.1.0 에선 해당 버그가 &lt;strong&gt;진짜로&lt;/strong&gt; 수정되었다고 가정할 수 있다. &lt;code&gt;java.time&lt;/code&gt; 쓰러 가즈아! &lt;/p&gt;

</description>
      <category>android</category>
    </item>
  </channel>
</rss>
