<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>woo.log</title>
        <link>https://velog.io/</link>
        <description>어제보다 나은 개발자가 되자</description>
        <lastBuildDate>Fri, 01 May 2026 09:25:36 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>woo.log</title>
            <url>https://velog.velcdn.com/images/dev_hyunwoo/profile/f8c4851d-0cf3-4528-8500-96ce1d46e7ba/image.png</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. woo.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/dev_hyunwoo" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[[kotlin] Class Discriminator  사용 경험담]]></title>
            <link>https://velog.io/@dev_hyunwoo/kotlin-Class-Discriminator-%EC%82%AC%EC%9A%A9-%EA%B2%BD%ED%97%98%EB%8B%B4</link>
            <guid>https://velog.io/@dev_hyunwoo/kotlin-Class-Discriminator-%EC%82%AC%EC%9A%A9-%EA%B2%BD%ED%97%98%EB%8B%B4</guid>
            <pubDate>Fri, 01 May 2026 09:25:36 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/dev_hyunwoo/post/22adea8c-1a9b-49b3-857a-40c25f6ab189/image.png" alt=""></p>
<p>kotlin으로 개발을 하는 사람이라면 sealed interface나 sealed class를 많이 써봤을 것이다. 단지 상속 가능한 타입을 컴파일 타임에서 알 수 있다는 장점만으로도 충분히 많이 잘 사용하는 거라고 생각한다. Class Discriminator를 사용해서 좋았던 경험을 적어볼려고 한다.</p>
<h2 id="목차">목차</h2>
<h3 id="1sealed란-무엇일까">1.sealed란 무엇일까?</h3>
<h3 id="2sealed-interface-vs-sealed-class에-차이점">2.sealed interface vs sealed class에 차이점</h3>
<h3 id="3class-discriminator-사용법">3.Class Discriminator 사용법</h3>
<hr>
<h2 id="1️⃣-sealed란-무엇일까">1️⃣ sealed란 무엇일까?</h2>
<p>sealed는 상속 가능한 타입의 집합을 컴파일 타임에서 고정한다는 키워드이다. </p>
<h3 id="왜-자주-쓰일까">왜 자주 쓰일까?</h3>
<p>우선 when을 사용할 때 exhaustive check가 가능하다. 새로운 하위 타입이 추가되면 컴파일 에러가 나서 빠트린 곳을 찾기 쉽다.</p>
<hr>
<h2 id="2️⃣-sealed-interface-vs-sealed-class에-차이점">2️⃣ sealed interface vs sealed class에 차이점</h2>
<p>이건 sealed만 붙었지 사실 interface vs class라고 봐도 무방하다. </p>
<h3 id="class">class</h3>
<ul>
<li>class는 생성자를 가질 수 있다.</li>
<li>단일 상속만 가능하다.</li>
<li>추상 메서드뿐 아니라 구현된 메서드도 가질 수 있음.</li>
</ul>
<h3 id="interface">interface</h3>
<ul>
<li>생성자를 가질 수 없다.</li>
<li>다중 구현이 가능하다.</li>
<li>기본 구현이 있는 메서드는 가질 수 있다.</li>
</ul>
<h3 id="그럼-어떤-걸-써야될까">그럼 어떤 걸 써야될까?</h3>
<p>sealed class를 써야될 때는 공통 프로퍼티를 생성자로 강제할 때 주로 사용하는 것 같다.</p>
<p>사실 그외에선 sealed interface를 사용하는게 맞는 것 같다.</p>
<p>우선 sealed interface는 다중구현이 가능하고 공통 프로퍼티를 유연하게 상속하여 사용할 수 있어서 범용성 있게 사용할 수 있기 때문이다.</p>
<hr>
<h2 id="3️⃣-class-discriminator-사용법">3️⃣ Class Discriminator 사용법</h2>
<h3 id="class-discriminator란">Class Discriminator란?</h3>
<p>타입 판별자라는 뜻이다.
kotlinx.serialization는 다형적 타입을 직렬화/역직렬화 할때 discriminator라는 특별한 필드를 사용한다. </p>
<h3 id="경험담">경험담</h3>
<p>서버에서 내려준 값들을 역직렬화 할 때 보통 data class에 @Serializable 조합으로 많이 사용한다. 나도 그렇게 사용해왔다. 근데 작업을 하다보면 하나의 API인데 타입에 따라 값이 다른 경우가 있다. 그런 경우에서 data class를 정의할 때 타입에 따라 모든 값을 고려하여 필드를 정의해준 경험이 있다면 내가 알려주는 방식을 사용하면 좀 더 깔끔하게 타입을 나눌 수 있을 것이다.</p>
<h3 id="기존에-했던-방식">기존에 했던 방식</h3>
<p>우선 예를 들어보면 SNS관리하는 API가 있다고 가정하자. 각 SNS별로 화면에 필요한 값들이 공통적으로 있는데 몇개는 각 SNS별로 필드가 다른 상황이다. 그래서 각 사용자가 연결된 SNS에 따라 값들이 달라지는데 그걸 모두 정의했다고 생각해보자</p>
<pre><code class="language-kotlin">
@Serializable
data class SNSDetailResponse(
    // 공통
    val type : SNS,
    val title : String,
    val content : String,
    val name : String,

    // naver
    val todayCount : Int?,

    // instargram
    val followerCount : Int?,

    // youtube
    val subscriber : Int?,
)

@Serializable
enum class SNS {
    @SerialName(&quot;naver&quot;)
    Naver,
    @SerialName(&quot;instagram&quot;)
       Instagram,
    @SerialName(&quot;youtube&quot;)
    Youtube
}
</code></pre>
<p>보통 이렇게 정의를 하여 공통인 부분들은 non nullable로 하고 타입마다 다르게 내려주는 값들은 nullable로 하여 역직렬화시 크래시를 방지해준다.</p>
<h3 id="class-discriminator를-사용한-방식">Class Discriminator를 사용한 방식</h3>
<pre><code class="language-kotlin">
sealed interface SNSDetailResponse {
    val title : String
    val content : String
    val name : String

    @Serializable
    @SerialName(&quot;naver&quot;)
    data class Naver(
        override val title: String,
        override val content: String,
        override val name: String,
        val today : Int
    ) : SNSDetailResponse

    @Serializable
    @SerialName(&quot;instagram&quot;)
    data class Naver(
        override val title: String,
        override val content: String,
        override val name: String,
        val followerCount : Int
    ) : SNSDetailResponse

    @Serializable
    @SerialName(&quot;youtube&quot;)
    data class Naver(
        override val title: String,
        override val content: String,
        override val name: String,
        val subscriber : Int
    ) : SNSDetailResponse
}
</code></pre>
<p>이렇게 정의하면 끝난다! 지금처럼 한개만 별도 필드를 가지고 있을 때는 모든 필드를 정의하는 게 더 간단하게 보일지라도 각 SNS별로 각 필드가 많아진다면 당연히 가독성이 떨어질 것 이다.</p>
<p>@SerialName에서 타입을 정의해주면 디폴트가 type에 따라 나눠주기 때문에 서버에서 type으로 내려주면 된다.</p>
<pre><code>{
  &quot;type&quot;: &quot;instagram&quot;,
  &quot;title&quot;: &quot;오늘의 일상&quot;,
  &quot;content&quot;: &quot;맛집 다녀왔어요&quot;,
  &quot;name&quot;: &quot;김철수&quot;,
  &quot;hashtags&quot;: [&quot;맛집&quot;, &quot;일상&quot;],
  &quot;imageUrl&quot;: &quot;https://...&quot;
}</code></pre><p> 하지만 다른 값으로 판별을 하고 싶다면 </p>
<pre><code class="language-kotlin">val json = Json {
    classDiscriminator = &quot;name&quot;  
}</code></pre>
<p>json을 생성할 때 바꿔주면 된다. 서버와 협의하여 편한걸로 하는게 좋을 것 같다.</p>
<hr>
<h2 id="후기">후기</h2>
<p>기존에는 모든 필드를 고려했다면 저렇게 타입을 나눠서 분리하니깐 가독성이 좋아졌다.
물론 초기 생성할 때는 번거로울 수 있지만 나중에 유지보수를 할 때에는 도움이 많이 될 것 같다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[kotlin] Generic 맛만 보기]]></title>
            <link>https://velog.io/@dev_hyunwoo/kotlin-Generic-%EB%A7%9B%EB%A7%8C-%EB%B3%B4%EA%B8%B0</link>
            <guid>https://velog.io/@dev_hyunwoo/kotlin-Generic-%EB%A7%9B%EB%A7%8C-%EB%B3%B4%EA%B8%B0</guid>
            <pubDate>Sat, 28 Mar 2026 07:48:03 GMT</pubDate>
            <description><![CDATA[<p>안드로이드 개발을 하면서 Generic를 써봤지만 제대로 이해하고 쓴 적이 없는 것 같다.
그래서 이번 기회에 무슨 맛인지만 볼려고 한다.</p>
<p><img src="https://velog.velcdn.com/images/dev_hyunwoo/post/18c7d0f0-42f2-49a5-b429-62ec2e551543/image.png" alt=""></p>
<hr>
<h2 id="🥸-제네릭generic이란">🥸 제네릭(Generic)이란?</h2>
<p>제네릭이란 간단하게 말하자면 <strong>타입을 일반화해서 재사용성을 높이는 문법</strong>이다.
특히 타입 안정성과 유연성이 좋기 때문에 많이 사용된다.</p>
<h3 id="😓-그래서-어떻게-사용하는건데">😓 그래서 어떻게 사용하는건데..?</h3>
<p><img src="https://velog.velcdn.com/images/dev_hyunwoo/post/e6b59a8f-65eb-4a18-875e-c34f2f2f06b1/image.png" alt="">
제네릭은 총 5가지에 타입이 있다. </p>
<p>기본 형태는 </p>
<pre><code class="language-kotlin">class Box&lt;T&gt;(val value : T)</code></pre>
<p>이런식으로 사용한다. </p>
<h3 id="😇-그래서-장점이-뭔데">😇 그래서 장점이 뭔데...</h3>
<p>간단한 예시를 들자면 </p>
<p>내가 Log를 남기기 위해 Log 남기는 함수를 만든다고 생각해보자.
근데 파라미터의 값이 Int도 있고 String도 있다고 가정해보자.
그럼 타입별로 함수를 만들어야된다.</p>
<pre><code class="language-kotkin">// Int형
fun printInt(value : Int) { 
    println(value
}

// String형
fun printInt(value : String) { 
    println(value
}</code></pre>
<p>하지만 제네릭을 쓰면 </p>
<pre><code class="language-kotlin">fun &lt;T&gt;print(value : T) {
    println(value)
}</code></pre>
<p>하나로 해결이 가능하다.</p>
<p>하지만 별도에 타입별로 처리를 안해줘도 되는 이유는</p>
<p>println의 파라미터 타입이 Any?이기 때문이다.</p>
<pre><code class="language-kotlin">/** Prints the given [message] and the line separator to the standard output stream. */
public actual fun println(message: Any?) {
    printlnImpl(message?.toString())
}</code></pre>
<p>그렇기 때문에 타입 캐스팅을 안해줘도 되는 것이다. </p>
<p>만약 파라미터 타입이 정해져있으면 타입 캐스팅을 별도 처리 해줘야된다.</p>
<h3 id="👮🏼-타입-제한-upper-bound">👮🏼 타입 제한 (Upper Bound)</h3>
<p>제네릭을 사용할 때 타입을 제한할 수도 있다. 예를 들면</p>
<pre><code class="language-kotlin">fun &lt;T : Number&gt; sun(a : T, b : T) : Double {
    return a.toDouble() + b.toDouble()
}</code></pre>
<p><T : Number> Number의 하위 타입만 받을 수 있다는 뜻이다. 그래서 
Number을 상속받는 Int, Long, Float, Double만 받을 수 있고 나머지 타입은 모두 컴파일 에러가 뜬다.</p>
<p>공변성과 반공변성에 대해서도 알고 싶으면 부족하지만 나의 <a href="https://velog.io/@dev_hyunwoo/Kotlin-%EA%B3%B5%EB%B3%80%EC%84%B1-%EB%B0%98%EA%B3%B5%EB%B3%80%EC%84%B1-%EA%B7%B8%EA%B2%8C-%EB%AD%94%EB%8D%B0">이전글</a>을 확인해도 좋다</p>
<hr>
<h2 id="🥳-그래서-실무에서-어떻게-사용될까">🥳 그래서 실무에서 어떻게 사용될까?</h2>
<p>지금 현재 회사에서는 Composable한 함수를 재사용되기 위해 많이 사용되는데 </p>
<p>  보통 앱을 만들 때 각 회사마다 디자인 시스템이 있다. 지금 만들고 있는 앱도 디자인 시스템이 있다. 그래서 공통적으로 쓰는 컴포넌트가 있는데 대표적으로 Tabs에 대해 소개해보고 싶다.</p>
<p>앱 개발자라면  Tabs를 많이 사용해봤을 것이다. 
<img src="https://velog.velcdn.com/images/dev_hyunwoo/post/b93c0c8e-870f-4bbf-bd42-77990d481da4/image.png" alt=""></p>
<p>  요런 느낌인데 이런 컴포넌트들이 앱내에 공통적으로 사용된다.
  근데 각 화면에 같은 디자인들을 매번 코드를 작성하기엔 비효율적이다.
  그래서 보통 이런 디자인시스템에서 공통 컴포넌트들은 재사용 가능하게끔 만든다.</p>
<h3 id="☝️-우선-interface로-보여줄-값들을-정의한다">☝️ 우선 Interface로 보여줄 값들을 정의한다.</h3>
<pre><code class="language-kotlin">interface Tabs {
      val title : String // Tab 화면에 보여질 텍스트
  }</code></pre>
<p>Tabs에선 보통 title하나면 충분하기 때문에 하나만 정의했다.</p>
<h3 id="𝟚-제너릭-공통-함수-생성">𝟚 제너릭 공통 함수 생성</h3>
<pre><code class="language-kotlin">@Composable
  fun &lt;T : Tabs&gt; TabBar(
      tabs: List&lt;T&gt;,
      selectedTab : T,
      onSelectedTab : (T) -&gt; Unit
  ) {
   ...
  }</code></pre>
<p>제네릭 타입을 T를 받되 타입 제한을 지정하여 Tabs를 상속받은 구현체만 받도록 했다.</p>
<h3 id="3️⃣-구현하여-사용해보자">3️⃣ 구현하여 사용해보자.</h3>
<pre><code class="language-kotlin">  // Tabs 구현
enum class MainTabs(override val title : String) : Tabs {
      HOME(&quot;홈&quot;),
      MY(&quot;마이&quot;),
      SEARCH(&quot;검색&quot;)
  }


 // MainScreen.kt
  TabBar(
      tabs = MainTabs.entries,    
    selectedTab = MainTabs.HOME,
      onSelectedTab = viewModel::onSelectedTab
  )</code></pre>
<p>이렇게 하면 Tabs를 각 화면에 맞게 생성만 해준다면 번거롭게 매번 로직을 생성 안해도 재사용 할 수 있게된다.
  그게 바로 제너릭에 장점인 것 같다.</p>
<hr>
<h2 id="👨🏼💻-후기">👨🏼‍💻 후기</h2>
<p>  제너릭은 개발을 하면서 꼭 필요한 문법인 것 같다. 화면을 만들때도 최대한 재사용을 할려고 노력한다. 앞으로 제너릭을 더 활용하는 개발자가 되고싶다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Android] derivedStateOf의 진짜 사용법]]></title>
            <link>https://velog.io/@dev_hyunwoo/Android-derivedStateOf%EC%9D%98-%EC%A7%84%EC%A7%9C-%EC%82%AC%EC%9A%A9%EB%B2%95</link>
            <guid>https://velog.io/@dev_hyunwoo/Android-derivedStateOf%EC%9D%98-%EC%A7%84%EC%A7%9C-%EC%82%AC%EC%9A%A9%EB%B2%95</guid>
            <pubDate>Sat, 28 Feb 2026 07:11:54 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/dev_hyunwoo/post/0395b7c4-40f5-4d67-ad63-2c77312866f6/image.png" alt=""></p>
<h2 id="개요">개요</h2>
<p>derivedStateOf를 실무에서 종종 사용한 적이 있다.
최근에 사용했던 경험은 배송지를 등록하는 화면에서 조건에 맞을 때 다음 버튼이 활성화 되면 되는 작업이였다.</p>
<pre><code class="language-kotlin">// AddAddressScreen.kt
val canGoNext by remember {
    derivedStateOf {
        uiModel.addressName.isNotEmpty &amp;&amp; // 주소가 빈값이 아닌지
        uiModel.regionCode.isNotEmpty &amp;&amp; // 지역코드가 빈값이 아닌지
        uiModel.addressName.length &lt;= 5 &amp;&amp; // 주소가 5글자 이내인지
    }
}</code></pre>
<p>라는 로직을 작성한 뒤 테스트 해보왔다. 근데 동작을 제대로 안헀다.
부끄럽게도 난 이유를 몰라서 찾아봤었다. </p>
<p>그래서 이참에 안됐던 이유와 함께 derivedStateOf를 알아볼려고 한다.</p>
<hr>
<h2 id="🦷-안됐던-이유는">🦷 안됐던 이유는?</h2>
<p>안됐던 이유는 부끄럽게도 remember에 key값을 안줬기때문이다. 현재 derivedStateOf 안에 값들은 uiModel로 이루어져있는 값들인데 state한 값들이 아니여서 자동으로 추적이 안됐기때문이다.</p>
<pre><code class="language-kotlin">// AddAddressScreen.kt
val canGoNext by remember(uiModel) {
    derivedStateOf {
        uiModel.addressName.isNotEmpty &amp;&amp; // 주소가 빈값이 아닌지
        uiModel.regionCode.isNotEmpty &amp;&amp; // 지역코드가 빈값이 아닌지
        uiModel.addressName.length &lt;= 5 &amp;&amp; // 주소가 5글자 이내인지
    }
}</code></pre>
<p>그래서 이게 올바른 구현이다.</p>
<hr>
<h2 id="🌞-그럼-이해완료">🌞 그럼 이해완료?</h2>
<p>위에 로직에서 이상한 점을 눈치 못챘다면 당신은 제대로 이해하지 못하고 있는 것이다.. (나포함ㅠ) </p>
<blockquote>
<p>최근에 사용했던 경험은 배송지를 등록하는 화면에서 조건에 맞을 때 다음 버튼이 활성화 되면 되는 작업이였다.</p>
</blockquote>
<p>해당 조건을 만족시키기 위해서는 derivedStateOf를 굳이 사용할 필요가 없다.
다시말해 derivedStateOf를 사용해도 최적화 시키는데 전혀 도움이 안되고 있다는 소리다.</p>
<h3 id="🤦🏻♂️-왜">🤦🏻‍♂️ 왜?</h3>
<p>derivedStateOf는 state로부터 파생된 결과값을 만드는데 이때 state값이 빈번하게 변할 때 사용하는 것이 좋다.</p>
<p>예를 들자면</p>
<pre><code class="language-kotlin">val addressTextFieldState = TextFieldState() // state한 값

val enableButton by remember(uiModel.regionCode) {
    derivedStateOf {
        addressTextFieldState.text.isNotEmpty &amp;&amp; // 주소가 빈값이 아닌지
        uiModel.regionCode.isNotEmpty &amp;&amp; // 지역코드가 빈값이 아닌지
        addressTextFieldState.text.length &lt;= 5 &amp;&amp; // 주소가 5글자 이내인지
    }
}</code></pre>
<p>이 경우이다. 주소를 입력할 때 초당 수차례 발생하는 걸 방지할 수 있다는 점에서 충분히 derivedStateOf를 잘쓰고있다고 생각한다.</p>
<p>그래서 내가 하고자 했던 로직은 사실 의미가 없는 것이고 사실</p>
<pre><code class="language-kotlin">val canGoNext =
    uiModel.addressName.isNotEmpty() &amp;&amp;
    uiModel.regionCode.isNotEmpty() &amp;&amp;
    uiModel.addressName.length &lt;= 5</code></pre>
<p>이렇게 사용하면 되는 것이다...!!</p>
<hr>
<h2 id="결론">결론</h2>
<p>derivedStateOf는 텍스트필드나 스크롤값들처럼 빈번하게 변하는 state한 값들로부터 파생된 결과값을 도출할 때 제일 효율적으로 사용할 수 있는 것 같다. </p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Android] Toast는 어떻게 동작할까?]]></title>
            <link>https://velog.io/@dev_hyunwoo/Android-Toast%EB%8A%94-%EC%96%B4%EB%96%BB%EA%B2%8C-%EB%8F%99%EC%9E%91%ED%95%A0%EA%B9%8C</link>
            <guid>https://velog.io/@dev_hyunwoo/Android-Toast%EB%8A%94-%EC%96%B4%EB%96%BB%EA%B2%8C-%EB%8F%99%EC%9E%91%ED%95%A0%EA%B9%8C</guid>
            <pubDate>Sat, 14 Feb 2026 05:33:15 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/dev_hyunwoo/post/63537b42-074a-460e-94d3-da818ac21fbb/image.png" alt=""></p>
<h2 id="🏁-시작하기-앞서">🏁 시작하기 앞서..</h2>
<p><a href="https://velog.io/@skydoves/manifest-android-interview">skydoves님의 글을</a> 읽으며, 마치 나의 치부를 정확히 짚어낸 것 같은 기분이 들었다.
그동안 나는 면접을 준비하거나 공부를 할 때, 어떤 기술을 사용해 왔는지, 그리고 그 기술을 어떻게 활용했는지에만 집중해왔다.</p>
<p>솔직히 말하면, 그 기술이 내부적으로 어떻게 동작하는지에 대해서 깊이 고민하며 공부한 적은 많지 않았다.
“나는 이 기술을 사용할 수 있다”는 사실을 보여주는 데에만 신경 썼을 뿐, 그 이면에 있는 구조나 원리에 대해서는 크게 신경 쓰지 않았던 것이다.</p>
<p>지금 돌아보면, 그것이 나의 분명한 약점이었다.
분명 실무에서는 익숙하게 사용하던 기술들이었지만, 막상 면접에서 “이 기술은 내부적으로 어떻게 동작하나요?”라는 질문을 받으면 선뜻 대답하지 못했던 기억이 적지 않다.
사용은 하고 있었지만, 이해하고 있다고 말하기에는 부족한 상태였던 것이다.</p>
<p>최근에는 내부 동작을 항상 살펴보려고 한다.
그래서 오늘은 안드로이드에서 흔히 사용하지만 어떻게 동작하는지는 잘 몰랐던 Toast에 대해 알아보려한다.</p>
<hr>
<h2 id="1️⃣-toast는-context가-왜-필요할까">1️⃣ Toast는 Context가 왜 필요할까</h2>
<h3 id="☝️-system-service에-접근하기-위함">☝️ System Service에 접근하기 위함</h3>
<p>Toast는 WindowManager를 통해 화면에 표시되낟. 이때 Context에서 지원하는 getSystemService() 함수를 통해 WindowManager 객체를 가져와서 사용한다.</p>
<pre><code class="language-kotlin">private WindowManager getWindowManager(View view) {
    Context context = mContext.get();
    if (context == null &amp;&amp; view != null) {
        context = view.getContext();
    }
    if (context != null) {
        return context.getSystemService(WindowManager.class);
    }
    return null;
}

// e.g.
WindowManager.addView(toastView, layoutParams)</code></pre>
<blockquote>
<p>Window는 최상위 UI 컨테이너로 우리가 보는 모든 UI ( Activity, Dialog, Toast)등 모두 Window에 붙어 보여지게 된다.</p>
</blockquote>
<h3 id="✌️-resource에-접근하기-위함">✌️ Resource에 접근하기 위함</h3>
<p>Toast의 기본 위치, Gravity값, 레이아웃 등은 모두 System Resource에서 가져온다. System Resource는 Android OS 내부 리소스를 의미하며ㅡ 시스템 전체에서 공유한다. </p>
<pre><code class="language-java">mTN.mY = context.getResources().getDimensionPixelSize(
    com.android.internal.R.dimen.toast_y_offset);
mTN.mGravity = context.getResources().getInteger(
    com.android.internal.R.integer.config_toastDefaultGravity);</code></pre>
<h3 id="🤟-layout을-inflation하기-위함">🤟 Layout을 Inflation하기 위함</h3>
<p>Toast의 기본 텍스트 뷰를 생성할 때 LayoutInflater가 필요하며, 이 역시 Context를 통해 접근한다. </p>
<pre><code class="language-java">public static View getTextToastView(Context context, CharSequence text) {
    View view = LayoutInflater.from(context).inflate(TEXT_TOAST_LAYOUT, null);
    TextView textView = view.findViewById(com.android.internal.R.id.message);
    textView.setText(text);
    return view;
}</code></pre>
<hr>
<h2 id="2️⃣-io-스레드에서-toast를-띄우면">2️⃣ I/O 스레드에서 Toast를 띄우면?</h2>
<blockquote>
<p>결론부터 말하면 I/O 스레드에서 Toast를 띄우면 RuntimeException이 나면서 크래시가 발생한다.</p>
</blockquote>
<h3 id="💁-왜그럴까">💁 왜그럴까?</h3>
<pre><code class="language-java">/**
 * Constructs an empty Toast object.  If looper is null, Looper.myLooper() is used.
 * @hide
 */
public Toast(@NonNull Context context, @Nullable Looper looper) {
    mContext = context;
    mToken = new Binder();
    looper = getLooper(looper);
    mHandler = new Handler(looper);
    mCallbacks = new ArrayList&lt;&gt;();
    mTN = new TN(context, context.getPackageName(), mToken,
            mCallbacks, looper);
    mTN.mY = context.getResources().getDimensionPixelSize(
            com.android.internal.R.dimen.toast_y_offset);
    mTN.mGravity = context.getResources().getInteger(
            com.android.internal.R.integer.config_toastDefaultGravity);
}

private Looper getLooper(@Nullable Looper looper) {
    // Looper 검증 로직!
    if (looper != null) {
        return looper;
    }
    return checkNotNull(Looper.myLooper(),
            &quot;Can&#39;t toast on a thread that has not called Looper.prepare()&quot;);
}</code></pre>
<p>토스트는 내부적으로 Handler를 사용하여 처리하고 있다. Handler는 Looper가 있는 스레드에서 동작할 수 있는데, Main 스레드의 Looper는 앱이 시작되는 시점에 초기화가 이뤄지는 반면, I/O 스레드는 기본적으로 Looper가 초기화 되지 않는다. </p>
<pre><code class="language-java">public static @NonNull &lt;T&gt; T checkNotNull(
        final T reference,
        final @NonNull @CompileTimeConstant String messageTemplate,
        final Object... messageArgs) {
    if (reference == null) {
        throw new NullPointerException(String.format(messageTemplate, messageArgs));
    }
    return reference;
}</code></pre>
<p>그래서 Looper.myLooper()가 null을 반환하여 checkNotNull()에서 RuntimeException을 던진다.</p>
<p>따라서 Toast를 띄울려면 I/O 스레드에서 Main 스레드로 전환해야된다.</p>
<hr>
<h2 id="3️⃣-여러-앱에서-동시에-toast를-띄우면">3️⃣ 여러 앱에서 동시에 Toast를 띄우면?</h2>
<blockquote>
<p>결론부터 말하자면 여러앱에서 동시에 Toast를 띄우면 시스템이 전부 받아서 순서대로 보여준다. 즉, 겹쳐서 동시에 뜨지는 않는다.</p>
</blockquote>
<h3 id="왜-겹치지-않을까">왜 겹치지 않을까?</h3>
<p>Toast는 앱마다 따로 관리되는 게 아니라, 
시스템 레벨에서 중앙 관리되기 때문이다.</p>
<p>즉, 각 앱이 Toast를 호출하면 시스템에서 하나의 큐로 들어오게되고 
순차적으로 표시가 된다. 
이걸 관리하는 주체가 Android 시스템이다.</p>
<p>그래서 앱이 담당하는게 아니여서 앱이 종료가 되도 Toast는 유지가 될 수 있었던 것이다.</p>
<hr>
<h2 id="🌜-결론">🌜 결론</h2>
<p>내가 흔히 사용하던 Toast조차, 익숙함에 속아 더 깊이 알아보려 하지 않았다는 사실이 부끄럽게 느껴졌다.
Toast는 단순하고 쉬워 보이는 API였지만, 그 내부에는 Window, Looper, Handler 등 복잡한 시스템 레벨의 로직이 촘촘히 엮여 있었다.</p>
<p>이번 정리를 통해 다시 한 번 느낀 것은,
캡슐화는 복잡함을 없앤 것이 아니라, 잘 숨겨두었을 뿐이라는 사실이다.
그리고 그 안을 들여다보려는 노력이 결국 개발자의 깊이를 만든다는 것도.</p>
<p><strong>참조</strong>
<a href="https://velog.io/@mraz3068/Android-Toast-Deep-Dive">https://velog.io/@mraz3068/Android-Toast-Deep-Dive</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Effective Kotlin] 아이팀27 - 변화로부터 코드를 보호하려면 추상화를 사용하라]]></title>
            <link>https://velog.io/@dev_hyunwoo/Effective-Kotlin-%EC%95%84%EC%9D%B4%ED%8C%8027-%EB%B3%80%ED%99%94%EB%A1%9C%EB%B6%80%ED%84%B0-%EC%BD%94%EB%93%9C%EB%A5%BC-%EB%B3%B4%ED%98%B8%ED%95%98%EB%A0%A4%EB%A9%B4-%EC%B6%94%EC%83%81%ED%99%94%EB%A5%BC-%EC%82%AC%EC%9A%A9%ED%95%98%EB%9D%BC</link>
            <guid>https://velog.io/@dev_hyunwoo/Effective-Kotlin-%EC%95%84%EC%9D%B4%ED%8C%8027-%EB%B3%80%ED%99%94%EB%A1%9C%EB%B6%80%ED%84%B0-%EC%BD%94%EB%93%9C%EB%A5%BC-%EB%B3%B4%ED%98%B8%ED%95%98%EB%A0%A4%EB%A9%B4-%EC%B6%94%EC%83%81%ED%99%94%EB%A5%BC-%EC%82%AC%EC%9A%A9%ED%95%98%EB%9D%BC</guid>
            <pubDate>Sun, 18 Jan 2026 06:43:29 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/dev_hyunwoo/post/bde1f070-7afa-4767-a51c-f0b1e42966d4/image.png" alt=""></p>
<p><em>물 위를 걷는 것과 명세서로 소프트웨어를 개발하는 것은 쉽다. 둘 다 동결되어 있다면...</em>
<em>- 에드워드 V.베라드(Edward V. Berard)</em></p>
<p>함수와 클래스 등의 추상화로 실질적인 코드를 숨기면, 사용자가 세부 사항을 알지 못해도 괜찮다는 장점이 있다. 그리고 이후에 실질적인 코드를 원하는대로 수정할 수도 있다. 예를 들면 정렬 알고리즘을 함수로 추출하면, 이를 사용하는 코드에 어떠한 영향도 주지 않고, 함수의 성능을 최적화할 수 있다.</p>
<p>이번 절에서는 추상화를 통해 변화로부터 코드를 보호하는 행위가 어떤 자유를 가져오는지 살펴보자.</p>
<hr>
<h2 id="1️⃣-상수">1️⃣ 상수</h2>
<p>리터럴은 아무것도 설명하지 않는다. 따라서 코드에서 반복적으로 등장할때 문제가 된다. 
이러한 리터럴을 상수 프로퍼티로 변경하면 해당 값에 의미있는 이름을 붙일 수 있다.</p>
<p>비밀번호 유효성 검사하는 간단한 예를 살펴보자</p>
<pre><code class="language-kotlin">fun isPasswordValid(text : String) : Boolean {
    if(text.length &lt; 7) return false
    //...
}</code></pre>
<p>여기서 숫자 7은 아마도 &#39;비밀번호의 최소 길이&#39;를 나타내겠지만, 이해하는 데 시간이 걸린다. 상수로 빼내면 훨씬 쉽게 이해할 수 있을 겁니다. </p>
<pre><code class="language-kotlin">const val MIN_PASSWORD_LENGTH = 7

fun isPasswordValid(text : String) : Boolean {
    if(text.length &lt; MIN_PASSWORD_LENGTH) return false
    //...
}</code></pre>
<p>이렇게 하면 &#39;비밀번호의 최소 길이&#39;를 변경하기도 쉽다. 함수의 내부 로직을 전혀 이해하지 못해도, 상수의 값만 변경하면 된다.</p>
<hr>
<h2 id="2️⃣-함수">2️⃣ 함수</h2>
<p>애플리케이션을 개발하고 있는데, 사용자에게 토스트 메시지를 자주 출력해야하는 상황이 발생했다고 하자. 기본적으로 다음과 같은 코드를 사용해서 토스트 메시지를 출력한다.</p>
<pre><code class="language-kotlin">Toast.makeText(this, message, Toast.LENGTH_LONG).show()</code></pre>
<p>이렇게 많이 사용되는 알고리즘은 간단한 확장 함수로 만들어서 사용할 수 있다.</p>
<pre><code class="language-kotlin">fun Context.toast(
    message: String,
    duration: Int = Toast.LENGTH_LONG
) {
    Toast.makeText(this, messagem duration).show()
}

// 사용
context.toast(message)

// 액티비티 또는 컨텍스트의 서브클래스에서 사용할 경우
toast(message)</code></pre>
<p>이렇게 일반적인 알고리즘을 추출하면, 토스트를 출력하는 코드를 항상 기억해 두지 않아도 괜찮다. 또한 이후에 토스트를 출력하는 방법이 변경되어도, 확장 함수 부분만 수정하면 되므로 유지보수성이 향상된다.</p>
<p>만약 토스트가 아니라 스낵바라는 다른 형태의 방식으로 출력해야 한다면, 다음과 같이 스낵바를 출력하는 확장 함수를 만들고, 기존의 Context.toast()를 Context.snackbar()로 한꺼번에 수정하면 된다.</p>
<pre><code class="language-kotlin">fun Context.snackbar(
    message: String,
    length: Int = Toast.LENGTH_LONG
) {
    //...
}</code></pre>
<p>하지만 이러한 해결 방법은 좋지 않다. 내부적으로만 사용하더라도 함수의 이름을 직접 바꾸는 것은 위험할 수 있기 때문이다. 함수의 이름은 한꺼번에 바꾸기는 쉽지만, 파라미터는 한꺼번에 바꾸기가 어렵기 때문에 Toast.LENGTH_LONG이 계속 사용되는 문제도 있다.</p>
<p>따라서 메시지를 출력하는 더 추상적인 방법이 필요해보인다. 토스트 출력을 토스트라는 개념과 무관한 showMessage라는 높은 레벨의 함수로 옮겨보자</p>
<pre><code class="language-kotlin">fun Context.showMessage(
    message: String,
    duration: MessageLength = MessageLength.LONG
) {
    val toastDuration = when(duration) {
        SHORT -&gt; Length.LENGTH_SHORT
           LONG -&gt; Length.LENGTH_LONG
    }
    Toast.makeText(this, message, toastDuration).show()
}

enum class MessageLength { SHORT, LONG }</code></pre>
<p>가장 큰 변화는 이름이다. 일부 개발자는 이름 변경은 그냥 레이블을 붙이는 방식의 변화이므로, 큰 차이가 없다고 생각하지만 이러한 관점은 사실 컴파일러의 관점에서만 유효하다. 사람의 관점에서는 이름이 바뀌면 큰 변화가 일어난 것이다.</p>
<p>함수는 매우 단순한 추상화지만, 제한이 많다. 예를 들면 함수는 상태를 유지하지 않는다. 또한 함수 시그니처를 변경하면 프로그램 전체에 큰 영향을 줄 수 있다. 구현을 추상화 할 수 있는 더 강력한 방법으로는 클래스가 있다.</p>
<hr>
<h2 id="3️⃣-클래스">3️⃣ 클래스</h2>
<pre><code class="language-kotlin">class MessageDisplay(val context: Context) {
    fun show(
        message: String,
        duration: MessageLength = MessageLength.LONG
    ) {
        val toastDuration = when(duration) {
            SHORT -&gt; Length.SHORT
            LONG -&gt; Length.LONG
        }
        Toast.makeText(context, message, toastDuration).show()
    }

    enum class MessageLength { SHORT, LONG }

    // 사용
    val messageDisplay = MessageDisplay(context)
    messageDisplay.show(&quot;Message&quot;)
}</code></pre>
<p>클래스가 함수보다 더 강력한 이유는 상태를 가질 수 있으며, 많은 함수를 가질 수 있다는 점 때문이다. </p>
<p>의존성 주입 프레임워크를 사용하면, 클래스 생성을 위임할 수도 있다.</p>
<pre><code class="language-kotlin">@Inject lateinit var messageDisplay: MessageDisplay</code></pre>
<p>게다가 메시지를 출력하는 더 다양한 종류의 메서드를 만들 수도 있다.</p>
<pre><code class="language-kotlin">messageDisplay.setChristmasMode(true)</code></pre>
<p>이처럼 클래스는 훨씬 더 많은 자유를 보장해준다. 하지만 여전히 한계가 있다. 예를 들면 클래스가 final이라면, 해당 클래스 타입 아래에 어떤 구현이 있는지 알 수 있다. open 클래스를 활용하면 조금은 더 자유를 얻을 수 있다. open 클래스는 서브클래스를 대신 제공할 수 있기 때문이다. 더 많은 자유를 얻으려면, 더 추상적이게 만들면 된다. 바로 인터페이스 뒤에 클래스를 숨기는 방법이다.</p>
<hr>
<h2 id="4️⃣-인터페이스">4️⃣ 인터페이스</h2>
<p>코틀린 표준 라이브러리를 읽어보면, 거의 모든 것이 인터페이스로 표현된다는 것을 확인할 수 있을 것이다. 예를 들면 </p>
<ul>
<li>listOf 함수는 List를 리턴한다. 여기서 List는 인터페이스이다. listOf는 팩토리 메서드라고 할 수 있다.</li>
<li>컬렉션 처리 함수는 Iterable 또는 Collection의 확장 함수로서, List, Map등을 리턴한다. </li>
</ul>
<p>이것들이 모두 인터페이스 이다.</p>
<p>라이브러리를 만드는 사람은 내부 클래스의 가시성을 제한하고, 인터페이스를 통해 이를 노출하는 코드를 많이 사용한다. 이렇게 하면 사용자가 클래스를 직접 사용하지 못하므로, 라이브러리를 만드는 사람은 인터페이스만 유지한다면, 별도의 걱정 없이 자신이 원하는 형태로 그 구현을 변경할 수 있다. <strong>즉, 인터페이스 뒤에 객체를 숨김으로써 실질적인 구현을 추상화하고, 사용자가 추상화된 것에만 의존하게 만들 수 있다는 것이다. 즉, 결합(coupling)을 줄일 수 있는 것이다.</strong> </p>
<pre><code class="language-kotlin">interface MessageDisplay {
    fun show(
        message: String,
        duration: MessageLength = LONG
    )
}

class ToastDisplay(val context: Context) : MessageDisplay {
    override fun show(
        message: String,
        duration: MessageLength
    ) {
        val toastDuration = when(duration) {
            SHORT -&gt; Length.SHORT
            LONG -&gt; Length.LONG
        }
        Toast.makeText(context, message, tostDuration).show()
    }
}

enum class MessageLength { SHORT, LONG }</code></pre>
<p>이렇게 구성하면 더 많은 자유를 얻을 수 있다. 이러한 클래스는 태블릿에서 토스트를 출력하게 만들 수도 있고, 스마트폰에서도 출력하게 만들 수 있다. 또한 안드로이드, iOS, 웹에서 공유해서 사용하는 공통 모듈에서도 MessageDisplay를 사용할 수 있다. </p>
<p>마지막으로 선언과 사용이 분리되어 있으므로, ToastDisplay 등의 실제 클래스를 자유롭게 변경할 수 있다. 다만 사용 방법을 변경할려면, MessageDisplay 인터페이스를 변경하고, 이를 구현하는 모든 클래스를 변경해야 한다.</p>
<hr>
<h2 id="😅-후기">😅 후기</h2>
<p>추상화는 단순하게 중복성을 제거해서 코드를 구성하기 위한 것은 아닌 것 같다.
추상화는 코드를 변경해야 할 때 도움이 많이 된다고 생각하기 때문에, 
추상화를 사용하는 것은 굉장히 어렵지만, 이를 배우고 이해해야 추상화의 장단점의 균형을 찾을 수 있을 것 같다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Effective Kotlin] 아이템26 - 함수 내부의 추상화 레벨을 통일하라]]></title>
            <link>https://velog.io/@dev_hyunwoo/Effective-Kotlin-%EC%95%84%EC%9D%B4%ED%85%9C26-%ED%95%A8%EC%88%98-%EB%82%B4%EB%B6%80%EC%9D%98-%EC%B6%94%EC%83%81%ED%99%94-%EB%A0%88%EB%B2%A8%EC%9D%84-%ED%86%B5%EC%9D%BC%ED%95%98%EB%9D%BC</link>
            <guid>https://velog.io/@dev_hyunwoo/Effective-Kotlin-%EC%95%84%EC%9D%B4%ED%85%9C26-%ED%95%A8%EC%88%98-%EB%82%B4%EB%B6%80%EC%9D%98-%EC%B6%94%EC%83%81%ED%99%94-%EB%A0%88%EB%B2%A8%EC%9D%84-%ED%86%B5%EC%9D%BC%ED%95%98%EB%9D%BC</guid>
            <pubDate>Sun, 04 Jan 2026 07:58:23 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/dev_hyunwoo/post/b27387d7-d824-4185-ae40-af32a759a694/image.png" alt=""></p>
<h2 id="📚-서론">📚 서론</h2>
<p>컴퓨터는 굉장히 복잡한 장치이다. 이러한 복잡함이 여러 계층에 다양한 요소로서 분할되어 있으므로 쉽게 사용할 수 있는 것이다. 
즉, 개발자 관점에서 컴퓨터에서 가장 낮은 추상화 계층은 하드웨이다.
제어 명령은 0과 1로 이루어지지만 이를 쉽게 읽을 수 있게 일대일로 대응되는 어셈블리라는 언어로 표현한다. 하지만 어셈블리 언어로 프로그래밍하는 것은 굉장히 어렵고, 오늘날 우리가 사용하는 것과 같은 애플리케이션을 만드는 일은 상상도 못한다. 프로그래밍을 간단하게 할 수 있게, 엔지니어는 한 언어를 다른 언어로 변환해주는 프로그램인 컴파일러를 만들었다. 최초의 컴파일 언어는 어셈블리로 만들어졌다. 최초의 언어는 또 다른 고수준에 언어를 만들어냈고, 만들어낸 고수준에 언어는 또 더 높은 고수준에 언어를 만들어냈다. 그렇게 C, C++ 등의 높은 레벨 언어들이 만들어 진 것이다.</p>
<hr>
<h2 id="❕본론">❕본론</h2>
<h3 id="1️⃣-추상화-레벨">1️⃣ 추상화 레벨</h3>
<p>일반적으로 컴퓨터 과학자들은 어떤 계층이 높은 레벨인지 낮은 레벨인지를 구분한다. 높은 레벨로 갈수록 물리 장치로부터 멀어진다. 높은 레벨일수록 걱정해야 하는 세부적인 내용들은 적어진다. 하지만 좋은 것만은 아니다. 왜냐하면 단순함은 얻지만, 제어력은 잃게되기 때문이다. 예를 들어 C 언어는 메모리 관리를 직접 할수 있다. 하지만 자바는 GC가 자동으로 메모리를 관리해주기 때문에 최적화 하는것이 굉장히 힘들다.</p>
<h3 id="2️⃣-추상화-레벨-통일">2️⃣ 추상화 레벨 통일</h3>
<p>함수도 높은 레벨과 낮은 레벨을 구분해서 사용해야 한다는 원칙이 있다. 이를 <strong>추상화 레벨 통일 원칙</strong>이라고 부른다. 예를 들어 버튼 하나만 누르면 커피를 만들 수 있는 커피 머신을 생각해보자</p>
<pre><code class="language-kotlin">class CoffeeMachine {
    fun makeCoffee() {
        // 수백 개의 변
        // 복잡한 로직
        // 낮으 수준의 최적화 잔뜩!
    }
}</code></pre>
<p>이러한 코드는 오래된 프로그램에서 많이 볼 수 있다. 오늘날 우리는 모두 이런 코드의 문제점을 아주 잘 알고있다고 생각한다. 가장 큰 어려움은 유지보수이다. 저 코드에서 물의 온도를 수정해달라는 요청이 들어온다고 생각하면 끔찍하다. 모든 로직을 살펴본 뒤 정확히 그 부분을 수정해야된다. 그래서 최근에는 우리는 함수를 계층처럼 나누어서 사용하고 있다.</p>
<pre><code class="language-kotlin">class CoffeeMachine {
    fun makeCoffee() {
        boilWater()
        brewCoffee()
        pourCoffee()
        pourMilk()
    }
}</code></pre>
<p> 이제 이 함수가 대체 어떤 식으로 동작하는 지 확실하게 알 수 있다.
 누군가는 낮은 레벨을 이해해야 한다며느 해당 부분의 코드만 살펴보면 된다.
 매우 간다한 추상화를 추출해서 가독성을 크게 향상 시킨 것이다.</p>
<p> 이는 <strong>함수는 작아야하며, 최소한의 책임만을 가져야 한다</strong>라는 일반적인 규칙이다. 
 또한 어떤 함수가 다른 함수보다 좀 복잡하다면 일부 부분을 추출해서 추상화하는 것이 좋다.</p>
<hr>
<h2 id="정리">정리</h2>
<p>입출력이 저수준의 모듈이고, 그걸 처리하는 비지니스 로직이 고수준의 모듈이라고 생각해보니깐
UI로직과 비지니스 로직을 얼마나 잘 분리할 수록 좋은 프로젝트 인것 같다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Effective Kotlin] 2장 가독성]]></title>
            <link>https://velog.io/@dev_hyunwoo/Effective-Kotlin-2%EC%9E%A5-%EA%B0%80%EB%8F%85%EC%84%B1</link>
            <guid>https://velog.io/@dev_hyunwoo/Effective-Kotlin-2%EC%9E%A5-%EA%B0%80%EB%8F%85%EC%84%B1</guid>
            <pubDate>Sat, 20 Dec 2025 08:58:55 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/dev_hyunwoo/post/ad408376-7e93-4b84-bc75-f8b70b513b0e/image.png" alt=""></p>
<p>Item 11 ~ Item 18 까지 읽고 정리한 글 이다.
주로 가독성을 다룬 내용이고 코더 보다는 읽는 사람을 기준으로 좋은 코드를 설명하고 있다.</p>
<hr>
<h2 id="1️⃣-가독성을-목표로-설계하라">1️⃣ 가독성을 목표로 설계하라</h2>
<blockquote>
<p>개발자가 코드를 작성하는 데는 1분 걸리지만, 이를 읽는 데는 10분이 걸린다</p>
</blockquote>
<p>로버트 마틴의 <code>클린 코드</code>라는 책에서 나오는 유명한 이야기이다.</p>
<pre><code class="language-kotlin">// 구현 A
if (person != null &amp;&amp; person.isAdult) {
    view.showPerson(person)
} else {
    view.showError()
}

// 구현 B
person?.takeIf { it.isAdult }
    ?.let(view::showPerson)
    ? : view.showError()
</code></pre>
<p>두 코드를 살펴보면 극과 극이다. 난 솔직히 B를 보면 깔끔하군.. 이런 생각을 했었다.
하지만 A코드는 투박하지만 이해하기는 정말 쉬웠다. 
짧다고 B를 좀 더 선호했다면 좋은 대답이 아니다. </p>
<p>참고로 두 코드 같은 결과가 나올 것 같지만 <strong>아니다.</strong></p>
<p>B의 코드에서는 showPerson()이 null을 반환한다면 showError()가 호출될 것이기 때문이다.</p>
<p>B의 코드를 보면 직접 코드를 작성한 사람이 아니라면 이러한 오류는 찾기 힘들 것 이다. 그렇기 때문에 구현 A처럼 가독성을 중요시 하는 게 좋을 것 같다. 
(나부터 노력해야겠다..)</p>
<hr>
<h2 id="2️⃣-unit을-리턴하지-말라">2️⃣ Unit?을 리턴하지 말라</h2>
<p>마치 Boolean이 true 또는 false를 갖는 것처럼 Unit?도 Unit? 또는 null이라는 값을 가질 수 있다. 따라서 Boolean과 Unit?은 서로 바꿔서 사용할 수 있다. </p>
<pre><code class="language-kotlin">fun keyIsCorrect(key : String) : Boolean = //...

if(!keyIsCorrect(key)) return


----

fun verifyKey(key : String) : Unit? = //...
verifyKey(key) ?: return</code></pre>
<p>이러한 코드는 멋있게 보일 수도 있겠지만, 읽을 때는 전혀 그렇지 않다. Unit?으로 불을 표현하는 것은 오해의 소지가 있으며 예측하기 어렵다.</p>
<pre><code class="language-kotlin">getData()?.let{ view.showData(it) } ?: view.showError()</code></pre>
<p>getData()가 null이 아닌 값일 때 showData()가 null이면 showError()도 같이 호출되기 때문에 이런 코드보단 if-else 조건문을 사용하는 것이 훨씬 이해하기 쉽고 깔끔하다.</p>
<hr>
<h2 id="3️⃣변수-타입--리시버를-명시적으로-지정하라">3️⃣변수 타입 &amp; 리시버를 명시적으로 지정하라</h2>
<p>이 부분은 실무에서 정말 공감되는 이야기이다.
대다수의 개발자들이 처음부터 설계하고 작성한 개발자는 거의 없을 것이다.
회사를 옮기다보면 누군가 남겨놓은 레거시 코드를 다뤄야 한다.
이 때 대다수의 레거시 코드는 변수타입과 리비서를 명시적으로 지정하지 않아 읽기가 참 불편했던 경험이 많았다.</p>
<h3 id="변수-타입">변수 타입</h3>
<pre><code class="language-kotlin">
// 보통 실무에선 compose를 다룬다고 가정하면 이렇게 사용하는 게 많다.
@Composable
fun MainScreen() {
    val uiModel by viewModel.uiModel.collectAsStateWithLifecycle()
}

@Composable
fun SecondScreen() {
    val uiModel by viewModel.uiModel.collectAsStateWithLifecycle()
}</code></pre>
<p>다른 화면일지어도 정의하는 방식이 모두 동일하게 uiModel 또는 state 이런식으로 받는 경우가 많다.
그래서 해당 객체가 어떤 타입인지는 당연히  viewModel 들어가서 uiModel의 타입을 확인해야된다. </p>
<h3 id="리시버">리시버</h3>
<p>리시버는 진짜 지정해주지 않으면 내가 작성한 코드지만 며칠뒤에 보면 나도 헷갈린다.
그럼 다른 사람이 본다면 정말 하나도 이해하지 못할 것이며 분석하느라 힘을 다쓸 것이다.</p>
<pre><code class="language-kotlin">// 보통 나는 mapper에서 많이 경험 했던 것 같다.
fun Response.toPerson(
    friends : List&lt;String&gt;
) : Person {
    return Person(
        name = friends.find { it == this.name },
        ...
    )
}
</code></pre>
<p>실무에서 이렇게 간단하게 맵핑하진 않겠지만 정말 간단하게 예시를 들어봐도 
벌써 가독성이 떨어진다. 
it이 무엇일까를 볼려면 수신객체가 무엇인지를 봐야되고 수신객체의 타입을 볼려면 파라미터를 봐야된다. </p>
<pre><code class="language-kotlin">fun Response.toPerson(
    friends: List&lt;String&gt;
): Person {
    val me = this
    return Person(
        name = friends.find { friend -&gt; friend == me.name }
        ...
    )
}</code></pre>
<p>복잡한 코드였다면 리시버가 여러개일 것이다. 그때 리시버를 하나씩 지정해준다면
가독성이 훨씬 좋아질 것이다.</p>
<hr>
<h2 id="👮🏼-2장-가독성을-읽으면서">👮🏼 2장 가독성을 읽으면서..</h2>
<p>한국어를 못하는 사람에게 있어보이는 말을 해봤자 상대방은 하나도 못알아들을 것이다. 상대방이 이해할 수 있게 말하는 게 진짜 멋있는 거고 잘하는 것이라고 생각한다.
코드도 마찬가지인 것 같다. 
코드를 줄인다고 좋은 코드가 아니고, 멋있는 척을 하면 할수록 나는 좋은 코드에서 멀어지고 있었다. 오늘부터라도 이해하기 쉽게  <code>좋은 코드</code>를 작성하도록 노력해야겠다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Effective Kotlin] 아이템3 - 최대한 플랫폼 타입을 사용하지 말라]]></title>
            <link>https://velog.io/@dev_hyunwoo/Effective-Kotlin-%EC%95%84%EC%9D%B4%ED%85%9C3-%EC%B5%9C%EB%8C%80%ED%95%9C-%ED%94%8C%EB%9E%AB%ED%8F%BC-%ED%83%80%EC%9E%85%EC%9D%84-%EC%82%AC%EC%9A%A9%ED%95%98%EC%A7%80-%EB%A7%90%EB%9D%BC</link>
            <guid>https://velog.io/@dev_hyunwoo/Effective-Kotlin-%EC%95%84%EC%9D%B4%ED%85%9C3-%EC%B5%9C%EB%8C%80%ED%95%9C-%ED%94%8C%EB%9E%AB%ED%8F%BC-%ED%83%80%EC%9E%85%EC%9D%84-%EC%82%AC%EC%9A%A9%ED%95%98%EC%A7%80-%EB%A7%90%EB%9D%BC</guid>
            <pubDate>Wed, 19 Nov 2025 09:04:53 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/dev_hyunwoo/post/0df473a4-cdd7-41fc-8e4f-2f55285a5f06/image.png" alt=""></p>
<p>Effective Kotlin을 읽고 정리하는 글이다.</p>
<h3 id="주제--💔-플랫폼-타입을-믿지말자">주제 : 💔 플랫폼 타입을 믿지말자</h3>
<h2 id="오늘의-목차">오늘의 목차</h2>
<h3 id="1플랫폼-타입이란">1.플랫폼 타입이란?</h3>
<h3 id="2-왜-사용하면-안될까">2. 왜 사용하면 안될까?</h3>
<hr>
<h2 id="一-플랫폼-타입이란">一 플랫폼 타입이란?</h2>
<h4 id="플랫폼-타입platform-type이란-다른-프로그래밍-언어에서-전달되어서-nullable인지-아닌지-알-수-없는-타입을-말한다">플랫폼 타입(platform type)이란, 다른 프로그래밍 언어에서 전달되어서 nullable인지 아닌지 알 수 없는 타입을 말한다.</h4>
<p>가끔 어떤 타입인지 볼 때 예를 들어 
String이라고 하면 2가지 정도에 타입으로 나눌 수 있다.
String은 notNull타입으로 볼 수 있고, String?은  nullable타입으로 볼 수 있다.
하지만 그것 이외에 String!을 봤던 적이 있었을 것이다.
이 타입이 플랫폼 타입이다.</p>
<hr>
<h2 id="⼆-왜-사용하면-안될까">⼆ 왜 사용하면 안될까?</h2>
<p>보통은 코틀린에서 자바 함수를 사용 할 때 </p>
<pre><code class="language-java">public class UserRepo {
    public class UserRepo {
        public @NotNull User getUser1() {
            //...
        } 
        public @Nullable User getUser2() {
            //...
        }
    }
}</code></pre>
<h4 id="notnull-또는-nullable이라는-어노테이션을-통해-코틀린에서-사용할-때-nullable타입인지-아닌지를-파악할-수-있다-하지만-이런-어노테이션과-주석-설명이-없으면-사용자-입장에선-어떤타입인지-알기-힘들다">@NotNull 또는 @Nullable이라는 어노테이션을 통해 코틀린에서 사용할 때 nullable타입인지 아닌지를 파악할 수 있다. 하지만 이런 어노테이션과 주석 설명이 없으면 사용자 입장에선 어떤타입인지 알기 힘들다.</h4>
<h4 id="그럴-때-npe를-조심해야된다">그럴 때 NPE를 조심해야된다.</h4>
<pre><code class="language-java">public class JavaClass {
    public String getValue() {
        return null;
    }
}</code></pre>
<pre><code class="language-kotlin">fun statedType() {
    val value : String = JavaClass().value // 여기서 NPE 발생 !!
    //...
    println(value.length)
}


fun platformType() {
    val value = JavaClass().value
    //...
    println(value.length) // 여기서 NPE 발생 !!
}</code></pre>
<p>두가지 모두 NPE가 발생하지만 위치에 차이가 있다.</p>
<p>statedType에서는 자바에서 값을 가져올 때 NPE가 발생한다. 그래서 이 위치에서 오류가 발생하면 nullable 타입이라는 걸 쉽게 알 수 있다. </p>
<p>하지만 platformType에서는 값을 활용할 때 NPE가 발생한다. 예시에서는 간단한 표현식이지만
복잡한 표현식에서는 현재 변수가 nullable인지 파악하기 힘들 것 이다. 현재 변수를 안전하게 한두 번 사용했더라도, 이후에 다른 개발자가 사용할 때 nullable인지 인지를 못하면 또 NPE가 발새될 것이기 때문이다.</p>
<hr>
<h2 id="후기">후기</h2>
<p>플랫폼 타입을 사용하는 코드가 있다면 제거하는게 좋아보인다.
현재에는 인지하더라도 추후 유지보수하는 개발자가 인지하기가 어려워보이기 때문이다.
그리고 자바 코드에 @NotNull, @Nullable 같은 어노테이션을 활용하는 것도 좋아보인다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Effective Kotlin] 아이템1 - 가변성을 제한하라]]></title>
            <link>https://velog.io/@dev_hyunwoo/Effective-Kotlin-%EC%95%84%EC%9D%B4%ED%85%9C1-%EA%B0%80%EB%B3%80%EC%84%B1%EC%9D%84-%EC%A0%9C%ED%95%9C%ED%95%98%EB%9D%BC</link>
            <guid>https://velog.io/@dev_hyunwoo/Effective-Kotlin-%EC%95%84%EC%9D%B4%ED%85%9C1-%EA%B0%80%EB%B3%80%EC%84%B1%EC%9D%84-%EC%A0%9C%ED%95%9C%ED%95%98%EB%9D%BC</guid>
            <pubDate>Sun, 16 Nov 2025 08:19:08 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/dev_hyunwoo/post/aeeb69ef-7efd-4df2-9c48-0665a8d085a6/image.png" alt=""></p>
<p>Effective Kotlin을 읽고 정리하는 글이다.</p>
<h3 id="주제--😇-코틀린에서-가변성을-제한하는-방법이-있다">주제 : 😇 코틀린에서 가변성을 제한하는 방법이 있다.</h3>
<h2 id="오늘의-목차">오늘의 목차</h2>
<h3 id="1-읽기-전용-프로퍼티-val">1. 읽기 전용 프로퍼티 (val)</h3>
<h3 id="2-가변-컬렉션과-읽기-전용-컬렉션-구분하기">2. 가변 컬렉션과 읽기 전용 컬렉션 구분하기</h3>
<h3 id="3-데이터-클래스의-copy">3. 데이터 클래스의 copy</h3>
<hr>
<h2 id="㆒-읽기-전용-프로퍼티">㆒ 읽기 전용 프로퍼티</h2>
<p>코틀린에서는 val를 사용하면 읽기 전용 프로퍼티를 만들수 있다. </p>
<pre><code class="language-kotlin">val a = 10
a = 20 // 오류!</code></pre>
<p>하지만 읽기 전용 프로퍼티가 완전히 변경 불가능한 것은 아니라는 것을 주의해야된다.
읽기 전용 프로퍼티가 mutable 객체를 담고 있다면, 내부적으로 변할 수 있다.</p>
<pre><code class="language-kotlin">val list = mutableListOf(1,2,3)
list.add(4)

print(list) // [1,2,3,4]</code></pre>
<p>또 다른 경우가 있는데 게터를 정의할 때 var 프로퍼티를 사용하면 var 프로퍼티가 변경될 때 변할 수 있다.</p>
<pre><code class="language-kotlin">var name : String = &quot;Woo&quot;
var SurName : String = &quot;Kim&quot;

val fullName 
    get() = &quot;$SurName $name&quot;

println(fullName) // &quot;Kim Woo&quot;
name = &quot;Boo&quot;
println(fullName) // &quot;Kim Boo&quot;</code></pre>
<hr>
<h2 id="二-가변-컬렉션과-읽기-전용-컬렉션-구분하기">二 가변 컬렉션과 읽기 전용 컬렉션 구분하기</h2>
<p>위에 설명과 마찬가지로 코틀린에선 읽고 쓸 수 있는 컬렉션과 읽기 전용 컬렉션이 있다.
<img src="https://velog.velcdn.com/images/dev_hyunwoo/post/5a09c8cf-90c7-4c77-88f3-91a6bd9285ab/image.png" alt="">
코틀린을 공부하다가 이런 이미지는 많이 봐왔었다.</p>
<p>mutable이 붙은 인터페이스는 대응되는 읽기 전용 인터페이스를 상속 받아서, 변경을 위한 메서드를 추가한 것이다. </p>
<pre><code class="language-kotlin">inline fun &lt;T,R&gt; Iterable&lt;T&gt;.map(
    transformation: (T) -&gt; R
) : List&lt;R&gt; {
    val list = ArrayList&lt;R&gt;()
    for (elem in this) {
        list.add(transformaiton(elem))
    }
    return list
}</code></pre>
<p>Iterable.map() 코드를 보면 진짜 불변하게 만들지 않고 읽기 전용으로 설계하였다.
이로 인해서 더 많은 자유를 얻을 수 있다. 
가변으로 동작하여 메모리나 접근 자체에 대한 효율을 높이고
사용시에는 읽기 전용으로 안정성을 높일 수 있기 때문이다.</p>
<p>하지만 다운캐스팅을 위반해서 사용하는 일이 있을 수 있다. </p>
<pre><code class="language-kotlin">val list = listOf(1,2,3)

// 이렇게 사용 금지!!
if(list is MutableList) {
    list.add(4)
}</code></pre>
<h4 id="읽기전용으로-리턴하면-그-객체는-읽기-전용으로만-사용해야-된다">읽기전용으로 리턴하면 그 객체는 읽기 전용으로만 사용해야 된다!</h4>
<hr>
<h2 id="㆔-데이터-클래스의-copy">㆔ 데이터 클래스의 copy</h2>
<p>String이나 Int처럼 내부적인 상태를 변경하지 않는 immutable 객체를 많이 사용하면 여러 장점이 있다.</p>
<h4 id="1️⃣-한-번-정의된-상태가-유지되므로-코드를-이해하기-쉽다">1️⃣ 한 번 정의된 상태가 유지되므로, 코드를 이해하기 쉽다.</h4>
<h4 id="2️⃣-immutable-객체는-공유했을-때도-충돌이-따로-이루어지지-않으므로-병렬-처리를-안전하게-할-수-있다">2️⃣ immutable 객체는 공유했을 때도 충돌이 따로 이루어지지 않으므로, 병렬 처리를 안전하게 할 수 있다.</h4>
<h4 id="3️⃣-immutable-객체에-대한-참조는-변경되지-않으므로-쉽게-캐시할-수-있다">3️⃣ immutable 객체에 대한 참조는 변경되지 않으므로, 쉽게 캐시할 수 있다.</h4>
<h4 id="4️⃣-immutable-객체는-방어적-복사본을-만들-필요가-없다">4️⃣ immutable 객체는 방어적 복사본을 만들 필요가 없다.</h4>
<h4 id="5️⃣-immutable-객체는-다른-객체를-만들-때-활용하기-좋다">5️⃣ immutable 객체는 다른 객체를 만들 때 활용하기 좋다.</h4>
<h4 id="6️⃣-immutable-객체는-set-or-map의-키로-사용할-수-있다">6️⃣ immutable 객체는 set or map의 키로 사용할 수 있다.</h4>
<ul>
<li>mutable객체는 사용 불가능하다. set 과 map이 내부적으로 해시 테이블을 사용하고, 해시 테이블은 처음 요소를 넣을 때 요소의 값을 기반으로 버킷을 결정하기 때문에 수정이 발생하면 해시 테이블 내부에서 요소를 찾지 못한다.</li>
</ul>
<pre><code class="language-kotlin">data class User(
    val name : String,
    val surName : String
)

var user = User(&quot;Woo&quot;, &quot;Kim&quot;)
user = user.copy(surName = &quot;Park&quot;)
print(user) // User(name=woo, surName=Park)</code></pre>
<p>이렇게 data 한정자는 copy라는 메서드를 만들어 주기 때문에 데이터 모델 클래스를 만들어 immutable 객체로 만드는 것이 많은 장점을 가지므로 기본적으로 이렇게 사용하는 것이 좋다.</p>
<hr>
<h2 id="＋추가-변경-가능-지점을-줄여라">＋추가) 변경 가능 지점을 줄여라</h2>
<pre><code class="language-kotlin">val list1 : MutableList&lt;Int&gt; = mutableListOf()
var list2 : List&lt;Int&gt; = listOf()

list1.add(1)
list2 = list2 + 1</code></pre>
<p>list1과 list2의 동작은 모두 정상적이지만 장단점이 있다.
두가지 모두 변경 가능 지점이 있지만 그 위치가 다르다.</p>
<p>list1은 리스트 구현 내부에 변경 가능 지점이 있고 멀티스레드 처리가 이루어질 경우, 내부적으로 적절한 동기화가 되어 있는지 확실하게 알 수 없다.</p>
<p>list2는 프로퍼티 자체가 변경 가능 지점이다. 따라서 멀티스레드 처리에 안정성이 더 좋다고 할 수 있다. </p>
<p>mutable 컬렉션을 사용하는 것이 더 간단하지만, mutable 프러퍼티를 사용하면 객체 변경을 제어하기 더 쉽다.</p>
<h3 id="❌--그래서-최악에-방식은-프로퍼티와-컬렉션-모두-변경-가능한-지점을-만드는-것이다">❌  그래서 최악에 방식은 프로퍼티와 컬렉션 모두 변경 가능한 지점을 만드는 것이다.</h3>
<pre><code class="language-kotlin">// 최악 !!!
var list3 = mutableListOf&lt;Int&gt;()</code></pre>
<h2 id="추가-변경-가능-지점을-노출하지-말기">+추가) 변경 가능 지점을 노출하지 말기</h2>
<pre><code class="language-kotlin">data class User(val name : String)

class UserRepository {
    private val storedUser : MutableMap&lt;Int, String&gt; = 
        mutableMapOf()

    fun loadAll() : MutableMap&lt;Int, String&gt; {
        return storedUsers
    }
}</code></pre>
<p>상태를 나타내는 mutable 객체를 외부에 노출하는 것은 굉장히 위험하다.</p>
<pre><code class="language-kotlin">val userRepository = UserRepository()

val storedUsers = userRepository.loadAll()
storedUsers[4] = &quot;Woo&quot;

print(userRepository.loadAll()) // {4=Woo}</code></pre>
<p>이렇게 loadAll()을 사용해서 private 상태인 UserRepository를 외부에서 변경할 수 있게된다.</p>
<h2 id="이를-방지하는-방법">이를 방지하는 방법</h2>
<h3 id="방어적-복제">방어적 복제</h3>
<pre><code class="language-kotlin">class UserHolder {
    private val user : MutableUser()

    fun get() : MutableUser {
        return user.copy()
    }
}</code></pre>
<h3 id="업캐스트">업캐스트</h3>
<pre><code class="language-kotlin">data class User(val name : String)

class UserRepository {
    private val storedUser : MutableMap&lt;Int, String&gt; = 
        mutableMapOf()

    fun loadAll() : Map&lt;Int, String&gt; {
        return storedUsers
    }
}</code></pre>
<p>loadAll()의 반환값을 MutableMap -&gt; Map으로 업캐스트하여 가변성을 제어할 수 있다.</p>
<hr>
<h2 id="후기">후기</h2>
<p>내가 개발을 하면서 이러한 부분은 고려하지 않은 것 같다.
var보단 val을 선호하며
mutable컬렉션을 선호하기보단 mutable프로퍼티를 선호해야겠다.</p>
<p>끝.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Android]TFT API 이용해서 전적 검색 앱 만들기 - 5 (마무리)]]></title>
            <link>https://velog.io/@dev_hyunwoo/AndroidTFT-API-%EC%9D%B4%EC%9A%A9%ED%95%B4%EC%84%9C-%EC%A0%84%EC%A0%81-%EA%B2%80%EC%83%89-%EC%95%B1-%EB%A7%8C%EB%93%A4%EA%B8%B0-5-%EB%A7%88%EB%AC%B4%EB%A6%AC</link>
            <guid>https://velog.io/@dev_hyunwoo/AndroidTFT-API-%EC%9D%B4%EC%9A%A9%ED%95%B4%EC%84%9C-%EC%A0%84%EC%A0%81-%EA%B2%80%EC%83%89-%EC%95%B1-%EB%A7%8C%EB%93%A4%EA%B8%B0-5-%EB%A7%88%EB%AC%B4%EB%A6%AC</guid>
            <pubDate>Tue, 11 Nov 2025 04:38:08 GMT</pubDate>
            <description><![CDATA[<p>TFTLog앱에 관한 마지막 글이다. 앞으로 기능개발이나 유지보수는 꾸준히 하겠지만 한동한은 플러터로 앱을 만드느라 당분간은 쉽지 않을 것 같다 ㅎㅎ</p>
<p><img src="https://velog.velcdn.com/images/dev_hyunwoo/post/66431f14-cc21-4ba6-8f16-587d3d35ab8b/image.png" alt=""></p>
<hr>
<h2 id="🔍-기능-추가">🔍 기능 추가</h2>
<p>우선 <a href="https://velog.io/@dev_hyunwoo/AndroidTFT-API-%EC%9D%B4%EC%9A%A9%ED%95%B4%EC%84%9C-%EC%A0%84%EC%A0%81-%EA%B2%80%EC%83%89-%EC%95%B1-%EB%A7%8C%EB%93%A4%EA%B8%B0-4">이전 글</a>을 기준으로 배포를 할려고 했으나 다소 완성도가 낮아 기본적으로 내가 쓰면서 불편했던 점을 보완 및 추가하여 배포하기로 하였다.</p>
<h3 id="1-전적-검색-기록-추가">1. 전적 검색 기록 추가</h3>
<p>내가 검색했던 유저 또는 내가 클릭해서 들어간 유저의 프로필을 맨 첫번째 화면(검색 전)에 보여주기로 하였다. 그래서 여기선 Room을 이용하여 저장하고 가져오는 방식으로 정했다. 그리고 해당 유저의 프로필을 클릭하면 전적 검색을 할 수 있도록 구현하였다.</p>
<p><img src="https://velog.velcdn.com/images/dev_hyunwoo/post/67bf3521-abaa-4c80-b8ee-1c92a2855e60/image.gif" alt=""></p>
<h3 id="2-뒤로가기-추가">2. 뒤로가기 추가</h3>
<p>첫번째 화면, 즉 검색 전 화면이 추가됐으므로 사용자 입장에선 2가지 화면이 존재하는 것이다. (물론 개발자에 입장에선 하나의 화면이다.)
<strong>1.검색했던 유저 프로필 히스토리 화면
2.전적검색 화면</strong>
그래서 2번화면에서 1번화면으로 갈 수 있는 방법을 추가해야겠다고 생각했다. 원래 os백키를 생각했겠지만 것보다 더 직관적으로 만들수 있는 방법을 찾다가 FAB로 간단하면서 직관적인 방법을 택했다. </p>
<p><img src="https://velog.velcdn.com/images/dev_hyunwoo/post/83812d89-3b8c-486e-bb88-8db35659e971/image.gif" alt=""></p>
<hr>
<h2 id="🧑💻-배포까지">🧑‍💻 배포까지..</h2>
<p>배포를 많이 해왔던 입장에선 딱히 어려움은 없었다. 
개인정보처리방침 링크를 생성하는 것도, 스크린샷을 만드는 것도 모두 순조롭게 처리해갔다.</p>
<p>TFT로그에는 Riot의 api key가 필요하다. 이 api key가 없으면 말그대로 깡통 같은 앱이다. 근데 나는 서버를 구축하지 않았기에 api key가 서버 드리븐이 아닌 하드코딩으로 넣어버렸다...
여기서 한가지 문제가 생겼다. 
Riot에서는 테스트 api key를 주는데 24시간만 유효한다는 점이다. 그래서 테스트 api key 대신 프로덕트 key를 받아야되는데 Riot에선 앱을 등록하고 그 url을 전달해야만 프로덕트 key를 발급해줬다.... 
근데 플레이스토어에 올리기 위해선 심사를 거쳐야되는데 거의 24시간 이상이다. 그래서 24시간 이내 심사까지 완료되야 배포를 할 수 있다..  결론은 리젝을 당했고 방법을 찾다가
비공개 테스트를 먼저 올리면 나중에 심사할 때 빨라진다는 글을 보고 비공개 테스트부터 릴리즈하고 그 다음 api key를 갈아끼운 후 심사를 넣었더니 거의 12시간만에 심사 통과가 되었다.</p>
<p><img src="https://velog.velcdn.com/images/dev_hyunwoo/post/d422b005-647f-41e0-b4fd-9d24d7e44df3/image.png" alt=""></p>
<h4 id="⭐️드디어-게시완료⭐️">⭐️드디어 게시완료..!!⭐️</h4>
<p><a href="https://play.google.com/store/apps/details?id=com.woo.tft_log">스토어 링크</a> 입니다 많관부!</p>
<p><del>(프로덕트 키 받기전까진 안될거에요.. 거의 일주일 걸린다고 합니다. 현재 날짜 기준 11/11)</del></p>
<hr>
<h2 id="😊-후기">😊 후기</h2>
<p>앱을 처음부터 만들다보니 내가 미흡했던 부분도 많이 배웠던 것 같다.
특히 mvi 아키텍쳐랑 앱 아키텍쳐를 직접 설계하고 써보니 왜 이렇게 사용하는지를 파악할 수 있었고 다른 jetpack 기술들도 많이 접해보니 아직 나는 부족한 것 같다.</p>
<p>더욱더 발전해나아가야겠다는 원동력을 얻은 기회였던 것 같다.</p>
<h3 id="끝">끝</h3>
]]></description>
        </item>
        <item>
            <title><![CDATA[[플러터] Stateless Widget 과 Stateful Widget 이란?]]></title>
            <link>https://velog.io/@dev_hyunwoo/%ED%94%8C%EB%9F%AC%ED%84%B0-Stateless-Widget-%EA%B3%BC-Stateful-Widget-%EC%9D%B4%EB%9E%80</link>
            <guid>https://velog.io/@dev_hyunwoo/%ED%94%8C%EB%9F%AC%ED%84%B0-Stateless-Widget-%EA%B3%BC-Stateful-Widget-%EC%9D%B4%EB%9E%80</guid>
            <pubDate>Sun, 02 Nov 2025 04:47:00 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/dev_hyunwoo/post/6d38b82d-7529-4018-9617-1b3c6661ede2/image.png" alt=""></p>
<p>안드로이드를 네이티브로 3년동안 개발하면서 플러터에 존재는 강하게 느껴왔다.
그래서 일을 하면서도 플러터로 앱을 취미로 개발하기도 하였다. 
근데 정작 개념을 알기보다는 무작정 앱을 만들려고 하다보니 개념쪽으로 더 알아가고 싶다고 느껴서 이번 기회에 플러터에 대해 하나씩 공부해볼려고 한다.</p>
<hr>
<h2 id="👁️-widget이란">👁️ Widget이란?</h2>
<p>플러터에서 Widget은 Jetpack Compose의 @Composable 함수에서 반환되는 UI 단위와 비슷한 개념이다. 플러터에 모든 ui는 Widget으로 이루어져 있다.</p>
<hr>
<h2 id="🤔-stateless-widget이란">🤔 Stateless Widget이란?</h2>
<p>Stateless Widget이란 상태를 가지지 않는 위젯이다.
데이터가 변하지 않고 사용자와의 인터렉션이 필요없는 경우 사용한다.</p>
<p><code>StatelessWidget</code>을 상속받아서 <code>build()</code>라는 메서드를 오버라이드 해야된다.</p>
<p><code>build()</code>에선 화면에 표시될 렌더링을 반환한다.</p>
<hr>
<h2 id="🥹-stateful-widget이란">🥹 Stateful Widget이란?</h2>
<p>Stateful Widget은 반대로 상태를 가지는 위젯이다.
사용자와 인터렉션을 통해 ui가 변경되기도 하며 또 데이터가 변할때도 ui가 변한다.</p>
<p>Stateful Widget을 사용할 때는 우선 <code>StatefulWidget</code>을 상속을 받는다.
그 다음 클래스는 <code>State&lt;T&gt;</code> 상속을 받는다.
첫번째 클래스가 상속받는 <code>StatefulWidget</code>은 <code>State&lt;T&gt;</code> 객체를 생성하는  <code>createState()</code> 메서드를 오버라이드 해야된다.</p>
<p>두번째 클래스에서 상속받는 <code>State&lt;T&gt;</code>에서는 <code>build()</code>라는 메서드를 오버라이드 해야되는데 이때 두번째 클래스는 <code>State&lt;T&gt;</code>을 반환하기 때문에 <code>createState()</code>자리에 들어가서 2개의 클래스가 연결된다. </p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Android]TFT API 이용해서 전적 검색 앱 만들기 - 4]]></title>
            <link>https://velog.io/@dev_hyunwoo/AndroidTFT-API-%EC%9D%B4%EC%9A%A9%ED%95%B4%EC%84%9C-%EC%A0%84%EC%A0%81-%EA%B2%80%EC%83%89-%EC%95%B1-%EB%A7%8C%EB%93%A4%EA%B8%B0-4</link>
            <guid>https://velog.io/@dev_hyunwoo/AndroidTFT-API-%EC%9D%B4%EC%9A%A9%ED%95%B4%EC%84%9C-%EC%A0%84%EC%A0%81-%EA%B2%80%EC%83%89-%EC%95%B1-%EB%A7%8C%EB%93%A4%EA%B8%B0-4</guid>
            <pubDate>Mon, 20 Oct 2025 07:58:17 GMT</pubDate>
            <description><![CDATA[<p><a href="https://velog.io/@dev_hyunwoo/AndroidTFT-API-%EC%9D%B4%EC%9A%A9%ED%95%B4%EC%84%9C-%EC%A0%84%EC%A0%81-%EA%B2%80%EC%83%89-%EC%95%B1-%EB%A7%8C%EB%93%A4%EA%B8%B0-3">이전글</a>
4탄에서 해결하겠다고한 문제점은 해결 못했다. 방법을 모르겠다. 공식 문서에 있는 메타데이터를 이용하여 챔피언 ID값을 매칭시켜서 가져온 것 뿐인데 없는 정보들이 있다. 그건 아마 라이엇에 실수라고 생각할려고 한다.. </p>
<h3 id="이번-글에-목차">이번 글에 목차</h3>
<ul>
<li>JetpackPaging3를 적용</li>
<li>서치바를 Sticky Header에서 반응형으로 변경</li>
<li>유저의 프로필 추가</li>
</ul>
<hr>
<h2 id="📃-jetpack-paging3">📃 Jetpack Paging3</h2>
<h3 id="💉-적용한-계기">💉 적용한 계기</h3>
<p>왜 모든 전적검색 앱들이 웹뷰를 사용하는 지 알 것 같다. 왜냐하면 매치정보를 보여주는 화면에서 보여줘야될게 너무 많았다. 챔피언, 특성, 아이템등등 렌더링 할 게 너무 많으니 앱에서 구동하기엔 너무 벅찼다. 그래서 나는 모든 기능과 화면을 앱으로 만들고 있어서 최대한 렌더링을 줄일 수 있는 방법을 찾았지만 꼭 필요한 기능이라 줄일 수가 없었다. 그래서 초기에 렌더링에 방해되지 않게 최소한에 데이터를 가져오는 게 나의 최선인 것 같았다. 그래서 페이징을 이용하여 최소한에 데이터를 초기에 보여주기 위해 적용했다.</p>
<h3 id="👿-적용">👿 적용</h3>
<p>매치id를 list로 가져오는 api가 있고 그 매치id로 매치정보를 가져오는 api가 별도로 있었다. 그래서 나는 구조적으로 매치id를 가져오는 기능에 페이징을 적용하여 그 값으로 페이징에서 load를 할 때 마다 매치정보를 가져온 후 매핑까지 해줘서 그 값을 페이징 데이터로 Flow형태로 내보내기로 하였다.</p>
<h3 id="matchitempagingsourcekt">MatchItemPagingSource.kt</h3>
<pre><code class="language-kotlin">   override suspend fun load(params: LoadParams&lt;Int&gt;): LoadResult&lt;Int, MatchEntity&gt; {
        return try {
            val currentPage = params.key ?: 1
            val matchIds = tftRepository.getMatchIdsByPuuid(
                start = (currentPage - 1) * PAGE_SIZE, count = PAGE_SIZE, puuid = puuid
            )
            val nextPage = if (matchIds.size != 5) null else currentPage + 1
            val prevPage = if (currentPage == 1) null else currentPage - 1
            val data = matchIds.mapNotNull { matchId -&gt;
                tftRepository.getMatchEntity(puuid = puuid, matchId = matchId)
            }
            LoadResult.Page(
                data = data, nextKey = nextPage, prevKey = prevPage
            )
        } catch (t: Throwable) {
            LoadResult.Error(throwable = t)
        }
    }</code></pre>
<p>load만 가져와봤다.PAGE_SIZE는 5로 설정하였다.  puuid로 매치Id를 리스트로 가져온 후
매치Id로 매치정보를 가져오는 로직이다. getMatchEntity에서는 매치정보를 가져온 후 Entity로 맵핑하여 반환해주는 로직이다.</p>
<h3 id="pagingrepositoryimplkt">PagingRepositoryImpl.kt</h3>
<pre><code class="language-kotlin">override suspend fun getMatchPagingData(puuid: String): Flow&lt;PagingData&lt;MatchEntity&gt;&gt; {
        return Pager(
            config = PagingConfig(
                pageSize = PAGE_SIZE,
                initialLoadSize = PAGE_SIZE,
                prefetchDistance = 1,
                enablePlaceholders = false
            ), pagingSourceFactory = {
                MatchItemPagingSource(
                    tftRepository = tftRepository, puuid = puuid
                )
            }
        ).flow
    }</code></pre>
<p>Pager를 만들어 반환하는 로직이다. 우선 pageSize는 아까 정의한 5를 넣고,
초기 로드하는 크기도 5로 하였다. 어차피 화면에 보이는 아이템은 기기마다 다르겠지만 5를 안넘길 것 같아서 그렇게 설정하였다. prefetchDistance 이 속성은 몇개 남았을 때 다음 페이지를 불러올 것인가를 정의하는 건데 나는 최소한에 페이지를 불러오는 게 목표이기에 최소값인 1를 설정해주었다. </p>
<h3 id="mainviewmodelkt">MainViewModel.kt</h3>
<pre><code class="language-kotlin">    private val _puuid = MutableStateFlow&lt;String?&gt;(null)

    @OptIn(ExperimentalCoroutinesApi::class)
    val matchListFlow: Flow&lt;PagingData&lt;MatchEntity&gt;&gt; = _puuid.filterNotNull().flatMapLatest {
        pagingRepository.getMatchPagingData(it)
    }.cachedIn(viewModelScope)</code></pre>
<p>이제 viewModel에서 소비 할 차례이다. 우선 _puuid가 바뀔 때 마다 불러오도록하였고 마지막에 cachedIn이라는 메서드를 통해 viewModelScope의 생명주기동안에 캐싱할 수 있도록 설정해주었다. 그렇게 해야 화면회전을 해도 재생성되지 않고 유지할 수 있다.</p>
<h3 id="mainscreenkt">MainScreen.kt</h3>
<pre><code class="language-kotlin">     val matchPagingData = viewModel.matchListFlow.collectAsLazyPagingItems()
    val isLoadingAppend = matchPagingData.loadState.append is LoadState.Loading
    val isLoadingRefresh by remember {
        derivedStateOf {
            matchPagingData.loadState.refresh is LoadState.Loading &amp;&amp; state.hasSearch
        }
    }

// MatchItemComponent.kt
fun LazyListScope.matchItemsComponent(
    matchItems: LazyPagingItems&lt;MatchEntity&gt;,
    onClickID: (Participant) -&gt; Unit
) {
    items(
        count = matchItems.itemCount,
        key = matchItems.itemKey { it.gameId },
        contentType = { matchItems[it] }) { index -&gt;
        val matchItem = remember { matchItems[index] }
        matchItem?.let {
            MatchItem(matchItem = matchItem, onClickID = onClickID)
        }
    }
}</code></pre>
<p>페이징 데이터를 LazyPagingItems으로 변환시켜주는 collectAsLazyPagingItems을 사용하여 
LazyColumn안에서 적용하면 된다. pagingData로 초기 로딩과 아이템을 새로 가져올 때를 알 수 있기에 적절하게 로딩을 띄워 줄 수가 있다. items를 이용할 때 아직은 lazyPagingItems를 지원해주는 게 없기 때문에 count를 이용하여 index를 받아와서 직접 가져와야 된다. </p>
<hr>
<h2 id="🔍-searchbar-sticky-header---반응형으로-변경">🔍 SearchBar Sticky Header -&gt; 반응형으로 변경</h2>
<h3 id="🤦🏻♂️-변경한-이유">🤦🏻‍♂️ 변경한 이유</h3>
<p>흠.. 그냥 sticky header를 썼는데 뒤에 남은 필요없는 공간들이 답답하게 느껴졌다. 그래서 공부도 할겸 반응형으로 변경하기로 하였다 </p>
<h3 id="💉-적용">💉 적용</h3>
<p>우선 기존에 Sticky Header를 그냥 item으로 바꿔서 column에 최상단에 위치하도록 하였다.</p>
<pre><code class="language-kotlin">                item {
                    Box(
                        modifier = Modifier
                            .background(color = AppColors.PrimaryColor)
                            .padding(bottom = 10.dp)
                    ) {
                        MainTopbar(
                            onClickSearch = {
                                viewModel.setEvent(
                                    MainContract.Event.OnClickSearch(
                                        it
                                    )
                                )
                            },
                            textFieldState = textFieldState,
                            currentText = currentText
                        )
                    }
                }</code></pre>
<p>그 뒤에 이제 맨 상단에 아이템이 기존에 서치바이기 때문에 나는 이 서치바가 안보이면 반응형 서치바를 보이도록 하고 서치바가 화면에 보이는 순간 반응형 서치바를 숨기도록 할 것이다.
즉, 2개의 서치바가 있다고 생각하면 된다. 그래서 같은 상태를 보여주기위해 기존에 MainTopbar에 있던 로컬 변수들을 모두 상태 호이스팅하여 최상단에서 관리해줬다. 그래서 같은 상태를 유지할 수 있도록 하였다.</p>
<pre><code class="language-kotlin">    val lazyListState = rememberLazyListState()
    val showReactiveBar by remember {
        derivedStateOf { lazyListState.firstVisibleItemIndex &gt; 0 }
    }</code></pre>
<p>lazyListState를 생성한 뒤 최상단에 있는 서치바가 안보이면 showReactiveBar를 true로 하여 반응형 바를 노출시켜줄 것이다.</p>
<pre><code class="language-kotlin">         Box(contentAlignment = Alignment.TopCenter) {
            LazyColumn(
                modifier = Modifier.padding(paddingValues),
                state = lazyListState,
                contentPadding = PaddingValues(vertical = 10.dp)
            ) {
            // ...
            } // LazyColumn
            if (showReactiveBar) {
                MainTopbar(
                    modifier = Modifier.padding(top = 10.dp),
                    onClickSearch = {
                        viewModel.setEvent(
                            MainContract.Event.OnClickSearch(
                                it
                            )
                        )
                    },
                    textFieldState = textFieldState,
                    currentText = currentText
                )
            }
        } // Box
    }
</code></pre>
<p>로직을 LazyColumn밖에 Box 스코프를 넣고 코드 맨 밑에 적용하면 된다.
그럼 최상단 Layer에서 보여주게 된다. 최상단에 item에선 반응형 바가 노출될 때 안보여주도록 구현하지 않은 이유는 LazyColumn에 item이 갑자기 안보여지면 위치가 위로 땡겨지기 때문에 부자연스러운 움직임이 된다. 그래서 기존 서치바는 항상 노출시켜준 상태에서 반응형 서치바만 노출/미노출 시켜주는 게 특징이다.</p>
<p><strong><em>동작 영상은 마지막에 올리도록 하겠다.</em></strong></p>
<hr>
<h2 id="🙋♂️-프로필-적용">🙋‍♂️ 프로필 적용</h2>
<h3 id="💉-적용-1">💉 적용</h3>
<p>프로필이라면 소환사의 아이콘, 레벨, 전적, 아이디, 승률, 티어정도로 생각했다. 그래서 이정보들을 가져올려면 3개의 api들이 필요하다.</p>
<h3 id="1-riotaccountv1accountsby-puuidpuuid">1. /riot/account/v1/accounts/by-puuid/{puuid}</h3>
<p>우선 puuid를 통해 아이디를 가져오는 api이다. 기존엔 아이디랑 태그로 가져오고 있어서 새로 필요했다.
 response는 puuid, gameName, tagline이다. 
 기존에 검색한 값을 쓰면 안되냐?라는 궁금증이 있을 것 같은데 기존에는 소문자 대문자를 구별하지 않았기에 정확한 아이디와 태그값을 가져오기를 원해서 귀찮더라도 api를 통해 가져오고 싶었다.</p>
<h3 id="2-tftleaguev1by-puuidpuuid">2. tft/league/v1/by-puuid/{puuid}</h3>
<p><em>++ 기존에는 BASE_URL이 모두 <code>https://asia.api.riotgames.com/</code>    였다면 
이 api는 <code>https://kr.api.riotgames.com/</code> 로 써야된다. 
그래서 할 수 없이 ApiService를 새로 만들어서 di를 써서 retrofit 객체를 새로 생성해주었다.</em></p>
<p>이 api의 response값은 </p>
<pre><code class="language-kotlin">@Serializable
data class LeagueByPuuidResponse(
    @SerialName(&quot;tier&quot;)
    val tier: Tier,
    @SerialName(&quot;rank&quot;)
    val rank: Rank,
    @SerialName(&quot;leaguePoints&quot;)
    val leaguePoints: Int,
    @SerialName(&quot;wins&quot;)
    val wins: Int,
    @SerialName(&quot;losses&quot;)
    val losses: Int,
)</code></pre>
<p>전적, lp, 티어등에 정보를 가져올 수 있다. 여기서 Tier와 Ranks는 enum class로 따로 정의하여 맵핑시켜주고 있다.</p>
<pre><code class="language-kotlin">@Serializable
enum class Tier(val displayName: String) {
    @SerialName(&quot;IRON&quot;)
    IRON(&quot;아이언&quot;),

    @SerialName(&quot;BRONZE&quot;)
    BRONZE(&quot;브론즈&quot;),

    @SerialName(&quot;SILVER&quot;)
    SILVER(&quot;실버&quot;),

    @SerialName(&quot;GOLD&quot;)
    GOLD(&quot;골드&quot;),

    @SerialName(&quot;PLATINUM&quot;)
    PLATINUM(&quot;플래티넘&quot;),

    @SerialName(&quot;EMERALD&quot;)
    EMERALD(&quot;에메랄드&quot;),

    @SerialName(&quot;DIAMOND&quot;)
    DIAMOND(&quot;다이아몬드&quot;),

    @SerialName(&quot;MASTER&quot;)
    MASTER(&quot;마스터&quot;),

    @SerialName(&quot;GRANDMASTER&quot;)
    GRANDMASTER(&quot;그랜드마스터&quot;),

    @SerialName(&quot;CHALLENGER&quot;)
    CHALLENGER(&quot;챌린저&quot;);
}

@Serializable
enum class Rank(val number: Int) {
    @SerialName(&quot;I&quot;)
    I(1),

    @SerialName(&quot;II&quot;)
    II(2),

    @SerialName(&quot;III&quot;)
    III(3),

    @SerialName(&quot;IV&quot;)
    IV(4)
}</code></pre>
<p>굳이 enum으로 맵핑 안시켜주고 맵퍼에서 entity로 변경할 때 해도 상관 없을 것 같다. 근데 유지보수하기엔 이게 깔끔해보여서 적용해봤다.</p>
<h3 id="3tftsummonerv1summonersby-puuidpuuid">3.tft/summoner/v1/summoners/by-puuid/{puuid}</h3>
<p>_++이 api도 BASE_URL이 <code>https://kr.api.riotgames.com/</code> 로 써야된다. _</p>
<pre><code class="language-kotlin">@Serializable
data class SummonerByPuuidResponse(
    @SerialName(&quot;profileIconId&quot;)
    val profileIconId: Int,
    @SerialName(&quot;summonerLevel&quot;)
    val summonerLevel: Long,
)</code></pre>
<p>이 api는 puuid로 프로필 id와 레벨을 가져올 수 있다.</p>
<p>저 profileIconId를 이용하여 cdn으로 잘 조합해서 아이콘 이미지를 가져올 수있는데 </p>
<pre><code class="language-kotlin">// ImageUtils.kt
    fun createImageUrl(id: String, type: String, version: String): String {
        val regex = Regex(&quot;&quot;&quot;\d+\.\d+&quot;&quot;&quot;)
        val match = regex.find(version)?.value
        val currentVersion = &quot;$match.1&quot;
        val imageName = when (type) {
            ImageType.ITEM.type, ImageType.PROFILE.type -&gt; {
                &quot;$id.png&quot;
            }

            else -&gt; {
                if (id.contains(&quot;png&quot;)) {
                    id
                } else {
                    val season = Regex(&quot;&quot;&quot;TFT(\d+)&quot;&quot;&quot;).find(id.uppercase())?.groupValues?.get(1)
                    &quot;$id.TFT_Set$season.png&quot;
                }
            }
        }
        return &quot;https://ddragon.leagueoflegends.com/cdn/$currentVersion/img/$type/$imageName&quot;
    }</code></pre>
<p>기존에 챔피언과 아이템에 조합처럼 id를 마지막에 넣고 png를 붙여주면 된다. 근데 version에 대한 값은 어디서 가져오는 지 몰라서 무난하게 15.1.1을 넣어주고 있다. </p>
<hr>
<h2 id="🎬-동작-영상">🎬 동작 영상</h2>
<p><img src="https://velog.velcdn.com/images/dev_hyunwoo/post/944c09a9-ae95-44c8-bb5a-731559ebcef7/image.gif" alt=""></p>
<p>아직도 버벅임이 조금 있지만 렌더링할 게 너무 많아서 어쩔 수 없는 것 같다. ㅠㅠ 
좀 더 개선할 방법을 찾아봐야될 것 같다.</p>
<p>아마 5탄은 마지막으로 마무리 및 배포까지 포스팅 할 것 같다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Kotlin In Action] 5장 람다로 프로그래밍 정리]]></title>
            <link>https://velog.io/@dev_hyunwoo/Kotlin-In-Action-5%EC%9E%A5-%EB%9E%8C%EB%8B%A4%EB%A1%9C-%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%B0%8D-%EC%A0%95%EB%A6%AC</link>
            <guid>https://velog.io/@dev_hyunwoo/Kotlin-In-Action-5%EC%9E%A5-%EB%9E%8C%EB%8B%A4%EB%A1%9C-%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%B0%8D-%EC%A0%95%EB%A6%AC</guid>
            <pubDate>Mon, 13 Oct 2025 06:30:22 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/dev_hyunwoo/post/a80defc4-7c03-4ff1-9a9e-f07268890471/image.png" alt=""></p>
<p><code>Kotlin In Action - 5장 람다로 프로그래밍</code>부분을 읽다가 도움이 될만한 것들을 정리하는 글이다.</p>
<hr>
<h3 id="1-멤버참조">1. 멤버참조</h3>
<p>kotlin에서는 람다를 사옹하면 가독성을 향상시키고 보일러플레이트 코드를 줄일 수 있기 때문에 많이 사용한다.</p>
<pre><code class="language-kotlin">val person = listOf(Person(14, 홍길동), Person(24, 둘리), Person(54, 허준)) 
val oldestPerson = person.maxBy { it.age } 



data class Person(val age : Int, val name : String)</code></pre>
<p>많이들 이런식으로 최대값을 찾을 것이다. 또한 멤버참조를 이용하여 많이 넘기기도 한다. </p>
<p>만약 어떤 함수에 람다를 넘겨야 할 때가 생겼는데 이미 정의된 함수라면 다시 정의된 함수에 로직을 다시 쓰는 건 비효율적일 것이다. 이때 멤버참조를 사용하면 된다.</p>
<hr>
<h3 id="2-count-vs-size">2. count vs size</h3>
<p>개발을 하다보면 컬렉션 api를 사용한 후 크기를 알아야 할 때가 많은데 이때 크기만 알면 된다면 앞으로는 size대신 count를 사용하는걸 권장드린다.
그 이유는 filter를 사용하여 남겨진 원소들에 크기를 구할때 만약 size를 이용한다면 filter에 만족하는 컬렉션이 중간에 하나 더 생기게 된다. 반면 count는 조건에 만족하는 원소를 따로 저장하지 않기 때문에 훨씬 더 효율적이다.</p>
<pre><code class="language-kotlin">val size = people.filter { it.age &gt; 27 }.size // x
val count = people.count { it.age &gt; 27 } // o</code></pre>
<hr>
<h3 id="3지연-계산lazy-컬렉션-연산">3.지연 계산(lazy) 컬렉션 연산</h3>
<p>map이나 filter 같은 몇 가지 컬렉션 함수들은 결과 컬렉션을 <strong>즉시</strong> 생성한다. 이는 컬렉션 함수를 연쇄하면 매 단계마다 계산 중간 결과를 새로운 컬렉션에 임시로 담는다는 말이다. 원소가 1,2개 일 때는 상관이 없지만 몇백, 몇천개 일때는 성능차이가 달라진다.</p>
<pre><code class="language-kotlin">val list = listOf(1,2,3,4)

val first = list.map { it * it}.find { it &gt; 3 } // 4 

val firstWithSequence = list.asSequence()
                            .map { it * it}
                            .find { it &gt; 3 } // 4
</code></pre>
<p>이 때 asSequence로 변환하여 컬렉션 api들을 수행한다면 성능이 향상 될 것이다. </p>
<h3 id="왜그럴까">왜그럴까?</h3>
<p>동작차이에 있다.</p>
<p>시퀀스를 사용안하면 1, 4, 9 ,16이라는 컬렉션을 하나 임시로 만들 것이다.
그 후 find로 3이상인 원소를 찾을 것이다. 그럼 9, 16까지 굳이 필요했을까?</p>
<p>아니다. 시퀀스는 그 점을 해결해준다.</p>
<p>시퀀스를 사용하면 map에선 1을 먼저 계산한다. 그 뒤 find가 수행되지만 조건에 맞지않아 반환되지 않는다. 그 다음 map에서는 4가 생성되고 find에서 조건이 맞아 4가 반환된다. 결국 map에서는 나머지 3,4에 대한 작업을 수행하지 않아도 되기 때문에 여기서 성능 이점을 찾을 수 있다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Android]TFT API 이용해서 전적 검색 앱 만들기 - 3]]></title>
            <link>https://velog.io/@dev_hyunwoo/AndroidTFT-API-%EC%9D%B4%EC%9A%A9%ED%95%B4%EC%84%9C-%EC%A0%84%EC%A0%81-%EA%B2%80%EC%83%89-%EC%95%B1-%EB%A7%8C%EB%93%A4%EA%B8%B0-3</link>
            <guid>https://velog.io/@dev_hyunwoo/AndroidTFT-API-%EC%9D%B4%EC%9A%A9%ED%95%B4%EC%84%9C-%EC%A0%84%EC%A0%81-%EA%B2%80%EC%83%89-%EC%95%B1-%EB%A7%8C%EB%93%A4%EA%B8%B0-3</guid>
            <pubDate>Mon, 06 Oct 2025 09:09:05 GMT</pubDate>
            <description><![CDATA[<p><a href="https://velog.io/@dev_hyunwoo/AndroidTFT-API-%EC%9D%B4%EC%9A%A9%ED%95%B4%EC%84%9C-%EC%A0%84%EC%A0%81-%EA%B2%80%EC%83%89-%EC%95%B1-%EB%A7%8C%EB%93%A4%EA%B8%B0-2">이전글</a>에서 말했었던 문제점들을 해결하기위해 꽤 많은 작업이 필요했었다..</p>
<p>우선 가장 큰 문제점은 버전별로 챔피언에 사진이 다르다는 것이다. 예를 들자면 빅토르라는 챔피언은 넷플릭스 시리즈인 아케인이 나온 시점 이후 버전부터는 이미지 이름이 그냥 <code>빅토르</code>가 아닌 <code>진화된 빅토르</code>로 나오기 때문에 사진 cdn링크를 수작업으로 하드코딩하는 건 무리가 있었다. 그리고 추가적으로 cdn에 필요한 정보는 이미지 이름도 있지만 버전도 넣어줘야된다. </p>
<pre><code class="language-kotlin">return &quot;https://ddragon.leagueoflegends.com/cdn/$currentVersion/img/$type/$imageName&quot;</code></pre>
<p>이해하기 쉽게 코드를 보여주자면 이런 느낌이다. 그래서 저 버전에 맞는 imageName을 가져와야된다. </p>
<p>기존에 있던 앱들을 확인해보면 웹으로 만들어서 웹뷰로 보여주는 것 같다. 
그 이유는 로컬에 정보를 저장하여 이미지 링크를 포맷해서 가져오기엔 앱 하나론 꽤 무거운 작업인 것 같았다. 그래도 웹을 할 줄 모르고.. 그렇다고 서버를 두기엔 너무 일이 커질 것 같아서 죽이되든 밥이되든 일단 앱하나로 가기로 결정했다.</p>
<hr>
<h2 id="💿-데이터-로컬-저장">💿 데이터 로컬 저장</h2>
<p>우선 데이터를 서버가 아닌 로컬에 저장해주기로 하였다. 그래서 원래는 스플래시 화면이 없었지만 스플래시가 필요할 것 같아서 추가해주기로 했다. </p>
<p><img src="https://velog.velcdn.com/images/dev_hyunwoo/post/56ed2432-8913-46bb-980b-88d78f1ee3db/image.gif" alt="">
<del>(로고는 내가 gpt 이용해서 만든거다..)</del></p>
<p>그래서 스플래시에서 해주는 건 크게 2가지다.</p>
<h3 id="1️⃣-최신버전-비교">1️⃣ 최신버전 비교</h3>
<pre><code class="language-kotlin">  val localLatestVersion: String = 
  datastoreRepository.getLatestVersion().first().ifEmpty { &quot;0.0.0&quot; }
  val latestVersion: String = result.data.first()
  if (localLatestVersion.compareVersion(latestVersion) &lt; 0) {
      // TODO 버전별로 챔피언 데이터 가져오기
  }</code></pre>
<p>비교하는 부분의 로직을 보여주자면 간단하게 로컬에 저장한 최신버전과 api를 통해 가져온 최신버전이 일치한다면 패스해주고 만약 최신버전이 업데이트가 됐다면 모든 버전의 챔피언 데이터를 가져온다.</p>
<h3 id="2️⃣챔피언-데이터-저장">2️⃣챔피언 데이터 저장</h3>
<pre><code class="language-kotlin">  private suspend fun getChampion(version: String) {
        when (val result = dragonRepository.getChampions(version = version)) {
            is ApiResult.Success -&gt; {
                db.setChampionEntities(championEntities = result.data.toChampionEntity())
            }

            is ApiResult.Error -&gt; {
                setEffect { SplashContract.Effect.ShowMessage(message = result.message) }
            }
        }
    }</code></pre>
<p>우선 버전을 파라미터로 넣어주면 거기에 맞는 챔피언 메타데이터를 가져올 수 있다. </p>
<pre><code class="language-kotlin">fun TFTChampionResponse.toChampionEntity(): List&lt;ChampionEntity&gt; {
    return this.data.map {
        ChampionEntity(
            championId = it.value.id.lowercase(),
            imageName = it.value.image.full
        )
    }
}</code></pre>
<p>결과값을 로컬 db에 저장해주는 로직도 보이는데 그냥 dto의 값을 넣어주기보단 entity로 필요한 부분으로 mapper를 이용하여 맵핑하여 저장해준다. 나는 챔피언 id와 이미지이름만 있으면 돼서 2개로 맵핑을 해줬다. lowercase를 해준 이유는 종종 챔피언 값이 소문자로 줄때가 있어서 로컬에 저장한 값과 매칭이 안되는 이슈가 있었다.</p>
<hr>
<h2 id="🌆-이미지-가져오기">🌆 이미지 가져오기</h2>
<p><img src="https://velog.velcdn.com/images/dev_hyunwoo/post/f16a5cef-8746-494a-812d-4cb72b0028d8/image.png" alt=""></p>
<h3 id="👿구조적-문제점">👿구조적 문제점</h3>
<p>이제 로컬에 저장된 메타데이터를 이용하여 챔피언 id에 맞는 이미지 이름을 가져와야된다. 근데 여기서 많은 고민을 했다. 원래는 viewModel에서 맵핑을 해주고 있었다. 근데 이제 db에 접근하여 이미지를 가져온 뒤 그 이미지에 맞는 값을 맵핑해줘야 된다. 그 작업은 viewModel에서 해주기엔 너무 무거운 작업이였다. 그리고 내가 지금 이용한 앱 아키텍쳐는 domain layer가 없다. 그래서 모든 작업은 data layer에서 해줘야된다. 그래서 어쩔수 없이 구조를 과감히 변경할 필요가 있었다. </p>
<h3 id="data-layer에서는-ui-layer의-의존성이-있으면-안된다">data layer에서는 ui layer의 의존성이 있으면 안된다.</h3>
<p>그래서 기존에 ui Layer에 있던 맵핑 관련 로직들을 모두 data layer로 옮겼다. 예를들면 api 결과값을 가져와서 성공 or 실패로 맵핑해주는 역할을 모두 repository에서 해주다보니 오히려 내가 이전까지 잘못 썼다는 걸 깨달았다. data layer에서 맵핑을 해주는게 엄청 깔끔해보였다. ui layer에서는 단순히 그 가공된 값을 사용하기만 해주는 게 맞았다.</p>
<pre><code class="language-kotlin">// TFTRepositoryImpl.kt
override suspend fun getMatchByMatchId(puuid: String, matchId: String): ApiResult&lt;MatchEntity&gt; {
        return when (val result =
            safeApiCall { riotApiService.getMatchByMatchId(matchId = matchId) }) {
            is ApiResult.Success -&gt; {
                val ids =
                    result.data.info.participants.flatMap { participant -&gt; participant.units.map { unit -&gt; unit.characterId.lowercase() } }
                        .distinct()
                val images = db.getImages(ids)?.associate { it.championId to it.imageName }
                ApiResult.Success(
                    data = result.data.toMatchEntity(
                        puuid = puuid,
                        images = images ?: hashMapOf()
                    )
                )
            }

            is ApiResult.Error -&gt; {
                result
            }
        }
    }</code></pre>
<hr>
<h2 id="😂새로운-문제점">😂새로운 문제점..</h2>
<p><img src="https://velog.velcdn.com/images/dev_hyunwoo/post/fcd223d8-48ca-476c-8a0e-ab84ab52f1ca/image.jpg" alt="">
하드코딩이 아닌 공식사이트에서 제공해주는 데이터를 가져와서 cdn링크를 가져와도 없는 챔피언이 있다.. ㅠㅠ 믿고싶지 않았다. 단순히 검색해서 챔피언 이미지를 가져오는 것도 힘들었다. 오늘따라 백엔드분들이 너무 보고싶어졌다..</p>
<h3 id="해결해서-4탄으로-돌아오겠습니다">해결해서 4탄으로 돌아오겠습니다..</h3>
<hr>
<h2 id="번외로-jetpack-navigation3-후기">번외로 Jetpack Navigation3 후기</h2>
<p>스플래시가 추가된 이후에 스플래시에서 메인으로 이동을 시켜줘야됐다. 근데 이동 후에  메인화면에서 뒤로가기를 눌렀을 때 스플래시 화면이 아닌 앱종료가 됐어야했기에 스플래시 스택을 지워야됐다.</p>
<p>그래서 지우는 방법이 별도로 있나해서 문서를 뒤져봤지만 아직 알파단계라 정보가 거의 없었다. 그래서 그냥 backstack을 스택으로 관리중이니 Splash에 대한 스택을 지우면 되지않을까 했다.</p>
<pre><code class="language-kotlin">   onNavigateToMain = {
                        backStack.add(Route.Main)
                        backStack.remove(Route.Splash)
                    }</code></pre>
<p>이렇게 remove로 Splash를 지워주니 정상 동작하긴 했다. 하지만 맞게 쓰고 있는지는 잘 모르겠다. 나중에 정보가 더 나오면 찾아봐야겠다.</p>
<h2 id="끝">끝!</h2>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Android]TFT API 이용해서 전적 검색 앱 만들기 - 2]]></title>
            <link>https://velog.io/@dev_hyunwoo/AndroidTFT-API-%EC%9D%B4%EC%9A%A9%ED%95%B4%EC%84%9C-%EC%A0%84%EC%A0%81-%EA%B2%80%EC%83%89-%EC%95%B1-%EB%A7%8C%EB%93%A4%EA%B8%B0-2</link>
            <guid>https://velog.io/@dev_hyunwoo/AndroidTFT-API-%EC%9D%B4%EC%9A%A9%ED%95%B4%EC%84%9C-%EC%A0%84%EC%A0%81-%EA%B2%80%EC%83%89-%EC%95%B1-%EB%A7%8C%EB%93%A4%EA%B8%B0-2</guid>
            <pubDate>Tue, 23 Sep 2025 06:07:13 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/dev_hyunwoo/post/ecc62d64-2ad9-4f6f-9464-dd5c8d361c4f/image.png" alt=""></p>
<p>이번에 혼자 개발을 하다보니 팀의 중요성을 점점 깨닫고 있다..</p>
<p>저 구린 디자인을 보면 생각나는 디자이너분들...(나의 최선이다ㅠㅠ)
가공되지않은 데이터를 프론트에서 사용할 수 있게끔 잘 가공해준 백엔드분들..</p>
<p>많은 분들이 떠올라진다.. 혼자 개발을 하다보니 순전히 개발하는 시간보다 문서를 찾고 데이터 가공하는 시간이 더 오래걸리는 것 같다..</p>
<p>아직 갈길이 멀었지만 현재까지 진행상황과 앞으로의 진행예정인 상황들을 간단하게 공유할려고 한다.</p>
<hr>
<h2 id="🧑💻-api-호출">🧑‍💻 API 호출</h2>
<p>기초작업을 끝낸 후 서치바를 생성한 뒤 이제 api호출을 하여 데이터를 가져올 일만 남았다. 근데 문서가 모두 영어로 되어있어서 어디서 어떻게 가져와야되는지 찾기가 어려웠다. 그래서 문서를 막 뒤지다가 </p>
<blockquote>
<p><code>/riot/account/v1/accounts/by-riot-id/{gameName}/{tagLine}</code>
gameName - 롤에서의 닉네임이다. e.g. hideonbush
tagLine - 롤에서의 태그명이다. e.g. KR1</p>
</blockquote>
<p>이 api를 이용하면 해당유저의 uuid를 준다. uuid는 78자의 고유값이며 이걸로 해당 유저의 매치정보를 조회 할 수 있다.</p>
<p>그래서 나의 계획은 매치리스트를 가져와서 해당 매치에 대한 정보를 다시 api를 호출하여 가져온 뒤 보여줄 생각이였다.</p>
<blockquote>
<p><code>tft/match/v1/matches/by-puuid/{puuid}/ids</code></p>
</blockquote>
<p>그래서 우선 매치리스트를 가져오는 api를 사용했다. 해당 결과값은<code>list&lt;String&gt;</code>이다. 안에는 matchId가 들어있고 해당 matchId로</p>
<blockquote>
<p><code>tft/match/v1/matches/{matchId}</code></p>
</blockquote>
<p>이 api를 호출하여 정보를 가져오면 된다.</p>
<hr>
<h2 id="🏙️-이미지">🏙️ 이미지</h2>
<p>데이터는 모두 정상적으로 가져올 수 있었고 문제는 이미지였다.
매치정보에선 이미지 url을 주는 것이 아닌 이미지 id를 줬다.
그 이미지 id를 라이엇이 제공해주는 json파일에서 매칭시켜서 가져와야된다.
이 과정이 난 너무 복잡하다고 느껴서 비정상적인 방법으로 해결하고자 했다.</p>
<pre><code class="language-kotlin">  fun createImageUrl(id: String, type: String, version: String): String {
        val regex = Regex(&quot;&quot;&quot;\d+\.\d+&quot;&quot;&quot;)
        val match = regex.find(version)?.value
        val currentVersion = &quot;$match.1&quot;
        val imageName = when (type) {
            ImageType.CHAMPION.type -&gt; {
                val season = Regex(&quot;&quot;&quot;TFT(\d+)&quot;&quot;&quot;).find(id)?.groupValues?.get(1)
                &quot;$id.TFT_Set$season&quot;
            }

            else -&gt; id
        }
        return &quot;https://ddragon.leagueoflegends.com/cdn/$currentVersion/img/$type/$imageName.png&quot;
    }
}</code></pre>
<p>이 로직을 잠깐 설명하자면 매치정보에서 주는 id값과 version값을 어떻게 파싱하여 url로 만들어줬다. 그래서 성공인 줄? 알았지만...</p>
<p>맨위에 보여지는 화면처럼 변수가 있었다. 몇몇의 챔피언들은 저 형식이 아닌 다른 형식이 있다는 점이였다. 그래서 그 예외되는 챔피언들만 예외처리를 해줄수도 있었지만 앞으로 버전이 늘어나면서 그러한 예외처리를 매번 해줄 수는 없을 것 같아서 방법을 바꾸기로 했다.</p>
<hr>
<h2 id="😵💫-해결방법">😵‍💫 해결방법</h2>
<p>내가 생각한 해결방법은 우선 최신 버전들을 모아논 json파일이 있다.
<code>https://ddragon.leagueoflegends.com/api/versions.json</code>
이 파일을 스플래시에서 호출하여 최신버전이 있는 지 체크한다. (아마 sharedPreference로?)
체크 한 후 없으면 그대로 메인화면 이동하고 만약 있다면 그 버전의 asset.json파일을 호출하여 다운받는다. 다운을 받고 data class로 파싱한 후 room을 이용하여 로컬에 저장하고 이미지를 가져올 때 room에서  json파일 꺼내 이미지 id를 가져올 예정이다. </p>
<p>물론 3탄에서..</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Android]TFT API 이용해서 전적 검색 앱 만들기 - 1]]></title>
            <link>https://velog.io/@dev_hyunwoo/AndroidTFT-API-%EC%9D%B4%EC%9A%A9%ED%95%B4%EC%84%9C-%EC%A0%84%EC%A0%81-%EA%B2%80%EC%83%89-%EC%95%B1-%EB%A7%8C%EB%93%A4%EA%B8%B0-1</link>
            <guid>https://velog.io/@dev_hyunwoo/AndroidTFT-API-%EC%9D%B4%EC%9A%A9%ED%95%B4%EC%84%9C-%EC%A0%84%EC%A0%81-%EA%B2%80%EC%83%89-%EC%95%B1-%EB%A7%8C%EB%93%A4%EA%B8%B0-1</guid>
            <pubDate>Mon, 15 Sep 2025 08:10:24 GMT</pubDate>
            <description><![CDATA[<p>공부 할 겸 TFT API를 이용하여 전적 검색하는 앱을 만들어볼려고 한다.</p>
<p>우선 <a href="https://developer.riotgames.com">https://developer.riotgames.com</a> 에 접속하여 로그인을 프로젝트를 등록해야되는데 앱 이름과 앱 설명을 한 뒤 만들면 되는데 어떤 서비스를 할 것 인지 물어본다. 롤, TFT, 룬테라가 있는데 나는 롤만 해봤고 롤체는 안해본 터라 TFT의 풀네임을 모르고 lol에 포함되어있겠지 해서 리그오브레전드를 골랐는데 알고보니 TFT는 Teamfight Tactics의 약자였던 것이다. 난 처음 들어본 이름이라 몰랐었다.. 그래서 등록했던 프로젝트를 삭제 후 TFT를 제대로 골라서 등록했는데 LOL과 다르게 바로 승인을 안해주고 펜딩 상태가 되었다..
그래서 일단 api먼저 사용하기 전에 초기 설정부터 해주자라고 생각해서 우선 앱을 만들기로 했다.</p>
<hr>
<h2 id="⚙️-초기-설정">⚙️ 초기 설정</h2>
<h3 id="ui---100-compose">UI - 100% Compose</h3>
<p>뭐.. 당연하게 100% Compose를 이용할 생각이다. xml을 굳이 사용할 필요성을 못느꼈기 때문이다.</p>
<h3 id="아키텍쳐---앱-아키텍쳐">아키텍쳐 - 앱 아키텍쳐</h3>
<p>기존에 클린 아키텍쳐를 공부하고 있었는데, 안드로이드에서 직접 소개한 아키텍쳐가 있는데 그게 <a href="https://developer.android.com/topic/architecture?hl=ko">앱 아키텍쳐</a>이다. 클린 아키텍쳐와 다르게 도메인레이어가 옵셔널이라는 점이다. 그래서 내가 만들 앱이 그리 크지 않는 점을 고려해 도메인레이어를 빼고 앱 아키텍쳐를 사용해서 구현할 생각이다.</p>
<h3 id="ui-패턴---mvi">UI 패턴 - MVI</h3>
<p>기존에도 MVI를 사용해봤지만 누군가 이미 만든 구조를 그대로 써왔던터라 깊게 이해하고 쓰진 못했던 것 같아서 이번 기회에 내가 직접 구조를 설계하면서 사용해보고 싶어서 MVI를 사용하기로 했었다.</p>
<h3 id="navigation3">Navigation3</h3>
<p>원래 Jetpack Navigation을 주로 사용해봤다. 그래서 이번에 Alpha 단계인
Navigation3 (<a href="https://developer.android.com/guide/navigation/navigation-3?hl=ko&amp;_gl=1%2A1wxi5mn%2A_up%2AMQ..%2A_ga%2AMTI1MDcxODM2Mi4xNzU3OTE4MTQ4%2A_ga_6HH9YJMN9M%2AczE3NTc5MTgxNDgkbzEkZzAkdDE3NTc5MTgxNDgkajYwJGwwJGg0ODMxMTA3MTk.">개발자 문서</a>)을 사용해볼려고 한다.</p>
<hr>
<h2 id="🧭-navigation3">🧭 Navigation3</h2>
<p>내가 만들 앱이 아직 초기 설정 단계라 화면 이동 및 삭제 그리고 초기 구현로직 정도 이다. 자세한 내용은 개발자 문서를 참고하면 된다.</p>
<h3 id="의존성-추가">의존성 추가</h3>
<p>의존성 추가는 공식문서에 나온대로 모두 적용하면 된다.<img src="https://velog.velcdn.com/images/dev_hyunwoo/post/fed1e474-7699-4a17-8cfd-3ff73e570a76/image.png" alt=""></p>
<pre><code>androidx.compose.material3.adaptive:adaptive-navigation3</code></pre><p>근데 이 의존성을 추가할려면 maven에 추가도 필요해서 추가해주었다.</p>
<pre><code class="language-kotlin">// settings.gradle.kts
pluginManagement {
    repositories {
        google()
        gradlePluginPortal()
        mavenCentral()
        maven {
            // You can find the maven URL for other artifacts (e.g. KMP, METALAVA) on their
            // build pages.
            url = uri(&quot;https://androidx.dev/snapshots/builds/[buildId]/artifacts/repository&quot;)
        }
    }
}

dependencyResolutionManagement {
    repositories {
        google()
        mavenCentral()
        maven {
            url = uri(&quot;https://androidx.dev/snapshots/builds/[buildId]/artifacts/repository&quot;)
        }
    }
}</code></pre>
<p>buildId는 25.09.15 기준 <code>14101258</code>이 최신이여서 이걸로 적용하였다.</p>
<h3 id="이동-및-뒤로가기">이동 및 뒤로가기</h3>
<p>기존 navigation은 backStack을 직접 관리하진 않았다. 그래서 자유롭게 다룬적이 없는 것 같다. 하지만 이번에 Navigation3에서는 backStack을 직접 키로 관리하여 이동 및 뒤로가기를 한다. </p>
<h4 id="1-우선-스냅샵-형태에-리스트를-만들어야된다">1. 우선 스냅샵 형태에 리스트를 만들어야된다.</h4>
<pre><code class="language-kotlin">// Route.kt
sealed interface Route {
    @Serializable
    data object Main : Route

    @Serializable
    data class Second(
        val data: String
    ) : Route
}

// MainActivity.kt#onCreate
// 초기 화면을 Main으로 설정
setContent {
 val backStack = remember { mutableStateListOf&lt;Route&gt;(Route.Main) } 
 ...</code></pre>
<p>우선 시작화면으로 초기값을 설정한 뒤 리스트 형태로 만들어주면 된다.</p>
<pre><code class="language-kotlin">backStack.add(Route.Second(&quot;바보&quot;)) // Second 화면으로 이동
backStack.removeLastOrNull // 뒤로가기 
</code></pre>
<p>이제 이동 및 뒤로가기는 해당 리스트를 이용하면 된다.</p>
<p>아직 제대로 깊게 사용하진 않았지만 직관적이면서 정말 간단한 것 같다. </p>
<h4 id="2-navdisplay">2. NavDisplay</h4>
<p>기존과 다른점은 NavHost말고 NavDisplay를 구현하면 된다.
NavDisplay 역시 DSL로도 지원을 해준다. 그래서 나는 DSL를 사용하여 구현해봤다. 
<em>(DSL 사용 안한 예시코드는 개발자 문서에 있다.)</em></p>
<pre><code class="language-kotlin">// TFTLogNavDisplay.kt
@Composable
fun TFTLogNavDisplay(
    modifier: Modifier = Modifier,
    backStack: List&lt;Route&gt;,
    backStackAdd: (Route) -&gt; Unit,
    backStackRemove: () -&gt; Unit
) {
    NavDisplay(
        backStack = backStack,
        onBack = { backStackRemove() },
        entryProvider = entryProvider {
            entry&lt;Route.Main&gt; {
                MainScreen(
                    modifier = modifier,
                    onNavigate = backStackAdd
                )
            }
            entry&lt;Route.Second&gt; { key -&gt;
                SecondScreen(
                    modifier = modifier,
                    data = key.data
                )
            }
        }
    )
}</code></pre>
<p>나는 backStack과 화면 이동을 최상위인 MainActivity에서 관리하게 하였다.
onBack의 프로퍼티는 뒤로가기 트리거 할 때 람다형태로 호출된다.
entryProvider는 backStack을 NavEntry로 변환해준뒤 현재 스택에 맞는 화면을 보여줄 수 있게 해준다. 나머지는 NavHost랑 비슷하다. data가 있는 경우에는 key를 이용하여 값을 가져올 수 있다.</p>
<hr>
<h2 id="후기">후기</h2>
<p>아직 초기 설정 단계라 깊게 써본건 아니지만 매우 직관적이고 또 쉽게 구현 할 수 있었다. 좀 더 활용할 수 있는 방법을 찾아보고 싶다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Android] popBackStack vs navigateUp]]></title>
            <link>https://velog.io/@dev_hyunwoo/Android-popBackStack-vs-navigateUp</link>
            <guid>https://velog.io/@dev_hyunwoo/Android-popBackStack-vs-navigateUp</guid>
            <pubDate>Fri, 12 Sep 2025 06:08:34 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/dev_hyunwoo/post/35c3bed7-7073-47ef-a1d3-b88e7908fcf4/image.png" alt=""></p>
<p>안드로이드 개발을 하면서 Jetpack Navigation을 이용하여 화면간에 이동을 많이 해보았다. 그러면서 종종 겪는 문제가 있다.</p>
<p><img src="https://velog.velcdn.com/images/dev_hyunwoo/post/988a84d3-3906-4c18-baff-42bb61f32245/image.gif" alt=""></p>
<p>바로 이런 문제일 것이다. A에서 B로 이동 후 B에서 화면을 터치하면 popBackStack을 호출하는 앱이다.
B에서 A로 popBackStack이 호출되면 애니메이션이 발생하면서 최상위 스택이 제거가 되는데 애니메이션이 발생할 때 터치를 여러번하여 popBackStack이 2번 호출되어 NavHost까지 제거되기 때문이다. </p>
<h3 id="popbackstack의-내부로직">popBackStack의 내부로직</h3>
<p><img src="https://velog.velcdn.com/images/dev_hyunwoo/post/7072e1a3-74b3-4caa-8646-a5752b2b550e/image.png" alt="">
<strong><em>(NavController.kt#executePopOperations)</em></strong> 에서 복잡한 내부 코드들 중 일부를 가져왔다. 
 <img src="https://velog.velcdn.com/images/dev_hyunwoo/post/8652b3fa-f1c2-4d59-9c57-4262a4cdf837/image.png" alt=""></p>
<p>여기서 popBackStackInternal이란 확장함수의 내부를 타고 가보면 이런 로직이 있다. </p>
<blockquote>
<ol>
<li>popBackStack()을 실패했을 경우 (현재 백스택이 존재하지 않을 경우)</li>
<li>popUpTo로 설정한 범위까지 순회했을 경우 </li>
</ol>
</blockquote>
<p>이 두경우일 때 위에 콜백함수로 백스택 엔트리에서 제거해준다.</p>
<h3 id="그래서-왜-이런일이-발생할까">그래서 왜 이런일이 발생할까?</h3>
<p><img src="https://velog.velcdn.com/images/dev_hyunwoo/post/ab4c14a7-7626-4f2a-8a20-56a2b3cc8c1c/image.png" alt="">
현재 로직을 보면 backQueue가 비어있지 않을 때 popBackStack을 동작한다.
backQueue가 비어있지 않을 때 동작하는게 맞지 않나? 라고 생각할 수 있지만
backQeueu에선 기본적으로 destination만 포함하고 있는게 아니다.
navGraph도 포함하고 있다. 그래서 destination이 없어도 navGraph만 존재하더라도 동작을 하기 때문에 navHost가 제거 될 수 있다.</p>
<h3 id="navigateup은-다른가">navigateUp은 다른가?</h3>
<p>navigateUp은 내부 구조가 조금 다르다.<img src="https://velog.velcdn.com/images/dev_hyunwoo/post/534f4160-0a6d-4c28-b22f-dd3a05037888/image.png" alt="">
<img src="https://velog.velcdn.com/images/dev_hyunwoo/post/68decb99-0a3a-47b6-b74e-a6333643e96c/image.png" alt=""></p>
<p>destinationCountOnBackStack이라는 변수는 NavGraph가 아닌 진짜 destination으로만 카운트한 값이다.</p>
<p>이 값이 1일때 그니깐 최상단 스택만 남아있을 때는 딥링크로 들어왔는지에 대해 체크하는 로직을 하고 popBackStack() 함수를 호출 하진 않는다. 그래서 navigateUp에선 최상단 화면을 보장받을 수 있다.</p>
<p>그리고 딥링크 관련해서는 다른앱에서 딥링크로 앱을 들어왔을 시 navigateUp을 호출하면 호출했던 앱으로 이동하게 된다.</p>
<hr>
<h3 id="결론">결론</h3>
<p>navigateUp 과 popBackStack은 동작은 비슷해보이지만 가지고 있는 성질은 다른 것 같다.</p>
<p>navigateUp은 사용자의 진입점에 따라 돌아가기 위해 사용하면 될 것 같고
popBackStack은 말그대로 현재 백스택 뿐만 아니라 자기가 원하는 범위만큼 스택을 제거할 때 사용하면 될 것 같다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[kotlin] Flow Flattening에 종류를 알아보자]]></title>
            <link>https://velog.io/@dev_hyunwoo/kotlin-Flow-Flattening%EC%97%90-%EC%A2%85%EB%A5%98%EB%A5%BC-%EC%95%8C%EC%95%84%EB%B3%B4%EC%9E%90</link>
            <guid>https://velog.io/@dev_hyunwoo/kotlin-Flow-Flattening%EC%97%90-%EC%A2%85%EB%A5%98%EB%A5%BC-%EC%95%8C%EC%95%84%EB%B3%B4%EC%9E%90</guid>
            <pubDate>Tue, 09 Sep 2025 09:15:41 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/dev_hyunwoo/post/592eb1a8-7af2-485e-a7f2-4ea7dabc1803/image.png" alt=""></p>
<p>Flow Flattening이란 여러 Flow를 하나의 Flow로 만들어준다.</p>
<p>종류에는 flatMapConcat, flatMapMerge, flatMapLatest가 있다.</p>
<hr>
<h2 id="flatmapconcat">flatMapConcat</h2>
<p>flatMapConcat에 가장 큰 특징은 순서와 데이터를 보장해준다는 점이다.</p>
<pre><code class="language-kotlin">val flowA = flow {
    emit(&quot;A&quot;)
    emit(&quot;B&quot;)
    emit(&quot;C&quot;)
}

val flowB = flow {
    emit(1)
    emit(2)
    emit(3)

}

fun main() {
    runBlocking {
        flowA
            .flatMapConcat{ a -&gt; flowB.map { b -&gt; &quot;$a - $b&quot; } }
            .collect { println(it)}
    }
}

/**
A - 1
A - 2
A - 3
B - 1
B - 2
B - 3
C - 1
C - 2
C - 3
*/</code></pre>
<p>2개의 플로우가 있고 flatMapConcat을 이용하여 flowA,flowB를 하나의 flow로 만든 결과값이다. 순서와 데이터가 모두 나오는 걸 볼 수 있다.
순서는 어떻게 보장될까?<img src="https://velog.velcdn.com/images/dev_hyunwoo/post/68a7045c-c06c-4ecd-a33e-eb89a8bb45fa/image.png" alt="">
내부 코드를 보면 이해할 수 있다. emitAll인 부분을 보면 현재 flow를 모두 방출한 후 다음 Flow로 넘어가는 걸 볼 수 있다. 여기서 순서와 데이터가 보장된다. </p>
<p>하지만 단점이 있다. 각 flow에 딜레이가 있으면 모두 방출을 하는데 시간이 많이 소요된다. 그래서 최신 데이터를 받기엔 어려움이 있다.</p>
<hr>
<h2 id="flatmapmerge">flatMapMerge</h2>
<p>flatMapMerge는 위에 말한 단점을 해결할 수 있다. </p>
<p>이번엔 위와 다르게 flowA엔 딜레이 0.5초를 줬고 flowB엔 1초를 줘봤다.</p>
<pre><code class="language-kotlin">val flowA = flow {
    emit(&quot;A&quot;)
    delay(500)
    emit(&quot;B&quot;)
    delay(500)
    emit(&quot;C&quot;)
}

val flowB = flow {
    emit(1)
    delay(1000)
    emit(2)
    delay(1000)
    emit(3)
}

fun main() {
    runBlocking {
        flowA
            .flatMapMerge { a -&gt; flowB.map { b -&gt; &quot;$a - $b&quot; } }
            .collect { println(it)}
    }
}

/**
A - 1
B - 1
C - 1
A - 2
B - 2
C - 2
A - 3
B - 3
C - 3
*/</code></pre>
<p>아까와 다르게 순서가 보장되지 않는 걸 볼 수 있다. 
한마디로 2개의 플로우가 병렬적으로 수집된다고 보면 된다.</p>
<p>내부코드를 봐보자
<img src="https://velog.velcdn.com/images/dev_hyunwoo/post/a32958ce-f04d-4fd0-9c94-d009ca0d3098/image.png" alt="">
flatMapConcat과 비슷해보이지만 concurrency라는 파라미터가 추가됐다.<img src="https://velog.velcdn.com/images/dev_hyunwoo/post/c2a31331-a396-4f2e-aef0-75732d285fb6/image.png" alt="">
flattenMerge코드를 들어가보면 concurrency가 1일 때는 flattenConcat과 같은 동작을 하는 걸 볼 수 있다. 즉, concurrency가 1일 때는 직렬적으로 동작을 하고 그게 flatMapConcat이라는 것도 알 수 있다.</p>
<p>그럼 1이 아닐때 동작하는 ChannelFlowMerge 부분도 살펴보자</p>
<p><img src="https://velog.velcdn.com/images/dev_hyunwoo/post/e3fd24be-9827-417d-9d52-1b2b35e5016d/image.png" alt="">
<strong>ChannelFlowMerge#collectTo</strong> 핵심 로직만 가져왔다.
해당 로직을 보면 Semaphore에 concurrency값을 넣어 설정해준다.</p>
<p><em>*Semaphore는 동시에 접근할 수 있는 스레드를 제한하는 객체라고 보면 된다.</em></p>
<p>그 뒤 flow만큼 Coroutine을 생성하여 수집하여 채널에 넣어주고 있다.</p>
<pre><code class="language-kotlin">inner = 
  #1 flowB.map { b -&gt; &quot;A - $b&quot; }
  #2 flowB.map { b -&gt; &quot;B - $b&quot; }
  #3 flowB.map { b -&gt; &quot;C - $b&quot; }
</code></pre>
<p>inner는 사실상 저런 값을 가진다고 생각하면 된다. 
저 3개를 collect하는 코루틴을 순차적으로 생성하여 emit을 하고 downstream에선 channel에서 값을 받아 쓴다.</p>
<hr>
<h2 id="flatmaplatest">flatMapLatest</h2>
<p>collect와 collectLatest의 차이점을 알면 flatMapLatest도 쉽게 이해할 수 있을 것이다.</p>
<p>collectLatest도 값을 수집할 때 순서대로 수집하되 수집하는 도중 최신 값이 수집되면 취소하고 최신값을 바로 수집하는 동작 원리랑 똑같다고 보면 된다.
그래서 특징은 순서는 보장되지만 데이터가 보장이 안된다는 점이다.</p>
<pre><code class="language-kotlin">val flowA = flow {
    emit(&quot;A&quot;)
    delay(500)
    emit(&quot;B&quot;)
    delay(500)
    emit(&quot;C&quot;)
}

val flowB = flow {
    emit(1)
    delay(1000)
    emit(2)
    delay(1000)
    emit(3)
}

fun main() {
    runBlocking {
        flowA
            .flatMapLatest { a -&gt; flowB.map { b -&gt; &quot;$a - $b&quot; } }
            .collect { println(it)}

    }
}

/**
A - 1
B - 1
C - 1
C - 2
C - 3
*/</code></pre>
<p>FlowA에서 A의 flow를 방출을 하고 있을 때 B의 flow가 시작되면 A의 플로우는 중지하고 B의 flow가 시작하게 된다. 그래서 A, B모두 1밖에 방출을 못했다.<img src="https://velog.velcdn.com/images/dev_hyunwoo/post/0c4695e7-fddc-4d60-ac28-266663b034d5/image.png" alt="">
<strong><em>ChannelFlowTransformLatest#flowCollect</em></strong>라는 로직을 가져왔는데 이 로직을 보면 이전 flow의 job을 취소하고 최신 flow로 collect를 하는 로직을 볼 수 있다.</p>
<hr>
<h2 id="요약">요약</h2>
<h3 id="flatmapconcat-1">flatMapConcat</h3>
<ol>
<li>동기적으로 flow를 수집할 수 있게 그만큼 수집되는 데 지연될 수 있다. </li>
<li>순서 보장이 된다.</li>
<li>데이터 보장이 된다.</li>
</ol>
<h3 id="flatmapmerge-1">flatMapMerge</h3>
<ol>
<li>각 flow를 병렬적으로 방출해서 오래되는 작업이 있더라도 지연되지 않는다.</li>
<li>그대신 순서 보장이 안된다.</li>
<li>데이터 보장은 된다.</li>
</ol>
<h3 id="flatmaplatest-1">flatMapLatest</h3>
<ol>
<li>flow를 수집하는 도중에 새로운 flow가 방출되면 현재 flow는 취소되고 새로운 flow가 방출된다.</li>
<li>순차적으로 방출은 된다. (순서 보장이라고 하기엔 유실될 수 있어서 애매...)</li>
<li>데이터 보장은 안된다.</li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Android] Retrofit2을 사용할 때 Dispatcher.IO가 필요할까?]]></title>
            <link>https://velog.io/@dev_hyunwoo/Android-Retrofit2%EC%9D%84-%EC%82%AC%EC%9A%A9%ED%95%A0-%EB%95%8C-Dispatcher.IO%EA%B0%80-%ED%95%84%EC%9A%94%ED%95%A0%EA%B9%8C</link>
            <guid>https://velog.io/@dev_hyunwoo/Android-Retrofit2%EC%9D%84-%EC%82%AC%EC%9A%A9%ED%95%A0-%EB%95%8C-Dispatcher.IO%EA%B0%80-%ED%95%84%EC%9A%94%ED%95%A0%EA%B9%8C</guid>
            <pubDate>Mon, 08 Sep 2025 07:01:25 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/dev_hyunwoo/post/756e8bed-3203-488c-b3a3-6b853a86d633/image.png" alt=""></p>
<p>안드로이드 앱 개발을 할 때 retrofit을 많이 쓰시죠.
그때 Coroutine과 같이 사용을 할 때 Dispatcher를 설정해주고 있으신가요?</p>
<p>그렇다면 이제부터라도 안쓰시는 게 낫다.</p>
<hr>
<p>Retrofit에서 interface를 정의하는 방식은 총 3가지가 있다.</p>
<h3 id="1execute">1.execute()</h3>
<p>이 메서드는 동기적으로 요청을 한다.
요청을 한 순간 해당 스레드가 막히고 따로 쓰레드를 지정안해주면 
ANR이 발생할 수 있다.</p>
<h3 id="2enqueue">2.enqueue()</h3>
<p>이 메서드는 서버에 비동기적으로 요청을 하고 콜백을 통해 반환값을 받는다.
콜백을 통해 코드를 작성하면 가독성과 코드 양이 많아지므로 Coroutine과 같이 쓰는 걸 추천한다.</p>
<h3 id="3supend--enqueue">3.supend + enqueue()</h3>
<pre><code class="language-kotlin">// KotlinExtensions.kt
suspend fun &lt;T : Any&gt; Call&lt;T&gt;.await(): T {
  return suspendCancellableCoroutine { continuation -&gt;
    continuation.invokeOnCancellation {
      cancel()
    }
    enqueue(object : Callback&lt;T&gt; {
      override fun onResponse(call: Call&lt;T&gt;, response: Response&lt;T&gt;) {
        if (response.isSuccessful) {
          val body = response.body()
          if (body == null) {
            val invocation = call.request().tag(Invocation::class.java)!!
            val service = invocation.service()
            val method = invocation.method()
            val e = KotlinNullPointerException(
              &quot;Response from ${service.name}.${method.name}&quot; +
                &quot; was null but response body type was declared as non-null&quot;,
            )
            continuation.resumeWithException(e)
          } else {
            continuation.resume(body)
          }
        } else {
          continuation.resumeWithException(HttpException(response))
        }
      }

      override fun onFailure(call: Call&lt;T&gt;, t: Throwable) {
        continuation.resumeWithException(t)
      }
    })
  }
}</code></pre>
<p>위에 코드를 보면 suspendCancellableCoroutine으로 블럭을 만들어서 enqueue를 이용하여 내부적으로 콜백을 구현한 걸 볼 수 있다. 그래서 별도에 
enqueue()를 사용할 필요없이 호출만 해주면 내부적으로 처리해주기 때문에 가독성 측면에서 매우 좋다. 하지만 콜백을 직접 오버라이드 하는 게 아니여서 성공인지 실패인지는 직접 확인하여 처리해줘야된다.</p>
<hr>
<p>왜 안써도 되는 것인지 이제 본격적으로 알아보자.</p>
<h3 id="retrofit-내부코드를-살펴보자">Retrofit 내부코드를 살펴보자</h3>
<p><img src="https://velog.velcdn.com/images/dev_hyunwoo/post/c796bd33-a3ff-463e-afb6-691b9e029145/image.png" alt="">
<img src="https://velog.velcdn.com/images/dev_hyunwoo/post/c1c65647-0b05-4225-b344-474d48f90eb4/image.png" alt=""></p>
<h2 id="callfactory란">CallFactory란</h2>
<p>CallFactory는 네트워크를 담당하는 OkHttpClient의 인터페이스다.</p>
<p><img src="https://velog.velcdn.com/images/dev_hyunwoo/post/5fff1503-706f-46ef-af19-8a82a368b5b3/image.png" alt=""></p>
<p>Dispatcher.kt에 있는 코드이다. 실제로 요청을 할 때 별도의 ThreadPool을 생성하여 동작시키고 있는 걸 확인할 수 있었다.
<img src="https://velog.velcdn.com/images/dev_hyunwoo/post/5f5715f1-4367-4667-ba68-17b07c4060cb/image.png" alt="">
이걸 갑자기 왜 보여주냐면 enqueue을 실행하면 내부적으로 해당 dispatcher를 이용하여 네트워크 요청을 하게된다.!
<img src="https://velog.velcdn.com/images/dev_hyunwoo/post/25adfacd-c37a-4992-b30e-a1dda67562f1/image.png" alt="">
enqueue내부 코드이다. promoteAndExecute()를 살펴보면</p>
<p><img src="https://velog.velcdn.com/images/dev_hyunwoo/post/e4ae4c3c-d2a4-40bc-b3d5-4a4818a35270/image.png" alt="">
요약하자면 대기중인 요청을 검사하여 실행 가능하면 실행 리스트로 넘긴 후
스레드풀에서 동작하게 하는 로직이다. 여기서 executorService라는 스레드풀로 넘기는데 
<img src="https://velog.velcdn.com/images/dev_hyunwoo/post/3d344e43-9a9c-467f-b4e2-0d3c7a5f1eab/image.png" alt="">
이게 바로 아까 봤던 별도로 생성된 스레드풀이다.</p>
<p>그래서 결론적으로는 io로 안해줘도 별도의 스레드풀을 생성하여 동작한다</p>
<h2 id="callbackexecutor란">callbackExecutor란</h2>
<p>네트워크 통신이 끝난 후 콜백을 어느 스레드에서 받을 지 결정한다.</p>
<p><img src="https://velog.velcdn.com/images/dev_hyunwoo/post/65a1a8ae-a21e-4acb-863c-7169a83de6c1/image.png" alt="">
만약 아무 설정을 안해주면 Platform.callbackExecutor로 초기화가 되는데 
<img src="https://velog.velcdn.com/images/dev_hyunwoo/post/da608b05-3b12-4b3d-a2d2-c72fa43803b9/image.png" alt=""></p>
<p>안드로이드 개발이라면 Dalvik으로 가서 AndroidMainExecutor()로 생성이 될 것이다.<img src="https://velog.velcdn.com/images/dev_hyunwoo/post/5678b244-5a49-4b2b-9e3c-4ffbe0dc8af1/image.png" alt="">
마지막으로 AndroidExecutor로 들어가보면 MainLooper에 가져와서 처리해주고 있는 걸 볼 수 있다.</p>
<hr>
<p>이렇게 내부코드를 통해 말로만 듣던 어떻게 io에서 처리가 되고 콜백은 main에서 처리가 되는 지 눈으로 확인해보니깐 깊게 이해한 느낌이 들어 뿌듯하다.</p>
<p>앞으로 뭔가 궁금증이 생길때는 내부코드를 보는 버릇을 들여야 될 것 같다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Android] Channel vs SharedFlow]]></title>
            <link>https://velog.io/@dev_hyunwoo/Android-Channel-vs-SharedFlow</link>
            <guid>https://velog.io/@dev_hyunwoo/Android-Channel-vs-SharedFlow</guid>
            <pubDate>Fri, 05 Sep 2025 05:27:23 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/dev_hyunwoo/post/f8bff30a-8fc0-4726-b475-3019427a5ca6/image.png" alt=""></p>
<p>안드로이드 개발을 할 때 이벤트를 수집하고 소비하는 방식에는 크게 2가지가 있다. Channel과 SharedFlow이다. 이 두가지에 대해 차이점과 특징을 알아볼려고 한다.</p>
<hr>
<h2 id="channel">Channel</h2>
<pre><code class="language-kotlin">// MainViewModel.kt
private val _channel = Channel&lt;Int&gt;()
val channel = _channel.receiveAsFlow()</code></pre>
<p>요렇게 사용된다. channel은 기본적으로 Flow형태가 아님으로 수집을 할때는 Flow형태로 수집할 수 있도록 .receiveAsFlow()를 사용하여 coldFlow로 변형해준다.</p>
<p>우선 특징들을 말해보자면 크게 3가지가 있다. </p>
<ol>
<li>백그라운드에서 수집이 가능하다.</li>
<li>단일 구독자인 경우에 주로 사용된다.</li>
<li>buffer가 꽉차면 수집을 suspend한다.</li>
</ol>
<pre><code class="language-kotlin">// MainViewModel.kt
    private val _channel = Channel&lt;Int&gt;(3)
    val channel = _channel.receiveAsFlow()

    init {
        viewModelScope.launch {
            repeat(100){
                Log.d(&quot;Channel&quot;, &quot;send $it&quot;)
                _channel.send(it)
                delay(1000)
            }
        }
    }

// MainScreen.kt
   val lifecycleOwner = LocalLifecycleOwner.current
    LaunchedEffect(Unit) {
        delay(3000)
        lifecycleOwner.lifecycle.repeatOnLifecycle(state = Lifecycle.State.STARTED) {
            viewModel.channel.collect {
                Log.d(&quot;Channel&quot;, &quot;MainScreen[1]: collect $it&quot;)
            }
        }
    }</code></pre>
<p>이렇게 구성되어 있다고 가정해보자</p>
<p>실행하면 로그는 어떻게 찍히는지도 살펴보자<img src="https://velog.velcdn.com/images/dev_hyunwoo/post/bc2e196a-2dd8-4c8b-8191-4d885caa7b65/image.png" alt=""></p>
<p>우선 첫 실행 했을 때다. 수집한 뒤 onStart가 되자마자 소비하는 걸 볼 수 있다. 그럼 백그라운드에서 동작도 한번 보자 
<img src="https://velog.velcdn.com/images/dev_hyunwoo/post/ecf8aa93-d3ac-41d5-b2c5-d5f6b9828ec6/image.png" alt="">
onStop이 호출된 순간 소비를 멈추고 백그라운드로 진입를 했는데도 Channel에선 수집을 멈추지 않는다. buffer를 총 4개를 담는 걸 볼 수 있다. (아마도 4개인 이유는 디폴트인 0으로 설정하면 한개를 기본적으로 담아두는데 buffer + 1이라고 봐야될 것 같다.) 그리고 난 후 buffer가 꽉차면 수집을 suspend한 뒤  다시 포그라운드로 왔을 때 onStart가 호출되면 한번에 3,4,5,6을 소비하는 것을 볼 수 있다. 소비가 끝나자 다시 수집을 시작한다.</p>
<p>이번엔 구독자가 2명인 경우 어떻게 되나 살펴보자</p>
<p><img src="https://velog.velcdn.com/images/dev_hyunwoo/post/85bacc94-25c2-4fa1-ad81-6d50cc1c78b6/image.png" alt="">
사이좋게 하나씩 소비하는 걸 확인해볼수 있었다. </p>
<p>그럼 만약 백그라운드에서 버퍼를 4개 채우고 다시 포그라운드 왔을 때 2개씩 나눠서 소비를 할까?
<img src="https://velog.velcdn.com/images/dev_hyunwoo/post/89c98e26-51cd-47d9-b0e4-793f4fd39afa/image.png" alt="">
정답은 독식(?)한다!. 그다음 소비할 녀석이 모두 가져가버린다.
그래서 만약 구독자가 2명 이상이라면 이벤트 처리가 매우 어려울 것 같다.</p>
<p>그래서 구독자가 2명이상일 때는 SharedFlow를 쓰는 것 같다.</p>
<hr>
<h2 id="sharedflow">SharedFlow</h2>
<pre><code class="language-kotlin">    // MainViewModel.kt
     private val _sharedFlow = MutableSharedFlow&lt;Int&gt;()
    val sharedFlow = _sharedFlow.asSharedFlow()

    init {
        viewModelScope.launch {
            repeat(100){
                Log.d(&quot;sharedFlow&quot;, &quot;send $it&quot;)
                _sharedFlow.emit(it)
                delay(1000)
            }
        }
    }


   // MainScreen.kt
    LaunchedEffect(Unit) {
        lifecycleOwner.lifecycle.repeatOnLifecycle(state = Lifecycle.State.STARTED) {
            viewModel.sharedFlow.collect {
                Log.d(&quot;sharedFlow&quot;, &quot;MainScreen[1]: collect $it&quot;)
            }
        }
    }</code></pre>
<p>이렇게 구성해보았다.
<img src="https://velog.velcdn.com/images/dev_hyunwoo/post/b3859703-f8da-49a5-b48c-b94f05306711/image.png" alt=""></p>
<p>포그라운드에선 Channel하고 같은 동작을 기대할 수 있을 것이다.</p>
<p>하지만 백그라운드로 가는 순간 완전히 달라진다.
<img src="https://velog.velcdn.com/images/dev_hyunwoo/post/a93292cc-ca45-4972-a9cf-159aacf35090/image.png" alt="">
백그라운드로 가는 순간 수집은 하지만 구독자가 없으므로 그동안 수집했던 데이터가 유실된다. 이게 가장 큰 특징인 것 같다. </p>
<p>그럼 SharedFlow에서 extraBufferCapacity와 replay도 같이 알아보자</p>
<h3 id="extrabuffercapacity">extraBufferCapacity</h3>
<p>구독자가 있을 경우에 소비가 느릴 때는 extraBufferCapacity + 1만큼 버퍼에 쌓아둔다. 수집과 소비를 최적화를 시킬 때 주로 사용한다.</p>
<p>하지만 구독자가 없을 경우에는 적용되지않고 계속 수집하게된다.</p>
<h3 id="replay">replay</h3>
<p>이건 구독할 때 replay에 설정값만큼 소비하게 된다.
<img src="https://velog.velcdn.com/images/dev_hyunwoo/post/3f1a8872-3dfe-4e86-86b5-72e7837a4140/image.png" alt="">
로그를 보면 가장 최신값 3,4,5를 한번에 받을 수 있다.</p>
<h3 id="구독자가-2명이면-어떻게-될까">구독자가 2명이면 어떻게 될까?</h3>
<p><img src="https://velog.velcdn.com/images/dev_hyunwoo/post/f6930cc5-46b7-496b-858c-f9b7d48b9d06/image.png" alt="">
2명의 구독자가 같은 값을 받는 걸 볼 수 있다.</p>
<hr>
<h2 id="결론">결론</h2>
<p>그래서 뭘 쓰라고?</p>
<p>흠,, 나도 최근까지는 SharedFlow로 이벤트 처리를 해왔다. 
하지만 <a href="https://medium.com/prnd/viewmodel%EC%97%90%EC%84%9C-%EB%8D%94%EC%9D%B4%EC%83%81-eventflow%EB%A5%BC-%EC%82%AC%EC%9A%A9%ED%95%98%EC%A7%80-%EB%A7%88%EC%84%B8%EC%9A%94-3974e8ddffed">테드박님에 글을 읽고</a> 생각이 조금 달라졌다.Channel은 단일 구독자라는 단점이 있지만 안드로이드 개발을 하면서 UI에서 이벤트 처리가 굳이 2명 이상의 구독자가 필요할 까라는 생각이 들었다. 그리고 백그라운드에서 event를 사용자에게 안보여줘도 될까? 라는 생각도 들면서 앞으로의 개발을 할때는 Channel을 사용 할 것 같다.</p>
]]></description>
        </item>
    </channel>
</rss>