<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>버미</title>
    <link>https://bum2.tistory.com/</link>
    <description></description>
    <language>ko</language>
    <pubDate>Mon, 22 Jun 2026 17:09:15 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>Bum_2</managingEditor>
    <item>
      <title>[안드로이드] Compose Navigation 3 개괄적 이해</title>
      <link>https://bum2.tistory.com/208</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;기존에 사용하던 &lt;a href=&quot;https://bum2.tistory.com/139&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Navigation 2(NavHost + NavController + NavGraph)&lt;/a&gt;에 익숙한 입장에서, 2025년 11월에 1.0 안정 버전이 출시된 Jetpack Navigation 3(이하 Nav3)는 어떤 점이 달라졌고, 왜 새로 설계됐는지, 그리고 실제로 어떻게 사용하는지 살펴보고자 한다.&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;Nav3가 등장한 이유&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;Nav2는 2018년에 설계된 라이브러리다. Compose가 세상에 나오기도 전이다. 그래서 &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;Nav2는 본질적으로&lt;/b&gt; &lt;b&gt;명령형(imperative)&lt;/b&gt; &lt;b&gt;패러다임&lt;/b&gt;&lt;/span&gt;을 따른다.&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;navController.navigate(&quot;home&quot;) &lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;navController.popBackStack()&lt;/span&gt;&lt;/blockquote&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;위 코드는 &quot;컨트롤러에게 명령을 내리면 &amp;rarr; 내부 백스택이 바뀌고 &amp;rarr; UI가 갱신된다&quot;는 흐름이다. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;문제는 Compose가 모든 영역에서 &lt;b&gt;상태 기반(state-driven)&lt;/b&gt; 모델을 채택하고 있다는 점&lt;/span&gt;이다. mutableStateOf, remember, collectAsState 등 Compose의 모든 도구는 &quot;상태가 바뀌면 UI가 다시 그려진다&quot;는 단방향 흐름을 따른다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;결국 &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;Compose 앱 안에서 내비게이션만 다른 패러다임으로 동작&lt;/b&gt;&lt;/span&gt;하는 구조였고, 이것은 다음과 같은 실질적 문제를 만들었다.&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;백스택이 라이브러리 내부에 숨겨져 있어서 직접 조작이 어려움&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;인자 전달이 라우트 문자열 파싱 기반이라 타입 안전성이 약함&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&quot;한 화면에 한 destination&quot; 가정 때문에 태블릿/폴더블의 list-detail 같은 적응형 레이아웃 구현이 어색함&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;ViewModel 스코핑이 라이브러리 내부에 숨어 있어 디버깅이 어려움&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;테스트하려면 인스트루먼트 테스트가 필요&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;Nav3는&lt;/b&gt; 이 문제들을 해결하기 위해 &lt;b&gt;백스택을 개발자에게 완전히 노출시키는 방향&lt;/b&gt;으로 다시 설계됐다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;Nav2와 Nav3의 개념 대응&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;기존 Nav2의 요소를 그대로 매핑해보면 다음과 같다.&lt;/span&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;div&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style12&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;span&gt;Nav3&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;Nav2&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;NavHost&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;NavDisplay&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;화면을 그려주는 컨테이너 Composable&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;NavController&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;backStack (리스트 그 자체)&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;화면 이동 / 백스택 조작&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;NavGraph&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;entryProvider&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;키 &amp;rarr; 화면 매핑 정의&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;라우트 문자열 &quot;product/{id}&quot;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;NavKey를 구현한 데이터 클래스&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;화면 식별자 + 인자&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;핵심은&lt;/span&gt; NavController라는 별도의 객체가 사라지고,&lt;/b&gt; &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;백스택 리스트 자체가 곧 내비게이션 상태&lt;/b&gt;&lt;/span&gt;가 됐다는 점이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;Nav3의 구성 요소&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt; 본격적으로 살펴보기 전에, Nav3에서 등장하는 핵심 요소들을 먼저 정리하면 다음과 같다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 168px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style12&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;width: 15.4651%; height: 21px; text-align: center;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;요소&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 29.3023%; height: 21px; text-align: center;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;타입 / 형태&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 30.2326%; height: 21px; text-align: center;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;역할&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 25%; height: 21px; text-align: center;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;Nav2&amp;nbsp;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 42px;&quot;&gt;
&lt;td style=&quot;width: 15.4651%; height: 42px; text-align: center;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;NavKey&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 29.3023%; height: 42px;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;인터페이스 (데이터 클래스로 구현)&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 30.2326%; height: 42px;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;화면 식별, 인자&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 25%; height: 42px;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;라우트 문자열 &quot;product/{id}&quot;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 42px;&quot;&gt;
&lt;td style=&quot;width: 15.4651%; height: 42px; text-align: center;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;backStack&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 29.3023%; height: 42px;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;SnapshotStateList&amp;lt;NavKey&amp;gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 30.2326%; height: 42px;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;현재 쌓여 있는 화면들의 리스트(상태)&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 25%; height: 42px;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;NavController 내부 백스택&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 42px;&quot;&gt;
&lt;td style=&quot;width: 15.4651%; height: 42px; text-align: center;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;NavDisplay&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 29.3023%; height: 42px;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;Composable&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 30.2326%; height: 42px;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;백스택을 관찰해 마지막 항목을 그리는 컨테이너&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 25%; height: 42px;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;NavHost&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;width: 15.4651%; height: 21px; text-align: center;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;entryProvider&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 29.3023%; height: 21px;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;람다 (DSL)&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 30.2326%; height: 21px;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;키를 통한 화면 매핑&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 25%; height: 21px;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;NavGraph&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;NavKey&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;화면을 식별하는 키&lt;/b&gt;다. 라우트 문자열 대신 데이터 클래스로 정의한다.&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;@Serializable
data object Home : NavKey

@Serializable
data class ProductDetail(val productId: String) : NavKey

@Serializable
data class Checkout(
    val items: List&amp;lt;CartItem&amp;gt;,
    val couponCode: String?
) : NavKey&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;NavKey 인터페이스 구현 &amp;rarr; 라이브러리에 &quot;이 키는 저장 가능하다&quot;고 알림&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;@Serializable &amp;rarr; 프로세스 죽음(process death) 후에도 복원 가능&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;문자열 라우트 시절에는 arguments?.getString(&quot;productId&quot;)처럼 꺼내야 했던 인자가, Nav3에서는 그냥 key.productId로 접근된다. 컴파일러가 타입을 검증해주고, IDE의 리팩토링 기능도 완벽하게 동작한다.&lt;/span&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;backStack&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;현재 쌓여 있는 화면들의 리스트다. &lt;b&gt;정확히는 SnapshotStateList&amp;lt;NavKey&amp;gt;&lt;/b&gt;다.&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;pre class=&quot;isbl&quot;&gt;&lt;code&gt;val backStack = rememberNavBackStack(Home)&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;rememberNavBackStack은 구성 변경과 프로세스 죽음을 가로질러 백스택을 유지해주는 편의 함수다. 초기값으로 넣은 Home이 Nav2의 startDestination 역할을 한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;이 리스트는 &lt;b&gt;개발자가 직접 소유하고 직접 조작&lt;/b&gt;한다.&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;backStack.add(ProductDetail(&quot;abc&quot;))   // navigate
backStack.removeLastOrNull()           // popBackStack
backStack.clear()                      // 백스택 비우기&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;NavDisplay&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;백스택을 관찰하다가 마지막 항목을 그려주는 Composable이다. Nav2의 NavHost에 해당한다.&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;NavDisplay(
    backStack = backStack,
    onBack = { backStack.removeLastOrNull() },
    entryProvider = entryProvider {
        // ...
    }
)&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;onBack은 시스템 백버튼을 눌렀을 때 실행되는 람다다. 보통 마지막 항목을 제거한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;entryProvider&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;키 &amp;rarr; 화면 매핑을 정의한다. Nav2의 NavGraph에 해당한다.&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;entryProvider = entryProvider {
    entry&amp;lt;Home&amp;gt; {
        HomeScreen()
    }
    entry&amp;lt;ProductDetail&amp;gt; { key -&amp;gt;
        ProductDetailScreen(productId = key.productId)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;entry&amp;lt;T&amp;gt; { ... } DSL은 내부적으로 when (key) { is T -&amp;gt; ... } 패턴매칭을 깔끔하게 표현한 것이다. 람다의 key 파라미터는 자동으로 해당 타입으로 추론된다.&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;기본 사용 예제&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;위 요소들을 모두 합친 최소한의 코드는 다음과 같다.&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;@Serializable
data object Home : NavKey
@Serializable
data class ProductDetail(val productId: String) : NavKey

@Composable
fun App() {
    val backStack = rememberNavBackStack(Home)

    NavDisplay(
        backStack = backStack,
        onBack = { backStack.removeLastOrNull() },
        entryProvider = entryProvider {
            entry&amp;lt;Home&amp;gt; {
                HomeScreen(
                    onProductClick = { id -&amp;gt;
                        backStack.add(ProductDetail(id))
                    }
                )
            }
            entry&amp;lt;ProductDetail&amp;gt; { key -&amp;gt;
                ProductDetailScreen(
                    productId = key.productId,
                    onBack = { backStack.removeLastOrNull() }
                )
            }
        }
    )
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;흐름은 다음과 같다.&lt;/span&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;사용자가 HomeScreen에서 상품을 탭&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;backStack.add(ProductDetail(&quot;abc&quot;)) 실행&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;backStack은 SnapshotStateList이므로 변경을 Compose가 감지&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;NavDisplay가 리컴포지션되면서 리스트의 마지막 항목을 확인&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;entryProvider에서 ProductDetail 타입에 매칭되는 람다 실행&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;ProductDetailScreen(&quot;abc&quot;)가 화면에 렌더링됨&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;별도의 컨트롤러도, 그래프 탐색도 없다. &lt;b&gt;상태 변경 &amp;rarr; UI 갱신&lt;/b&gt;이라는 Compose의 기본 패턴 그대로다.&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;백스택 조작 &amp;mdash; popUpTo, launchSingleTop은 어떻게?&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;Nav2의 DSL(popUpTo, inclusive, launchSingleTop 등)이 모두 사라졌다. 대신 백스택이 리스트이므로 &lt;b&gt;Kotlin Collection API&lt;/b&gt;로 직접 조작한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;popUpTo 패턴&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;fun SnapshotStateList&amp;lt;NavKey&amp;gt;.popUpTo(
    key: NavKey,
    inclusive: Boolean = false
) {
    val index = indexOfFirst { it == key }
    if (index &amp;lt; 0) return
    val targetSize = if (inclusive) index else index + 1
    while (size &amp;gt; targetSize) removeLastOrNull()
}

// 사용
backStack.popUpTo(Login, inclusive = true)
backStack.add(Home)&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;launchSingleTop 패턴&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;fun SnapshotStateList&amp;lt;NavKey&amp;gt;.navigateSingleTop(key: NavKey) {
    if (lastOrNull() != key) add(key)
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;로그아웃 등 백스택 초기화&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;div&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;backStack.clear()
backStack.add(Login)&lt;/code&gt;&lt;/pre&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;특정 화면으로 돌아가기&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;fun SnapshotStateList&amp;lt;NavKey&amp;gt;.popBackTo(key: NavKey) {
    val index = indexOfFirst { it == key }
    if (index &amp;lt; 0) return
    while (size &amp;gt; index + 1) removeLastOrNull()
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;Nav2의 DSL로 표현 불가능했던 케이스&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;// 백스택에서 모든 ProductDetail 화면 제거
backStack.removeAll { it is ProductDetail }

// 특정 조건을 만족하는 가장 최근 화면까지 돌아가기
val targetIndex = backStack.indexOfLast { it is Home || it is Login }
if (targetIndex &amp;gt;= 0) {
    while (backStack.size &amp;gt; targetIndex + 1) backStack.removeLastOrNull()
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;Nav2 시절에는 Google이 제공하는 DSL의 조합으로 표현 가능한 흐름만 만들 수 있었지만, Nav3는 &lt;b&gt;자료구조를 직접 다루기 때문에&lt;/b&gt; 표현 가능한 케이스의 폭이 훨씬 넓다.&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;EntryDecorator &amp;mdash; 횡단 관심사 처리&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;각 NavEntry에 공통 동작을 추가하는 메커니즘이다. Nav2에서 라이브러리가 알아서 처리해주던 상태 보존, ViewModel 스코핑 같은 동작을 Nav3는 &lt;b&gt;명시적으로&lt;/b&gt; 데코레이터로 부착한다.&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;NavDisplay(
    backStack = backStack,
    entryDecorators = listOf(
    // rememberSaveable 동작 보장 (configuration change, process death 대응)
    rememberSaveableStateHolderNavEntryDecorator(),
    // ViewModel을 NavEntry 생명주기에 스코프
    rememberViewModelStoreNavEntryDecorator()
    ),
    onBack = { backStack.removeLastOrNull() },
    entryProvider = entryProvider { /* ... */ }
)&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;rememberViewModelStoreNavEntryDecorator()를 추가하면 ViewModel이 Activity가 아니라 해당 화면 단위로 스코프된다. Nav2에서 backstack entry 단위로 ViewModel을 묶던 것과 비슷하지만, 동작이 코드에 드러난다는 점이 중요하다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;적응형 레이아웃 &amp;mdash; Scenes&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;Nav2의 가장 큰 한계 중 하나가 &quot;한 번에 한 화면&quot;이라는 가정이었다. 태블릿에서 list-detail을 동시에 보여주려면 별도의 SlidingPaneLayout이나 Adaptive 라이브러리를 끌어와야 했다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;Nav3는 백스택이 그냥 리스트이므로 &lt;b&gt;마지막 두 개를 동시에 그린다&lt;/b&gt; 같은 동작이 자연스럽게 가능하다. 이를 위해 sceneStrategy 파라미터를 제공한다.&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;pre class=&quot;jboss-cli&quot;&gt;&lt;code&gt;NavDisplay(
    backStack = backStack,
    sceneStrategy = ListDetailSceneStrategy(),
    // ...
)&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;SinglePaneSceneStrategy(기본값), ListDetailSceneStrategy 등의 전략을 선택할 수 있고, 직접 구현도 가능하다. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;폴더블/태블릿/Compose Multiplatform을 고려해야 하는 시대에 결정적인 차이다.&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;결과 전달은 어떻게?&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;Nav2에서 가장 어색했던 부분 중 하나가 previousBackStackEntry.savedStateHandle.set(...) 패턴으로 이전 화면에 결과를 넘기는 것이었다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;Nav3에서는 &lt;b&gt;공유 상태&lt;/b&gt;로 해결한다. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;보통 공유 ViewModel을 쓰거나, 단순한 경우 콜백을 활용한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;class CheckoutFlowViewModel : ViewModel() {
    var selectedAddress by mutableStateOf&amp;lt;Address?&amp;gt;(null)
}

entry&amp;lt;AddressList&amp;gt; {
    val vm: CheckoutFlowViewModel = hiltViewModel(/* 공유 스코프 */)
    AddressListScreen(
        onAddressSelected = { addr -&amp;gt;
            vm.selectedAddress = addr
            backStack.removeLastOrNull()
        }
    )
}

entry&amp;lt;Checkout&amp;gt; {
    val vm: CheckoutFlowViewModel = hiltViewModel(/* 같은 스코프 */)
    CheckoutScreen(address = vm.selectedAddress)
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;내비게이션이 상태로 표현되니, 화면 간 데이터 공유도 일반적인 상태 공유 패턴 그대로 해결할 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;정리&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;Nav2가 &quot;컨트롤러가 그래프를 따라 화면을 옮긴다&quot;였다면, Nav3는 &lt;b&gt;&quot;백스택은 내가 들고 있는 리스트고, NavDisplay는 그 리스트를 관찰해 마지막 항목을 그린다&quot;&lt;/b&gt; 가 전부다.&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 168px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style12&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;width: 23.5659%; text-align: center; height: 21px;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;패러다임&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 35.0775%; text-align: center; height: 21px;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;명령형(Nav2)&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 41.3565%; text-align: center; height: 21px;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;상태 기반(Nav3)&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;width: 23.5659%; height: 21px; text-align: center;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;백스택 소유&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 35.0775%; height: 21px;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;라이브러리 내부&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 41.3565%; height: 21px;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;개발자&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;width: 23.5659%; height: 21px; text-align: center;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;화면 식별&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 35.0775%; height: 21px;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;라우트 문자열&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 41.3565%; height: 21px;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;데이터 클래스&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;width: 23.5659%; height: 21px; text-align: center;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;이동 명령&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 35.0775%; height: 21px;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;navigate(&quot;route&quot;)&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 41.3565%; height: 21px;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;backStack.add(Route)&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;width: 23.5659%; height: 21px; text-align: center;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;뒤로가기&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 35.0775%; height: 21px;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;popBackStack()&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 41.3565%; height: 21px;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;backStack.removeLastOrNull()&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;width: 23.5659%; height: 21px; text-align: center;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;인자 전달&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 35.0775%; height: 21px;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;문자열 파싱&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 41.3565%; height: 21px;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;프로퍼티 접근&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;width: 23.5659%; height: 21px; text-align: center;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;적응형 UI&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 35.0775%; height: 21px;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;별도 라이브러리 필요&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 41.3565%; height: 21px;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;SceneStrategy 기본 지원&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;width: 23.5659%; height: 21px; text-align: center;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;테스트&amp;nbsp;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 35.0775%; height: 21px;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;인스트루먼트 테스트&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 41.3565%; height: 21px;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;유닛 테스트 가능&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;Compose의 나머지 모든 부분이 상태 기반으로 동작하는데, 내비게이션만 명령형이었던 일관성의 균열이 Nav3에 와서야 비로소 메워졌다고 볼 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;기존 Nav2를 사용 중인 프로젝트라면 당장 마이그레이션할 필요는 없지만, 신규 Compose 프로젝트나 적응형 UI / Compose Multiplatform을 염두에 둔 프로젝트라면 Nav3로 시작하는 것을 고려해볼 만하다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR'; color: #9d9d9d;&quot;&gt;- Reference&lt;/span&gt;&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR'; color: #9d9d9d;&quot;&gt;&lt;a style=&quot;color: #9d9d9d;&quot; href=&quot;https://developer.android.com/guide/navigation/navigation-3&quot;&gt;Navigation 3 공식 문서&lt;/a&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR'; color: #9d9d9d;&quot;&gt;&lt;a style=&quot;color: #9d9d9d;&quot; href=&quot;https://android-developers.googleblog.com/2025/11/jetpack-navigation-3-is-stable.html&quot;&gt;Jetpack Navigation 3 is stable (Android Developers Blog)&lt;/a&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR'; color: #9d9d9d;&quot;&gt;&lt;a style=&quot;color: #9d9d9d;&quot; href=&quot;https://android-developers.googleblog.com/2025/05/announcing-jetpack-navigation-3-for-compose.html&quot;&gt;Announcing Jetpack Navigation 3 (Google I/O 2025)&lt;/a&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR'; color: #9d9d9d;&quot;&gt;&lt;a style=&quot;color: #9d9d9d;&quot; href=&quot;https://developer.android.com/guide/navigation/navigation-3/migration-guide&quot;&gt;Navigation 3 Migration Guide&lt;/a&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>안드로이드</category>
      <author>Bum_2</author>
      <guid isPermaLink="true">https://bum2.tistory.com/208</guid>
      <comments>https://bum2.tistory.com/208#entry208comment</comments>
      <pubDate>Tue, 26 May 2026 17:41:15 +0900</pubDate>
    </item>
    <item>
      <title>[안드로이드] API 36 버전 업데이트 정리</title>
      <link>https://bum2.tistory.com/198</link>
      <description>&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;안드로이드 API 36버전에서 변화되는 내용을 정리하고자 한다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;먼저, 안드로이드 16(API 레벨 36) OS를 갖는 모든 디바이스에 적용되는 내용부터 살펴보자.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;정식 출시는 25년 6월에 출시되었고 구글 플레이 의무화는 26년 8월 부터다. &lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;다음은&lt;/b&gt; targetSdkVersion에 관계없이, &lt;b&gt;Android 16 기기 위에서 실행되는 모든 앱에 적용&lt;/b&gt;된다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&amp;nbsp;1. Job 스케줄링 실행 쿼터 강화&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;백그라운드 Job의 실행 시간 쿼터가 더 엄격&lt;/b&gt;하게 관리된다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;아래 세 가지 조건에 따라 쿼터 적용 방식이 달라진다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;앱 대기 버킷(Standby Bucket): Active 버킷에도 넉넉한 쿼터가 적용되기 시작&lt;/li&gt;
&lt;li&gt;앱 가시성 상태: 앱이 화면에 보이는 동안 시작된 Job이 숨겨진 후에도 계속되면 쿼터 적용&lt;/li&gt;
&lt;li&gt;포그라운드 서비스 동시 실행: 포그라운드 서비스와 함께 실행 중인 Job도 쿼터 적용 대상&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대용량 데이터 전송 작업은 User-initiated data transfer jobs 사용을 권장&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;앱 대기 버킷(App Standby Bucket)과 쿼터 &lt;br /&gt;&lt;br /&gt;&lt;b&gt;앱 대기 버킷은&lt;/b&gt; Android 9(API 28)부터 도입된 배터리 관리 메커니즘으로, &lt;b&gt;시스템이 각 앱을 사용 빈도와 패턴에 따라 다섯 등급(Active, Working Set, Frequent, Rare, Restricted) 중 하나로 자동 분류&lt;/b&gt;하는 개념이다. &lt;br /&gt;&lt;b&gt;분류는 전적으로 개인화&lt;/b&gt;되어 있어서 같은 앱이라도 사용자의 행동 패턴에 따라 다른 버킷에 들어간다. 매일 출퇴근길에 쓰는 지도 앱은 누군가에겐 Active 버킷이지만, 1년에 한 번 여행 갈 때만 쓰는 사람에겐 Rare 버킷이 되는 식이다. &lt;br /&gt;머신러닝(Adaptive Battery)이 탑재된 기기에서는 한 발 더 나아가 &quot;이 사용자가 이 시간대에 이 앱을 쓸 가능성&quot;까지 예측해서 동적으로 버킷을 조정하고, ML이 없는 기기에서는 단순히 최근 사용 시점 기준으로 정렬한다.&lt;br /&gt;&lt;br /&gt;쿼터는 이 버킷 등급에 따라 차등 적용되는 백그라운드 잡(Job)의 실행 시간 예산이다. &lt;br /&gt;일회성 타이머가 아니라 롤링 시간 윈도우 안에서의 누적 실행 시간 한도라서, &quot;지난 60분 동안 이미 20분을 썼다면 더 못 돌리고 시간이 지나면서 예산이 다시 차오른다&quot;는 식으로 동작한다. &lt;br /&gt;Working Set은 60분당 일반 잡 20분, &lt;br /&gt;Frequent는 4시간당 10분, &lt;br /&gt;Rare는 24시간당 10분으로 점점 빡빡해지며, &lt;br /&gt;Restricted는 하루 1회 10분 배치 세션 안에서 다른 앱 잡들과 묶여서만 실행될 정도로 가혹하다. &lt;br /&gt;Active 버킷은 Android 16 이전엔 무제한이었지만, 이제는 &quot;넉넉한(generous)&quot; 쿼터가 적용되기 시작했다.&lt;br /&gt;&lt;br /&gt;Android 16의 핵심 변경은 이 쿼터의 사각지대를 없앤 것이다. 기존엔 앱이 화면에 떠 있는 동안 시작된 잡이나 포그라운드 서비스와 함께 도는 잡은 사실상 무제한으로 돌 수 있었는데, 이런 우회로가 막혔다. 단 포그라운드 서비스(FGS) 자체는 별도 규칙을 따르므로 mediaPlayback이나 location 같은 타입은 여전히 무기한 실행되고, 쿼터에 걸려 중단되는 건 어디까지나 그 옆에서 함께 돌던 백그라운드 잡들이다. &lt;br /&gt;&lt;b&gt;즉&lt;/b&gt; Spotify의 음악 재생이나 카카오맵의 내비게이션은 그대로 살아 있고, 그 &lt;b&gt;앱들이 사용자 모르게 묻어 돌리던 동기화&amp;middot;캐싱&amp;middot;통계 업로드 같은 잡들이 제어 대상&lt;/b&gt;이 된다.&lt;br /&gt;&lt;br /&gt;대용량 데이터 전송은 User-initiated data transfer job을 쓰고, 일반 백그라운드 작업은 WorkManager에 맡겨 시스템이 최적 타이밍을 잡도록 하며, 충전 중&amp;middot;Wi-Fi 연결 같은 제약 조건을 적극 활용해 사용자 버킷이 무엇이든 시스템이 알아서 풀어주는 시점에 작업이 실행되도록 설계하는 것이 핵심이다.&lt;/blockquote&gt;
&lt;h3 style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;br /&gt;&lt;b&gt;2. Intent 리다이렉션 공격 기본 차단&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;악성 앱이 인텐트 extras를 조작해 취약한 앱의 비공개 컴포넌트를 실행하는 Intent Redirection 공격에 대해 플랫폼 수준의 보안이 기본 적용된다. 일반적인 방식으로 인텐트를 사용하는 앱은 영향이 없다.&lt;/p&gt;
&lt;h3 style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;br /&gt;&lt;b&gt;3. 블루투스 본드 손실 처리 개선&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;기존에는 블루투스 기기와의 본드(페어링 정보)가 손실되면 시스템이 자동으로 재페어링을 시도한다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Android 16부터는 &lt;b&gt;연결을 끊고 본드 정보를 유지한 채 사용자에게 시스템 다이얼로그로 알려준다&lt;/b&gt;. 아울러 두 가지 새 인텐트가 추가된다. &lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;-&amp;nbsp;ACTION_KEY_MISSING:&amp;nbsp;원격&amp;nbsp;본드&amp;nbsp;손실&amp;nbsp;감지&amp;nbsp;시&amp;nbsp;수신 &lt;br /&gt;&amp;nbsp;&amp;nbsp;-&amp;nbsp;ACTION_ENCRYPTION_CHANGE:&amp;nbsp;링크&amp;nbsp;암호화&amp;nbsp;상태&amp;nbsp;변경&amp;nbsp;시&amp;nbsp;수신 &lt;br /&gt;&lt;br /&gt;OEM마다 구현 방식이 다를 수 있으므로 실기기 테스트를 권장&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;h3 style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;br /&gt;&amp;nbsp;&lt;b&gt;4. 가상 디바이스 디스플레이 오버라이드&lt;/b&gt; &lt;br /&gt;&lt;br /&gt;&lt;/h3&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;차량&amp;nbsp;인포테인먼트,&amp;nbsp;VR&amp;nbsp;기기&amp;nbsp;등&amp;nbsp;가상&amp;nbsp;디바이스&amp;nbsp;소유자가&amp;nbsp;앱을&amp;nbsp;외부&amp;nbsp;화면에&amp;nbsp;투영할&amp;nbsp;때,&amp;nbsp;해당&amp;nbsp;앱의&amp;nbsp;방향(orientation),&amp;nbsp;비율,&amp;nbsp;리사이즈&amp;nbsp;제한을&amp;nbsp;무시하도록&amp;nbsp;설정할&amp;nbsp;수&amp;nbsp;있습니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&gt;targetSdk 36 지정 시 추가 적용 사항&lt;br /&gt;build.gradle에서 targetSdkVersion = 36으로 설정했을 때만 추가로 적용된다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;1. 대화면 Adaptive Layout 강제 적용&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;가장 영향이 큰 변경사항으로, &lt;b&gt;최소 너비 600dp 이상인 화면&lt;/b&gt;(태블릿, 폴더블 펼침 화면 등)&lt;b&gt;에서 아래 속성이 모두 무시&lt;/b&gt;된다. &lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;-&amp;nbsp;android:screenOrientation &lt;br /&gt;&amp;nbsp;&amp;nbsp;-&amp;nbsp;resizeableActivity=&quot;false&quot; &lt;br /&gt;&amp;nbsp;&amp;nbsp;-&amp;nbsp;가로세로&amp;nbsp;비율(Aspect&amp;nbsp;Ratio)&amp;nbsp;제한&amp;nbsp;전체 &lt;br /&gt;&lt;br /&gt;앱이 디스플레이 전체를 채우며, 필러박스(letterbox / pillarbox)가 사라진다. 세로 고정으로 개발된 앱은 레이아웃이 깨질 수 있어 사전 검증이 필수. &lt;br /&gt;&lt;br /&gt;예외&amp;nbsp;대상 &lt;br /&gt;&amp;nbsp;&amp;nbsp;- 게임 앱 (android:appCategory=&quot;game&quot; 설정 시)&lt;br /&gt;&lt;b&gt;적용 대상&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;- &lt;b&gt;Galaxy Z Fold 펼친 상태&lt;/b&gt;, 갤럭시 탭 등&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;br /&gt;&lt;b&gt;2. 엣지 투 엣지(Edge-to-Edge) 강제 적용&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;앱 배경이 상태 바, 내비게이션 바 아래까지 확장된다. UI 요소와 터치 영역은 기존과 동일하게 safe zone 내에 유지되지만, 배경 색상이 시스템 바 영역까지 채워진다. Android 11(API 30) 이상 기기에 적용.&lt;/p&gt;
&lt;h3 style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;3. MediaStore 버전 앱별 고유값 변경&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;MediaStore.getVersion()이 이제 앱마다 다른 고유값을 반환.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;버전 문자열의 포맷을 파싱해서 추가 정보를 추출하는 로직이 있다면 수정이 필요하다.&lt;/p&gt;
&lt;h3 style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;br /&gt;&lt;br /&gt;&lt;b&gt;4. scheduleAtFixedRate 누락 실행 제한&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;앱이 유효하지 않은 라이프사이클 상태에 있는 동안 예약 실행이 여러 번 누락되더라도, 앱 복귀 시 최대 1번만 즉시 실행된다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;기존에는 누락된 모든 실행이 한꺼번에 몰아서 실행되었다. &lt;br /&gt;&lt;br /&gt;STPE_SKIP_MULTIPLE_MISSED_PERIODIC_TASKS 호환 플래그로 사전 테스트 가능.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;5. 로컬 네트워크 접근 권한 (옵트인 단계) &lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;LAN 기기(프린터, 스마트TV, NAS 등)와의 통신에 별도 권한이 필요해진다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;API 36에서는 옵트인 테스트 단계이며, API 37부터 의무화.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;br /&gt;&lt;b&gt;6. 카메라 Pro API 강화&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;야간 모드 씬 감지, 하이브리드 자동 노출, 정밀 색온도 조절, 모션 포토 Intent 액션 추가,&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;UltraHDR HEIC 인코딩 지원이 추가된다. &lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;h3 style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;br /&gt;&lt;b&gt;7. Health Connect 업데이트&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;새 데이터 타입 ACTIVITY_INTENSITY가 추가되고, WHO 가이드라인 기반의 FHIR 포맷 의료 기록 읽기&amp;middot;쓰기 API가 도입된다.&lt;/p&gt;
&lt;h3 style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;br /&gt;&lt;br /&gt;&lt;b&gt;8. 예측 뒤로가기 (3-button 내비게이션)&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;기존에 제스처 내비게이션에서만 지원되던 Predictive Back 애니메이션이 3-button 내비게이션에서도 동작한다. finishAndRemoveTaskCallback(), moveTaskToBackCallback()도 함께 추가된다. &lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;h3 style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;br /&gt;&lt;b&gt;9. 내장 사진 선택기 API&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;앱 뷰 계층 구조에 사진 선택기를 직접 임베드할 수 있는 새 API가 추가됩니다. 광범위한 미디어 접근 권한 없이 사용할 수 있다. &lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;h3 style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;br /&gt;&lt;b&gt;10. SDK_INT_FULL / 마이너 버전 지원&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;VERSION_CODES_FULL 열거형과 Build.getMinorSdkVersion() 메서드가 추가되어 주 버전(36)과 부 버전(36.1 등)을 구분한 API 분기 처리가 가능해진다.&lt;/p&gt;
&lt;h3 style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;br /&gt;&lt;b&gt;11. WiFi 위치 보안 강화&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;WiFi 6(802.11az) 기반 근접 센싱의 정확도와 보안이 향상된다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;- Reference&lt;br /&gt;&lt;br /&gt;&lt;span style=&quot;color: #9d9d9d;&quot;&gt;&amp;nbsp;&amp;nbsp;-&amp;nbsp;&lt;a style=&quot;color: #9d9d9d;&quot; href=&quot;https://developer.android.com/about/versions/16/behavior-changes-all&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Android&amp;nbsp;16&amp;nbsp;동작&amp;nbsp;변경사항&amp;nbsp;&amp;mdash;&amp;nbsp;모든&amp;nbsp;앱&lt;/a&gt; &lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #9d9d9d;&quot;&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #9d9d9d;&quot;&gt;&amp;nbsp;&amp;nbsp;-&amp;nbsp;&lt;a style=&quot;color: #9d9d9d;&quot; href=&quot;https://developer.android.com/about/versions/16/behavior-changes-16&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Android&amp;nbsp;16&amp;nbsp;동작&amp;nbsp;변경사항&amp;nbsp;&amp;mdash;&amp;nbsp;targetSdk&amp;nbsp;36&amp;nbsp;앱&lt;/a&gt; &lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #9d9d9d;&quot;&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #9d9d9d;&quot;&gt;&amp;nbsp;&amp;nbsp;-&amp;nbsp;&lt;a style=&quot;color: #9d9d9d;&quot; href=&quot;https://developer.android.com/about/versions/16/features&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Android&amp;nbsp;16&amp;nbsp;새&amp;nbsp;기능&amp;nbsp;및&amp;nbsp;API&lt;/a&gt; &lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #9d9d9d;&quot;&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #9d9d9d;&quot;&gt;&amp;nbsp;&amp;nbsp;-&amp;nbsp;&lt;a style=&quot;color: #9d9d9d;&quot; href=&quot;https://android-developers.googleblog.com/2025/01/orientation-and-resizability-changes-in-android-16.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Android&amp;nbsp;16&amp;nbsp;Orientation&amp;nbsp;&amp;amp;&amp;nbsp;Resizability&amp;nbsp;변경사항&amp;nbsp;(공식&amp;nbsp;블로그)&lt;/a&gt;&lt;/span&gt; &lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&lt;/p&gt;</description>
      <category>안드로이드</category>
      <author>Bum_2</author>
      <guid isPermaLink="true">https://bum2.tistory.com/198</guid>
      <comments>https://bum2.tistory.com/198#entry198comment</comments>
      <pubDate>Fri, 1 May 2026 21:40:50 +0900</pubDate>
    </item>
    <item>
      <title>[안드로이드] 웹뷰와 커스텀 탭</title>
      <link>https://bum2.tistory.com/193</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;웹뷰와 커스텀 탭 모두 웹 페이지의 내용을 보여주는 컴포넌트다. 이 글을 통해 웹뷰를 상기해보고 커스텀 탭이 어떻게 차이가 있는지 알아보자.&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;웹뷰&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;웹뷰는 안드로이드에서 지원하는 컴포넌트로, 앱 내부에 웹 브라우저를 내장하여 웹 페이지를 렌더링하는 컴포넌트이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;즉, 앱 안에 브라우저를 직접 구현하는 방식이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;WebView의 핵심은 &lt;b&gt;&amp;ldquo;렌더링&amp;rdquo;과 &amp;ldquo;제어&amp;rdquo;를 분리해서 이해&lt;/b&gt;하는 것이다. HTML을 파싱하고 DOM을 구성하며, 레이아웃을 계산하고 &lt;b&gt;화면에 그리는 작업은 모두 Chromium 기반의 웹 엔진이 담당&lt;/b&gt;한다. 하지만 그 엔진은 앱 프로세스 안에서 동작하며, 앱은 WebView API를 통해 이 엔진을 제어할 수 있다. 즉, &lt;b&gt;렌더링 자체는 엔진이 수행&lt;/b&gt;하지만, 어떤 페이지를 로드할지, 어떤 JavaScript를 실행할지, 어떤 이벤트를 가로챌지는 전부 앱이 결정한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;이 구조 덕분에 WebView에서는 일반 브라우저에서는 불가능한 수준의 제어가 가능하다. 예를 들어 evaluateJavascript()를 사용하면 웹 페이지 내부의 DOM에 접근해 데이터를 가져올 수 있다. 또한 JavaScript 인터페이스를 통해 웹과 네이티브 간 양방향 통신도 구현할 수 있다. 이처럼 WebView는 단순히 웹 페이지를 보여주는 것을 넘어, &lt;b&gt;웹을 앱의 일부처럼 동작하게 만드는 강력한 도구&lt;/b&gt;다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;하지만 이러한 강력함은 동시에 책임을 의미한다. WebView는 앱 내부에서 실행되기 때문에 보안과 안정성을 개발자가 직접 관리해야 한다. 외부에서 로드한 HTML에 대한 검증 없이 JavaScript 인터페이스를 열어두면 XSS와 같은 취약점이 발생할 수 있고, 잘못된 설정은 사용자 데이터 유출로 이어질 수 있다. 또한 웹 렌더링을 앱 내부에서 수행하기 때문에 성능에 대한 고려도 필요하다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;결국 WebView는 &amp;ldquo;브라우저를 여는 기술&amp;rdquo;이 아니라, &lt;b&gt;브라우저 엔진을 앱 안에 두고 직접 웹을 렌더링하고 제어하는 환경&lt;/b&gt;이다. 웹과 네이티브의 경계를 허물고 하나의 사용자 경험으로 통합할 수 있다는 점에서 매우 유용하지만, 그만큼 구조를 정확히 이해하고 사용하는 것이 중요하다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;한 줄로 정리하면 다음과 같다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;WebView는 브라우저를 실행하는 것이 아니라, 브라우저 엔진을 앱 내부에서 사용해 웹을 직접 제어하는 기술&lt;/span&gt;이다.&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;특징&amp;nbsp;&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;HTML, CSS, JavaScript 를 직접 렌더링 가능&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;JavaScript 인터페이스를 통해, 안드로이드 &amp;harr; 웹 양방향 통신 가능&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;장점&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;완전 제어가 가능해서 높은 자유도&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;앱 상태와 강하게 연동 가능&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;단점&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;앱 내부에서 렌더링이 되기 때문에, 성능 부담&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;XSS, JS Injection 등 보안 취약점 존재&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;커스텀 탭&amp;nbsp;&lt;/span&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;크롬과 같은 외부 브라우저를 앱 위에서 실행하는 방식이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;즉, 브라우저를 직접 구현하는 것이 아닌, 빌려서 사용하는 방식이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;안드로이드에서 웹 페이지를 열 때 흔히 &amp;ldquo;브라우저를 빌려쓴다&amp;rdquo;는 표현을 사용한다. 특히 Custom Tabs를 설명할 때 자주 등장하는 말인데, 이 표현은 방향은 맞지만 정확한 구조를 이해하지 않으면 쉽게 오해로 이어진다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;우선 중요한 전제부터 짚고 가야 한다. 안드로이드에서 브라우저는 단순히 &amp;ldquo;항상 백그라운드에서 실행되고 있는 존재&amp;rdquo;가 아니다. Chrome이든 Samsung Internet이든 브라우저 앱은 단지 설치된 애플리케이션일 뿐이며, 사용하지 않으면 프로세스가 아예 존재하지 않을 수도 있다. 즉, &amp;ldquo;기본 브라우저가 있으니 항상 프로세스도 떠 있다&amp;rdquo;는 생각은 틀린 가정이다. 안드로이드는 필요할 때만 프로세스를 생성하고, 필요 없으면 종료하는 구조이기 때문이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;그렇다면 Custom Tabs에서 말하는 &amp;ldquo;브라우저를 빌려쓴다&amp;rdquo;는 것은 무엇을 의미할까? 핵심은 &lt;b&gt;브라우저의 내부 인스턴스에 직접 접근하는 것이 아니라, &lt;span style=&quot;color: #ee2323;&quot;&gt;브라우저 앱과&lt;/span&gt; &lt;span style=&quot;color: #ee2323;&quot;&gt;IPC&lt;/span&gt;(Inter-Process Communication), 정확히는 &lt;span style=&quot;color: #ee2323;&quot;&gt;Binder를 통해 협업하는 구조&lt;/span&gt;&lt;/b&gt;라는 점이다. Custom Tabs를 사용할 때 앱은 브라우저 프로세스 내부 객체를 직접 제어하는 것이 아니라, 브라우저가 제공하는 CustomTabsService에 바인딩하여 세션을 생성하고, 필요한 기능을 요청한다. 이후 실제 웹 콘텐츠는 브라우저 쪽 Activity가 실행되어 렌더링을 담당한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;동작 흐름을 보면 더 명확하다. 앱에서 CustomTabsIntent.launchUrl()을 호출하면, 시스템은 Custom Tabs를 지원하는 브라우저를 찾는다. 이때 브라우저 프로세스가 이미 실행 중이라면 그대로 재사용하고, 실행 중이 아니라면 OS가 새로 프로세스를 생성한다. 이후 (선택적으로) bindCustomTabsService()를 통해 브라우저의 서비스에 연결할 수 있는데, 이 단계는 필수가 아니라 성능 최적화를 위한 선택 사항이다. 여기서 warmup()이나 세션 생성, URL prefetch 같은 작업을 통해 페이지 로딩 속도를 개선할 수 있다. 마지막으로 브라우저의 Activity가 foreground로 올라오며 실제 화면이 표시된다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;여기서 중요한 포인트는 Custom Tabs가 &amp;ldquo;앱 내부에 웹을 렌더링하는 방식&amp;rdquo;이 아니라는 것이다. 즉, &lt;b&gt;WebView처럼 앱 프로세스 안에서 웹을 그리는 것이 아니라&lt;/b&gt;, &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;브라우저 프로세스가 별도로 존재하고 그 Activity가 화면에 표시되는 구조&lt;/b&gt;&lt;/span&gt;다. &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;다만 툴바 색상 커스터마이징, 애니메이션, 뒤로가기 동작 등으로 인해 사용자 입장에서는 마치 앱 내부 화면처럼 자연스럽게 이어지는 경험을 제공&lt;/b&gt;&lt;/span&gt;한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;또 하나 중요한 점은 &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;모든 브라우저가 Custom Tabs를 지원하는 것은 아니라는 것&lt;/b&gt;&lt;/span&gt;이다. 따라서 실제 구현에서는 CustomTabsClient.getPackageName() 등을 통해 지원 브라우저를 먼저 탐색하고, &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;없는 경우에는 ACTION_VIEW로 fallback하는 구조&lt;/b&gt;&lt;/span&gt;를 사용한다. 이 fallback은 &amp;ldquo;같은 방식으로 동작한다&amp;rdquo;는 의미가 아니라, 단순히 &amp;ldquo;어쨌든 URL은 열리게 한다&amp;rdquo;는 의미에 가깝다. 즉, Custom Tabs가 제공하는 최적화된 UX와 성능은 포기하고 기본 브라우저 실행으로 대체하는 것이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;정리하면 Custom Tabs는 단순히 브라우저를 여는 기술이 아니라, &lt;b&gt;브라우저 앱의 기능을 IPC 기반으로 재사용하면서도 앱과 자연스럽게 이어지는 사용자 경험을 제공하는 방식&lt;/b&gt;이다. 브라우저 프로세스는 항상 존재하는 것이 아니라 필요할 때 생성되며, 서비스 바인딩은 성능 최적화를 위한 선택적 단계다. 그리고 최종적으로 화면을 그리는 주체는 여전히 브라우저라는 점을 이해하는 것이 가장 중요하다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;한 줄로 정리하면 다음과 같다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;Custom Tabs는 브라우저를 &amp;ldquo;내 앱 안에 띄우는 기술&amp;rdquo;이 아니라, 브라우저와 협업하여 &amp;ldquo;브라우저가 그린 화면을 앱처럼 보이게 사용하는 기술&amp;rdquo;&lt;/span&gt;이다.&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;특징&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;크롬(or 기본) 브라우저 엔진 사용&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;쿠키 및 로그인 상태 자동 공유&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;주소창 및 보안 UI 제공&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;장점&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;이미 실행 중인 브라우저를 활용하여 빠른 성능&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;크롬 정책을 적용하여 높은 보안&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;단점&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;HTML/DOM 제어 불가능&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;JavaScript 주입 불가&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;앱과의 직접적인 데이터 연결 어려움&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Reference&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #9d9d9d;&quot;&gt;- &lt;a style=&quot;color: #9d9d9d;&quot; href=&quot;https://github.com/GoogleChrome/custom-tabs-client/blob/f55501961a211a92eacbe3c2f15d7c58c19c8ef9/Application/src/main/java/org/chromium/customtabsclient/MainActivity.java#L208&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;custom-tabs-client 코드&lt;/a&gt;&lt;/span&gt;&lt;/p&gt;</description>
      <category>안드로이드</category>
      <author>Bum_2</author>
      <guid isPermaLink="true">https://bum2.tistory.com/193</guid>
      <comments>https://bum2.tistory.com/193#entry193comment</comments>
      <pubDate>Mon, 23 Mar 2026 21:02:40 +0900</pubDate>
    </item>
    <item>
      <title>[KMP] Ktor Auth Plugin은 401을 어떻게 처리할까? Bearer 토큰 자동 갱신 흐름 분석</title>
      <link>https://bum2.tistory.com/187</link>
      <description>&lt;p data-end=&quot;493&quot; data-start=&quot;371&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;OkHttp를 사용할 때는 OkHttp 클라이언트에 체이닝을 걸어 Interceptor와 Authenticator로 access token을 헤더에 추가하고, 401이 발생하면 refresh token을 통해 토큰을 갱신하는 구조를 직접 설계했다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-end=&quot;572&quot; data-start=&quot;500&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;하지만 Ktor Client에서는 Auth 플러그인과 Bearer Provider만 설정하여 이러한 흐름을 처리할 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-end=&quot;662&quot; data-start=&quot;579&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;그렇다면 Ktor는 내부적으로 어떤 흐름으로 401을 감지하고, 언제 refreshTokens를 호출하며, 어떻게 동일한 요청을 재시도하는 걸까? 이번 글에서는 Ktor Bearer 인증의 내부 동작 과정을 코드 레벨에서 분석해보려고 한다.&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h3 data-end=&quot;662&quot; data-start=&quot;579&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;Ktor&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-end=&quot;662&quot; data-start=&quot;579&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;Ktor는 코틀린에서 사용할 수 있는 &lt;b&gt;비동기 HTTP 클라이언트 기반 라이브러리&lt;/b&gt;다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-end=&quot;662&quot; data-start=&quot;579&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;내부적으로 코루틴을 기반으로 동작&lt;/b&gt;하며 &lt;b&gt;각 플랫폼&lt;/b&gt;(iOS, Android, Desktop)&lt;b&gt;에 맞는 엔진을 통해 네트워크 통신을 수행&lt;/b&gt;한다.&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&gt;
&lt;h3 data-end=&quot;662&quot; data-start=&quot;579&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;AuthProvider&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-end=&quot;662&quot; data-start=&quot;579&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;ktor-client-auth 에서 공식적으로 제공하는 AuthProvider는 Bearer, Basic, Digest Auth 프로바이더가 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-end=&quot;662&quot; data-start=&quot;579&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;이 외에도 AuthProvider를 자신의 서비스에 맞게 생성하여 사용할 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-end=&quot;662&quot; data-start=&quot;579&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;Basic은 (TLS 위에서) 단순 자격증명 방식이라 토큰 기반(OAuth/JWT) 시나리오에는 보통 사용하지 않으며, Digest는 현대 API에서는 덜 쓰인다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&gt;
&lt;h3 data-end=&quot;662&quot; data-start=&quot;579&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;BearerAuthProvider&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-end=&quot;662&quot; data-start=&quot;579&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;BearerAuthProvider는 Authorization 에 &quot;Bearer $token&quot;을 붙이는 API에 사용하는 프로바이더&lt;/b&gt;다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-end=&quot;662&quot; data-start=&quot;579&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;시그니처는 아래와 같다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-end=&quot;662&quot; data-start=&quot;579&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1772257729568&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class BearerAuthProvider(
    private val refreshTokens: suspend RefreshTokensParams.() -&amp;gt; BearerTokens?,
    loadTokens: suspend () -&amp;gt; BearerTokens?,
    private val sendWithoutRequestCallback: (HttpRequestBuilder) -&amp;gt; Boolean = { true },
    private val realm: String?,
    cacheTokens: Boolean = true,
    private val nonCancellableRefresh: Boolean = false,
)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;bearer 을 사용하면 AuConfig 의 bearer가 호출되고 생성자를 통해 BearerAUthProvider를 통해 초기화되는 구조다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;흐름과 같이 이해하기 위해 아래와 같이 코드를 작성했다고 하자.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1772258080452&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;fun creatApiHttpClient(
    engine: HttpClientEngine,
    tokenManager: TokenManager,
    isDebug: Boolean = true,
    baseUrl: String,
    additionalConfig: HttpClientConfig&amp;lt;*&amp;gt;.() -&amp;gt; Unit = {}
): HttpClient = HttpClient(engine = engine) {
    defaultRequest {
        url.takeFrom(baseUrl)
    }

	...

    install(Auth) {
        bearer {
            sendWithoutRequest { request -&amp;gt;
                val path = request.url.encodedPath
                !(path.startsWith(ApiPaths.LOGIN_PLATFORM) ||
                        path.startsWith(ApiPaths.TOKEN_REFRESH))
            }
            loadTokens {
                val access = tokenManager.loadAccessToken().takeIf { it.isNotBlank() }
                val refresh = tokenManager.loadRefreshToken().takeIf { it.isNotBlank() }
                if (access != null &amp;amp;&amp;amp; refresh != null) {
                    BearerTokens(accessToken = access, refreshToken = refresh)
                } else null
            }
            refreshTokens {
                tokenManager.refreshAndGetNewAccessToken()
                    .takeIf { it.isNotBlank() }
                    ?.let { newAccess -&amp;gt;
                        val newRefresh = tokenManager.loadRefreshToken().takeIf { it.isNotBlank() }
                        BearerTokens(accessToken = newAccess, refreshToken = newRefresh ?: &quot;&quot;)
                    }
            }
        }
    }

    additionalConfig()
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;설명할 부분은 bearer 블럭에서 sendWithoutRequest 블럭, loadTokens 블럭, refreshTokens 블럭이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;먼저 sendWithoutRequest 블럭부터 살펴보자.&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;sendWithoutRequest 블럭&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;이 블럭은 API 요청을 하기 직전에, onRequest 블럭에서 sendWithoutRequest가 true를 반환하는 provider에 한해서 Authorization 헤더를 붙인다.&lt;b&gt; Authorization 헤더를 붙이지 말아야할 API Path 들에 대해서 사용&lt;/b&gt;한다. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;아래 코드를 보자.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1772258501390&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@OptIn(InternalAPI::class)
public val Auth: ClientPlugin&amp;lt;AuthConfig&amp;gt; = createClientPlugin(&quot;Auth&quot;, ::AuthConfig) {
    val providers = pluginConfig.providers.toList()

    client.attributes.put(AuthProvidersKey, providers)

    val tokenVersions = ConcurrentMap&amp;lt;AuthProvider, AtomicCounter&amp;gt;()
    val tokenVersionsAttributeKey =
        AttributeKey&amp;lt;MutableMap&amp;lt;AuthProvider, Int&amp;gt;&amp;gt;(&quot;ProviderVersionAttributeKey&quot;)

	...
    
   onRequest { request, _ -&amp;gt;
        providers.filter { it.sendWithoutRequest(request) }.forEach { provider -&amp;gt;
            LOGGER.trace { &quot;Adding auth headers for ${request.url} from provider $provider&quot; }
            val tokenVersion = tokenVersions.computeIfAbsent(provider) { AtomicCounter() }
            val requestTokenVersions = request.attributes
                .computeIfAbsent(tokenVersionsAttributeKey) { mutableMapOf() }
            requestTokenVersions[provider] = tokenVersion.atomic.value
            provider.addRequestHeaders(request)
        }
    } 
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;a href=&quot;https://github.com/ktorio/ktor/blob/main/ktor-client/ktor-client-plugins/ktor-client-auth/common/src/io/ktor/client/plugins/auth/Auth.kt&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&amp;lt;ktor-client-auth의 Auth.kt 중 일부&amp;gt;&lt;/a&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;토큰의 최신화 파악을 위해, 프로바이더에서 사용한 토큰의 Counter 값을 스냅샷 처리한다. 이는 refreshTokens에서 사용한다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;이 부분에서 이해가가지 않은 점이 있었다. sendWithoutRequest 네이밍을 사용해서 이 프로바이더가 True라면 헤더에 Authorization 값을 붙여서 보낸다는 점이다. 그렇다면 sendWithoutRequest가 아니라, sendRequest 이라는 네이밍을 사용했다면 직관적으로 와닿지 않았을까라는 생각을 했다.&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;찾아보니, 이 콜백의 원래 의도는 401을 기다리지 말고, 처음 요청부터(= without challenge) Authorization을 붙일지 말지 결정하는 용도라고 한다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;즉 네이밍은&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&quot;send (auth) without request(ing auth)&quot; 보다는 &quot;challenge(WWW-Authenticate)를 기다리지 않고 먼저 보낼래?&amp;rdquo; 같은 뉘앙스에 더 가깝다. 이는&amp;nbsp;&amp;ldquo;challenge 없이도 Authorization을 선제적으로 붙일지&amp;rdquo; 의 의미가 숨어있다.&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt; 즉&lt;b&gt; sendWithoutRequest 에서 &quot;request&quot;가 &quot;내가 보내는 HTTP 요청&quot;이 아니라 &quot;서버가 보내는 인증 요구(challenge)&quot;&lt;/b&gt; 아는 것이다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;결론적으로, 서버의 인증 없이 보내도 되는 API 라는 것&lt;/b&gt;.&lt;/span&gt;&lt;/blockquote&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&gt;
&lt;h3 style=&quot;text-align: left;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;loadTokens&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;이 블럭은 앱 내부에서 엑세스 토큰을 로드하는 로직을 담당&lt;/b&gt;한다. &lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;BearerTokens(access_token, refresh_token)을 반환해야하며, access_token을 담아 리턴해야한다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;public val Auth: ClientPlugin&amp;lt;AuthConfig&amp;gt; 에서 addRequestHeaders 메소드를 사용할 때 내부에서 AuthTokenHolder 인스턴스가 호출한다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1772259703014&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class BearerAuthProvider(
    private val refreshTokens: suspend RefreshTokensParams.() -&amp;gt; BearerTokens?,
    loadTokens: suspend () -&amp;gt; BearerTokens?,
    private val sendWithoutRequestCallback: (HttpRequestBuilder) -&amp;gt; Boolean = { true },
    private val realm: String?,
    cacheTokens: Boolean = true,
    private val nonCancellableRefresh: Boolean = false,
)  : AuthProvider {

    private val tokensHolder = AuthTokenHolder(loadTokens, cacheTokens)
    
        override suspend fun addRequestHeaders(request: HttpRequestBuilder, authHeader: HttpAuthHeader?) {
        val token = tokensHolder.loadToken() ?: return

        request.headers {
            val tokenValue = &quot;Bearer ${token.accessToken}&quot;
            if (contains(HttpHeaders.Authorization)) {
                remove(HttpHeaders.Authorization)
            }
            if (request.attributes.contains(AuthCircuitBreaker).not()) {
                append(HttpHeaders.Authorization, tokenValue)
            }
        }
    }

    public override suspend fun refreshToken(response: HttpResponse): Boolean {
        val newToken = tokensHolder.setToken(nonCancellableRefresh) {
            refreshTokens(RefreshTokensParams(response.call.client, response, tokensHolder.loadToken()))
        }
        return newToken != null
    }
	
	...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;a href=&quot;https://github.com/ktorio/ktor/blob/main/ktor-client/ktor-client-plugins/ktor-client-auth/common/src/io/ktor/client/plugins/auth/providers/BearerAuthProvider.kt&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&amp;lt;ktor-client-auth의 BearerAuthProvider.kt 중 일부&amp;gt;&lt;/a&gt;&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt; refreshTokens&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;이 블럭은 엑세스 토큰 갱신 로직을 수행하는 블럭&lt;/b&gt;이다. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;이것 또한, loadTokens와 마찬가지로 BearerTokens(access_token, refresh_token)을 반환해야하며, access_token은 담아 리턴해야한다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1772259899527&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@OptIn(InternalAPI::class)
public val Auth: ClientPlugin&amp;lt;AuthConfig&amp;gt; = createClientPlugin(&quot;Auth&quot;, ::AuthConfig) {
    val providers = pluginConfig.providers.toList()

    client.attributes.put(AuthProvidersKey, providers)

    val tokenVersions = ConcurrentMap&amp;lt;AuthProvider, AtomicCounter&amp;gt;()
    val tokenVersionsAttributeKey =
        AttributeKey&amp;lt;MutableMap&amp;lt;AuthProvider, Int&amp;gt;&amp;gt;(&quot;ProviderVersionAttributeKey&quot;)

	...
    
    suspend fun Send.Sender.executeWithNewToken(
        call: HttpClientCall,
        provider: AuthProvider,
        oldRequest: HttpRequestBuilder,
        authHeader: HttpAuthHeader?
    ): HttpClientCall {
        val request = HttpRequestBuilder()
        request.takeFromWithExecutionContext(oldRequest)
        provider.addRequestHeaders(request, authHeader)
        request.attributes.put(AuthCircuitBreaker, Unit)

        LOGGER.trace { &quot;Sending new request to ${call.request.url}&quot; }
        return proceed(request)
    } 
    
    suspend fun refreshTokenIfNeeded(
        call: HttpClientCall,
        provider: AuthProvider,
        request: HttpRequestBuilder
    ): Boolean {
        val tokenVersion = tokenVersions.computeIfAbsent(provider) { AtomicCounter() }
        val requestTokenVersions = request.attributes
            .computeIfAbsent(tokenVersionsAttributeKey) { mutableMapOf() }
        val requestTokenVersion = requestTokenVersions[provider]

        if (requestTokenVersion != null &amp;amp;&amp;amp; requestTokenVersion &amp;gt;= tokenVersion.atomic.value) {
            LOGGER.trace { &quot;Refreshing token for ${call.request.url}&quot; }
            if (!provider.refreshToken(call.response)) {
                LOGGER.trace { &quot;Refreshing token failed for ${call.request.url}&quot; }
                return false
            } else {
                requestTokenVersions[provider] = tokenVersion.atomic.incrementAndGet()
            }
        }
        return true
    }
    
    on(Send) { originalRequest -&amp;gt;
        val origin = proceed(originalRequest)
        if (!pluginConfig.isUnauthorizedResponse(origin.response)) return@on origin
        if (origin.request.attributes.contains(AuthCircuitBreaker)) return@on origin

        var call = origin

        val candidateProviders = HashSet(providers)

        while (pluginConfig.isUnauthorizedResponse(call.response)) {
            LOGGER.trace { &quot;Unauthorized response for ${call.request.url}&quot; }

            val (provider, authHeader) = findProvider(call, candidateProviders) ?: run {
                LOGGER.trace { &quot;Can not find auth provider for ${call.request.url}&quot; }
                return@on call
            }

            LOGGER.trace { &quot;Using provider $provider for ${call.request.url}&quot; }

            candidateProviders.remove(provider)
            if (!refreshTokenIfNeeded(call, provider, originalRequest)) return@on call
            call = executeWithNewToken(call, provider, originalRequest, authHeader)
        }
        return@on call
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;a href=&quot;https://github.com/ktorio/ktor/blob/main/ktor-client/ktor-client-plugins/ktor-client-auth/common/src/io/ktor/client/plugins/auth/Auth.kt&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&amp;lt;Auth.kt&amp;gt;&lt;/a&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;on(Send) 블럭에서 API를 호출한 resonse값을 pluginConfig.isUnauthorizedResponse() 메서드로 401이 발생했는지 아닌지 확인해서&amp;nbsp;401이 아니라면 return 하여 추가적인 로직을 수행하지 않는다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;401이 발생했다면, Providers 중에서 최신화된 토큰 counter을 사용하지 않는 것을 대상이 있다면 refreshTokenIfNeeded 메소드를 통해 업데이트한다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;refreshTokenIfNeeded 메소드에서 최신화된 Counter 값을 사용하여 401이 발생한 경우, refreshTokens 블럭이 콜백으로 호출된다. &lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt; &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;refreshTokenIfNeeded 가 호출되기 까지의 &lt;/span&gt;흐름을&lt;/span&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;&amp;nbsp;정리해보자면 아래와 같다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;2068&quot; data-start=&quot;1893&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1973&quot; data-start=&quot;1893&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;요청을 보낼 때(onRequest) &lt;b&gt;&amp;ldquo;이 요청이 어떤 토큰 버전으로 나갔는지&amp;rdquo; 스냅샷을&lt;/b&gt; request attribute에 &lt;b&gt;저장&lt;/b&gt;함.&lt;/span&gt;&lt;/li&gt;
&lt;li data-end=&quot;2057&quot; data-start=&quot;1974&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;401이 왔을 때(on(Send)), 무조건 refresh를 요청하면&amp;nbsp;&lt;b&gt;동시에 여러 요청이 401일 때 refresh 폭발&lt;/b&gt;이 날 수 있음.&lt;/span&gt;&lt;/li&gt;
&lt;li data-end=&quot;2068&quot; data-start=&quot;2058&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;그래서 아래의 케이스인지 비교:&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;2150&quot; data-start=&quot;2070&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;case 1) 이 요청이 보낸 토큰이 &amp;lsquo;최신 버전&amp;rsquo;이었다&lt;/b&gt;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&amp;rarr; 그럼 &amp;ldquo;진짜로 토큰이 만료된 것&amp;rdquo;일 가능성이 높으니 refresh 시도.&lt;/span&gt;&lt;/p&gt;
&lt;p data-end=&quot;2267&quot; data-start=&quot;2152&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;case 2) 이 요청이 보낸 토큰이 &amp;lsquo;최신이 아니게 되어버렸다&amp;rsquo;&lt;/b&gt; (다른 코루틴/요청이 이미 refresh 해버림)&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&amp;rarr; 굳이 또 refresh 하지 말고, &lt;b&gt;그냥 새 토큰으로 재시도.&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt; AuthCircuitBreaker로 &lt;b&gt;내가 재시도한 요청이 또 401이어도 다시 auth 로직 타지 않도록 무한 루프 방지.&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;- Refrences&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #9d9d9d;&quot;&gt;&lt;a style=&quot;color: #9d9d9d;&quot; href=&quot;https://ktor.io/docs/client-bearer-auth.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;ktor-client-auth 공식 문서&lt;/a&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #9d9d9d;&quot;&gt;&lt;a style=&quot;color: #9d9d9d;&quot; href=&quot;https://api.ktor.io/ktor-client-auth/io.ktor.client.plugins.auth.providers/-bearer-auth-provider/index.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;BearerAuthProvider 관련 코드&lt;/a&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #9d9d9d;&quot;&gt;&lt;a style=&quot;color: #9d9d9d;&quot; href=&quot;https://bayy1216.tistory.com/18&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;[Android]&amp;nbsp;Ktor&amp;nbsp;Client에서&amp;nbsp;Jwt&amp;nbsp;인증&amp;nbsp;로직&amp;nbsp;구현하기&lt;/a&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>안드로이드/KMP</category>
      <category>KMP</category>
      <category>KTOR</category>
      <category>ktor-auth</category>
      <author>Bum_2</author>
      <guid isPermaLink="true">https://bum2.tistory.com/187</guid>
      <comments>https://bum2.tistory.com/187#entry187comment</comments>
      <pubDate>Sat, 14 Feb 2026 00:26:29 +0900</pubDate>
    </item>
    <item>
      <title>[KMP] KMP에서 iOS 라이브러리는 어떻게 연결될까?</title>
      <link>https://bum2.tistory.com/182</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;Kotlin Multi Platform(KMP) 사용하다가 플랫폼 별로 라이브러리를 연결해야하는 상황에서 추가적인 작업이 필요하다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;Android 에서는 Gradle dependency로 간단하게 연결할 수 있지만, iOS에서는 어떻게 연결할지 알아보자.&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h3 data-end=&quot;440&quot; data-start=&quot;414&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;KMP에서 플랫폼별 구현이 필요한 이유&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p data-end=&quot;513&quot; data-start=&quot;442&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;KMP는 공통 Kotlin 코드를 작성하고,&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;각 플랫폼(Android / iOS)에서 실제 구현(actual)을 제공한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-end=&quot;519&quot; data-start=&quot;515&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;문제는 iOS SDK가 Swift 와 Objective-C 으로 구성되어있다는 점&lt;/b&gt;이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-end=&quot;519&quot; data-start=&quot;515&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;코틀린에서는 이를 직접 사용할 수 없기 때문에, 연결 과정이 필요하다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-end=&quot;519&quot; data-start=&quot;515&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-end=&quot;614&quot; data-start=&quot;565&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;iOS SDK가 KMP에 연결되는 흐름을 보면 아래와 같다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;486&quot; data-origin-height=&quot;746&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bbJXWB/dJMcaiIWWB9/NlPDK4kmKujkk0VpxP8dqK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bbJXWB/dJMcaiIWWB9/NlPDK4kmKujkk0VpxP8dqK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bbJXWB/dJMcaiIWWB9/NlPDK4kmKujkk0VpxP8dqK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbbJXWB%2FdJMcaiIWWB9%2FNlPDK4kmKujkk0VpxP8dqK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;315&quot; height=&quot;484&quot; data-origin-width=&quot;486&quot; data-origin-height=&quot;746&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;CocoaPods &lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;CoCoaPods는 iOS에서 사용하는 의존성 관리자&lt;/b&gt;다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;마치, 안드로이드의 Gradle과 같이 라이브러리를 설치 &amp;amp; 관리해준다고 생각하면 될 것 같다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;KMP에서 CocoaPods는 아래와 같은 역할을 한다.&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;iOS 네이티브 라이브러리 다운로드&lt;/span&gt;&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;Objective-C 헤더 노출&lt;/span&gt;&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;Kotlin Native가 접근할 수 있는 구조 제공&lt;/span&gt;&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;Gradle 에서는 아래와 같이 선언한다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1770474439481&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;cocoapods {
    pod(&quot;NMapsMap&quot;)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt; cinterop &lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;cinterop은 Kotlin Native 도구다&lt;/b&gt;.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;KMP에서 cinterop은 아래와 같은 역할을 한다.&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;Objective-C 헤더 분석&lt;/span&gt;&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;Kotlin 타입으로 변환&lt;/span&gt;&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;네이티브 함수 연결&lt;/span&gt;&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;예를 들어 iOS SDK에 아래와 같은 코드가 있다고 하자.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1770474515378&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@interface MapView
- (void)setZoom:(double)zoom;
@end&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;여기서 cinterop은 아래와 같이 바인딩 한다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1770474541678&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;val map = MapView()
map.setZoom(10.0)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;Swift SDK 역시 대부분 아래와 같은 과정을 거쳐서 연결된다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;Swift &amp;rarr; Objective-C 인터페이스 노출 &amp;rarr;&amp;nbsp;cinterop&amp;nbsp;처리&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;  처음에 cinterop이 바인딩 역할을 해준다고해서 RN의 브릿지와 어떤 차이가 있을까 궁금해졌다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;간략하게 정리하면,&amp;nbsp;&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;RN(Reat Native)의 브릿지는 JS &amp;harr; Native 런타임 연결 역할을한다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;반면, KMP의 cinterop은 컴파일 타임 네이티브 바인딩 역할을 한다.&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt; 두 방식 모두 네이티브 API를 호출한다는 공통점이 있지만,&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;React Native의 &lt;b&gt;브릿지&lt;/b&gt;는 &lt;b&gt;런타임에 JS와 네이티브 사이&lt;/b&gt;를 중계하는 &lt;b&gt;통신 계층&lt;/b&gt;이고,&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;KMP의 &lt;b&gt;cinterop&lt;/b&gt;은 &lt;b&gt;컴파일 단계&lt;/b&gt;에서 네이티브 API에 대한 &lt;b&gt;Kotlin 바인딩을 생성&lt;/b&gt;한다는 차이가 있다. &lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;위의 구조 덕분에 KMP에서는 Cocoapods를 사용하여 Kotlin 코드 베이스로 iOS의 라이브러리를 호출하여 확장할 수 있게 된다.&lt;/span&gt;&lt;/p&gt;</description>
      <category>안드로이드/KMP</category>
      <author>Bum_2</author>
      <guid isPermaLink="true">https://bum2.tistory.com/182</guid>
      <comments>https://bum2.tistory.com/182#entry182comment</comments>
      <pubDate>Wed, 28 Jan 2026 21:20:28 +0900</pubDate>
    </item>
    <item>
      <title>[안드로이드] MVI에서 Event 처리: Channel vs SharedFlow</title>
      <link>https://bum2.tistory.com/175</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR'; color: #333333;&quot;&gt;MVI(Model-View-Intent)로 앱을 만들다 보면, 사용자 입력(Event)을 처리할 때, Channel를 사용할지 SharedFlow 을 사용할지 고민이 되곤 했다. 이번 포스팅에서 이 둘의 차이점과 MVI패턴의 어떤 상황에서 사용하는 것이 유리할지 정리해보자.&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR'; color: #333333;&quot;&gt;&lt;b&gt;Channel&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR'; color: #333333;&quot;&gt;코루틴 간, &lt;b&gt;값을 전달하기 위해 설계된 통신 도구&lt;/b&gt;이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR'; color: #333333;&quot;&gt;핵심은 보내는 쪽과 받는 쪽의 역할이 명확히 분리되어있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR'; color: #333333;&quot;&gt;이게 무슨 말이냐면,&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR'; color: #333333;&quot;&gt;Channel에서는:&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR'; color: #333333;&quot;&gt;Sender는 send만 할 수 있다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR'; color: #333333;&quot;&gt;Receiver는 receive만 할 수 있다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR'; color: #333333;&quot;&gt;한 쪽이 다른 한 쪽의 상태를 직접 관찰하거나 침범할 수 없다는 것이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR'; color: #333333;&quot;&gt;즉, &lt;b&gt;생산자와 소비자가 &quot;서로를 모른다&quot;는 것&lt;/b&gt;이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR'; color: #333333;&quot;&gt;&lt;b&gt;특징&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR'; color: #333333;&quot;&gt;1. &lt;b&gt;소비 중심 모델&lt;/b&gt;이다&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR'; color: #333333;&quot;&gt;이벤트는 받는 쪽이 가져가야만 사라진다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR'; color: #333333;&quot;&gt;한 번 receive 된 값은 다시 받을 수 없다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR'; color: #333333;&quot;&gt;위 특성 때문에, 이벤트는 정확히 한 번만 처리된다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR'; color: #333333;&quot;&gt;2. &lt;b&gt;버퍼 크기&lt;/b&gt;(Capacity)&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR'; color: #333333;&quot;&gt;Channel은 버퍼 크기에 따라 동작이 달라진다.&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR'; color: #333333;&quot;&gt;RENDEZVOUS(0)&lt;/span&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR'; color: #333333;&quot;&gt;받는 쪽이 준비되지 않으면 send 가 suspend&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR'; color: #333333;&quot;&gt;BUFFERED&lt;/span&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR'; color: #333333;&quot;&gt;일정량이 버퍼에 쌓임&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR'; color: #333333;&quot;&gt;UNLIMITED&lt;/span&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR'; color: #333333;&quot;&gt;사실상 메모리 주의 필요&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR'; color: #333333;&quot;&gt;Channel은 이벤트가 얼마나 쌓일 수 있는지,&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR'; color: #333333;&quot;&gt;생산자가 소비자를 얼마나 기다릴지를 제어할 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR'; color: #333333;&quot;&gt;이는 &lt;b&gt;이벤트 발생 빈도가 큰 경우에 중요&lt;/b&gt;하다.&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR'; color: #333333;&quot;&gt;&lt;b&gt;SharedFlow&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR'; color: #333333;&quot;&gt;SharedFlow의 목표는 Flow를 여러 곳에서 동시에 수집할 수 있도록 하는 것이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR'; color: #333333;&quot;&gt;&lt;b&gt;특징&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR'; color: #333333;&quot;&gt;&lt;b&gt;Hot Flow&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR'; color: #333333;&quot;&gt;수집 여부와 상관없이 값을 emit한다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR'; color: #333333;&quot;&gt;값은 흘러가고 Collector는 그 흐름에 참여 한다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR'; color: #333333;&quot;&gt;&lt;b&gt;Broadcast Stream&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR'; color: #333333;&quot;&gt;하나의 emit에 대해서 여러 Collector에게 동시 전달이 가능하다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR'; color: #333333;&quot;&gt;Collector 간에는 서로 독립이다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR'; color: #333333;&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;┌─ Collector A&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Serif KR'; color: #333333;&quot;&gt;Emitter&amp;nbsp;──┼─&amp;nbsp;Collector&amp;nbsp;B&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Serif KR'; color: #333333;&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;└─ Collector C&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR'; color: #333333;&quot;&gt;&lt;b&gt;이벤트 정책&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR'; color: #333333;&quot;&gt;SharedFlow는 기본적으로 이벤트를 보장하지 않는다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR'; color: #333333;&quot;&gt;대신, 정책을 개발자가 선택할 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR'; color: #333333;&quot;&gt;&lt;b&gt;replay&lt;/b&gt;&lt;/span&gt;&lt;br /&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR'; color: #333333;&quot;&gt;새 Collector가 과거 이벤트를 받을지&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR'; color: #333333;&quot;&gt;&lt;b&gt;extraBufferCapacity&lt;/b&gt;&lt;/span&gt;&lt;br /&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR'; color: #333333;&quot;&gt;순간 폭주 흡수&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR'; color: #333333;&quot;&gt;&lt;b&gt;onBufferOverflow&lt;/b&gt;&lt;/span&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR'; color: #333333;&quot;&gt;넘치면 멈출지 / 버릴지&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR'; color: #333333;&quot;&gt;&lt;b&gt;SharedFlow는&lt;/b&gt; '스트림'을 전제로 하기 때문에,&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Serif KR'; color: #333333;&quot;&gt;기본적으로 &lt;b&gt;큐(버퍼)를 중심 개념으로 갖지 않는다&lt;/b&gt;.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Serif KR'; color: #333333;&quot;&gt;정책 상, 선택적으로 버퍼를 사용한다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Serif KR'; color: #333333;&quot;&gt; Channel은 처음부터 큐를 중심으로 설계된 구조여서&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Serif KR'; color: #333333;&quot;&gt;버퍼(capacity)가 통신 의미와 강하게 결합되어 있다.&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR'; color: #333333;&quot;&gt;Channel은 다음 조건에 잘 맞는다.&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR'; color: #333333;&quot;&gt;&lt;b&gt;이벤트는&lt;/b&gt; 반드시 &lt;b&gt;한 번만 처리&lt;/b&gt;되어야 할 때&lt;/span&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR'; color: #333333;&quot;&gt;저장, 제출, 결제 등&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR'; color: #333333;&quot;&gt;입력 순서가 의미 있을 때&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR'; color: #333333;&quot;&gt;단일 이벤트 처리 루프를 유지하고 싶을 때&lt;/span&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR'; color: #333333;&quot;&gt;Event &amp;rarr; Reduce &amp;rarr; State/Effect 흐름이 명확&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR'; color: #333333;&quot;&gt;이벤트 유실이나 중복 처리를 피하고 싶을 때&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR'; color: #333333;&quot;&gt;SharedFlow는 다음 조건에 잘 맞는다.&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR'; color: #333333;&quot;&gt;&lt;b&gt;이벤트가 연속적/빈번하게 발생&lt;/b&gt;할 때&lt;/span&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR'; color: #333333;&quot;&gt;검색어 입력, 스크롤, 드래그&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR'; color: #333333;&quot;&gt;최신 입력만 처리해도 될 때&lt;/span&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR'; color: #333333;&quot;&gt;이전 요청 취소하고 최신만 반영&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR'; color: #333333;&quot;&gt;Flow 연산자로 이벤트를 가공하고 싶을 때&lt;/span&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR'; color: #333333;&quot;&gt;debounce, throttle, flatMapLatest 등&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR'; color: #333333;&quot;&gt;여러 곳에서 이벤트를 받거나 관찰할 가능성이 있을 때&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR'; color: #333333;&quot;&gt;필자는 Channel과 SharedFlow를 고민하다가 SharedFlow를 선택하게 되었다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR'; color: #333333;&quot;&gt;그 이유는 아래와 같다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR'; color: #333333;&quot;&gt;&lt;b&gt;1. 안드로이드 생명주기와의 궁합&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR'; color: #333333;&quot;&gt;repeatOnLifecycle 같은 패턴과 자연스럽게 결합이 가능하다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR'; color: #333333;&quot;&gt; UI는 스트림을 &lt;b&gt;구독하고 해제하는 역할만&lt;/b&gt; 담당하고,&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Serif KR'; color: #333333;&quot;&gt;이 구독/해제의 타이밍이 화면의 활성/비활성 상태와 맞물리면서&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Serif KR'; color: #333333;&quot;&gt;마치 생명주기 이벤트에 반응하는 것처럼 보인다. &lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1767878396597&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;lifecycleScope.launch {
    repeatOnLifecycle(Lifecycle.State.STARTED) {
        viewModel.events.collect { event -&amp;gt;
            handle(event)
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR'; color: #333333;&quot;&gt;&lt;b&gt;2. debounce/ flatMapLatest 같은 체인 제공&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-end=&quot;1408&quot; data-start=&quot;1324&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR'; color: #333333;&quot;&gt;입력 이벤트를 스트림으로 다루는 경우,&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Serif KR'; color: #333333;&quot;&gt;debounce, flatMapLatest 같은 Flow 연산자를 바로 적용할 수 있다는 점이 크다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-end=&quot;1495&quot; data-start=&quot;1410&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR'; color: #333333;&quot;&gt;Channel에서도 consumeAsFlow()로 변환하면 유사한 처리는 가능하지만,&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Serif KR'; color: #333333;&quot;&gt;이 경우 스트림 가공을 위해 구조가 한 단계 더 복잡해진다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-end=&quot;1557&quot; data-start=&quot;1497&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR'; color: #333333;&quot;&gt;입력 이벤트를 &lt;b&gt;사용하기 편하게 가공한다는 관점&lt;/b&gt;에서는&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Serif KR'; color: #333333;&quot;&gt;SharedFlow가 더 자연스럽게 느껴졌다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR'; color: #333333;&quot;&gt;위의 이유 때문에 결국 안드로이드에서 UI-Compose의 궁합에 잘 맞게 된 것 같고 선택하게 된 계기가 되었다.&lt;/span&gt;&lt;/p&gt;</description>
      <category>안드로이드</category>
      <author>Bum_2</author>
      <guid isPermaLink="true">https://bum2.tistory.com/175</guid>
      <comments>https://bum2.tistory.com/175#entry175comment</comments>
      <pubDate>Mon, 22 Dec 2025 16:42:24 +0900</pubDate>
    </item>
    <item>
      <title>[안드로이드] JDK, JRE, JVM 그리고 Gradle JDK</title>
      <link>https://bum2.tistory.com/159</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR'; color: #000000;&quot;&gt;안드로이드 프로젝트 세팅을 위해, Gradle 파일에 JDK를 설정하는 부분이 있다. 이번 글에서는 Gradle에 JDK가 왜 필요한지, 간단한 관련 개념과 함께 포스팅하여 남기고자 한다.&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;JVM&amp;nbsp;&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR'; color: #000000;&quot;&gt;&lt;b&gt;Java Virtual Machine&lt;/b&gt; 으로 자바의 핵심이다. &lt;b&gt;.java 파일이 컴파일되어 생성된 .class 바이트코드를 실제로 실행하는 가상 머신&lt;/b&gt;이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR'; color: #000000;&quot;&gt;JVM 내부에는 다음의 요소들이 존재한다.&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR'; color: #000000;&quot;&gt;&lt;b&gt;클래스 로더 시스템&lt;/b&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR'; color: #000000;&quot;&gt;&lt;b&gt;런타임 데이터 영역&lt;/b&gt; (Heap, Stack, Method Area 등)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR'; color: #000000;&quot;&gt;&lt;b&gt;실행 엔진&lt;/b&gt; (Interpreter, JIT 컴파일러)&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR'; color: #000000;&quot;&gt;&lt;b&gt;자바 코드가 운영체제와 직접 연결되지 않고 실행할 수 있도록 해주는 엔진&lt;/b&gt;이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;JRE&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR'; color: #000000;&quot;&gt;&lt;b&gt;Java Runtime Environment&lt;/b&gt; 으로 &lt;b&gt;JVM에 자바 표준 라이브러리와 런타임 환경&lt;/b&gt;을 더한 개념이다.&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR'; color: #000000;&quot;&gt;JVM&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR'; color: #000000;&quot;&gt;표준 클래스 (&lt;span style=&quot;text-align: start;&quot;&gt;java.lang, java.util 등)&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR'; color: #000000;&quot;&gt;실행에 필요한 런타임 라이브러리&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR'; color: #000000;&quot;&gt;&lt;b&gt;JRE는 자바 프로그램을 실행하는데 필요한 최소 환경&lt;/b&gt;이며 &lt;b&gt;컴파일이나 디버깅과 같은 개발 도구는 포함하지 않는&lt;/b&gt;다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;JDK&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR'; color: #000000;&quot;&gt;&lt;b&gt;Java Development Kit&lt;/b&gt; 으로 &lt;b&gt;JRE에 개발 도구까지 포함한 패키지&lt;/b&gt;다.&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR'; color: #000000;&quot;&gt;JRE&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR'; color: #000000;&quot;&gt;javac (컴파일러)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR'; color: #000000;&quot;&gt;jdb (디버거)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR'; color: #000000;&quot;&gt;javadoc 등 각종 개발 도구&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR'; color: #000000;&quot;&gt;자바 개발자는 결국 JDK 를 설치해서 개발과 실행을 모두 수행하게 된다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR'; color: #000000;&quot;&gt;네이티브 안드로이드 프로젝트에서 build Gradle 파일을 보면 아래와 같이 어떤 JDK를 지정해야하는 코드를 볼 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1767408932716&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;android {
    compileOptions {
        sourceCompatibility = JavaVersion.VERSION_17
        targetCompatibility = JavaVersion.VERSION_17
    }
    kotlinOptions {
        jvmTarget = &quot;17&quot;
    }
    
    ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR'; color: #000000;&quot;&gt;먼저 이 설정은 실행 시점이 아닌 컴파일 시점에 사용된다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR'; color: #000000;&quot;&gt;JDK에 있는 개발도구인 javac나 kotlinc 컴파일러가 어떤 JVM 버전을 기준으로 코드를 해석하고, 어떤 바이트 코드를 만들어야 하는지 결정하는 설정이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt; compileOptions &lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;pre id=&quot;code_1767412532667&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;android {
    compileOptions {
        sourceCompatibility = JavaVersion.VERSION_17
        targetCompatibility = JavaVersion.VERSION_17
    }
    
    ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR'; color: #000000;&quot;&gt; 자바 코드에 대한 설정이다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;sourceCompatibility&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR'; color: #000000;&quot;&gt; 자바 소스 코드의 문법 기준이다. &lt;b&gt;어디까지의 Java 문법을 허용할 것인가&lt;/b&gt;를 정한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR'; color: #000000;&quot;&gt;예를 들어,&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR'; color: #000000;&quot;&gt;VERSION_8 : lambda 가능&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR'; color: #000000;&quot;&gt;VERSION_17 : record, sealed class 등 가능&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;targetCompatibility&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR'; color: #000000;&quot;&gt;생성되는 .class 바이트코드의 JVM 버전이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR'; color: #000000;&quot;&gt;이 &lt;b&gt;클래스 파일은 최소 어떤 JVM 에서 실행 가능하도록 할 것인가&lt;/b&gt;를 정한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR'; color: #000000;&quot;&gt;대부분의 경우, 문법과 실행 대상 JVM을 맞추기 때문에 sourceCompatibility 와 targetCompatibility는 동일하다.&lt;/span&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR'; color: #000000;&quot;&gt;여기서 궁금한 점은 굳이 이 둘을 따로 설정하도록 나눠놨다는 것이다.&amp;nbsp;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Serif KR'; color: #000000;&quot;&gt;찾아본 결과, 새로운 자바 문법을 사용하면서도 오래된 JVM 환경과의 호환성을 유지해야하는 경우를 지원하기 위해서다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Serif KR'; color: #000000;&quot;&gt;sourceCompatibility&amp;nbsp; = 11, targetCompatibility&amp;nbsp; = 8 와 같은 경우는 그나마 자주 사용되지만,&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Serif KR'; color: #000000;&quot;&gt;sourceCompatibility&amp;nbsp; = 17, targetCompatibility&amp;nbsp; = 11 와 같은 경우는 record나 sealed 문법을 사용할 수 없어서 잘 사용하지 않는다.&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt; kotlinOptions&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1767412623735&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;android {
    kotlinOptions {
        jvmTarget = &quot;17&quot;
    }
    
    ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR'; color: #000000;&quot;&gt;위의 블럭은 코틀린 컴파일러 전용 옵션이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR'; color: #000000;&quot;&gt;Kotlin은 JVM 바이트 코드로 컴파일되는데, 이 때 생성되는 .class 파일의 JVM 타겟 버전을 지정한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR'; color: #000000;&quot;&gt;즉, &lt;b&gt;코틀린 코드가 최소 JVM 몇 버전 부터 이해할 수 있는 .class 파일 형식으로 컴파일&lt;/b&gt;할지 설정한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR'; color: #000000;&quot;&gt; javac와 달리 kotlinc의 컴파일 옵션에는 source 버전을 별도로 지정할 필요가 없다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Serif KR'; color: #000000;&quot;&gt;이는 Kotlin의 언어 규칙이 컴파일 타임에 모두 처리&amp;middot;변환되어, 람다나 타입 추론과 같은 개념이 실행 시점(JVM)에는 남지 않도록 설계되었기 때문이다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Serif KR'; color: #000000;&quot;&gt;그 결과 JVM은 Kotlin 언어를 해석하지 않고, 최소한의 바이트코드 정보만을 기반으로 실행을 담당한다. &lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR'; color: #000000;&quot;&gt;아래와 같이 코틀린 컴파일러의 버전을 Gradle 플러그인/의존성이 결정하기만 하면, source 버전을 자동으로 컴파일러의 버전으로 따라간다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1767411701906&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;plugins {
    kotlin(&quot;android&quot;) version &quot;1.9.25&quot;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR'; color: #000000;&quot;&gt;지금까지 언급했던 내용은 빌드 결과물에 대한 버전 지정에 관한 내용이었다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR'; color: #000000;&quot;&gt;이제, Gradle JVM에 대해 알아보자.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR'; color: #000000;&quot;&gt;&lt;b&gt;Gradle JVM은 Gradle 이라는 빌드 도구 자체를 실행하는 JVM&lt;/b&gt;을 말한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR'; color: #000000;&quot;&gt;Gradle은 Java/Kotlin으로 작성된 JVM 어플리케이션이다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR'; color: #000000;&quot;&gt;즉, &lt;b&gt;실행 자체가 JVM 없이는 불가능&lt;/b&gt;하다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;967&quot; data-start=&quot;946&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR'; color: #000000;&quot;&gt;Gradle JVM은 다음을 실행한다:&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1084&quot; data-start=&quot;969&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;982&quot; data-start=&quot;969&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR'; color: #000000;&quot;&gt;Gradle core&lt;/span&gt;&lt;/li&gt;
&lt;li data-end=&quot;1014&quot; data-start=&quot;983&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR'; color: #000000;&quot;&gt;build.gradle.kts (Kotlin DSL)&lt;/span&gt;&lt;/li&gt;
&lt;li data-end=&quot;1053&quot; data-start=&quot;1015&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR'; color: #000000;&quot;&gt;Gradle plugin (AGP, Kotlin plugin 등)&lt;/span&gt;&lt;/li&gt;
&lt;li data-end=&quot;1066&quot; data-start=&quot;1054&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR'; color: #000000;&quot;&gt;각종 task 로직&lt;/span&gt;&lt;/li&gt;
&lt;li data-end=&quot;1084&quot; data-start=&quot;1067&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR'; color: #000000;&quot;&gt;테스트 JVM fork 관리&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR'; color: #000000;&quot;&gt;Gradle JVM 버전에 따라 컴파일에 직접적으로 영향을 주지는 않지만,&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR'; color: #000000;&quot;&gt; Kotlin plugin이 특정 JDK 이상 요구한다던지, AGP 8.x 이상은 그래들 JDK 가 최소 17 이상이어야 한다던지하는 간접적으로 영향을 미친다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR'; color: #000000;&quot;&gt;Gradle JVM 버전 설정은 Settings에서 Build Tools에서 Gradle JDK를 지정할 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR'; color: #000000;&quot;&gt;정리하면, &lt;b&gt;Gradle JVM은&lt;/b&gt; 빌드 결과물의 JVM 버전을 의미하는 것이 아니라,&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR'; color: #000000;&quot;&gt;&lt;b&gt; Gradle이라는 빌드 시스템 자체를 실행하는 JVM을 의미&lt;/b&gt;하며, &lt;b&gt;컴파일러와 타겟 JVM 설정과는 완전히 독립적인 개념&lt;/b&gt;이다.&lt;/span&gt;&lt;/p&gt;</description>
      <category>안드로이드</category>
      <category>android</category>
      <category>android studio</category>
      <category>Gradle</category>
      <category>gradle jdk</category>
      <category>jdk</category>
      <category>jre</category>
      <category>JVM</category>
      <author>Bum_2</author>
      <guid isPermaLink="true">https://bum2.tistory.com/159</guid>
      <comments>https://bum2.tistory.com/159#entry159comment</comments>
      <pubDate>Thu, 9 Oct 2025 16:53:23 +0900</pubDate>
    </item>
    <item>
      <title>[안드로이드] 안드로이드 스튜디오 무선 연결이 안될 때</title>
      <link>https://bum2.tistory.com/158</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;안드로이드 스튜디오에서 무선 연결이 가끔 안될 때가 있다. 와이파이를 다시 연결하던가 IDE를 종료 후 다시 실행시켜도 동작하지 않으면 아래와 같은 방법을 사용하자.&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;1. CMD 실행&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;먼저, CMD를 실행하자. 관리자로 실행하지 않아도 된다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;안드로이드 스튜디오를 설치할 때, 대부분 platform-tools를 함께 설치했을 것이다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;안되어있다면, &lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt; Android Studio &amp;rarr; Settings &amp;gt; Languages &amp;amp; Frameworks 에서 Android SDK Platform-Tools 항목을 클릭해 설치하자&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-style=&quot;style3&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;2. platform-tools 폴더로 이동&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;CMD&amp;nbsp; 창에서 아래 명령어를 입력하자. &amp;lt;계정 이름&amp;gt; 에는 독자가 사용하고 있는 계정 이름을 넣으면 된다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1764069268494&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;cd C:\Users\&amp;lt;계정 이름&amp;gt;\AppData\Local\Android\Sdk\platform-tools&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;대부분 AppData 경로에 있지만, 혹시나 해당 경로에 platform-tools이 없는 경우,&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt; 1. 항목에 있는 경로에 들어가면 ANdroid SDK Location을 알 수 있다. 여기에서 platform-tools path로 이동하면 된다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;(Android Studio &amp;rarr; Settings &amp;gt; Languages &amp;amp; Frameworks 에서 Android SDK Platform-Tools)&lt;/span&gt;&lt;/blockquote&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-style=&quot;style3&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;3. 디바이스 기기 페어하기 (보안 연결)&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;디바이스 설정에서 페어링 코드로 기기 페어링을 클릭하면 &amp;lt;사진 1&amp;gt; 같이 나오는데 DeviceIp : Port 를 가지고 페어를 할 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;명령어는 아래의 명령어를 입력하자.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1764070091750&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;adb pair DeviceIp:Port&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;507&quot; data-origin-height=&quot;290&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bpGdO8/dJMcadtDygq/jDNFfXz3KBQax5I9ofgRP1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bpGdO8/dJMcadtDygq/jDNFfXz3KBQax5I9ofgRP1/img.png&quot; data-alt=&quot;&amp;amp;lt;사진 1&amp;amp;gt;&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bpGdO8/dJMcadtDygq/jDNFfXz3KBQax5I9ofgRP1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbpGdO8%2FdJMcadtDygq%2FjDNFfXz3KBQax5I9ofgRP1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;322&quot; height=&quot;184&quot; data-origin-width=&quot;507&quot; data-origin-height=&quot;290&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;&amp;lt;사진 1&amp;gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;입력하면 기기 페어링 코드를 입력하는 문구가 나오는데, Wi-Fi 페어링 코드를 입력하자.&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;4. 디바이스 기기 연결하기 (실제 디버깅 연결)&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;3번 항목은 보안을 위해 진행하는 연결&lt;/b&gt;이며, &lt;b&gt;4번 항목이 실제로 기기를 연결하는 과정&lt;/b&gt;이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;3번 항목에서 사용했던 다이얼로그 창(기기 페어링)은 자동으로 사라지는데 안 사라졌다면 취소로 제거하자.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;그러면 &amp;lt;사진 2&amp;gt; 와 같은 화면으로 되돌아오는데 그 곳에서 Device Ip : Port를 사용하면 된다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;명령어는 아래의 명령어를 입력하자.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1764070656393&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;adb connect DeviceIp:Port&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;506&quot; data-origin-height=&quot;1088&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Qtnzl/dJMcacas3CJ/ClUsDIBpxUk15OWC5eD2a0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Qtnzl/dJMcacas3CJ/ClUsDIBpxUk15OWC5eD2a0/img.png&quot; data-alt=&quot;&amp;amp;lt;사진 2&amp;amp;gt;&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Qtnzl/dJMcacas3CJ/ClUsDIBpxUk15OWC5eD2a0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FQtnzl%2FdJMcacas3CJ%2FClUsDIBpxUk15OWC5eD2a0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;354&quot; height=&quot;761&quot; data-origin-width=&quot;506&quot; data-origin-height=&quot;1088&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;&amp;lt;사진 2&amp;gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>안드로이드</category>
      <category>무선 연결</category>
      <category>무선 페어링</category>
      <category>안드로이드 스튜디오</category>
      <category>페어링</category>
      <author>Bum_2</author>
      <guid isPermaLink="true">https://bum2.tistory.com/158</guid>
      <comments>https://bum2.tistory.com/158#entry158comment</comments>
      <pubDate>Thu, 2 Oct 2025 18:02:04 +0900</pubDate>
    </item>
    <item>
      <title>[안드로이드] Gson과 kotlinx.serialization 차이</title>
      <link>https://bum2.tistory.com/155</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;안드로이드에서 Retrofit을 주입할 때, ConverterFactory로 Gson을 사용할지 kotlinx.serialization 중에 어떤걸 사용할지 고민한 경험이 있다. 이 둘은 어떤 목적에 따라 사용하는지 정리해보자.&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;256&quot; data-start=&quot;178&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;네트워크 통신을 수행하기 위해서는 서버와 데이터를 주고받아야 하며, 안드로이드에서는 일반적으로 &lt;b&gt;Retrofit&lt;/b&gt;을 사용한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-end=&quot;410&quot; data-start=&quot;263&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;이때 응용 계층에서는 주로 &lt;b&gt;JSON&lt;/b&gt;이나 &lt;b&gt;XML&lt;/b&gt;과 같은 데이터 형식을 통해 정보를 교환한다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;하지만 &lt;b&gt;Java나 Kotlin은 객체 지향 언어&lt;/b&gt;이기 때문에, &lt;b&gt;네트워크로 전달받은 데이터를&lt;/b&gt; &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;객체 형태로 변환하여 관리&lt;/b&gt;&lt;/span&gt;해야 한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-end=&quot;576&quot; data-start=&quot;417&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;따라서 이러한 &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;데이터 직렬화 과정을 수행&lt;/b&gt;&lt;/span&gt;할 때, &lt;b&gt;Gson&lt;/b&gt; 또는 &lt;b&gt;kotlinx.serialization&lt;/b&gt;과 같은 라이브러리를 사용하게 되며, &lt;b&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;두 라이브러리의 차이는 바로 객체와 JSON 간 변환을 수행하는 방식&lt;/span&gt;&lt;/b&gt;에 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-end=&quot;576&quot; data-start=&quot;417&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-end=&quot;134&quot; data-start=&quot;100&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt; &lt;b&gt;kotlinx.serialization&amp;nbsp;&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-end=&quot;134&quot; data-start=&quot;100&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; font-family: 'Noto Serif KR';&quot;&gt;kotlinx.serialization은 &lt;b&gt;Kotlin 언어 공식 직렬화&lt;/b&gt;(Serialization) &lt;b&gt;라이브러리&lt;/b&gt;다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-end=&quot;134&quot; data-start=&quot;100&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;Kotlin 객체를 JSON, ProtoBuf, CBOR 등 다양한 데이터 형식으로 변환하거나 반대로 역직렬화(Deserialization) 하기 위해 사용한다. &lt;/span&gt;&lt;span style=&quot;color: #333333;&quot;&gt;JetBrains 이 직접 개발 및 유지보수하며, &lt;/span&gt;&lt;span style=&quot;color: #333333;&quot;&gt;Kotlin 컴파일러와 함께 사용하는 &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;컴파일 시점에 변환 코드를 자동 생성&lt;/b&gt;&lt;/span&gt;하는 것이 가장 큰 특징이다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-end=&quot;134&quot; data-start=&quot;100&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&gt;
&lt;h3 data-end=&quot;448&quot; data-start=&quot;430&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt; &lt;b&gt;주요 특징 정리&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-end=&quot;476&quot; data-start=&quot;450&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;1. Kotlin 공식 지원&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;612&quot; data-start=&quot;477&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;520&quot; data-start=&quot;477&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;JetBrains에서 개발한 &lt;b&gt;Kotlin 전용 라이브러리&lt;/b&gt;.&lt;/span&gt;&lt;/li&gt;
&lt;li data-end=&quot;612&quot; data-start=&quot;521&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;Kotlin 컴파일러와 통합되어 있기 때문에, Kotlin 코드 스타일 및 기능(예: data class, sealed class 등)과 자연스럽게 연동.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;663&quot; data-start=&quot;619&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;2. 컴파일 타임 코드 생성&amp;nbsp;&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;810&quot; data-start=&quot;664&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;747&quot; data-start=&quot;664&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;@Serializable 어노테이션이 붙은 클래스에 대해, Kotlin 컴파일러가 &lt;b&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;JSON 변환 로직을 자동으로 생성&lt;/span&gt;.&lt;/b&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li data-end=&quot;810&quot; data-start=&quot;748&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;런타임에 리플렉션(reflection)을 사용하지 않으므로 &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;속도가 빠르고, 메모리 사용량이 적다.&lt;/b&gt;&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1761748119740&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Serializable
data class User(val name: String, val platForm: String)

// 직렬화 (encode)
Json.encodeToString(User(&quot;Bum2&quot;, &quot;Tistory&quot;))
// &amp;rarr; {&quot;name&quot;:&quot;Bum2&quot;,&quot;platForm&quot;:&quot;Tistory&quot;}

// 역직렬화 (decode)
Json.decodeFromString&amp;lt;User&amp;gt;(&quot;&quot;&quot;{&quot;name&quot;:&quot;Bum2&quot;,&quot;platForm&quot;:&quot;Tistory&quot;}&quot;&quot;&quot;)
// &amp;rarr; User(name=&quot;Bum2&quot;, platForm=&quot;Tistory&quot;)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-end=&quot;841&quot; data-start=&quot;817&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;841&quot; data-start=&quot;817&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;3. 플러그인 설정 필요&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;931&quot; data-start=&quot;842&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;931&quot; data-start=&quot;842&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;코드를 자동 생성하기 위해선 org.jetbrains.kotlin.plugin.serialization &lt;b&gt;플러그인을 Gradle에 등록&lt;/b&gt;해야 한다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre id=&quot;code_1761748064669&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// Data Module Gradle

plugins { id(&quot;org.jetbrains.kotlin.plugin.serialization&quot;) }&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;1509&quot; data-start=&quot;1476&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;1509&quot; data-start=&quot;1476&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;4. 타입 안정성과 Null 안전성 보장&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1650&quot; data-start=&quot;1510&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1567&quot; data-start=&quot;1510&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;컴파일 시점에 직렬화 가능한 필드와 타입을 검사&lt;/b&gt;하여, &lt;b&gt;런타임 오류를 최소화&lt;/b&gt;.&lt;/span&gt;&lt;/li&gt;
&lt;li data-end=&quot;1650&quot; data-start=&quot;1568&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;ignoreUnknownKeys, explicitNulls 등의 설정으로 서버 응답에도 유연하게 대응.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre id=&quot;code_1761748418973&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;val json = Json {
    ignoreUnknownKeys = true // 서버에서 불필요한 키가 와도 무시
    explicitNulls = false // null 필드는 생략
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;1825&quot; data-start=&quot;1788&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;5. Kotlin Multiplatform 지원&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1947&quot; data-start=&quot;1826&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1918&quot; data-start=&quot;1826&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;JVM(Android)뿐만 아니라 JS, iOS, Native 등 &lt;b&gt;Kotlin Multiplatform 프로젝트에서도 사용 가능&lt;/b&gt;.&lt;/span&gt;&lt;/li&gt;
&lt;li data-end=&quot;1947&quot; data-start=&quot;1919&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;Gson이나 Moshi보다 확장성이 뛰어남.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;1981&quot; data-start=&quot;1954&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;6. Retrofit과의 연동&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;2023&quot; data-start=&quot;1982&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;2023&quot; data-start=&quot;1982&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;Retrofit에서 ConverterFactory로 연동 가능:&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre id=&quot;code_1761748766812&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Provides
@Singleton
@Bum2
fun provideSeraRetrofit(
    okHttpClient: OkHttpClient,
    json: Json,
    @Bum2 baseUrl: String,
): Retrofit {
    return Retrofit.Builder()
        .baseUrl(baseUrl)
        .client(okHttpClient)
        .addConverterFactory(
            json.asConverterFactory((requireNotNull(&quot;application/json&quot;.toMediaTypeOrNull()))),
        )
        .build()
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot; data-start=&quot;1016&quot; data-end=&quot;1043&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;7. 다양한 데이터 포맷 지원&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot; data-start=&quot;1044&quot; data-end=&quot;1115&quot;&gt;
&lt;li data-start=&quot;1044&quot; data-end=&quot;1115&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;단순 JSON뿐만 아니라,&amp;nbsp;CBOR, ProtoBuf, Properties 등 다양한 포맷으로 직렬화 가능.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;div style=&quot;color: #333333; text-align: start;&quot;&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style12&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;항목&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;설명&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;이름&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;kotlinx.serialization&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;만든 곳&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;JetBrains (Kotlin 공식)&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;용도&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;Kotlin 객체 &amp;harr; JSON/CBOR/ProtoBuf 등 데이터 형식 변환&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;작동 시점&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;컴파일 타임 (Code Generation)&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;핵심 특징&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;빠름, 타입 안전, 리플렉션 없음, 멀티플랫폼 지원&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;Retrofit 연동법&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;.addConverterFactory(json.asConverterFactory(...))&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;플러그인 필요 여부&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;org.jetbrains.kotlin.plugin.serialization 등록 필수&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style2&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-end=&quot;124&quot; data-start=&quot;107&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;Gson&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #666666; text-align: start;&quot; data-start=&quot;128&quot; data-end=&quot;306&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;b&gt;Gson&lt;/b&gt;은&amp;nbsp;&lt;b&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;Google에서 개발한 JSON 직렬화&lt;/span&gt;(Serialization) 및 역직렬화(Deserialization) &lt;span style=&quot;color: #ee2323;&quot;&gt;라이브러리&lt;/span&gt;다.&lt;/b&gt;&lt;/span&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #666666; text-align: start;&quot; data-start=&quot;311&quot; data-end=&quot;402&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;Retrofit이나 OkHttp 같은 네트워크 라이브러리와 함께 사용되어 &lt;/span&gt;&lt;span style=&quot;color: #333333;&quot;&gt;서버에서 주고받는 JSON 데이터를 자동으로 객체 형태로 처리할 수 있게 해준다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&gt;
&lt;h3 data-end=&quot;427&quot; data-start=&quot;409&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;주요 특징 정리&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-end=&quot;463&quot; data-start=&quot;429&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;1. Google에서 개발한 JSON 파서&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;574&quot; data-start=&quot;464&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;516&quot; data-start=&quot;464&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&amp;ldquo;Google JSON&amp;rdquo;의 약자로, &lt;b&gt;Google이 직접 개발&lt;/b&gt;한 라이브러리.&amp;nbsp;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;622&quot; data-start=&quot;581&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;2. 리플렉션(Reflection) 기반의 런타임 변환&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;794&quot; data-start=&quot;623&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;687&quot; data-start=&quot;623&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;Gson은 &lt;b&gt;앱이 실행 중일 때(런타임)&lt;/b&gt; 클래스의 필드 정보를 읽어(JSON 키와 매칭) 객체를 생성.&lt;/span&gt;&lt;/li&gt;
&lt;li data-end=&quot;794&quot; data-start=&quot;688&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;컴파일 시점에 코드를 생성하지 않고, &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;런타임에 리플렉션을 사용&lt;/b&gt;&lt;/span&gt;하기 때문에 &lt;b&gt;설정이 단순하고 빠르게 적용&lt;/b&gt;가능.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;하지만 &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;런타임 오버헤드(성능 비용)는 약간 존재&lt;/b&gt;&lt;/span&gt;.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;
&lt;div&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;3. Retrofit과의 쉬운 연동&lt;/b&gt;&lt;/span&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1146&quot; data-start=&quot;1106&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1146&quot; data-start=&quot;1106&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;별도의 플러그인 설정 없이 &lt;b&gt;한 줄 의존성으로 바로 사용&lt;/b&gt; 가능.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre id=&quot;code_1761749578891&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// Data Module dependencies
implementation(&quot;com.squareup.retrofit2:converter-gson:2.11.0&quot;)

@Provides
@Singleton
@Bum2
fun provideRetrofit(
    okHttpClient: OkHttpClient,
    @Bum2 baseUrl: String,
): Retrofit {
    return Retrofit.Builder()
        .baseUrl(baseUrl)
        .client(okHttpClient)
        .addConverterFactory(GsonConverterFactory.create())
        .build()
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;1477&quot; data-start=&quot;1452&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;4. 어노테이션 기반 매핑&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1559&quot; data-start=&quot;1478&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1559&quot; data-start=&quot;1478&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;JSON 키 이름과 객체 프로퍼티 이름이 다를 때는 @SerializedName 어노테이션을 사용해 매핑.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre id=&quot;code_1761749831557&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;data class User(
    @SerializedName(&quot;user_name&quot;) val name: String,
    @SerializedName(&quot;user_age&quot;) val age: Int
)&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;1724&quot; data-start=&quot;1696&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;1724&quot; data-start=&quot;1696&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;5. 유연한 설정과 커스터마이징&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1771&quot; data-start=&quot;1725&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1771&quot; data-start=&quot;1725&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;날짜 포맷, 널 처리, pretty print, 필드 제외 등 다양한 옵션 제공&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1761749873515&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;val gson = GsonBuilder()
    .setPrettyPrinting()
    .serializeNulls()
    .create()&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style12&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;항목&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;설명&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;이름&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;Gson (Google JSON)&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;만든 곳&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;Google&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;용도&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;Java/Kotlin 객체 &amp;harr; JSON 데이터 형식 변환&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;작동 시점&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;런타임 (Reflection 기반)&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;핵심 특징&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;설정이 간단, 직관적인 사용법, 런타임 리플렉션 사용, 널 처리 및 매핑 유연&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;Retrofit 연동법&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;.addConverterFactory(GsonConverterFactory.create())&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;플러그인 필요 여부&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;별도 플러그인 불필요 (의존성 추가만으로 사용 가능)&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-end=&quot;352&quot; data-start=&quot;158&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;322&quot; data-start=&quot;166&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;결국 &lt;b&gt;Gson&lt;/b&gt;과 &lt;b&gt;kotlinx.serialization&lt;/b&gt;은 &lt;b&gt;객체 &amp;harr; JSON으로 변환하기 위한 도구&lt;/b&gt;다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;Gson&lt;/b&gt;은 &lt;b&gt;런타임에 리플렉션을 사용해 변환을 수행&lt;/b&gt;한다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;별도의 설정 없이 빠르게 적용할 수 있다는 장점이 있지만, 앱이 실행 중일 때 클래스 구조를 탐색하므로 &lt;b&gt;약간의 성능 오버헤드&lt;/b&gt;가 발생한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-end=&quot;634&quot; data-start=&quot;472&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;반면 &lt;b&gt;kotlinx.serialization&lt;/b&gt;은 &lt;b&gt;컴파일 시점&lt;/b&gt;에 변환 코드를 미리 생성한다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;Kotlin 컴파일러와 직접 통합되어 &lt;b&gt;리플렉션 없이 빠르고 타입 안정적으로 작동&lt;/b&gt;하며, 멀티플랫폼 환경에서도 동일한 직렬화 로직을 공유할 수 있다는 강점을 지닌다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-end=&quot;815&quot; data-start=&quot;643&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>안드로이드</category>
      <author>Bum_2</author>
      <guid isPermaLink="true">https://bum2.tistory.com/155</guid>
      <comments>https://bum2.tistory.com/155#entry155comment</comments>
      <pubDate>Tue, 23 Sep 2025 19:01:35 +0900</pubDate>
    </item>
    <item>
      <title>[안드로이드] Gradle 이란</title>
      <link>https://bum2.tistory.com/154</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;안드로이드 개발을 하다보면 그래들 파일에 대해서 자주 접하게 되는데 Gradle이 뭔지 알아보자.&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;Gradle&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;소프트웨어를 빌드할 수 있는 오픈소스 빌드 자동화 툴&lt;/span&gt;&lt;/b&gt;이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;안드로이드에서 빌드란 &lt;span style=&quot;color: #ee2323;&quot;&gt;.&lt;b&gt;apk 파일을 만드는 과정&lt;/b&gt;&lt;/span&gt;이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;인텔리제이 IDE는 다양한 빌드 자동화 도구(Maven, Ant, Gradle)를 지원하지만, 안드로이드 스튜디오 IDE는 빌드 배포 시스템으로 Gradle만 사용할 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;Gradle 빌드 과정&amp;nbsp;&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;Gradle의 빌드 진행 과정은 다음과 같다.&lt;/span&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;초기화&lt;/span&gt;(Initialization)&lt;/b&gt; : 어떤 프로젝트들을 빌드할지 결정하고, 각 프로젝트에 대한 초기 환경을 설정하는 단계이다. 즉, 멀티 모듈 구조에서 &lt;b&gt;어떤 모듈을 빌드에 포함할지 결정&lt;/b&gt;하고, 각 모듈의 Gradle Project 객체를 구성하는 단계.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;구성&lt;/span&gt;(Configutation)&lt;/b&gt; : 초기화 단계에서 생성된 &lt;b&gt;각 모듈의 build.gradle&lt;/b&gt;(또는 build.gradle.kts) &lt;b&gt;파일을 실행&lt;/b&gt;해, 빌드에 필요한 &lt;b&gt;Task를 구성하고 의존 관계를 설정&lt;/b&gt;하는 단계.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;실행&lt;/span&gt;(Execution)&lt;/b&gt; : 구성 단계에서 생성된 Task를 기반으로, 사용자가 요청한 Task와 그에 의존하는 &lt;b&gt;Task를 실제로 실행하여 빌드를 수행하는 단계&lt;/b&gt;.&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;1. 초기화 단계&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt; 어떤 프로젝트들을 빌드할지 결정&lt;/b&gt;&lt;/span&gt;하고, 각 프로젝트에 대한 초기 환경을 설정하는 단계이다. 즉, 멀티 모듈 구조에서 어떤 모듈들을 빌드에 포함할지 결정하고, &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;각 모듈의 Gradle Project 객체를 구성하는 단계&lt;/b&gt;&lt;/span&gt;이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;211&quot; data-start=&quot;98&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;첫 번째로, settings.gradle.kts 파일이 실행되어 include(&quot;:app&quot;, &quot;:data&quot;, &quot;:domain&quot;) 등의 코드를 통해 &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;빌드에 포함할 모듈을 결정&lt;/b&gt;&lt;/span&gt;한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-end=&quot;325&quot; data-start=&quot;218&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;이 과정에서 &lt;b&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;Gradle은 각 모듈에 대응하는 Project 객체를 생성&lt;/span&gt;&lt;/b&gt;하고, 이를 기반으로 &lt;b&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;전체 빌드의&lt;/span&gt; &lt;span style=&quot;color: #ee2323;&quot;&gt;프로젝트 트리를 구성&lt;/span&gt;&lt;/b&gt;한다.&lt;/span&gt;&lt;/p&gt;
&lt;div style=&quot;background-color: #1e1f22; color: #bcbec4;&quot;&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;//settings.gradle.kts 코드 예시
pluginManagement {
    ...
}
dependencyResolutionManagement {
   ...
}

rootProject.name = &quot;Bum2_tistory&quot;
include(&quot;:app&quot;)
include(&quot;:data&quot;)
include(&quot;:domain&quot;)
include(&quot;:core&quot;)&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;2. 구성 단계&amp;nbsp;&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt; Project 객체에 빌드 설정을 적용하는 단계&lt;/span&gt;&lt;/b&gt;. &lt;b&gt;Gradle은 각 모듈의 build.gradle&lt;/b&gt;(또는 build.gradle.kts) &lt;b&gt;파일을 실행&lt;/b&gt;하여, 초기화 단계에서 생성된 Project 객체에 Android Gradle Plugin(AGP)을 포함한 &lt;b&gt;플러그인과 빌드 설정을 적용&lt;/b&gt;한다.&lt;/span&gt;&lt;/p&gt;
&lt;div style=&quot;background-color: #1e1f22; color: #bcbec4;&quot;&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;// Domain 모듈 build.gradle.kts 예시
plugins {
    alias(libs.plugins.android.library)
    alias(libs.plugins.kotlin.android) 
}

android {
    namespace = &quot;com.tistory.bum2&quot;
    compileSdk = 35

    defaultConfig {
        minSdk = 28

        testInstrumentationRunner = &quot;androidx.test.runner.AndroidJUnitRunner&quot;
        consumerProguardFiles(&quot;consumer-rules.pro&quot;)
    }

    packaging {
        resources.excludes.add(&quot;META-INF/gradle/incremental.annotation.processors&quot;)
    }

    buildTypes {
        release {
            isMinifyEnabled = false
            proguardFiles(
                getDefaultProguardFile(&quot;proguard-android-optimize.txt&quot;),
                &quot;proguard-rules.pro&quot;,
            )
        }
    }
    buildFeatures {
        buildConfig = true
    }
    compileOptions {
        sourceCompatibility = JavaVersion.VERSION_17
        targetCompatibility = JavaVersion.VERSION_17
    }
    kotlinOptions {
        jvmTarget = &quot;17&quot;
    }
}

dependencies {
    testImplementation(libs.junit) 
    
    // paging
    implementation(libs.androidx.paging.compose)

    // timber
    implementation(libs.timber)

    implementation(libs.javax.inject)
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;build.gradle 파일의 각 블럭으로 Project 객체의 메서드가 호출&lt;/b&gt;된다. 각 블럭의 속성을 정리해보자면 다음과 같다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;plugins 블록&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;Android Gradle Plugin(AGP)&lt;/b&gt; 과 &lt;b&gt;Kotlin Plugin&lt;/b&gt; 등 필요한 플러그인을 프로젝트에 적용.&lt;/span&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li data-end=&quot;275&quot; data-start=&quot;211&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;id(&quot;com.android.application&quot;) : Android 애플리케이션 빌드를 위한 플러그인&lt;/span&gt;&lt;/li&gt;
&lt;li data-end=&quot;342&quot; data-start=&quot;276&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;id(&quot;org.jetbrains.kotlin.android&quot;) : Kotlin 언어를 지원하기 위한 플러그인&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;여기서는 Android 애플리케이션 빌드를 위한 Android Plugin과 Kotlin 언어를 사용하기 위한 Kotlin Plugin이 적용되어 있습니다.&amp;nbsp;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;android 블록&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;Android Gradle Plugin(AGP)&lt;/b&gt; 이 제공하는 DSL로, 애플리케이션의 전반적인 빌드 설정을 정의하는 핵심 영역&lt;/span&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;namespace&lt;/b&gt;: 애플리케이션의 기본 패키지 이름&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;compileSdk&lt;/b&gt;: 컴파일 시 사용할 Android API 버전&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;defaultConfig { ... }:&lt;/b&gt; 앱의 기본 설정을 정의 (예: applicationId, minSdk, targetSdk, versionCode, versionName) &lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;buildTypes { ... }&lt;/b&gt;: 빌드 유형 정의 (예: release 빌드에서 isMinifyEnabled, ProGuard 설정 등) &lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;compileOptions { ... }&lt;/b&gt;: Java 소스 및 대상 호환성 지정 &lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;kotlinOptions { ... }:&lt;/b&gt; Kotlin 컴파일러 옵션 지정. (jvmTarget 등) &lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;buildFeatures { ... }:&lt;/b&gt; 빌드 기능을 활성화/비활성화 (예: Compose 활성화) &lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;composeOptions { ... }:&lt;/b&gt; Compose 관련 컴파일러 확장 버전 설정 &lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;packaging { ... }:&lt;/b&gt; 패키징 시 특정 리소스나 파일을 제외하도록 설정 &amp;nbsp;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;dependencies 블록&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt; dependencies 블록은 &lt;b&gt;프로젝트가 빌드될 때 필요한 외부 라이브러리나 모듈 의존성&lt;/b&gt;을 정의&amp;nbsp;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;3. 실행 단계&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt; 구성단계에서 생성&amp;middot;설정된 모든 Project의 Task 그래프 중에서, &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;사용자가 요청한 Task 및 그 의존 Task를 실제로 실행&lt;/b&gt;&lt;/span&gt;하는 단계.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;안드로이드 스튜디오에서 ./gradlew build --scan&amp;nbsp; 명령어를 사용하면, 아래와 같이 독자의 프로젝트의 빌드 단계 과정에 따라 시간이 측정된다.&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;586&quot; data-origin-height=&quot;205&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/k99eq/dJMb9WelJGq/gimkwZYhkhDECR3Ay2CD50/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/k99eq/dJMb9WelJGq/gimkwZYhkhDECR3Ay2CD50/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/k99eq/dJMb9WelJGq/gimkwZYhkhDECR3Ay2CD50/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fk99eq%2FdJMb9WelJGq%2FgimkwZYhkhDECR3Ay2CD50%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;586&quot; height=&quot;205&quot; data-origin-width=&quot;586&quot; data-origin-height=&quot;205&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style7&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;Gradle이 Task 기반으로 동작하는 이유&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;Gradle은 작업 단위를 명확히 정의하고, &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;그들 간의 의존 관계를 그래프 형태로 관리하기 위해 Task 기반으로 설계&lt;/b&gt;&lt;/span&gt;되었다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;Task는 독립적인 빌드 단위&lt;/b&gt;&lt;/span&gt;다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;예를 들어 다음과 같은 Task가 존재한다.&lt;/span&gt;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style12&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;Task&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;입력&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;출력&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;compileDebugKotlin&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;.kt 파일&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;.class 파일&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;mergeResources&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;XML/이미지 등&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;병합된 리소스 폴더&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;packageDebug&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;클래스&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;.apk&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;위와 같이, &lt;b&gt;각 Task의 입력과 출력을 명시적으로 추적&lt;/b&gt;하기 때문에, &lt;b&gt;변경이 감지되지 않은 Task는&lt;/b&gt; 재실행하지 않고 결과를 &lt;b&gt;재활용&lt;/b&gt;할 수 있다. 따라서 이 Task기반 구조 덕분에 &lt;b&gt;변경된 부분만 빌드함으로써 증분 빌드가 가능&lt;/b&gt;하다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p id=&quot;92cb&quot; style=&quot;background-color: #ffffff; color: #242424; text-align: start;&quot; data-testid=&quot;storyTitle&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #9d9d9d; font-family: 'Noto Serif KR';&quot;&gt;&lt;a style=&quot;color: #9d9d9d;&quot; href=&quot;https://medium.com/@renovatio0424/%EC%95%88%EB%93%9C%EB%A1%9C%EC%9D%B4%EB%93%9C-%EA%B0%9C%EB%B0%9C%EC%9E%90%EB%9D%BC%EB%A9%B4-%EC%95%8C%EC%95%84%EC%95%BC-%ED%95%98%EB%8A%94-gradle-%EC%9B%90%EB%A6%AC-c39a3299ba6d&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;안드로이드 개발자라면 알아야 하는 Gradle 원리&lt;/a&gt;&lt;/span&gt;&lt;/p&gt;</description>
      <category>안드로이드</category>
      <author>Bum_2</author>
      <guid isPermaLink="true">https://bum2.tistory.com/154</guid>
      <comments>https://bum2.tistory.com/154#entry154comment</comments>
      <pubDate>Tue, 16 Sep 2025 18:13:11 +0900</pubDate>
    </item>
  </channel>
</rss>