<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>marigold_log</title>
        <link>https://velog.io/</link>
        <description>많은 것을 알아가고 싶은 Android 주니어 개발자</description>
        <lastBuildDate>Tue, 24 Mar 2026 10:22:47 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>marigold_log</title>
            <url>https://velog.velcdn.com/images/marigold_/profile/04c497fa-05de-4f21-85cd-de49e409671a/image.jpg</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. marigold_log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/marigold_" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[[Android] Thread]]></title>
            <link>https://velog.io/@marigold_/Android-Thread</link>
            <guid>https://velog.io/@marigold_/Android-Thread</guid>
            <pubDate>Tue, 24 Mar 2026 10:22:47 GMT</pubDate>
            <description><![CDATA[<p>UI를 구현하다 보면 네트워크 요청이나 데이터베이스 작업처럼 시간이 오래 걸리는 작업을 처리해야 하는 경우가 많습니다. 이런 작업을 메인 스레드에서 그대로 실행하면 화면이 멈추거나 앱이 응답하지 않는 문제가 발생할 수 있습니다.</p>
<p>그래서 안드로이드에서는 오래 걸리는 작업을 별도의 Thread에서 실행하는 방식이 기본적으로 사용됩니다. 이번 글에서는 Thread가 무엇인지, 그리고 UI 구현에서 왜 중요한지 정리해보려고 합니다.</p>
<hr>
<h3 id="🚀-thread란">🚀 Thread란?</h3>
<p>Thread는 프로그램 내에서 작업을 실행하는 가장 기본적인 단위입니다. 안드로이드 앱은 기본적으로 하나의 메인 스레드(UI 스레드)에서 시작되며, 이 스레드는 화면을 그리거나 사용자 입력을 처리하는 역할을 담당합니다.</p>
<p>하지만 메인 스레드에서 시간이 오래 걸리는 작업을 수행하면 화면이 멈추게 됩니다. 이를 방지하기 위해 별도의 Thread를 생성하여 작업을 처리할 수 있습니다.</p>
<pre><code class="language-kotlin">Thread {
    // 오래 걸리는 작업 (예: 네트워크 요청)
}.start()</code></pre>
<p>Thread는 강력하지만 직접 사용할 경우 몇 가지 단점이 있습니다.</p>
<ul>
<li>생성 비용이 큼 (무겁다)</li>
<li>생명주기를 직접 관리해야 함</li>
<li>코드가 복잡해지기 쉬움</li>
<li>메모리 누수나 충돌 위험</li>
</ul>
<p>특히 화면이 사라졌는데도 Thread가 계속 실행되는 경우, 불필요한 작업이 계속 수행될 수 있습니다.</p>
<hr>
<h3 id="⚠️-ui-thread와의-관계">⚠️ UI Thread와의 관계</h3>
<p>안드로이드에서 중요한 점은 UI는 반드시 메인 스레드에서만 업데이트할 수 있다는 것입니다. 즉, 백그라운드 Thread에서 작업을 수행한 후에는 다시 메인 스레드로 돌아와 UI를 갱신해야 합니다.</p>
<pre><code class="language-kotlin">Thread {
    val result = &quot;데이터 로드 완료&quot;

    Handler(Looper.getMainLooper()).post {
        textView.text = result
    }
}.start()</code></pre>
<p>이처럼 Thread를 사용할 때는 작업 실행 (백그라운드), UI 반영 (메인 스레드)을 명확하게 나눠야 합니다.</p>
<hr>
<h3 id="🔄-coroutine과의-차이">🔄 Coroutine과의 차이</h3>
<p>Thread를 이해했다면, 자연스럽게 Kotlin의 Coroutine과의 차이도 함께 알아두는 것이 좋습니다. Coroutine은 Thread 위에서 동작하는 경량 작업 단위로, 비동기 처리를 더 간단하게 만들어주는 도구입니다.</p>
<p>두 개념의 핵심 차이는 다음과 같습니다.</p>
<ul>
<li>Thread → 운영체제(OS)가 직접 관리하는 실제 작업 단위</li>
<li>Coroutine → 하나의 Thread 위에서 여러 작업을 효율적으로 처리하는 방식</li>
</ul>
<p>Thread를 사용할 경우 작업을 직접 생성하고, 실행 흐름과 스레드 전환까지 모두 관리해야 합니다.</p>
<pre><code class="language-kotlin">Thread {
    val result = doWork()

    Handler(Looper.getMainLooper()).post {
        updateUi(result)
    }
}.start()</code></pre>
<p>반면 Coroutine을 사용하면 이러한 과정을 더 간단하게 표현할 수 있습니다.</p>
<pre><code class="language-kotlin">lifecycleScope.launch {
    val result = withContext(Dispatchers.IO) {
        doWork()
    }
    updateUi(result)
}</code></pre>
<p>이처럼 Coroutine은 아래와 같은 특징을 가지고 있습니다.</p>
<ul>
<li>메인 스레드 전환을 자동으로 처리</li>
<li>코드 흐름을 순차적으로 유지</li>
<li>생명주기와도 자연스럽게 연동</li>
</ul>
<p>특히 안드로이드에서는 Android Jetpack의 lifecycleScope, viewModelScope 등을 통해
화면이 사라지면 자동으로 작업이 취소되도록 만들 수 있습니다.</p>
<hr>
<h3 id="🎯-마무리">🎯 마무리</h3>
<p>Thread는 안드로이드에서 비동기 작업을 처리하기 위한 가장 기본적인 방법입니다. 하지만 직접 관리해야 할 요소가 많기 때문에 실무에서는 점점 더 추상화된 방식이 사용되고 있습니다.</p>
<ul>
<li>Thread → 직접 생성하고 관리해야 하는 기본 도구</li>
<li>Coroutine → Thread 위에서 동작하며 더 간단하게 비동기 처리 가능</li>
</ul>
<p>안드로이드에서 안정적인 UI를 만들기 위해서는 메인 스레드를 블로킹하지 않는 것이 핵심입니다. Thread는 그 출발점이 되는 개념이며, 이후 Coroutine과 같은 기술을 이해하기 위한 중요한 기반이 됩니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Kotlin] Elvis]]></title>
            <link>https://velog.io/@marigold_/Kotlin-Elvis</link>
            <guid>https://velog.io/@marigold_/Kotlin-Elvis</guid>
            <pubDate>Mon, 23 Mar 2026 13:15:30 GMT</pubDate>
            <description><![CDATA[<p>Kotlin에서는 null을 안전하게 처리하고 기본값을 지정하기 위해 <strong>엘비스 연산자(Elvis Operator, ?:)</strong>를 자주 사용합니다.</p>
<p>특히 안드로이드나 코딩 테스트, 일반 Kotlin 프로젝트에서도 null 처리 코드를 훨씬 간결하게 만들어주는 핵심 기능입니다.</p>
<hr>
<h3 id="🚀-elvis-연산자란">🚀 Elvis 연산자란?</h3>
<p>Elvis 연산자(?:)는 값이 null일 경우 대체 값을 반환하는 연산자입니다.</p>
<pre><code class="language-kotlin">val name: String? = null

val result = name ?: &quot;default name&quot;

println(result) // default name</code></pre>
<h4 id="동작-과정">동작 과정</h4>
<ol>
<li>name이 null이 아니면 → 그대로 사용</li>
<li>name이 null이면 → 오른쪽 값 반환</li>
</ol>
<p>즉, null일 때 안전하게 기본값을 설정할 수 있습니다.</p>
<hr>
<h3 id="🎯-결론">🎯 결론</h3>
<p>Elvis 연산자는 단순한 문법이 아니라</p>
<ul>
<li>null 안전 처리</li>
<li>기본값 지정</li>
<li>코드 간결화</li>
</ul>
<p>를 위한 Kotlin의 핵심 기능입니다.</p>
<p>특히 null 처리가 많은 Kotlin 코드에서 반드시 익숙해져야 하는 필수 문법입니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Kotlin] let, run, apply]]></title>
            <link>https://velog.io/@marigold_/Kotlin-let-run-apply</link>
            <guid>https://velog.io/@marigold_/Kotlin-let-run-apply</guid>
            <pubDate>Sun, 22 Mar 2026 17:03:57 GMT</pubDate>
            <description><![CDATA[<p>Kotlin에서는 null을 안전하게 처리하고 기본값을 지정하기 위해 <strong>엘비스 연산자(Elvis Operator, ?:)</strong>를 자주 사용합니다.</p>
<p>특히 안드로이드나 코딩 테스트, 일반 Kotlin 프로젝트에서도 null 처리 코드를 훨씬 간결하게 만들어주는 핵심 기능입니다.</p>
<hr>
<h3 id="🚀-elvis-연산자란">🚀 Elvis 연산자란?</h3>
<p>Elvis 연산자(?:)는 값이 null일 경우 대체 값을 반환하는 연산자입니다.</p>
<pre><code class="language-kotlin">val name: String? = null

val result = name ?: &quot;default name&quot;

println(result) // default name</code></pre>
<h4 id="동작-과정">동작 과정</h4>
<ol>
<li>name이 null이 아니면 → 그대로 사용</li>
<li>name이 null이면 → 오른쪽 값 반환</li>
</ol>
<p>즉, null일 때 안전하게 기본값을 설정할 수 있습니다.</p>
<hr>
<h3 id="🎯-결론">🎯 결론</h3>
<p>Elvis 연산자는 단순한 문법이 아니라</p>
<ul>
<li>null 안전 처리</li>
<li>기본값 지정</li>
<li>코드 간결화</li>
</ul>
<p>를 위한 Kotlin의 핵심 기능입니다.</p>
<p>특히 null 처리가 많은 Kotlin 코드에서 반드시 익숙해져야 하는 필수 문법입니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[CS] Process, Thread]]></title>
            <link>https://velog.io/@marigold_/CS-Process-Thread</link>
            <guid>https://velog.io/@marigold_/CS-Process-Thread</guid>
            <pubDate>Wed, 25 Feb 2026 10:02:59 GMT</pubDate>
            <description><![CDATA[<p>개발을 하다 보면 다음과 같은 상황을 자주 마주합니다.</p>
<ul>
<li>하나의 프로그램에서 여러 작업을 동시에 처리해야 하거나</li>
<li>작업 간 자원을 효율적으로 공유해야 하거나</li>
<li>성능 향상을 위해 병렬 처리가 필요하거나</li>
</ul>
<p>이런 문제를 이해하기 위해 반드시 알아야 할 개념이 바로 Process와 Thread 입니다. 이번 글에서는 Process와 Thread가 무엇인지, 왜 필요한지, 그리고 어떻게 동작하는지를 알아보겠습니다.</p>
<hr>
<h3 id="🚀-process란">🚀 Process란?</h3>
<p>Process(프로세스)는 실행 중인 프로그램을 의미합니다. 단순한 프로그램 파일(.exe, .app 등)이 아니라, 메모리에 올라가 CPU 자원을 할당받아 실제로 실행되고 있는 상태를 말합니다.</p>
<blockquote>
<p>프로그램 실행 → 운영체제가 메모리에 적재 → 하나의 Process 생성</p>
</blockquote>
<p>각 프로세스는 독립적인 메모리 공간을 가지며, 다른 프로세스와 기본적으로 자원을 공유하지 않습니다.</p>
<hr>
<h3 id="💡왜-process가-필요할까">💡왜 Process가 필요할까?</h3>
<h4 id="1-프로그램의-독립성-보장">1. 프로그램의 독립성 보장</h4>
<ul>
<li>각 프로세스는 독립된 메모리 공간 보유</li>
<li>하나의 프로세스가 오류로 종료되어도 다른 프로세스에는 영향 없음</li>
<li>안정성 확보</li>
</ul>
<h4 id="2-운영체제의-자원-관리">2. 운영체제의 자원 관리</h4>
<ul>
<li>CPU, 메모리, 파일 등 자원을 프로세스 단위로 관리</li>
<li>실행, 대기, 종료 등의 상태 관리 가능</li>
</ul>
<h4 id="3-동시-실행-환경-제공">3. 동시 실행 환경 제공</h4>
<ul>
<li>여러 프로그램을 동시에 실행 가능</li>
<li>브라우저, 음악 플레이어, 게임을 동시에 실행해도 되는 이유</li>
</ul>
<hr>
<h3 id="🚀-thread란">🚀 Thread란?</h3>
<p>Thread(스레드)는 프로세스 내에서 실행되는 작업의 흐름 단위입니다. 하나의 프로세스 안에는 여러 개의 스레드가 존재할 수 있으며, 이를 멀티스레딩(Multi-threading)이라고 합니다.</p>
<blockquote>
<p>하나의 Process
├─ Thread 1
├─ Thread 2
└─ Thread 3</p>
</blockquote>
<p>스레드는 프로세스의 메모리 공간을 공유합니다.</p>
<hr>
<h3 id="💡왜-thread가-필요할까">💡왜 Thread가 필요할까?</h3>
<h4 id="1-성능-향상">1. 성능 향상</h4>
<ul>
<li>여러 작업을 동시에 처리 가능</li>
<li>멀티코어 CPU 환경에서 병렬 처리 가능</li>
</ul>
<h4 id="2-자원-공유-용이">2. 자원 공유 용이</h4>
<ul>
<li>같은 프로세스 내 메모리 공유</li>
<li>데이터 전달 비용이 적음</li>
</ul>
<h4 id="3-사용자-경험-개선">3. 사용자 경험 개선</h4>
<ul>
<li>한 작업이 오래 걸려도 다른 작업은 계속 수행 가능
예: 파일 다운로드 중에도 UI는 멈추지 않음</li>
</ul>
<hr>
<h3 id="🚀-process-vs-thread-핵심-차이">🚀 Process vs Thread 핵심 차이</h3>
<table>
<thead>
<tr>
<th>구분</th>
<th>Process</th>
<th>Thread</th>
</tr>
</thead>
<tbody><tr>
<td>메모리</td>
<td>독립적</td>
<td>프로세스 내에서 공유</td>
</tr>
<tr>
<td>생성 비용</td>
<td>큼</td>
<td>상대적으로 작음</td>
</tr>
<tr>
<td>안정성</td>
<td>높음</td>
<td>한 스레드 오류가 전체 프로세스에 영향 가능</td>
</tr>
<tr>
<td>통신 방식</td>
<td>IPC 필요</td>
<td>메모리 공유</td>
</tr>
</tbody></table>
<hr>
<h3 id="🎯-마무리">🎯 마무리</h3>
<p>Process와 Thread는 단순한 실행 단위의 차이를 넘어, 안정성과 성능 사이의 균형을 결정하는 핵심 개념입니다.</p>
<p>정리하면 Process는 독립성과 안정성을 제공하고, Thread는 자원 공유와 성능 향상에 유리합니다. 하지만 스레드는 동기화 문제(경쟁 상태, 데드락 등)를 유발할 수 있어 추가적인 관리가 필요하다는 단점도 존재합니다.</p>
<p>현대 소프트웨어 개발 환경에서 멀티스레딩은 거의 기본 전제 조건이며, 백엔드·프론트엔드·모바일·게임 개발자 모두가 반드시 이해해야 할 개념입니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Kotlin] BufferedReader]]></title>
            <link>https://velog.io/@marigold_/Kotlin-BufferedReader</link>
            <guid>https://velog.io/@marigold_/Kotlin-BufferedReader</guid>
            <pubDate>Wed, 11 Feb 2026 15:08:15 GMT</pubDate>
            <description><![CDATA[<p>Kotlin에서 대량의 입력을 빠르게 처리하려면 BufferedReader를 사용하는 것이 일반적입니다.
특히 코딩 테스트처럼 입력이 많은 환경에서는 기본 readLine()보다 훨씬 뛰어난 성능을 제공합니다.</p>
<p>하지만 BufferedReader는 단독으로 사용되지 않습니다.
System.in과 InputStreamReader를 함께 사용하여 입력 스트림을 효율적으로 처리합니다.</p>
<hr>
<h3 id="🚀-bufferedreader란">🚀 BufferedReader란?</h3>
<p>BufferedReader는 입력 데이터를 버퍼에 저장해 두었다가 한 줄씩 읽어오는 클래스입니다.
입출력(I/O)은 비용이 큰 작업이기 때문에, 한 번에 많이 읽어두고 처리하는 것이 성능상 유리합니다.</p>
<hr>
<h3 id="🚀-systemin이란">🚀 System.in이란?</h3>
<p>System.in은 표준 입력(Standard Input) 스트림입니다. 쉽게 말해, 우리가 콘솔에서 입력하는 데이터를 프로그램으로 전달해주는 통로입니다.</p>
<ul>
<li>타입: InputStream</li>
<li>바이트(byte) 단위로 데이터를 읽음</li>
</ul>
<p>즉, System.in은 문자열이 아니라 바이트 스트림입니다.</p>
<hr>
<h3 id="🚀-inputstreamreader란">🚀 InputStreamReader란?</h3>
<p>InputStreamReader는 바이트 스트림을 문자 스트림으로 변환해주는 역할을 합니다.</p>
<ul>
<li>System.in → 바이트 입력</li>
<li>InputStreamReader → 바이트를 문자로 변환</li>
<li>BufferedReader → 변환된 문자를 버퍼에 저장 후 한 줄씩 읽기</li>
</ul>
<p>즉, 데이터 흐름은 다음과 같습니다.</p>
<blockquote>
<p>System.in (바이트 입력) -&gt; InputStreamReader (문자 변환) -&gt; BufferedReader (버퍼링 + 줄 단위 읽기)</p>
</blockquote>
<hr>
<h3 id="💻-사용-예시">💻 사용 예시</h3>
<pre><code class="language-kotlin">import java.io.BufferedReader
import java.io.InputStreamReader

val br = BufferedReader(InputStreamReader(System.`in`))

val n = br.readLine().toInt()
val input = br.readLine()</code></pre>
<h4 id="동작-과정">동작 과정</h4>
<ol>
<li>System.in이 콘솔 입력을 바이트 형태로 받음</li>
<li>InputStreamReader가 이를 문자로 변환</li>
<li>BufferedReader가 내부 버퍼에 저장</li>
<li>readLine()으로 한 줄씩 빠르게 읽어옴</li>
</ol>
<hr>
<h3 id="⚡-왜-빠를까">⚡ 왜 빠를까?</h3>
<p>기본 readLine()은 내부적으로 입력 스트림에 직접 접근하는 방식이라 입력이 많으면 느려질 수 있습니다. 반면 BufferedReader는</p>
<ul>
<li>한 번에 많은 데이터를 읽음</li>
<li>I/O 호출 횟수를 줄임</li>
<li>객체 생성 최소화</li>
<li>대량 입력에서 압도적인 성능 차이</li>
</ul>
<p>특히 수만~수십만 줄 입력을 처리할 때 속도 차이가 확연히 드러납니다.</p>
<hr>
<p>🎯 결론</p>
<p>BufferedReader는 단순한 입력 도구가 아니라</p>
<ul>
<li>System.in (바이트 입력)</li>
<li>InputStreamReader (문자 변환)</li>
<li>BufferedReader (버퍼링 처리)</li>
</ul>
<p>이 세 가지가 함께 동작하는 구조입니다.
다음과 같은 경우 반드시 사용하는 것이 좋습니다.</p>
<ul>
<li>코딩 테스트</li>
<li>대량의 파일/콘솔 입력 처리</li>
<li>성능이 중요한 프로그램</li>
</ul>
<p>입력이 많다면 readLine() 대신 BufferedReader 조합을 기본값처럼 사용하는 것이 좋습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Kotlin] StringTokenizer]]></title>
            <link>https://velog.io/@marigold_/Kotlin-StringTokenizer</link>
            <guid>https://velog.io/@marigold_/Kotlin-StringTokenizer</guid>
            <pubDate>Mon, 02 Feb 2026 08:54:47 GMT</pubDate>
            <description><![CDATA[<p>Kotlin의 StringTokenizer는 문자열을 효율적으로 분리하여 처리할 수 있는 클래스입니다.
공백이나 특정 구분자를 기준으로 문자열을 나누는 작업이 잦은 상황에서 split()보다 훨씬 뛰어난 성능을 제공하며, 반복적인 문자열 파싱이 필요한 경우 유용하게 사용됩니다.</p>
<p>StringTokenizer는 문자열을 한 번에 나누어 배열을 생성하지 않고, 내부적으로 토큰의 위치만 관리하면서 순차적으로 처리하기 때문에 불필요한 객체 생성을 줄일 수 있습니다.</p>
<hr>
<h3 id="🚀-stringtokenizer란">🚀 StringTokenizer란?</h3>
<p>StringTokenizer는 Kotlin의 문자열 분리 클래스입니다. 일반적인 String.split()은 문자열을 분리할 때마다 여러 개의 String 객체와 컬렉션을 생성하지만, StringTokenizer는 하나의 문자열을 유지한 채 토큰을 하나씩 꺼내어 처리합니다.</p>
<p>문자열을 반복적으로 파싱하는 경우, StringTokenizer의 성능 차이가 확실하게 드러납니다.</p>
<pre><code class="language-kotlin">val input = &quot;1 3&quot;
val parts = input.split(&quot; &quot;)

val command = parts[0].toInt()
val value = parts[1].toInt()</code></pre>
<p>이 코드는 split() 호출 시 문자열 배열이 생성되며, 분리된 각 요소 또한 새로운 String 객체로 만들어집니다.</p>
<pre><code class="language-kotlin">import java.util.StringTokenizer

val input = &quot;1 3&quot;
val st = StringTokenizer(input)

val command = st.nextToken().toInt()
val value = st.nextToken().toInt()</code></pre>
<p>StringTokenizer는 내부 문자열을 그대로 유지하면서 토큰을 순차적으로 반환하므로 훨씬 효율적입니다. 문자열 배열을 생성하지 않기 때문에 객체 생성 비용 감소 및 GC 부담 감소, 그리고 메모리 사용량이 감소합니다.</p>
<hr>
<h3 id="🎯-결론">🎯 결론</h3>
<p>StringTokenizer는 문자열을 자주 분리하거나 반복적으로 파싱해야 하는 상황에서 split()보다 훨씬 빠르고 효율적인 선택입니다.</p>
<ul>
<li>공백으로 구분된 입력을 반복 처리할 때</li>
<li>대량의 문자열 입력을 파싱해야 할 때</li>
<li>성능이 중요한 문자열 처리 로직</li>
</ul>
<p>이러한 경우에는 split() 대신 StringTokenizer 사용을 우선적으로 고려하는 것이 좋습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Android] Navigation3]]></title>
            <link>https://velog.io/@marigold_/Android-Navigation3</link>
            <guid>https://velog.io/@marigold_/Android-Navigation3</guid>
            <pubDate>Tue, 20 Jan 2026 13:37:58 GMT</pubDate>
            <description><![CDATA[<p>Jetpack Compose로 UI를 구현하다 보면 화면 전환을 위해 navigation-compose를 거의 필수적으로 사용하게 됩니다. 하지만 Navigation2는 Compose와 함께 쓰기에는 다소 아쉬운 점들이 있었고, 복잡한 앱일수록 구조 관리가 쉽지 않았습니다.</p>
<p>이런 문제를 해결하기 위해 Jetpack Compose Navigation3 (이하 Navigation3) 가 등장했습니다. 이번 글에서는 Navigation3가 기존 Navigation2에 비해 무엇이 좋아졌는지, 그리고 기본적인 사용 방법을 정리해 보려고 합니다.</p>
<hr>
<h3 id="🚧-navigation2의-한계">🚧 Navigation2의 한계</h3>
<p>Navigation2는 XML 기반 Navigation 개념을 Compose로 가져온 형태에 가까웠습니다. 그래서 다음과 같은 불편함이 있었습니다.</p>
<h4 id="1-navcontroller-중심의-명령형-api">1. NavController 중심의 명령형 API</h4>
<pre><code class="language-kotlin">navController.navigate(&quot;detail/123&quot;)</code></pre>
<ul>
<li>문자열 기반 route</li>
<li>컴파일 타임 안전성 부족</li>
<li>파라미터 실수 → 런타임 에러</li>
</ul>
<h4 id="2-화면-상태와-네비게이션-상태의-분리">2. 화면 상태와 네비게이션 상태의 분리</h4>
<ul>
<li>ViewModel의 상태와 Navigation 상태가 따로 놀기 쉬움</li>
<li>“이 화면이 왜 열렸지?”를 추적하기 어려움</li>
</ul>
<h4 id="3-멀티-백스택-복잡한-그래프-구성의-어려">3. 멀티 백스택, 복잡한 그래프 구성의 어려</h4>
<ul>
<li>BottomNavigation + Nested graph 구성 시 코드 가독성 급격히 하락</li>
<li>테스트하기도 까다로움</li>
</ul>
<hr>
<h3 id="🚀-navigation3란">🚀 Navigation3란?</h3>
<p>Navigation 3는 Compose의 상태 중심(State-driven) 패러다임에 맞춰 완전히 새롭게 설계된 네비게이션 API입니다.</p>
<ul>
<li>안전한 타입 모델</li>
<li>NavController 의존도 감소</li>
<li>Navigation 상태를 UI State처럼 다룸</li>
<li>테스트와 구조화에 훨씬 유리</li>
</ul>
<hr>
<h3 id="🌱-navigation3의-핵심-개념">🌱 Navigation3의 핵심 개념</h3>
<h4 id="1-커스텀-타입-모델">1. 커스텀 타입 모델</h4>
<p>Navigation 3에서는 문자열 route 대신 타입으로 화면을 정의합니다.</p>
<pre><code class="language-kotlin">sealed interface Screen {
    data object Home : Screen
    data class Detail(val id: Long) : Screen
}</code></pre>
<ul>
<li>컴파일 타임 안전성</li>
<li>파라미터 실수 방지</li>
<li>IDE 자동완성 지원</li>
</ul>
<h4 id="2-navigation-상태를-직접-관리">2. Navigation 상태를 직접 관리</h4>
<p>Navigation3에서는 화면 스택 자체를 상태(State) 로 관리합니다.</p>
<pre><code class="language-kotlin">val backStack = rememberNavBackStack(Screen.Home)

backStack += Screen.Detail(id = 1L)</code></pre>
<p>더 이상 navController.navigate()에 의존하지 않아도 됩니다.</p>
<h4 id="3-navhost-→-navdisplay">3. NavHost → NavDisplay</h4>
<p>Navigation3에서는 NavHost 대신 NavDisplay를 사용합니다.</p>
<pre><code class="language-kotlin">@Composable
fun App() {
    val backStack = rememberNavBackStack(Screen.Home)

    NavDisplay(
        backStack = backStack,
        onBack = { backStack.removeLastOrNull() }
    ) { screen -&gt;
        when (screen) {
            is Screen.Home -&gt; HomeScreen(
                onClick = {
                    backStack += Screen.Detail(1L)
                }
            )
            is Screen.Detail -&gt; DetailScreen(screen.id)
        }
    }
}</code></pre>
<p>또는 when문이 아닌 entryProvider를 사용할 수도 있습니다.</p>
<pre><code class="language-kotlin">@Composable
fun App() {
    val backStack = rememberNavBackStack(Screen.Home)

    NavDisplay(
        backStack = backStack,
        entryProvider = entryProvider {
            entry&lt;Screen.Home&gt; {
                HomeScreen(
                  onClick = {
                      backStack += Screen.Detail(1L)
                  }
                  )
            }
        }
    )
}</code></pre>
<ul>
<li>Compose다운 선언적 구조</li>
<li>화면 흐름을 한눈에 파악 가능</li>
</ul>
<hr>
<h3 id="🧩-naventrydecorator란">🧩 NavEntryDecorator란?</h3>
<p>Navigation3에서는 화면 하나하나를 NavEntry라는 단위로 다룹니다. 그리고 각 NavEntry에 대해 어떤 부가 동작을 적용할지를 결정하는 것이 NavEntryDecorator입니다. 대표적으로 많이 쓰이는 것이 바로 아래 두 가지입니다.</p>
<pre><code class="language-kotlin">rememberSaveableStateHolderNavEntryDecorator()
rememberViewModelStoreNavEntryDecorator()</code></pre>
<p>이 둘은 화면 상태와 ViewModel 생명주기 관리를 담당합니다.</p>
<h4 id="1-remembersaveablestateholdernaventrydecorator">1. rememberSaveableStateHolderNavEntryDecorator</h4>
<p>각 NavEntry마다 Saveable 상태를 분리해서 유지해 줍니다. 즉, 화면 A → 화면 B → 다시 A의 흐름대로 화면이 이동할 때, A의 rememberSaveable 상태가 유지됩니다. 만약 사용하지 않으면, 화면 이동 시 Composable이 dispose되어 다시 돌아오면 상태가 초기화됩니다. 입력 폼, 스크롤 위치, 탭 내부 상태 등을 유지해야할 때 사용합니다.</p>
<h4 id="2-rememberviewmodelstorenaventrydecorator">2. rememberViewModelStoreNavEntryDecorator</h4>
<p>각 NavEntry마다 독립적인 ViewModelStore를 제공합니다. 즉, 화면마다 ViewModel 생명주기가 명확히 분리되어 backStack에서 화면이 제거되면 ViewModel도 함께 clear됩니다. </p>
<p>실제 사용 시에는 둘을 함께 사용하는 것이 거의 표준입니다.</p>
<pre><code class="language-kotlin">NavDisplay(
    backStack = backStack,
    entryDecorators = listOf(
        rememberSaveableStateHolderNavEntryDecorator(),
        rememberViewModelStoreNavEntryDecorator()
    ),
    entryProvider = entryProvider {
        entry&lt;Screen.Home&gt; {
            HomeScreen()
        }
        entry&lt;Screen.Detail&gt; {
            DetailScreen()
        }
    }
)</code></pre>
<hr>
<h3 id="🎯-마무리">🎯 마무리</h3>
<p>Navigation 3는 단순한 API 개선이 아니라 Compose에 맞는 네비게이션 패러다임으로의 전환이라고 볼 수 있습니다.</p>
<ul>
<li>Navigation 2 → 명령형, 문자열 기반</li>
<li>Navigation 3 → 선언적, 상태 기반, 타입 안전</li>
</ul>
<p>Compose가 상태 중심 UI인 만큼, 화면 이동 또한 상태로 관리하는 Navigation 3의 방향성은 매우 자연스럽습니다. 아직은 실험 단계지만, 앞으로 Compose Navigation의 표준이 될 가능성이 높은 만큼 지금부터 개념 정도는 익혀두면 충분히 가치가 있다고 생각합니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[CS] NAT]]></title>
            <link>https://velog.io/@marigold_/CS-NAT</link>
            <guid>https://velog.io/@marigold_/CS-NAT</guid>
            <pubDate>Tue, 06 Jan 2026 13:05:50 GMT</pubDate>
            <description><![CDATA[<p>네트워크를 다루다 보면 다음과 같은 상황을 자주 마주합니다.</p>
<ul>
<li>하나의 공인 IP로 여러 기기가 인터넷에 접속해야 하거나</li>
<li>내부 네트워크 구조를 외부에 노출하고 싶지 않거나</li>
<li>서버는 내부에 있지만 외부 요청은 받아야 하는 경우</li>
</ul>
<p>이런 문제를 해결하기 위해 등장한 개념이 바로 NAT(Network Address Translation) 입니다. 이번 글에서는 NAT가 무엇인지, 왜 필요한지, 그리고 어떻게 동작하는지를 알아보겠습니다.</p>
<hr>
<h3 id="🚀-nat란">🚀 NAT란?</h3>
<p>NAT(Network Address Translation)는 IP 주소를 변환해주는 기술입니다. 주로 사설 IP ↔ 공인 IP 사이를 변환하는 역할을 하며, 공유기(라우터)에서 기본적으로 사용됩니다.</p>
<blockquote>
<p>내부 네트워크 (사설 IP)  →  NAT  →  인터넷 (공인 IP)</p>
</blockquote>
<hr>
<h3 id="왜-nat가-필요할까">왜 NAT가 필요할까?</h3>
<h4 id="1-ipv4-주소-부족-문제">1. IPv4 주소 부족 문제</h4>
<ul>
<li>IPv4 주소는 약 43억 개로 한정됨</li>
<li>모든 기기에 공인 IP를 할당하기에는 부족</li>
<li>여러 기기가 하나의 공인 IP를 공유해야 했고 이를 가능하게 만든 기술이 NAT</li>
</ul>
<h4 id="2-보안-강화">2. 보안 강화</h4>
<ul>
<li>내부 네트워크 구조를 외부에서 직접 볼 수 없음</li>
<li>외부에서 먼저 요청하지 않으면 내부 접근 불가</li>
<li>기본적인 방화벽 역할 수행</li>
</ul>
<h4 id="3-네트워크-구조-단순화">3. 네트워크 구조 단순화</h4>
<ul>
<li>내부 IP 변경에도 외부 설정 영향 최소화</li>
<li>서버 이전, 내부 구조 변경에 유연함</li>
</ul>
<p>NAT는 여러 기기가 하나의 공인 IP를 공유하게 만든 기술이며, 기본적인 방화벽 역할을 수행합니다.</p>
<hr>
<h3 id="🚀-nat의-주요-종류">🚀 NAT의 주요 종류</h3>
<h4 id="1-snat-source-nat">1. SNAT (Source NAT)</h4>
<ul>
<li>내부 → 외부로 나갈 때 사용</li>
<li>출발지 IP를 공인 IP로 변환</li>
<li>192.168.0.10 → 203.xxx.xxx.xxx</li>
</ul>
<h4 id="2-dnat-destination-nat">2. DNAT (Destination NAT)</h4>
<ul>
<li>외부 → 내부로 들어올 때 사용</li>
<li>목적지 IP를 내부 IP로 변환</li>
<li>흔히 포트 포워딩이라고 부름</li>
<li>203.xxx.xxx.xxx:8080 → 192.168.0.10:8080</li>
</ul>
<h4 id="3-pat-port-address-translation">3. PAT (Port Address Translation)</h4>
<ul>
<li>IP + 포트 번호까지 함께 변환</li>
<li>가정용 공유기에서 가장 많이 사용</li>
<li>공인 IP 하나 + 여러 포트 → 여러 내부 기기</li>
</ul>
<hr>
<h3 id="🎯-마무리">🎯 마무리</h3>
<p>NAT는 단순한 주소 변환 기술이 아니라,  인터넷의 확장성과 보안을 동시에 해결한 핵심 기술입니다.</p>
<p>정리하면 NAT는 사설 IP와 공인 IP를 변환함으로써 IP 자원을 절약하고 기본적인 보안을 제공하지만, 서버 운영이나 실시간 통신 환경에서는 추가 설정(포트 포워딩)이 필요하다는 단점도 존재합니다.</p>
<p>현대 네트워크 환경에서 NAT는 거의 필수 전제 조건이며, 서버 개발자·모바일 개발자·게임 개발자 모두가 기본적으로 이해하고 있어야 할 개념입니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Android] WithTimeout, WithTimeoutOrNull]]></title>
            <link>https://velog.io/@marigold_/Android-WithTimeout-WithTimeoutOrNull</link>
            <guid>https://velog.io/@marigold_/Android-WithTimeout-WithTimeoutOrNull</guid>
            <pubDate>Tue, 30 Dec 2025 06:08:35 GMT</pubDate>
            <description><![CDATA[<p>네트워크 요청, DB 조회, 사용자 입력 대기 등 언제 끝날지 알 수 없는 작업을 다루는 경우가 있습니다. 이런 작업이 오래 걸리면</p>
<ul>
<li>화면이 멈춘 것처럼 보이거나</li>
<li>불필요한 리소스를 계속 사용하거나</li>
<li>UX 저해</li>
</ul>
<p>위와 같은 문제가 발생할 수 있습니다. Coroutine에서는 이런 문제를 해결하기 위해 WithTimeout과 WithTimeoutOrNull을 제공합니다. 이번 글에서는 WithTimeout과 WithTimeoutOrNull이 무엇이고, 어떻게 사용하는지 알아보겠습니다.</p>
<hr>
<h3 id="🚀-withtimeout이란">🚀 WithTimeout이란?</h3>
<p>WithTimeout은 지정한 시간 안에 작업이 끝나지 않으면 코루틴(Coroutine)을 자동으로 취소하는 기능입니다. 코루틴은 기본적으로 취소 가능한 구조(cancellable) 를 가지고 있고, WithTimeout은 이 특성을 이용해 시간 제한을 안전하게 적용합니다.</p>
<pre><code class="language-kotlin">try {
    withTimeout(3000) {
        delay(5000)
        println(&quot;실행되지 않음&quot;)
    }
} catch (e: TimeoutCancellationException) {
    println(&quot;타임아웃 발생! 하지만 프로그램은 계속 실행됩니다.&quot;)
}</code></pre>
<p>시간이 초과되면 TimeoutCancellationException이 발생하며 try-catch를 통해 프로그램이 죽지 않게 하거나 후속 작업이 작동되도록 할 수 있습나다.</p>
<hr>
<h3 id="🚀-withtimeoutornull이란">🚀 WithTimeoutOrNull이란?</h3>
<p>WithTimeoutOrNull은 시간이 초과되면 null을 반환합니다.</p>
<pre><code class="language-kotlin">val result = withTimeoutOrNull(3000) {
    delay(5000)
    &quot;성공&quot;
}

println(result) // null</code></pre>
<hr>
<h3 id="⚠️-timeout이-동작하지-않는-경우">⚠️ Timeout이 동작하지 않는 경우</h3>
<p>Timeout은 취소 가능한 suspend 함수에서만 정상 동작합니다.</p>
<pre><code class="language-kotlin">withTimeout(1000) {
    Thread.sleep(5000) // ❌ 취소 불가
}

withTimeout(1000) {
    delay(5000) // ✅ 취소 가능
}</code></pre>
<hr>
<h3 id="🎯-마무리">🎯 마무리</h3>
<p>Timeout은 단순히 “시간 제한” 기능이 아니라 UI를 안전하게 보호하기 위한 필수 장치입니다.</p>
<ul>
<li>withTimeout → 시간 초과 시 예외</li>
<li>withTimeoutOrNull → 시간 초과 시 null</li>
</ul>
<p>Compose와 ViewModel 기반의 구조에서는 timeout을 명시적으로 설정하는 것만으로도 UI 안정성이 크게 향상됩니다. 상태 중심 UI인 만큼, 언제 값을 수집할지뿐 아니라 언제 포기할지도 명확히 하는 것이 중요합니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Android] CollectAsStateWithLifecycle]]></title>
            <link>https://velog.io/@marigold_/Android-CollectAsStateWithLifecycle</link>
            <guid>https://velog.io/@marigold_/Android-CollectAsStateWithLifecycle</guid>
            <pubDate>Mon, 29 Dec 2025 10:18:59 GMT</pubDate>
            <description><![CDATA[<p>UI를 구현하다 보면 Flow나 StateFlow로부터 값을 수집해서 화면에 상태를 반영해야 하는 경우가 많습니다. Jetpack Compose에서는 이를 위해 collectAsState()를 자주 사용합니다.</p>
<p>하지만 화면의 생명주기를 고려하지 않고 Flow를 수집하면, 화면이 보이지 않는 상태에서도 계속 값이 수집되거나 불필요한 연산이 발생할 수 있습니다.</p>
<p>이런 문제를 해결하기 위해 Compose에서는collectAsStateWithLifecycle 이라는 API를 제공합니다. 이번 글에서는 collectAsStateWithLifecycle이 무엇인지, 그리고 실제 UI 구현에서 왜 중요한지 정리해보려고 합니다.</p>
<hr>
<h3 id="🚀-collectasstatewithlifecycle">🚀 CollectAsStateWithLifecycle?</h3>
<p>CollectAsStateWithLifecycle은 Flow를 Lifecycle-aware하게 수집하도록 도와주는 함수입니다. 이 함수는 내부적으로 repeatOnLifecycle을 사용하여, Lifecycle이 STARTED 이상일 때만 Flow를 수집하고 화면이 STOPPED 상태가 되면 자동으로 수집을 중단합니다. 즉, UI가 실제로 보이고 있을 때만 상태를 수집하도록 보장해 줍니다.</p>
<pre><code class="language-kotlin">class MainViewModel : ViewModel() {
    private val _uiState = MutableStateFlow(UiState())
    val uiState: StateFlow&lt;UiState&gt; = _uiState
}

@Composable
fun MainScreen(viewModel: MainViewModel) {
    val uiState by viewModel.uiState.collectAsStateWithLifecycle()

    Text(text = uiState.title)
}</code></pre>
<p>ViewModel에서 StateFlow로 UI 상탤르 관리한다고 가정하면 위와 같이 코드를 작성할 수 있습니다. 이렇게 하면 화면이 보일 때만 상태를 수집하고 화면이 사라지면 자동으로 수집을 중단하며, 다시 돌아왔을 때 최신 상태를 안전하게 반영할 수 있습니다.</p>
<p>CollectAsStateWithLifecycle은 Compose UI에서는 사실상 표준이라고 봐도 됩니다.</p>
<hr>
<h3 id="🎯-마무리">🎯 마무리</h3>
<p>collectAsStateWithLifecycle은 단순히 편의 함수가 아니라 UI 안정성과 성능을 동시에 챙길 수 있는 필수 도구입니다.</p>
<ul>
<li>collectAsState → 상태 수집은 되지만 lifecycle은 고려하지 않음</li>
<li>collectAsStateWithLifecycle → 화면이 살아 있을 때만 안전하게 수집</li>
</ul>
<p>Jetpack Compose가 상태 중심 UI인 만큼, 상태를 언제 수집할 것인가도 매우 중요한 요소입니다. Compose에서 Flow를 UI로 연결할 때는 습관처럼 collectAsStateWithLifecycle을 사용하는 것이 가장 안전한 선택이 될 것입니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Android] Debounce, DistinctUntilChanged]]></title>
            <link>https://velog.io/@marigold_/Android-Debounce-DistinctUntilChanged</link>
            <guid>https://velog.io/@marigold_/Android-Debounce-DistinctUntilChanged</guid>
            <pubDate>Wed, 24 Dec 2025 16:13:13 GMT</pubDate>
            <description><![CDATA[<p>UI를 구현하다 보면 사용자 입력이나 상태 변경에 따라 이벤트가 아주 자주 발생하는 경우가 많습니다.
예를 들어 검색 입력, 스크롤 이벤트, 버튼 연타, 상태 변화 감지 등은 그대로 처리하면 불필요한 연산이나 네트워크 호출로 이어지기 쉽습니다.</p>
<p>이런 문제를 해결하기 위해 Kotlin Flow에서는 Debounce, DistinctUntilChanged와 같은 연산자를 제공합니다.</p>
<p>이번 글에서는 Flow에서 자주 사용되는 Debounce와 DistinctUntilChanged가 각각 어떤 역할을 하는지, 그리고 실제 UI 구현에서 어떻게 활용할 수 있는지 정리해보려고 합니다.</p>
<hr>
<h3 id="🚀-debounce란">🚀 Debounce란?</h3>
<p>debounce는 연속적으로 발생하는 이벤트 중 마지막 이벤트만 일정 시간 후에 처리하도록 도와주는 연산자입니다. 즉, 이벤트가 계속 들어오고 있다면 처리를 미루고, 일정 시간 동안 추가 이벤트가 없을 때만 값을 방출합니다.</p>
<pre><code class="language-kotlin">flow
    .debounce(300)
    .collect { value -&gt;
        // 처리 로직
    }</code></pre>
<p>debounce를 사용하는 가장 대표적인 예시는 검색 입력입니다. 사용자가 키보드를 입력할 때마다 API를 호출하면 비효율적이기 때문에, 입력이 멈춘 뒤 일정 시간이 지난 후에만 검색 요청을 보내는 것이 일반적입니다.</p>
<pre><code class="language-kotlin">searchQuery
    .debounce(500)
    .collect { query -&gt;
        search(query)
    }</code></pre>
<p>이렇게 하면 사용자가 &quot;compose&quot;를 입력할 때
c → co → com → comp ... 마다 요청을 보내는 것이 아니라,
입력이 끝난 뒤 한 번만 요청하게 됩니다.</p>
<hr>
<h3 id="🚀-distinctuntilchanged란">🚀 DistinctUntilChanged란?</h3>
<p>distinctUntilChanged는 이전 값과 동일한 값이 연속으로 들어오면 무시하는 연산자입니다.</p>
<pre><code class="language-kotlin">flow
    .distinctUntilChanged()
    .collect { value -&gt;
        // 처리 로직
    }</code></pre>
<p>distinctUnitlChanged를 사용하여 같은 값이 반복해서 emit되는 경우를 방지할 수 있습니다. 같은 검색어가 다시 설정되는 경우, 같은 UI상태가 재전달 되는 경우, recomposition 과정에서 동일한 값이 다시 흘러오는 경우의 상황에서 distinctUntilChanged를 사용하면 실제로 값이 변경된 경우에만 처리할 수 있습니다. 불필요한 UI 업데이트나 사이드 이펙트를 줄이는 데 매우 유용합니다.</p>
<hr>
<h3 id="🔗-debounce--distinctuntilchanged-함께-사용하기">🔗 debounce + distinctUntilChanged 함께 사용하기</h3>
<p>실제 UI에서는 이 두 연산자를 함께 사용하는 경우가 가장 많습니다. 예를 들어 검색 기능을 구현할 때는 같은 검색어는 다시 처리하지 않고 입력이 멈췄을 때만 요청을 보내고 싶습니다.</p>
<pre><code class="language-kotlin">searchQuery
    .debounce(500)
    .distinctUntilChanged()
    .collect { query -&gt;
        search(query)
    }</code></pre>
<p>이 조합을 사용하면</p>
<ul>
<li>사용자가 빠르게 입력해도 마지막 값만 처리되고</li>
<li>같은 검색어로는 중복 호출이 발생하지 않습니다.</li>
</ul>
<p>결과적으로 네트워크 요청 수 감소 + UX 개선이라는 두 가지 효과를 동시에 얻을 수 있습니다.</p>
<hr>
<h3 id="🎯-마무리">🎯 마무리</h3>
<p>debounce와 distinctUntilChanged는 단순한 Flow 연산자이지만, UI 성능과 사용자 경험에 큰 영향을 미치는 도구입니다.</p>
<ul>
<li>debounce → 너무 자주 발생하는 이벤트를 정리</li>
<li>distinctUntilChanged → 의미 없는 중복 처리를 제거</li>
</ul>
<p>UI에서 “얼마나 많은 이벤트가 발생했는가”보다는 “정말로 처리해야 할 이벤트인가”에 초점을 맞춘다면, 이 두 연산자는 매우 강력한 무기가 될 수 있습니다.</p>
<p>특히 Jetpack Compose처럼 상태 중심 UI에서는 불필요한 이벤트를 줄이는 것이 곧 성능 최적화로 이어지기 때문에 적절한 위치에 debounce와 distinctUntilChanged를 사용하는 습관을 들여두면 많은 도움이 됩니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Android] VisibleItemsInfo, ViewportStartOffset, ViewportEndOffset]]></title>
            <link>https://velog.io/@marigold_/Android-VisibleItemsInfo-ViewportStartOffset-ViewportEndOffset</link>
            <guid>https://velog.io/@marigold_/Android-VisibleItemsInfo-ViewportStartOffset-ViewportEndOffset</guid>
            <pubDate>Tue, 23 Dec 2025 12:26:05 GMT</pubDate>
            <description><![CDATA[<p>UI를 구현하다 보면 사용자 입력이나 상태 변경에 따라 이벤트가 아주 자주 발생하는 경우가 많습니다. 예를 들어 검색 입력, 스크롤 이벤트, 버튼 연타, 상태 변화 감지 등은 그대로 처리하면 불필요한 연산이나 네트워크 호출로 이어지기 쉽습니다.</p>
<p>이런 문제를 해결하기 위해 Kotlin Flow에서는 debounce, distinctUntilChanged와 같은 연산자를 제공합니다.</p>
<p>이번 글에서는 Flow에서 자주 사용되는 Debounce와 distinctUntilChanged가 각각 어떤 역할을 하는지,
그리고 실제 UI 구현에서 어떻게 활용할 수 있는지 정리해보려고 합니다.</p>
<hr>
<h3 id="🚀-visibleiteminfo란">🚀 VisibleItemInfo란?</h3>
<p>visibleItemsInfo는 현재 화면(Viewport)에 실제로 표시되고 있는 아이템들의 정보를 담고 있는 리스트입니다. LazyColumn 또는 LazyRow의 스크롤 상태를 관리하는 LazyListState의 layoutInfo를 통해 접근할 수 있습니다.</p>
<pre><code class="language-kotlin">val listState = rememberLazyListState()
val visibleItemsInfo = listState.layoutInfo.visibleItemsInfo</code></pre>
<p>visibleItemsInfo는 LazyListItemInfo 객체의 리스트로 구성되어 있으며,</p>
<p>각 아이템은 다음과 같은 정보를 포함합니다.</p>
<ul>
<li>index : 아이템의 인덱스</li>
<li>offset : Viewport 기준 아이템의 시작 위치 (px)</li>
<li>size : 아이템의 크기 (px)</li>
<li>key : 아이템에 지정된 key (있을 경우)</li>
</ul>
<p>이 값을 활용하면 특정 아이템이 화면에 노출되고 있는지 확인하거나, 마지막 아이템이 보이는 시점에 페이징 처리와 같은 UI 제어가 가능합니다.</p>
<pre><code class="language-kotlin">val isItemVisible = visibleItemsInfo.any { it.index == targetIndex }</code></pre>
<p>visibleItemsInfo는 리스트의 스크롤 위치를 기준으로 추가 데이터를 로드해야 하는 상황에서도 자주 활용됩니다. 현재 화면에 보이는 아이템 중 가장 마지막 아이템의 인덱스를 확인하면, 사용자가 리스트의 하단에 도달했는지를 판단할 수 있습니다.</p>
<pre><code class="language-kotlin">val layoutInfo = listState.layoutInfo
val visibleItemsInfo = layoutInfo.visibleItemsInfo

val lastVisibleItemIndex =
    visibleItemsInfo.lastOrNull()?.index ?: 0

if (lastVisibleItemIndex == layoutInfo.totalItemsCount - 1) {
    // 다음 페이지 데이터 로드
}</code></pre>
<p>이 방식은 마지막 아이템이 화면에 보이기 시작하는 시점에 이벤트를 발생시키기 때문에,
스크롤이 끝까지 도달한 이후가 아닌 자연스러운 페이징 UX를 구현할 수 있습니다.</p>
<hr>
<h3 id="🚀-viewportstartoffset란">🚀 ViewportStartOffset란?</h3>
<p>viewportStartOffset은 스크롤 가능한 영역(Viewport)의 시작 지점을 의미합니다.
즉, 아이템들이 화면에 보이기 시작하는 기준 위치(px)입니다.</p>
<pre><code class="language-kotlin">val listState = rememberLazyListState()
val viewportStartOffset = listState.layoutInfo.viewportStartOffset</code></pre>
<p>일반적으로는 0 값을 가지지만, contentPadding이 설정된 경우에는 음수 값이 될 수 있습니다.</p>
<p>이 값은 아이템의 offset과 함께 사용되어 아이템이 화면에 얼마나 노출되어 있는지를 판단하는 기준점으로 활용됩니다.</p>
<hr>
<h3 id="🚀-viewportendoffset란">🚀 ViewportEndOffset란?</h3>
<p>viewportEndOffset은 Viewport의 끝 지점,즉 화면에서 아이템이 더 이상 보이지 않는 마지막 기준 위치(px)를 의미합니다.</p>
<pre><code class="language-kotlin">val listState = rememberLazyListState()
val viewportEndOffset = listState.layoutInfo.viewportEndOffset</code></pre>
<p>보통 LazyColumn의 경우 화면 높이, LazyRow의 경우 화면 너비에 해당하는 값이 됩니다. 이 값은 viewportStartOffset과 함께 사용하여 아이템이 완전히 보이는지, 또는 일부만 노출되고 있는지를 판단하는 데 유용합니다.</p>
<pre><code class="language-kotlin">val isFullyVisible =
    item.offset &gt;= viewportStartOffset &amp;&amp;
    item.offset + item.size &lt;= viewportEndOffset</code></pre>
<hr>
<h3 id="🎯-마무리">🎯 마무리</h3>
<p>visibleItemsInfo, viewportStartOffset, viewportEndOffset는 단순히 스크롤 위치를 확인하기 위한 값이라기보다는, 사용자에게 실제로 어떤 UI가 노출되고 있는지를 기준으로 화면을 제어할 수 있게 해주는 정보라고 볼 수 있습니다.</p>
<p>이 값들을 적절히 활용하면 무한 스크롤과 같은 페이징 처리뿐만 아니라, 아이템 노출 여부에 따른 이벤트 처리, 스크롤 위치에 반응하는 UI 애니메이션 등 보다 정교한 사용자 경험을 구현할 수 있습니다.</p>
<p>스크롤 가능한 UI를 구현할 때 “얼마나 스크롤되었는가”보다는 “무엇이 화면에 보이고 있는가”에 초점을 맞춘다면, Jetpack Compose에서 제공하는 layoutInfo는 그에 대한 좋은 힌트를 제공해 줄 것입니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Android] LiveData, StateFlow ]]></title>
            <link>https://velog.io/@marigold_/Android-LiveData-StateFlow</link>
            <guid>https://velog.io/@marigold_/Android-LiveData-StateFlow</guid>
            <pubDate>Mon, 22 Dec 2025 11:20:50 GMT</pubDate>
            <description><![CDATA[<p>UI는 항상 상태(State)의 결과물입니다. 로딩 중인지, 데이터가 있는지, 에러가 발생햇는지에 따라 화면은 서로 다른 모습으로 그려집니다. </p>
<p>안드로이드에서는 이러한 상태 변화를 UI에 전달하기 위해 LiveData와 StateFlow라는 두 가지 대표적인 도구를 제공합니다.</p>
<p>이번 글에서는 상태(State)라는 개념을 중심으로 LiveData와 StateFlow의 차잊머과 선택 기준을 정리해보겠습니다.</p>
<hr>
<h3 id="🚀-상태state란">🚀 상태(State)란?</h3>
<p>UI에서 말하는 상태란</p>
<blockquote>
<p>화면을 그리기 위해 필요한 현재값</p>
</blockquote>
<p>예를 들면 다음과 같습니다.</p>
<ul>
<li>로딩 중인가?</li>
<li>데이터가 있는가?</li>
<li>에러가 발생했는가?</li>
</ul>
<p>이 상태가 변경되면 UI는 그에 맞게 다시 그려져야 합니다. LiveData와 StateFlow는 바로 이 상태 변화를 UI에 전달하기 위한 도구입니다.</p>
<hr>
<h3 id="🚀-livedata란">🚀 LiveData란?</h3>
<p>LiveData는 Android Jetpack에서 제공하는 안드로이드 생명주기에 최적화된 데이터 홀더입니다.</p>
<h4 id="특징">특징</h4>
<ul>
<li>생명주기 인식 : Activity나 Fragment가 STARTED 또는 RESUMED 상태일 때만 데이터를 업데이트 합니다. (메모리 누수 방지 및 비정상 종료 예방)</li>
<li>자동 구독 해제 : 화면이 파괴(Destory)되면 관찰도 자동으로 멈춥니다.</li>
<li>Main Thread 중심 : UI 업데이트에 특화되어 있어 주로 메인 스레드에서 동작합니다.</li>
</ul>
<p>위와 같은 특징이 있지만, 안드로이드 플랫폼에 종속적이라 순수 Kotlin 환경(Unit Test 등)에서 사용하기 까다롭고, 복잡한 데이터 변환(연산)을 처리하기에는 다소 부족합니다.</p>
<hr>
<h3 id="🚀-stateflow란">🚀 StateFlow란?</h3>
<p>StateFlow는 Coroutine 기반의 현대적인 상태 관리 스트림으로, Kotlin Coroutine 라이브러리에서 제공하는 상태를 보관하는 Flow입니다. LiveData와 유사하지만 더 유연합니다.</p>
<h4 id="특징-1">특징</h4>
<ul>
<li>초기값 필수 : 상태가 항상 존재해야하므로 초기값이 반드시 필요합니다. (null 안전성 향상)</li>
<li>순수 Kotlin : Android 의존성이 없어 Domain 레이어나 테스트 환경에서도 자유롭게 사용 가능합니다.</li>
<li>강력한 연산자 : 코루틴의 연산자(filter, map, zip 등)을 활용해 데이터를 정교하게 가공할 수 있습니다.</li>
</ul>
<p>LiveData에 비해 더 유연하지만 기본적으로 생명주기를 인식하지 않기 때문에, UI에서 사용할 때는 repeatOnLifecycle 같은 API를 통해 생명주기에 맞춰 Collect하도록 처리해야 합니다. 처음엔 번거로워 보이지만, 언제 수집하고 언제 멈출지 명확하게 제어할 수 있다는 장점이 됩니다.</p>
<hr>
<h3 id="🧭-언제-무엇을-써야-할까">🧭 언제 무엇을 써야 할까?</h3>
<h4 id="livedata가-적합한-경우">LiveData가 적합한 경우</h4>
<ul>
<li>기존 XML 기반 레거시 프로젝트</li>
<li>DataBinding 중심 구조</li>
<li>Coroutine 도입이 어려운 환경</li>
</ul>
<h4 id="stateflow가-적합한-경우">StateFlow가 적합한 경우</h4>
<ul>
<li>신규 프로젝트</li>
<li>MVVM + Coroutine 기반</li>
<li>Jetpack Compose 사용</li>
<li>상태와 이벤트를 명확히 분리하고 싶을 때</li>
</ul>
<hr>
<h3 id="🎯-마무리">🎯 마무리</h3>
<p>LiveData와 StateFlow는 서로 대체 관계라기보다, 프로젝트의 구조와 기술 스택에 따라 선택해야 할 상태 관리 도구입니다.</p>
<p>LiveData는 안드로이드 생명주기에 밀접하게 결합되어 있어 기존 XML 기반 UI나 레거시 프로젝트에서 여전히 유효한 선택지입니다. 반면 StateFlow는 Coroutine을 중심으로 한 현대적인 아키텍처에서 상태를 더 명확하고 유연하게 표현할 수 있는 도구입니다.</p>
<p>중요한 것은 어떤 도구를 쓰느냐보다, 상태를 명확히 정의하고, 그 상태에 따라 UI를 일관되게 그리는 구조를 만드는 것입니다. 이 기준을 잡고 있다면, LiveData든 StateFlow든 상황에 맞게 올바른 선택을 할 수 있을 것입니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Android] Coroutine Join, Cancel]]></title>
            <link>https://velog.io/@marigold_/Android-Coroutine-Join-Cancel</link>
            <guid>https://velog.io/@marigold_/Android-Coroutine-Join-Cancel</guid>
            <pubDate>Mon, 22 Dec 2025 07:30:29 GMT</pubDate>
            <description><![CDATA[<p>Coroutine을 사용하다보면 단순히 비동기 작업을 실행하는 것보다 언제 끝나는지, 중간에 멈출 수 있는지, 다음 로직을 언제 실행해야 하는지가 더 중요해지는 순간이 옵니다.</p>
<p>이 때 핵심적으로 등장하는 개념이 join, cancel입니다. 이번 글에서는 join과 cancel을 중심으로 코루틴의 생명주기를 어떻게 제어하는지 정리해보고, 추가적으로 cancelAndJoin에 대해서도 알아보겠습니다.</p>
<hr>
<h3 id="🚀-join이란">🚀 Join이란?</h3>
<p>join은 Job이 끝날 때까지 기다리는 suspend 함수입니다.</p>
<ul>
<li>반환값 X</li>
<li>성공/실패 여부만 기다림</li>
<li>순서 보장이 목적</li>
<li>Job이 예외로 종료되면 예외가 전파됨</li>
</ul>
<pre><code class="language-kotlin">val job = launch {
    delay(1000)
    println(&quot;작업 끝&quot;)
}

job.join()
println(&quot;이 코드는 작업 이후 실행됨&quot;)</code></pre>
<p>위의 코드 블럭을 실행해보면 job이 완전히 종료된 이후에 println이 실행됩니다. 즉, join은 이 작업이 끝난 이후에만 다음 로직을 실행하고 싶다는 의도를 명확하게 표현합니다. join은 작업 순서를 보장해야할 때, 결과는 필요없고 종료 시점만 중요할 때, 여러 Job의 종료를 기다릴 때 사용합니다.</p>
<p>await와 비슷해보이지만, await는 결과 값을 사용하는 로직에 쓰이고, join은 겨로가와 상관없이 작업 완료 시점만 보장하고 싶을 때 사용합니다.</p>
<hr>
<h3 id="🚀-cancel이란">🚀 Cancel이란?</h3>
<p>cancel은 코루틴을 취소 상태로 전환시키는 함수입니다. 중요한 점은 즉시 종료가 아니라, 취소 요청이라는 것입니다.</p>
<pre><code class="language-kotlin">val job = launch {
    repeat(5) {
        delay(500)
        println(&quot;작업 중 $it&quot;)
    }
}

delay(1200)
job.cancel()
println(&quot;cancel 호출&quot;)</code></pre>
<h4 id="cancel의-핵심-포인트">cancel의 핵심 포인트</h4>
<ul>
<li>코루틴은 협력적 취소(cooperative cancellation) 구조</li>
<li>delay, yield, isActive 같은 취소 체크 지점에서만 멈춤</li>
<li>무한 루프나 blocking 코드에서는 취소되지 않을 수 있음<pre><code class="language-kotlin">while (isActive) {
  // 안전한 취소 포인트
}</code></pre>
</li>
</ul>
<p>cancel은 더 이상 필요없는 작업을 중단해야할 때, 화면전환이나 스코프 종료, 타임아웃 처리, 리소스 낭비를 막고 싶을 때 주로 사용합니다.</p>
<hr>
<h3 id="🧠-cancelandjoin">🧠 CancelAndJoin</h3>
<p>Cancel과 Join을 하나로 합친 cancelAndJoin도 있습니다.</p>
<pre><code class="language-kotlin">job.cancelAndJoin()</code></pre>
<p>이 한 줄은 다음을 의미합니다.</p>
<ol>
<li>작업을 취소 요청하고</li>
<li>실제로 종료될 때까지 기다린다.</li>
</ol>
<p>즉, 이 코루틴은 이제 필요없고, 완전히 정리된 이후 다음 단계로 넘어간다라는 의미입니다. </p>
<hr>
<h3 id="🎯-마무리">🎯 마무리</h3>
<p>join과 cancel은 단순한 API가 아니라 코루틴의 생명주기를 명확하게 표현하는 도구입니다.</p>
<ul>
<li>join → 작업이 끝날 때까지 기다림</li>
<li>cancel → 더 이상 필요 없는 작업에 대한 중단 요청</li>
<li>cancelAndJoin → 작업을 중단하고 완전히 정리한 뒤 다음 단계로 이동</li>
</ul>
<p>이 개념들을 명확히 구분해 사용하면, 코루틴 코드는 더 읽기 쉬워지고, 예측 가능하며, 안정적으로 동작하게 됩니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Kotlin] StringBuilder]]></title>
            <link>https://velog.io/@marigold_/Kotlin-StringBuilder</link>
            <guid>https://velog.io/@marigold_/Kotlin-StringBuilder</guid>
            <pubDate>Fri, 19 Dec 2025 05:32:38 GMT</pubDate>
            <description><![CDATA[<p>Kotlin의 StringBuilder는 문자열을 효율적으로 생성하고 수정할 수 있는 클래스입니다.
문자열을 자주 변경해야 하는 상황에서 String보다 훨씬 뛰어난 성능을 제공하며, 반복적인 문자열 연결이 필요한 경우 필수적으로 사용됩니다.</p>
<p>StringBuilder는 가변(mutable) 문자열 객체로, 내부 버퍼를 직접 수정하면서 문자열을 다루기 때문에 불필요한 객체 생성을 줄일 수 있습니다.</p>
<hr>
<h3 id="🚀-stringbuilder란">🚀 StringBuilder란?</h3>
<p>StringBuilder는 Kotlin의 가변 문자열 클래스입니다. 일반적인 String은 불변(immutable)이기 때문에 값이 변경될 때마다 새로운 객체가 생성되지만, StringBuilder는 하나의 객체 내부에서 문자열을 계속 수정합니다.</p>
<p>문자열을 반복적으로 누적하는 경우, StringBuilder의 성능 차이가 확실하게 드러납니다.</p>
<pre><code class="language-kotlin">var result = &quot;&quot;

for (i in 1..5) {
    result += i
}

println(result) // 12345</code></pre>
<p>이 코드는 반복문마다 새로운 String 객체를 생성합니다. String을 사용해 문자열을 이어 붙이면, 매번 새로운 String 객체가 생성되고 기존 객체는 버려집니다.</p>
<pre><code class="language-kotlin">val sb = StringBuilder()

for (i in 1..5) {
    sb.append(i)
}

println(sb.toString()) // 12345</code></pre>
<p>StringBuilder는 내부 버퍼를 재사용하기 때문에 훨씬 효율적입니다. StringBuilder는 하나의 객체만 유지하면서 문자열을 변경하므로, 객체 생성 비용 감소 및 GC 부담 감소, 그리고 메모리 사용량이 감소합니다.</p>
<hr>
<h3 id="🎯-결론">🎯 결론</h3>
<p>StringBuilder는 문자열을 자주 변경하거나 누적해야 하는 상황에서
String보다 훨씬 빠르고 효율적인 선택입니다.</p>
<ul>
<li>반복문에서 문자열을 누적할 때</li>
<li>로그 메시지, SQL 쿼리, JSON 문자열 생성</li>
<li>성능이 중요한 문자열 처리 로직</li>
</ul>
<p>이러한 경우에는 String 대신 StringBuilder 사용을 우선적으로 고려하는 것이 좋습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Android] Supervisor]]></title>
            <link>https://velog.io/@marigold_/Android-Supervisor</link>
            <guid>https://velog.io/@marigold_/Android-Supervisor</guid>
            <pubDate>Thu, 18 Dec 2025 13:40:49 GMT</pubDate>
            <description><![CDATA[<p>이번 글에서는 SupvervisorJob과 SuvpervisorScope의 차이를 알아보겠습니다.</p>
<hr>
<h3 id="🚀-supervisorjob은이란">🚀 SupervisorJob은이란?</h3>
<p>SupervisorJob은 자식 코루틴 간의 실패를 서로 전파하지 않는 Job입니다.</p>
<pre><code class="language-kotlin">val scope = CoroutineScope(Dispatchers.Main + SupervisorJob())

scope.launch {
    launch {
        error(&quot;실패&quot;)
    }
    launch {
        delay(1000)
        println(&quot;정상 실행&quot;)
    }
}</code></pre>
<p>위 코드에서는 첫 번째 코루틴이 실패하더라도 두 번째 코루틴은 정상적으로 실행됩니다. SupervisorJob은 독립적인 작업들을 한 스코프에서 관리하고 싶을 때 유용합니다.</p>
<h4 id="대표적인-사용-사례">대표적인 사용 사례</h4>
<ul>
<li>여러 네트워크 요청 중 일부 실패를 허용해야 할 때</li>
<li>하나의 실패가 전체 화면 로직을 망치면 안 되는 경우</li>
<li>ViewModel 내부에서 독립적인 작업 관리</li>
</ul>
<hr>
<h3 id="🚀-supervisorscope이란">🚀 SupervisorScope이란?</h3>
<p>supervisorScope는 SupervisorJob을 사용하는 suspend 함수 기반 스코프 빌더입니다.</p>
<ul>
<li>SupervisorJob과 동일한 실패 규칙</li>
<li>단, 함수 범위 내에서만 적용</li>
<li>모든 자식이 끝날 때까지 suspend</li>
</ul>
<pre><code class="language-kotlin">suspend fun loadData() = supervisorScope {
    launch {
        error(&quot;실패&quot;)
    }
    launch {
        delay(1000)
        println(&quot;정상 실행&quot;)
    }
}</code></pre>
<p>여기서 첫 번째 코루틴이 실패해도 두 번째 코루틴은 취소되지 않습니다. supervisorScope는 일시적으로 실패 격리가 필요한 경우에 적합합니다.</p>
<h4 id="대표적인-사용-사례-1">대표적인 사용 사례</h4>
<ul>
<li>suspend 함수 내부에서 병렬 작업 처리</li>
<li>일부 작업 실패를 허용하고 나머지는 계속 진행해야 할 때</li>
<li>try-catch로 전체를 감싸고 싶지 않은 구조</li>
</ul>
<hr>
<h3 id="🎯-마무리">🎯 마무리</h3>
<ul>
<li>SupervisorJob<ul>
<li>스코프 전체의 실패 전파 방지<ul>
<li>ViewModel, 장기 스코프에 적합</li>
</ul>
</li>
</ul>
</li>
<li>supervisorScope<ul>
<li>특정 로직 블록에서만 실패 격리</li>
<li>suspend 함수 내부 병렬 처리에 적합</li>
</ul>
</li>
</ul>
<blockquote>
<p>하나의 실패가 모든 코루틴을 망치게 둘 것인가, 아니면 독립적으로 관리할 것인가?</p>
</blockquote>
<p>이 질문에 대한 답이 바로 Supervisor의 존재 이유입니다.</p>
<p>코루틴을 잘 쓰기 위한 핵심은 동시성보다 실패를 어떻게 다룰 것인가를 설계하는 것입니다. SupervisorJob과 supervisorScope를 이해하면, 코루틴 에러 처리의 시야가 한 단계 넓어질 것입니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Android] Coroutine Builder (launch, aysnc)]]></title>
            <link>https://velog.io/@marigold_/Android-async-launch</link>
            <guid>https://velog.io/@marigold_/Android-async-launch</guid>
            <pubDate>Wed, 17 Dec 2025 10:07:54 GMT</pubDate>
            <description><![CDATA[<p>Android 코루틴을 처음 접하면 launch와 async를 단순히 &quot;비동기로 실행하는 함수&quot; 정도로 이해하기 쉽습니다. 하지만 두 함수는 모두 코루틴 빌더(Coroutine Builder)이며, 목적과 사용 방식이 명확히 다릅니다.</p>
<p>이번 글에서는 먼저 코루틴 빌더란 무엇인지 간단히 짚고, launch와 async 각각의 개념과 사용법을 살펴본 뒤, 두 빌더의 차이를 정리해보겠습니다.</p>
<hr>
<h3 id="🚀-코루틴-빌더란">🚀 코루틴 빌더란?</h3>
<p>코루틴 빌더(Coroutine Builder)는 코루틴을 시작하는 함수입니다. 즉, suspend 함수나 비동기 로직을 실제로 실행 가능한 코루틴으로 만들어주는 역할을 합니다. 여러 코루틴 빌더가 있지만 이번 글에서는 launch, async에 대해 알아보겠습니다. 이 둘은 항상 CoroutineScope 안에서 사용됩니다.</p>
<pre><code class="language-kotlin">viewModelScope.launch {
    // 코루틴 시작
}</code></pre>
<hr>
<h3 id="🚀-launch란">🚀 launch란?</h3>
<p>launch는 결과를 반환하지 않는 코루틴 빌더입니다. 단순히 어떤 작업을 비동기로 실행하고 끝내는 용도로 사용됩니다.</p>
<pre><code class="language-kotlin">viewModelScope.launch {
  delay(1000)
  println(&quot;작업 완료&quot;)
}</code></pre>
<p>launch는 실행 결과를 다른 로직에서 사용할 필요가 없는 경우에 적합합니다. UI 상태 업데이트, DB 저장, 로그 출력 등의 상황에서 보통 사용됩니다.</p>
<hr>
<h3 id="🚀-async란">🚀 async란?</h3>
<p>async는 결과를 반환하는 코루틴 빌더입니다. 실제 값은 바로 반환되지 않고, Deferred&lt;T&gt; 형태로 감싸져 반환됩니다.</p>
<pre><code class="language-kotlin">val deferred = async {
    repository.fetchUser()
}</code></pre>
<p>async는 비동기 작업의 결과가 이후 로직에 반드시 필요할 때 사용하며, 병렬 처리에 적합합니다. 결과는 await 호출 시점에 흭득하기 때문에 async는 반드시 await와 함께 사용되어야 의미가 있습니다.</p>
<pre><code class="language-kotlin">viewModelScope.launch {
  val userDeferred = async { repository.fetchUser() }
  val postDeferred = async { repository.fetchPosts() }

  val user = userDeferred.await()
  val posts = postDeferred.await()

  _uiState.value = UiState(user, posts)
}</code></pre>
<p>이처럼 여러 작업을 동시에 실행하고 결과를 조합해야 할 때 async가 강력한 역할을 합니다.</p>
<hr>
<h3 id="🎯-마무리">🎯 마무리</h3>
<ul>
<li>결과가 필요 없는 작업 → launch</li>
<li>결과가 필요한 작업 → async + await</li>
</ul>
<p>async를 단순히 launch처럼 사용하는 것은 잘못된 사용이며, 오히려 코드의 의도를 흐리게 만듭니다. 두 코루틴 빌더의 역할을 명확히 구분하면 코루틴 코드는 훨씬 읽기 쉽고 안전해집니다.</p>
<p>코루틴을 잘 쓰기 위한 핵심은 복잡한 문법이 아니라, 상황에 맞는 빌더를 선택하는 것입니다. 이 기준만 잘 기억해두어도 Android 비동기 코드를 다루는 감각이 한층 좋아질 것입니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Android] Scope]]></title>
            <link>https://velog.io/@marigold_/Android-scope</link>
            <guid>https://velog.io/@marigold_/Android-scope</guid>
            <pubDate>Wed, 10 Dec 2025 17:29:54 GMT</pubDate>
            <description><![CDATA[<p>Android 코루틴을 처음 접하면 헷갈리는 개념 중 하나가 Scope입니다. 왜 scope를 사용해야하고, 어떤 scope를 사용해야하는지 의문이 생기게 됩니다.</p>
<p>이번 글에서는 코루틴 Scope가 무엇인지, 왜 필요한지, 어떤 종류가 있고 어떻게 사용하는지 정리해보도록 하겠습니다.</p>
<hr>
<h3 id="🚀-scope스코프란">🚀 Scope(스코프)란?</h3>
<p>Scope는 코루틴의 생명주기를 관리하는 범위입니다. 쉽게 말해 &quot;이 코루틴이 언제 시작되고 언제 끝나는지&quot;를 결정하는 관리자라고 생각하면 됩니다.</p>
<p>왜 Scope가 필요한가?</p>
<pre><code class="language-kotlin">// ❌ 이런 코드는 불가능
launch {  // 에러! Scope가 없음
    delay(1000)
}

// ✅ Scope와 함께 사용
lifecycleScope.launch {
    delay(1000)
}</code></pre>
<h4 id="scope가-없을-경우"><strong>Scope가 없을 경우</strong></h4>
<ul>
<li>코루틴이 언제 취소되어야 하는지 정의할 방법이 없음음</li>
<li>메모리 누수 위험 (화면이 사라져도 작업이 계속 실행될 수 있음)</li>
</ul>
<h4 id="scope가-있을-경우"><strong>Scope가 있을 경우</strong></h4>
<ul>
<li>코루틴의 생명주기를 관리할 수 있음</li>
<li>구조화된 동시성으로 안전한 비동기 처리</li>
<li>예측 가능한 생명주기 (단, 어떤 Scope인지에 따라 동작이 다름)</li>
</ul>
<hr>
<h3 id="📦-scope의-종류">📦 Scope의 종류</h3>
<p>Android에서 사용하는 주요 Scope들을 살펴보겠습니다.</p>
<h4 id="1-globalscope"><strong>1. globalScope</strong></h4>
<pre><code class="language-kotlin">GlobalScope.launch {
    // 앱이 종료될 때까지 살아있음
}</code></pre>
<h4 id="특징"><strong>특징</strong></h4>
<ul>
<li>앱 전체 생명주기 동안 유지</li>
<li>절대 자동으로 취소되지 않음</li>
</ul>
<p>globalScope가 가진 특징 때문에 거의 사용되지 않고 있습니다.  Activity가 종료되어도 코루틴이 계속 실행되어 메모리 누수가 발생할 수 있고, 코루틴이 언제 끝날지 알 수가 없습니다. 또한 불필요한 작업이 백그라운드에서 계쏙 돌아가기 때문에 리소스 낭비가 발생합니다. globalScope가를 사용해야될 대부분의 상황에 더 나은 대안이 있어서 globalScope는 거의 사용되지 않고 있습니다.</p>
<h4 id="2-viewmodelscope"><strong>2. viewModelScope</strong></h4>
<pre><code class="language-kotlin">class MyViewModel : ViewModel() {
    fun loadData() {
        viewModelScope.launch {
            val data = repository.fetchData()
            _uiState.value = data
        }
    }
}</code></pre>
<h4 id="특징-1"><strong>특징</strong></h4>
<ul>
<li>ViewModel이 clear될 때 자동으로 취소</li>
<li>Configuration Change (화면 회전 등)에도 유지</li>
<li>ViewModel의 생명주기와 완벽하게 동기화</li>
</ul>
<p>viewModelScope는 ViewModel에서 사용되는 Scope입니다. ViewModel의 내부의 모든 비동기 작업에 사용되며, 네트워크 요청 또는 데이터베이스 조회를 할 때 사용됩니다. 추가적으로 비즈니스 로직 처리를 할 때도 viewModelScope를 사용합니다.</p>
<h4 id="3-lifecyclescope"><strong>3. lifeCycleScope</strong></h4>
<pre><code class="language-kotlin">class MyActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        lifecycleScope.launch {
            // Activity가 destroy되면 자동 취소
        }
    }
}</code></pre>
<h4 id="특징-2"><strong>특징</strong></h4>
<ul>
<li>Lifecycle이 DESTROYED 상태가 되면 자동 취소</li>
<li>Activity/Fragment의 생명주기와 완벽하게 동기화</li>
<li>Configuration Change 시 재생성됨</li>
</ul>
<p>lifeCycleScope은 UI를 직접 업데이트하거나 일회성 작업에 사용됩니다. ViewModel없이 간단한 작업을 처리할 때도 사용됩니다. lifeCycleScope은 Activity가 백그라운드로 가도 계속 실행되는데, 이를 방지하기 위해 사용되는 것이 RepeatOnLifeCycle입니다.</p>
<pre><code class="language-kotlin">// 일반 lifecycleScope
lifecycleScope.launch {
    // Activity가 백그라운드로 가도 계속 실행됨
    flow.collect { data -&gt;
        updateUI(data)
    }
}

// repeatOnLifecycle (추천)
lifecycleScope.launch {
    repeatOnLifecycle(Lifecycle.State.STARTED) {
        // STARTED 상태일 때만 실행
        // 백그라운드로 가면 자동 중지, 돌아오면 재시작
        flow.collect { data -&gt;
            updateUI(data)
        }
    }
}</code></pre>
<p>RepeatOnLifeCycle을 사용하면 위의 코드의 설명과 같이 Activity의 생명주기가 STARTED일 때만 코루틴이 작동되고, 백그라운드로 가면 중지됩니다.</p>
<h4 id="4-remembercoroutinescope"><strong>4. rememberCoroutineScope</strong></h4>
<pre><code class="language-kotlin">@Composable
fun MyScreen() {
    val scope = rememberCoroutineScope()

    Button(onClick = {
        scope.launch {
            // 버튼 클릭 시 코루틴 실행
        }
    }) {
        Text(&quot;클릭&quot;)
    }
}</code></pre>
<h4 id="특징-3"><strong>특징</strong></h4>
<ul>
<li>Composable이 화면에서 사라지면 자동 취소</li>
<li>Recomposition에도 안전하게 유지</li>
<li>이벤트 기반 작업에 최적</li>
</ul>
<p>rememberCoroutineScope는 버튼 클릭 같은 일회성 이벤트에 주로 사용됩니다. SnackBar, Toast를 표시하거나, 애니메이션을 트리거하는데 사용되는 Scope입니다.</p>
<h4 id="5-coroutinescope">5. coroutineScope</h4>
<pre><code class="language-kotlin">class MyRepository {
    private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())

    fun fetchData() {
        scope.launch {
            // 작업
        }
    }

    fun cleanup() {
        scope.cancel() // 수동으로 취소 필요!
    }
}</code></pre>
<h4 id="특징-4"><strong>특징</strong></h4>
<ul>
<li>완전한 수동 관리</li>
<li>생명주기를 직접 제어</li>
<li>취소도 직접 호출 필요</li>
</ul>
<p>coroutineScope를 직접 생성해 사용하는 경우는 Repository나 Manager 같은 커스텀 클래스, 또는 Android 컴포넌트가 아닌 곳에서 코루틴을 사용할 때입니다.<br>특별한 생명주기 관리가 필요한 상황에서도 직접 생성해 사용할 수 있지만,<br>이 경우에는 반드시 cancel()을 호출해 메모리 누수를 방지해야 합니다.</p>
<h4 id="6-supervisorscope">6. supervisorScope</h4>
<pre><code class="language-kotlin">suspend fun loadUserProfile() = supervisorScope {
    val userDeferred = async { loadUser() }
    val postsDeferred = async { loadPosts() }

    // 하나가 실패해도 다른 작업은 계속됨
    val user = runCatching { userDeferred.await() }
    val posts = runCatching { postsDeferred.await() }

    combineResult(user, posts)
}</code></pre>
<h4 id="특징-5"><strong>특징</strong></h4>
<ul>
<li>자식 코루틴 중 하나가 실패해도 전체 Scope가 취소되지 않음</li>
<li>기본 CoroutineScope와 달리 부모가 자식의 예외를 전파받지 않음</li>
<li>복수의 독립적인 작업을 병렬로 실행할 때 유용</li>
<li><blockquote>
<p>(예: 프로필 + 알림 + 피드 로딩을 동시에, 하나 실패해도 UI는 표시 가능)</p>
</blockquote>
</li>
</ul>
<p>supervisorScope은 &quot;여러 자식 작업을 동시에 수행하지만, 서로의 실패로 인해 전체 작업이 중단되면 안 되는 상황&quot;에서 사용됩니다. 예외가 발생해도 전체 코루틴이 죽지 않으므로, 구조화된 동시성을 유지하면서도 안정적인 처리가 가능합니다.</p>
<hr>
<h3 id="🎯-마무리">🎯 마무리</h3>
<p>코루틴을 안전하고 예측 가능하게 사용하기 위해서는 작업의 성격과 생명주기에 맞는 Scope를 선택하는 것이 무엇보다 중요합니다. 각 Scope의 특성과 역할을 이해해두면 구조화된 동시성을 자연스럽게 적용할 수 있고, 더 안정적인 비동기 코드를 만들 수 있습니다.</p>
<p>결국 코루틴을 제대로 활용하는 첫 단계는 Scope를 올바르게 이해하고 상황에 맞게 사용하는 것입니다.</p>
<p>이 점을 기억해두면 Android 개발에서 코루틴을 훨씬 더 명확하고 효율적으로 다룰 수 있습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Android] Coroutine]]></title>
            <link>https://velog.io/@marigold_/Android-Coroutine</link>
            <guid>https://velog.io/@marigold_/Android-Coroutine</guid>
            <pubDate>Wed, 10 Dec 2025 07:07:59 GMT</pubDate>
            <description><![CDATA[<p>Android 개발을 하다보면 &quot;이 작업 메인 스레드에서 처리해도 되나?&quot;, &quot;비동기 처리를 더 깔끔하게 할 수 없을까?&quot;같은 고민이 생기기 마련입니다. 특히 네트워크 요청, 디스크 IO, 데이터 변환 같은 시간이 오래 걸리는 작업은 UI를 멈추게 만들 위험이 있기 때문에 비동기 처리가 필수적입니다.</p>
<p>과거에는 이런 문제를 해결하기 위해 콜백·RxJava 등을 사용했지만, Kotlin이 도입되면서 완전히 다른 방식이 등장했습니다. 바로 코루틴(Coroutines) 입니다.</p>
<p>이번 글에서는 코루틴이 무엇인지, 왜 필요한지, 기본 개념 및 예제, 그리고 Compose와 함께 사용할 때의 팁까지 정리해보겠습니다.</p>
<hr>
<h3 id="🚀-coroutine코루틴이란">🚀 Coroutine(코루틴)이란?</h3>
<p>코루틴은 Kotlin에서 제공하는 비동기적으로 실행되는 코드를 간소화하기 위해 Android에서 사용할 수 있는 설계 패턴입니다. 쉽게 말하면 <strong>스레드를 직접 만들지 않고 비동기 작업을 작성할 수 있게 도와주는 일종의 경량 스레드</strong>입니다.</p>
<h4 id="코루틴의-핵심-특징"><strong>코루틴의 핵심 특징</strong></h4>
<ul>
<li><strong>가볍다</strong></li>
<li><blockquote>
<p>수천 개의 코루틴도 스레드 몇 개만으로 처리 가능</p>
</blockquote>
</li>
<li><blockquote>
<p>성능 + 메모리 효율</p>
</blockquote>
</li>
<li><strong>중단 (suspend)</strong></li>
<li><blockquote>
<p>실행을 잠시 멈추고 다시 이어서 실행 가능</p>
</blockquote>
</li>
<li><blockquote>
<p>비동기 작업을 동기 코드처럼 자연스럽게 작성</p>
</blockquote>
</li>
<li><strong>구조화된 동시성</strong></li>
<li><blockquote>
<p>코루틴의 생명주기를 스코프로 관리하여 누수없는 안전한 비동기 처리</p>
</blockquote>
</li>
<li><strong>캔슬 지원</strong></li>
<li><blockquote>
<p>필요 없어진 작업은 즉시 취소하여 네트웤, 요청, 무한 루프 등 불필요한 연산 절약</p>
</blockquote>
</li>
</ul>
<hr>
<h3 id="🔧-기본-사용법">🔧 기본 사용법</h3>
<pre><code class="language-kotlin">val scope = CoroutineScope(Dispatchers.Main)

suspend fun fetchData(): String {
    delay(1000) // 네트워크 요청처럼 1초 지연된다고 가정
    return &quot;완료!&quot;
}

scope.launch {
    // 여기서부터 코루틴 실행
    val result = fetchData()
    println(result)
}</code></pre>
<p>코루틴 안에서 시간이 오래 걸리는 작업은 suspend 함수로 분리합니다. suspend 함수는 중단이 가능한 함수로, 중단한다는 것은 스레드를 점유하지 않으면서 작업이 잠시 멈췄다가 이어서 실행될 수 있다는 의미입니다.</p>
<hr>
<h3 id="⚙️-코루틴-디스패처">⚙️ 코루틴 디스패처</h3>
<p>코루틴이 어떤 스레드에서 실행될지 결정하는 요소입니다.</p>
<ul>
<li><strong>Dispatcher.Main</strong></li>
<li><blockquote>
<p>UI 작업(Compose 포함)</p>
</blockquote>
</li>
<li><blockquote>
<p>메인 스레드에서 실행</p>
</blockquote>
</li>
<li><strong>Dispatcher.IO</strong></li>
<li><blockquote>
<p>네트워크·파일 IO 작업</p>
</blockquote>
</li>
<li><blockquote>
<p>네트워크 요청, 데이터베이스 쿼리, 파일 읽기/쓰기</p>
</blockquote>
</li>
<li><strong>Dispatcher.Default</strong></li>
<li><blockquote>
<p>CPU 집중 계산 작업</p>
</blockquote>
</li>
<li><blockquote>
<p>JSON 파싱, 정렬, 필터링 등</p>
</blockquote>
</li>
<li><strong>Dispatcher.Unconfined</strong></li>
<li><blockquote>
<p>특수한 케이스 (일반적으로 사용 비추천)</p>
</blockquote>
</li>
</ul>
<hr>
<h3 id="🚀-withcontext란">🚀 withContext란?</h3>
<p>코루틴 내에서 작업의 성격에 따라 디스패처를 전환해야할 때가 있습니다. 이 때, withContext를 사용합니다.</p>
<pre><code class="language-kotlin">suspend fun loadUserData(): User {
    // Main 디스패처에서 시작
    val userId = getCurrentUserId()

    // IO 작업을 위해 디스패처 전환
    val user = withContext(Dispatchers.IO) {
        database.getUserById(userId) // DB 조회
    }

    // 다시 Main으로 돌아옴
    return user
}</code></pre>
<p>withContext는 지정한 디스패처로 전환 후, 블록 내 작업이 끝나면 다시 원래 디스패처로 돌아옵니다. 단, 아래의 코드 예시와 같이 디스패처 전환을 자주하면 안됩니다.</p>
<pre><code class="language-kotlin">// ❌ 나쁜 예: 불필요한 디스패처 전환 반복
suspend fun processItems(items: List&lt;Item&gt;) {
    items.forEach { item -&gt;
        withContext(Dispatchers.IO) { // 매번 전환!
            saveToDatabase(item)
        }
    }
}

// ✅ 좋은 예: 한 번만 전환
suspend fun processItems(items: List&lt;Item&gt;) {
    withContext(Dispatchers.IO) { // 한 번만 전환
        items.forEach { item -&gt;
            saveToDatabase(item)
        }
    }
}</code></pre>
<h4 id="디스패처-전환을-자주-하면-안-되는-이유">디스패처 전환을 자주 하면 안 되는 이유</h4>
<ul>
<li><strong>컨텍스트 전환 비용</strong></li>
<li><blockquote>
<p>디스패처를 바꿀 때마다 스레드 전환 비용이 발생합니다. 수백 ~ 수천 번 반복되면 성능 저하가 발생합니다.</p>
</blockquote>
</li>
<li><strong>불필요한 오버헤드</strong></li>
<li><blockquote>
<p>withContext는 새로운 코루틴을 만들고, 작업을 스케줄링하고, 다시 돌아오는 과정을 거칩니다.</p>
</blockquote>
</li>
</ul>
<p>따라서 작업의 단위가 클 때 디스패치를 전환하고, 반복문 안에서는 가급적 전환하지 않습니다.</p>
<hr>
<h3 id="📌-구조화된-동시성">📌 구조화된 동시성</h3>
<p>코루틴이 다른 비동기 라이브러리와 다른 핵심 개념입니다.</p>
<pre><code class="language-kotlin">coroutineScope {
    launch { ... }
    launch { ... }
}</code></pre>
<p>부모 스코프가 살아 있는 동안, 자식 코루틴이 모두 끝날 때까지 함께 관리됩니다.
→ 예측 가능한 생명주기 관리 가능</p>
<p>Android에서는 주로 다음 스코프를 사용합니다</p>
<ul>
<li>viewModelScope (ViewModel)</li>
<li>lifecycleScope (Activity/Fragment)</li>
<li>rememberCoroutineScope (Compose)</li>
</ul>
<pre><code class="language-kotlin">@Composable
fun MyScreen() {
    val scope = rememberCoroutineScope()

    Button(onClick = {
        scope.launch {
            delay(1000)
            println(&quot;완료!&quot;)
        }
    }) {
        Text(&quot;실행&quot;)
    }
}</code></pre>
<hr>
<h3 id="⚡-코루틴을-쓰면-좋은-상황">⚡ 코루틴을 쓰면 좋은 상황</h3>
<ul>
<li>네트워크 요청</li>
<li>데이터베이스 쿼리</li>
<li>파일 IO</li>
<li>스크롤 이벤트 처리</li>
<li>무거운 계산 작업</li>
<li>반복적으로 갱신되는 데이터 처리</li>
</ul>
<hr>
<h3 id="❗-코루틴-사용-시-주의점">❗ 코루틴 사용 시 주의점</h3>
<ul>
<li>GlobalScope 사용 금지 (누수 위험)</li>
<li>무한 루프 반복 시 반드시 isActive 체크</li>
<li>디스패처를 잘못 쓰면 UI가 멈출 수 있음</li>
<li>suspend 함수를 남용하면 설계가 복잡해질 수 있음</li>
<li>반복문 안에서 불필요하게 withContext 호출하지 말 것</li>
<li>Dispatchers.Unconfined는 특별한 경우가 아니면 사용하지 말 것</li>
</ul>
<hr>
<h3 id="🎯-마무리">🎯 마무리</h3>
<p>코루틴은 Kotlin에서 비동기 작업을 가장 깔끔하고 안정적으로 처리할 수 있는 핵심 도구입니다.
이전의 콜백 지옥, 복잡한 Rx 체이닝에 비해 훨씬 읽기 쉽고, 스레드 관리도 직접 하지 않아도 됩니다.</p>
<p>Compose와 함께 사용할 때도 최적의 방식이므로, 앱이 점점 비동기 연산을 많이 사용할수록 코루틴은 필수가 됩니다.</p>
<hr>
<h3 id="💡-참고자료">💡 참고자료</h3>
<p><a href="https://developer.android.com/kotlin/coroutines?hl=ko">Android 공식 문서 - 코루틴</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Android] derivedStateOf]]></title>
            <link>https://velog.io/@marigold_/Android-derivedStateOf</link>
            <guid>https://velog.io/@marigold_/Android-derivedStateOf</guid>
            <pubDate>Tue, 09 Dec 2025 11:59:42 GMT</pubDate>
            <description><![CDATA[<p>compose로 UI를 개발하다보면 상태 하나만 관리하면 될 줄 알았던 UI가 점점 복잡해지는 순간이 찾아옵니다.</p>
<ul>
<li>리스트는 그대로인데, 정렬된 버전이 필요</li>
<li>입력값은 하나인데, 유효성 검증 결과 필요</li>
<li>원본 데이터는 그대로지만, 펄터링된 결과값 표츌</li>
<li>사용자가 스크롤할 때마다 계산해야하는 파생 값</li>
</ul>
<p>이렇게 &#39;원본 상태&#39;외에도 그 상태로부터 파생되는 값들이 점점 늘어나면서 상태 관리가 어려워지고 코드도 복잡해지기 시작합니다.</p>
<p>특히 compose는 recomposition이 자주 발생하기 때문에 이런 파생 값들을 잘못 관리하면 불필요한 연산 + 성능 저하로 이어지기 쉽습니다. 그래서 이번 글에서는 Jetpack Compose에서 파생 상태를 효율적으로 다루는 derivedStateOf를 기본 개념부터 mutableStateOf와 비교, 실제 예제까지 정리해보겠습니다.</p>
<hr>
<h3 id="🚀-derivedstateof란">🚀 derivedStateOf란?</h3>
<p>derivedStateOf는 Jetpack Compose에서 제공하는 파생 상태 계산 도구입니다. 다른 상태로부터 계산되는 값을 효율적으로 관리할 수 있도록 도와줍니다.</p>
<p>주요 특징</p>
<ul>
<li>메모이제이션(memoization) 기반 최적화</li>
<li><blockquote>
<p>의존하는 값이 변경될 때만 재계산</p>
</blockquote>
</li>
<li><blockquote>
<p>같은 입력값에 대해서는 이전 계산 결과를 재사용</p>
</blockquote>
</li>
<li>비싼 연산 최적화에 특화</li>
<li><blockquote>
<p>필터링, 정렬, 파싱 등 성능에 민감한 작업에 유용</p>
</blockquote>
</li>
<li><blockquote>
<p>Recomposition 중에도 불필요한 재계산 방지</p>
</blockquote>
</li>
<li>상태 중복 없음</li>
<li><blockquote>
<p>파생된 값을 따로 상태로 저장할 필요 없음</p>
</blockquote>
</li>
<li><blockquote>
<p>원본 상태만 관리하면 파생 값은 자동으로 계산됨</p>
</blockquote>
</li>
<li>관찰 가능한(Observable) 상태</li>
<li><blockquote>
<p>결과값이 변경되면 해당 값을 구독하는 컴포저블만 recomposition</p>
</blockquote>
</li>
<li><blockquote>
<p>불필요한 UI 업데이트 최소화</p>
</blockquote>
</li>
</ul>
<hr>
<h3 id="🔧-기본-사용법">🔧 기본 사용법</h3>
<pre><code class="language-kotlin">@Composable
fun SearchScreen() {
    var searchQuery by remember { mutableStateOf(&quot;&quot;) }
    val items = remember { listOf(&quot;Apple&quot;, &quot;Banana&quot;, &quot;Cherry&quot;, &quot;Date&quot;, &quot;Elderberry&quot;) }

    val filteredItems by remember {
        derivedStateOf {
            items.filter { it.contains(searchQuery, ignoreCase = true) }
        }
    }

    Column {
        TextField(
            value = searchQuery,
            onValueChange = { searchQuery = it },
            label = { Text(&quot;검색&quot;) }
        )

        LazyColumn {
            items(filteredItems) { item -&gt;
                Text(item, modifier = Modifier.padding(8.dp))
            }
        }
    }
}</code></pre>
<p>동작 원리</p>
<ol>
<li>searchQuery가 변경될 때만 filteredItems가 재계산됨</li>
<li>searchQuery가 같은 값으로 여러 번 recomposition되어도 한 번만 계산</li>
<li>필터링 결과가 실제로 달라질 때만 LazyColumn이 업데이트됨</li>
</ol>
<hr>
<h3 id="⚖️-mutablestateof-vs-derivedstateof">⚖️ mutableStateOf vs derivedStateOf</h3>
<p>파생 상태를 관리할 때 mutableStateOf를 사용할지, derivedStateOf를 고민하게 되는데, 두 방식의 차이를 비교해보겠습니다.</p>
<p>mutableStateOf를 사용한 방식 (❌ 비효율적)</p>
<pre><code class="language-kotlin">@Composable
fun SearchScreenWithMutableState() {
    var searchQuery by remember { mutableStateOf(&quot;&quot;) }
    val items = remember { listOf(&quot;Apple&quot;, &quot;Banana&quot;, &quot;Cherry&quot;, &quot;Date&quot;, &quot;Elderberry&quot;) }

    // mutableStateOf로 필터링 결과를 별도 상태로 관리
    var filteredItems by remember { mutableStateOf(items) }

    Column {
        TextField(
            value = searchQuery,
            onValueChange = { newQuery -&gt;
                searchQuery = newQuery
                // 수동으로 필터링 결과 업데이트 - 실수하기 쉬움!
                filteredItems = items.filter { 
                    it.contains(newQuery, ignoreCase = true) 
                }
            },
            label = { Text(&quot;검색&quot;) }
        )

        LazyColumn {
            items(filteredItems) { item -&gt;
                Text(item, modifier = Modifier.padding(8.dp))
            }
        }
    }
}</code></pre>
<p><strong>문제점</strong></p>
<ul>
<li>상태를 두 번 관리해야 함 (searchQuery, filteredItems)</li>
<li>업데이트 로직이 분산되어 있어 실수하기 쉬움</li>
<li>searchQuery를 다른 곳에서도 변경한다면 filteredItems 업데이트를 잊어버릴 수 있음</li>
<li>메모리 낭비 (원본과 필터링 결과 모두 저장)</li>
</ul>
<br/>

<p>derivedStateOf를 사용한 방식 (✅ 효율적)</p>
<pre><code class="language-kotlin">@Composable
fun SearchScreenWithDerivedState() {
    var searchQuery by remember { mutableStateOf(&quot;&quot;) }
    val items = remember { listOf(&quot;Apple&quot;, &quot;Banana&quot;, &quot;Cherry&quot;, &quot;Date&quot;, &quot;Elderberry&quot;) }

    // derivedStateOf로 자동 계산
    val filteredItems by remember {
        derivedStateOf {
            items.filter { it.contains(searchQuery, ignoreCase = true) }
        }
    }

    Column {
        TextField(
            value = searchQuery,
            onValueChange = { searchQuery = it },
            label = { Text(&quot;검색&quot;) }
        )

        LazyColumn {
            items(filteredItems) { item -&gt;
                Text(item, modifier = Modifier.padding(8.dp))
            }
        }
    }
}</code></pre>
<p><strong>장점</strong></p>
<ul>
<li>원본 상태(searchQuery)만 관리</li>
<li>파생 값은 자동으로 계산되고 캐싱됨</li>
<li>로직이 한 곳에 집중되어 유지보수 쉬움</li>
<li>메모리 효율적 (계산 결과만 캐싱)</li>
</ul>
<hr>
<h3 id="🎯-마무리">🎯 마무리</h3>
<p>derivedStateOf는 Jetpack Compose에서 파생 상태를 효율적으로 관리하는 핵심 도구입니다. 이를 적절히 활용하면 불필요한 재계산을 방지하고, 코드를 더 선언적이고 유지보수하기 쉽게 만들 수 있습니다. 상태 관리가 복잡해질 때마다 &quot;이 값은 다른 상태로부터 파생되는 것인가?&quot;를 한 번 생각해고, 그렇다면, derivedStateOf가 여러분의 코드를 한층 더 깔끔하게 만들 수 있을 것입니다.</p>
<hr>
<h3 id="💡-참고자료">💡 참고자료</h3>
<p><a href="https://developer.android.com/jetpack/compose/state">Jetpack Compose 공식 문서 - State</a>
<a href="https://developer.android.com/jetpack/compose/performance">Compose Performance 가이드</a>
<a href="https://developer.android.com/reference/kotlin/androidx/compose/runtime/package-summary#derivedStateOf(kotlin.Function0)">derivedStateOf API 문서</a></p>
]]></description>
        </item>
    </channel>
</rss>