<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>O_Gyong's TECH</title>
    <link>https://ogyong.tistory.com/</link>
    <description>안드로이드 기술 정리</description>
    <language>ko</language>
    <pubDate>Fri, 10 Jul 2026 06:32:07 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>O_Gyong</managingEditor>
    <item>
      <title>[Android] Clip 속성 알아보기: ClipChildren, ClipToOutline, ClipToPadding</title>
      <link>https://ogyong.tistory.com/93</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;안드로이드에서 UI 작업을 하다 보면 View의 경계를 다뤄야 할 일들이 종종 있는데, 이때 Clip 네이밍이 들어간 속성 (ClipChildren, ClipToOutline, ClipToPadding) 들이 도움을 준다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;ClipChildren&lt;/b&gt;&lt;/h3&gt;
&lt;pre id=&quot;code_1770778483516&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;?xml version=&quot;1.0&quot; encoding=&quot;utf-8&quot;?&amp;gt;
&amp;lt;androidx.constraintlayout.widget.ConstraintLayout 
    android:id=&quot;@+id/main&quot;
    android:layout_width=&quot;match_parent&quot;
    android:layout_height=&quot;match_parent&quot;&amp;gt;

    &amp;lt;FrameLayout
        android:id=&quot;@+id/fl_children&quot;
        android:layout_width=&quot;250dp&quot;
        android:layout_height=&quot;200dp&quot;
        android:background=&quot;@color/blue&quot;&amp;gt;

        &amp;lt;TextView
            android:layout_width=&quot;150dp&quot;
            android:layout_height=&quot;200dp&quot;
            android:layout_marginTop=&quot;100dp&quot;
            android:background=&quot;@color/red&quot;
            android:text=&quot;Children&quot; /&amp;gt;
    &amp;lt;/FrameLayout&amp;gt;
&amp;lt;/androidx.constraintlayout.widget.ConstraintLayout&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;안드로이드에서 기본적으로 자식 View는 부모 View의 경계를 넘어갈 수 없다. 그래서 위의 xml 내용에서 TextView의 높이 100dp는 렌더링 되지 않고 잘리게 된다.&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;ClipChildren은&amp;nbsp; 자식 View가 자신의 경계를 벗어날 수 있도록 해준다. 기본값은 true로 적용을 위해 false값을 지정해야한다. 주의할점은 xml에서 부모 View가 아닌 최상단 View에 적용을 해줘야 한다.&lt;/p&gt;
&lt;pre id=&quot;code_1770782623164&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// xml
android:clipChildren=&quot;false&quot;

// activity
val main = findViewById&amp;lt;ConstraintLayout&amp;gt;(R.id.main)
main.clipChildren = false&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;844&quot; data-origin-height=&quot;443&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/uTLna/dJMcac24ahW/0oXxOqUOwwWkOuHm7MgAk1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/uTLna/dJMcac24ahW/0oXxOqUOwwWkOuHm7MgAk1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/uTLna/dJMcac24ahW/0oXxOqUOwwWkOuHm7MgAk1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FuTLna%2FdJMcac24ahW%2F0oXxOqUOwwWkOuHm7MgAk1%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;844&quot; height=&quot;443&quot; data-origin-width=&quot;844&quot; data-origin-height=&quot;443&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;ClipToOutline&lt;/b&gt;&lt;/h3&gt;
&lt;pre id=&quot;code_1770776460599&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;    &amp;lt;FrameLayout
        android:id=&quot;@+id/fl_outline&quot;
        android:layout_width=&quot;match_parent&quot;
        android:layout_height=&quot;wrap_content&quot;
        android:background=&quot;@drawable/bg_round_12&quot;&amp;gt;

        &amp;lt;TextView
            android:id=&quot;@+id/tv_outline&quot;
            android:layout_width=&quot;match_parent&quot;
            android:layout_height=&quot;wrap_content&quot;
            android:background=&quot;@color/gray&quot;/&amp;gt;
    &amp;lt;/FrameLayout&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;xml에 정의된 내용을 보면 FrameLayout은 background로 테두리를 둥글게 표시되도록 처리했다. 하지만 TextView의 너비가 match_parent로 적용되어 FrameLayout의 영역을 덮어씌워져서 둥근 테두리를 가리게된다.&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;ClipToOutline은 내가 background로 설정한 View의 경계선을 기준으로 내부 View의 영역을 잘라낸다. 기본값은 false이며 적용을 위해 true로 설정해야 한다.&lt;/p&gt;
&lt;pre id=&quot;code_1770777406203&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// xml
android:clipToOutline=&quot;true&quot;

// activity
val flOutline = findViewById&amp;lt;FrameLayout&amp;gt;(R.id.fl_outline)
flOutline.clipToOutline = true&lt;/code&gt;&lt;/pre&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;861&quot; data-origin-height=&quot;89&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/d66kHU/dJMb996nrTg/pwgQjqc1AFC7Y0KS7SqGnk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/d66kHU/dJMb996nrTg/pwgQjqc1AFC7Y0KS7SqGnk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/d66kHU/dJMb996nrTg/pwgQjqc1AFC7Y0KS7SqGnk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fd66kHU%2FdJMb996nrTg%2FpwgQjqc1AFC7Y0KS7SqGnk%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;861&quot; height=&quot;89&quot; data-origin-width=&quot;861&quot; data-origin-height=&quot;89&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;ClipToPadding&lt;/b&gt;&lt;/h3&gt;
&lt;pre id=&quot;code_1770783089498&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;        &amp;lt;ScrollView
            android:id=&quot;@+id/sv_padding&quot;
            android:layout_width=&quot;match_parent&quot;
            android:layout_height=&quot;0dp&quot;
            android:background=&quot;@color/gray&quot;
            android:paddingTop=&quot;50dp&quot;
            app:layout_constraintTop_toBottomOf=&quot;@id/btn_padding&quot;
            app:layout_constraintBottom_toBottomOf=&quot;parent&quot;&amp;gt;

            &amp;lt;LinearLayout
                android:layout_width=&quot;match_parent&quot;
                android:layout_height=&quot;wrap_content&quot;
                android:orientation=&quot;vertical&quot;&amp;gt;

                &amp;lt;TextView
                    android:layout_width=&quot;match_parent&quot;
                    android:layout_height=&quot;400dp&quot;
                    android:background=&quot;#FF9800&quot;
                    android:text=&quot;끝까지 스크롤 해보세요&quot; /&amp;gt;
            &amp;lt;/LinearLayout&amp;gt;
        &amp;lt;/ScrollView&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Padding은 View의 내부 공간을 결정하기 때문에 스크롤 같은 동작은 Padding 영역을 유지하며 수행하게 된다. 위 xml의 경우 paddingTop:&quot;50dp&quot;로 인해 그 공간만큼을 차지된 상태로 스크롤이 수행될 것이다.&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: #333333; text-align: start;&quot;&gt;ClipToPadding은 이러한 상황에서 주로 쓰인다. 내부 콘텐츠가 Padding 영역을 무시하게 되며, 실제 View의 끝 영역까지 도달하게끔 한다. 기본값은 true이며 적용을 위해 false로 설정해야 한다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1770783877417&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// xml
android:clipToPadding=&quot;false&quot;

// activity
val svPadding = findViewById&amp;lt;ScrollView&amp;gt;(R.id.sv_padding)
svPadding.clipToPadding = false&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;ezgif.com-crop.gif&quot; data-origin-width=&quot;456&quot; data-origin-height=&quot;392&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/LQsdD/dJMcabC76t5/1KaF5MdNKTuk2qMa0UMq41/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/LQsdD/dJMcabC76t5/1KaF5MdNKTuk2qMa0UMq41/img.gif&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/LQsdD/dJMcabC76t5/1KaF5MdNKTuk2qMa0UMq41/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/LQsdD/dJMcabC76t5/1KaF5MdNKTuk2qMa0UMq41/img.gif&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;456&quot; height=&quot;392&quot; data-filename=&quot;ezgif.com-crop.gif&quot; data-origin-width=&quot;456&quot; data-origin-height=&quot;392&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;figure data-ke-type=&quot;image&quot; data-ke-mobilestyle=&quot;widthOrigin&quot; data-ke-style=&quot;alignCenter&quot;&gt;
&lt;figcaption style=&quot;display: none;&quot;&gt;&lt;/figcaption&gt;
&lt;span style=&quot;height: 1px; left: 0px; bottom: 0px;&quot; class=&quot;line_drop_guide horizontal&quot;&gt;&lt;/span&gt;&lt;/figure&gt;</description>
      <category>Android/UI</category>
      <category>Android</category>
      <category>Crop</category>
      <category>Kotlin</category>
      <author>O_Gyong</author>
      <guid isPermaLink="true">https://ogyong.tistory.com/93</guid>
      <comments>https://ogyong.tistory.com/93#entry93comment</comments>
      <pubDate>Wed, 11 Feb 2026 20:13:18 +0900</pubDate>
    </item>
    <item>
      <title>[Android] Worker Pool 패턴을 통한 작업의 효율/안정성 올리기</title>
      <link>https://ogyong.tistory.com/92</link>
      <description>&lt;figure id=&quot;og_1768811135110&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;[Android] API의 배치 처리와 동시성 처리 : chunked vs semaphore&quot; data-og-description=&quot;기존 문자를 전송하는 로직은 chunked(10)를 통해 10개의 배치를 만들고 배치 내부는 비동기로 실행하되, 배치 내부의 작업이 모두 끝나면 다음 배치로 넘어가는 순차 방식으로 구현을 했다. // sendMm&quot; data-og-host=&quot;ogyong.tistory.com&quot; data-og-source-url=&quot;https://ogyong.tistory.com/90&quot; data-og-url=&quot;https://ogyong.tistory.com/90&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/cl3c89/dJMb9dHhQrq/vYw7S1pNlVAjMtvvNbn2Xk/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/ezJRh/dJMb9g44Xo5/Hu4ZQ3KlK5oOaIm2UiKBiK/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800&quot;&gt;&lt;a href=&quot;https://ogyong.tistory.com/90&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://ogyong.tistory.com/90&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/cl3c89/dJMb9dHhQrq/vYw7S1pNlVAjMtvvNbn2Xk/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/ezJRh/dJMb9g44Xo5/Hu4ZQ3KlK5oOaIm2UiKBiK/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;[Android] API의 배치 처리와 동시성 처리 : chunked vs semaphore&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;기존 문자를 전송하는 로직은 chunked(10)를 통해 10개의 배치를 만들고 배치 내부는 비동기로 실행하되, 배치 내부의 작업이 모두 끝나면 다음 배치로 넘어가는 순차 방식으로 구현을 했다. // sendMm&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;ogyong.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지난번에 다량의 API 호출 건에 대해 리스트의 chunked와 코루틴의 semaphore를 비교해 보고 semaphore를 사용하려 했다. 하지만 semaphore를 사용했을 때 구조 상 처리할 데이터의 수량만큼 코루틴을 생성해야 하는 문제가 발목을 잡았는데, Worker Pool 패턴에 대해 알게 되었다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;b&gt;Worker Pool&amp;nbsp; / Producer-Consumer / Worker Thread&lt;/b&gt;&lt;br /&gt;위 3가지를 섞어서 말하기도 한다.&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;Worker Pool 패턴은 작업을 처리할 일꾼들을 미리 고용하고, 작업을 처리할 일이 생길 때마다 대기 중인 일꾼에게 일을 맡기는 방식이다. 다시 말하자면 정해진 수의 스레드를 생성하고, 작업을 큐(Queue)에 쌓아두면 스레드에서 작업을 하나씩 처리하는 방식이다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;pre id=&quot;code_1768882662900&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;    val channel = Channel&amp;lt;MmsData&amp;gt;(Channel.UNLIMITED) // 처리할 데이터가 100개 내외기 때문에 UNLIMITED 사용
    mmsList.forEach { channel.send(it) }
    channel.close()

    supervisorScope {
        repeat(5) { // or List(5)
            launch {
                for(mmsItem in channel) {
                    sendMms(mmsItem)
                }
            }
        }
    }&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;작업을 담을 공간으로 코루틴에서 제공하는 Channel을 사용했다. Channel은 Queue와 매우 유사하게 동작을 하지만, suspend로 동기화가 가능한 코루틴끼리 값을 주고받는 통로이다.&lt;span style=&quot;color: #000000; text-align: left;&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;color: #000000; text-align: left;&quot;&gt;&lt;b&gt;channel.send(it)&lt;/b&gt;&lt;br /&gt;처리할 데이터인 mmsList의 Item을 channel에 채워준다&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000; text-align: left;&quot;&gt;&lt;span style=&quot;color: #000000; text-align: left;&quot;&gt;&lt;b&gt;repeat(5) or List(5) / launch&lt;/b&gt;&lt;br /&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #000000; text-align: left;&quot;&gt;&lt;span style=&quot;color: #000000; text-align: left;&quot;&gt;5개의 코루틴을 생성하여 동시성을 확보한다.&lt;/span&gt; &lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000; text-align: left;&quot;&gt;&lt;span style=&quot;color: #000000; text-align: left;&quot;&gt;&lt;span style=&quot;color: #000000; text-align: left;&quot;&gt;&lt;b&gt;for(mmsItem in channel)&lt;/b&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000; text-align: left;&quot;&gt;5개의 코루틴이 channel에 접근하여 작업을 꺼내 수행한다.(같은 작업을 볼 수 없음)&lt;/span&gt; &lt;br /&gt;&lt;/span&gt; &lt;/span&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;style8&quot; /&gt;
&lt;p&gt;&lt;span style=&quot;color: #000000;&quot;&gt;사실 semaphore()가 동시 실행 수를 제어하는데 있어서 간단하게 쓰기 좋았고, 특히 경량 스레드라고 불리는 코루틴이 &lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;100개 내외로 생기는 것은 메모리에 영향을 주지 않을 정도로 미미하다고 한다. 그래도 지금보다 대량의 API 처리가 필요할 때 Worker Pool 패턴을 사용하면 좋을 것 같다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;</description>
      <category>작업 일지</category>
      <category>Android</category>
      <category>Channel</category>
      <category>Kotlin</category>
      <author>O_Gyong</author>
      <guid isPermaLink="true">https://ogyong.tistory.com/92</guid>
      <comments>https://ogyong.tistory.com/92#entry92comment</comments>
      <pubDate>Tue, 20 Jan 2026 20:04:26 +0900</pubDate>
    </item>
    <item>
      <title>[Android] ConnectivityManager로 데이터 절약 모드 감지하기</title>
      <link>https://ogyong.tistory.com/91</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;운영 중인 앱의 기능으로 오전 08:00가 되면 WorkManager를 통해 고객에게 문자를 전송하고 있다. 만약 문자 전송에 실패할 경우 에러 내역을 파일로 저장하고, 미전송 내역을 남겨 수동으로 전송할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt; &lt;b&gt;stackTrace: java.net.ConnectException: Failed to connect to ...&lt;/b&gt; &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&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;color: #333333; text-align: start;&quot;&gt;08:00에&lt;span&gt; 찍히고서 &lt;/span&gt;&lt;/span&gt;재전송 로그가 1분 내외로 찍힌 것이 이상하다고 느껴 다른 원인을 찾아봤다.&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;/b&gt;'일 때 백그라운드에서 네트워크를 차단한다는 내용을 봤다. 수동 전송의 경우 사용자가 앱 내에서 직접 처리를 하는 포그라운드 상태라서 문제 상황이 일치했다. 아니나 다를까 사용자분은 데이터 절약 모드를 활성화한 상태였고, 비활성화를 요청드린 다음 날 정상적으로 문자 전송이 된 것을 확인했다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;데이터 절약 모드 체크하기&lt;/b&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1767688491338&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;    /**
     * 데이터 절약 모드 체크
     */
    fun checkMobileDataSaveMode(context: Context): Boolean {
        val cm = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
        val status = ConnectivityManagerCompat.getRestrictBackgroundStatus(cm)
        return status == ConnectivityManagerCompat.RESTRICT_BACKGROUND_STATUS_ENABLED
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터 절약 모드를 체크하는 방법은 간단하다. &lt;b&gt;ConnectivityManager&lt;/b&gt;를 사용하면 현재 기기가 데이터 절약 모드인지 확인할 수 있다. WorkManager의 doWork() 블록에서 네트워크를 요청하기 전에 해당 함수를 호출하여 데이터 절약 모드가 활성화된 경우 상태 알림을 띄워 사용자가 어떤 사유로 전송에 실패했는지 알 수 있게 처리했다.&lt;/p&gt;</description>
      <category>작업 일지</category>
      <category>Android</category>
      <category>ConnectivityManager</category>
      <category>Kotlin</category>
      <category>데이터 절약 모드</category>
      <author>O_Gyong</author>
      <guid isPermaLink="true">https://ogyong.tistory.com/91</guid>
      <comments>https://ogyong.tistory.com/91#entry91comment</comments>
      <pubDate>Tue, 6 Jan 2026 20:49:39 +0900</pubDate>
    </item>
    <item>
      <title>[Android] API의 배치 처리와 동시성 처리 : chunked vs semaphore</title>
      <link>https://ogyong.tistory.com/90</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;운영중인 앱에는 문자를 전송하는 기능이 있다. 문자를 전송하는 로직은 chunked()를 통해 배치를 만들고, 배치 내부는 비동기로 실행하되, 배치 내부의 작업이 모두 끝나면 다음 배치로 넘어가는 순차 방식으로 구현이 되어 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1766124520480&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// mmsList: List&amp;lt;MmsReq&amp;gt;
val result = mmsList.chunked(10).flatMap { batch -&amp;gt;
    batch.map { job -&amp;gt;
        async { sendMms(job) }
    }.awaitAll()
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 방식은 단건씩 처리하는 것과 비교했을 때 효율을 높이고, 처리량을 조절함으로써 로컬과 서버에 부담을 주지 않는다는 장점이 있다. 하지만 현재 배치의 작업이 지연이 생겼을 때 다음 배치의 시작이 지연되어 처리 속도가 떨어질 수 있다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코루틴에서 제공하는 semaphore()는 동시에 permit을 획득할 수 있는 작업 수를 제한하여, 하나의 작업이 끝나면 다음 작업을 자동으로 채워서 동시 실행 수를 유지시켜준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1766124537544&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// mmsList: List&amp;lt;MmsReq&amp;gt;
val semaphore = Semaphore(10)
val result = mmsList.map { mmsData -&amp;gt;
    async {
        semaphore.withPermit {
            sendMms(mmsData)
        }
    }
}.awaitAll()&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;semaphore() 를 적용하면서 배치에 대한 신경을 쓰지 않아도 되서 좋았고, 코드의 일부분을 정리해서 올렸지만 chunked()를 썼을 때와 달리 코드가 정돈됨을 느꼈다.&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;단, semaphore를 사용하면 현재 구조상 아이템의 개수만큼 코루틴을 생성하기 때문에 작업 데이터가 굉장히 많은 경우 메모리를 크게 잡아 먹게 된다. 그리고 withPermit이 실행되면서 코루틴이 일시 중단되었다가 재개되는데, 아이템의 개수가 많고 개별 작업 시간이 매우 짧다면 스위칭으로 인한 오버헤드가 작업 시간보다 커지는 문제가 발생할 수 있다고 한다. (작업 개수만틈 Deferred 객체가 생성 됨)&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 함수를 비교하여 사용할 때 충분한 테스트가 필요할 것 같다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;chunked&lt;/b&gt;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;- 작업시간에 편차가 없는 경우&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;- 리스트가 매우 큰 경우&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;- 코루틴 개수와 메모리를 확실히 제한하려는 경우&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;semaphore&lt;/b&gt;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;- 작업시간에 편차가 있는 경우&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;- 리스트가 고정되지 않고 추가 및 삭제로 변경되는 경우 동시성을 제한할 때&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;- 전체 작업 시간을 단축하고 싶을 때(특히 작업마다의 처리 시간이 들쭉날쭉할 때)&lt;/span&gt;&lt;/blockquote&gt;</description>
      <category>작업 일지</category>
      <category>Android</category>
      <category>Kotlin</category>
      <author>O_Gyong</author>
      <guid isPermaLink="true">https://ogyong.tistory.com/90</guid>
      <comments>https://ogyong.tistory.com/90#entry90comment</comments>
      <pubDate>Fri, 19 Dec 2025 15:08:07 +0900</pubDate>
    </item>
    <item>
      <title>[Android][Compose] WorkManager &amp;amp; AlarmManger로 특정 시간에 백그라운드 작업 예약하기</title>
      <link>https://ogyong.tistory.com/89</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;안드로이드에서는 백그라운드 작업을 돕는 요소들이 있다. 그 요소들 중에서 &lt;b&gt;AlarmManager&lt;/b&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;와&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;b&gt;WorkManager&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;AlarmManager는 안드로이드 프레임워크에서 제공하는 API로 시스템 레벨의 알람 서비스에 접근하게 해 준다. 정확한 시간에 대한 작업이 필요한 경우에 사용되며, WorkManager가 없던 시절에는 BroadcastReceiver나 JobScheduler를 함께 사용하여 백그라운드 작업을 수행했다.&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;WorkManager는 백그라운드에서의 작업을 보장하는 안드로이드 Jetpack 라이브러리이다. 사용자가 화면을 벗어나거나, 앱이 종료가 되더라도 예약한 기능을 안정적으로 수행할 수 있다. 또한 기기의 상태(네트워크 연결, 저장공간, 충전 상태)와 같은 제약 조건을 사용하여 특정 상황에서의 작업 처리도 가능하다.&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;/p&gt;
&lt;figure id=&quot;og_1761194224488&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;태스크 예약 &amp;nbsp;|&amp;nbsp; Background work &amp;nbsp;|&amp;nbsp; Android Developers&quot; data-og-description=&quot;WorkManager를 사용하여 앱 또는 기기 다시 시작 시 실행되는 작업을 예약하는 방법을 설명합니다.&quot; data-og-host=&quot;developer.android.com&quot; data-og-source-url=&quot;https://developer.android.com/develop/background-work/background-tasks/persistent?hl=ko&quot; data-og-url=&quot;https://developer.android.com/develop/background-work/background-tasks/persistent?hl=ko&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/uGs2t/hyZLizTw0j/L3NK7YCKvrGXXnxEnGesK1/img.png?width=1201&amp;amp;height=676&amp;amp;face=0_0_1201_676&quot;&gt;&lt;a href=&quot;https://developer.android.com/develop/background-work/background-tasks/persistent?hl=ko&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://developer.android.com/develop/background-work/background-tasks/persistent?hl=ko&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/uGs2t/hyZLizTw0j/L3NK7YCKvrGXXnxEnGesK1/img.png?width=1201&amp;amp;height=676&amp;amp;face=0_0_1201_676');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;태스크 예약 &amp;nbsp;|&amp;nbsp; Background work &amp;nbsp;|&amp;nbsp; Android Developers&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;WorkManager를 사용하여 앱 또는 기기 다시 시작 시 실행되는 작업을 예약하는 방법을 설명합니다.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;developer.android.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&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;WorkManager의 작업은 정해진 시간에 작업을 수행하지 않는다. 시스템이 해당 작업을 수행하기 앞서 효율적인 시점을 선택을 하게 된다. 이런 지연적인 특성은 '정확한 시점의 실행'이 아닌 '언젠가는 반드시 실행'이라는 의미를 갖는다. WorkManager를 사용하되 특정한 시간에 작업을 수행하게 하려면 AlarmManager를 함께 써야 한다.&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;AlarmManager를 설정하는 버튼을 만들고, 버튼을 클릭 시 WorkManager를 실행한다. WorkManager에서 Room의 값을 업데이트하여 UI에 반영되는지 확인하는 예제를 다음과 같이 만들었다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;◾ 의존성 등록&lt;/h3&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt; libs.version.toml &lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1761198811529&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;[versions]
lifecycleVersion = &quot;2.9.4&quot;
roomVersion = &quot;2.8.2&quot;
workVersion = &quot;2.10.5&quot;
hiltVersion = &quot;2.57.2&quot;
hiltWorkVersion = &quot;1.3.0&quot;

[libraries]
# ViewModel &amp;amp; LiveData/Flow
lifecycle-viewmodel-compose = { module = &quot;androidx.lifecycle:lifecycle-viewmodel-compose&quot;, version.ref = &quot;lifecycleVersion&quot; }
lifecycle-runtime-compose = { module = &quot;androidx.lifecycle:lifecycle-runtime-compose&quot;, version.ref = &quot;lifecycleVersion&quot; }

# Room
room-ktx = { module = &quot;androidx.room:room-ktx&quot;, version.ref = &quot;roomVersion&quot; }
room-runtime = { module = &quot;androidx.room:room-runtime&quot;, version.ref = &quot;roomVersion&quot; }
room-compiler = { module = &quot;androidx.room:room-compiler&quot;, version.ref = &quot;roomVersion&quot; }

# WorkManager
work-runtime-ktx = { module = &quot;androidx.work:work-runtime-ktx&quot;, version.ref = &quot;workVersion&quot; }

# Hilt, HiltPlugin에서 사용 됨 (hilt.android, hilt.compiler)
hilt-android = { group = &quot;com.google.dagger&quot;, name = &quot;hilt-android&quot;, version.ref = &quot;hiltVersion&quot; }
hilt-compiler = { group = &quot;com.google.dagger&quot;, name = &quot;hilt-compiler&quot;, version.ref = &quot;hiltVersion&quot; }
hilt-work = { group = &quot;androidx.hilt&quot;, name = &quot;hilt-work&quot;, version.ref = &quot;hiltWorkVersion&quot; }
hilt-android-compiler = { group = &quot;androidx.hilt&quot;, name = &quot;hilt-compiler&quot;, version.ref = &quot;hiltWorkVersion&quot; }

[plugins]
ksp = { id = &quot;com.google.devtools.ksp&quot;, version.ref = &quot;kspVersion&quot; }
hilt = { id = &quot;com.google.dagger.hilt.android&quot;, version.ref = &quot;hiltVersion&quot; }

[bundles]
room = [&quot;room-ktx&quot;, &quot;room-runtime&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;이 프로젝트에서 사용하게 될 라이브러리(work, hilt, room, lifecycle)를 버전 카탈로그에 추가했다. 참고로 Worker 클래스는 Hilt를 사용한 의존성 주입이 가능한데, 이를 위해 &lt;b&gt;hilt-work&lt;/b&gt;와 &lt;b&gt;hilt-compiler&lt;/b&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 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;◾&lt;span&gt;&amp;nbsp;&lt;/span&gt;AlarmManager 설정&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AlarmManager는 특정 시간에 알람을 켜라고 안드로이드 시스템에 요청한다. 특정 시간이 되면 안드로이드 시스템은 Broadcast를 통해 알람을 울린다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;AlarmScheduler.kt&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1761272739844&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Singleton
class AlarmScheduler @Inject constructor(
    @ApplicationContext private val context: Context
) {
    private val alarmManager = context.getSystemService(Application.ALARM_SERVICE) as AlarmManager

    private fun pendingIntent(): PendingIntent {
        val intent = Intent(context, AlarmReceiver::class.java)
        return PendingIntent.getBroadcast(
            context,
            ALARM_CODE,
            intent,
            PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
        )
    }

    fun schedule(triggerAtMillis: Long) {
        Log.d(&quot;LOG_TAG&quot;, &quot;AlarmScheduler on&quot;)
        val pi = pendingIntent()
        alarmManager.setAndAllowWhileIdle(
            AlarmManager.RTC_WAKEUP,
            triggerAtMillis,
            pi
        )
    }

    fun cancel() {
        alarmManager.cancel(pendingIntent())
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AlarmScheduler 클래스는 알람을 예약하는 역할을 하는 클래스다.&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;PendingIntent에 Broadcast를 받을 Class와 알람이 어떻게 동작할지에 대한 플래그를 정의한다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;- PendingIntent.FLAG_UPDATECURRENT&lt;/b&gt;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;ALARM_CODE로 이미 등록된 PendingIntent가 있을 경우 기존 알람을 취소하지 않고 내용을 현재 Intent로 덮어쓰기(알람 시간 재설정)&lt;/span&gt;&lt;br /&gt;&lt;b&gt;&lt;span style=&quot;color: #000000;&quot;&gt;- PendingIntent.FLAG_IMMUTABLE&lt;/span&gt;&lt;br /&gt;&lt;/b&gt;&lt;span style=&quot;color: #000000;&quot;&gt;시스템이 이 Intent 내용을 수정할 수 없다고 명시(Android 12 이상부터 필수)&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;schedule() 함수에서는 알람을 등록하는 역할을 하며, &lt;b&gt;alarmManager.setAndAllowWhileIdle()&lt;/b&gt; 함수가 이 기능을 담당한다. triggerAtMillis가 알람이 울릴 정확한 시간이다.(밀리초 단위의 UTC 타임스태프)&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;- RTC_WAKEUP&lt;/b&gt;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;RTC(Real Time Clock)은 실제 시간을 의미하고, WAKEUP은 기기가 Doze 모두(깊은 절정 상태)에 빠져 있더라도 강제로 기기를 깨우는 것을 의미한다.&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;AlarmReceiver.kt&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1761204071003&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class AlarmReceiver : BroadcastReceiver() {
    override fun onReceive(context: Context, intent: Intent) {
        Log.d(&quot;LOG_TAG&quot;, &quot;AlarmReceiver On&quot;)

        val workManager = WorkManager.getInstance(context.applicationContext)
        val workerRequest = OneTimeWorkRequestBuilder&amp;lt;SampleCountWorker&amp;gt;()
            .setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST)
            .build()

        workManager.enqueueUniqueWork(
            &quot;SampleCountWorkName&quot;,
            ExistingWorkPolicy.KEEP,
            workerRequest
        )
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AlarmReceiver 클래스는 알람을 수신하여 동작할 행동을 정의한 클래스다. AlarmScheduler에서 &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;지정한 시간이 되면&lt;span&gt; AlarmMangaer가 Intent를 통해 AlarmReceiver를 깨운다.&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;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;BraodcastReceiver가 알람을 수신하게 되면 onReceive() 함수가 자동으로 호출된다. onReceive 블록에서는 백그라운드 작업을 위해 WorkManager에 위임한다. onReceive()에서 모든 작업을 끝내지 않는 이유는 해당 함수가 메인 스레드에서 실행이 되고, 약 10초가 넘어가면 ANR을 발생시킨다고 한다.&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 style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;AndroidManifest.xml&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1761204520229&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;?xml version=&quot;1.0&quot; encoding=&quot;utf-8&quot;?&amp;gt;
&amp;lt;manifest xmlns:android=&quot;http://schemas.android.com/apk/res/android&quot;
    xmlns:tools=&quot;http://schemas.android.com/tools&quot;&amp;gt;
    &amp;lt;uses-permission android:name=&quot;android.permission.SCHEDULE_EXACT_ALARM&quot; /&amp;gt;

    &amp;lt;application
        android:name=&quot;.SampleApplication&quot;
        ...
       
       
        &amp;lt;receiver
            android:name=&quot;.core.alarm.AlarmReceiver&quot;
            android:exported=&quot;false&quot; /&amp;gt;
    &amp;lt;/application&amp;gt;
&amp;lt;/manifest&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정확한 시간에 알람을 사용하기 위해 &lt;b&gt;SCHEDULE_EXACT_ALARM&lt;/b&gt;이라는 특수 권한을 선언한다. 그리고 BroadcastReceiver를 사용하기 때문에 Manifest에 AlarmReceiver를 등록해 준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;MainViewModel.kt&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1761209768227&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;fun setAlarmManager() {
    val timer = 5 // timer(초) 이후 alarmManager 실행
    val calendar = Calendar.getInstance()
    calendar.add(Calendar.SECOND, timer)

    val newHour = calendar.get(Calendar.HOUR_OF_DAY)
    val newMinute = calendar.get(Calendar.MINUTE)
    val newSecond = calendar.get(Calendar.SECOND)

    // 값 업데이트 (ui 업데이트)
    _selectedHour.value = newHour
    _selectedMinute.value = newMinute
    _selectedSecond.value = newSecond

    alarmScheduler.cancel() // 기존 알람 제거
    alarmScheduler.schedule(triggerAtMillis = calendar.timeInMillis)

    viewModelScope.launch {
        _toastEvent.emit(&quot;${timer}초 뒤에 WorkManager가 실행됩니다.&quot;)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;알람 세팅 버튼을 누르면 ViewModel의 setAlarmManager() 함수를 호출하게 했는데, 임시 설정으로 현재 시간에 5초를 더한 시간에 알람을 울리게 하였다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;◾&lt;span&gt;&amp;nbsp;&lt;/span&gt;WorkManager + Hilt로 의존성 주입하기&lt;/h3&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;SampleApplication.kt&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1761200183673&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@HiltAndroidApp
class SampleApplication : Application(), Configuration.Provider {
    @Inject
    lateinit var workerFactory: HiltWorkerFactory

    override val workManagerConfiguration: Configuration
        get() = Configuration.Builder().setWorkerFactory(workerFactory).build()
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Hilt가 제공하는 &lt;b&gt;HiltWorkerFactory&lt;/b&gt;를 WorkManager의 기본 팩토리로 지정해 준다. 이 설정을 통해 WorkManager가 Hilt로부터 의존성을 주입받은 Worker를 생성할 수 있게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;AndroidManifest.xml&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1761200680052&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;    &amp;lt;application
        android:name=&quot;.SampleApplication&quot;
        ...

        &amp;lt;provider
            android:name=&quot;androidx.startup.InitializationProvider&quot;
            android:authorities=&quot;${applicationId}.androidx-startup&quot;
            tools:node=&quot;remove&quot;/&amp;gt;
    &amp;lt;/application&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;work-runtime 라이브러리에는 기본적으로 WorkManager를 초기화하는 기능이 추가되어 있어서 앱을 시작하면 &lt;b&gt;InitializationProvider&lt;/b&gt;가 자동으로 WorkManager를 기본 설정으로 초기화한다. 그래서 SampleApplication에서 초기화 코드를 작성했음에도 적용이 안 되는 문제가 발생한다. &lt;b&gt;tools:node=&quot;remove&quot;&lt;/b&gt;를 사용하여 InitializationProvider 자체를 제거하여 자동 초기화를 막아준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;SampleCountWorker.kt&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1761200967399&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@HiltWorker
class SampleCountWorker @AssistedInject constructor(
    @Assisted appContext: Context,
    @Assisted params: WorkerParameters,
    private val increaseSampleCountUseCase: IncreaseSampleCountUseCase
) : CoroutineWorker(appContext = appContext, params = params) {
    override suspend fun doWork(): Result {
        try {
            Log.d(&quot;LOG_TAG&quot;, &quot;SampleCountWorker On&quot;)
            increaseSampleCountUseCase()
        } catch (e: Exception) {
            Log.e(&quot;SampleCountWorker&quot;, &quot;Error increasing sample count&quot;, e)
        }

        return Result.success()
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SampleCountWorker는 Worker가 동작하는 클래스다. CoroutineWorker는 WorkManager의 Worker 클래스 중 하나로 작업을 실행하는 함수 doWork()가 suspend 함수가 된다. 이로 인해 비동기 코드를 doWork 블록 내부에서 간단하게 사용할 수 있다.&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;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;- @HiltWorker&lt;/b&gt;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Worker 클래스를 Hilt가 관리함을 알림&lt;/span&gt;&lt;br /&gt;&lt;b&gt;&lt;span style=&quot;color: #000000;&quot;&gt;- @AssistedInject&lt;/span&gt;&lt;/b&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;(런타임에 사용되는 값 + DI 요소)인 경우에 사용하는 패턴&lt;/span&gt;&lt;br /&gt;&lt;b&gt;&lt;span style=&quot;color: #000000;&quot;&gt;- @Assited&lt;/span&gt;&lt;/b&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;WorkerManager가 런타임 시 제공할 요소임을 알림&lt;/span&gt;&lt;br /&gt;&lt;b&gt;&lt;span style=&quot;color: #000000;&quot;&gt;- IncreaseSampleCountUseCase&lt;/span&gt;&lt;/b&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;nbsp;Application에서 선언한 HiltWorkerFactory가 Hilt 그래프로부터 해당 클래스를 찾아줌&lt;/span&gt;&lt;/blockquote&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;WorkManger_AlarmManger.gif&quot; data-origin-width=&quot;300&quot; data-origin-height=&quot;600&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bMTd2E/dJMb9OAIOXw/BQSNvMMpK5hF4rOwFm7NJ1/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bMTd2E/dJMb9OAIOXw/BQSNvMMpK5hF4rOwFm7NJ1/img.gif&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bMTd2E/dJMb9OAIOXw/BQSNvMMpK5hF4rOwFm7NJ1/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/bMTd2E/dJMb9OAIOXw/BQSNvMMpK5hF4rOwFm7NJ1/img.gif&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;300&quot; height=&quot;600&quot; data-filename=&quot;WorkManger_AlarmManger.gif&quot; data-origin-width=&quot;300&quot; data-origin-height=&quot;600&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;[AlarmManager Set] 버튼을 클릭하면 5초 뒤 AlarmManger를 통해 WorkManager가 실행되고 Room의 Sample 값을 1 증가시킨다. View에서는 Sample 값을 Flow로 수집을 하고 있기 때문에 백그라운드에서 값이 업데이트가 정상적으로 되었는지 확인할 수 있다.&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;a href=&quot;https://github.com/OhGyong/Android_Study/tree/master/WorkManager%20with%20AlarmManager&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;전체 코드&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1761288331877&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;Android_Study/WorkManager with AlarmManager at master &amp;middot; OhGyong/Android_Study&quot; data-og-description=&quot;안드로이드 개발 공부. Contribute to OhGyong/Android_Study development by creating an account on GitHub.&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/OhGyong/Android_Study/tree/master/WorkManager%20with%20AlarmManager&quot; data-og-url=&quot;https://github.com/OhGyong/Android_Study/tree/master/WorkManager%20with%20AlarmManager&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bhSmKB/hyZLgCfTp1/dvtQRhpWhfzKLUz8E5h2x1/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/bc82Kb/hyZLiNAofN/LNgoy5RJ4dw6CrxJPPV55K/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://github.com/OhGyong/Android_Study/tree/master/WorkManager%20with%20AlarmManager&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/OhGyong/Android_Study/tree/master/WorkManager%20with%20AlarmManager&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bhSmKB/hyZLgCfTp1/dvtQRhpWhfzKLUz8E5h2x1/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/bc82Kb/hyZLiNAofN/LNgoy5RJ4dw6CrxJPPV55K/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Android_Study/WorkManager with AlarmManager at master &amp;middot; OhGyong/Android_Study&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;안드로이드 개발 공부. Contribute to OhGyong/Android_Study development by creating an account on GitHub.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;</description>
      <category>Android/기타</category>
      <category>AlarmManager</category>
      <category>Android</category>
      <category>Background</category>
      <category>compose</category>
      <category>workmanager</category>
      <author>O_Gyong</author>
      <guid isPermaLink="true">https://ogyong.tistory.com/89</guid>
      <comments>https://ogyong.tistory.com/89#entry89comment</comments>
      <pubDate>Fri, 24 Oct 2025 20:48:31 +0900</pubDate>
    </item>
    <item>
      <title>[Android] Could not create task ':app:kspDebugKotlin'.</title>
      <link>https://ogyong.tistory.com/88</link>
      <description>&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;Could&amp;nbsp;not&amp;nbsp;determine&amp;nbsp;the&amp;nbsp;dependencies&amp;nbsp;of&amp;nbsp;task&amp;nbsp;':app:compileDebugJavaWithJavac'. &lt;/b&gt;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;&amp;gt;&amp;nbsp;Could&amp;nbsp;not&amp;nbsp;create&amp;nbsp;task&amp;nbsp;':app:kspDebugKotlin'. &lt;/b&gt;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;gt;&amp;nbsp;Cannot&amp;nbsp;query&amp;nbsp;the&amp;nbsp;value&amp;nbsp;of&amp;nbsp;task&amp;nbsp;':app:kspDebugKotlin'&amp;nbsp;property&amp;nbsp;'classpathSnapshotProperties.useClasspathSnapshot'&amp;nbsp;because&amp;nbsp;it&amp;nbsp;has&amp;nbsp;no&amp;nbsp;value&amp;nbsp;available.&lt;/b&gt;&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트를 새로 만들고 의존성을 추가 후 앱을 실행했더니 위와 같은 에러가 발생했다. 의존성을 추가하고 sync에서 문제가 발생하지 않아서 조금 헤매었는데, 이 에러는 KGP와 KSP의 버전이 불일치할 때 자주 나타난다고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;245&quot; data-origin-height=&quot;173&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bXRSEP/btsQ91l324w/R3LKo42spOn9TRg2I4qrqk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bXRSEP/btsQ91l324w/R3LKo42spOn9TRg2I4qrqk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bXRSEP/btsQ91l324w/R3LKo42spOn9TRg2I4qrqk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbXRSEP%2FbtsQ91l324w%2FR3LKo42spOn9TRg2I4qrqk%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;245&quot; height=&quot;173&quot; data-origin-width=&quot;245&quot; data-origin-height=&quot;173&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;버전 카탈로그에 적용한 버전 정보를 보면 kotlin과 kspVersion의 버전이 맞지 않았다. 코틀린 버전을 ksp에 맞춰 &quot;2.1.10&quot;으로 수정했더니 정상적으로 앱이 빌드 됐다.&lt;/p&gt;</description>
      <category>Android/이슈 처리</category>
      <category>Android</category>
      <category>issue</category>
      <author>O_Gyong</author>
      <guid isPermaLink="true">https://ogyong.tistory.com/88</guid>
      <comments>https://ogyong.tistory.com/88#entry88comment</comments>
      <pubDate>Tue, 14 Oct 2025 19:46:28 +0900</pubDate>
    </item>
    <item>
      <title>[Android] 인앱 업데이트(in-app update) 적용하기</title>
      <link>https://ogyong.tistory.com/86</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;figure id=&quot;og_1758004550446&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;인앱 업데이트 &amp;nbsp;|&amp;nbsp; Other Play guides &amp;nbsp;|&amp;nbsp; Android Developers&quot; data-og-description=&quot;이 페이지는 Cloud Translation API를 통해 번역되었습니다. 인앱 업데이트 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. 사용자는 기기에서 앱을 최신 상태로 &quot; data-og-host=&quot;developer.android.com&quot; data-og-source-url=&quot;https://developer.android.com/guide/playcore/in-app-updates?hl=ko&quot; data-og-url=&quot;https://developer.android.com/guide/playcore/in-app-updates?hl=ko&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/EDbji/hyZJbVlJvm/0zK5S7mZtSK1gpkqP1kNZ0/img.png?width=1201&amp;amp;height=676&amp;amp;face=0_0_1201_676,https://scrap.kakaocdn.net/dn/byhQww/hyZJkjkRXJ/tNF84fZLBEtC4486PVWskk/img.png?width=1384&amp;amp;height=916&amp;amp;face=0_0_1384_916,https://scrap.kakaocdn.net/dn/bxNkhY/hyZI6l7JlU/6k0rLFHEkyakOKOMYvugp0/img.png?width=904&amp;amp;height=916&amp;amp;face=0_0_904_916&quot;&gt;&lt;a href=&quot;https://developer.android.com/guide/playcore/in-app-updates?hl=ko&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://developer.android.com/guide/playcore/in-app-updates?hl=ko&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/EDbji/hyZJbVlJvm/0zK5S7mZtSK1gpkqP1kNZ0/img.png?width=1201&amp;amp;height=676&amp;amp;face=0_0_1201_676,https://scrap.kakaocdn.net/dn/byhQww/hyZJkjkRXJ/tNF84fZLBEtC4486PVWskk/img.png?width=1384&amp;amp;height=916&amp;amp;face=0_0_1384_916,https://scrap.kakaocdn.net/dn/bxNkhY/hyZI6l7JlU/6k0rLFHEkyakOKOMYvugp0/img.png?width=904&amp;amp;height=916&amp;amp;face=0_0_904_916');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;인앱 업데이트 &amp;nbsp;|&amp;nbsp; Other Play guides &amp;nbsp;|&amp;nbsp; Android Developers&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;이 페이지는 Cloud Translation API를 통해 번역되었습니다. 인앱 업데이트 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. 사용자는 기기에서 앱을 최신 상태로&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;developer.android.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;플레이스토어에 앱을 최신 버전으로 배포했을 때, 앱을 사용하던 유저에게 업데이트 할 것을 알리고 싶을 때가 있다. 인앱 업데이트(in-app update)는 이것을 가능하게 하는 Google Play Core 라이브러리 기능이다.&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;b&gt;◾ 업데이트 방식&lt;/b&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;1. 유연한 업데이트(Flexible udpates)&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;유연한 업데이트는 사용자가 업데이트를 하는 동안 앱을 사용하는 경우에 적합하다. 이 방식은 앱 사용 흐름에 방해를 주지 않고 업데이트를 진행할 수 있다는 점이 특징이다. 백그라운드에서 업데이트가 진행되고, 업데이트가 완료된 후 앱이 재실행되어 새로운 버전을 사용할 수 있게 된다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;847&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/biuuQO/btsQCONVt2c/5auiH1x7j3Q5WRNYg0tuG1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/biuuQO/btsQCONVt2c/5auiH1x7j3Q5WRNYg0tuG1/img.png&quot; data-alt=&quot;유연한 업데이트 예시 (하단 업데이트 팝업)&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/biuuQO/btsQCONVt2c/5auiH1x7j3Q5WRNYg0tuG1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbiuuQO%2FbtsQCONVt2c%2F5auiH1x7j3Q5WRNYg0tuG1%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;772&quot; height=&quot;511&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;847&quot;/&gt;&lt;/span&gt;&lt;figcaption&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;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;2.&lt;/b&gt;&lt;b&gt;&amp;nbsp;즉시 업데이트(immediate updates)&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉시 업데이트는 사용자의 앱 사용을 막고 업데이트를 우선하는 방식으로, 업데이트가 중요한 경우에 적합하다. 앱을 강제로 업데이트해야 하는 경우에 이 방식이 좋아 보인다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;904&quot; data-origin-height=&quot;916&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dwcaFz/btsQCwGKglf/uiB3CorCoQjWrBPbiI4Kd0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dwcaFz/btsQCwGKglf/uiB3CorCoQjWrBPbiI4Kd0/img.png&quot; data-alt=&quot;즉시 업데이트 예시 (전체 화면 팝업)&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dwcaFz/btsQCwGKglf/uiB3CorCoQjWrBPbiI4Kd0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdwcaFz%2FbtsQCwGKglf%2FuiB3CorCoQjWrBPbiI4Kd0%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;560&quot; height=&quot;567&quot; data-origin-width=&quot;904&quot; data-origin-height=&quot;916&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;즉시 업데이트 예시 (전체 화면 팝업)&lt;/figcaption&gt;
&lt;/figure&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;b&gt;◾ in-app updates 적용하기&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스플래시가 구현된 프로젝트는 스플래시 화면에서, 그렇지 않은 경우는 로그인 화면에서 앱 업데이트 체크를 진행하도록 했다. 즉시 업데이트 방식을 사용하여 사용자가 최신 버전을 사용할 수 있게끔 했다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h3 style=&quot;text-align: left;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #333333;&quot;&gt;1. 라이브러리 등록&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;build.gradle.kts(:app)&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1758081849810&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;implementation(&quot;com.google.android.play:app-update-ktx:2.1.0&quot;)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;in-app updates 라이브러리를 사용하기 위해 Gradle에 라이브러리를 등록시켜 준다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h3 style=&quot;text-align: left;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #333333;&quot;&gt;2. 업데이트 가능 여부 확인 및 업데이트&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;앱 업데이트를 체크하는 화면&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1758081948964&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;private val appUpdateManager: AppUpdateManager by lazy {
    AppUpdateManagerFactory.create(applicationContext)
}

private fun checkAppUpdate(intentType: Boolean) {
    // PlayStore에서 설치된 경우 설치 경로가 &quot;com.android.vending&quot;으로 됨
    val isRegisteredPlayStore = if (Build.VERSION.SDK_INT &amp;gt;= Build.VERSION_CODES.R) { // Android 11 기준
        val info = packageManager.getInstallSourceInfo(packageName)
        info.installingPackageName == &quot;com.android.vending&quot;
    } else {
        packageManager.getInstallerPackageName(packageName) == &quot;com.android.vending&quot;
    }

    // PlayStore에 등록된 앱 아닌 경우 앱 체크 안 하도록 처리
    // 로컬 빌드는 앱 등록이 무조건 안 된 것으로 체크하여 addOnFailureListener를 타게 됨
    if (!isRegisteredPlayStore) {
        intentLogin(intentType)
        return
    }

    appUpdateManager.appUpdateInfo
        // PlayStore 통신 성공
        .addOnSuccessListener { appUpdateInfo -&amp;gt;
            if ((appUpdateInfo.updateAvailability() == UpdateAvailability.UPDATE_AVAILABLE &amp;amp;&amp;amp; appUpdateInfo.isUpdateTypeAllowed(AppUpdateType.IMMEDIATE))
                || appUpdateInfo.updateAvailability() == UpdateAvailability.DEVELOPER_TRIGGERED_UPDATE_IN_PROGRESS
            ) { 
            	// 업데이트 시작
                appUpdateManager.startUpdateFlowForResult(
                    appUpdateInfo,
                    this@SplashActivity,
                    AppUpdateOptions.newBuilder(AppUpdateType.IMMEDIATE).build(),
                    100
                )
            } else {
                intentLogin(intentType)
            }
        }
        // PlayStore 통신 실패 (Ex- PlayStore에 앱 등록이 안 된 경우)
        .addOnFailureListener { e -&amp;gt;
            Toast.makeText(this, &quot;앱 업데이트 실패&quot;, Toast.LENGTH_SHORT).show()
        }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앱 업데이트를 요청하기 전에 앱이 먼저 업데이트가 필요한지를 확인해야 한다. AppUpdateManager를 사용하여 업데이트가 있는지 확인할 수 있다.&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;color: #000000;&quot;&gt;&lt;b&gt;(appUpdateInfo.updateAvailability() == UpdateAvailability.UPDATE_AVAILABLE &amp;amp;&amp;amp; appUpdateInfo.isUpdateTypeAllowed(AppUpdateType.IMMEDIATE)&lt;/b&gt;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #333333;&quot;&gt;즉시 업데이트 가능한 상태인지&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;appUpdateInfo.updateAvailability() == UpdateAvailability.DEVELOPER_TRIGGERED_UPDATE_IN_PROGRESS&lt;/b&gt;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #333333;&quot;&gt;이전 업데이트에서 중단된 상태인지&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PlayStore에 앱이 등록된 경우 addOnSuccessListener 블록을 타게 되는데, 여기서 즉시 업데이트가 가능한 상태인지 그리고 이전의 업데이트에서 중단된 상태는 아닌지 확인한다.&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;이후 startUpdateFlowForResult()를 통해 업데이트를 시작한다. (업데이트 전체 화면으로 전환)&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: #ee2323;&quot;&gt;&lt;b&gt;※ 참고&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로컬 환경에서 빌드한 앱의 경우 PlayStore에 앱 등록이 안 된 것으로 판단하여 addOnFailureListener 블록이 실행된다. PlayStore에서 설치된 앱은 설치 경로가 &quot;com.android.vending&quot;으로 되어 있어서 구분이 가능하므로, 로컬 빌드인 경우에 대한 처리를 추가했다.&lt;/p&gt;</description>
      <category>Android/기타</category>
      <category>Android</category>
      <category>in-app update</category>
      <author>O_Gyong</author>
      <guid isPermaLink="true">https://ogyong.tistory.com/86</guid>
      <comments>https://ogyong.tistory.com/86#entry86comment</comments>
      <pubDate>Wed, 17 Sep 2025 22:35:19 +0900</pubDate>
    </item>
    <item>
      <title>[Android] WebView에서 다운로드 동작이 안 되는 이유</title>
      <link>https://ogyong.tistory.com/87</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;공지사항 화면을 WebView로 관리하고 있는데, 게시글 중에서 apk를 다운로드할 수 있는 링크를 클릭했는데 아무 이벤트가 동작하지 않는 문제가 생겼다. 신기한 점은 다른 WebView에서 똑같은 url로 같은 화면을 띄우는데, 다운로드 동작이 정상적으로 되었다.&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;해당 부분을 찾아보니 WebView를 사용할 때 WebViewClient를 적용한 경우 WebView의 기본 동작을 WebViewClient의 shouldOverrideUrlLoading()이 대신 수행한다고 한다.&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;그래서 WebViewClient를 적용 안 한 WebView의 경우 다운로드와 같은 특수 MIME 타입은 자동으로 외부 브라우저로 넘겨 처리를 하기 때문에 다운로드가 정상으로 동작한다. 하지만 WebViewCilent를 적용하면 shouldOverrideUrlLoading()이 WebView의 기본 동작을 대신하기 때문에 따로 처리를 하지 않으면 아무 동작을 하지 않는다.&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;해당 내용을 알고 공지사항의 WebView와 다른 WebView에 사용된 WebViewClient를 비교해 봤는데 공지사항 쪽 Client는 url 링크 처리나 다운로드 관련 처리가 없었고, 다른 WebView의 Client는 아래와 같은 처리가 되어 있어서 다운로드를 할 수 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1758094471549&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;override fun shouldOverrideUrlLoading(
    view: WebView?,
    request: WebResourceRequest?
): Boolean {
    view?.context ?: return false

    try {
        val intent = Intent(Intent.ACTION_VIEW, request?.url)
        view.context?.startActivity(intent)
        return true
    } catch (e: Exception) {
        return false
    }
}&lt;/code&gt;&lt;/pre&gt;</description>
      <category>작업 일지</category>
      <category>Android</category>
      <category>webview</category>
      <author>O_Gyong</author>
      <guid isPermaLink="true">https://ogyong.tistory.com/87</guid>
      <comments>https://ogyong.tistory.com/87#entry87comment</comments>
      <pubDate>Wed, 17 Sep 2025 16:35:42 +0900</pubDate>
    </item>
    <item>
      <title>[Android] Multi Module + build-logic + hilt</title>
      <link>https://ogyong.tistory.com/85</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;최근에 프로젝트에 새로 들어가면서 구조를 설계하게 되었다. 마지막에 다뤘던 방식은 presentation-domain-data로 틀을 잡고 다른 필요한 부분(common 등)은 모듈로 추가하는 방식을 썼는데, 이번에 Now In Android(NIA)의 구조를 참고하면서 그 내용을 정리해 봤다. NIA와 차이가 있는데, feature-core 대신 feature-domain-data로 틀을 잡고 core 모듈에 common과 di 등을 두는 걸 생각했다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #000000; text-align: left;&quot;&gt;◾ &lt;/span&gt;&lt;/b&gt;&lt;b&gt;모듈 생성하기&lt;/b&gt;&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;794&quot; data-origin-height=&quot;618&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bih3BP/btsQjlls69b/xkZSkeQk1raAXPju3rFkK0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bih3BP/btsQjlls69b/xkZSkeQk1raAXPju3rFkK0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bih3BP/btsQjlls69b/xkZSkeQk1raAXPju3rFkK0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbih3BP%2FbtsQjlls69b%2FxkZSkeQk1raAXPju3rFkK0%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;603&quot; height=&quot;469&quot; data-origin-width=&quot;794&quot; data-origin-height=&quot;618&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;File &amp;gt; New &amp;gt; New Module을 통해 모듈을 생성할 수 있다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;모듈을 생성할 때 Templates 항목에 여러 가지가 있는데, 사용하는 항목들은 아래와 같다.&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 data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1. Phone&amp;amp;Tablet&lt;br /&gt;&lt;/b&gt;앱 실행에 필요한 모든 요소(Activity, Manifests, Resource 파일 등)를 포함하며 최종적으로 .apk 또는 .aab 파일을 만드는 모듈이다. 보통 프로젝트를 생성할 때 생기는 &lt;span style=&quot;color: #006dd7;&quot;&gt;app&lt;/span&gt; 모듈이 여기에 속한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2. Android Library&lt;/b&gt;&lt;br /&gt;안드로이드 프레임워크를 사용하는 라이브러리 코드가 포함된 모듈이다. 'Phone&amp;amp;Tablet'과의 차이는 .aar 파일을 만든다.&lt;br /&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;ex) feature, data&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;3. Java or Kotlin Library&lt;/b&gt;&lt;br /&gt;순수 자바 또는 코틀린으로만 이루어졌으며 빌드 시 .jar 파일을 만드는 모듈이다. 안드로이드 프레임워크에 의존하지 않기 때문에 domain 모듈에서 사용하려고 한다.&lt;br /&gt;&lt;span style=&quot;color: #006dd7; text-align: start;&quot;&gt; &lt;span style=&quot;text-align: start;&quot;&gt;ex) &lt;/span&gt;core:common, domain, build-logic &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;color: #ee2323;&quot;&gt;&lt;b&gt;※ 참고&lt;/b&gt;&lt;/span&gt;&lt;br /&gt;feature:home, feature:setting(또는 &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;core:common, core:ui)&lt;/span&gt;과 같은 구조를 만들려면 feature 폴더를 만들고 하위 레이어의 항목들을 별도의 모듈로 만든 후 해당 폴더에 이동시켜야 한다. 그리고 다음과 같이 settings.gradle에서 projectPaths를 수정해 준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt; &lt;b&gt;settings.gradle.kts(Project)&lt;/b&gt; &lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1757469132496&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 수정 전
include(&quot;:home&quot;)

// 수정 후
include(&quot;:feature:home&quot;)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;&lt;b&gt;include&lt;/b&gt;&lt;/i&gt;는 하나의 Gradle 빌드에 모듈을 포함시키는 기능을 한다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #000000; text-align: left;&quot;&gt;◾ &lt;/span&gt;&lt;/b&gt;&lt;b&gt;build-logic 적용(커스텀 플러그인)&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;build-logic 모듈은 프로젝트에서 사용되는 빌드 로직을 중앙 집중화하고 재사용하기 위해 사용되는 모듈이다. 흔히 커스텀 플러그인이라고 하는 것이 build-logic 모듈 내에 담을 플러그인을 말한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt; &lt;b&gt;settings.gradle.kts(Project)&lt;/b&gt; &lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;pluginManagement {
    includeBuild(&quot;build-logic&quot;)
    repositories {
        google {
            content {
                includeGroupByRegex(&quot;com\\.android.*&quot;)
                includeGroupByRegex(&quot;com\\.google.*&quot;)
                includeGroupByRegex(&quot;androidx.*&quot;)
            }
        }
        mavenCentral()
        gradlePluginPortal()
    }
}
dependencyResolutionManagement {
    repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
    repositories {
        google()
        mavenCentral()
    }
}

enableFeaturePreview(&quot;TYPESAFE_PROJECT_ACCESSORS&quot;)

rootProject.name = &quot;ArchitectureMultiModule&quot;
include(&quot;:app&quot;)
include(&quot;:feature:home&quot;)
include(&quot;:domain&quot;)
include(&quot;:data&quot;)
include(&quot;:core:common&quot;)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;build-logic 모듈을 만들면 includeBuild(&quot;build-logic&quot;)를 선언하여 프로젝트에 build-logic이 포함되었고, 다른 모듈에서 플러그인처럼 사용할 수 있게 해달라고 알린다.&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;enableFeaturePreview(&quot;TYPESAFE_PROJECT_ACCESSORS&quot;)는 문자열 기반의 모듈 종속성을 코틀린 객체로 자동 변환을 시켜주고, gradle에 projects 접근 권한을 부여한다.(settings.gradle:build-logic에 선언해도 괜찮음) 이 명령어를 통해 아래와 같이 쓸 수 있다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;build.gradle.kts(:app)&lt;/b&gt; &lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1756976259361&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 적용 전
dependencies {
    implementation(project(&quot;:feature:home&quot;))
}

// 적용 후
dependencies {
    implementation(projects.feature.home)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt; &lt;b&gt;※ 참고&lt;/b&gt;&lt;/span&gt; &lt;br /&gt;rootProject 이름에 특수문자나 공백이 있을 경우 에러가 발생한다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt; &lt;b&gt;Extension.kt&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1757469283927&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;internal val Project.applicationExtension: CommonExtension&amp;lt;*, *, *, *, *, *&amp;gt;
    get() = extensions.getByType&amp;lt;ApplicationExtension&amp;gt;()

internal val Project.libraryExtension: CommonExtension&amp;lt;*, *, *, *, *, *&amp;gt;
    get() = extensions.getByType&amp;lt;LibraryExtension&amp;gt;()

internal val Project.androidExtension: CommonExtension&amp;lt;*, *, *, *, *, *&amp;gt;
    get() = runCatching { applicationExtension }
        .recoverCatching { libraryExtension }
        .onFailure { println(&quot;Could not find Library or Application extension from this project&quot;) }
        .getOrThrow()

internal val ExtensionContainer.libs: VersionCatalog
    get() = getByType&amp;lt;VersionCatalogsExtension&amp;gt;().named(&quot;libs&quot;)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;build-logic에서 사용하게 될 Proejct와 VersionCatalog이다. Application과 Library 타입을 같이 처리하기 위해서 runCatching과 recoverCatching을 사용했다. 마지막은 버전 카탈로그를 코드로 쉽게 불러올 수 있게 하도록 선언했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;KotlinAndroidPlugin.kt&lt;/b&gt; &lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1757469303193&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;internal class KotlinAndroidPlugin : Plugin&amp;lt;Project&amp;gt; {
    override fun apply(target: Project) {
        with(target) {
            val libs = extensions.libs

            pluginManager.apply(&quot;org.jetbrains.kotlin.android&quot;) // pluginId 접근 1
            pluginManager.apply(libs.findPlugin(&quot;kotlin-compose&quot;).get().get().pluginId) // pluginId 접근 2

            androidExtension.apply {
                compileSdk = 36

                defaultConfig {
                    minSdk = 26
                    testInstrumentationRunner = &quot;androidx.test.runner.AndroidJUnitRunner&quot;
                }

                buildTypes {
                    getByName(&quot;release&quot;) {
                        isMinifyEnabled = false
                        proguardFiles(
                            getDefaultProguardFile(&quot;proguard-android-optimize.txt&quot;),
                            &quot;proguard-rules.pro&quot;
                        )
                    }
                }

                compileOptions {
                    sourceCompatibility = JavaVersion.VERSION_17
                    targetCompatibility = JavaVersion.VERSION_17
                }
            }

            extensions.configure&amp;lt;KotlinAndroidProjectExtension&amp;gt; {
                compilerOptions.jvmTarget.set(JvmTarget.JVM_11)
            }

            dependencies {
                add(&quot;implementation&quot;, libs.findLibrary(&quot;androidx-core-ktx&quot;).get())
                add(&quot;implementation&quot;, libs.findLibrary(&quot;androidx-activity-compose&quot;).get())
                add(&quot;implementation&quot;, libs.findLibrary(&quot;androidx-ui&quot;).get())
                add(&quot;implementation&quot;, libs.findLibrary(&quot;androidx-ui-graphics&quot;).get())
                add(&quot;implementation&quot;, libs.findLibrary(&quot;androidx-ui-tooling-preview&quot;).get())
                add(&quot;implementation&quot;, libs.findLibrary(&quot;androidx-material3&quot;).get())
                add(&quot;implementation&quot;, platform(libs.findLibrary(&quot;androidx-compose-bom&quot;).get()))
                add(&quot;testImplementation&quot;, libs.findLibrary(&quot;junit&quot;).get())
                add(&quot;androidTestImplementation&quot;, libs.findLibrary(&quot;androidx-junit&quot;).get())
                add(&quot;androidTestImplementation&quot;, libs.findLibrary(&quot;androidx-espresso-core&quot;).get())
            }
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;KotlinAndoridPlugin은 Application과 Library에서 공통으로 사용되는 Android/Kotlin 설정을 묶어둔 커스텀 플러그인이다. plugins, android, dependencis을 모두 포함하여 중복 코드를 줄이고 일관되게 관리할 수 있다. 다만 Apllication마다 그리고 Library마다 공통으로 쓰이는 부분이 있어서 모듈의 양이 많다면 예시와 달리 AplicationPlugin, LibraryPlugin으로 분리하는 것이 더 좋아 보인다. 이 경우 NIA의 build-logic을 참고하면 도움이 될 거 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt; &lt;b&gt;build.gradle.kts(build-logic)&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1757469353681&quot; class=&quot;kotlin&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;plugins {
    `kotlin-dsl`
    `kotlin-dsl-precompiled-script-plugins`
}

dependencies {
    compileOnly(libs.android.gradlePlugin)
    compileOnly(libs.kotlin.gradlePlugin)
}

kotlin {
    compilerOptions {
        jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_11
    }
}

gradlePlugin {
    plugins {
        register(&quot;kotlinAndroid&quot;) {
            id = libs.plugins.architecturemultimodule.android.kotlinAndroid.get().pluginId
            implementationClass = &quot;com.study.architecturemultimodule.build_logic.KotlinAndroidPlugin&quot;
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;커스텀 플러그인 작업이 끝나면 각 모듈에서 사용할 수 있도록 Gradle에 등록해야 한다. gradlePlugin에 커스텀 플러그인을 등록해 준다.&lt;/p&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;1195&quot; data-origin-height=&quot;607&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bJFp5Q/btsQmEFKgDm/W2hWEXfwIWdbqdueTIcY6K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bJFp5Q/btsQmEFKgDm/W2hWEXfwIWdbqdueTIcY6K/img.png&quot; data-alt=&quot;(왼) gradle(app) / (오) gradle(feature:home)&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bJFp5Q/btsQmEFKgDm/W2hWEXfwIWdbqdueTIcY6K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbJFp5Q%2FbtsQmEFKgDm%2FW2hWEXfwIWdbqdueTIcY6K%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;787&quot; height=&quot;400&quot; data-origin-width=&quot;1195&quot; data-origin-height=&quot;607&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;(왼) gradle(app) / (오) gradle(feature:home)&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 각 모듈의 plugins에 등록하여 사용할 수 있다. 이런 식으로 공통된 부분을 build-logic에서 처리하여 반복되는 코드를 줄이고 일관성을 유지할 수 있게 되었다.&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;color: #ee2323;&quot;&gt; ※ 참고&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;커스텀 플러그인을 모듈에 등록할 때 application 또는 library 보다 먼저 선언되면 안 된다. Gradle을 빌드 스크립트를 순차적으로 처리하기 때문에 먼저 선언하게 되면 android 블록이 생성되지 않아 에러가 발생한다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;◾ build-logic에서 버전 카탈로그 접근하기&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금까지의 내용을 보면 KotlinAndroidPlugin을 libs로 접근하고 있는데, 접근하는 방법은 다음과 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt; &lt;b&gt;libs.versions.toml&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1757469592878&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;architecturemultimodule-android-kotlinAndroid = {id = &quot;architecturemultimodule.android.kotlinAndroid&quot;}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 버전 카탈로그에 커스텀 플러그인 id를 등록한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt; &lt;b&gt;settings.gradle.kts(build-logic)&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1756976531102&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;dependencyResolutionManagement {
    repositories {
        google()
        mavenCentral()
        gradlePluginPortal()
    }
    versionCatalogs {
        create(&quot;libs&quot;) {
            from(files(&quot;../gradle/libs.versions.toml&quot;))
        }
    }
}

rootProject.name = &quot;build-logic&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그다음 settings.gradle.kts(build-logic)을 만든다. include 명령어로 선언된 모듈은 libs(버전 카탈로그)에 그냥 접근할 수 있지만 includeBuild()로 선언된 build-logic의 경우 libs에 바로 접근할 수 없다. 그래서 별도의 settings.gradle.kts(build-logic)를 만들어서 버전 카탈로그에 접근할 수 있게 위의 처리를 해준다.&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;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;build.gradle.kts&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1757470113792&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# build.gradle/kts(build-logic)
gradlePlugin {
    plugins {
        register(&quot;kotlinAndroid&quot;) {
            id = &quot;architecturemultimodule.android.kotlinAndroid&quot;
            implementationClass = &quot;com.study.architecturemultimodule.build_logic.KotlinAndroidPlugin&quot;
        }
    }
}

# build.gradle/kts(app) 
plugins {
	alias(libs.plugins.android.application)
	id(&quot;architecturemultimodule.android.kotlinAndroid&quot;)
}&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #000000; text-align: left;&quot;&gt;◾&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/b&gt;&lt;b&gt;Hilt 적용하기&lt;/b&gt;&lt;/h2&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt; &lt;b&gt;libs.versions.toml&lt;/b&gt; &lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1757469727918&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;[versions]
// ...
hilt = &quot;2.48&quot;
ksp = &quot;2.0.21-1.0.27&quot;
androidxHiltNavigationCompose = &quot;1.2.0&quot;

[libraries]
// ...
# Hilt 관련 Plugin에서 사용
androidx-hilt-navigation-compose = { group = &quot;androidx.hilt&quot;, name = &quot;hilt-navigation-compose&quot;, version.ref = &quot;androidxHiltNavigationCompose&quot; }
hilt-android = { group = &quot;com.google.dagger&quot;, name = &quot;hilt-android&quot;, version.ref = &quot;hilt&quot; }
hilt-compiler = { group = &quot;com.google.dagger&quot;, name = &quot;hilt-compiler&quot;, version.ref = &quot;hilt&quot; }
hilt-core = { group = &quot;com.google.dagger&quot;, name = &quot;hilt-core&quot;, version.ref = &quot;hilt&quot; }
hilt-android-compiler = { group = &quot;com.google.dagger&quot;, name = &quot;hilt-android-compiler&quot;, version.ref = &quot;hilt&quot; }
hilt-android-testing = { group = &quot;com.google.dagger&quot;, name = &quot;hilt-android-testing&quot;, version.ref = &quot;hilt&quot; }

[plugins]
// ...
ksp = { id = &quot;com.google.devtools.ksp&quot;, version.ref = &quot;ksp&quot; }
hilt = { id = &quot;com.google.dagger.hilt.android&quot;, version.ref = &quot;hilt&quot; }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 Hilt 사용을 위해 버전 카탈로그에 필요한 라이브러리를 등록한다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt; &lt;b&gt;HiltAndroidPlugin.kt&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1757469761788&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;internal class HiltAndroidPlugin : Plugin&amp;lt;Project&amp;gt; {
    override fun apply(target: Project) {
        with(target) {
            pluginManager.apply(&quot;com.google.dagger.hilt.android&quot;)
            pluginManager.apply(&quot;com.google.devtools.ksp&quot;)

            val libs = extensions.getByType&amp;lt;VersionCatalogsExtension&amp;gt;().named(&quot;libs&quot;)

            dependencies {
                add(&quot;implementation&quot;, libs.findLibrary(&quot;hilt.android&quot;).get())
                add(&quot;ksp&quot;, libs.findLibrary(&quot;hilt.compiler&quot;).get())
                add(&quot;kspAndroidTest&quot;, libs.findLibrary(&quot;hilt.compiler&quot;).get())
            }
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt; &lt;b&gt;HiltCorePlugin.kt&lt;/b&gt; &lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1757469780792&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;internal class HiltCorePlugin : Plugin&amp;lt;Project&amp;gt; {
    override fun apply(target: Project) {
        with(target) {
            pluginManager.apply(&quot;com.google.devtools.ksp&quot;)

            val libs = extensions.getByType&amp;lt;VersionCatalogsExtension&amp;gt;().named(&quot;libs&quot;)

            dependencies {
                &quot;implementation&quot;(libs.findLibrary(&quot;hilt.core&quot;).get())
                &quot;ksp&quot;(libs.findLibrary(&quot;hilt.compiler&quot;).get())
            }
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Hilt를 각 모듈에서 사용하기 위해 커스텀 플러그인을 만들어야 하는데, app과 featrue 같은 android에 의존하는 모듈은 hilt-android를 사용해도 되지만 domain 같은 순수 kotlin 영역은 hilt-core를 사용해야 한다. 그래서 커스텀 플러그인을 만들 때 HiltAndroidPlugin, HiltCorePlugin을 만들었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt; &lt;b&gt;libs.versions.toml&lt;/b&gt; &lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1757469816142&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# Custom Plugin
architecturemultimodule-android-kotlinAndroid = { id = &quot;architecturemultimodule.android.kotlinAndroid&quot; }
architecturemultimodule-hiltAndroid = {id = &quot;architecturemultimodule.hiltAndroid&quot; }
architecturemultimodule-hiltCore = {id = &quot;architecturemultimodule.hiltCore&quot; }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt; &lt;b&gt;build.gradle.kts&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1757469832946&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# build.gradle.kts(:app)
plugins {
    alias(libs.plugins.android.application)
    alias(libs.plugins.architecturemultimodule.android.kotlinAndroid)
	alias(libs.plugins.architecturemultimodule.android.hiltAndroid)
}

# build.gradle.kts(:domain)
plugins {
    alias(libs.plugins.jetbrains.kotlin.jvm)
    alias(libs.plugins.architecturemultimodule.hiltCore)
}

# build.gradle.kts(:build-logic)
gradlePlugin {
    plugins {
        register(&quot;kotlinAndroid&quot;) {
            id = &quot;architecturemultimodule.android.kotlinAndroid&quot;
            implementationClass = &quot;com.study.architecturemultimodule.build_logic.KotlinAndroidPlugin&quot;
        }

        register(&quot;hiltAndroid&quot;) {
            id = &quot;architecturemultimodule.hiltAndroid&quot;
            implementationClass = &quot;com.study.architecturemultimodule.build_logic.HiltAndroidPlugin&quot;
        }

        register(&quot;hiltCore&quot;) {
            id = &quot;architecturemultimodule.hiltCore&quot;
            implementationClass = &quot;com.study.architecturemultimodule.build_logic.HiltCorePlugin&quot;
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;플러그인을 만들면 kotlinAndroid에 했던 것처럼 hiltAndroid와 hiltCore를 등록한다.. 이후 각 모듈의 plugins에 hiltAndroid 또는 hiltCore를 추가하여 사용하면 된다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/vUR6A/btsQqm5FrH8/bHsWIOy8af5OKKcedCe4Wk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/vUR6A/btsQqm5FrH8/bHsWIOy8af5OKKcedCe4Wk/img.png&quot; data-origin-width=&quot;486&quot; data-origin-height=&quot;251&quot; data-is-animation=&quot;false&quot; data-widthpercent=&quot;52.17&quot; style=&quot;width: 51.5605%; margin-right: 10px;&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/vUR6A/btsQqm5FrH8/bHsWIOy8af5OKKcedCe4Wk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FvUR6A%2FbtsQqm5FrH8%2FbHsWIOy8af5OKKcedCe4Wk%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;486&quot; height=&quot;251&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/eKZ1tM/btsQnBpLC3b/kwSdUNbOPPX1154HUNxCbK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/eKZ1tM/btsQnBpLC3b/kwSdUNbOPPX1154HUNxCbK/img.png&quot; data-origin-width=&quot;577&quot; data-origin-height=&quot;325&quot; data-is-animation=&quot;false&quot; style=&quot;width: 47.2767%;&quot; data-widthpercent=&quot;47.83&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/eKZ1tM/btsQnBpLC3b/kwSdUNbOPPX1154HUNxCbK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FeKZ1tM%2FbtsQnBpLC3b%2FkwSdUNbOPPX1154HUNxCbK%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;577&quot; height=&quot;325&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
  &lt;figcaption&gt;(왼) gradle(:app) / (오) gradle(core:di)&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Hilt 모듈 바인딩 설정을 core:di 모듈에서 하고 있어서 gradle(:core:di)에 data와 domain 모듈을 등록해야 하고, HiltAndroidApp을 기점으로 di가 매핑되기 때문에 gradle(:app)에 core:di 모듈을 등록해야 한다.&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;&lt;a href=&quot;https://github.com/OhGyong/Android_Study/tree/master/Architecture%20Multi%20Module&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;b&gt;전체 코드&lt;/b&gt;&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1757314071190&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;Android_Study/Architecture Multi Module at master &amp;middot; OhGyong/Android_Study&quot; data-og-description=&quot;안드로이드 개발 공부. Contribute to OhGyong/Android_Study development by creating an account on GitHub.&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/OhGyong/Android_Study/tree/master/Architecture%20Multi%20Module&quot; data-og-url=&quot;https://github.com/OhGyong/Android_Study/tree/master/Architecture%20Multi%20Module&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/TKWcf/hyZJbM2Ad1/s2x6IL5dfXBgjpVg2xAzDK/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/bPkLqC/hyZIOEK3x0/DRPLo65cknhVcZ2TathOG0/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://github.com/OhGyong/Android_Study/tree/master/Architecture%20Multi%20Module&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/OhGyong/Android_Study/tree/master/Architecture%20Multi%20Module&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/TKWcf/hyZJbM2Ad1/s2x6IL5dfXBgjpVg2xAzDK/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/bPkLqC/hyZIOEK3x0/DRPLo65cknhVcZ2TathOG0/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Android_Study/Architecture Multi Module at master &amp;middot; OhGyong/Android_Study&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;안드로이드 개발 공부. Contribute to OhGyong/Android_Study development by creating an account on GitHub.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;</description>
      <category>Android/Architecture</category>
      <category>Android</category>
      <category>build-logic</category>
      <category>hilt</category>
      <category>multi module</category>
      <author>O_Gyong</author>
      <guid isPermaLink="true">https://ogyong.tistory.com/85</guid>
      <comments>https://ogyong.tistory.com/85#entry85comment</comments>
      <pubDate>Mon, 8 Sep 2025 21:44:55 +0900</pubDate>
    </item>
    <item>
      <title>[Android][Compose] kizitonwose/Calendar, 캘린더 라이브러리 사용기 #3</title>
      <link>https://ogyong.tistory.com/84</link>
      <description>&lt;figure id=&quot;og_1752473066343&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;[Android] kizitonwose/Calendar, 캘린더 라이브러리 사용기 #2&quot; data-og-description=&quot;[Android] kizitonwose/Calendar 캘린더 라이브러리 사용기 #1GitHub - kizitonwose/Calendar: A highly customizable calendar view and compose library for Android and Kotlin Multiplatform.A highly customizable calendar view and compose library for And&quot; data-og-host=&quot;ogyong.tistory.com&quot; data-og-source-url=&quot;https://ogyong.tistory.com/83&quot; data-og-url=&quot;https://ogyong.tistory.com/83&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/RQ0SX/hyZnaAQYmE/B78lu2kWExjjcgmXYhfoiK/img.png?width=800&amp;amp;height=1777&amp;amp;face=0_0_800_1777,https://scrap.kakaocdn.net/dn/bfrhpY/hyZjC6Rvjo/Id6tow9vjOB56RMBQMZ8Qk/img.png?width=800&amp;amp;height=1777&amp;amp;face=0_0_800_1777,https://scrap.kakaocdn.net/dn/R9inW/hyZjkyesH0/l1O0zsfwg4kER3kNRaJ0F1/img.png?width=607&amp;amp;height=604&amp;amp;face=0_0_607_604&quot;&gt;&lt;a href=&quot;https://ogyong.tistory.com/83&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://ogyong.tistory.com/83&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/RQ0SX/hyZnaAQYmE/B78lu2kWExjjcgmXYhfoiK/img.png?width=800&amp;amp;height=1777&amp;amp;face=0_0_800_1777,https://scrap.kakaocdn.net/dn/bfrhpY/hyZjC6Rvjo/Id6tow9vjOB56RMBQMZ8Qk/img.png?width=800&amp;amp;height=1777&amp;amp;face=0_0_800_1777,https://scrap.kakaocdn.net/dn/R9inW/hyZjkyesH0/l1O0zsfwg4kER3kNRaJ0F1/img.png?width=607&amp;amp;height=604&amp;amp;face=0_0_607_604');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;[Android] kizitonwose/Calendar, 캘린더 라이브러리 사용기 #2&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;[Android] kizitonwose/Calendar 캘린더 라이브러리 사용기 #1GitHub - kizitonwose/Calendar: A highly customizable calendar view and compose library for Android and Kotlin Multiplatform.A highly customizable calendar view and compose library for And&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;ogyong.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot;&gt;kizitonwose/Calendar, 캘린더 라이브러리 사용기 #2는 View 기반으로 작업을 하였다. 이번에는 Compose를 기반으로 사용해 봤다. Compose로 사용하는 방법에 대해서도 정리가 잘 되어 있어서 큰 어려움 없이 적용할 수 있었다. (&lt;a title=&quot;Compose 기반 설명서&quot; href=&quot;https://github.com/kizitonwose/Calendar/blob/main/docs/Compose.md&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Compose 기반 설명서&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;style5&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;◾ 임시 데이터 설정&lt;/b&gt;&lt;/h3&gt;
&lt;pre id=&quot;code_1757486239373&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class MainActivity : AppCompatActivity() {
    private var sampleList = listOf&amp;lt;SampleData&amp;gt;()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        enableEdgeToEdge()
        setContent {
            Box(
                modifier = Modifier
                    .fillMaxSize()
                    .background(Color.White)
                    .windowInsetsPadding(WindowInsets.systemBars)
            ) {
                setSampleData()
                MainScreen(sampleList)
            }

        }
    }

    private fun setSampleData() {
        val today = LocalDate.now()
        val formatter = DateTimeFormatter.ofPattern(&quot;yyyy-MM-dd&quot;)
        sampleList = (0 until 50).map { i -&amp;gt;
            SampleData(
                total = Random.nextInt(10, 100),
                date = today.minusDays(i.toLong()).format(formatter)
            )
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;UI 작업은 MainScreen에서 하는데, 그전에 임시 데이터 설정을 MainActivity에서 전달해 줬다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;◾&lt;span&gt;&amp;nbsp;&lt;/span&gt;CalendarView 적용하기&lt;/b&gt;&lt;/h3&gt;
&lt;pre id=&quot;code_1757486259699&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Composable
fun MainScreen(sampleList: List&amp;lt;SampleData&amp;gt; = listOf()) {
    val currentMonth = remember { YearMonth.now() }
    val startMonth = remember { currentMonth.minusMonths(2) }
    val endMonth = remember { currentMonth.plusMonths(0) }
    val daysOfWeek = remember { daysOfWeek() }

    // 60일 전과 오늘 이후 날짜 사용 금지
    val today = LocalDate.now()
    val agoDate = today.minusDays(60)

    val calendarState = rememberCalendarState(
        startMonth = startMonth,
        endMonth = endMonth,
        firstVisibleMonth = currentMonth,
        firstDayOfWeek = daysOfWeek.first()
    )

    HorizontalCalendar(
        state = calendarState,
        dayContent = {
            Day(
                day = it,
                sampleList = sampleList,
                useDate = !it.date.isBefore(agoDate) &amp;amp;&amp;amp; !it.date.isAfter(today),
            )
        }
    )
}

@Composable
fun Day(
    day: CalendarDay,
    sampleList: List&amp;lt;SampleData&amp;gt;,
    useDate: Boolean,
) {
    val count = sampleList.find { it.date == day.date.toString() }?.total?:0

    Box(
        modifier = Modifier.aspectRatio(1f),
        contentAlignment = Alignment.Center
    ) {
        Column {
            Text(
                modifier = Modifier.align(Alignment.CenterHorizontally),
                text = day.date.dayOfMonth.toString(),
                color =
                    if(useDate) {
                        colorResource(R.color.black)
                    } else {
                        colorResource(R.color.colorGray)
                    },
                style = CalendarTheme.bodyMedium
            )

            if(useDate) {
                Text(
                    text = &quot;${count}건&quot;,
                    color = colorResource(R.color.colorPurple),
                    style = CalendarTheme.bodySmall
                )
            }
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 설명서의 내용에 따라 기본적인 세팅을 했다. currentMonth에 minusMonths를 2, plustMonths를 0으로 하여 CalendarView의 스크롤을 제한했다. 그리고 60일 전과 오늘 이후 날짜에 대한 영역을 비활성화가 되도록 useDate를 만들었다.&amp;nbsp;&lt;/p&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;857&quot; data-origin-height=&quot;857&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/r0gaz/btsPgFmzJxf/bkh1Kq8KUjpdoSSM9gSS30/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/r0gaz/btsPgFmzJxf/bkh1Kq8KUjpdoSSM9gSS30/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/r0gaz/btsPgFmzJxf/bkh1Kq8KUjpdoSSM9gSS30/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fr0gaz%2FbtsPgFmzJxf%2Fbkh1Kq8KUjpdoSSM9gSS30%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;250&quot; height=&quot;250&quot; data-origin-width=&quot;857&quot; data-origin-height=&quot;857&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&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;b&gt;◾ 요일 표시하기&lt;/b&gt;&lt;/h3&gt;
&lt;pre id=&quot;code_1757486280263&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Composable
fun MainScreen(sampleList: List&amp;lt;SampleData&amp;gt; = listOf()) {
    // 생략

    HorizontalCalendar(
        state = calendarState,
        dayContent = {
            Day(
                day = it,
                sampleList = sampleList,
                useDate = !it.date.isBefore(agoDate) &amp;amp;&amp;amp; !it.date.isAfter(today),
            )
        },
        monthHeader = { month -&amp;gt;
            val daysOfWeek = month.weekDays.first().map { it.date.dayOfWeek }
            Week(daysOfWeek = daysOfWeek)
        }
    )
}

@Composable
fun Week(daysOfWeek: List&amp;lt;DayOfWeek&amp;gt;) {
    Row(modifier = Modifier.fillMaxWidth()) {
        for (dayOfWeek in daysOfWeek) {
            Text(
                modifier = Modifier.weight(1f),
                text = dayOfWeek.getDisplayName(TextStyle.SHORT, Locale.getDefault()),
                color = colorResource(when(dayOfWeek) {
                    DayOfWeek.SUNDAY -&amp;gt; {
                        R.color.colorRed
                    }
                    DayOfWeek.SATURDAY -&amp;gt; {
                        R.color.colorBlue
                    }
                    else -&amp;gt; {
                        R.color.colorDarkGray
                    }
                }),
                style = CalendarTheme.titleMedium
            )
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HorizontalCalendar의 monthHeader를 통해 캘린더의 머리글을 정할 수 있는데, 요일명을 넘겨줘서 요일을 표시했다. 요일을 표시할 때 일요일과 토요일은 컬러를 각각 빨간색과 파란색으로 했다.&lt;/p&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;857&quot; data-origin-height=&quot;857&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/pjNb3/btsPiV1189i/jIHfD5jHL3dA3w5WDlgca0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/pjNb3/btsPiV1189i/jIHfD5jHL3dA3w5WDlgca0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/pjNb3/btsPiV1189i/jIHfD5jHL3dA3w5WDlgca0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FpjNb3%2FbtsPiV1189i%2FjIHfD5jHL3dA3w5WDlgca0%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;250&quot; height=&quot;250&quot; data-origin-width=&quot;857&quot; data-origin-height=&quot;857&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;◾ 연, 월 타이틀 표시하기&lt;/b&gt;&lt;/h3&gt;
&lt;pre id=&quot;code_1757486294655&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Composable
fun MainScreen(sampleList: List&amp;lt;SampleData&amp;gt; = listOf()) {
    // 생략

    val calendarState = rememberCalendarState(
        startMonth = startMonth,
        endMonth = endMonth,
        firstVisibleMonth = currentMonth,
        firstDayOfWeek = daysOfWeek.first()
    )

    Column {
        YearMonthTitle(calendarState)
        HorizontalCalendar(
            // 생략
        )
    }
}

@Composable
fun YearMonthTitle(calendarState: CalendarState) {
    val yearMoth = calendarState.firstVisibleMonth.yearMonth.toString()
    Text(
        modifier = Modifier.fillMaxWidth(),
        textAlign = TextAlign.Center,
        text = yearMoth,
        style = CalendarTheme.titleLarge,
        color = colorResource(R.color.black)
    )
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;rememberCalendarState는 값이 업데이트되는 것을 감지하여 리 컴포지션을 해준다. 리 컴포지션이 되는 것을 이용하여 연도와 월을 표시했다. firstVisibleMonth는 CalendarView에 표시되는 첫 번째 달 값이라고 한다.&lt;/p&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;857&quot; data-origin-height=&quot;857&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dC2m0w/btsPhux4Xy4/l4NkuL7hfinYPPpy9gYJUk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dC2m0w/btsPhux4Xy4/l4NkuL7hfinYPPpy9gYJUk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dC2m0w/btsPhux4Xy4/l4NkuL7hfinYPPpy9gYJUk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdC2m0w%2FbtsPhux4Xy4%2Fl4NkuL7hfinYPPpy9gYJUk%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;250&quot; height=&quot;250&quot; data-origin-width=&quot;857&quot; data-origin-height=&quot;857&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;◾ 날짜 선택&lt;/b&gt;&lt;/h3&gt;
&lt;pre id=&quot;code_1757486308855&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Composable
fun MainScreen(sampleList: List&amp;lt;SampleData&amp;gt; = listOf()) {
    // 생략
    
    var selectedDate by remember { mutableStateOf&amp;lt;LocalDate?&amp;gt;(null) }

    Column {
        YearMonthTitle(calendarState)
        HorizontalCalendar(
            modifier = Modifier.padding(top = 10.dp),
            state = calendarState,
            dayContent = {
                Day(
                    day = it,
                    sampleList = sampleList,
                    isSelected = selectedDate == it.date,
                    useDate = !it.date.isBefore(agoDate) &amp;amp;&amp;amp; !it.date.isAfter(today),
                    onClick = { calendarDay -&amp;gt;
                        selectedDate = if(selectedDate == it.date) null else it.date
                    }
                )
            },
            monthHeader = { month -&amp;gt;
                val daysOfWeek = month.weekDays.first().map { it.date.dayOfWeek }
                Week(daysOfWeek = daysOfWeek)
            }
        )
    }
}

@Composable
fun Day(
    day: CalendarDay,
    sampleList: List&amp;lt;SampleData&amp;gt;,
    isSelected: Boolean,
    useDate: Boolean,
    onClick: (CalendarDay) -&amp;gt; Unit
) {
    val count = sampleList.find { it.date == day.date.toString() }?.total?:0

    Box(
        modifier = Modifier
            .aspectRatio(1f)
            .background(
                color = if (isSelected) colorResource(R.color.colorPurple) else Color.Transparent
            )
            .clickable(
                enabled = useDate,
                onClick = { onClick(day) }
            ),
        contentAlignment = Alignment.Center
    ) {
        Column {
            Text(
                modifier = Modifier.align(Alignment.CenterHorizontally),
                text = day.date.dayOfMonth.toString(),
                color =
                    if(useDate) {
                        if(isSelected) colorResource(R.color.white)
                        else colorResource(R.color.black)
                    } else {
                        colorResource(R.color.colorGray)
                    },
                style = CalendarTheme.bodyMedium
            )

            if(useDate) {
                Text(
                    text = &quot;${count}건&quot;,
                    color =
                        if(isSelected) colorResource(R.color.white)
                        else colorResource(R.color.colorPurple),
                    style = CalendarTheme.bodySmall
                )
            }
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;날짜 클릭과 선택 해제를 위해서 MainScreen에는 selectedDate 변수를 추가하고, Day UI에 isSelected와 onClick을 매개변수로 추가했다.&lt;/p&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-filename=&quot;calendar compose.gif&quot; data-origin-width=&quot;250&quot; data-origin-height=&quot;500&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bj3X9s/btsPi3y2lVw/NZm6k0eFx1WJPuR9E9cJxK/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bj3X9s/btsPi3y2lVw/NZm6k0eFx1WJPuR9E9cJxK/img.gif&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bj3X9s/btsPi3y2lVw/NZm6k0eFx1WJPuR9E9cJxK/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/bj3X9s/btsPi3y2lVw/NZm6k0eFx1WJPuR9E9cJxK/img.gif&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;250&quot; height=&quot;500&quot; data-filename=&quot;calendar compose.gif&quot; data-origin-width=&quot;250&quot; data-origin-height=&quot;500&quot;/&gt;&lt;/span&gt;&lt;/figure&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;&lt;a title=&quot;전체 코드&quot; href=&quot;https://github.com/OhGyong/Android_Study/tree/calendar-by-kizitonwose-step3/Calendar%20by%20Kizitonwose&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;b&gt;전체 코드&lt;/b&gt;&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1752481900212&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;Android_Study/Calendar by Kizitonwose at calendar-by-kizitonwose-step3 &amp;middot; OhGyong/Android_Study&quot; data-og-description=&quot;안드로이드 개발 공부. Contribute to OhGyong/Android_Study development by creating an account on GitHub.&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/OhGyong/Android_Study/tree/calendar-by-kizitonwose-step3/Calendar%20by%20Kizitonwose&quot; data-og-url=&quot;https://github.com/OhGyong/Android_Study/tree/calendar-by-kizitonwose-step3/Calendar%20by%20Kizitonwose&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/zCkW7/hyZjqej9VP/pLtCqpuEvkps0JKnfa8BT1/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/cPp6gN/hyZjb9cCMe/Bf0BwVpgl4496PCXg5nCw1/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://github.com/OhGyong/Android_Study/tree/calendar-by-kizitonwose-step3/Calendar%20by%20Kizitonwose&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/OhGyong/Android_Study/tree/calendar-by-kizitonwose-step3/Calendar%20by%20Kizitonwose&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/zCkW7/hyZjqej9VP/pLtCqpuEvkps0JKnfa8BT1/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/cPp6gN/hyZjb9cCMe/Bf0BwVpgl4496PCXg5nCw1/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Android_Study/Calendar by Kizitonwose at calendar-by-kizitonwose-step3 &amp;middot; OhGyong/Android_Study&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;안드로이드 개발 공부. Contribute to OhGyong/Android_Study development by creating an account on GitHub.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Android/UI</category>
      <category>Andoird</category>
      <category>CalendarView</category>
      <category>compose</category>
      <category>kizitonwose</category>
      <category>Kotlin</category>
      <author>O_Gyong</author>
      <guid isPermaLink="true">https://ogyong.tistory.com/84</guid>
      <comments>https://ogyong.tistory.com/84#entry84comment</comments>
      <pubDate>Mon, 14 Jul 2025 22:20:31 +0900</pubDate>
    </item>
  </channel>
</rss>