<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>ryan.log</title>
        <link>https://velog.io/</link>
        <description>Seungjun Gong</description>
        <lastBuildDate>Thu, 07 May 2026 01:44:23 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>ryan.log</title>
            <url>https://velog.velcdn.com/images/last_game/profile/1468d393-2424-4d59-bb6a-c8f824456b74/image.jpg</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. ryan.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/last_game" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[🌀 derivedStateOf 의 Trade-Off]]></title>
            <link>https://velog.io/@last_game/derivedStateOf-%EC%9D%98-Trade-Off</link>
            <guid>https://velog.io/@last_game/derivedStateOf-%EC%9D%98-Trade-Off</guid>
            <pubDate>Thu, 07 May 2026 01:44:23 GMT</pubDate>
            <description><![CDATA[<h2 id="1️⃣-derivedstateof란-무엇인가">1️⃣ derivedStateOf란 무엇인가?</h2>
<h3 id="한-줄-정의">한 줄 정의</h3>
<blockquote>
<p>derivedStateOf는 하나 이상의 Compose State로부터 &quot;파생된 상태&quot;를 만들고,
결과 값이 실제로 바뀌었을 때만 리컴포지션을 유발하는 Effect API</p>
</blockquote>
<pre><code class="language-kotlin">var text by remember { mutableStateOf(&quot;&quot;) }
val filteredText by remember {
    derivedStateOf {
        text.filter { !it.isDigit() }
    }
}</code></pre>
<p>해당 코드를 통해서 <code>text</code> 의 값이 변경될때 내부 함수로 등록된 <code>filter</code> 가 호출되어 실행됩니다.</p>
<p><code>text</code> 가 바뀔 때마다 derivedStateOf 안의 식을 다시 계산하고, 결과가 달라졌을 때만 UI를 다시 그립니다.</p>
<h2 id="2️⃣-왜-derivedstateof가-필요한가">2️⃣ 왜 derivedStateOf가 필요한가?</h2>
<h3 id="compose의-기본-동작">Compose의 기본 동작</h3>
<p>State가 변경되면 해당 State를 읽은 Composable은 <strong>무조건 리컴포지션</strong>됩니다.</p>
<pre><code class="language-kotlin">val text by remember { mutableStateOf(&quot;&quot;) }
val filteredTextby = remember(text) { 
    text.filter { !it.isDigit() }
}</code></pre>
<p><code>text</code>가 바뀔 때마다 <code>filteredText</code>는 재계산되고 이를 사용하는 UI는 다시 그립니다. 문제는 <strong>text가 바뀌어도 필터링 결과가 동일한 경우에도 리컴포지션이 발생</strong>한다는 점입니다.</p>
<p>예를 들어 위 코드를 예시로 텍스트에서 숫자만 필터할때,  <code>&quot;abc&quot;</code> → <code>&quot;abcd&quot;</code> → <code>&quot;abc&quot;</code> 처럼 숫자가 없는 상태가 계속 유지되어도, <strong>text가 바뀌는 매 순간 UI를 다시 그립니다.</strong> 계산 결과가 같으면 실제 UI 변화가 없는데도 다시 그리는 것은 비효율적입니다.</p>
<h2 id="3️⃣-derivedstateof의-핵심-동작-구조">3️⃣ derivedStateOf의 핵심 동작 구조</h2>
<h3 id="내부적으로-무슨-일이-일어나는가">내부적으로 무슨 일이 일어나는가?</h3>
<pre><code class="language-kotlin">@StateFactoryMarker
public fun &lt;T&gt; derivedStateOf(calculation: () -&gt; T): State&lt;T&gt; =
    DerivedSnapshotState(calculation, null)</code></pre>
<p><code>derivedStateOf</code>는 내부적으로 <code>DerivedSnapshotState</code>라는 특수한 State 구현체를 생성합니다.</p>
<p><code>DerivedSnapshotState</code>는 다음과 같은 책임을 가집니다.</p>
<ul>
<li>계산 람다(<code>calculation</code>) 보관</li>
<li>계산 결과 캐싱</li>
<li>계산 중 읽힌 State 자동 추적</li>
<li>Snapshot 변경 시 캐시 유효성 판단</li>
<li>필요할 때만 재계산 수행</li>
</ul>
<p>이 모든 로직은 내부의 <code>ResultRecord</code>를 통해 관리됩니다.</p>
<pre><code class="language-kotlin">class ResultRecord&lt;T&gt;(snapshotId: SnapshotId) : StateRecord(snapshotId), DerivedState.Record&lt;T&gt; {
     override var dependencies: ObjectIntMap&lt;StateObject&gt; = emptyObjectIntMap()
     var result: Any? = Unset
     var resultHash: Int = 0
     ...
}</code></pre>
<p><code>result</code>: 마지막 계산 결과</p>
<p><code>dependencies</code>: 계산 중 읽힌 State 목록</p>
<p><code>resultHash</code>: <code>dependencies</code>들이 현재 snapshot 에서도 같은 상태인지 판단하기 위한 해시</p>
<h3 id="derivedsnapshotstate-구조">DerivedSnapshotState 구조</h3>
<p><img src="https://velog.velcdn.com/images/last_game/post/f216426e-5ff6-40a0-af4c-da15099acc5c/image.png" alt=""></p>
<pre><code class="language-kotlin">fun isValid(derivedState: DerivedState&lt;*&gt;, snapshot: Snapshot): Boolean {
    val snapshotChanged = sync {
        validSnapshotId != snapshot.snapshotId ||
            validSnapshotWriteCount != snapshot.writeCount
    }
    val isValid =
        result !== Unset &amp;&amp;
            (!snapshotChanged || resultHash == readableHash(derivedState, snapshot))

    if (isValid &amp;&amp; snapshotChanged) {
        sync {
            validSnapshotId = snapshot.snapshotId
            validSnapshotWriteCount = snapshot.writeCount
        }
    }

    return isValid
}</code></pre>
<p>해당 코드를 통해 현재 <code>ResultRecord</code> 가 주어진 <code>snapshot</code> 시점에서 최신 상태인지 확인합니다.</p>
<p>해당 부분에 최신 상태가 아니라면 <code>Snapshot.observe()</code> 를 통해서 <code>calculation()</code>을 호출해 새로운 <code>dependencies</code> 와 <code>result</code> 를 설정합니다.</p>
<blockquote>
<p>derivedStateOf는 state 값이 변경될 때마다 대신 “계산 + 비교” 비용을 항상 감수하는 구조입니다.</p>
</blockquote>
<h3 id="⚠️-중요한-성능-관점">⚠️ 중요한 성능 관점</h3>
<blockquote>
<p>원활한 성능을 보장하기 위해</p>
<p><strong>derivedStateOf 안에 아주 무거운 연산식을 넣는 것은 피해야 합니다.</strong></p>
</blockquote>
<blockquote>
<p>연산이 복잡해질수록 <code>derivedStateOf</code>의 “계산 + 비교 비용”이 <strong>리컴포지션으로 UI를 다시 그리는 비용보다 더 커질 수 있습니다.</strong></p>
</blockquote>
<h2 id="4️⃣-그래서-언제-derivedstateof가-효과적인가">4️⃣ 그래서 언제 derivedStateOf가 효과적인가?</h2>
<ul>
<li>When to Use derivedStateOf vs remember
<img src="https://velog.velcdn.com/images/last_game/post/81bff1a3-190e-4bc0-945d-10e83b1a49fd/image.png" alt=""></li>
</ul>
<h3 id="✅-사용하면-좋은-시나리오">✅ 사용하면 좋은 시나리오</h3>
<h3 id="공통-조건">공통 조건</h3>
<ul>
<li>State 가 <strong>자주 변경되지만</strong> 실제 값 변경은 드문 경우</li>
<li><code>derivedStateOf</code> 내부 연산 자체가 가벼운 경우</li>
</ul>
<p><strong>대표 예시:  스크롤 관련 처리</strong> <code>lazyListState</code></p>
<pre><code class="language-kotlin">@Composable
fun ScrollToTopButton(lazyListState: LazyListState, threshold: Int) {
      val isEnabled by remember(threshold) {
          derivedStateOf { lazyListState.firstVisibleItemIndex &gt; threshold }
    }

      Button(onClick = { }, enabled = isEnabled) {
        Text(&quot;Scroll to top&quot;)
      }
}</code></pre>
<p>스크롤 이벤트는 프레임마다 발생하지만, <code>firstVisibleItemIndex &gt; threshold</code>의 결과(Boolean)는 임계값을 넘는 순간에만 바뀝니다. <code>derivedStateOf</code>가 대부분의 프레임에서 리컴포지션을 막아 줍니다.</p>
<p>사용 예시)</p>
<ul>
<li>text 유효성 검증</li>
<li>필터링된 텍스트 표시  (<code>derivedStateOf { text.filter { it.isDigit() } }</code>)</li>
<li>LazyListState 의 스크롤 상태 처리</li>
</ul>
<h2 id="5️⃣-언제-derivedstateof를-쓰면-안-되는가">5️⃣ 언제 derivedStateOf를 쓰면 안 되는가?</h2>
<p>계산을 통한 오버헤드가 recomposition을 통해 UI를 업데이트하는 비용보다 더 크면 성능에 역효과가 날 수 있습니다.</p>
<h3 id="❌-피해야-할-시나리오">❌ 피해야 할 시나리오</h3>
<p><strong>1. 결과가 거의 항상 바뀌는 경우</strong></p>
<pre><code class="language-kotlin">@Composable
fun ScrollProgress(listState: LazyListState) {
    val progress by remember {
        derivedStateOf {
            val total = (listState.layoutInfo.totalItemsCount - 1).coerceAtLeast(1)
            listState.firstVisibleItemIndex / total.toFloat()
        }
    }

    LinearProgressIndicator(progress = progress)
}</code></pre>
<p>해당 예시는 스크롤할 때마다 <code>progress</code> 값이 바뀝니다. 결과가 거의 항상 달라지므로 <code>derivedStateOf</code>의 비교 비용만 추가될 뿐 리컴포지션 스킵 효과는 없습니다. 이런 경우는 단순히 recomposition을 허용하는 편이 낫습니다.</p>
<p><strong>2. 계산 비용이 비싼 경우</strong></p>
<ul>
<li><code>filter/map/groupBy/sortedBy</code></li>
<li>통계 계산(sum/average) + 중간 컬렉션 생성</li>
</ul>
<pre><code class="language-kotlin">@Composable
fun GroupedSectionList(viewModel: MyViewModel) {
    val items by viewModel.items.collectAsState()     
    val query by viewModel.query.collectAsState()      

    val grouped by remember {                          
        derivedStateOf {
            items                                      
                .asSequence()
                .filter { it.name.contains(query, ignoreCase = true) }
                .groupBy { it.category }
                .mapValues { (_, v) -&gt; v.sortedBy { it.date } }
                .toMap()
        }
    }
}</code></pre>
<p>이경우 <code>items</code>나 <code>query</code>가 바뀔 때마다 filter/groupBy/sortedBy 전체가 재실행됩니다. 그냥 리컴포지션을 하는 경우 보다 더 해당 식을 실행하고 <strong>비교하는 연산</strong>이 더 비싸질 수 있습니다.</p>
<p><strong>3. 단순 계산</strong></p>
<p>아래 같은 연산은 비용이 사실상 0에 가깝고, 리컴포지션 비용도 대부분 매우 낮습니다.</p>
<pre><code class="language-kotlin">derivedStateOf { list.isEmpty() }</code></pre>
<p>오히려 코드 복잡도를 증가 시키고 마찬가지로 추가적인 오버헤드를 발생시킬 수 있습니다.</p>
<h2 id="6️⃣-그럼-이런-경우엔-어떻게-해야-하나">6️⃣ 그럼 이런 경우엔 어떻게 해야 하나?</h2>
<p>derivedStateOf 안에서 정렬/그룹핑 같은 무거운 계산을 수행하면, UI 레이어에서 불필요한 재계산이 반복될 수 있습니다.
이럴 때는 <strong>계산을 Compose 밖으로 옮기고, Compose는 결과만 그리도록</strong> 만드는 방식이 가장 안전합니다.</p>
<ul>
<li>ViewModel에서 미리 계산해 <code>UiState</code>로 내려주기</li>
<li><code>Flow.map {}</code> / <code>StateFlow</code>로 데이터 변환 구성</li>
<li>필요한 경우 <code>Dispatchers.Default</code> 등 백그라운드 스레드에서 처리</li>
<li>UI는 “가공된 결과”만 받아 그림</li>
</ul>
<pre><code class="language-kotlin">class MyViewModel(
    private val repository: Repository
) : ViewModel() {

    val uiState: StateFlow&lt;UiState&gt; =
        repository.itemsFlow
            .map { items -&gt;
                UiState(
                    grouped = items.groupBy { it.category }
                )
            }
            .stateIn(
                scope = viewModelScope,
                started = SharingStarted.WhileSubscribed(5_000),
                initialValue = UiState()
            )
}

data class UiState(
    val grouped: Map&lt;Category, List&lt;Item&gt;&gt; = emptyMap()
)</code></pre>
<h2 id="7️⃣-실전-질문-정리">7️⃣ 실전 질문 정리</h2>
<h3 id="q-어떤-시나리오에서-derivedstateof가-효과적인가요">Q) 어떤 시나리오에서 derivedStateOf가 효과적인가요?</h3>
<blockquote>
<p>여러 State로부터 파생된 값이</p>
<p><strong>자주 읽히지만 실제 변경은 드문 경우</strong>에 효과적입니다.</p>
<p>특히 스크롤 상태로부터 Boolean 값을 파생하는 경우,</p>
<p>불필요한 recomposition을 줄이는 데 큰 도움이 됩니다.</p>
</blockquote>
<h3 id="q-언제-사용하지-말아야-하나요">Q) 언제 사용하지 말아야 하나요?</h3>
<blockquote>
<p><code>derivedStateOf</code> 안에 무거운 연산을 넣으면,</p>
<p>의존 State가 바뀔 때마다 재계산 비용이 누적되어 오히려 성능에 악영향을 줄 수 있습니다. </p>
<p>이런 경우 ViewModel이나 백그라운드 처리로 분리하는 것이 적절합니다.</p>
</blockquote>
<h3 id="q-remember-없이-derivedstateof만-쓰면-안-되나요">Q) remember 없이 derivedStateOf만 쓰면 안 되나요?</h3>
<blockquote>
<p>안 됩니다. <code>remember</code>가 없으면 리컴포지션마다 <code>DerivedSnapshotState</code> 객체가 새로 생성되어 캐시가 사라집니다.</p>
<p>이전 결과와 비교 자체가 불가능해지므로 <code>derivedStateOf</code>의 핵심 기능이 동작하지 않습니다. </p>
<p><strong>remember는 객체를 살려두는 역할, derivedStateOf는 그 객체가 살아있는 동안 비교하는 역할</strong>로 둘은 반드시 함께 사용해야 합니다.</p>
</blockquote>
<h2 id="📕-참고-자료">📕 참고 자료</h2>
<p>다음의 링크를 참고했습니다.</p>
<ul>
<li><a href="https://developer.android.com/develop/ui/compose/side-effects#derivedstateof">https://developer.android.com/develop/ui/compose/side-effects#derivedstateof</a></li>
<li><a href="https://haeti.palms.blog/compose-snapshot-system">https://haeti.palms.blog/compose-snapshot-system</a></li>
<li><a href="https://androidengineers.substack.com/p/deep-dive-into-derivedstateof-in">https://androidengineers.substack.com/p/deep-dive-into-derivedstateof-in</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Android 네트워크 끊김 감지 - ConnectivityManager]]></title>
            <link>https://velog.io/@last_game/Android-%EB%84%A4%ED%8A%B8%EC%9B%8C%ED%81%AC-%EB%81%8A%EA%B9%80-%EA%B0%90%EC%A7%80-ConnectivityManager</link>
            <guid>https://velog.io/@last_game/Android-%EB%84%A4%ED%8A%B8%EC%9B%8C%ED%81%AC-%EB%81%8A%EA%B9%80-%EA%B0%90%EC%A7%80-ConnectivityManager</guid>
            <pubDate>Thu, 30 Apr 2026 09:44:08 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>이 글은 <a href="https://github.com/android/nowinandroid">Now In Android</a> 프로젝트의 <code>ConnectivityManagerNetworkMonitor</code> 구현을 기반으로, 네트워크 모니터링의 필요성부터 실제 구현까지를 다룹니다.</p>
</blockquote>
<p>만약 앱에서 네트워크가 끊기게 된다면 어떻게 될까요? 기본적으로 서버와 통신을 주고 받는 앱이라면 사용자는 빈 화면을 보거나, 로딩이 끝나지 않거나, 알 수 없는 에러를 마주하게 됩니다. </p>
<p>그래서 네트워크 연결이 불안정한 경우에는 그 사실을 사용자에게 알려주는 UI가 필요합니다. 문제는, 그걸 <strong>어떤 방식으로</strong> 감지하고 보여줄 것이냐예요.</p>
<h2 id="네트워크-끊김-처리-방식">네트워크 끊김 처리 방식</h2>
<h3 id="1-서버-요청-실패-시-에러-ui-띄우기">1. 서버 요청 실패 시 에러 UI 띄우기</h3>
<p>가장 직관적인 방법이에요. API 요청이 실패하면 에러 다이얼로그나 스낵바를 띄웁니다. 구현이 단순하고, 네트워크 문제뿐 아니라 서버 장애,  타임아웃 등 모든 통신 실패를 한 번에 처리할 수 있다는 장점이 있어요.</p>
<p>단순 조회성 API 위주의 앱이라면 이것만으로도 충분해요.</p>
<h3 id="2-네트워크-연결-상태를-실시간으로-감지하기">2. 네트워크 연결 상태를 실시간으로 감지하기</h3>
<p><code>ConnectivityManager</code>를 사용하면 OS 수준에서 네트워크 연결 상태를 실시간으로 감지할 수 있어요. 네트워크가 연결되었는지, 끊겼는지를 Boolean 값으로 받아 앱 전반의 동작을 제어할 수 있습니다.</p>
<h3 id="그럼-언제-connectivitymanager가-더-적절할까">그럼 언제 ConnectivityManager가 더 적절할까?</h3>
<p>에러 다이얼로그 방식은 간단하지만, 다음과 같은 상황에서는 한계가 있어요.</p>
<p><strong>실시간 통신이 필요한 경우:</strong> 채팅, 라이브 스트리밍처럼 연결이 지속되어야 하는 앱에서는 네트워크가 끊겼는데도 불필요한 요청을 계속 보낼 수 있어요. 연결 상태를 미리 파악하면 이런 낭비를 막을 수 있습니다.</p>
<p><strong>오프라인 우선 아키텍처를 적용한 경우:</strong>  기본적으로 로컬 데이터를 기반으로 동작하되, 네트워크가 연결된 시점에만 서버 요청을 보내서 데이터를 동기화하는 구조예요. 연결 상태를 알 수 없으면 오프라인인데도 불필요한 요청을 시도하게 됩니다.</p>
<p>정리하면, ConnectivityManager는 <strong>네트워크 상태 변화에 따라 앱의 연결, 요청, UI 상태를 사전에 제어하기 위한 도구</strong>예요. 불필요한 네트워크 요청을 방지해 리소스 낭비와 배터리 소모를 줄이고, 오프라인 우선 아키텍처에서 적절한 시점에만 서버 요청을 보낼 수 있게 해줍니다.</p>
<h3 id="다만-만능은-아니에요">다만, 만능은 아니에요</h3>
<p>ConnectivityManager가 &quot;네트워크에 연결되어 있다&quot;고 알려줘도, 실제 서버 통신이 가능하다는 보장은 없어요. 서버 장애, DNS 문제, 인증 만료, 타임아웃 등 네트워크 연결과는 별개로 요청이 실패할 수 있는 이유는 얼마든지 있습니다.</p>
<p><strong>ConnectivityManager는 서버 요청 에러 처리를 대체하는 게 아니라, 네트워크 상태에 따라 불필요한 요청을 줄이고 UI를 사전에 제어하기 위한 보조 수단이에요.</strong> 결국 두 방식은 역할이 다른 것이고, 앱의 요구사항에 따라 함께 사용하는 것이 가장 적절합니다.</p>
<h2 id="connectivitymanager">ConnectivityManager</h2>
<p>ConnectivityManager를 제대로 사용하려면 함께 동작하는 4개의 클래스를 이해해야 해요.</p>
<p><strong>ConnectivityManager:</strong> 시스템의 연결 상태를 앱에 알리는 컨트롤 타워예요. 네트워크 콜백을 등록하고, 현재 네트워크 정보를 조회하는 진입점 역할을 합니다.</p>
<p><strong>Network:</strong> 기기가 연결된 특정 네트워크를 나타내는 객체예요. Wi-Fi 하나, 모바일 데이터 하나가 각각 별도의 Network 객체가 됩니다. 네트워크가 끊기면 이 객체도 함께 소멸해요.</p>
<p><strong>LinkProperties:</strong> DNS 서버, 로컬 IP 주소, 네트워크 경로 등 해당 네트워크의 상세 연결 정보를 담고 있어요. 다만 VPN 앱이나 네트워크 진단 도구 같은 특수한 경우가 아니라면 직접 다룰 일은 거의 없습니다.</p>
<p><strong>NetworkCapabilities:</strong> 해당 네트워크가 &quot;뭘 할 수 있는지&quot;를 알려주는 객체예요. 크게 두 가지를 확인할 수 있습니다. 하나는 <strong>전송 방식</strong>(Wi-Fi인지, 셀룰러인지, 이더넷인지)이고, 다른 하나는 <strong>네트워크 속성</strong>(인터넷 접근이 가능한지, 실제로 검증되었는지, 데이터 무제한인지)이에요.</p>
<h3 id="네트워크-상태를-읽는-두-가지-방식-스냅샷-vs-콜백">네트워크 상태를 읽는 두 가지 방식: 스냅샷 vs 콜백</h3>
<h4 id="순간적인-상태-가져오기-snapshot">순간적인 상태 가져오기 (Snapshot)</h4>
<p><code>activeNetwork</code>를 통해 현재 기본 네트워크 정보를 즉시 가져올 수 있어요. 디버깅이나 일시적인 확인에는 유용하지만, <strong>호출 이후의 변화는 알 수 없다</strong>는 단점이 있어요. 네트워크 상태는 언제든 바뀔 수 있기 때문에, 스냅샷만으로는 실시간 대응이 불가능해요.</p>
<h4 id="네트워크-이벤트-수신-대기-callback">네트워크 이벤트 수신 대기 (Callback)</h4>
<p>시스템에 콜백을 등록하여, 네트워크 상태가 변할 때마다 즉시 알림을 받는 방식이에요. 대부분의 앱에서 권장되는 방식이며, <code>NetworkCallback</code>을 구현하면 다음과 같은 이벤트를 받을 수 있습니다.</p>
<ul>
<li><strong><code>onAvailable()</code></strong> : 새로운 네트워크가 연결되어 사용 준비가 되었을 때</li>
<li><strong><code>onCapabilitiesChanged()</code></strong> : 네트워크 속성이 변했을 때 (예: 인터넷 연결 검증 완료, 무제한 데이터로 변경)</li>
<li><strong><code>onLinkPropertiesChanged()</code></strong> : DNS나 IP 주소 등 네트워크 설정이 변경되었을 때</li>
<li><strong><code>onLost()</code></strong> : 네트워크 연결이 완전히 끊어졌을 때</li>
</ul>
<h3 id="net_capability_internet-vs-net_capability_validated">NET_CAPABILITY_INTERNET vs NET_CAPABILITY_VALIDATED</h3>
<p><code>NetworkCapabilities</code>를 확인할 때 반드시 알아야 할 구분이 있어요.</p>
<p><strong><code>NET_CAPABILITY_INTERNET</code></strong>: 네트워크가 인터넷에 연결되도록 <em>설정</em>되어 있음을 의미해요. 문이 열려 있는 상태와 같습니다. 실제로 밖에 나갈 수 있는지는 별개의 문제예요.</p>
<p><strong><code>NET_CAPABILITY_VALIDATED</code></strong>: 실제로 공개 인터넷에 액세스할 수 있음이 <em>검증</em>된 상태예요. 문 밖으로 나갈 수 있는 상태입니다.</p>
<p>이 구분이 실제로 체감되는 대표적인 사례가 <strong>캡티브 포탈(Captive Portal)</strong>, 즉 로그인이 필요한 공용 Wi-Fi예요. 카페나 공항 Wi-Fi에 연결하면 처음에는 <code>INTERNET</code>은 있지만 <code>VALIDATED</code>는 아닌 상태입니다. 브라우저에서 로그인을 완료해야 비로소 <code>VALIDATED</code>가 돼요.</p>
<p>따라서 실제 인터넷 사용 가능 여부를 판단하려면 <code>NET_CAPABILITY_VALIDATED</code>를 확인해야 합니다.</p>
<h2 id="구현시-지켜야-할-주의사항">구현시 지켜야 할 주의사항</h2>
<p>Android 공식문서에서는 아래사항을 주의하라고 이야기해요.</p>
<h3 id="경합-상태race-condition에-주의하세요">경합 상태(Race Condition)에 주의하세요</h3>
<p>콜백 메서드 내부에서 <code>getNetworkCapabilities()</code> 같은 동기 메서드를 직접 호출하면 안 돼요. 네트워크 상태는 콜백이 호출된 시점과 동기 메서드가 실행되는 시점 사이에 바뀔 수 있기 때문입니다. 반환값이 최신 상태임을 보장할 수 없으므로, <strong>반드시 콜백의 인자로 전달된 객체를 그대로 사용해야 해요.</strong></p>
<h3 id="콜백-리소스-누수를-방지하세요">콜백 리소스 누수를 방지하세요</h3>
<p>네트워크 콜백은 앱 내부 객체만 사용하는 게 아니라, OS의 네트워크 서비스가 상태 추적과 이벤트 전달을 계속 유지해야 하는 <strong>시스템 리소스</strong>예요.</p>
<p>무제한으로 등록을 허용하면 다음과 같은 문제가 생깁니다.</p>
<p><strong>메모리/핸들 누수:</strong> 콜백을 해제하지 않으면 앱과 시스템 양쪽에 참조가 남아요.</p>
<p><strong>이벤트 폭증:</strong> 네트워크 변화마다 등록된 모든 콜백에 브로드캐스트해야 하므로, 콜백이 많을수록 비용이 커져요.</p>
<p><strong>시스템 안정성 저하:</strong> 악성 앱이나 버그 있는 앱이 과도하게 등록하면 전체 디바이스 성능에 영향을 줍니다.</p>
<p>그래서 안드로이드는 <strong>앱(UID) 단위로 등록 가능한 네트워크 요청/콜백 수를 100개로 제한</strong>하고 있으며, 이를 초과하면 <code>TooManyRequestsException</code>이 발생해요. 더 이상 필요하지 않은 콜백은 반드시 <code>unregisterNetworkCallback()</code>으로 해제해야 합니다.</p>
<p>Now In Android의 구현에서도 아래와 같은 패턴을 쓴건 이 때문이에요.</p>
<pre><code class="language-kotlin">callbackFlow { ... awaitClose { unregisterNetworkCallback(callback) } }</code></pre>
<p>Flow 수집이 끝나면 자동으로 콜백이 해제되어 리소스 누수를 방지합니다.</p>
<h3 id="workmanager와의-역할을-구분하세요">WorkManager와의 역할을 구분하세요</h3>
<p>&quot;인터넷이 연결되면 데이터를 동기화해라&quot; 같은 백그라운드 작업이 목적이라면, ConnectivityManager보다 <code>WorkManager</code>가 더 적절해요. WorkManager는 네트워크 제약 조건을 지정할 수 있고, 배터리 효율과 작업 보장 측면에서 훨씬 유리합니다.</p>
<p>ConnectivityManager는 <strong>UI 상태를 실시간으로 반영하는 데</strong> 집중하고, 백그라운드 동기화는 WorkManager에 맡기는 것이 역할 분담의 원칙이에요.</p>
<h2 id="실제-구현networkmonitorimpl-코드-분석">실제 구현(NetworkMonitorImpl 코드 분석)</h2>
<p>이론은 여기까지고, 이제 실제로 어떻게 구현했는지 코드를 살펴볼게요. 핵심은 <strong>ConnectivityManager의 콜백을 Kotlin Flow로 브릿지하는 것</strong>입니다.</p>
<h3 id="전체-코드">전체 코드</h3>
<pre><code class="language-kotlin">@Singleton
class NetworkMonitorImpl @Inject constructor(
    @param:ApplicationContext private val context: Context,
) : NetworkMonitor {

    override val isOnline: Flow&lt;Boolean&gt; = callbackFlow {
        val connectivityManager = context.getSystemService(ConnectivityManager::class.java)

        val callback = object : NetworkCallback() {
            private val networks = mutableSetOf&lt;Network&gt;()

            override fun onAvailable(network: Network) {
                networks += network
                trySend(true)
            }

            override fun onLost(network: Network) {
                networks -= network
                trySend(networks.isNotEmpty())
            }
        }

        val request = NetworkRequest.Builder()
            .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
            .addCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)
            .build()

        trySend(connectivityManager.isCurrentlyConnected())
        connectivityManager.registerNetworkCallback(request, callback)

        awaitClose {
            connectivityManager.unregisterNetworkCallback(callback)
        }
    }
        .distinctUntilChanged()
        .conflate()

    private fun ConnectivityManager.isCurrentlyConnected(): Boolean {
        val networkCapabilities = getNetworkCapabilities(activeNetwork) ?: return false
        return networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) &amp;&amp;
                networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)
    }
}</code></pre>
<h3 id="왜-callbackflow인가">왜 callbackFlow인가?</h3>
<p><code>ConnectivityManager</code>의 네트워크 콜백은 전형적인 <strong>콜백 기반 API</strong>예요. <code>onAvailable()</code>, <code>onLost()</code> 같은 메서드가 시스템에 의해 호출되는 구조입니다. 그런데 우리가 원하는 건 이 이벤트를 Compose UI나 ViewModel에서 <strong>Flow로 수집</strong>하는 거예요.</p>
<p><code>callbackFlow</code>는 바로 이 간극을 메워주는 코루틴 빌더예요. 콜백 기반 API를 Flow로 변환할 때 쓰는 정석 패턴입니다. 내부에서 <code>trySend()</code>를 통해 콜백 이벤트를 Flow의 값으로 전달할 수 있어요.</p>
<pre><code class="language-kotlin">override val isOnline: Flow&lt;Boolean&gt; = callbackFlow {
    // 여기서 콜백을 등록하고, trySend()로 값을 방출
    awaitClose {
        // Flow 수집이 끝나면 여기서 정리(cleanup)
    }
}</code></pre>
<p>그리고 <code>awaitClose</code> 블록이 중요해요. Flow 수집이 끝나는 시점에 <code>unregisterNetworkCallback()</code>을 호출해서 콜백을 해제합니다. 앞서 이야기한 콜백 100개 제한과 리소스 누수 문제를 이 한 줄로 해결하는 거예요.</p>
<h3 id="network-집합으로-온라인-여부-판단하기">Network 집합으로 온라인 여부 판단하기</h3>
<p>아래는 now in android 구현 방식을 많이 참고했는데요.</p>
<pre><code class="language-kotlin">val callback = object : NetworkCallback() {
    private val networks = mutableSetOf&lt;Network&gt;()

    override fun onAvailable(network: Network) {
        networks += network
        trySend(true)
    }

    override fun onLost(network: Network) {
        networks -= network
        trySend(networks.isNotEmpty())
    }
}</code></pre>
<p>이 부분이 구현의 핵심이에요. 왜 단순히 <code>onAvailable</code>이면 <code>true</code>, <code>onLost</code>면 <code>false</code>로 보내지 않았을까요?</p>
<p>안드로이드 기기는 <strong>여러 네트워크에 동시에 연결될 수 있기 때문</strong>이에요. 예를 들어 Wi-Fi와 모바일 데이터가 동시에 활성화된 상태에서 Wi-Fi가 끊기면 <code>onLost</code>가 호출됩니다. 이때 무조건 <code>false</code>를 보내면 실제로는 모바일 데이터로 인터넷이 가능한데도 오프라인으로 판단해 버려요.</p>
<p>그래서 <code>mutableSetOf&lt;Network&gt;()</code>로 현재 사용 가능한 네트워크를 추적하고, <code>onLost</code>에서는 해당 네트워크를 제거한 뒤 <strong>집합이 비어 있는지</strong>로 최종 온라인 여부를 결정합니다. 하나라도 남아 있으면 여전히 온라인이에요.</p>
<h3 id="초기-상태-처리-iscurrentlyconnected">초기 상태 처리: isCurrentlyConnected()</h3>
<pre><code class="language-kotlin">trySend(connectivityManager.isCurrentlyConnected())
connectivityManager.registerNetworkCallback(request, callback)</code></pre>
<p>콜백을 등록하기 <strong>전에</strong> 현재 연결 상태를 먼저 방출하고 있어요. 왜 이 순서가 중요할까요?</p>
<p><code>registerNetworkCallback()</code>은 등록 이후 발생하는 변화만 알려줘요. 이미 연결되어 있는 상태에서 Flow 수집을 시작하면, 변화가 없으니 콜백이 호출되지 않습니다. 그러면 수집자는 아무 값도 받지 못한 채 대기하게 돼요.</p>
<pre><code class="language-kotlin">private fun ConnectivityManager.isCurrentlyConnected(): Boolean {
    val networkCapabilities = getNetworkCapabilities(activeNetwork) ?: return false
    return networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) &amp;&amp;
            networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)
}</code></pre>
<p>그래서 콜백 등록 전에 <code>isCurrentlyConnected()</code>로 현재 스냅샷을 먼저 보내는 거예요. 수집자는 즉시 현재 상태를 받고, 이후 변화는 콜백을 통해 받게 됩니다.</p>
<h3 id="distinctuntilchanged와-conflate">distinctUntilChanged()와 conflate()</h3>
<pre><code class="language-kotlin">.distinctUntilChanged()
.conflate()</code></pre>
<p>Flow 체인 끝에 붙은 이 두 연산자는 각각 역할이 달라요.</p>
<p><strong><code>distinctUntilChanged()</code></strong> : 연속으로 같은 값이 방출되면 무시합니다. 예를 들어 Wi-Fi가 연결된 상태에서 모바일 데이터까지 연결되면 <code>onAvailable</code>이 두 번 호출되고, <code>trySend(true)</code>도 두 번 실행돼요. 하지만 이미 <code>true</code>인 상태에서 또 <code>true</code>를 받을 필요는 없으니, 중복을 걸러냅니다.</p>
<p><strong><code>conflate()</code></strong> : 수집자가 이전 값을 아직 처리하지 못했을 때 새 값이 들어오면, 중간 값을 건너뛰고 <strong>최신 값만</strong> 전달해요. 네트워크 상태가 빠르게 변할 때(예: 터널을 지나면서 연결/해제가 반복될 때) 수집자가 모든 중간 상태를 하나씩 처리할 필요가 없습니다. 우리가 필요한 건 <strong>지금 현재</strong> 온라인인지 아닌지, 그 최신 상태뿐이에요.</p>
<p>이 두 연산자의 조합으로 수집자는 <strong>의미 있는 변화가 있을 때만, 항상 최신 값을 받게 됩니다.</strong></p>
<h3 id="networkrequest-어떤-네트워크를-감시할-것인가">NetworkRequest: 어떤 네트워크를 감시할 것인가</h3>
<pre><code class="language-kotlin">val request = NetworkRequest.Builder()
    .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
    .addCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)
    .build()</code></pre>
<p><code>registerNetworkCallback</code>에 전달하는 <code>NetworkRequest</code>는 &quot;어떤 조건의 네트워크만 수집할지”를 정하는 필터 역할을 해요. 여기서는  <code>NET_CAPABILITY_INTERNET</code>으로 인터넷 연결 설정이 있는 네트워크만, 그리고 <code>NET_CAPABILITY_VALIDATED</code>로 실제 인터넷 접속이 검증된 네트워크만 감시해요. </p>
<p>인터넷과 무관한 네트워크와, 카페 Wi-Fi처럼 연결은 됐지만 로그인 전이라 실제로 인터넷이 안 되는 상태도 걸러냅니다.</p>
<h3 id="구현-요약">구현 요약</h3>
<ol>
<li><code>callbackFlow</code>로 콜백 기반 API를 Flow로 변환해요</li>
<li>콜백 등록 전에 <code>isCurrentlyConnected()</code>로 초기 상태를 먼저 방출해요</li>
<li><code>mutableSetOf&lt;Network&gt;()</code>로 활성 네트워크를 추적해서, 하나라도 남아 있으면 온라인으로 판단해요</li>
<li><code>awaitClose</code>에서 콜백을 해제해 리소스 누수를 방지해요</li>
<li><code>distinctUntilChanged()</code>로 중복 값을 제거하고, <code>conflate()</code>로 항상 최신 값만 전달해요</li>
</ol>
<h2 id="실제-구현-영상">실제 구현 영상</h2>
<p><img src="https://velog.velcdn.com/images/last_game/post/6b43d1e4-aec3-4489-9c18-bf4f33277c8c/image.gif" alt=""></p>
<h2 id="마무리">마무리</h2>
<p>단순 조회성 API 위주의 앱이라면, 요청 실패 시 에러 UI를 보여주는 것만으로 충분할 수 있어요. 하지만 실시간 통신이나 오프라인 우선 구조처럼 네트워크 상태가 앱의 동작 흐름에 직접적인 영향을 주는 경우에는 ConnectivityManager를 함께 사용하는 편이 더 적절합니다.</p>
<p>ConnectivityManager는 모든 네트워크 에러를 해결하기 위한 도구가 아니에요. <strong>네트워크 상태 변화에 따라 앱의 동작을 더 능동적으로 제어하기 위한 도구</strong>입니다. 서버 에러 처리까지 함께 갖춰야, 네트워크에 대한 대응이 완성돼요.</p>
<h2 id="참고">참고</h2>
<ul>
<li><a href="https://github.com/android/nowinandroid/blob/d6abd6d3e562ae0f017888f5d62042316a49a2d6/core/data/src/main/kotlin/com/google/samples/apps/nowinandroid/core/data/util/ConnectivityManagerNetworkMonitor.kt#L20">Now In Android - ConnectivityManagerNetworkMonitor.kt</a></li>
<li><a href="https://developer.android.com/develop/connectivity/network-ops/reading-network-state?hl=ko">Android 공식 문서 - 네트워크 상태 읽기</a></li>
<li><a href="https://developer.android.com/reference/android/net/ConnectivityManager">ConnectivityManager API Reference</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Type-Safe Navigation과 restoreState의 충돌 이슈 정리]]></title>
            <link>https://velog.io/@last_game/restoreState-%EC%99%80-route-%ED%8C%8C%EB%9D%BC%EB%AF%B8%ED%84%B0-%EC%B6%A9%EB%8F%8C-%ED%95%B4%EA%B2%B0-in-Compose-Navigation</link>
            <guid>https://velog.io/@last_game/restoreState-%EC%99%80-route-%ED%8C%8C%EB%9D%BC%EB%AF%B8%ED%84%B0-%EC%B6%A9%EB%8F%8C-%ED%95%B4%EA%B2%B0-in-Compose-Navigation</guid>
            <pubDate>Fri, 20 Mar 2026 15:17:33 GMT</pubDate>
            <description><![CDATA[<p><a href="https://velog.io/@last_game/restoreState%EB%A1%9C-%EC%9D%B8%ED%95%B4-%EB%B0%9C%EC%83%9D%ED%95%98%EB%8A%94-SideEffect%EC%99%80-%ED%95%B4%EA%B2%B0-%EB%B0%A9%EB%B2%95-in-Compose-Navigation">앞서 <code>navOptions</code>와 <code>restoreState</code>에 대해 다루었다</a>면,
이번에는 <strong>Type-Safe Navigation에서 route의 파라미터 값에 따라 달라지는 id 문제</strong>로 인해
<code>restoreState</code>가 의도한 대로 동작하지 않았던 케이스를 정리해보려 합니다.</p>
<h3 id="1️⃣-매칭-탭의-기본-구조">1️⃣ 매칭 탭의 기본 구조</h3>
<p>Matching 탭의 navigation 코드는 다음과 같습니다.</p>
<pre><code class="language-kotlin">// MatchingNavigation.kt 일부
fun NavController.navigateToMatching(
    initTab: MatchingType = MatchingType.RECEIVE,
    navOptions: NavOptions? = null,
) = navigate(Matching(initTab = initTab), navOptions)

@Serializable
data class Matching(
    val initTab: MatchingType = MatchingType.RECEIVE,
) : MainTabRoute</code></pre>
<p>그리고 ViewModel에서는 다음과 같이 초기 탭을 설정합니다.</p>
<pre><code class="language-kotlin">private val initTab = savedStateHandle.toRoute&lt;Matching&gt;().initTab

private val _uiState = MutableStateFlow(
    MatchingContract.State(selectedType = initTab)
)</code></pre>
<p>Type-Safe Navigation을 활용해 <code>Matching(initTab = ACCEPTED)</code>와 같은 형태로
직렬화된 route를 생성하고, 이를 기반으로 ViewModel의 초기 상태를 구성하는 방식입니다.</p>
<h3 id="2️⃣-정상-동작-케이스">2️⃣ 정상 동작 케이스</h3>
<p>다음과 같은 흐름에서는 문제가 없었습니다.</p>
<pre><code>홈 → 매칭(확정) → 홈 → 매칭</code></pre><p>이 경우, 매칭 화면에서 <code>ACCEPTED</code>로 탭을 변경한 뒤 홈으로 이동했다가 다시 매칭으로 돌아오면 기존에 선택한 <code>ACCEPTED</code> 탭이 그대로 유지됩니다.</p>
<p>이는 <code>restoreState</code> 로 인해 기존 <code>NavBackStackEntry</code>와 ViewModel이 복구되었기 때문입니다.</p>
<p>즉, ViewModel이 재생성되지 않고 복구되기 때문에
<code>selectedType = ACCEPTED</code> 상태가 그대로 유지되는 것이죠.</p>
<h3 id="3️⃣-문제-발생-케이스">3️⃣ 문제 발생 케이스</h3>
<p>문제는 다음 흐름에서 발생합니다.</p>
<pre><code>홈 → 매칭(확정) → 홈 → 알림 → 매칭(받은) → 홈 → 매칭</code></pre><p>여기서 기대했던 동작은 다음과 같습니다.</p>
<ul>
<li>알림에서 <code>navigate(initTab = RECEIVE)</code>를 호출하면 <code>RECEIVE</code> 탭이 선택됨</li>
<li>이후 홈을 거쳐 다시 매칭으로 이동하면 마지막 상태(RECEIVE)가 유지됨</li>
</ul>
<p>하지만 실제 동작은 다음과 같았습니다.</p>
<pre><code>ACCEPTED → RECEIVE → ACCEPTED</code></pre><p>알림에서 <code>RECEIVE</code>는 정상 적용되지만, 이후 탭으로 매칭에 진입하면 <strong>기존에 선택했던 ACCEPTED가 다시 선택되는 현상</strong>이 발생했습니다.</p>
<p><img src="https://velog.velcdn.com/images/last_game/post/7ed8d6e7-ee55-492c-9bc5-8241264dcc4d/image.gif" alt=""></p>
<p><code>홈 → 매칭(확정) → 홈 → 알림 → 매칭(받은) → 홈 → 매칭(기존: 확정)</code></p>
<h3 id="4️⃣-원인--route-파라미터에-따른-id-변경">4️⃣ 원인 – route 파라미터에 따른 id 변경</h3>
<p>로그를 찍어보니 결정적인 힌트가 있었습니다.</p>
<blockquote>
<p>route의 파라미터 값에 따라 NavBackStackEntry의 id 값이 달라지고 있었습니다.</p>
</blockquote>
<ul>
<li><code>Matching(initTab = ACCEPTED)</code></li>
<li><code>Matching(initTab = RECEIVE)</code></li>
</ul>
<p>서로 <strong>다른 route 문자열</strong>을 생성하고, Navigation 내부적으로도 <strong>서로 다른 엔트리로 취급</strong>하고 있었습니다.</p>
<p><img src="https://velog.velcdn.com/images/last_game/post/641a39f2-ddb5-47e2-ba43-99b61ac6feab/image.png" alt=""></p>
<p>엔트리ID 값이 달라짐..</p>
<p>그 이유를 찾아보니, 바텀 탭 이동에는 restore 로직이 존재(<code>restoreState = true</code>) 하였으나 알림 화면에서의 navigate에는 restore 로직이 없었습니다.</p>
<p>결과적으로,</p>
<ul>
<li>탭 이동 시에는 기존 매칭 엔트리가 복구되고</li>
<li>알림에서 이동할 때는 새로운 엔트리가 생성됩니다.</li>
</ul>
<p>즉, 매칭 화면이 <strong>2개의 다른 BackStackEntry로 공존</strong>하는 상태가 되었던 것입니다.</p>
<h3 id="5️⃣-그렇다면-restore를-넣으면-해결되지-않을까">5️⃣ 그렇다면 restore를 넣으면 해결되지 않을까?</h3>
<p>처음에는 알림에서 매칭으로 이동할 때도 <code>restoreState = true</code>를 적용하면 문제가 해결될 것이라 생각했습니다.</p>
<p>하지만 이 방식은 구조적으로 한계가 있었습니다.</p>
<p><code>restoreState</code>는 단순히 화면을 다시 그리는 옵션이 아니라, <strong>기존 <code>NavBackStackEntry</code> 자체를 복구하는 동작</strong>입니다.</p>
<p>즉, 엔트리를 새로 생성하는 것이 아니라 이전에 저장된 엔트리를 그대로 되살립니다.
이 경우, 해당 엔트리에 연결된 <code>ViewModel</code>과 <code>SavedStateHandle</code>도 함께 복구됩니다.</p>
<pre><code class="language-kotlin">savedStateHandle.toRoute&lt;Matching&gt;().initTab</code></pre>
<p>이 코드는 <strong>이 엔트리가 처음 생성될 때 전달된 route 파라미터</strong>를 읽습니다.</p>
<p>하지만 restore가 개입하면 엔트리는 이전에 설정된 값인 initTab 의 초기 파라미터 값으로 설정되어, 가장 최신에 전달된 파라미터 값으로 복구하기는 어려웠습니다.</p>
<ul>
<li><code>route</code>는 <strong>초기 생성 시점의 입력값</strong></li>
<li><code>restore</code>는 <strong>기존 상태의 복구</strong></li>
</ul>
<p>라는 서로 다른 역할을 가지게 되고, restore가 적용된 구조에서는 route 파라미터를 통해 상태를 갱신할 수 없게 됩니다.</p>
<p>따라서 <code>navigate(initTab = RECEIVE)</code>를 호출하더라도, 이미 복구된 엔트리는 최초에 설정된 값(예: ACCEPTED)을 그대로 유지하게 됩니다.</p>
<p>결론적으로, <strong>restore가 적용되는 구조에서는 route 파라미터 방식만으로 상태를 변경할 수 없었습니다.</strong></p>
<h3 id="6️⃣-해결-방법--route가-아닌-savedstatehandle을-직접-제어">6️⃣ 해결 방법 – route가 아닌 savedStateHandle을 직접 제어</h3>
<p>결국 해결 방법은 다음과 같이 수정했고, 해당 <a href="https://github.com/TEAM-SMASHING/SMASHING-ANDROID/pull/239/changes#diff-dc878fe938984a2438cd312f1990ef72fa7a95774bc65d04bb11565aae545808">PR</a> 에 적용되어 있습니다.</p>
<ul>
<li>route 파라미터에 의존하지 않음</li>
<li>BackStackEntry의 <code>savedStateHandle</code>에 값을 명시적으로 <code>set</code></li>
<li>매칭 화면 진입 시 <code>get</code>하여 ViewModel 상태를 업데이트</li>
</ul>
<pre><code class="language-kotlin">private object MatchingArgs {
    const val INIT_TAB = &quot;matching_init_tab&quot;
}

fun NavController.setMatchingArgs(tab: MatchingType?) { // navigate 시 저장
    getBackStackEntry(Matching).savedStateHandle[MatchingArgs.INIT_TAB] = tab
}

fun SavedStateHandle.getMatchingArgs(): MatchingType? { // Matching 화면 get
    return get&lt;MatchingType&gt;(MatchingArgs.INIT_TAB)
}

fun SavedStateHandle.removeMatchingArgs() { // Matching 화면 get 이후 삭제
    remove&lt;MatchingType&gt;(MatchingArgs.INIT_TAB)
}</code></pre>
<p>탭 전환 시에는 다음과 같은 NavOptions를 사용하도록 정리했습니다.</p>
<pre><code class="language-kotlin">/**
 * 백스택을 초기화하면서도 이전 Destination의 상태를 저장하고 복원하는 NavOptions를 생성합니다.
 *
 * 사용 사례:
 * - 탭 전환 시 백스택 클리어 + 이전 탭 상태 복원 (Bottom Navigation)
 * - 특정 화면에서 메인으로 복귀하되 상태 유지가 필요한 경우
 */
fun clearBackStackWithRestoreNavOptions() = navOptions {
    popUpTo(0) {
        saveState = true
        inclusive = true
    }
    launchSingleTop = true
    restoreState = true
}</code></pre>
<p>해당 방식은 다음과 같은 역할을 합니다.</p>
<ul>
<li>이전 탭의 상태는 저장되며</li>
<li>동일 탭 재진입 시 복원되고</li>
<li>중복 스택은 방지됩니다.</li>
</ul>
<p>그리고 상태 변경이 필요한 경우에는 route가 아닌 <code>SavedStateHandle</code>을 통해 명시적으로 제어하도록 구조를 분리했습니다.</p>
<h3 id="7️⃣-정리">7️⃣ 정리</h3>
<p>이번 이슈를 통해 정리할 수 있었던 포인트는 다음과 같습니다.</p>
<ol>
<li>Type-Safe Navigation의 route 파라미터는 &quot;초기 생성 시점&quot;에만 의미가 있다.</li>
<li>restoreState는 엔트리를 재생성하지 않고 기존 엔트리를 복구한다.</li>
<li>따라서 restore가 개입하는 구조에서는 route 파라미터 방식만으로는 상태를 제어하기 어렵다.</li>
<li>restore 이후 상태 변경이 필요하다면 <code>savedStateHandle</code>을 직접 제어하는 방식이 더 명확하다.</li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[restoreState로 인해 발생하는 SideEffect와 해결 방법 in Compose Navigation]]></title>
            <link>https://velog.io/@last_game/restoreState%EB%A1%9C-%EC%9D%B8%ED%95%B4-%EB%B0%9C%EC%83%9D%ED%95%98%EB%8A%94-SideEffect%EC%99%80-%ED%95%B4%EA%B2%B0-%EB%B0%A9%EB%B2%95-in-Compose-Navigation</link>
            <guid>https://velog.io/@last_game/restoreState%EB%A1%9C-%EC%9D%B8%ED%95%B4-%EB%B0%9C%EC%83%9D%ED%95%98%EB%8A%94-SideEffect%EC%99%80-%ED%95%B4%EA%B2%B0-%EB%B0%A9%EB%B2%95-in-Compose-Navigation</guid>
            <pubDate>Fri, 20 Mar 2026 15:03:44 GMT</pubDate>
            <description><![CDATA[<p>현재 스프린트를 진행중인 스매싱 프로젝트에서 navigation 리팩 작업을 하면서 저도 헷갈리는 것들이 있고, 앞으로 화면 라우팅을 세팅할때 어떻게 하면 좋을 지 고민해 본 내용을 적어보았습니다.</p>
<p>기존 메인 탭 화면의 경우는 아래와 같았습니다.</p>
<pre><code class="language-kotlin">val navOptions = navOptions {
    navController.currentDestination?.route?.let {
        popUpTo(it) {
            saveState = true
            inclusive = true
        }
        launchSingleTop = true
        restoreState = true
    }
}

when (tab) {
    MainTab.HOME -&gt; navController.navigateToHome(navOptions = navOptions)
    MainTab.SEARCH -&gt; navController.navigateToSearch(navOptions = navOptions)
    MainTab.MATCHING -&gt; navController.navigateToMatching(navOptions = navOptions)
    MainTab.PROFILE -&gt; navController.navigateToMyProfile(navOptions = navOptions)
}</code></pre>
<h3 id="여기서-navoptions란">여기서 navOptions란?</h3>
<p><code>navOptions</code>는 <code>navigate()</code> 호출 시 <strong>네비게이션 동작을 제어하기 위한 옵션 모음</strong>입니다.
<em>(이동하면서 백스택을 어떻게 다룰지 / 기존 상태를 어떻게 처리할지를 결정)</em></p>
<ul>
<li><p><strong>popUpTo(id/route)</strong>: 특정 목적지까지 백스택을 pop(제거)하겠다는 의미입니다.</p>
<ul>
<li><p><strong>inclusive</strong>: <code>popUpTo</code>로 지정한 목적지까지 포함해서 제거할지 여부</p>
</li>
<li><p><strong>saveState</strong>: <code>popUpTo</code>로 인해 제거되는 목적지의 <strong>SavedState 기반 상태를 저장</strong>합니다.</p>
<p>  예를 들어 home → search로 이동할 때, home 화면의 <code>rememberSaveable</code> 상태나 스크롤 위치, viewmodel 등 <strong>복원 가능한 UI 상태</strong>가 저장될 수 있습니다.</p>
</li>
</ul>
</li>
<li><p><strong>launchSingleTop</strong>: 이동하려는 목적지가 이미 백스택 top에 있다면 중복으로 쌓지 않습니다.</p>
</li>
<li><p><strong>restoreState</strong>: 과거에 <code>saveState = true</code>로 저장해둔 상태가 있다면, <code>navigate()</code> 시점에 이를 복원합니다.</p>
<ul>
<li><code>saveState</code>와 함께 사용될 때 의미가 있으며, 저장된 상태가 없으면 복원되지 않습니다.</li>
</ul>
</li>
</ul>
<h3 id="기존-메인-탭-이동-시-실제-동작-정리">기존 메인 탭 이동 시 실제 동작 정리</h3>
<p>위 내용을 통해, 정리하자면 기존 메인 탭의 경우는 탭 간 이동시 아래와 같이 동작합니다.</p>
<ul>
<li>popUpTo(it) { inclusive = true }  → 현재 화면을 스택에서 제거</li>
<li>popUpTo(it) { saveState = true } → 현재 화면의 상태를 저장</li>
<li>launchSingleTop = true → 지금 이동하는 라우트는 <code>inclusive = true</code> 로 항상 새로 생성되기 때문에 스택에 존재하는 라우트가 없음. 현재로서는 큰 의미 없음</li>
<li>restoreState = true → 만약 과거에 <code>saveState</code>로 저장된 상태가 있다면 이를 복구</li>
</ul>
<p>여기서 주의 깊게 봐야할 부분은 <code>saveStae</code> 와 <code>restoreState</code> 부분 입니다.</p>
<p>해당 값으로 인해 <code>NavBackStackEntry</code> 는 복구되고 <code>viewmodel</code> 은 다시 복구 됩니다.</p>
<p><em>(viewmodel 은 ViewmodelStore 를 key 처럼 사용해서 기존에 값이 있으면 복구하고, NavBackStackEntry 는 ViewmodelStore 를 가지고 있는 ViewModelStoreOwner 를 상속 받고 있습니다.)</em></p>
<p>viewmodel 복구 방식 참고: <a href="https://yangsooplus.tistory.com/8">https://yangsooplus.tistory.com/8</a></p>
<h3 id="viewmodel-의-복구로-인해-생기는-sideeffect">Viewmodel 의 복구로 인해 생기는 SideEffect</h3>
<p>상태가 유지되는 방식(특히 스크롤 위치 유지)은 UX 관점에서는 장점이 될 수 있지만, 구현 방식에 따라 의도치 않은 사이드 이펙트를 만들 수 있습니다.</p>
<p>대표적으로 Home에서 Search 화면으로 이동한 뒤 스크롤을 내렸다가, 다시 탭 이동/복귀를 반복하는 경우:</p>
<ul>
<li>스크롤 위치가 유지되면서 화면이 “이전 상태 그대로”로 복구됨</li>
<li>그 결과, UI 진입 시점에 실행되는 로직(예: <code>LaunchedEffect</code> 기반 fetch)이 무한 스크롤 조건과 결합되면 <strong>연쇄적으로 fetch가 발생하게 됩니다.</strong></li>
</ul>
<p>정리하자면, 기존에 저희는 <code>LaunchedEffect</code>로 <strong>화면이 구성될 때마다 fetch</strong> 하는 로직을 넣어두었습니다.</p>
<p>이로 인해, 탭 이동/복귀 과정에서 UI 상태가 유지된 채로 재구성되는 상황이 생기다 보니, 무한 스크롤 조건과 결합되면 <strong>연쇄적으로 fetch</strong>하는 케이스가 발생했습니다.</p>
<h3 id="그렇다면-해결-방법은">그렇다면 해결 방법은?</h3>
<ol>
<li><p><strong><code>savedStateHandle</code> 로 refresh 트리거 하기</strong>
 이전 화면에서 Search를 새로고침 해야 하는 상황이 명확한 경우, <code>SavedStateHandle</code>로 refresh 플래그를 전달하고 Search 쪽에서 이를 받아 <code>refresh()</code>를 수행하는 방법입니다.</p>
<ul>
<li>장점: 어떤 이벤트로 refresh가 발생하는지가 명확해지고, 네트워크 호출을 강하게 제어할 수 있음</li>
<li>단점: Search로 이동하는 경로(혹은 Search로 돌아오는 경로)마다 플래그 세팅이 필요해져 <strong>전파 비용이 늘고</strong>, 흐름이 많아지면 복잡성이 올라갈 수 있음</li>
</ul>
</li>
<li><p>상태 값을 기준으로 fetch 트리거를 제한하기</p>
<p> Search에서 fetch가 필요해지는 조건이 실제로는 아래처럼 정해져 있었습니다.</p>
<ul>
<li><p>지역 변경(Region 복귀 이후)</p>
</li>
<li><p>티어 변경</p>
</li>
<li><p>성별 변경</p>
</li>
<li><p>바텀탭 이동(탭 복귀)</p>
<p>이 중에서 <strong>지역/티어/성별 변경은 검색 조건이 바뀐 것</strong>이기 때문에,
해당 값이 변경될 때는 아래를 함께 수행하도록 했습니다.</p>
</li>
<li><p>스크롤을 최상단으로 초기화</p>
</li>
<li><p>fetch 실행</p>
<p>반대로 바텀탭 이동/복귀는 조건이 바뀐 것이 아니라 화면을 다시 보는 것이므로, 탭 복귀 시에는 스크롤 상태를 그대로 유지하고 fetch를 실행하지 않는 방향을 선택할 수 있습니다.</p>
</li>
</ul>
</li>
</ol>
<p>결론적으로 제가 선택한 방식은 바텀탭 클릭 시마다 자동으로 fetch를 수행하기보다는,</p>
<ul>
<li>조건(지역/티어/성별) 변경 시에만 fetch</li>
<li>바텀탭 복귀 시에는 상태 유지</li>
<li>데이터 최신화는 추후 <strong>사용자 액션 기반(끌어서 새로고침 등)</strong> 으로 제공</li>
</ul>
<p>이 방식이 네트워크 호출을 예측 가능하게 만들고, 상태 복원의 장점도 살릴 수 있다고 판단했습니다.</p>
<br/>
<br/>


<p>PS. 최근 Navigation3와 같은 새로운 네비게이션 구조가 등장했지만, 이번에는 기존 Navigation 구조를 충분히 이해하고 문제를 해결하는 데 집중했습니다.
<em>추후 안정화 시점에 맞춰 Navigation3 도입을 고려해 리팩토링할 예정입니다 :)</em></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[OkHttp 토큰 갱신하기]]></title>
            <link>https://velog.io/@last_game/OkHttp-%ED%86%A0%ED%81%B0-%EA%B0%B1%EC%8B%A0%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@last_game/OkHttp-%ED%86%A0%ED%81%B0-%EA%B0%B1%EC%8B%A0%ED%95%98%EA%B8%B0</guid>
            <pubDate>Wed, 17 Dec 2025 08:57:22 GMT</pubDate>
            <description><![CDATA[<p>안드로이드에서 로그인 기능을 붙이고, OAuth로 보호되는 API를 호출하다 보면 거의 반드시 마주치는 문제가 있습니다.</p>
<ul>
<li>액세스 토큰(Access Token)이 만료되면 API가 401 Unauthorized를 내려준다.</li>
<li>이때 리프레시 토큰(Refresh Token)으로 토큰을 재발급하고,</li>
<li>실패하면 사용자에게 재로그인을 요구해야 한다.</li>
</ul>
<p>Retrofit 기반 프로젝트에서는 보통 OkHttp 레벨에서 “토큰 만료 → 갱신 → 재요청” 시나리오를 처리합니다.</p>
<blockquote>
<p><strong>간단 용어 정리</strong></p>
<ul>
<li>OAuth: 비밀번호를 주지 않고도 제3자에게 내 정보 접근(권한)을 허용하는 표준 방식
주로 Access Token, Refresh Token을 사용</li>
</ul>
<ul>
<li>OIDC(OpenID Connect): OAuth 위에 “로그인 인증” 목적을 얹은 표준
ID Token이 추가될 수 있음, OAuth는 권한, OIDC는 인증에 초점</li>
</ul>
<ul>
<li>JWT(Json Web Token): 토큰 포맷(형식) 중 하나
토큰 안에 클레임(유저/권한/만료 등)을 담아 서명한 구조</li>
</ul>
</blockquote>
<blockquote>
<p>&quot;OAuth = JWT”는 아니고, OAuth 토큰이 JWT 의 한 형태일 수도 있습니다.</p>
</blockquote>
<h2 id="oauth-토큰-갱신-하기">OAuth 토큰 갱신 하기</h2>
<p>기본적으로 retrofit 을 사용하는 프로젝트에서 OAuth 토큰 만료를 및 갱신 시나리오를 처리하는 경우, 앱의 모든 네트워크 요청이 OkHttp 를 거치기 때문에 헤더에서 토큰을 가로채고 이를 갱신하는 로직을 한번만 구현하면 됩니다.</p>
<p>해당 방식을 해결하기 위한 방법으로 Okhttp 에서 두가지 방법을 지원합니다.
<code>Authenticator</code> 와 <code>Interceptor</code> 가 있습니다.</p>
<h3 id="okhttp-authenticator">Okhttp Authenticator</h3>
<p>Authenticator는 OkHttp가 <code>401</code> 혹은 <code>407</code>을 받았을 때 호출합니다.</p>
<p>Authenticator 는 프록시 서버에 대한 인증인 실패 코드인 <code>407</code>, 웹서버 인증 실패 코드인 <code>401</code> 을 응답 받을 때 실행됩니다.</p>
<p><img src="https://velog.velcdn.com/images/last_game/post/e9956dab-11e5-418f-84a1-7987dc46ac8d/image.png" alt=""></p>
<p>Autenticator 를 구현하기 위해서는 해당 인터페이스를 상속받아 구현해야합니다.
<code>authenticate</code> 메소드를 오버라이딩해서 내부에 인증 fallback 처리를 해줍니다.</p>
<p>내부 구현방식을 보겠습니다.</p>
<pre><code class="language-kotlin">if (response.request().header(&quot;Authorization&quot;) != null) {
  return null; // Give up, we&#39;ve already failed to authenticate.
}

String credential = Credentials.basic(...)
return response.request().newBuilder()
    .header(&quot;Authorization&quot;, credential)
    .build();</code></pre>
<p>먼저 웹서버 즉, 이미 지난 권한 인증을 시도했는지 확인하는 로직이 필요합니다.
<code>authenticate</code> 로 받은 response 는 이전 요청에 대한 응답을 가지고 있기 때문에 <code>response.request().header(&quot;Authorization&quot;)</code> 를 통해 이미 인증 헤더를 가지고 있는지 확인합니다.</p>
<p>인증 재시도 로직은 딱한번만 수행해야하기 때문에 위와 같이 인증 요청을 이전에 수행하였는지 파악하여 무한 재시도를 막기 위한 로직을 처리합니다.</p>
<p>좀 더 명시적으로 제한한다고하면 아래와 같이 시도횟수를 카운팅하여 제한하는 방법이 있습니다.</p>
<pre><code class="language-kotlin">private int responseCount(Response response) {
  int result = 1;
  while ((response = response.priorResponse()) != null) {
    result++;
  }
  return result;
}</code></pre>
<h3 id="최종-코드"><strong>최종 코드</strong></h3>
<p>제가 이번에 진행한 프로젝트에서는 토큰 재갱신 API 호출 로직이 이미 repostiory 에 정의해 사용하고 있었기에 해당 로직을 그대로 사용하는 방식으로 적용했습니다.</p>
<p>구현은 크게 3가지로 나눌 수 있습니다.</p>
<ol>
<li>시도 횟수 제한(무한 반복 방지)</li>
<li>토큰 재발급 요청 및 헤더 교체</li>
<li>토큰 재발급 실패 처리(로그아웃/재로그인 유도)</li>
</ol>
<p><strong>시도횟수 제한, 무한 반복 방지</strong></p>
<pre><code class="language-kotlin">if (responseCount(response) &gt;= MAX_RESPONSE_COUNT) return null

private fun responseCount(response: Response): Int {
    var count = 1
    var current = response.priorResponse
    while (current != null) {
        count++
        current = current.priorResponse
    }
    return count
}</code></pre>
<p>앞서 살펴본 바와 같이 401 이 뜬 후 재요청을 시도 한후 다시 401 이 떠서 해당 로직을 반복할 수 있기에 횟수 제한을 설정하였습니다.</p>
<p><strong>토큰 갱신 요청</strong></p>
<pre><code class="language-kotlin">synchronized(this) { // 동기화
    val newAccessToken = runBlocking {
        tryReissueToken()
    } ?: return null

    return response.request.newBuilder() // 새 accessToken 으로 요청
        .header(AUTHORIZATION_HEADER, &quot;$BEARER_PREFIX $newAccessToken&quot;)
        .build()
}

private suspend fun tryReissueToken(): String? {
    val refreshToken = tokenDataStore.getRefreshToken()
        ?: return handleReissueFailure()

    ... // 리프레쉬 토큰으로 토큰 재갱신 로직
    )
}</code></pre>
<p>갱신시에는 <code>authenticate</code> 가 동기로 선언이 되어 있기 때문에 runBlocking 을 이용해 해 스레드를 일시 블로킹합니다.</p>
<p><strong>토큰 갱신 실패</strong></p>
<pre><code class="language-kotlin">private suspend fun handleReissueFailure(): String? {
    tokenDataStore.clearInfo()
    appRestarter.restartApp(isStartLogin = true)
    return null
}</code></pre>
<p>갱신 실패시에는 토큰을 전부 제거하고, 다이얼로그를 띄워 로그인 화면으로 이동하거나, Process Phoenix 등을 사용해 앱을 재시작하게 합니다.</p>
<p>마지막으로 해당 TokenAutenticator 를 OkhttpClient 에 설정해줍니다.</p>
<pre><code class="language-kotlin">val okHttpClient = OkHttpClient.Builder()
    .authenticator(TokenAuthenticator(tokenManager))
    // 다른 설정 ...
    .build()</code></pre>
<h3 id="okhttp-interceptor">Okhttp Interceptor</h3>
<p>Authenticator와 달리 Interceptor 는 요청 혹은 응답이 처리되기 전 이를 가로채서 수정하는 방식을 사용합니다. 기존에 <code>401</code>, <code>407</code> 코드 응답시 호출되었던 Autenticator 와 다르게 직접적으로 상태 코드를 확인해 좀 더 유연하게 처리할 수 있습니다.</p>
<pre><code class="language-kotlin">override fun intercept(chain: Interceptor.Chain): Response {
    val token = tokenProvider.getToken() // 현재 토큰 가져오기

    // 요청에 토큰 추가하는 로직 생략...
    val response = chain.proceed(requestWithToken)

    // 토큰 만료시 (401 응답)
    if (response.code = 401) {
        // ... 이하 토큰 갱신 및 실패 로직 수행
    }
}</code></pre>
<p>핵심은 응답 코드를 확인해 앞서 Autenticator 에서 수행했던 로직을 처리 하는 것 입니다.
이를 통해 보다 명시적으로 서버의 응답 코드에 대응해서 처리할 수 있다는 장점이 있습니다.</p>
<pre><code class="language-kotlin">val okHttpClient = OkHttpClient.Builder()
    .addInterceptor(TokenInterceptor(tokenProvider))
    // ... 다른 설정 ...
    .build()</code></pre>
<p>마찬가지로 Interceptor 도 다음과 같이 등록합니다.</p>
<h2 id="추가-고려사항">추가 고려사항</h2>
<p>OkHttpClient를 하나만 공유하고, 동일 호스트로 비동기 요청을 동시에 많이 보내는 환경이라면 데드락(또는 무한 대기) 시나리오가 발생할 수 있습니다.</p>
<p>OkHttp는 <a href="https://github.com/square/okhttp/blob/8e0cc1b398a10c27a0921a14bc53ca770169d83c/okhttp/src/commonJvmAndroid/kotlin/okhttp3/Dispatcher.kt#L66"><code>Dispatcher.maxRequestsPerHost</code></a>로 호스트당 비동기 요청 동시 실행을 기본 5개로 제한합니다.
<a href="https://angrypodo.tistory.com/25">(해당 시나리오는 다음 글을 참고했습니다.)</a></p>
<p>따라서 아래 상황이 가능해집니다.</p>
<ul>
<li>동일 호스트로 비동기 요청 5개가 동시에 실행 중인 상태에서</li>
<li>5개 모두 401을 받고 <code>authenticate()</code>로 들어감</li>
<li><code>authenticate()</code> 내부에서 <code>runBlocking</code>으로 Dispatcher 스레드를 점유</li>
<li>동시에 refresh 요청을 같은 OkHttpClient로 <code>enqueue</code>하면, 이미 호스트 슬롯이 꽉 차서 “6번째 요청”이 ready 큐에 쌓인 채 실행을 못 함</li>
<li>결과적으로 <strong>authenticate는 refresh를 기다리고, refresh는 실행 슬롯을 기다리는</strong> 상태가 되어 무한 대기가 발생할 수 있습니다.</li>
</ul>
<p>이런 케이스를 피하려면 다음 중 하나가 필요합니다.</p>
<ul>
<li>Authenticator 내부 refresh 요청을 <strong>동기(<code>Call.execute()</code>) 기반으로 분리</strong>해서 Dispatcher 슬롯 경쟁을 피하기</li>
<li>refresh 전용 <code>OkHttpClient</code>를 분리해서 <strong>메인 요청 Dispatcher와 분리</strong>하기</li>
</ul>
<p>제가 진행하고 있는 프로젝트의 경우 해당 부분에 대해서 OkhttpClient 를 분리해서 사용하고 있었기에 따로 동기 기반의 요청으로 구조 변경을 하진 않았습니다.</p>
<h2 id="마무리">마무리</h2>
<p>이번 글에서는 안드로이드 앱의 네트워크 통신에서 필수적인 토큰 만료 처리와 OkHttp를 활용한 갱신 전략을 살펴보았습니다.</p>
<p>단순히 401 Unauthorized에 대응하는 것을 넘어, Authenticator와 Interceptor의 특성을 정확히 이해하고 선택하는 것이 중요합니다. 특히 비동기 요청이 빈번한 환경에서 발생할 수 있는 <strong>동시성 이슈(Concurrency Issue)</strong>와 네트워크 데드락(Deadlock) 문제는 안정적인 앱 서비스를 위해 반드시 짚고 넘어가야 할 지점입니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[🍫 Custom Snack Bar 를 만들어보자 ~]]></title>
            <link>https://velog.io/@last_game/Custom-Snack-Bar-%EB%A5%BC-%EB%A7%8C%EB%93%A4%EC%96%B4%EB%B3%B4%EC%9E%90</link>
            <guid>https://velog.io/@last_game/Custom-Snack-Bar-%EB%A5%BC-%EB%A7%8C%EB%93%A4%EC%96%B4%EB%B3%B4%EC%9E%90</guid>
            <pubDate>Wed, 03 Dec 2025 11:55:21 GMT</pubDate>
            <description><![CDATA[<h2 id="1-기획-의도-내가-어떤-기능을-어떻게-동작하도록-구현하고자-했었는데">1. [기획 의도] 내가 어떤 기능을 어떻게 동작하도록 구현하고자 했었는데,</h2>
<table>
<thead>
<tr>
<th><img src="https://velog.velcdn.com/images/last_game/post/f761fab3-543a-4e4e-92e3-d7d931f8b5d2/image.png" width="900" /></th>
<th><img src="https://velog.velcdn.com/images/last_game/post/0c8fc7e5-8dfc-4eee-956f-a02b141dfdbc/image.png" width="860" /></th>
</tr>
</thead>
</table>
<p>저는 최근에 스낵바를 구현하면서 각각의 화면에서 특정 버튼 클릭 시 스낵바가 3초 동안 노출되고, &quot;이동&quot; 버튼을 누르면 특정 화면으로 이동하는 기능이 필요했습니다.</p>
<p>하지만 실제로 구현을 시작하자마자 생각보다 많은 문제가 등장했습니다.</p>
<p>이 글에서는</p>
<blockquote>
<p>&quot;Compose의 Snackbar가 왜 3초 고정 시간이 안 되는지?&quot;
&quot;왜 연속 클릭 시 스낵바가 반복 호출되는지?&quot;
&quot;어떻게 커스텀 스낵바를 안정적으로 구현했는지?&quot;</p>
</blockquote>
<p>위에 대한 해결 과정을 기록해보려고 합니다.</p>
<p><img src="https://velog.velcdn.com/images/last_game/post/74b68997-35a6-4e51-aea4-7ad62ba637a5/image.png" alt=""></p>
<p>명세는 다음과 같이 되어 있었어요.</p>
<blockquote>
<p><strong>기능을 정리해 보자면</strong></p>
<ul>
<li>고정 텍스트</li>
<li>“이동” 텍스트 클릭시 페이지 이동</li>
<li>3초간 스낵바가 띄워져 있어야함</li>
</ul>
</blockquote>
<h2 id="2-문제-파악-어떤-문제가-발생했고">2. [문제 파악] 어떤 문제가 발생했고,</h2>
<p>먼저 스낵바를 구현하기 위해서 <a href="https://developer.android.com/develop/ui/compose/components/snackbar#snackbar-with-action">공식문서</a>를 확인해 보았습니다.</p>
<p>밑에서 하나하나씩 뜯어 볼게요..!</p>
<p><strong>SnackBar</strong> 를 구현하기 위해서는 크게 3가지 재료가 필요했습니다.</p>
<ol>
<li><p><strong>SnackBar UI</strong>(스낵바 컴포저블 함수 UI)</p>
<p><img src="https://velog.velcdn.com/images/last_game/post/894c267b-7e17-46b5-94e3-12a11f0302a9/image.png" alt="스낵바 컴포저블 함수 UI"></p>
<p>실제로 <code>androidx.compose.material3</code> 에서 <code>Snackbar</code> 컴포넌트를 제공하고 있습니다.</p>
</li>
</ol>
<ul>
<li><p>코드</p>
<pre><code class="language-kotlin">@Composable
fun Snackbar(
    modifier: Modifier = Modifier,
    action: @Composable (() -&gt; Unit)? = null,
    dismissAction: @Composable (() -&gt; Unit)? = null,
    actionOnNewLine: Boolean = false,
    shape: Shape = SnackbarDefaults.shape,
    containerColor: Color = SnackbarDefaults.color,
    contentColor: Color = SnackbarDefaults.contentColor,
    actionContentColor: Color = SnackbarDefaults.actionContentColor,
    dismissActionContentColor: Color = SnackbarDefaults.dismissActionContentColor,
    content: @Composable () -&gt; Unit,
) {
    Surface(
        modifier = modifier,
        shape = shape,
        color = containerColor,
        contentColor = contentColor,
        shadowElevation = SnackbarTokens.ContainerElevation,
    ) {
        val textStyle = SnackbarTokens.SupportingTextFont.value
        val actionTextStyle = SnackbarTokens.ActionLabelTextFont.value
        CompositionLocalProvider(LocalTextStyle provides textStyle) {
            when {
                actionOnNewLine &amp;&amp; action != null -&gt;
                    NewLineButtonSnackbar(
                        text = content,
                        action = action,
                        dismissAction = dismissAction,
                        actionTextStyle = actionTextStyle,
                        actionContentColor = actionContentColor,
                        dismissActionContentColor = dismissActionContentColor,
                    )
                else -&gt;
                    OneRowSnackbar(
                        text = content,
                        action = action,
                        dismissAction = dismissAction,
                        actionTextStyle = actionTextStyle,
                        actionTextColor = actionContentColor,
                        dismissActionColor = dismissActionContentColor,
                    )
            }
        }
    }
}</code></pre>
<p>하지만, 이는 <code>shadowElevation</code> 을 고정값으로 사용하고 있기에 커스텀한 SnackBar 를 만들기에는 부적합하다고 생각해 직접 컴포저블을 구성했습니다.</p>
</li>
<li><p>MelonActionSnackbar 코드</p>
<pre><code class="language-kotlin">@Composable
fun Snackbar(
    modifier: Modifier = Modifier,
    action: @Composable (() -&gt; Unit)? = null,
    dismissAction: @Composable (() -&gt; Unit)? = null,
    actionOnNewLine: Boolean = false,
    shape: Shape = SnackbarDefaults.shape,
    containerColor: Color = SnackbarDefaults.color,
    contentColor: Color = SnackbarDefaults.contentColor,
    actionContentColor: Color = SnackbarDefaults.actionContentColor,
    dismissActionContentColor: Color = SnackbarDefaults.dismissActionContentColor,
    content: @Composable () -&gt; Unit,
) {
    Surface(
        modifier = modifier,
        shape = shape,
        color = containerColor,
        contentColor = contentColor,
        shadowElevation = SnackbarTokens.ContainerElevation,
    ) {
        val textStyle = SnackbarTokens.SupportingTextFont.value
        val actionTextStyle = SnackbarTokens.ActionLabelTextFont.value
        CompositionLocalProvider(LocalTextStyle provides textStyle) {
            when {
                actionOnNewLine &amp;&amp; action != null -&gt;
                    NewLineButtonSnackbar(
                        text = content,
                        action = action,
                        dismissAction = dismissAction,
                        actionTextStyle = actionTextStyle,
                        actionContentColor = actionContentColor,
                        dismissActionContentColor = dismissActionContentColor,
                    )
                else -&gt;
                    OneRowSnackbar(
                        text = content,
                        action = action,
                        dismissAction = dismissAction,
                        actionTextStyle = actionTextStyle,
                        actionTextColor = actionContentColor,
                        dismissActionColor = dismissActionContentColor,
                    )
            }
        }
    }
}</code></pre>
</li>
</ul>
<p> Snackbar 에 들어갈 <code>message</code>, 액션 버튼의 텍스트인 <code>actionLabel</code>, 실제 어떤 동작을 할지 <code>action</code> 함수를 받을수 있도록 구성해보았어요.</p>
<ol start="2">
<li><strong>SnackBarHost</strong>
<code>Scaffold</code>에서 사용되는 Snackbar들을 표시하기 위한 <code>Host</code>로,
<code>Material</code> 스펙과 <code>hostState</code>에 따라 <code>Snackbar</code>를 적절한 시점에 보여주고 숨기고 닫는 역할을 합니다.</li>
</ol>
<ul>
<li>코드<pre><code class="language-kotlin">@Composable
fun SnackbarHost(
    hostState: SnackbarHostState,
    modifier: Modifier = Modifier,
    snackbar: @Composable (SnackbarData) -&gt; Unit = { Snackbar(it) },
) {
    val currentSnackbarData = hostState.currentSnackbarData
    val accessibilityManager = LocalAccessibilityManager.current
    LaunchedEffect(currentSnackbarData) {
        if (currentSnackbarData != null) {
            val duration =
                currentSnackbarData.visuals.duration.toMillis(
                    currentSnackbarData.visuals.actionLabel != null,
                    accessibilityManager,
                )
            delay(duration)
            currentSnackbarData.dismiss()
        }
    }
    FadeInFadeOutWithScale(
        current = hostState.currentSnackbarData,
        modifier = modifier,
        content = snackbar,
    )
}</code></pre>
<code>SnackBarHostState</code> 와 <code>snackbar</code> 컴포저블 함수를 받아서 이를 띄워주는 역할을 합니다 !</li>
</ul>
<ol start="3">
<li><p><strong>SnackBarHostState</strong>
스낵바의 상태를 관리하고, <code>showSnackbar()</code>를 통해 스낵바를 띄웁니다.</p>
<p>보통 <code>remember</code> 를 이용해서 저장 한 후,  Scaffold 에 <code>SnackBarHost</code> 를 구성할 때 사용해요.</p>
<p>스낵바를 조작하는 데 핵심인 상태로, 주로 살펴볼 곳은 <code>Mutex()</code> 를 이용한 방식과 <code>SnackbarData</code>, <code>showSnackbar()</code> 입니다.</p>
</li>
</ol>
<ul>
<li><p><strong>Mutex() 와 showSnackBar()</strong>
Snackbar 요청 즉, 띄우는 함수는 <code>SnackBarHostState</code> 인스턴스가 가지고 있는
<code>showSnackBar()</code> 를 호출해서 띄우게 되는데요 ..!</p>
<blockquote>
<p>만약, 사용자가 여러번 스낵바 요청을 하게 되면 어떻게 될까요??
스낵바가 동시에 두개가 뜰 가능성은 존재 하지 않을까요..?? </p>
</blockquote>
<p>스낵바를 띄우는 함수인 <code>showSnackBar()</code> 내부 코드를 간단히 보자면 …        </p>
</li>
</ul>
<pre><code class="language-kotlin">  suspend fun showSnackbar(visuals: SnackbarVisuals): SnackbarResult =
        mutex.withLock {
            try {
                return suspendCancellableCoroutine { continuation -&gt;
                    currentSnackbarData = SnackbarDataImpl(visuals, continuation)
                }
            } finally {
                currentSnackbarData = null
            }
}      </code></pre>
<p>  주의 깊게 봐야할 점은 <code>mutex.withLock</code> 부분입니다. </p>
<p> <code>mutex</code> 는 보통 여러 코루틴이 공유 자원에 대해 데이터 무결성 혹은 하나의 작업을 완료하도록 보장할때 사용합니다
  <code>withLock</code> 블록 안에 있는 작업을 완전히 수행하게 합니다. 이후 작업요청이 들어오면 이전 작업이 완료될때까지 대기한후 실행합니다.</p>
<p>  즉, 사용자가 연속적으로 클릭을 하게 되면 두개가 동시에 뜨는 것이 아닌, 작업을 큐처럼 쌓아두고 스낵바가 사라지면 다음 스낵바를 띄워주게 됩니다 !!</p>
<ul>
<li><p><strong>showSnackbar()</strong></p>
<p>앞서 본 <code>showSnackbar()</code> 를 사용하기 위해서는 실제로 외부에서 사용할때는 아래 코드를 호출해서 사용합니다.</p>
<pre><code class="language-kotlin">suspend fun showSnackbar(
    message: String,
    actionLabel: String? = null,
    withDismissAction: Boolean = false,
    duration: SnackbarDuration =              // duration 고정
        if (actionLabel == null) SnackbarDuration.Short else SnackbarDuration.Indefinite,
): SnackbarResult =
        showSnackbar(SnackbarVisualsImpl(message, actionLabel, withDismissAction, duration))</code></pre>
<p><code>SnackbarDuration</code> 은 <code>Short</code>, <code>Long</code>, <code>Indefinite</code> 3가지 duration 만을 지정 할수 있게 되어 있습니다.
각각의 시간은 4초, 10초, 무한 으로되어 있어서 저희가 구현하고자 하는 3초는 해당 방식으로 구현 할 수 없었습니다 😭😭</p>
</li>
</ul>
<h3 id="문제점-정리">문제점 정리</h3>
<p>이제 정리를 해보자면 ..!</p>
<ul>
<li>elevation 고정값이기에, 직접 커스텀 컴포넌트를 구현(해결)</li>
<li>스낵바 여러번 클릭시마다 이전 스낵바는 취소하고 새로운 스낵바만 동작하도록 구성해야한다.
<strong>but, 기존 구조는 스낵바 대기가 쌓이는 구조</strong></li>
<li>snackbar duration 값을 3초로 지정
<strong>but, 기존 구조는 시간이 4초, 10초, 무한으로 지정되어 있음.</strong></li>
</ul>
<br/>

<h2 id="3-해결-과정-그-문제를-해결하기-위해-어떻게-접근했으며">3. [해결 과정] 그 문제를 해결하기 위해 어떻게 접근했으며,</h2>
<p>이제 ..! 앞서 2가지 문제를 해결한 방법에 대해 이야기 해볼게요 ㅎㅎ
우선 저는 snackbar 에 대한 로직을 따로 분리하고 싶었기에 SnackbarController 라는 클래스를 만들어주었습니다 ! </p>
<blockquote>
<ul>
<li><p>MelonSnackbarController</p>
<pre><code class="language-kotlin">  class MelonSnackbarController(
      private val coroutineScope: CoroutineScope,
  ) {
      val snackbarHostState = SnackbarHostState()

      var currentRequest by mutableStateOf&lt;MelonSnackbarActionRequest?&gt;(null)
          private set

      fun show(request: MelonSnackbarActionRequest) {
          currentRequest = request

          coroutineScope.launch {
              launch {
                  snackbarHostState.showSnackbar(
                      message = request.message,
                      actionLabel = request.actionLabel,
                      withDismissAction = true,
                      duration = SnackbarDuration.Short,
                  )
              }
          }
      }

      fun performAction() {
          currentRequest?.onClick?.invoke()
          currentRequest = null

          coroutineScope.launch {
              snackbarHostState.currentSnackbarData?.dismiss()
          }
      }
  }

  @Composable
  fun rememberMelonSnackbarController(
      scope: CoroutineScope = rememberCoroutineScope(),
  ): MelonSnackbarController =
      remember {
          MelonSnackbarController(scope)
      }
</code></pre>
<p>  <code>showSnackbar</code> 는 suspend 함수이기 때문에 <code>rememberCoroutineScope</code> 를 사용해서 동작하도록 하였습니다. <code>controller</code> 자체도 생성 후 초기화 되지 않게 하기 위해 remember 로 감싸 주었어요.</p>
</li>
</ul>
<p>다음으로는 Snackbar 요청을 위한 모델과 외부에서 트리거 하는 방식으로 사용하기 위해 CompositonLoacal 을 사용했습니다.</p>
<ul>
<li><p>MelonSnackbarModel</p>
<pre><code class="language-kotlin">  @Immutable
  data class MelonSnackbarActionRequest(
      val message: String,
      val actionLabel: String,
      val onClick: () -&gt; Unit,
  )

  val LocalMelonSnackbarTrigger =
      staticCompositionLocalOf&lt;(MelonSnackbarActionRequest) -&gt; Unit&gt; {
          error(&quot;No MelonSnackbarTrigger provided&quot;)
      }</code></pre>
</li>
</ul>
<p>실제 사용하는 곳에서는 snackbarController 를 호출하는 로직을 trigger 로 선언하여 외부에서 사용하게 했습니다.</p>
<ul>
<li><p>선언 &amp; 사용</p>
<pre><code class="language-kotlin">  // MainScreen 에서 선언
  val snackbarController = rememberMelonSnackbarController()
  val snackbarTrigger: (MelonSnackbarActionRequest) -&gt; Unit =
      remember {
          { request -&gt; snackbarController.show(request) }
      }

  Scaffold(
      snackbarHost = {
          SnackbarHost(
              hostState = snackbarController.snackbarHostState,
          ) { data -&gt;
              val request = snackbarController.currentRequest ?: return@SnackbarHost

              MelonActionSnackbar(
                  message = request.message,
                  actionLabel = request.actionLabel,
                  action = snackbarController::performAction,
                  modifier =
                      Modifier
                          .padding(
                              start = 16.dp,
                              end = 16.dp,
                              bottom = 16.dp,
                          ),
              )
          }
      },

  // 외부 Screen 에서 사용 ex. HomeScreen
  val snackbarTrigger = LocalMelonSnackbarTrigger.current
  val snackbarRequest =
      MelonSnackbarActionRequest(
          message = &quot;mixup count $count&quot;,
          actionLabel = stringResource(snackbar_move_action_label),
          onClick = navigateToMixUp,
      )
  snackbarTrigger(snackbarRequest) // &lt;- 실제 호출하는 함수</code></pre>
<p>  snackbarTrigger 가 호출이 되면 snackbarRequest 가 전달되고 이를 통해 <a href="http://snackbarController.show">snackbarController.show</a> 가 호출이 됩니다.</p>
</li>
</ul>
</blockquote>
<p>다시 돌아와서 보자면 ..!</p>
<ol>
<li><p>스낵바 여러번 클릭시마다 이전 스낵바는 취소하고 새로운 스낵바만 동작하도록 구성</p>
<p>기존 구조는 연속 클릭시 해당 스낵바에 대한 요청이 사라지지 않고 대기했다가 계속해서 이전 호출을 다시 불러와지는것을 볼 수 있는데요..!!</p>
<p><img src="https://velog.velcdn.com/images/last_game/post/78e30c8b-608b-41f7-9d1f-1c490eded0fd/image.gif" alt="스낵바 동작 구성"></p>
<p>따라서 명시적으로 이를 해제하게 하는 방법을 사용했어요 !
방법은 <code>snackbarHostState</code> 의 <code>currentSnackbarData</code> 에 <code>dissmiss</code> 함수를 사용하면되는데요 ..!</p>
<p>아래는 매번 호출시 마다 이전 <code>snackbar</code> 를 명시적으로 해제해주는 코드입니다.</p>
<pre><code class="language-kotlin">fun show(request: MelonSnackbarActionRequest) {
   snackbarHostState.currentSnackbarData?.dismiss() // 명시적으로 Dismiss

   currentRequest = request

   coroutineScope.launch {
       launch {
           snackbarHostState.showSnackbar(
               message = request.message,
               actionLabel = request.actionLabel,
               withDismissAction = true,
               duration = SnackbarDuration.Short,
           )
       }
   }
}</code></pre>
</li>
<li><p>snackbar duration 값을 3초로 지정</p>
<p> 그러면 !! 이제 시간을 조정해봐야 겠죠?? 앞서 본것과 같이 <code>Duration</code> 을 <code>Short</code> 로 지정할 경우, 가장 짧아야 4초의 시간을 지정할 수 있었어요 </p>
<p> 따라서 Snackbar 를 명시적으로 3초의 시간이 지나게 되면 타임 아웃이 걸리게 하여 스낵바가 dismiss 되는 로직이 필요했습니다 </p>
<p> 해당 방식을 구현하기 위해서 corutineScope 내부에 delay 를 담당하는 launch 블록을 만들고 delay 가 지나면 dismiss 하게 하였어요</p>
</li>
</ol>
<ul>
<li><p>코드        </p>
<pre><code class="language-kotlin">private const val SNACKBAR_AUTO_DISMISS_MS = 3000L

fun show(request: MelonSnackbarActionRequest) {
    currentRequest = request

    coroutineScope.launch {
        snackbarHostState.currentSnackbarData?.dismiss()

        launch {
            snackbarHostState.showSnackbar(
                message = request.message,
                actionLabel = request.actionLabel,
                withDismissAction = true,
                duration = SnackbarDuration.Indefinite, // 무한으로 설정
            )
        }

        launch {                                            
            delay(SNACKBAR_AUTO_DISMISS_MS)  // 3초 딜레이    
            snackbarHostState.currentSnackbarData?.dismiss()
        }                                                   
    }
}       </code></pre>
</li>
</ul>
<p><img src="https://velog.velcdn.com/images/last_game/post/268a4e10-e0f2-4100-97b9-ae43ef698c7d/image.gif" alt="3초 딜레이 코드"></p>
<p>해당 코드로 실행 해보았는데 뭔가 이상하죠 ..?
분명 3초가 지나고 나서 snackbar 가 사라져야하는데 왜 바로 사라지는 걸까요?????</p>
<p>문제는 코루틴에 있었습니다. 기존의 코루틴이 계속해서 동작하고 있었기 때문인데요 </p>
<p>정리하자면...</p>
<blockquote>
<ol>
<li>showSnackbar 수행 x 15<ol start="2">
<li>매 show 마다 delay 코루틴 수행 </li>
<li>각 코루틴 별로 delay 3초가 지나면 dismiss 를 호출</li>
<li>현재의 스낵바가 dismiss 된다</li>
</ol>
</li>
</ol>
</blockquote>
<p>네 이상 깡깡이의 삽질이었습니다</p>
<p>그럼 이 문제를 어떻게 해결 했냐면 ~
코루틴의 <code>job.cancle()</code>을 이용해서 스낵바 요청에 대해서 1대1로 취소해주는 방식을 사용했어요.</p>
<ul>
<li><p>코드</p>
<pre><code class="language-kotlin">fun show(request: MelonSnackbarActionRequest) {

    coroutineScope.launch {
        snackbarHostState.currentSnackbarData?.dismiss()

        val job = launch {
            snackbarHostState.showSnackbar(
                message = request.message,
                actionLabel = request.actionLabel,
                withDismissAction = true,
                duration = SnackbarDuration.Indefinite,
            )
        }

        launch {
            delay(SNACKBAR_AUTO_DISMISS_MS)
            job.cancel()          // 3초가 지나면 작업 취소
        }
    }
}</code></pre>
</li>
</ul>
<p>   <img src="https://velog.velcdn.com/images/last_game/post/a0617d56-68db-43ad-8df8-80efe15a0590/image.gif" alt=""></p>
<p>  음 .. 잘되는거 같죠..??</p>
<p>  그래서 ! 이제 해결된거 아니냐 라고 생각하실 수 도 있지만 해당 구조는 문제점을 하나 가지고 있습니다.</p>
<p>  이전에 dismiss 된 코루틴 즉, 이전의 delay 가 계속 동작하고 있기에 문제가 돼요.
제가 원한건 스낵바가 새로 호출이되면 이전의 작업이 완전히 취소 되기를 바랬어요.</p>
<p>  따라서 이전 <code>job</code> 을 참조하여 새로운 작업이 들어오면 <code>cancle</code> 해주는 로직을 구성했습니다.</p>
<ul>
<li><p>최종 코드</p>
<pre><code class="language-kotlin">private const val SNACKBAR_AUTO_DISMISS_MS = 3000L

class MelonSnackbarController(
    private val coroutineScope: CoroutineScope,
) {
    val snackbarHostState = SnackbarHostState()

    var currentRequest by mutableStateOf&lt;MelonSnackbarActionRequest?&gt;(null)
        private set

    private var actionJob: Job? = null // 이전 job 을 참조할 변수
    private var timerJob: Job? = null  // 이전 job 을 참조할 변수

    fun show(request: MelonSnackbarActionRequest) {
        currentRequest = request

        coroutineScope.launch {
            clearCurrentSnackbar()

            val job =
                launch {
                    snackbarHostState.showSnackbar(
                        message = request.message,
                        actionLabel = request.actionLabel,
                        withDismissAction = true,
                    )
                }

            actionJob = job

            timerJob =
                launch {
                    delay(SNACKBAR_AUTO_DISMISS_MS)
                    clearCurrentSnackbar()
                    currentRequest = null
                }
        }
    }

        // 전체 job cancel 및 리소스 제거
    private fun clearCurrentSnackbar() {
        actionJob?.cancel()
        timerJob?.cancel()
        actionJob = null
        timerJob = null
    }

    fun performAction() {
        currentRequest?.onClick?.invoke()
        currentRequest = null

        clearCurrentSnackbar()
    }
}        </code></pre>
</li>
</ul>
<h2 id="4-추후-목표-더-개선하기-위해-어떤-점을-더-고려할-수-있는지">4. [추후 목표] 더 개선하기 위해 어떤 점을 더 고려할 수 있는지</h2>
<p>앞서 구현한 방식은 Job 을 직접 들고 있으면서
이전 스낵바 Job/타이머를 취소하고 새 스낵바를 띄우는 방식이었는데요 !</p>
<p>추후에 해당 코드를 개선한다고 한다면 Flow + collectLatest 로 flow 기반의 처리로 요청시마다 매번 마지막 요청만을 처리해서 구현할 수도 있을 거 같습니다 !</p>
<p>또, 현재 구조는 매번 스낵바를 새로 불러오는데 해당 방식 말고 스낵바가 떠있는 동안에는 새로운 스낵바가 안 뜨게 하라는 요구사항이 있을 수도 있습니다.</p>
<p>그런경우에는 Channel 의 BufferOverFlow 정책을 적용해 사용하거나 이전의 요청 값을 비교해 이를 막는 방식이 있겠습니다 ㅎㅎ.. </p>
<p>글 읽어 주셔서 감사합니다 ~!!</p>
<br/>
<br/>

<h2 id="📕-참고-자료">📕 참고 자료</h2>
<p>다음의 링크를 참고했습니다.</p>
<p><a href="https://developer.android.com/develop/ui/compose/components/snackbar?hl=ko">compose snackbar 공식 문서</a>
<a href="https://fornewid.medium.com/jetpack-compose-custom-snackbar-cd3297c80736">compose custom snackbar 아티클</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Kotlin Scope 함수에 대해 알아보자]]></title>
            <link>https://velog.io/@last_game/Kotlin-Scope-%ED%95%A8%EC%88%98%EC%97%90-%EB%8C%80%ED%95%B4-%EC%95%8C%EC%95%84%EB%B3%B4%EC%9E%90</link>
            <guid>https://velog.io/@last_game/Kotlin-Scope-%ED%95%A8%EC%88%98%EC%97%90-%EB%8C%80%ED%95%B4-%EC%95%8C%EC%95%84%EB%B3%B4%EC%9E%90</guid>
            <pubDate>Fri, 14 Nov 2025 13:18:45 GMT</pubDate>
            <description><![CDATA[<p>코틀린의 Scope 함수를 정리하고 학습한 뒤 각각에 어떤 상황에서 scope 함수를 쓰는것이 좋을지 알아보겠습니다.</p>
<h2 id="scope-함수">Scope 함수?</h2>
<blockquote>
<p>Scope 함수란?</p>
<p>Scope 함수(Scope Function) 는 객체를 더 간결하고 읽기 쉽게 다루기 위해,
객체의 컨텍스트(Context) 안에서 람다 블록을 실행하도록 도와주는 Kotlin 표준 라이브러리의 함수입니다.</p>
</blockquote>
<p>스코프 함수를 사용한 예를 들면서 한번 볼게요.</p>
<pre><code class="language-kotlin">val person = Person()
person.name = &quot;Tom&quot;
person.age = 30
person.introduce()

// Scope 함수 적용
val person = Person().apply {
    name = &quot;Tom&quot;
    age = 30
    introduce()
}</code></pre>
<p>매번 <code>person.</code>을 써야 해서 코드가 길고 보기 불편하죠.
이걸 한 번에 묶어서 표현할 수 있는 게 바로 Scope 함수예요.</p>
<blockquote>
<p> 💡 추가로 알아보면 좋을 것</p>
</blockquote>
<ul>
<li>고차 함수(<a href="https://kotlinlang.org/docs/reference/lambdas.html">Higher-Order Functions</a>)란?
함수를 인자로 받거나, 함수를 반환하는 함수
즉, 함수를 데이터처럼 다루는 개념<pre><code class="language-kotlin">  fun operate(x: Int, y: Int, operation: (Int, Int) -&gt; Int): Int { // 함수 전달
      return operation(x, y)
  }</code></pre>
</li>
<li>람다 표현식(<a href="https://kotlinlang.org/docs/reference/lambdas.html">lambda expression</a> )이란?
이름이 없는 함수(익명 함수, anonymous function)
간단한 함수를 한 줄로 표현하는 문법<pre><code class="language-kotlin">  fun main() {
      val result = operate(3, 5) { a, b -&gt; a + b } // { paramter -&gt; body } 형식
      println(result) // 8
  }</code></pre>
</li>
<li>수신 객체란?
<code>수신 객체 타입</code>: 확장함수를 가지게 되는 클래스의 이름
<code>수신 객체</code>: 확장 함수를 호출하는 클래스의 객체 </li>
</ul>
<h2 id="scope-함수-종류">Scope 함수 종류</h2>
<p>스코프 함수에는 <code>let</code>, <code>run</code>, <code>apply</code>, <code>also</code>, <code>with</code> 가 있어요.
각각은 유사한 기능을 수행하지만 함수 정의와 구현에 각각이 차이가 있어 다르게 사용됩니다.</p>
<table>
<thead>
<tr>
<th>함수(Function)</th>
<th>객체 참조(Object Reference)</th>
<th>결과 값(Return Value)</th>
<th>사용 사례(Common Use Case)</th>
</tr>
</thead>
<tbody><tr>
<td>let</td>
<td>it</td>
<td>람다 마지막행(Lambda result)</td>
<td>null 처리 또는 값 변환</td>
</tr>
<tr>
<td>run</td>
<td>this</td>
<td>람다 마지막행(Lambda result)</td>
<td>계산 또는 초기화</td>
</tr>
<tr>
<td>apply</td>
<td>this</td>
<td>수신 객체(Object itself, 객체 자체)</td>
<td>객체 구성</td>
</tr>
<tr>
<td>also</td>
<td>it</td>
<td>수신 객체(Object itself, 객체 자체)</td>
<td>side effect 추가</td>
</tr>
<tr>
<td>with</td>
<td>this</td>
<td>람다 마지막행(Lambda result)</td>
<td>그룹화 작업</td>
</tr>
</tbody></table>
<ul>
<li><p>각각의 스코프 함수 내부 정의    </p>
<pre><code class="language-kotlin">  inline fun &lt;T, R&gt; with(receiver: T, block: T.() -&gt; R): R { // 수신객체 지정 람다
      return receiver.block()
  }

  inline fun &lt;T&gt; T.also(block: (T) -&gt; Unit): T { // 람다 파라미터
      block(this)
      return this
  }

  inline fun &lt;T&gt; T.apply(block: T.() -&gt; Unit): T { // 수신객체 지정 람다
      block()
      return this
  }

  inline fun &lt;T, R&gt; T.let(block: (T) -&gt; R): R {  // 람다 파라미터
      return block(this)
  }

  inline fun &lt;T, R&gt; T.run(block: T.() -&gt; R): R { // 수신객체 지정 람다
      return block()
  }</code></pre>
</li>
</ul>
<h3 id="this-와-it">this 와 it</h3>
<p>Kotlin의 Scope 함수(let, apply, run, also, with)는 객체를 블록 안에서 사용할 수 있는 공통점이 있습니다.
그런데 블록 안에서 그 객체를 부르는 방식(객체 참조)이 두 가지로 나뉘어요.</p>
<p>1️⃣ <code>this</code> — 수신 객체 지정 람다 (Receiver Lambda)</p>
<ul>
<li>객체의 내부 컨텍스트에서 코드를 실행</li>
<li>객체 내부에 프로퍼티를 바로 사용</li>
<li><code>this</code>는 생략 가능</li>
</ul>
<p>위와 같은 특징들로 <code>this</code> 는 주로 함수를 호출하거나 내부에서 프로퍼티를 접근하는 경우 유용합니다.</p>
<pre><code class="language-kotlin">val person = Person().apply {
    name = &quot;안드콩&quot;     // this.name
    age = 25          // this.age
    introduce()       // this.introduce()
}</code></pre>
<p>여기서 <code>this</code> 는 <code>Person</code> 가리키게 됩니다. apply 블로 안에서 프로퍼티를 수정하거나 메소드를 바로 호출이 가능하게합니다.</p>
<p>2️⃣ <code>it</code> — 람다 인자 (Lambda Argument)</p>
<ul>
<li>객체를 인자로 넘겨받는 형태</li>
<li>외부에서 객체를 전달받아 다루는 방식</li>
<li><code>it</code>은 생략 불가능</li>
</ul>
<pre><code class="language-kotlin">val person = Person(&quot;안드콩&quot;, 25)
person.also { it -&gt; 
    println(&quot;이름: ${it.name}, 나이: ${it.age}&quot;)
}

val person = Person(&quot;안드콩&quot;, 25)
person.also { value -&gt;
    println(&quot;이름: ${value.name}, 나이: ${value.age}&quot;)
}</code></pre>
<p>여기서 <code>it</code> 는 <code>person</code> 을 가리키게 됩니다.</p>
<br/>
이제 !! 각각의 함수에 대한 종류와 목적에 대해 알아보겠습니다.

<p>그전에 하나 정리하고 가자면 ~</p>
<ul>
<li><code>apply</code> <code>also</code> 는 (수신)객체 자체를 반환합니다.</li>
<li><code>let</code> <code>run</code> <code>with</code> 는 람다 결과(람다의 마지막 행) 을 반환합니다.</li>
</ul>
<br/>

<h3 id="letnull-saftey-및-값-변환">let(Null saftey 및 값 변환)</h3>
<p><code>let</code>은 nullable 객체를 처리하거나 값을 변환하는 데 주로 사용합니다. 
객체를 <code>it</code> 로 참조하고 람다의 결과를 반환합니다.</p>
<p>3가지 정도의 경우에 사용합니다.</p>
<ul>
<li>지정된 값이 null 이 아닌 경우에 코드를 실행해야 하는 경우.</li>
<li>Nullable 객체를 다른 Nullable 객체로 변환하는 경우.</li>
<li>단일 지역 변수의 범위를 제한 하는 경우.</li>
</ul>
<pre><code class="language-kotlin">@Composable
fun ProfileScreen(user: User?) {    // nullable 처리
    user?.let {
        Text(&quot;이름: ${it.name}&quot;)
    } ?: Text(&quot;사용자 정보가 없습니다.&quot;)
}

val nicknameLength = user?.let { it.nickname.length } ?: 0 // 값 변환

getPersonDao().let { dao -&gt;    // 지역 스코프 제한
    dao.insert(Person(&quot;안드콩&quot;, 25))
}</code></pre>
<h3 id="run계산-및-초기화">run(계산 및 초기화)</h3>
<p><code>run</code>은 <code>this</code>를 사용하여 객체를 참조하고 람다의 결과를 반환합니다. 
객체를 초기화하거나 값을 계산하는 데 적합합니다.</p>
<pre><code class="language-kotlin">val service = MultiportService(&quot;https://example.kotlinlang.org&quot;, 80)

// 값 초기화 run
val result = service.run {
    port = 8080
    query(prepareRequest() + &quot; to port $port&quot;)
}

// 값 초기화 let
val letResult = service.let {
    it.port = 8080
    it.query(it.prepareRequest() + &quot; to port ${it.port}&quot;)
}</code></pre>
<p>이 예제는 값을 초기화 하는 코드입니다. <code>let</code> 으로도 충분히 표현 가능하지만, run 을 사용한 부분이 훨씬 깔끔해 보입니다.</p>
<p>참고로 run 은 확장 함수가 아닌 <code>run</code> 단독으로 쓰일경우 함수로도 사용가능합니다. 이경우 여전히 마지막 줄의 값을 반환합니다. 주로, 계산을 특정 범위 내에서 사용하고 싶을때 사용하면 유용합니다.</p>
<pre><code class="language-kotlin">val hexNumberRegex = run {
    val digits = &quot;0-9&quot;
    val hexDigits = &quot;A-Fa-f&quot;
    val sign = &quot;+-&quot;

    Regex(&quot;[$sign]?[$digits$hexDigits]+&quot;)
}</code></pre>
<h3 id="apply객체-구성">apply(객체 구성)</h3>
<p><code>apply</code>는 객체를 this로 참조하고 객체 자체를 반환합니다.
어떤 객체의 인스턴스를 생성과 동시에, 변수에 담기 전 초기화를 할 때 주로 사용합니다.</p>
<pre><code class="language-kotlin">val user = User().apply {
    name = &quot;Alice&quot;
    age = 25
}</code></pre>
<h3 id="alsoside-effect-처리">also(Side Effect 처리)</h3>
<p><code>also</code>는 객체를 <code>it</code>로 참조하고 객체 자체를 반환합니다. 로깅 또는 유효성 검사와 같은 Side Effect 처리을 위해 설계되었습니다.</p>
<pre><code class="language-kotlin">class Book(author: Person) {
    val author = author.also {
      requireNotNull(it.age)
      print(it.name)
    }
}</code></pre>
<h3 id="with그룹화-작업">with(그룹화 작업)</h3>
<p><code>with</code>는 객체를 가져와 <code>this</code>로 참조하고 람다의 결과를 반환합니다. 그룹화 작업에 적합합니다.</p>
<p>Non-nullable (Null 이 될수 없는) 수신 객체 이고 결과가 필요하지 않은 경우에만 <code>with</code> 를 사용합니다.</p>
<pre><code class="language-kotlin">val person: Person = getPerson()
with(person) {
    print(name)
    print(age)
}</code></pre>
<p>사실 이외에도 takeIf, takeUnless 등등.. 몇개가 더 있는데요. 이부분은 공식문서를 참고하시면 좋을거 같습니다 !!</p>
<h2 id="⚠️-scope-함수-사용-시-주의점">⚠️ Scope 함수 사용 시 주의점</h2>
<p>Kotlin 공식 문서에서는 이렇게 말합니다</p>
<blockquote>
<p>범위 함수는 코드를 간결하게 만들지만,
가독성을 떨어뜨리거나 혼동을 줄 수 있으므로 과도한 사용은 피해야 한다.</p>
<p>특히 여러 Scope 함수를 중첩하거나 연쇄 호출(chain) 하는 경우
컨텍스트 객체를 헷갈리기 쉬우니 주의하라.</p>
</blockquote>
<p>기본적으로 중첩해서 사용하는 경우는 가독성을 해치기에 지양하라고 이야기 하고 있습니다.</p>
<p>특히 <code>this</code> 의 경우, (수신)객체가 암시적으로 전달되기에, 이를 사용하는 <code>apply</code>, <code>run</code>, <code>with</code> 는 중첩해서는 안됩니다.</p>
<p><code>also</code> 와 <code>let</code> 을 중첩할 경우에는 <code>it</code> 를 사용해서는 안됩니다. 대신 명식적인 이름을 제공해 코드상 이름이 혼동되지 않게 합니다.</p>
<p>바른 예)</p>
<pre><code class="language-kotlin">val greeting = userRepository.getUser()
    ?.also { println(&quot;데이터 로드 성공: ${it.name}&quot;) }
    ?.let { user -&gt; &quot;안녕하세요, ${user.name}님!&quot; }
    ?: &quot;사용자 정보를 불러올 수 없습니다.&quot;</code></pre>
<p>다음과 같이 각각의 스코프 함수의 특성에 맞게 적절히 체이닝 하는 것은 좋아보입니다.</p>
<h2 id="결론">결론</h2>
<p>스코프 함수를 공부하다 보면 “굳이 이렇게까지 사용할 필요가 있을까?”라는 의문이 들 수 있습니다.</p>
<p>하지만 다양한 스코프 함수의 의도를 이해해두면 다른 사람이 작성한 코드를 읽는 데 큰 도움이 되고 상황에 맞게 적절히 사용했을 때 훨씬 간결하고 코틀린스러운 코드를 작성할 수 있습니다.
<br/></p>
<p>공식 문서에서도 다음과 같이 말합니다.</p>
<blockquote>
<p>서로 다른 스코프 함수는 사용 사례가 겹칠 수 있으며, 팀이나 프로젝트에서 정한 규칙에 따라 적절한 함수를 선택해 사용할 수 있습니다.</p>
</blockquote>
<p>즉, 스코프 함수는 “무조건 이렇게 써야 한다”는 정답이 있는 것이 아니라,
어떤 목적을 가지고 이 스코프 함수를 선택했는지 명확하다면 팀 내 컨벤션에 따라 통일감 있게 사용하는 것이 가장 중요합니다.</p>
<p>올바른 사용 목적을 기반으로 일관성 있게 적용한다면 스코프 함수는 분명 코드 품질을 높이는 데 기여할 수 있습니다.</p>
<h2 id="📕-참고-자료">📕 참고 자료</h2>
<p>다음의 링크를 참고했습니다.</p>
<p><a href="https://kotlinlang.org/docs/scope-functions.html">Scope functions 코틀린 공식문서</a>
<a href="https://kotlinlang.org/docs/extensions.html">Extensions 코틀린 공식문서</a>
<a href="https://kotlinlang.org/docs/lambdas.html">고차함수와 람다 코틀린 공식문서</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Kotlin 의 프로퍼티를 알아보자 (feat. Backing Properties)]]></title>
            <link>https://velog.io/@last_game/Kotlin-%EC%9D%98-%ED%94%84%EB%A1%9C%ED%8D%BC%ED%8B%B0%EB%A5%BC-%EC%95%8C%EC%95%84%EB%B3%B4%EC%9E%90-feat.-Backing-Properties</link>
            <guid>https://velog.io/@last_game/Kotlin-%EC%9D%98-%ED%94%84%EB%A1%9C%ED%8D%BC%ED%8B%B0%EB%A5%BC-%EC%95%8C%EC%95%84%EB%B3%B4%EC%9E%90-feat.-Backing-Properties</guid>
            <pubDate>Wed, 12 Nov 2025 08:31:58 GMT</pubDate>
            <description><![CDATA[<p>먼저 아래 내용에 대한 질문을 바탕으로 저는 작성해 보았습니다.</p>
<ul>
<li>프로퍼티는 무엇인지? 내부적으로 어떻게 동작하는지?</li>
<li>또, 프로퍼티에는 프로퍼티의 값을 뒷받침 하는 필드인 backing field 라는 것을 사용한다고 하는데 내부 구현은 어떻게 되고 동작방식에는 차이가 있는지?</li>
<li>코틀린에서 변경 가능한 데이터보다 변경 할 수 없는 불변 데이터 사용을 도와주는 방법은 무엇인지?</li>
</ul>
<p>용어 및 개념 정리</p>
<ul>
<li>getter 와 setter 를 통틀어서 “accessors(접근자)” 라고 부름.</li>
<li>property 는 필드와 접근자인 getter, setter 를 통틀어서 부르는 말(속성).</li>
</ul>
<br/>

<h2 id="1-kotlin-의-property">1. Kotlin 의 Property</h2>
<p>Kotlin에서 <code>var</code>나 <code>val</code>로 변수를 선언하면, 단순히 값을 저장하는 변수를 만드는 것처럼 보이지만 사실은 그렇지 않습니다.</p>
<p>Kotlin에서는 클래스 내에서 변수를 선언할 때 <strong>컴파일 시점에 자동으로 getter와 setter로 변환</strong>됩니다.
즉, Java에서의 “필드(field)” 개념과는 달리, Kotlin은 이 추상화된 구조를 <strong>프로퍼티(property)</strong> 라고 부릅니다.</p>
<pre><code class="language-kotlin">// Kotlin
var name: String = &quot;콩틀린&quot;

// Java
private String name = &quot;콩틀린;

public String getName() {
    return name;
}

public void setName(String name) {
    this.name = name;
}</code></pre>
<p>코틀린은 이처럼 property 라는 개념을 적용하여 추상화된 접근 방식을 제공합니다. 이는 캡슐화와 데이터 보호 측면에서 매우 유리합니다.</p>
<h2 id="2-backing-field">2. <strong>Backing Field</strong></h2>
<p>코틀린 프로퍼티 내에서 필드를 사용하기 위해서는 백킹 필드(Backing Field) 를 사용합니다.</p>
<p>Backing Field 는 프로퍼티의 실제 값을 저장하는 저장소 입니다. <code>getter</code>와 <code>setter</code>가 값을 읽고 쓸 수 있도록 Kotlin 컴파일러가 자동으로 생성하는 내부 변수예요.
즉, 우리가 직접 선언하고 보여지진 않지만 프로퍼티의 실제 값을 담고 있는 숨겨진 저장소라고 생각하시면 돼요.</p>
<h3 id="⚙️-backing-field-생성-조건">⚙️ Backing Field 생성 조건</h3>
<p>다만, 이러한 Backing Field 는 항상 생기지 않고 직접 선언해서 사용하는 건 아닙니다.</p>
<blockquote>
<p>생성 조건은 아래와 같습니다.</p>
<ul>
<li>접근자(Getter, Setter) 중에 하나 이상의 기본 구현을 사용하는 경우</li>
<li>접근자(Getter, Setter) 를 재정의하여 field 식별자를 참조하는 경우</li>
</ul>
</blockquote>
<p>field 를 직접 생성(선언) 은 불가능하지만 <code>field</code> 키워드를 통해서 참조 및 조작을 수행할 수 있습니다.
<img src="https://velog.velcdn.com/images/last_game/post/8ab28603-0231-4fd7-898f-321c98fbd630/image.png" alt="kotlin 선언 기본구조"></p>
<p>예시)</p>
<pre><code class="language-kotlin">var nickname: String = &quot;콩틀린&quot;
    get() = field.uppercase()
    set(value) {
        field = value.trim()
    }
</code></pre>
<h3 id="❌-backing-field-생성-되지-않는-경우">❌ Backing Field 생성 되지 않는 경우</h3>
<p>아래 코드의 <code>isEmpty</code> 경우 백킹 필드가 만들어 지지 않습니다.</p>
<pre><code class="language-kotlin">// Kotlin
class A {
    var size = 0
    val isEmpty: Boolean
        get() = this.size == 0
}

// Java 변환 코드
public final class A {
   @NotNull
   private int size;

   public final int getSize() {
      return this.size;
   }

   public final void setSize(@NotNull int var1) {
      this.size = var1;
   }

   public final boolean isEmpty() {
      return this.size == 0;
   }
}</code></pre>
<p><code>isEmpty</code> 의 getter 를 커스텀 하는데 이에 대한 <code>field</code> 를 사용한 직접적인 접근을 수행하지 않기 때문에 실제 Java 변환 코드에서는 <code>isEmpty</code> 필드가 만들어지지 않은 것을 알 수 있습니다.</p>
<h3 id="⚠️-주의할-점">⚠️ 주의할 점</h3>
<p>Kotlin 공식 문서에서는 getter/setter를 복잡하게 커스터마이징하는 것을 <strong>지양하라</strong>고 말합니다.
커스텀 접근자 내부에 연산이나 비즈니스 로직이 들어가면, 상태 관리가 어려워지고 예측 불가능한 부작용이 발생할 수 있기 때문입니다. 따라서 <strong>프로퍼티는 단순 상태를 나타내거나 설정하기 위한 목적</strong>으로만 설정하는 것이 좋고 비즈니스 로직의 경우 별도의 함수를 정의하는 것을 권장합니다.</p>
<blockquote>
<p>정리해 본 팁</p>
<ul>
<li>시간 복잡도 O(1) 을 넘어가는 연산을 지양</li>
<li>비즈니스 로직 지양</li>
<li>비결정적인 경우 지양(동일한 입력이나 조건에서도 결과가 일정하지 않은 코드)</li>
<li>getter 에서 상태변경을 시도하는 것을 지양</li>
</ul>
</blockquote>
<h2 id="3-backing-properties">3. Backing Properties</h2>
<p>기본적으로 <strong>한 객체에서 외부로 데이터를 제공할 때</strong>에는, <strong>가능한 한 불변인 상태로 제공해주는 것이 좋습니다.</strong>
이를 처리하는 방법 중 하나가 Backing Property 입니다. 
⇒ 내부 속성으로는 수정 할 수 있지만 외부적으로 수정할 수 없게 도와주는 코딩 패턴</p>
<p>코드를 통해 먼저 보겠습니다 ..!</p>
<pre><code class="language-kotlin">class ShoppingCart {
    // Backing property
    private val _items = mutableListOf&lt;String&gt;()

    // Public read-only view
    val items: List&lt;String&gt;
        get() = _items

    fun addItem(item: String) {
        _items.add(item)
    }

    fun removeItem(item: String) {
        _items.remove(item)
    }
}</code></pre>
<p><code>_items</code> 는 <code>private</code> 하게 변수를 선언해서 관리하고 이를 <code>items</code> 에서는 이를 받아 <code>getter</code> 만 구현해서 외부에서 사용될 프로퍼티는 읽기만 가능하게 하고 있어요.</p>
<p>공식 문서의 코딩 컨벤션에 따르면 Backing property 는 프로퍼티명 앞에 언더바( _ ) 를 붙입니다.</p>
<blockquote>
<p>Use a leading underscore when naming backing properties to follow Kotlin <a href="https://kotlinlang.org/docs/coding-conventions.html#names-for-backing-properties">coding conventions</a>.</p>
</blockquote>
<h3 id="❓왜-backing-property-를-쓸까">❓왜 Backing Property 를 쓸까?</h3>
<p>코틀린에서는 <code>get</code>, <code>set</code> 키워드 앞에 한정자 private 을 사용하게 되면 이를 외부에서 <code>getter</code> , <code>setter</code> 를 외부에서 사용하지 못하게 할수 있습니다.</p>
<pre><code class="language-kotlin">class BankAccount() {
    var balance: Int = 100
        // Only the class can modify the balance
        private set 
}

fun main() {
    val account = BankAccount()

    account.balance = 100  // Error: cannot assign because setter is private
}</code></pre>
<p>위 코드와 같이 balance 는 <code>private set</code> 으로 선언 되어 있기 때문에 선언된 클래스 내에서만 값을 수정할 수 있고 외부에서는 수정이 불가능합니다.</p>
<p>이제 한가지 의문점이 드는데요??</p>
<blockquote>
<p>Backing Property 도 외부에서 접근이 불가능하게 하려고 한건데 그냥 <code>private set</code> 을 써도 되지 않나요?</p>
</blockquote>
<pre><code class="language-kotlin">class Test1 {
    private var _word = &quot;test&quot; // Backing Property
    val word: String
        get() = _word
}

class Test2 {
    var word = &quot;test&quot; // private set 방식
        private set
}

// Java 변환 코드
public final class Test1 {
   private String _word = &quot;test&quot;;

   @NotNull
   public final String getWord() {
      return this._word;
   }
}

public final class Test2 {
   @NotNull
   private String word = &quot;test&quot;;

   @NotNull
   public final String getWord() {
      return this.word;
   }
}</code></pre>
<p>네 놀랍게도 Backing Property 방식도 <strong>필드와 setter</strong> 를 생성하지 않기 때문에 구현 방식에 차이가 없습니다..!</p>
<p>하지만 아래와 같은 경우는 어떨까요??
<strong>MutableList</strong> 와 <strong>List</strong> 를 보겠습니다.
MutableList 에 내장 메소드인 <code>add()</code> 를 통해 단순히 원소를 하나 추가하는 상황이라고 생각해 보겠습니다.
여기서는 단순히 <code>_word</code> 를 전달하는 것이 아닌 <code>_words.toList()</code> 로 데이터를 복사해 전달했습니다.
<em>(참고: MutableList 은 List 를 상속받고 있어 List → MutableList 로 다운캐스팅이 가능합니다.)</em></p>
<pre><code class="language-kotlin">class Test1 {
    private val _words = mutableListOf&lt;String&gt;()
    val words: List&lt;String&gt;
        get() = _words.toList()
}

class Test2 {
    var words = mutableListOf&lt;String&gt;()
        private set
}

fun main() {
    val test1 = Test1()
    // List MutableList 다운캐스팅
    (test1.words as MutableList&lt;String&gt;).add(&quot;테스트&quot;) // Error: classCastException 
    println(test1.words)

    val test2 = Test2()
    test2.words.add(&quot;테스트&quot;)
    println(test2.words)
}</code></pre>
<p>위 코드에서 backing field 의 getter 를 구현할때 <code>toList()</code> 를 사용했습니다.
<code>toList()</code> 사용시에는 새로운 메모리에 데이터만을 복사한 객체를 생성하기 때문에 다운캐스팅이 불가능합니다 !!<del>(참고로 <code>toList()</code> 로 전달하는 것이 아닌 <code>_words</code> 만 바로 전달한다면 캐스팅이 가능하겠죠?)</del></p>
<h2 id="4-asstateflow-asshareflow">4. .asStateFlow() .asShareFlow()</h2>
<p>실제 안드로이드 개발시 ViewModel 내부에 상태관리시 <code>MutableStateFlow</code> 와 <code>MutableSharedFlow</code> 를 사용할 일이 많습니다. (<del>Flow 개념은 생략하겠습니다..</del>)</p>
<p>이때 Backing Property 를 사용합니다.</p>
<pre><code class="language-kotlin">private val _uiState = MutableStateFlow(UiState())
val uiState = _uiState.asStateFlow()

private val _sideEffect = MutableSharedFlow&lt;SideEffect&gt;()
val sideEffect = _sideEffect.asSharedFlow()</code></pre>
<p>MutableStateFlow 와 MutableSharedFlow 는 각각 StateFlow 와 SharedFlow 로 받아서 불변인 Flow 로 사용하게 합니다.</p>
<pre><code class="language-kotlin">private val _count = MutableStateFlow&lt;Int&gt;(0)
val count : StateFlow&lt;Int&gt; = _count

private val _errorEvent = MutableSharedFlow&lt;String&gt;()
val _errorEvent : SharedFlow&lt;String&gt; = _errorEvent</code></pre>
<p>여기서 중요한점은 MutableStateFlow → StateFlow 와 MutableSharedFlow → SharedFlow 각각의 클래스 → 인터페이스 구조로 구현해 만들어진 클래스여서
StateFlow → MutableStateFlow 로 다운 캐스팅이 가능하게 됩니다.</p>
<pre><code class="language-kotlin">fun main(){
    val _flow = MutableStateFlow&lt;Int&gt;(1)
    val flow: StateFlow&lt;Int&gt; = _flow

    println(_flow.value)
    (flow as MutableStateFlow).value = 2
    println(_flow.value)
}

// 출력 결과
// 1
// 2</code></pre>
<p>해당 코드와 같이 backing property 를 사용했음에도 다운 캐스팅을 통해서 값을 변경하게 합니다.</p>
<p>이를 보완하기 위해서는 <code>asStateFlow()</code>,  <code>asSharedFlow()</code> 를 통해서 보완할 수 있습니다!</p>
<p><img src="https://velog.velcdn.com/images/last_game/post/a2228f6b-3407-4b08-8b0d-00b6b1a61052/image.png" alt=""></p>
<p>해당 확장함수는 ReadOnlyStateFlow 라는 래퍼 클래스로 감싸서 전달해주는 것을 볼 수 있는데
이는 StateFlow 타입으로 한번 감싸써 강제 캐스팅을 막을 수 있습니다.</p>
<pre><code class="language-kotlin">fun main(){
    val _flow = MutableStateFlow&lt;Int&gt;(1)
    val flow: StateFlow&lt;Int&gt; = _flow.asStateFlow()

    println(_flow.value)
    (flow as MutableStateFlow).value = 2  // Error: classCastException 
    println(_flow.value)
}</code></pre>
<p><a href="https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/-state-flow/">공식 문서</a>에서도 사용하고 있어 <code>asStateFlow()</code>이를 안쓸 이유는 없어 보입니다!!</p>
<h2 id="📕-참고-자료">📕 참고 자료</h2>
<p>다음의 링크를 참고했습니다.</p>
<p><a href="https://kotlinlang.org/docs/properties.html#backing-fields">Properties | kotlin Docs</a>
<a href="https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/-state-flow/">StateFlow | kotlinx.coroutines - Kotlin Programming Language Docs</a>
<a href="https://kotlinlang.org/docs/coding-conventions.html#names-for-backing-properties">Coding conventions | kotlin Docs</a>
<a href="https://everyday-develop-myself.tistory.com/344">배준형 코틀린에서 Backing Properties를 왜 사용해야 하죠? 블로그</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[ViewModel 은 구성 변경시 데이터를 어떻게 복원할까?]]></title>
            <link>https://velog.io/@last_game/Android-ViewModel-Configuration-Changes</link>
            <guid>https://velog.io/@last_game/Android-ViewModel-Configuration-Changes</guid>
            <pubDate>Sat, 25 Oct 2025 15:32:01 GMT</pubDate>
            <description><![CDATA[<h2 id="viewmodel과-구성-변경configuration-change">ViewModel과 구성 변경(Configuration Change)</h2>
<h3 id="remember-vs-remembersaveable">remember vs rememberSaveable</h3>
<p>Compose 에서 <code>remember</code>는 Composable 함수에서 <strong>상태를 기억</strong>하기 위해 사용됩니다.
하지만 <code>remember</code>로 저장한 값은 <strong>Activity가 재생성되면 사라집니다.</strong></p>
<p>즉, 화면 회전, 다크 모드 전환 등 <strong>구성 변경(Configuration Change)</strong> 이 발생하면 Activity가 다시 만들어지면서 기존 상태가 초기화되죠.</p>
<p>이때 사용하는 것이 바로 <code>rememberSaveable</code>입니다.</p>
<blockquote>
<p>rememberSaveable은 구성 변경이 일어나더라도 값을 자동으로 복원해주는 기능을 제공합니다.</p>
<p>내부적으로는 <code>SavedInstanceState</code>를 활용해 직렬화 가능한 데이터를 보존합니다.</p>
</blockquote>
<h3 id="구성-변경에-대응하는-여러-가지-방법">구성 변경에 대응하는 여러 가지 방법</h3>
<p>구성 변경 시 상태를 유지하는 방법은 여러 가지가 있습니다.
아래는 대표적인 네 가지 접근 방식이에요 👇</p>
<table>
<thead>
<tr>
<th>구분</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><strong>1. UI 상태 저장 및 복원</strong></td>
<td><code>onSaveInstanceState()</code> / <code>onRestoreInstanceState()</code>를 직접 구현해 일시적으로 상태를 저장 후 복원</td>
</tr>
<tr>
<td><strong>2. ViewModel</strong></td>
<td>UI 관련 데이터를 메모리에 보관하여 구성 변경에도 상태를 유지 (가장 권장되는 방식)</td>
</tr>
<tr>
<td><strong>3. 수동 구성 변경 처리</strong></td>
<td><code>AndroidManifest.xml</code>의 <code>android:configChanges</code> 속성을 이용해 구성 변경을 직접 처리 (<code>onConfigurationChanged()</code> 재정의)</td>
</tr>
<tr>
<td><strong>4. Compose의 rememberSaveable</strong></td>
<td>Compose 전용 상태 저장 함수로, 내부적으로 Bundle 기반 상태 복원을 자동 처리</td>
</tr>
</tbody></table>
<h3 id="viewmodel-의-역할">ViewModel 의 역할</h3>
<p>ViewModel은 단순히 비즈니스 로직을 분리하는 것뿐만 아니라,
<strong>UI 상태를 안정적으로 저장하고 유지하는 역할</strong>도 매우 중요하다고 생각합니다.</p>
<p>그렇담 ViewModel도 대표적인 구성 변경을 예방하는 방법 중 하나인데, 어떻게 데이터를 복원할까요? ViewModel은 Activity 와 독립적인 생명주기를 가지는 걸까요?</p>
<blockquote>
<p>✅ <strong><em>사용자가 기기를 회전하면 Activity 또는 Fragment가 소멸되고
다시 생성되지만 ViewModel은 파괴되지 않아 데이터가 그대로 유지되도록 보장합니다.</em></strong></p>
</blockquote>
<h2 id="🔍-viewmodel은-activity와-독립적인-생명주기를-가질까">🔍 ViewModel은 Activity와 독립적인 생명주기를 가질까?</h2>
<p>이를 이해하려면 먼저 ViewModel이 <strong>어떻게 생성되고 저장되는지</strong> 알아야 합니다.</p>
<h3 id="viewmodel-생성-방식">ViewModel 생성 방식</h3>
<pre><code class="language-kotlin">// 1️⃣ 기본 ViewModelProvider 사용
val viewModel = ViewModelProvider(this).get(MainViewModel::class.java)

// 2️⃣ Kotlin delegate 사용 (간결한 KTX 방식)
val viewModel by viewModels&lt;MainViewModel&gt;()

// 3️⃣ Compose 환경에서 사용
val viewModel: MainViewModel = viewModel()</code></pre>
<h4 id="1️⃣-viewmodelprovider-방식">1️⃣ ViewModelProvider 방식</h4>
<p>가장 기본적인 방식으로, 직접 <code>ViewModelProvider</code>를 통해 ViewModel을 가져옵니다.
명시적으로 <code>ViewModelStoreOwner</code>(예: Activity, Fragment)를 지정할 수 있습니다.</p>
<h4 id="2️⃣-kotlin-delegate-by-viewmodels">2️⃣ Kotlin Delegate (<code>by viewModels</code>)</h4>
<p>KTX 확장을 활용한 간결한 방식으로, 내부적으로는 <code>ViewModelProvider</code>와 동일하게 동작합니다.</p>
<ul>
<li><p><code>factoryProducer</code>가 있으면 해당 팩토리를, 없으면 <code>defaultViewModelProviderFactory</code> 사용</p>
</li>
<li><p><code>extrasProducer</code>가 있으면 호출하여 <code>CreationExtras</code> 생성</p>
</li>
<li><p>결과적으로 <strong><code>ViewModelLazy</code> 인스턴스</strong>를 반환 (<code>Lazy&lt;VM&gt;</code> 형태로 <code>by</code> 사용 가능)</p>
</li>
<li><p>ViewModel은 <strong>처음 접근 시점에 생성 (Lazy Initialization)</strong> 됩니다.</p>
</li>
<li><p>내부에서 동일한 방식으로 호출됩니다.</p>
<p>  <code>ViewModelProvider(viewModelStore, factory, extras).get(VM::class.java)</code> </p>
</li>
</ul>
<p><img src="https://velog.velcdn.com/images/last_game/post/9f6fb1e8-a68e-4c10-ac42-87f8beed8869/image.png" alt=""></p>
<h4 id="3️⃣-compose에서의-생성">3️⃣ Compose에서의 생성</h4>
<p>Compose 환경에서는 별도의 Delegate 없이 <code>androidx.lifecycle.viewmodel.compose.viewModel()</code>을 사용합니다.</p>
<ul>
<li><code>LocalViewModelStoreOwner</code>를 자동으로 참조</li>
<li>동일한 스코프 내에서 구성 변경 시에도 ViewModel이 재사용</li>
<li><code>key</code>, <code>factory</code>, <code>owner</code> 등을 직접 지정해 범위를 제어할 수도 있습니다.</li>
</ul>
<pre><code class="language-kotlin">@Composable
fun MainScreen() {
    val viewModel: MainViewModel = viewModel()
}</code></pre>
<h3 id="viewmodel-생성-과정">ViewModel 생성 과정</h3>
<p>ViewModel이 실제로 어떻게 만들어지는지 한 번 따라가볼게요 👇</p>
<p><img src="https://velog.velcdn.com/images/last_game/post/184332aa-9dec-4b8d-8d66-12854254e4a8/image.png" alt=""></p>
<ol>
<li><code>ViewModelProvider</code>를 통해 ViewModel 인스턴스를 요청</li>
<li>내부에서 <code>ViewModelStoreOwner</code>를 참조하여 <code>ViewModelStore</code>를 가져옴</li>
<li><code>ViewModelStore</code>에 기존 인스턴스가 있는지 확인</li>
<li>없으면 <code>Factory</code>를 통해 새 ViewModel 생성</li>
<li>생성된 ViewModel 인스턴스를 <code>ViewModelStore</code>에 저장</li>
</ol>
<p>이후 같은 Owner 내에서 다시 요청하더라도 <strong>동일한 인스턴스</strong>를 반환합니다.</p>
<h3 id="viewmodelstore--viewmodelstoreowner">ViewModelStore &amp; ViewModelStoreOwner</h3>
<h4 id="viewmodelstore">ViewModelStore</h4>
<p><img src="https://velog.velcdn.com/images/last_game/post/bc1cb3e2-347c-4b63-aed9-1b6ae98ff8ba/image.png" alt=""></p>
<ul>
<li>ViewModel 인스턴스를 저장하는 컨테이너 역할</li>
<li>내부적으로 <code>Map</code> 구조로 관리됩니다.</li>
</ul>
<h4 id="viewmodelstoreowner">ViewModelStoreOwner</h4>
<ul>
<li><p><code>ViewModelStore</code>를 소유하고 관리하는 주체입니다.</p>
</li>
<li><p>대표적으로 <code>Activity</code>, <code>Fragment</code>, <code>NavBackStackEntry</code>가 이에 해당합니다.</p>
<p>  실제로 ViewModelStoreOwner 를 상속 받고 있어요.</p>
</li>
<li><p>각 Owner가 파괴될 때 <code>ViewModelStore</code>의 <code>clear()</code>가 호출되어 ViewModel이 정리됩니다.</p>
</li>
</ul>
<blockquote>
<p>✅ Compose의 경우, NavBackStackEntry가 네비게이션 스택에서 제거될 때 ViewModel이 함께 해제됩니다.</p>
</blockquote>
<br/>

<h2 id="viewmodel의-생명주기">ViewModel의 생명주기</h2>
<p><img src="https://velog.velcdn.com/images/last_game/post/d31e92b0-7350-455f-805f-29137c5c2dcf/image.png" alt=""></p>
<p>ViewModel은 <strong>Activity나 Fragment의 생명주기와 1:1로 묶이지 않습니다.</strong>
대신 <code>ViewModelStoreOwner</code>에 의해 관리되며,
기본적으로 Activity가 완전히 종료(<code>onDestroy</code>)될 때 함께 제거됩니다.</p>
<p>그러면 이러한 질문이 생길 수도 있습니다.
configuration change 가 일어날때 destory 가 발생했다가 다시 create 되면 ViewModel 도 제거되는거 아닌가요??</p>
<blockquote>
<p><em>ViewModel은 Activity나 Fragment의 생명주기와 직접적으로 연결되어 있지 않습니다.
대신, LifecycleEventObserver를 통해 Activity의 라이프사이클 이벤트를 감지하며
“구성 변경 중인지”를 검사한 뒤, ViewModelStore를 clear할지 결정합니다.</em></p>
</blockquote>
</details>


<h3 id="내부-동작">내부 동작</h3>
<p>내부 동작을 통해 어떻게 처리되고 있는지 보겠습니다.</p>
<p>(1) LifecycleEventObserver 내부 동작</p>
<pre><code class="language-kotlin">public class ComponentActivity {

    getLifecycle().addObserver(new LifecycleEventObserver() {
        @Override
        public void onStateChanged(@NonNull LifecycleOwner source,
                                   @NonNull Lifecycle.Event event) {
            if (event == Lifecycle.Event.ON_DESTROY) {
                // Context 정리
                mContextAwareHelper.clearAvailableContext();

                // 구성 변경 중이 아니라면 ViewModelStore 제거
                if (!isChangingConfigurations()) {
                    getViewModelStore().clear();
                }

                mReportFullyDrawnExecutor.activityDestroyed();
            }
        }
    });
}</code></pre>
<p>이 코드에서 핵심은 구성 변경 중(<code>isChangingConfigurations() == true</code>)이면 <code>ViewModelStore.clear()</code>를 호출하지 않는다는 점입니다.
→ 덕분에 ViewModel은 구성 변경 중에 ViewModel 이 삭제가 되지 않습니다.</p>
<p>(2) onRetainNonConfigurationInstance()에서 ViewModelStore 보존</p>
<p>한가지 과정이 더 남았는데요. Android는 Activity가 파괴되기 전에
<code>onRetainNonConfigurationInstance()</code>를 통해 <code>NonConfigurationInstances</code> 라는 곳에 <code>ViewModelStore</code>를 <strong>임시 보관</strong>합니다. 이는 시스템 메모리에 보관해 유지하는 방식입니다.</p>
<p>이후 Activity 가 다시 그려질 때 <code>ensureViewModelStore()</code> 를 호출해 <code>ViewModelStore</code> 를 복구합니다.</p>
<p><img src="https://velog.velcdn.com/images/last_game/post/23c5a41c-297c-4bf6-879e-a69458e5db16/image.png" alt=""></p>
<pre><code class="language-kotlin">// Activity 에서 자동적으로 화면 회전 동안 ViewModel Store를 보유합니다.
public final Object onRetainNonConfigurationInstance() {
    ViewModelStore viewModelStore = mViewModelStore;
    if (viewModelStore == null) {
        NonConfigurationInstances nc = getLastNonConfigurationInstance();
        if (nc != null) {
            viewModelStore = nc.viewModelStore;
        }
    }

    if (viewModelStore == null) {
        return null; // Nothing to retain
    }

    // Package ViewModelStore for Android to preserve
    NonConfigurationInstances nci = new NonConfigurationInstances();
    nci.viewModelStore = viewModelStore; // This survives destruction!
    return nci;
}

// 회전후 자동적으로 ViewModelStore 를 복구 합니다. 
void ensureViewModelStore() {
    if (mViewModelStore == null) {
        NonConfigurationInstances nc = getLastNonConfigurationInstance();
        if (nc != null) {
            mViewModelStore = nc.viewModelStore; // Restoration
        }
        if (mViewModelStore == null) {
            mViewModelStore = new ViewModelStore();
        }
    }
}</code></pre>
<h3 id="예시-화면-회전-시-viewmodel-유지-과정">예시: 화면 회전 시 ViewModel 유지 과정</h3>
<pre><code class="language-kotlin">// === 앱 처음 실행 ===
class MainActivity : AppCompatActivity() {

    lateinit var viewModel: UserViewModel

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        // ViewModelProvider를 통해 UserViewModel 가져오기
        // 처음 실행 시: ViewModelStore에 없음 → 새 인스턴스 생성
        viewModel = ViewModelProvider(this)[UserViewModel::class.java]

                // 사용자가 데이터 입력
                viewModel.counter = 5
                viewModel.userInput = &quot;Hello&quot;
    }
}

// === 화면 회전 발생 ===
// 1. Activity의 onRetainNonConfigurationInstance() 호출
//    -&gt; ViewModelStore를 시스템 메모리에 보관
// 2. 기존 Activity 종료, 새 Activity 생성

class MainActivity : AppCompatActivity() {

    lateinit var viewModel: UserViewModel

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        // ViewModelProvider를 통해 UserViewModel 가져오기
        // 화면 회전 시: ViewModelStore에 기존 인스턴스 존재 → 새로 생성하지 않고 기존 인스턴스 반환
        viewModel = ViewModelProvider(this)[UserViewModel::class.java]

        // 이전 Activity에서 입력한 데이터 유지
        // viewModel.counter == 5
        // viewModel.userInput == &quot;Hello&quot;
    }
}
</code></pre>
<p>ViewModel의 생명주기는 ViewModelStoreOwner(Activity, Fragment 또는 기타 생명주기 인식 컴포넌트일 수 있음)에 연결됩니다. ViewModel은 ViewModelStoreOwner의 범위 내에서 존재하며, <code>onRetainNonConfigurationInstance()</code>를 통해 화면 회전과 같은 구성 변경 시에도 데이터와 상태가 유지되도록 보장합니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[SOPT] Android Part 회고]]></title>
            <link>https://velog.io/@last_game/SOPT-Android-Part-%ED%9A%8C%EA%B3%A0</link>
            <guid>https://velog.io/@last_game/SOPT-Android-Part-%ED%9A%8C%EA%B3%A0</guid>
            <pubDate>Tue, 29 Jul 2025 01:23:57 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>SOPT 는 <em>&quot;Shout Our Passion Together&quot;</em> 라는 슬로건을 가진 대학생 연합 IT 벤처 창업 동아리 입니다. </p>
</blockquote>
<h3 id="sopt-를-시작하게-된-계기">SOPT 를 시작하게 된 계기</h3>
<p><strong>SOPT</strong> 를 처음 알게 된 건 <a href="https://devlog-wjdrbs96.tistory.com/240">학교 선배의 블로그</a>를 보면서 처음 알게 되었는데요..! 제가 본받고 싶은 분이 참여 했던 동아리기도 하고, 다양한 파트의 사람들과 협업을 해보고 싶은 생각이 컸기에 <strong>SOPT</strong> 를 지원하게 되었습니다!
면접보다 <strong>서류</strong>에서 내가 준비할 수 있는 부분을 충분히 보여줄 수 있다고 생각했기에 서류를 매우 꼼꼼히 준비했고 다른 합격 후기 블로그들을 보면서 어떤식으로 준비해 가면 좋은지 나름의 꿀팁(?)을 많이 적용해 보려고 했었습니다.</p>
<p>다행히 AT SOPT 36기 서류합격을 하고, 서류합격발표 이후에 면접이 그주 주말에 바로 있었기에 짧은 시간 준비해서 면접을 보러갔습니다.
면접은 언제 봐도 정말정말 떨리고 어려운 것 같습니다. 면접은 <strong>임원진 면접</strong>과 <strong>파트장 면접</strong> 2가지로 나뉘어서 진행되었습니다. 운영진 면접에서 준비해온 내용을 잘 이야기하지 못하고.. 많이 떨면서 대답을 해서 &quot;그냥 마음 편하게 생각하자. 이미 엎질러진 물이야.&quot;라는 마음으로 파트장 면접에 들어갔습니다. 처음 파트장님께서도 본인도 처음 sopt 면접을 볼때 임원진 면접을 잘하지 못하였는데 파트 면접을 잘해서 들어왔다라고 편하게 이야기 해주셔서 마음을 내려놓고 편한 마음으로 파트 면접을 잘 마무리하여 최종적으로 합격할 수 있었습니다.</p>
<p align="center">
    <img src="https://velog.velcdn.com/images/last_game/post/25293509-4af9-4136-bcef-197eecd543ef/image.png" width="500"/>
</p>

<p>SOPT 를 하면서 Jetpack Compose 에 대한 개발은 처음이어서 낯설었지만 기존의 xml 방식보다 훨씬 직관적이면서 재미있었습니다. 학교 일정과 여러가지로 겹쳐 배운 내용을 적용해보기에 급급했지만 그럼에도 8번의 세미나와 합동세미나를 통해 많은 내용을 배울 수 있었습니다.</p>
<h3 id="5주간의-협업-앱잼">5주간의 협업, 앱잼</h3>
<p>SOPT에 들어오면서 가장 기대되었던 활동인 앱잼을 참여하였습니다. 앱잼은 기획, 디자인, 서버, 안드, iOS 파트원들이 모여 한 팀이 되어 하나의 앱을 5주동안 개발합니다. 5주라는 기간이 길게 느껴질 수도 있지만, 실제로는 기획과 디자인이 완료된 후 본격적인 개발에 돌입하기 때문에, 체감상 개발 기간은 약 3주로 빠듯했습니다.</p>
<h4 id="📝-kpt-회고">📝 KPT 회고</h4>
<p>5주간 좋은 팀원들을 만나 잘 마무리할수 있었습니다. <strong>&quot;봉투백서&quot;</strong> 라는 맞춤형 경조사비 가이드 앱을 개발을 진행하였고, 팀원들의 도움 덕분에 잘 마무리 할 수 있었습니다.</p>
<p align="center">
 <img src="https://velog.velcdn.com/images/last_game/post/cc3a5e51-2488-46f0-bbb4-f4b83a0c2d49/image.png" width="150"/>
</p>

<br/>

<p><strong>앞으로 계속 유지할 것(Keep):</strong>
개발을 진행하면서 세미나에서 다루지 않았던 nested navigation, gradient 적용 등 커스텀 요소들을 다룰 일이 많았습니다. </p>
<p>또한 커스텀 뷰, 디자인시스템을 적용한 컴포넌트를 직접 구현하고 내용을 정리하는 과정에서 많은 부분 배웠는데요. 앞으로도 직접 구현을 하면서 배운 내용을 정리하는 습관을 가지고 팀원들과 소통하는 분위기를 이어가고 싶습니다. </p>
<p><strong>이번에 발생한 문제(Problem):</strong><br>후반부 일정이 예상보다 빠듯하게 흘러가면서 전체 테스트나 예외 케이스에 대한 대응이 부족했던 점이 아쉬웠습니다.
또한 코드 리뷰에 대해 깊이 있게 고민하지 못한 것 같고, 파트장님의 리뷰 질문에 스스로 코드의 동작 원리를 <strong>명확히 설명하지 못한</strong> 순간도 기억에 남습니다.</p>
<p>질문 예시들</p>
<ul>
<li><code>collectAsStateWithLifecycle</code> 의 역할 및 내부 구조</li>
<li><code>gradient</code> 의 구현 방식, 어떻게 그려지는지 등등</li>
</ul>
<p><strong>다음에 새롭게 시도할 것(Try):</strong>
앞으로 현재 부족했던 개념에 대한 학습을 챙기고, 특정 기능을 구현을 하면서
왜 이렇게 만들었는지, 어떤 대안이 있었는지를 정리하여 더 깊은 이해와 설명 능력을 키워보는 것도 도전해 보고 싶습니다.
추가로 담당 개발이 아닌 부분인 카카오로그인, 로띠에 대해서도 학습을 진행하여 구글로그인까지 토이프로젝트에 적용해 볼까 생각중입니다.</p>
<br/>

<h3 id="💬-마무리하며">💬 마무리하며</h3>
<p>앱잼을 통해 정말 좋은 팀원들을 만나서 행복하게 개발을 할 수 있었습니다.
앞으로도 이 소중한 관계를 잘 이어가기 위해, 성장해 나가야겠다고 다짐했습니다.</p>
<p>팀원들과 함께했기에 잘 마무리 할수 있었다고 생각하고 1차 스프린트에서도 더 성장해 나가면 좋겠습니다 :)</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[알고리즘] N-Queens 문제]]></title>
            <link>https://velog.io/@last_game/%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98-N-Queens-%EB%AC%B8%EC%A0%9C</link>
            <guid>https://velog.io/@last_game/%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98-N-Queens-%EB%AC%B8%EC%A0%9C</guid>
            <pubDate>Sun, 21 Jan 2024 03:30:12 GMT</pubDate>
            <description><![CDATA[<p><a href="https://www.acmicpc.net/problem/9663">문제링크</a></p>
<p>20분 문제 풀이 실패한 문제. 퀸을 배치할 수 있는 모든 경우의 수를 생각하려고 하니 어려웠다.</p>
<h2 id="🤔-thinking">🤔 Thinking</h2>
<p>핵심이 되는 것은 퀸의 특성에 있다. </p>
<blockquote>
<p>퀸은 가로, 세로, 대각선을 이동할 수 있기에 매번 새로 퀸을 배치 할 때마다 조건을 확인한다.</p>
</blockquote>
<p>체스판을 2차원 배열로 생각하면 같은 행에는 퀸은 한개만 배치가 가능하다.
<img src="https://velog.velcdn.com/images/last_game/post/9a330632-4f6a-480d-a6d8-855ec394205b/image.png" alt=""></p>
<p>따라서 행을 기준으로 하나의 열을 선택하면 다음 행을 찾아 비교한다.</p>
<p><img src="https://velog.velcdn.com/images/last_game/post/92f2caf0-b857-4d0f-b429-a9bd8c855d0c/image.png" alt=""></p>
<hr>
<p>만약 행마다 퀸이 배치되어 있어야하기 때문에 <strong>행에서 조건을 전부 만족하는 경우가 없는 경우 답이 될 수 없다(non-promising)</strong>. 따라서 다음 행을 이어서 탐색하지 않고 백트래킹을 수행하여 다음 가능한 행을 선택한다. </p>
<p><img src="https://velog.velcdn.com/images/last_game/post/a1d91d11-985c-4c5f-8a89-65a15e8cc5e8/image.png" alt=""></p>
<p>위와 같은 경우 3번째 행의 탐색을 종료하고 2번째행을 이어서 탐색한다.</p>
<p>1번째 행의 1번 열부터 n번열까지 선택을 했을 때 다음 행의 1번열부터 n번열까지 탐색을 하면서 카운트한다.</p>
<h2 id="코드">코드</h2>
<p>위의 예시와 같이 행을 기준으로 코드를 작성한다.
<code>colList[row] = i</code> 에서 <strong><code>colList</code></strong>는 <strong><code>row</code></strong> 행을 기준으로 했을 때 열의 위치 <strong><code>i</code></strong> 를 의미한다.
<img src="https://velog.velcdn.com/images/last_game/post/71565524-542f-4e58-a80a-813157051b88/image.png" alt=""></p>
<p>여기서 구현하기 까다로운 것은 조건을 확인하는 것이다.
우선 행은 이미 한개의 기물만 배치하고 있기 때문에 확인해야할 조건은 다음과 같다.</p>
<blockquote>
<ul>
<li>모든 기물이 같은 열에 있으면 안된다.</li>
<li>모든 기물이 대각선으로 만나면 안된다.</li>
</ul>
</blockquote>
<p>** 1. 모든 기물이 같은 열에 있으면 안된다. **
같은 열에 있는 지 확인하는 것은 단순히 배치하고자 하는 기물의 열과 기존에 배치한 기물의 열이 같은지만 확인하면 된다.
colList 의 값이 열에 해당하기 때문에 전체 반복문을 돌면서 확인한다.</p>
<p>** 2. 모든 기물이 대각선으로 만나면 안된다. **
대각선으로 확인하는 과정은 <strong>x 증가량과 y 증가량이 같은 경우</strong>로 생각할 수 있다.
예를 들어 <code>(1, 1)</code> 과 <code>(2, 2)</code> 는 같은 대각선 상에 존재한다. <code>(1, 2)</code> 와 <code>(3, 4)</code> 는 x 증가량과 y 증가량이 <code>2</code>로 같은 경우여서 대각선에 위치한다. </p>
<p>거꾸로 (1, 2) 와 (2, 1) 은 x 증가량은 1 , y 증가량은 -1 이지만 절댓값은 같은 것을 확인할 수 있다. 
다른 경우에서도 마찬가지로 <strong>x의 변화량과 y의 변화량이 같은 경우</strong> 같은 대각선 상에 위치한다.</p>
<pre><code class="language-python">ans = 0

def promising(row, colList):
    for i in range(row):    # 현재까지의 모든 행을 확인하면서 조건체킹
        if (colList[row] == colList[i]  # 열 체크
            or abs(colList[row] - colList[i]) == abs(row-i)):   # 대각선 체크
            return False
    return True

def n_queens(n, row, colList):
    global ans
    if row == n:    # 모든 행에 기물이 배치 된 경우 카운트
        ans += 1
        return

    for i in range(n):
        colList[row] = i    # row 행에 i 번째열에 기물 배치
        if promising(row, colList):        # 기물 배치가 가능하면 다음 행 탐색
            n_queens(n, row+1, colList)

n = int(input())
colList = [0] * n
n_queens(n, 0, colList)
print(ans)</code></pre>
<br/>
<br/>
<br/>
<br/>
<br/>
<br/>
<br/>

<h2 id="📕-참고-문헌">📕 참고 문헌</h2>
<p>** 다음의 링크를 참고했습니다.**
<a href="https://www.youtube.com/watch?v=z4wKvYdd6wM">파이썬으로 배우는 알고리즘 기초: 19. n-Queens 문제의 구현</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Kotlin] Nothing]]></title>
            <link>https://velog.io/@last_game/Kotlin-Nothing</link>
            <guid>https://velog.io/@last_game/Kotlin-Nothing</guid>
            <pubDate>Thu, 18 Jan 2024 08:30:10 GMT</pubDate>
            <description><![CDATA[<p>Kotlin에서 <code>Nothing</code> 은 다음과 같이 정의되어 있다.</p>
<blockquote>
<p><strong>Nothing has no instances. You can use Nothing to represent &quot;a value that never exists&quot;: for example, if a function has the return type of Nothing, it means that it never returns (always throws an exception).</strong></p>
<pre><code class="language-kotlin">public class Nothing private constructor()</code></pre>
</blockquote>
<p><code>Nothing</code> 은 <code>private 생성자</code> 로 생성자에 접근 할 수 없어 인스턴스를 가지지 않는다.
<code>Nothing</code> 은 어떠한 값이 존재하지 않는다.</p>
<h2 id="nothing의-쓰임">Nothing의 쓰임</h2>
<p><code>Nothing</code> 은 주로 함수의 리턴 타입으로 사용한다.</p>
<h3 id="1-반환하지-않는-함수">1. 반환하지 않는 함수</h3>
<p><code>Nothing</code> 을 <strong>무한 루프</strong>나 <strong>항상 예외를 발생시키는 함수</strong>에서 리턴 타입으로 사용하면 함수를 <code>return(종료)</code> 하지 않는다.</p>
<p>예를 들어 다음과 같은 코드를 살펴보자.</p>
<pre><code class="language-kotlin">fun main(){
    infiniteLoop()
    println(&quot;end&quot;) // Unreachable code
}

fun infiniteLoop(): Nothing {
    while (true) {
        println(&quot;Nothing is not always returned.&quot;)
    }
}</code></pre>
<p>해당 함수는 계속 반복문을 돌기 때문에 함수를 빠져나오지 못할 것이다. <code>Noting</code> 을 반환형으로 선언 하면 이를 파악한 컴파일러는 다음 실행 부분인 <code>println(&quot;end&quot;)</code> 이 실행될 수 없음을 파악하고 <strong>Unreachable code</strong> 경고를 띄운다.</p>
<p>대표적으로 <code>TODO</code> 함수가 <code>Nothing</code> 반환형을 가지는 함수인데, 해당 코드를 구현하지 않으면 경고를 발생시켜 작업이 남아있음을 알릴 때 사용한다.</p>
<h3 id="2-모든-클래스의-자식-클래스">2. 모든 클래스의 자식 클래스</h3>
<p><code>Noting</code> 은 모든 클래스(타입)의 자식 클래스(타입)이다.</p>
<p><em>(반대로, Any 는 최상위 루트 타입으로 모든 클래스는 <code>Any</code> 를 상속 받는다.)</em></p>
<p>엘비스 연산자를 이용한 일반적인 예를 보자.</p>
<pre><code class="language-kotlin">    val nullableString: String ? = null
    val value : String = nullableString ?: 30 // 30은 오류</code></pre>
<p>해당 코드를 보면 <code>value</code> 는 <strong>String</strong> 타입으로 지정 되어 있어서 <strong>Int</strong> 타입인 <code>30</code> 은 에러가 난다.</p>
</br>

<p><strong>Nothing</strong> 타입인 <code>TODO()</code> 는 <code>value</code> 와 연산을 하여도 문제없이 컴파일이 가능하다.
<em>(이해를 위한 코드이며 실행시, TODO() 에서 NotImplementedError 를 반환한다.)</em></p>
<pre><code class="language-kotlin">    val nullableString: String ? = null
    val value : String = nullableString ?: TODO()</code></pre>
<h3 id="3-null-object-패턴">3. Null Object 패턴</h3>
<p>Kotlin 에서는 null-safety를 지원해서 Null Object 패턴이 필요없다고 생각할 수 있다. 여기서 이야기하는 Null Object 패턴은 조금 다르다.</p>
<p>함수 보면서 Null Object 패턴을 알아보자</p>
<pre><code class="language-kotlin">fun deleteFiles(files: List&lt;File&gt;? = null) {
    if (files != null) files.forEach { it.delete() }
}</code></pre>
<p>다음은 파일 리스트의 값을 전부 삭제하는 함수이다.
파일리스트가 값이 <code>null</code> 이 아닌지 확인해주기 위해 조건식으로 체크한다.
<strong>이는 함수내에서 files 를 사용할 때 여러곳에서 <code>null</code> 을 확인해야한다.</strong></p>
<p>이를 방지하기 위해 <code>Nothing</code> 을 이용한 <code>emptyList()</code> 메소드를 사용하면 간단하게 <code>null</code> 체크를 할 필요가 없다.</p>
<p><strong>기본 라이브러리 함수</strong></p>
<pre><code class="language-kotlin">// This function is already defined in the Kotlin standard library
fun emptyList() = object : List&lt;Nothing&gt; {
    override fun iterator(): Iterator&lt;Nothing&gt; = EmptyIterator
    ...
}</code></pre>
<p><strong>사용</strong></p>
<pre><code class="language-kotlin">fun deleteFiles(files: List&lt;File&gt; = emptyList()) {
    files.forEach { it.delete() }
}</code></pre>
<br/>
<br/>
<br/>
<br/>
<br/>
<br/>

<h2 id="📕-참고-문헌">📕 참고 문헌</h2>
<p><strong>다음의 링크를 참고했습니다.</strong></p>
<ul>
<li><a href="https://kotlinlang.org/docs/exceptions.html#the-nothing-type">https://kotlinlang.org/docs/exceptions.html#the-nothing-type</a></li>
<li><a href="https://readystory.tistory.com/143">https://readystory.tistory.com/143</a></li>
<li><a href="https://itnext.io/any-unit-nothing-and-all-their-friends-e39613b48235">https://itnext.io/any-unit-nothing-and-all-their-friends-e39613b48235</a></li>
<li><a href="https://stackoverflow.com/a/64742576/23145558">https://stackoverflow.com/a/64742576/23145558</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Kotlin] Void vs Unit]]></title>
            <link>https://velog.io/@last_game/Kotlin-Void-vs-Unit</link>
            <guid>https://velog.io/@last_game/Kotlin-Void-vs-Unit</guid>
            <pubDate>Wed, 17 Jan 2024 10:10:00 GMT</pubDate>
            <description><![CDATA[<h2 id="기본-타입basic-types">기본 타입(Basic types)</h2>
<p>Unit을 살펴보기 전에 코틀린에서의 기본 타입에 대해 알아보자.</p>
<blockquote>
<p>💡 자바와 다르게 코틀린의 모든 변수들은 객체로 정의된다.</p>
</blockquote>
<p>기본 타입에는 <code>Numbers(Byte, Int 등)</code>, <code>Unsigned integer(UByte, UInt 등)</code>, <code>Boolean</code>, <code>Char</code>, <code>String</code>, <code>Array</code> 가 있다.</p>
<p>자바에서 원시 타입(primitive type)을 사용했던 반면에 코틀린에서는 객체로 기본 타입을 표현한다. 
주의할 사항은 코드 상에서 객체로 존재할 뿐 코틀린의 기본 타입은 컴파일 시 자바의 <strong>primitive(원시) type 또는 wrapper 타입</strong>으로 자동 변환된다.</p>
<h2 id="void">Void</h2>
<p>자바에서 <code>void</code> 와 <code>Void</code>는 차이가 있다.</p>
<p><code>void</code>(소문자) 는 <strong>원시타입</strong>으로 <strong>반환 값이 존재하지 않음</strong>을 이야기하고 <code>Void</code> 는 <strong>객체타입</strong>으로 <strong>객체가 존재하지 않음</strong>을 말한다.</p>
<p>따라서 <code>Void</code> 에는 리턴 값으로 <code>null</code> 을 반환 해야 한다.</p>
<h2 id="unit">Unit?</h2>
<p><a href="https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-unit/">kotlin 공식문서</a> 에는 다음과 같이 설명한다.</p>
<blockquote>
<p><code>Unit</code> 은 자바에서 <code>void</code> 와 대응되는 타입(객체)이다.</p>
</blockquote>
<p>참고) <code>Unit</code>은 싱글톤 인스턴스이다.</p>
<p>코틀린의 기본 함수를 보자.</p>
<pre><code class="language-kotlin">fun printHello(): Unit {
    println(&quot;Hello World!&quot;)
}</code></pre>
<p>아무런 값을 반환하지 않을 때는 <code>Unit</code> 타입으로 지정한다.</p>
<br/>

<p>반환타입이 Unit의 경우는 다음과 같이 생략가능하다.</p>
<pre><code class="language-kotlin">fun printHello() {
    println(&quot;Hello World!&quot;)
}</code></pre>
<h3 id="왜-unit을-사용하는-가">왜 Unit을 사용하는 가?</h3>
<p>그렇다면 코틀린에서는 왜 <code>Unit</code> 을 사용하는 것 일까?</p>
<p>앞서 말했듯이 코틀린에서는 변수의 기본타입을 전부 클래스로 감싸 표현하고 있다.</p>
<p>기본적으로 자바에서 <strong>참조형(클래스)으로 아무값도 반환하지 않기 위해</strong>서는 앞서 말한 <code>Void</code>를 사용해야 할 것이다.</p>
<p>하지만 이렇게 되면 <strong><code>null</code>을 반환해야 하기 때문에</strong> <code>void</code>와 같이 사용하기 어렵다. 따라서 kotlin 에서는 <code>Unit</code> 이라는 객체 타입을 지원한다.</p>
<br/>
<br/>
<br/>
<br/>
<br/>
<br/>

<h2 id="📕-참고-문헌">📕 참고 문헌</h2>
<p><strong>다음의 링크를 참고했습니다.</strong></p>
<ul>
<li><a href="https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-unit/">https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-unit/</a></li>
<li><a href="https://kotlinlang.org/docs/functions.html#single-expression-functions">https://kotlinlang.org/docs/functions.html#single-expression-functions</a></li>
<li><a href="https://www.baeldung.com/kotlin/void-type">https://www.baeldung.com/kotlin/void-type</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[알고리즘] 최단 경로 문제(Shortest path problem)(1) - 다익스트라 알고리즘]]></title>
            <link>https://velog.io/@last_game/%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98-%EC%B5%9C%EB%8B%A8-%EA%B2%BD%EB%A1%9C-%EB%AC%B8%EC%A0%9CShortest-path-problem</link>
            <guid>https://velog.io/@last_game/%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98-%EC%B5%9C%EB%8B%A8-%EA%B2%BD%EB%A1%9C-%EB%AC%B8%EC%A0%9CShortest-path-problem</guid>
            <pubDate>Tue, 16 Jan 2024 10:43:29 GMT</pubDate>
            <description><![CDATA[<h2 id="최단-경로-문제shortest-path-problem">최단 경로 문제(Shortest path problem)</h2>
<p>최단 경로 문제는 그래프에서 <strong>시작 정점에서 다른 모든 정점까지의 간선 가중치의 합이 최소가 되는 경로</strong>를 찾는 문제이다.</p>
<p>최단 경로 알고리즘은 우리가 흔히 아는 <strong>네비게이션</strong>에 사용된다. 현재 위치에서 특정 위치까지 가는 거리를 계산하여 가장 빠른 경로를 추천해 줄 수 있다.</p>
<p>최단경로 알고리즘에는 여러 알고리즘이 있지만 대표적으로 3가지만 알아보자.</p>
<blockquote>
<p><strong>1.</strong> <a href="https://velog.io/@last_game/%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98-%EC%B5%9C%EB%8B%A8-%EA%B2%BD%EB%A1%9C-%EB%AC%B8%EC%A0%9CShortest-path-problem"><strong>다익스트라 알고리즘</strong>(음의 가중치를 포함하지 않는 최단 경로)</a>
<strong>2. 벨만-포드 알고리즘</strong>(음의 가중치를 허용하는 최단 경로)
<strong>3. 플로이드-워샬 알고리즘</strong>(모든 쌍에 대한 최단 경로)</p>
</blockquote>
<br/>

<h2 id="다익스트라dijkstra-알고리즘">다익스트라(Dijkstra) 알고리즘</h2>
<p><strong>음이 아닌 가중치</strong>가 있는 그래프를 기준으로 한다. <strong>한 정점에서 모든 정점에 이르는 최단 경로</strong>를 모두 구하는 알고리즘이다.</p>
<blockquote>
<p>다익스트라 알고리즘의 핵심은 <strong>시작 정점이 <code>s</code></strong>, <strong>도착 정점이 <code>w</code></strong> ,  <code>s</code> 와 <code>w</code> 가 <strong>인접한 정점을 <code>u</code></strong> 라고할 때 
<strong>s-&gt;w</strong> 로 바로 가는 거리와 <strong>s-&gt;u-&gt;w</strong> 로 거쳐서 가는 거리를 비교해 작은 거리를 저장한다.</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/last_game/post/0380daef-7d57-4887-9180-7dc969acd765/image.png" alt=""></p>
<p>그림을 보면 <strong>s-&gt;v</strong> 의 가중치는 6 이지만 <strong>s-&gt;u-&gt;v</strong> 는 5로 더 작은 가중치를 가진다.</p>
<h3 id="단계별-과정">단계별 과정</h3>
<p>예시를 통해 좀 더 살펴보자.</p>
<p><img src="https://velog.velcdn.com/images/last_game/post/94b5d852-849e-49c5-95d3-5f05240c9f74/image.png" alt=""></p>
<p>다음과 같은 그래프가 있다고 할 때 <code>A</code>에서 시작해서 모든 정점에 이르는 최단 경로를 구하는 과정을 보자. 전체 최단 경로의 값은 <code>distance</code> 배열에 저장한다.</p>
<p>초기 상태는 전체 <code>distance</code> 의 값을 무한대로 정의한다.</p>
<p>** 1. A 정점 방문 **
<strong>A 정점</strong> 방문시에 <strong>정점 A</strong>와 인접한 모든 정점의 <code>distance</code> 값을 기존 가중치 값으로 초기화 한다.
<img src="https://velog.velcdn.com/images/last_game/post/97eeb797-1697-4efc-95e8-8ea2aeea0fb2/image.png" alt=""></p>
<p>** 2. C 정점 방문 **
<code>distance</code> 값 중에서 가장 작은 가중치 값을 가지고 있는 ** 정점 C ** 를 방문한다.</p>
<p>** C ** 와 인접 한 정점 ** B ** , ** E ** , ** D ** 로 이동하는 경로를 살펴보자.
** 정점 C ** 방문시 ** A-&gt;C-&gt;B ** 로 가는 경로는 ** 8 ** 로 ** A-&gt;B ** 로 가는 경로인 ** 7 ** 이 더 작기 때문에 가지 않는다.
반대로 ** A-&gt;C-&gt;D ** 로 가는 경로는 ** 9 ** 이고 ** A-&gt;D ** 로 가는 경로는 10 이기 때문에 ** C-&gt;D ** 의 경로로 이동한다.
<img src="https://velog.velcdn.com/images/last_game/post/59f60ed3-16fe-4667-8d32-0472448aeb3b/image.png" alt=""></p>
<p>** 3. 나머지 정점 **
나머지 정점들도 위와 같은 방식으로 방문을 수행한다. 
결과를 보면 ** A-&gt;C-&gt;B-&gt;D-&gt;E-&gt;F ** 순으로 방문한다. 값이 같은 경우에는 로직에 따라 다르다.
<img src="https://velog.velcdn.com/images/last_game/post/a511ca4e-bb47-4073-916c-c6f8280017b9/image.png" alt=""></p>
<h3 id="코드">코드</h3>
<p>앞서 이야기 한것을 바탕으로 최단 거리를 구하는 로직을 살펴보자. 
<code>weight</code>은 간선의 가중치를 저장한 배열, 시작 정점에서 정점(index)까지의 거리를 저장하는 배열을 <code>distance</code>라고 하면 다음과 같은 식이 성립한다.</p>
<pre><code class="language-c">if(distance[u] + weight[u][w] &lt; distance[w]) // s-&gt;u-&gt;w 와 s-&gt;w 비교
    distance[w] = distance[u] + weight[u][w]</code></pre>
<h4 id="전체-코드">전체 코드</h4>
<p>C 로 작성한 코드이다. 해당 코드에서 인덱스는 정점을 나타낸다.</p>
<blockquote>
<p><code>weight</code>는 2차원 배열의 인접 행렬 그래프로 정점간의 간선 가중치를 나타낸다.
<code>distance</code>는 시작정점으로부터 최단 경로까지의 거리를 나타낸다.
<code>found</code>는 방문한 정점인지를 나타내는 배열로 <code>visited</code>라고도 표현한다.</p>
</blockquote>
<p><strong><code>chooose()</code></strong> 함수는 <strong>방문하지 않는 노드</strong> 중 <strong>가장 작은 거리를 찾는</strong> 함수이다. </p>
<pre><code class="language-c">#include &lt;stdio.h&gt;
#define TRUE 1
#define FALSE 0
#define MAX_VERTICES 6
#define INF 10000000

int weight[MAX_VERTICES][MAX_VERTICES] = { // 인접 행렬
    {0, 7, 6, 10, INF, INF},
    {INF, 0, INF, INF, 9, INF},
    {INF, 2, 0, 3, 7, INF},
    {INF, INF, INF, 0, INF, 4},
    {INF, INF, INF, INF, 0, INF},
    {INF, INF, INF,8, 9, 0}};

int distance[MAX_VERTICES];
int found[MAX_VERTICES];

int choose(int n)
{
    int i, min, minpos;
    min = INF; // 가장 작은 거리를 저장
    minpos = -1;
    for (i = 0; i &lt; n; i++)
        if (distance[i] &lt; min &amp;&amp; !found[i])
        { // 작은 거리 찾기
            min = distance[i];
            minpos = i;
        }
    return minpos;
}

void dijkstra(int start, int n)
{
    int i, u, w;
    for (i = 0; i &lt; n; i++)
    { // 전체 거리 초기화
        distance[i] = weight[start][i];
        found[i] = FALSE;
    }

    found[start] = TRUE; // 시작정점 방문
    distance[start] = 0;

    for (i = 0; i &lt; n - 2; i++)
    {
        u = choose(n); // u: 새로 방문하는 노드
        found[u] = TRUE;
        for (w = 0; w &lt; n; w++)
            if (!found[w])                                    // 방문하지 않는 노드 확인
                if (distance[u] + weight[u][w] &lt; distance[w]) // s-&gt;u-&gt;w 와 s-&gt;w 비교
                    distance[w] = distance[u] + weight[u][w];
    }
}

void main(){
    dijkstra(0, MAX_VERTICES);

    for(int i = 0; i &lt; MAX_VERTICES; i++)
        printf(&quot;%d &quot;, distance[i]); // 0 7 6 9 13 13
}</code></pre>
<h2 id="정리">정리</h2>
<p>다익스트라 알고리즘은 <strong>음의 간선을 제외한 특정 하나의 정점에서 다른 모든 정점으로 가는 최단 경로 알고리즘</strong>이다.
현실 세계에서 음의 간선이 존재하지 않아 현실세계에서 사용하기 적합한 알고리즘 중 하나이다. 
특히 최단거리를 구할 때 <strong>이전까지 구했던 최단 거리 정보를 사용한다는 점</strong>에서 다이나믹 프로그래밍 문제로 볼 수 있다.</p>
<br/>
<br/>
<br/>
<br/>
<br/>
<br/>
<br/>
<br/>

<h2 id="📕-참고-링크">📕 참고 링크</h2>
<p>** 다음의 링크를 참고했습니다.**
<a href="https://en.wikipedia.org/wiki/Shortest_path_problem">https://en.wikipedia.org/wiki/Shortest_path_problem</a>
<a href="https://velog.io/@dlgosla/%EC%B5%9C%EB%8B%A8%EA%B2%BD%EB%A1%9C-%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98">https://velog.io/@dlgosla/최단경로-알고리즘</a>
<a href="https://jina-developer.tistory.com/118">https://jina-developer.tistory.com/118</a>
<a href="https://www.youtube.com/watch?v=611B-9zk2o4">25강 - 다익스트라 알고리즘(Dijkstra Algorithm) [ 실전 알고리즘 강좌(Algorithm Programming Tutorial) #25 ]</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[프로그래머스] 퍼즐 조각 채우기 - 84021]]></title>
            <link>https://velog.io/@last_game/%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%A8%B8%EC%8A%A4-%ED%8D%BC%EC%A6%90-%EC%A1%B0%EA%B0%81-%EC%B1%84%EC%9A%B0%EA%B8%B0-84021</link>
            <guid>https://velog.io/@last_game/%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%A8%B8%EC%8A%A4-%ED%8D%BC%EC%A6%90-%EC%A1%B0%EA%B0%81-%EC%B1%84%EC%9A%B0%EA%B8%B0-84021</guid>
            <pubDate>Tue, 16 Jan 2024 08:37:00 GMT</pubDate>
            <description><![CDATA[<p><a href="https://school.programmers.co.kr/learn/courses/30/lessons/84021">문제링크</a></p>
<p>dfs/bfs 구현 보다는 2차원 배열 처리가 까다로운 문제였다.</p>
<h2 id="🤔-thinking">🤔 Thinking</h2>
<p>우선 문제를 읽고 아래와 같이 문제 풀이 방법을 정의하였다.</p>
<blockquote>
<ol>
<li><a href="#1-%ED%85%8C%EC%9D%B4%EB%B8%94-%EC%95%88%EC%9D%98-%EB%B8%94%EB%A1%9D-%EC%B0%BE%EA%B8%B0">테이블 안의 블록 찾기</a></li>
<li><a href="#2-%EA%B2%8C%EC%9E%84-%EB%B3%B4%EB%93%9C%EC%9D%98-%EB%B9%88-%EA%B3%B5%EA%B0%84-%EC%B0%BE%EA%B8%B0">게임 보드의 빈 공간 찾기</a></li>
<li><a href="#3-%EB%B8%94%EB%A1%9D-%EB%B9%88-%EA%B3%B5%EA%B0%84-%EB%A7%A4%EC%B9%AD">블록-빈 공간 매칭</a></li>
</ol>
</blockquote>
<h3 id="1-테이블-안의-블록-찾기">1. 테이블 안의 블록 찾기</h3>
<p>블록을 찾기 위해서는 DFS 또는 BFS 를 이용해 쉽게 구현할 수 있다. 아직 코드를 보고 잘 이해가 가지 않는 분은 <a href="https://www.youtube.com/watch?v=PqzyFDUnbrY">음료수 얼려먹기</a>와 같은 문제를 참고 바란다.</p>
<pre><code class="language-python">from collections import deque

def find_block(graph, v, n, move_to):    
    queue = deque([v])
    block = [v]

    while queue:
        x, y = queue.popleft()
        graph[x][y] = 0

        for i, j in move_to:
            xi, yj = x + i, y + j
            if -1 &lt; xi &lt; n and -1 &lt; yj &lt; n and graph[xi][yj]:
                block.append((xi, yj))
                queue.append((xi, yj))
    return block

def make_blocks(board, n):
    move_to = [(1, 0), (-1, 0), (0, -1), (0, 1)]
    blocks = []

    for i in range(n):
        for j in range(n):
            if board[i][j]:
                blocks.append(find_block(board, (i, j), n, move_to))
    return blocks </code></pre>
<p>전체 배열의 요소를 확인하면서 블록을 탐색한다. 코드가 복잡해지기에 <code>make_blocks()</code> 함수를 만들어 분리 시켰다.</p>
<h3 id="2-게임-보드의-빈-공간-찾기">2. 게임 보드의 빈 공간 찾기</h3>
<p>게임 보드에서는 앞서 사용한 <strong>블록 찾기</strong> 함수를 이용해 빈칸을 찾을 수 있다. 다만 테이블과 달리 블록에서는 <strong>찾고자하는 칸이 0 이므로 1</strong>로 변환시킨다.</p>
<pre><code class="language-python"># 빈칸 찾기(빈칸: 1, 블록: 0 변환)
board = list(map(lambda x: list(map(lambda y: 1 - y, x)), game_board))
blanks = make_blocks(board, n)</code></pre>
<p>위는 1로 변환 시킨 후 <code>make_blocks()</code> 함수를 호출 시킨 코드이다.</p>
<h3 id="3-블록-빈-공간-매칭">3. 블록-빈 공간 매칭</h3>
<p>해당 부분이 제일 까다로웠다😢 블록을 빈 공간에 매칭하기 위해서는 서로의 위치가 같아야 했고 블록이 회전 가능했기에 회전한 경우도 매칭을 확인해 주어야한다. </p>
<h4 id="인덱스-위치-변환">인덱스 위치 변환</h4>
<p>1, 2 를 통해서 블록의 인덱스 값을 가져왔다. 여기서 문제는 블록과 빈공간의 위치가 다르다는 점이다. 따라서 블록과 빈공간의 위치를 0, 0 으로 시작하는 박스 형태로 변환한다. </p>
<p><img src="https://velog.velcdn.com/images/last_game/post/88149a04-2ab1-4589-a37f-f50db8200d5c/image.png" alt=""></p>
<p>위의 그림은 6X6 2차원 배열이다. 현재 블록의 위치는 <code>[(3, 4), (4, 4), (5, 4), (4, 3)]</code> 으로 저장 되어 있다. </p>
<p>현재 블록을 3X2 크기의 박스 형태의 크기로 저장한다.</p>
<pre><code class="language-python">def make_box(block):
    x, y = zip(*block)
    col, row = max(x) - min(x) + 1, max(y) - min(y) + 1
    box = [[0] * row for _ in range(col)]

    for i, j in block:
        i, j = i - min(x), j - min(y)
        box[i][j] = 1

    return box</code></pre>
<p>x 축, y 축 의 길이를 계산하여 <code>(0, 0)</code> 에서 시작하는 배열을 만든다.</p>
<p>결과: <code>[[0, 1], [1, 1], [0, 1]]</code></p>
<h4 id="블록-회전">블록 회전</h4>
<p>3X3 의 간단한 2차원 배열에서 오른쪽 방향으로 90도 회전 한다고 생각해보자.</p>
<p><img src="https://velog.velcdn.com/images/last_game/post/744ad783-920b-44fb-8de3-d724fb32205a/image.png" alt=""></p>
<p>위 그림에서 <strong>1행은 마지막 열</strong>로 이동하고, <strong>2번째 행은 2번째 열</strong>, <strong>3번째행은 1번째 열</strong>로 이동한 것을 확인할 수 있다.</p>
<p>1행을 <code>index(row, col)</code> 형태로 정리하면 아래와 같다.</p>
<blockquote>
<p><strong>(0, 0), (0, 1), (0, 2) =&gt; (0, 2), (1, 2), (2, 2)</strong></p>
</blockquote>
<p>규칙을 찾아보면 i, j 를 각각 행과 열이라고 할 때 아래와 같다. </p>
<blockquote>
<p><strong>회전 후 row = j</strong>
<strong>회전 후 col = 마지막 인덱스(2) - i</strong></p>
</blockquote>
<p>위 방식을 이용해 시계방향으로 90도 회전하는 함수를 작성하였다.</p>
<pre><code class="language-python">def rotate_block(block):
    cnt = 0
    rotate_block = [[0]*len(block) for _ in range(len(block[0]))]

    for i in range(len(block)):
        for j in range(len(block[0])):
            if block[i][j]: # 블록의 크기를 반환하기 위해 더해준다.
                cnt += 1
            rotate_block[j][len(block)-1-i] = block[i][j]
    return rotate_block, cnt</code></pre>
<h2 id="💻-전체-코드">💻 전체 코드</h2>
<p>위 방식대로 블록과 빈칸을 찾고 박스 형태로 만들어서 총 4번 회전하면서 빈칸과 블록이 일치하는 지 확인하였다.</p>
<pre><code class="language-python">from collections import deque

def find_block(graph, v, n, move_to):    
    queue = deque([v])
    block = [v]

    while queue:
        x, y = queue.popleft()
        graph[x][y] = 0

        for i, j in move_to:
            xi, yj = x + i, y + j
            if -1 &lt; xi &lt; n and -1 &lt; yj &lt; n and graph[xi][yj]:
                block.append((xi, yj))
                queue.append((xi, yj))
    return block

def make_blocks(board, n):
    move_to = [(1, 0), (-1, 0), (0, -1), (0, 1)]
    blocks = []

    for i in range(n):
        for j in range(n):
            if board[i][j]:
                blocks.append(find_block(board, (i, j), n, move_to))
    return blocks

def make_box(block):
    x, y = zip(*block)
    col, row = max(x) - min(x) + 1, max(y) - min(y) + 1
    box = [[0] * row for _ in range(col)]

    for i, j in block:
        i, j = i - min(x), j - min(y)
        box[i][j] = 1

    return box

def rotate_block(block):
    cnt = 0
    rotate_block = [[0]*len(block) for _ in range(len(block[0]))]

    for i in range(len(block)):
        for j in range(len(block[0])):
            if block[i][j]:  # 블록의 크기를 반환하기 위해 더해준다.
                cnt += 1
            rotate_block[j][len(block)-1-i] = block[i][j]
    return rotate_block, cnt

def match_blank(blank, blocks):
    for i, block in enumerate(blocks):
        block = make_box(block)
        for _ in range(4):
            block, cnt = rotate_block(block)   # 블록 회전
            if blank == block:
                del blocks[i]
                return cnt
    return 0

def solution(game_board, table):
    answer = 0
    n = len(table)  

    blocks = make_blocks(table, n)  # 블록 찾기
    # 빈칸 찾기(빈칸: 1, 블록: 0 변환)
    board = list(map(lambda x: list(map(lambda y: 1 - y, x)), game_board))
    blanks = make_blocks(board, n)

    # 빈칸-블록 매치(박스 만들기 매칭)
    for blank in blanks:
        blank = make_box(blank)
        answer += match_blank(blank, blocks)

    return answer</code></pre>
<br/>
<br/>
<br/>
<br/>
<br/>
<br/>
<br/>

<h2 id="📕-참고-문헌">📕 참고 문헌</h2>
<p>** 다음의 링크를 참고했습니다.**
<a href="https://school.programmers.co.kr/learn/courses/30/lessons/84021">https://school.programmers.co.kr/learn/courses/30/lessons/84021</a>
<a href="https://www.youtube.com/watch?v=PqzyFDUnbrY">https://www.youtube.com/watch?v=PqzyFDUnbrY</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[프로그래머스] 게임 맵 최단 거리 - 1844]]></title>
            <link>https://velog.io/@last_game/%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%A8%B8%EC%8A%A4-%EA%B2%8C%EC%9E%84-%EB%A7%B5-%EC%B5%9C%EB%8B%A8-%EA%B1%B0%EB%A6%AC-1844</link>
            <guid>https://velog.io/@last_game/%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%A8%B8%EC%8A%A4-%EA%B2%8C%EC%9E%84-%EB%A7%B5-%EC%B5%9C%EB%8B%A8-%EA%B1%B0%EB%A6%AC-1844</guid>
            <pubDate>Thu, 11 Jan 2024 02:30:01 GMT</pubDate>
            <description><![CDATA[<p><a href="https://school.programmers.co.kr/learn/courses/30/lessons/1844">문제링크</a></p>
<h2 id="🤔-thinking">🤔 Thinking</h2>
<h4 id="첫번째-풀이">첫번째 풀이</h4>
<p>전체 경로에 따른 최단경로를 구하기 위해서 전체 가능한 경로를 전부 찾으면 풀 수 있다고 생각하여 dfs를 이용해 작성하였다.</p>
<h3 id="💻-dfs-로-짠-코드">💻 DFS 로 짠 코드</h3>
<pre><code class="language-python">def out_maps(x, y, maps):
    return (-1 &lt; x &lt; len(maps)) and (-1 &lt; y &lt; len(maps[0])) # 맵을 벗어난 경우

def dfs_mat(maps, move_to, v, move):
    x, y = v

    if x == len(maps)-1 and y == len(maps[0])-1:           # 상대팀 진영에 도착한 경우
        global answer
        answer = min(answer, move)
        return

    for mx, my in move_to:
        if (out_maps(x+mx, y+my, maps) and maps[x+mx][y+my]): # 맵에 장애물이 없는 지 확인
            maps[x+mx][y+my] = 0
            dfs_mat(maps, move_to, (x+mx, y+my), move+1)     # 이동
            maps[x+mx][y+my] = 1                            # 이전 상태 복구

def solution(maps):
    global answer
    max_size = 100 * 100 + 1
    answer = max_size

    move_to = [(1, 0), (-1, -0), (0, -1), (0, 1)]

    dfs_mat(maps, move_to, (0,0), 1)

    return  -1 if (max_size == answer)  else answer</code></pre>
<h3 id="나의-실수">나의 실수</h3>
<p>테스트를 돌려보니 효율성 테스트에서 전부 시간초과가 나왔다.
<img src="https://velog.velcdn.com/images/last_game/post/46a2e598-79ff-4b8a-9a1c-35dea9e9a5c4/image.png" alt=""></p>
<p>dfs에서는 백트레킹(?) 처럼 방문 후 다시 돌아왔을 때 이전상태로 바꿔주어야한다. </p>
<pre><code class="language-python">maps[x+mx][y+my] = 0
dfs_mat(maps, move_to, (x+mx, y+my), move+1)     # 이동
maps[x+mx][y+my] = 1                            # 이전 상태 복구</code></pre>
<p>dfs를 사용하면 이동시 매번 가능한 전체 경로를 탐색하기 때문에 매우 많은 경우의 수를 탐색해야 한다. <strong>최단 거리가 아닌 경우도 탐색한다.</strong>
<br/></p>
<h2 id="🙆♂️-정답-풀이">🙆‍♂️ 정답 풀이</h2>
<h4 id="두번째-풀이">두번째 풀이</h4>
<p><img src="https://velog.velcdn.com/images/last_game/post/38dae9fc-46b2-43c3-8aa0-e359482d2089/image.gif" alt=""></p>
<p>BFS 에서는 최단 거리를 구할 때 조건을 만족하면 종료하기 때문에 전체 경로를 탐색하는 dfs의 방식 보다 더 빠르다.
BFS와 DFS의 차이점을 실제 문제로 깨닫게 해주는 문제였다.</p>
<h3 id="💻-bfs-로-짠-코드">💻 BFS 로 짠 코드</h3>
<p>BFS에서는 현재 이동거리를 큐에 인덱스와 같이 저장하였다.</p>
<pre><code class="language-python">from collections import deque

def out_maps(x, y, maps):
    return (-1 &lt; x &lt; len(maps)) and (-1 &lt; y &lt; len(maps[0]))

def bfs_mat(maps, v):
    queue = deque([v])
    move_to = [(1, 0), (-1, -0), (0, -1), (0, 1)]

    max_size = 100 * 100 + 1
    result = max_size

    while(queue):
        x, y, move = queue.popleft()

        for mx, my in move_to:
            if (out_maps(x+mx, y+my, maps) and maps[x+mx][y+my]):
                queue.append((x+mx, y+my, move+1))
                maps[x+mx][y+my] = 0                    # 방문

        if x == len(maps)-1 and y == len(maps[0])-1:
            result = min(result, move)

    return -1 if result == max_size else result

def solution(maps):
    answer = bfs_mat(maps, (0,0,1))

    return  answer</code></pre>
<br/>
<br/>
<br/>
<br/>
<br/>
<br/>
<br/>
<br/>
<br/>

<h2 id="📕-참조-링크">📕 참조 링크</h2>
<p><a href="https://school.programmers.co.kr/learn/courses/30/lessons/1844">게임 맵 최단거리 - 프로그래머스</a>
<a href="https://www.codingame.com/learn/pathfinding">pathfinding.gif</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[프로그래머스] 타겟넘버 - 43165]]></title>
            <link>https://velog.io/@last_game/%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%A8%B8%EC%8A%A4-%ED%83%80%EA%B2%9F%EB%84%98%EB%B2%84-43165</link>
            <guid>https://velog.io/@last_game/%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%A8%B8%EC%8A%A4-%ED%83%80%EA%B2%9F%EB%84%98%EB%B2%84-43165</guid>
            <pubDate>Wed, 10 Jan 2024 05:42:46 GMT</pubDate>
            <description><![CDATA[<p><a href="https://school.programmers.co.kr/learn/courses/30/lessons/43165">문제링크</a></p>
<p>DFS/BFS 문제 중 2차원 배열이 아닌 노드간의 그래프 형식의 문제의 경우에도 <a href="https://velog.io/@last_game/%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98-%EA%B9%8A%EC%9D%B4%EC%9A%B0%EC%84%A0%ED%83%90%EC%83%89DFS%EA%B3%BC-%EB%84%88%EB%B9%84%EC%9A%B0%EC%84%A0%ED%83%90%EC%83%89BFS">DFS/BFS</a> 문제임을 알게 되어서 기록한다. </p>
<h2 id="🤔-thinking">🤔 Thinking</h2>
<h4 id="나의-접근">나의 접근</h4>
<p>처음에 음수인 경우와 양수인 경우를 각각 노드마다 정한 후 모든 경우의 수를 파악해야 한다고 생각하였다.
재귀 함수를 이용해서 아래와 같이 전부 비교하면 될 것이라고 생각하였다.</p>
<pre><code class="language-python">[  1  1  1  1  1  ] 기준
1. [  1  1  1  1  1  ]
2. [ -1  1  1  1  1  ]
3. [  1 -1  1  1  1  ]

...

n-1.[  1 -1 -1 -1 -1  ]
n.  [ -1 -1 -1 -1 -1  ]</code></pre>
<p>다만, 코드를 작성해서 구현하는 것은 쉽게 떠오르지 않았다.</p>
<h4 id="💡-아이디어">💡 아이디어</h4>
<blockquote>
<p>0부터 n-1 번까지 다음 인덱스의 number 에 <strong>(+) 또는 (-) 부호</strong> 를 붙여 각 숫자에서 2가지 경우의 수를 만든다. 이렇게 모든 경우의 수를 확인 할 수 있다. </p>
</blockquote>
<p>기존의 방식은 2차원 배열의 그래프를 이용한 dfs/bfs 문제를 봐왔는데 이렇게 직접 경우의 수를 만들어 그래프 형태로 풀 수 있음을 알게되었다.
<img src="https://velog.velcdn.com/images/last_game/post/9a4b2e57-3b39-40e8-8f39-71638df63599/image.PNG" alt=""></p>
<h2 id="💻-code">💻 Code</h2>
<p>각각의 인덱스 값을 알아야 다음 number를 추가할 수 있기 때문에 인덱스도 같이 매개변수로 넘겨 준다.</p>
<h4 id="dfs로-푼-코드">DFS로 푼 코드</h4>
<pre><code class="language-python">def dfs(numbers, target, v, idx):
    global answer

    if idx == len(numbers): # n 번째인 경우 종료
        if v == target:        # 조건 만족시 카운트
            answer += 1
        return
    dfs(numbers, target, v+numbers[idx], idx+1) # + 부호 붙이기
    dfs(numbers, target, v-numbers[idx], idx+1) # - 부호 붙이기

def solution(numbers, target):
    global answer
    answer = 0

    dfs(numbers, target, 0, 0)

    return answer</code></pre>
<h4 id="bfs로-푼-코드">BFS로 푼 코드</h4>
<pre><code class="language-python">from collections import deque

def bfs(numbers, target, vertex, index):
    result = 0
    queue = deque([(vertex, index)]) # 큐 사용

    while queue:
        v, idx = queue.popleft()

        if idx == len(numbers):
            if v == target:
                result += 1
        else:
            queue.append((v+numbers[idx], idx+1)) # + 부호 붙이기
            queue.append((v-numbers[idx], idx+1)) # - 부호 붙이기

    return result

def solution(numbers, target):
    answer = bfs(numbers, target, 0, 0)

    return answer</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[[자료구조] 힙(Heap) in C]]></title>
            <link>https://velog.io/@last_game/%EC%9E%90%EB%A3%8C%EA%B5%AC%EC%A1%B0-%ED%9E%99Heap-in-C</link>
            <guid>https://velog.io/@last_game/%EC%9E%90%EB%A3%8C%EA%B5%AC%EC%A1%B0-%ED%9E%99Heap-in-C</guid>
            <pubDate>Sun, 07 Jan 2024 12:11:32 GMT</pubDate>
            <description><![CDATA[<h2 id="힙이란">힙이란?</h2>
<ul>
<li>무엇인가 쌓아놓은 것과 비슷한 형태의 특정한 자료구조로 트리 중에서 완전 이진 트리의 형태를 가진다.</li>
<li>여러개의 값들 중에서 최대 값이나 최소 값을 빠르게 찾아내도록 만들어진 자료구조이다.</li>
<li>우선순위 큐를 구현 할 때 힙을 사용한다.<blockquote>
<p><strong>우선순위 큐?</strong>
우선순위 큐는 우선순위가 높은 순서로 먼저 나가게 되는 큐로 대표적으로 최단경로 알고리즘을 구현할때 사용한다.</p>
</blockquote>
</li>
</ul>
<p><img src="https://velog.velcdn.com/images/last_game/post/a86c4815-62b6-41d3-a16e-04f1a60b21c8/image.png" alt="완전이진트리 예시"></p>
<blockquote>
<p>완전 이진 트리는 <a href="https://velog.io/@last_game/%EC%9E%90%EB%A3%8C%EA%B5%AC%EC%A1%B0-%ED%8A%B8%EB%A6%ACTree-in-C">트리</a>에서 설명하였듯이 이진 트리 중에서 앞에서부터 중간에 빈 노드없이 차례대로 노드를 채우고 있는 형태를 말한다. </p>
</blockquote>
<h2 id="힙heap의-종류">힙(heap)의 종류</h2>
<p>힙은 최대 힙과 최소 힙으로 나뉜다. 그림과 함께 살펴보자.</p>
<h3 id="최대힙max-heap">최대힙(max heap)</h3>
<p>부모노드의 키 값이 자식 노드의 키 값보다 크거나 같은 완전 이진 트리를 말한다.
항상 부모노드의 키값이 자식 노드의 키값보다 크거나 같아야한다.(부모노드 &gt;= 자식노드)</p>
<p><img src="https://velog.velcdn.com/images/last_game/post/4598d9eb-9a73-470a-8f0c-20230bd70abf/image.png" alt=""></p>
<p>오른쪽 그림의 경우 부모노드가 자식노드보다 작은 경우가 존재하기 때문에 최대힙, 즉 힙을 만족하지 못한다.</p>
<h3 id="최소힙min-heap">최소힙(min heap)</h3>
<p>부모노드의 키 값이 자식 노드의 키 값보다 작거나 같은 완전 이진 트리를 말한다.
최대힙과 반대로 항상 부모노드의 키값이 자식 노드의 작거나 같아야한다.(부모노드 &lt;= 자식노드)</p>
<p><img src="https://velog.velcdn.com/images/last_game/post/700a2da6-a4dd-482b-bbbc-c99a3455fbbb/image.png" alt=""></p>
<p>그림을 보면 둘다 부모 노드가 자식노드보다 작거나 같은 것을 확인 할 수 있지만 오른쪽 그림의 경우 완전 이진 트리를 만족하지 못하여 최소힙이라고 할 수 없다.</p>
<blockquote>
<h5 id="참고-느슨한-정렬">참고) 느슨한 정렬?</h5>
<p>힙은 레벨에 따라 일부 정렬되어 있어 느슨한 정렬을 유지 한다.
평균적으로 큰값이 상위 레벨에 있고 작은 값이 하위레벨에 존재한다.</p>
</blockquote>
<h2 id="힙heap의-구현">힙(heap)의 구현</h2>
<p>힙은 배열을 사용해서 구현할 수 있다. 각 노드를 위에서부터 순서대로 번호(index)를 부여하여 저장한다.
배열의 첫번째 인덱스인 0은 구현을 쉽게 하기위해 사용하지 않는다.</p>
<p>위를 바탕으로 기존의 트리에서 접근하는 방식과 같이 부모 자식간의 관계를 인덱스로 정의 할 수 있다.</p>
<pre><code class="language-c">왼쪽 자식 인덱스 = (부모의 인덱스) * 2
오른쪽 자식 인덱스 = (부모의 인덱스) * 2 + 1
부모의 인덱스 = (자식노드의 인덱스) / 2</code></pre>
<p><img src="https://velog.velcdn.com/images/last_game/post/2231009a-7265-4ed1-976d-70dd24d4515b/image.png" alt=""></p>
<h3 id="c언어-코드">C언어 코드</h3>
<h4 id="힙-정의">힙 정의</h4>
<p>힙의 전체 요소의 크기와 배열을 정의 한다.</p>
<pre><code class="language-c">#define MAX_ELEMENT 200

typedef struct {
    int key;
} element;

typedef struct {
    element heap[MAX_ELEMENT];
    int heap_size;
} HeapType;</code></pre>
<h4 id="힙-삽입">힙 삽입</h4>
<ol>
<li>삽입하고자 하는 원소를 맨 뒤에 삽입 한다.</li>
<li>최소힙 또는 최대힙을 만족하도록 위치를 조정한다.</li>
</ol>
<p>최대 힙의 경우 삽입할 노드와 부모노드를 비교하여 삽입 노드가 크면 삽입노드와 부모 노드의 위치를 변경한다.
아래는 최대 힙으로 구현한 삽입 함수이다. 최소힙은 비교과정을 작은 경우로 변경하면 된다.</p>
<pre><code class="language-c">void insert_max_heap(HeapType* h, element item)
{
    int i;
    i = ++(h-&gt;heap_size); // 삽입된 노드의 인덱스를 의미한다.

    // 삽입노드가 부모노드보다 크면 한 레벨 위로 올라간다.
    while((i != 1) &amp;&amp; (item.key &gt; h-&gt;heap[i/2].key){ 
        h-&gt;heap[i] = h-&gt;heap[i/2]; // 부모노드를 자식 노드로 내린다.
        i /= 2; // 레벨 위로 가기
    }
    h-&gt;heap[i] = item; // 올바른 위치에 삽입 노드를 저장
}</code></pre>
<h4 id="힙-삭제">힙 삭제</h4>
<ol>
<li>삭제할 노드(1번째 인덱스의 값)을 저장 한다.</li>
<li>마지막 위치의 노드를 1번째 인덱스의 위치에 교체한다.</li>
<li>최소힙 또는 최대힙을 만족하도록 위치를 조정한다.</li>
</ol>
<p>아래는 최대 힙으로 작성한 힙 삭제 함수이다. 삭제 노드를 반환한다.</p>
<pre><code class="language-c">element delete_max_heap(HeapType* h)
{
    int parent, child;
    element item, temp;

    item = h-&gt;heap[1]; // 삭제할 노드(반환할 노드) 저장
    temp = h-&gt;heap[(h-&gt;heap_size)--]; // 마지막 노드

    parent = 1;
    child = 2;
    while(child &lt;= h-&gt;heap_size){
        // 더 큰 자식 노드와 비교하기 위해 자식노드 찾기
        if((child &lt; h-&gt;heap_size) &amp;&amp; (h-&gt;heap[child].key &lt; h-&gt;heap[child + 1].key))
            child++;
       // 자식 노드와 기존의 마지막 위치의 노드 비교
       if(temp.key &gt;= h-&gt;heap[child].key) break;
       // 레벨 아래로 가기
       h-&gt;heap[parent] = h-&gt;heap[child];
       parent = child;
       child *= 2;
    }
    h-&gt;heap[parent] = temp; // 올바른 위치에 마지막 노드를 저장
    return item; // 삭제된 노드 반환
}   </code></pre>
<br/>

<h2 id="마무리">마무리</h2>
<p>힙은 <strong>삽입 삭제</strong> 할 때 최악의 경우 전체 힙의 높이만큼 비교하기 때문에 노드의 개수가 n일 경우 <strong>O(log₂n)</strong>의 복잡도를 가진다. </p>
<p>힙을 사용하면 힙에 원소를 삽입 후 하나씩 값을 가져오면 <strong>정렬된 값</strong>을 얻을 수 있다.
정렬시에는 (전체 원소를 삽입하는 연산 * 비교연산)으로 <strong>O(nlog₂n)</strong>의 복잡도를 가진다.</p>
<p>최단 경로 문제와 같이 <strong>우선순위 큐</strong>를 사용하는 문제에서 활용되기 때문에 어떤식으로 동작하는지 기억해두자.</p>
<br/>
<br/>
<br/>
<br/>
<br/>
<br/>
<br/>
<br/>

<h2 id="📕-참고-문헌">📕 참고 문헌</h2>
<p>** 다음의 문헌을 참고했습니다.**
C언어로 쉽게 풀어쓴 자료구조 [ 개정3판 ]
천인국, 공용해, 하상호 저 | 생능출판사 | 2021년 08월 20일
<a href="https://gmlwjd9405.github.io/2018/05/10/data-structure-heap.html">[자료구조] 힙(heap)이란 - heejeong Kwon 10 May 2018</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Git Commit 메시지 규칙]]></title>
            <link>https://velog.io/@last_game/Git-Commit-%EB%A9%94%EC%8B%9C%EC%A7%80-%EA%B7%9C%EC%B9%99</link>
            <guid>https://velog.io/@last_game/Git-Commit-%EB%A9%94%EC%8B%9C%EC%A7%80-%EA%B7%9C%EC%B9%99</guid>
            <pubDate>Sun, 31 Dec 2023 05:35:46 GMT</pubDate>
            <description><![CDATA[<p>그동안 내 Git Repository의 Commit을 보면서 엉망으로 커밋 메시지가 작성되어 있어 커밋 메시지 규칙에 대해 알아보고 이를 적용해 보고자 한다.</p>
<h2 id="git-commit-메시지-convention">Git Commit 메시지 convention</h2>
<p>Git에서 Commit 메시지를 일정한 형식으로 작성하는 규칙을 말한다. 관습적으로 사용하는 가이드라인이 있지만, 각 프로젝트에 따라 별도의 규칙을 만들어 적용하기도 한다.
<img src="https://velog.velcdn.com/images/last_game/post/026d049a-3603-41df-885e-7462a9912938/image.png" alt="https://github.com/stephenparish/org-web/commits/master/?after=4fe770a1f34f4880bf7db5794e9d01eeb79bb11f+34"></p>
<h3 id="왜-중요한데">왜 중요한데?</h3>
<p>깃을 사용하는 이유는 프로젝트 관리, 협업 등 다양한 이유가 있다. 모든 커밋 메시지가 &quot;오류수정&quot;, &quot;기능 추가&quot; 이러한 방식으로 작성되어 있으면 직접 코드를 이해하고 협업을 원할하게 하기 어려울 것이다.</p>
<p>깃을 제대로 활용하려면 커밋 규칙을 잘 따르는 것이 중요하다. </p>
<h4 id="1-프로젝트-이력관리">1. 프로젝트 이력관리</h4>
<p>커밋 메시지를 통해서 각 변경사항이나 추가된 기능, 오류 수정이 어떤 목적으로 이루어졌는지 알 수 있다.
특정 이슈에 대한 수정 내용을 찾을 수 있다.</p>
<h4 id="2-협업-및-코드-리뷰">2. 협업 및 코드 리뷰</h4>
<p>다른 개발자들과 협업할 때, 커밋 메시지를 통해 다른 사람이 코드를 이해하는 데 도움이 된다.
이는 코드 리뷰나 병합을 할 때도 관리자가 적절하게 프로젝트를 관리하는 데 효율적이다.</p>
<h4 id="3-변경-이력-추적-및-문제-해결">3. 변경 이력 추적 및 문제 해결</h4>
<p>일관된 커밋 메시지로 소스 변경 이력을 효율적으로 추적할 수 있다. 이를 통해 문제 발생시 더 빠르게 원인을 찾아 수정하고, 전반적인 프로젝트 안정성을 높일 수 있다.</p>
<h2 id="git-commit-메시지-작성법">Git Commit 메시지 작성법</h2>
<p>커밋 메시지는 아래 양식을 따른다.</p>
<pre><code>&lt;type&gt;[optional scope]: &lt;description&gt;

[optional body]

[optional footer(s)]</code></pre><h4 id="type"><code>&lt;type&gt;</code></h4>
<p>커밋 타입을 말한다. 아래는 내가 android에서 사용하기 위해 <a href="https://jihyun-hamster.tistory.com/132">템플릿</a>을 수정하였다.</p>
<pre><code>[커밋 타입]  리스트
#   feat      : 기능 (새로운 기능)
#   fix       : 버그 (버그(에러) 수정)
#   design    : CSS, XML 등 사용자 UI 디자인 생성 및 변경
#   refactor  : 리팩토링
#   style     : 스타일 (theme, color 파일 구현, 수정: 비즈니스 로직에 변경 없음)
#   docs      : 문서 (Readme.md 추가, 수정, 삭제)
#   test      : 테스트 (테스트 코드 추가, 수정, 삭제: 비즈니스 로직에 변경 없음)
#   chore     : 기타 변경사항 (build.gradle 수정)
#   release   : 버전 릴리즈
#   (아래는 필요시 사용)
#   rename    : 파일 혹은 폴더명을 수정하거나 옮기는 작업만 하는 경우
#   remove    : 파일을 삭제하는 작업만 수행한 경우</code></pre><h4 id="description"><code>&lt;description&gt;</code></h4>
<p>변경 작업의 제목이나 간단한 요약을 작성한다. 50자 이내로 요약하여 작성한다. 마침표를 일반적으로 사용하지 않는다.</p>
<h4 id="body"><code>&lt;body&gt;</code></h4>
<p>선택 사항으로 작업한 내용이 복잡하거나 기록해 두어야하는 상세 내용이 있을 때 작성 한 줄당 72자 이내로 작성하는 것이 좋다. 무엇을 보단 왜? 에 대해 작성한다.</p>
<h4 id="footer"><code>&lt;footer&gt;</code></h4>
<p>선택 사항으로, 코드 작업과 관련된 이슈나 참조 링크가 있을 때 작성한다. 
이슈 생성시에는 <code>(#123)</code>과 같이 작성하고, 종료시에는 <code>close #123</code> 또는 <code>resloves #123</code>으로 작성한다. 
참조시에는 <code>ref 참조링크</code> 와 같이 작성한다.</p>
<h3 id="작성-예시">작성 예시</h3>
<pre><code>git commit -m &quot;fix: 리사이클러뷰 데이터 로딩 이슈 수정

리사이클러뷰에서 데이터 로딩시에 중복 데이터가
생성되어지는 이슈 수정.

resolves: #123&quot;</code></pre><h2 id="마무리">마무리</h2>
<p>커밋 메시지 규칙들에는 정답은 없으며 팀원들이나 자신만을 일정한 규칙을 가지고 진행한다. 
깃 커밋 메시지 규칙에 대해 알아보았으니 템플릿을 만들어서 자신의 프로젝트에 적용해 보면 좋을 것 같다.</p>
<p>템플릿 적용은 아래 링크를 참고하자.
깃 커밋 템플릿 만들어서 간편하게 커밋해보자!(<a href="https://jihyun-hamster.tistory.com/132">https://jihyun-hamster.tistory.com/132</a>)</p>
<br/>
<br/>
<br/>
<br/>
<br/>
<br/>
<br/>
<br/>
<br/>

<h2 id="📕-참고-문헌">📕 참고 문헌</h2>
<p>** 다음의 링크를 참고했습니다.**
<a href="https://yozm.wishket.com/magazine/detail/1974/">Git 커밋 메시지 컨벤션은 왜 중요할까? - 위시켓</a>
<a href="https://github.com/stephenparish/org-web/commits/master/?after=4fe770a1f34f4880bf7db5794e9d01eeb79bb11f+34">github.com/stephenparish/org-web/commits</a>
<a href="https://jihyun-hamster.tistory.com/132">깃 커밋 템플릿 만들어서 간편하게 커밋해보자! - jihyun-hamster</a>
<a href="https://gist.github.com/stephenparish/9941e89d80e2bc58a153">AngularJS Git 커밋 메시지 규칙</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[알고리즘] 하노이 탑]]></title>
            <link>https://velog.io/@last_game/%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98-%ED%95%98%EB%85%B8%EC%9D%B4-%ED%83%91</link>
            <guid>https://velog.io/@last_game/%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98-%ED%95%98%EB%85%B8%EC%9D%B4-%ED%83%91</guid>
            <pubDate>Sat, 30 Dec 2023 07:09:33 GMT</pubDate>
            <description><![CDATA[<h3 id="하노이-탑">하노이 탑</h3>
<blockquote>
<p>게임의 목적은 다음 두 가지 조건을 만족시키면서, 한 기둥에 꽂힌 원판들을 그 순서 그대로 다른 기둥으로 옮겨서 다시 쌓는 것이다. (<a href="https://ko.wikipedia.org/wiki/%ED%95%98%EB%85%B8%EC%9D%B4%EC%9D%98_%ED%83%91">위키백과-하노의의탑</a>)</p>
</blockquote>
<blockquote>
<ol>
<li>한 번에 한개의 원판만 옮길 수 있다.</li>
<li>가장 위에 있는 원판만 이동할 수 있다.</li>
<li>큰 원판이 작은 원판 위에 있어서는 안 된다.</li>
</ol>
</blockquote>
<p>그림을 통해 살펴보자.
<img src="https://velog.velcdn.com/images/last_game/post/e37af540-d588-438a-964f-5e98519d7600/image.gif" alt="하노이의 탑의 원리"></p>
<h4 id="2개의-원판-이동">2개의 원판 이동</h4>
<p>해당 그림에서는 기둥이 없지만 3개의 기둥이 있다고 생각해보자.</p>
<p>각각의 3개의 기둥을 A, B, C 이고 원판들을 A에서 C로 2개의 원판을 옮기는 방법을 보자. </p>
<ol>
<li>가장 위에 있는 원판을 B로 이동한다. A-&gt;B</li>
<li>두번째 원판을 목적지인 C로 이동한다. A-&gt;C</li>
<li>B로 이동 시켰던 첫번째 원판을 C로 이동한다. B-&gt;C</li>
</ol>
<p>총 3번 이동한다.</p>
<h4 id="3개의-원판-이동">3개의 원판 이동</h4>
<p>위를 바탕으로 3개의 원판을 이동하는 방법도 알아보자. 
편의상 원판을 1, 2, 3 으로 수를 부여해 숫자가 작을 수록 위의 원판이라고 생각한다.</p>
<ol>
<li>A-&gt;B
가장 위의 1, 2의 원판을 이동하는 경우이다. 2개의 원판을 이동하는 과정은 A에서 B로 2개의 원판을 옮기는 과정이다.<blockquote>
<p>A-&gt;C
A-&gt;B
C-&gt;B</p>
</blockquote>
</li>
<li>A-&gt;C
마지막 3의 원판을 이동하는 과정이다. 그대로 A-&gt;C 만 이동한다.</li>
<li>B-&gt;C
B로 옮겼던 1, 2의 원판을 다시 C로 옮기는 과정이다.<blockquote>
<p>B-&gt;A
B-&gt;C
A-&gt;C</p>
</blockquote>
</li>
</ol>
<p>3개의 원판을 옮기는 과정을 전부 알아보았다. 총 7번 이동하였다. </p>
<h3 id="결론">결론</h3>
<p>위의 2개, 3개의 원판을 이동한 과정을 통해 결론을 도출 해보자. </p>
<blockquote>
<ol>
<li><h5 id="a-b마지막-원판을-제외한-원판들을-목적지가-아닌-곳에-이동">A-&gt;B(마지막 원판을 제외한 원판들을 목적지가 아닌 곳에 이동)</h5>
</li>
<li><h5 id="a-c마지막-원판을-목적지에-이동">A-&gt;C(마지막 원판을 목적지에 이동)</h5>
</li>
<li><h5 id="a-c나머지-원판들을-목적지에-이동">A-&gt;C(나머지 원판들을 목적지에 이동)</h5>
</li>
</ol>
</blockquote>
<p>1번 3번의 경우 <strong>나머지 원판들을 특정 위치에 이동시키는 것은 한 위치에서 특정 위치로 이동하는 것으로 생각할 수 있다.</strong> 
이러한 특성 때문에 재귀함수로 구현이 가능하다.</p>
<h4 id="코드">코드</h4>
<pre><code class="language-python">def hanoi_tower(n, from, others, to, result):
    if n &lt;= 0: return
    hanoi_tower(n-1, from, to, others, result)     # A-&gt;B 이동
    print(A, C)                                    # A-&gt;C 이동  
    hanoi_tower(n-1, others, from, to, result)    # B-&gt;C 이동

def solution(n):
    answer = []
    hanoi_tower(n, 1, 2, 3, answer)
    return answer</code></pre>
<p><code>hanoi_tower</code> 함수에서 우리가 정의한 대로 재귀함수로 호출 하였다.</p>
<h4 id="원판의-이동-횟수">원판의 이동 횟수</h4>
<p>원판의 이동 횟수는 개수에 따라 일정한 특징을 가진다.</p>
<blockquote>
<p>1개 -&gt; 1회
2개 -&gt; 2²-1회
3개 -&gt; 2³-1회
...
..
n개 -&gt; 2ⁿ-1회</p>
</blockquote>
<p>원판의 이동횟수는 원판의 개수가 n이라고 할때 <strong>2ⁿ-1</strong> 를 가진다.
<br/></p>
<h3 id="마무리">마무리</h3>
<p>하노의 탑은 재귀함수를 적용하는 대표적인 예이지만 처음에 풀이 하려고 할 때는 쉽게 이해가 가지 않았다.
잘 이해가 가지 않은 분들은 학습에 참고한 영상 링크를 참고하면 좋을 것 같다.</p>
<p><strong>👇 클릭</strong>
  &nbsp; <a href="https://youtu.be/vq7dpFWpwAE?feature=shared">하노이 탑(Tower of Hanoi) 재귀호출 코드 구현 [혀니C코딩]</a>
  &nbsp; <a href="https://youtu.be/aPYE0anPZqI?feature=shared">재귀함수가 뭔가요? (Feat. 하노이의 탑) [얄팍한 코딩사전]</a></p>
<br/>
<br/>
<br/>
<br/>
<br/>
<br/>
<br/>
<br/>
<br/>


<h2 id="📕-참고-문헌">📕 참고 문헌</h2>
<p>** 다음을 참고했습니다.**
<a href="https://ko.wikipedia.org/wiki/%ED%95%98%EB%85%B8%EC%9D%B4%EC%9D%98_%ED%83%91">위키백과-하노의의탑</a>
<a href="https://ko.wikipedia.org/wiki/%ED%95%98%EB%85%B8%EC%9D%B4%EC%9D%98_%ED%83%91#/media/%ED%8C%8C%EC%9D%BC:Tower_of_Hanoi_4.gif">Tower_of_Hanoi_4.gif</a>
<a href="https://youtu.be/vq7dpFWpwAE?feature=shared">하노이 탑(Tower of Hanoi) 재귀호출 코드 구현 [혀니C코딩]</a>
<a href="https://youtu.be/aPYE0anPZqI?feature=shared">재귀함수가 뭔가요? (Feat. 하노이의 탑) [얄팍한 코딩사전]</a></p>
]]></description>
        </item>
    </channel>
</rss>