<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>marty_on.log</title>
        <link>https://velog.io/</link>
        <description>Im deep diving in Android</description>
        <lastBuildDate>Mon, 07 Jul 2025 00:32:57 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>marty_on.log</title>
            <url>https://velog.velcdn.com/images/marty_on/profile/dfc9aebc-ce70-4093-92d7-f15960f934f1/image.jpeg</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. marty_on.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/marty_on" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[Kotlin Flow 실전 활용 가이드 – WaveOn 앱 개발 경험을 바탕으로]]></title>
            <link>https://velog.io/@marty_on/Kotlin-Flow-%EC%8B%A4%EC%A0%84-%ED%99%9C%EC%9A%A9-%EA%B0%80%EC%9D%B4%EB%93%9C-WaveOn-%EC%95%B1-%EA%B0%9C%EB%B0%9C-%EA%B2%BD%ED%97%98%EC%9D%84-%EB%B0%94%ED%83%95%EC%9C%BC%EB%A1%9C</link>
            <guid>https://velog.io/@marty_on/Kotlin-Flow-%EC%8B%A4%EC%A0%84-%ED%99%9C%EC%9A%A9-%EA%B0%80%EC%9D%B4%EB%93%9C-WaveOn-%EC%95%B1-%EA%B0%9C%EB%B0%9C-%EA%B2%BD%ED%97%98%EC%9D%84-%EB%B0%94%ED%83%95%EC%9C%BC%EB%A1%9C</guid>
            <pubDate>Mon, 07 Jul 2025 00:32:57 GMT</pubDate>
            <description><![CDATA[<p>안녕하세요, marty입니다.
이번 글에서는 Kotlin Flow의 핵심 개념과 실전 활용 방법을 WaveOn 앱 개발 경험을 바탕으로 정리해보겠습니다.</p>
<hr>
<h2 id="flow의-기본-개념">Flow의 기본 개념</h2>
<p>Flow는 비동기 데이터 스트림을 처리하는 Kotlin 라이브러리입니다. 시간에 따라 지속적으로 발생하는 데이터를 효율적으로 처리할 수 있도록 설계되었습니다.</p>
<h3 id="기본-구조">기본 구조</h3>
<pre><code class="language-kotlin">val flow = flow {
    emit(1)  // 데이터 방출
    delay(1000)
    emit(2)  // 추가 데이터 방출
    delay(1000)
    emit(3)  // 연속적인 데이터 스트림
}</code></pre>
<hr>
<h2 id="flow의-핵심-구성요소">Flow의 핵심 구성요소</h2>
<h3 id="flow-builder-패턴">Flow Builder 패턴</h3>
<pre><code class="language-kotlin">// 1. flow { } - 기본 Flow 생성
val basicFlow = flow {
    for (i in 1..5) {
        emit(i)
        delay(100)
    }
}

// 2. flowOf() - 고정값으로 Flow 생성
val fixedFlow = flowOf(1, 2, 3, 4, 5)

// 3. asFlow() - 컬렉션을 Flow로 변환
val listFlow = listOf(1, 2, 3, 4, 5).asFlow()

// 4. callbackFlow - 콜백 기반 API를 Flow로 변환
val callbackFlow = callbackFlow {
    val callback = object : ApiCallback {
        override fun onData(data: String) {
            trySend(data)
        }
    }
    api.registerCallback(callback)

    awaitClose { api.unregisterCallback(callback) }
}</code></pre>
<h3 id="stateflow와-sharedflow">StateFlow와 SharedFlow</h3>
<pre><code class="language-kotlin">// StateFlow - 현재 상태를 유지하는 Flow
val stateFlow = MutableStateFlow(0)

// SharedFlow - 이벤트를 발생시키는 Flow
val sharedFlow = MutableSharedFlow&lt;String&gt;()</code></pre>
<hr>
<h2 id="실전-활용-사례">실전 활용 사례</h2>
<h3 id="api-호출-결과-처리">API 호출 결과 처리</h3>
<pre><code class="language-kotlin">fun fetchWeatherData(): Flow&lt;WeatherResult&gt; = flow {
    emit(WeatherResult.Loading)

    try {
        val response = weatherApiService.getWeather()
        val weatherData = response.toWeatherData()
        emit(WeatherResult.Success(weatherData))
    } catch (e: Exception) {
        emit(WeatherResult.Error(e.message))
    }
}</code></pre>
<h3 id="데이터베이스-실시간-감지">데이터베이스 실시간 감지</h3>
<pre><code class="language-kotlin">@Query(&quot;SELECT * FROM reservations ORDER BY date ASC&quot;)
fun getAllReservations(): Flow&lt;List&lt;ReservationEntity&gt;&gt;

fun getUpcomingReservations(): Flow&lt;List&lt;Reservation&gt;&gt; {
    return reservationDao.getAllReservations()
        .map { entities -&gt; entities.map { it.toDomainModel() } }
        .map { reservations -&gt; 
            reservations.filter { it.date &gt;= Date() }
        }
}</code></pre>
<h3 id="여러-데이터-소스-조합">여러 데이터 소스 조합</h3>
<pre><code class="language-kotlin">fun getCombinedData(): Flow&lt;CombinedData&gt; {
    return combine(
        weatherRepository.getWeatherData(),
        temperatureRepository.getTemperatureData()
    ) { weather, temperature -&gt;
        CombinedData(weather, temperature)
    }
}</code></pre>
<hr>
<h2 id="flow-연산자-활용">Flow 연산자 활용</h2>
<h3 id="변환-연산자">변환 연산자</h3>
<pre><code class="language-kotlin">val numbers = flowOf(1, 2, 3, 4, 5)

// map - 각 값을 변환
numbers.map { it * 2 }  // 2, 4, 6, 8, 10

// filter - 조건에 맞는 값만 필터링
numbers.filter { it % 2 == 0 }  // 2, 4

// transform - 복잡한 변환
numbers.transform { value -&gt;
    emit(&quot;Number: $value&quot;)
    emit(&quot;Squared: ${value * value}&quot;)
}</code></pre>
<h3 id="조합-연산자">조합 연산자</h3>
<pre><code class="language-kotlin">val flow1 = flowOf(1, 2, 3)
val flow2 = flowOf(&quot;A&quot;, &quot;B&quot;, &quot;C&quot;)

// zip - 두 Flow를 짝지어서 조합
flow1.zip(flow2) { number, letter -&gt;
    &quot;$number$letter&quot;
}  // 1A, 2B, 3C

// combine - 여러 Flow의 최신 값들을 조합
combine(flow1, flow2) { number, letter -&gt;
    &quot;$number$letter&quot;
}</code></pre>
<h3 id="에러-처리-연산자">에러 처리 연산자</h3>
<pre><code class="language-kotlin">val riskyFlow = flow {
    emit(1)
    throw Exception(&quot;Something went wrong!&quot;)
}

// catch - 에러를 잡아서 처리
riskyFlow.catch { error -&gt;
    emit(-1)  // 에러 시 기본값
}

// onEach - 각 값에 대해 부수 효과 실행
riskyFlow.onEach { value -&gt;
    println(&quot;Received: $value&quot;)
}</code></pre>
<hr>
<h2 id="compose에서의-flow-활용">Compose에서의 Flow 활용</h2>
<h3 id="collectasstate-활용">collectAsState 활용</h3>
<pre><code class="language-kotlin">@Composable
fun WeatherScreen(viewModel: WeatherViewModel) {
    val weatherData by viewModel.weatherData.collectAsState()

    when (weatherData) {
        is WeatherResult.Loading -&gt; LoadingSpinner()
        is WeatherResult.Success -&gt; WeatherContent(weatherData.data)
        is WeatherResult.Error -&gt; ErrorMessage(weatherData.message)
    }
}</code></pre>
<h3 id="launchedeffect와의-조합">LaunchedEffect와의 조합</h3>
<pre><code class="language-kotlin">@Composable
fun WeatherScreen(viewModel: WeatherViewModel) {
    val weatherData by viewModel.weatherData.collectAsState()

    LaunchedEffect(Unit) {
        viewModel.fetchWeatherData()
    }

    // UI 구성...
}</code></pre>
<hr>
<h2 id="실전-개발-팁">실전 개발 팁</h2>
<h3 id="디버깅-전략">디버깅 전략</h3>
<pre><code class="language-kotlin">val debugFlow = originalFlow
    .onEach { println(&quot;Flow value: $it&quot;) }
    .catch { error -&gt; 
        println(&quot;Flow error: $error&quot;)
        throw error
    }</code></pre>
<h3 id="타임아웃-설정">타임아웃 설정</h3>
<pre><code class="language-kotlin">val timeoutFlow = originalFlow
    .timeout(5000)  // 5초 타임아웃
    .catch { error -&gt;
        if (error is TimeoutCancellationException) {
            emit(DefaultValue)
        } else {
            throw error
        }
    }</code></pre>
<h3 id="중복-제거">중복 제거</h3>
<pre><code class="language-kotlin">val distinctFlow = originalFlow
    .distinctUntilChanged()  // 연속된 중복 값 제거</code></pre>
<hr>
<h2 id="주의사항과-해결책">주의사항과 해결책</h2>
<h3 id="flow-수집-중-에러-처리">Flow 수집 중 에러 처리</h3>
<pre><code class="language-kotlin">// 올바른 에러 처리 방법
viewModelScope.launch {
    flow.catch { error -&gt;
        emit(DefaultValue)
    }.collect { value -&gt;
        processValue(value)
    }
}</code></pre>
<h3 id="메모리-누수-방지">메모리 누수 방지</h3>
<pre><code class="language-kotlin">private var job: Job? = null

fun startCollecting() {
    job = viewModelScope.launch {
        flow.collect { value -&gt;
            // 처리
        }
    }
}

fun stopCollecting() {
    job?.cancel()
}</code></pre>
<h3 id="stateflow-초기값-설정">StateFlow 초기값 설정</h3>
<pre><code class="language-kotlin">// 권장하는 방법
val stateFlow = MutableStateFlow(WeatherData.empty())</code></pre>
<hr>
<h2 id="성능-최적화">성능 최적화</h2>
<h3 id="적절한-dispatcher-사용">적절한 Dispatcher 사용</h3>
<pre><code class="language-kotlin">val optimizedFlow = originalFlow
    .flowOn(Dispatchers.IO)  // 백그라운드에서 처리
    .onEach { value -&gt;
        withContext(Dispatchers.Main) {
            updateUI(value)
        }
    }</code></pre>
<h3 id="버퍼링-전략">버퍼링 전략</h3>
<pre><code class="language-kotlin">val bufferedFlow = originalFlow
    .buffer(10)  // 10개까지 버퍼링
    .conflate()  // 최신 값만 유지</code></pre>
<hr>
<h2 id="waveon-앱에서의-flow-활용-사례">WaveOn 앱에서의 Flow 활용 사례</h2>
<ul>
<li>실시간 날씨 데이터 업데이트</li>
<li>예약 내역 실시간 동기화</li>
<li>여러 API 응답 조합</li>
<li>데이터베이스 변경 감지</li>
<li>사용자 인터랙션 이벤트 처리</li>
</ul>
<hr>
<h2 id="마치며">마치며</h2>
<p>Flow는 비동기 데이터 처리를 위한 강력한 도구입니다. 적절한 연산자 활용과 에러 처리, 성능 최적화를 통해 안정적이고 효율적인 데이터 스트림을 구축할 수 있습니다.</p>
<p>다음 글에서는 Flow 테스팅 방법과 Custom Flow 연산자 구현에 대해 다뤄볼 예정입니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[WaveOn 앱 개발기 3편: 오프라인 지원과 사용자 경험 개선 – Room DB 적용기]]></title>
            <link>https://velog.io/@marty_on/WaveOn-%EC%95%B1-%EA%B0%9C%EB%B0%9C%EA%B8%B0-3%ED%8E%B8-%EC%98%A4%ED%94%84%EB%9D%BC%EC%9D%B8-%EC%A7%80%EC%9B%90%EA%B3%BC-%EC%82%AC%EC%9A%A9%EC%9E%90-%EA%B2%BD%ED%97%98-%EA%B0%9C%EC%84%A0-Room-DB-%EC%A0%81%EC%9A%A9%EA%B8%B0</link>
            <guid>https://velog.io/@marty_on/WaveOn-%EC%95%B1-%EA%B0%9C%EB%B0%9C%EA%B8%B0-3%ED%8E%B8-%EC%98%A4%ED%94%84%EB%9D%BC%EC%9D%B8-%EC%A7%80%EC%9B%90%EA%B3%BC-%EC%82%AC%EC%9A%A9%EC%9E%90-%EA%B2%BD%ED%97%98-%EA%B0%9C%EC%84%A0-Room-DB-%EC%A0%81%EC%9A%A9%EA%B8%B0</guid>
            <pubDate>Mon, 07 Jul 2025 00:30:58 GMT</pubDate>
            <description><![CDATA[<p>안녕하세요, marty입니다.
이번 글에서는 WaveOn 앱의 오프라인 지원과 사용자 경험 개선을 위해 Room 데이터베이스를 도입하고, 주요 기능을 확장한 과정을 정리합니다.</p>
<hr>
<h2 id="오프라인-데이터의-필요성">오프라인 데이터의 필요성</h2>
<p>앱을 종료 후 재실행할 때마다 데이터가 초기화되는 문제를 경험했습니다. 예약 내역 등 주요 정보가 매번 새로 로딩되어야 하는 불편함이 있었고, 오프라인 환경에서도 데이터를 확인할 수 있도록 데이터 지속성이 필요하다고 판단했습니다. Compose 기반 UI와의 결합을 고려할 때, 데이터의 안정적 관리가 더욱 중요해졌습니다.</p>
<hr>
<h2 id="room-db-설정-및-적용">Room DB 설정 및 적용</h2>
<h3 id="의존성-추가">의존성 추가</h3>
<p>build.gradle.kts에 Room 관련 의존성을 추가하여 데이터베이스 환경을 구축했습니다.</p>
<pre><code class="language-kotlin">val roomVersion = &quot;2.6.1&quot;
implementation(&quot;androidx.room:room-runtime:$roomVersion&quot;)
implementation(&quot;androidx.room:room-ktx:$roomVersion&quot;)
ksp(&quot;androidx.room:room-compiler:$roomVersion&quot;)</code></pre>
<hr>
<h3 id="entity-및-typeconverter-구현">Entity 및 TypeConverter 구현</h3>
<p>데이터베이스 테이블 구조는 Entity 클래스로 정의했습니다. Date 타입 등은 Room에서 직접 지원하지 않으므로 TypeConverter를 별도로 구현해 타입 변환을 처리했습니다.</p>
<pre><code class="language-kotlin">@Entity(tableName = &quot;reservations&quot;)
data class ReservationEntity(
    @PrimaryKey(autoGenerate = true)
    val id: Long = 0,
    val reservationNumber: String,
    val sessionDate: Date,
    val sessionTime: String,
    val sessionType: String,
    val remainingSeats: Int,
    val totalSeats: Int,
    val price: Int,
    val status: String
)

class DateConverter {
    @TypeConverter
    fun fromTimestamp(value: Long?): Date? = value?.let { Date(it) }
    @TypeConverter
    fun dateToTimestamp(date: Date?): Long? = date?.time
}</code></pre>
<hr>
<h3 id="dao-data-access-object-설계">DAO (Data Access Object) 설계</h3>
<p>Room의 DAO를 통해 데이터베이스 접근 인터페이스를 정의했습니다. Flow를 반환하도록 하여 데이터 변경을 실시간으로 관찰할 수 있도록 했습니다.</p>
<pre><code class="language-kotlin">@Dao
interface ReservationDao {
    @Query(&quot;SELECT * FROM reservations ORDER BY sessionDate ASC&quot;)
    fun getAllReservations(): Flow&lt;List&lt;ReservationEntity&gt;&gt;

    @Insert(onConflict = OnConflictStrategy.REPLACE)
    suspend fun insertReservation(reservation: ReservationEntity): Long

    @Delete
    suspend fun deleteReservation(reservation: ReservationEntity)
}</code></pre>
<hr>
<h2 id="repository-패턴과-compose-연동">Repository 패턴과 Compose 연동</h2>
<p>Repository 계층에서 Room DB와 연동하여, 도메인 모델과 Entity 간 변환을 명확히 분리했습니다. Flow를 활용해 Compose UI와 자연스럽게 연결할 수 있었습니다.</p>
<pre><code class="language-kotlin">@Singleton
class ReservationRepository @Inject constructor(
    private val reservationDao: ReservationDao
) {
    fun getUpcomingReservations(): Flow&lt;List&lt;Reservation&gt;&gt; =
        reservationDao.getUpcomingReservations(Date()).map { entities -&gt;
            entities.map { it.toDomainModel() }
        }

    suspend fun insertReservation(reservation: Reservation): Long =
        reservationDao.insertReservation(reservation.toEntity())
}

@Composable
fun ReservationScreen(viewModel: ReservationViewModel) {
    val reservations by viewModel.reservations.collectAsState()
    LazyColumn {
        items(reservations) { reservation -&gt;
            ReservationCard(reservation)
        }
    }
}</code></pre>
<p>Entity와 Domain Model을 분리함으로써 데이터 계층과 비즈니스 로직의 독립성을 확보했습니다.</p>
<hr>
<h2 id="hilt를-통한-di-모듈-구성">Hilt를 통한 DI 모듈 구성</h2>
<p>Room 데이터베이스와 DAO를 Hilt DI 모듈로 제공하여, 의존성 주입을 통해 각 계층의 결합도를 낮췄습니다.</p>
<pre><code class="language-kotlin">@Module
@InstallIn(SingletonComponent::class)
object DatabaseModule {
    @Provides
    @Singleton
    fun provideDatabase(@ApplicationContext context: Context): WaveParkDatabase =
        WaveParkDatabase.getDatabase(context)

    @Provides
    @Singleton
    fun provideReservationDao(database: WaveParkDatabase): ReservationDao =
        database.reservationDao()
}</code></pre>
<hr>
<h2 id="사용자-경험-개선--qr-코드-및-로딩-피드백">사용자 경험 개선 – QR 코드 및 로딩 피드백</h2>
<p>예약 내역에 QR 코드를 추가하여 입장 절차를 간소화했습니다. ZXing 라이브러리를 활용해 QR 코드를 생성하고, Compose로 다이얼로그 UI를 구현했습니다.</p>
<pre><code class="language-kotlin">val writer = QRCodeWriter()
val bitMatrix = writer.encode(reservationNumber, BarcodeFormat.QR_CODE, 200, 200)

@Composable
fun QRCodeDialog(reservationNumber: String, onDismiss: () -&gt; Unit) {
    Dialog(onDismissRequest = onDismiss) {
        Card {
            Column(
                modifier = Modifier.padding(16.dp),
                horizontalAlignment = Alignment.CenterHorizontally
            ) {
                Text(&quot;예약 QR 코드&quot;, style = MaterialTheme.typography.h6)
                Spacer(modifier = Modifier.height(16.dp))
                Image(
                    painter = rememberQrBitmapPainter(reservationNumber),
                    contentDescription = &quot;QR Code&quot;,
                    modifier = Modifier.size(200.dp)
                )
            }
        }
    }
}</code></pre>
<p>네트워크 통신에는 타임아웃을 설정해 무한 로딩을 방지했고, 로딩 중에는 ProgressBar를 통해 사용자에게 명확한 피드백을 제공했습니다.</p>
<pre><code class="language-kotlin">private val okHttpClient = OkHttpClient.Builder()
    .addInterceptor(loggingInterceptor)
    .connectTimeout(30, TimeUnit.SECONDS)
    .readTimeout(30, TimeUnit.SECONDS)
    .build()</code></pre>
<hr>
<h2 id="주요-기능-및-기술적-성장">주요 기능 및 기술적 성장</h2>
<ul>
<li>실시간 날씨/수온 정보 제공</li>
<li>예약 내역 실시간 크롤링 및 오프라인 데이터 캐싱</li>
<li>QR 코드 생성 및 사용자 친화적 UI</li>
<li>안정적인 네트워크 처리와 로딩 피드백</li>
<li>XML에서 Compose로의 전환, MVVM 아키텍처, Hilt DI, Room DB, Flow, 코루틴, WebView, Jsoup, StateFlow 등 다양한 기술의 실전 적용</li>
</ul>
<hr>
<h2 id="다음-목표">다음 목표</h2>
<ul>
<li>빈자리 알림 푸시</li>
<li>카풀/커뮤니티 기능</li>
<li>관리자/운영자 기능</li>
<li>다국어 지원</li>
<li>테스트 코드 및 CI/CD</li>
</ul>
<hr>
<h2 id="마치며">마치며</h2>
<p>Compose, Flow, Room DB 등 현대적인 Android 개발 기술을 실제 서비스에 적용하며 데이터 관리와 사용자 경험의 중요성을 다시 한 번 확인할 수 있었습니다. 익숙한 환경에서 벗어나 새로운 기술을 도입하는 과정은 쉽지 않았지만, 그만큼 의미 있는 결과를 얻을 수 있었습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[WaveOn 앱 개발기 2편: 실시간 데이터 연동의 실제 – API부터 WebView까지]]></title>
            <link>https://velog.io/@marty_on/WaveOn-%EC%95%B1-%EA%B0%9C%EB%B0%9C%EA%B8%B0-2%ED%8E%B8-%EC%8B%A4%EC%8B%9C%EA%B0%84-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EC%97%B0%EB%8F%99%EC%9D%98-%EC%8B%A4%EC%A0%9C-API%EB%B6%80%ED%84%B0-WebView%EA%B9%8C%EC%A7%80</link>
            <guid>https://velog.io/@marty_on/WaveOn-%EC%95%B1-%EA%B0%9C%EB%B0%9C%EA%B8%B0-2%ED%8E%B8-%EC%8B%A4%EC%8B%9C%EA%B0%84-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EC%97%B0%EB%8F%99%EC%9D%98-%EC%8B%A4%EC%A0%9C-API%EB%B6%80%ED%84%B0-WebView%EA%B9%8C%EC%A7%80</guid>
            <pubDate>Fri, 04 Jul 2025 07:17:30 GMT</pubDate>
            <description><![CDATA[<p>안녕하세요, marty입니다.
이번 글에서는 WaveOn 앱에 실시간 데이터를 연동하는 과정을 정리해보려 합니다.
새로운 기술 스택(MVVM, Flow, Compose)을 적용하며 겪었던 시행착오와, 그 과정에서 얻은 인사이트를 공유합니다.</p>
<hr>
<h2 id="데이터-연동의-시작--retrofit-설정">데이터 연동의 시작 – Retrofit 설정</h2>
<p>새로운 프로젝트를 시작할 때마다 익숙한 라이브러리도 다시 한 번 점검하게 됩니다.
Retrofit과 OkHttp를 활용해 API 통신을 구성했습니다.
HttpLoggingInterceptor를 추가해 네트워크 통신을 투명하게 확인할 수 있도록 했고,
GsonConverterFactory로 데이터 파싱의 편의성도 챙겼습니다.</p>
<pre><code class="language-kotlin">val client = OkHttpClient.Builder()
    .addInterceptor(HttpLoggingInterceptor().apply {
        level = HttpLoggingInterceptor.Level.BODY
    })
    .build()

val retrofit = Retrofit.Builder()
    .baseUrl(&quot;https://apis.data.go.kr/&quot;)
    .client(client)
    .addConverterFactory(GsonConverterFactory.create())
    .build()</code></pre>
<hr>
<h2 id="날씨-api-연동--데이터-파싱의-고민">날씨 API 연동 – 데이터 파싱의 고민</h2>
<p>기상청 API를 통해 실시간 날씨 정보를 받아오는 과정에서는
카테고리별로 필요한 데이터를 추출하는 로직에 신경을 썼습니다.
처음에는 단순히 리스트에서 원하는 값을 찾는 방식으로 접근했지만,
점차 데이터 구조를 이해하고, 더 효율적인 파싱 방법을 고민하게 되었습니다.</p>
<pre><code class="language-kotlin">val sky = items.find { 
    it.category == &quot;SKY&quot; &amp;&amp; it.fcstTime == targetFcstTime 
}?.fcstValue

val pty = items.find { 
    it.category == &quot;PTY&quot; &amp;&amp; it.fcstTime == targetFcstTime 
}?.fcstValue</code></pre>
<hr>
<h2 id="수온-api-연동--코루틴과-비동기-처리">수온 API 연동 – 코루틴과 비동기 처리</h2>
<p>WavePark 인근의 바다 수온 정보도 앱에서 제공하고자 했습니다.
API 호출은 suspend 함수를 통해 코루틴 기반으로 처리했습니다.
비동기 프로그래밍의 장점을 살려, UI와 데이터 처리를 분리할 수 있었습니다.</p>
<pre><code class="language-kotlin">@GET(&quot;temperature&quot;)
suspend fun getWaterTemperature(): TemperatureResponse</code></pre>
<hr>
<h2 id="webview-내장--웹사이트와의-연결">WebView 내장 – 웹사이트와의 연결</h2>
<p>WavePark 공식 웹사이트를 앱 내에서 바로 확인할 수 있도록 WebView를 구성했습니다.
JavaScript, DOM Storage, Zoom 등 필요한 설정을 꼼꼼히 적용해
웹 환경과 유사한 사용자 경험을 제공하고자 했습니다.</p>
<pre><code class="language-kotlin">webView.settings.apply {
    javaScriptEnabled = true
    domStorageEnabled = true
    setSupportZoom(true)
}</code></pre>
<hr>
<h2 id="jsoup을-활용한-예약-내역-크롤링">Jsoup을 활용한 예약 내역 크롤링</h2>
<p>공식 API가 제공되지 않는 예약 내역은 Jsoup을 활용해 웹 크롤링 방식으로 처리했습니다.
HTML 구조를 파악하고, 필요한 데이터를 추출하는 과정에서
HTTP 헤더를 커스터마이징하여 접근 차단을 우회하는 등
실제 서비스 환경에서 마주칠 수 있는 다양한 상황을 경험할 수 있었습니다.</p>
<pre><code class="language-kotlin">val doc = Jsoup.connect(url)
    .userAgent(&quot;Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36&quot;)
    .header(&quot;Accept&quot;, &quot;text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8&quot;)
    .header(&quot;Accept-Language&quot;, &quot;ko-KR,ko;q=0.8,en-US;q=0.5,en;q=0.3&quot;)
    .get()

val reservations = doc.select(&quot;.reservation-item&quot;)
    .map { element -&gt;
        Reservation(
            number = element.select(&quot;.number&quot;).text(),
            date = element.select(&quot;.date&quot;).text(),
            // ...
        )
    }</code></pre>
<hr>
<h2 id="compose와-stateflow--선언형-ui와-데이터-흐름">Compose와 StateFlow – 선언형 UI와 데이터 흐름</h2>
<p>Compose와 StateFlow를 조합해
실시간 데이터가 자연스럽게 UI에 반영되도록 설계했습니다.
MutableStateFlow로 데이터를 관리하고,
collectAsState를 통해 Compose에서 손쉽게 상태를 구독할 수 있었습니다.</p>
<pre><code class="language-kotlin">class ReservationRepository @Inject constructor() {
    private val _reservations = MutableStateFlow&lt;List&lt;Reservation&gt;&gt;(emptyList())
    val reservations: StateFlow&lt;List&lt;Reservation&gt;&gt; = _reservations
}

// Compose에서 사용
@Composable
fun ReservationList(viewModel: ReservationViewModel) {
    val reservations by viewModel.reservations.collectAsState()

    LazyColumn {
        items(reservations) { reservation -&gt;
            ReservationItem(reservation)
        }
    }
}</code></pre>
<hr>
<h2 id="마치며">마치며</h2>
<p>날씨, 수온, 예약 내역 등 다양한 실시간 데이터를 앱에 연동하며
MVVM, Flow, Compose 등 최신 기술 스택의 장점을 직접 체감할 수 있었습니다.
각 기술의 특성을 이해하고, 실제 서비스에 적용하는 과정에서
데이터 흐름과 UI의 결합이 얼마나 중요한지 다시 한 번 느꼈습니다.</p>
<p>다음 글에서는 오프라인 지원을 위한 Room 데이터베이스 적용과
데이터 캐싱 전략에 대해 다뤄볼 예정입니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Compose와 함께 사용하는 데이터 라이브러리 비교 (LiveData vs Flow)]]></title>
            <link>https://velog.io/@marty_on/Compose%EC%99%80-%ED%95%A8%EA%BB%98-%EC%82%AC%EC%9A%A9%ED%95%98%EB%8A%94-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EB%9D%BC%EC%9D%B4%EB%B8%8C%EB%9F%AC%EB%A6%AC-%EB%B9%84%EA%B5%90-LiveData-vs-Flow</link>
            <guid>https://velog.io/@marty_on/Compose%EC%99%80-%ED%95%A8%EA%BB%98-%EC%82%AC%EC%9A%A9%ED%95%98%EB%8A%94-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EB%9D%BC%EC%9D%B4%EB%B8%8C%EB%9F%AC%EB%A6%AC-%EB%B9%84%EA%B5%90-LiveData-vs-Flow</guid>
            <pubDate>Fri, 04 Jul 2025 00:26:07 GMT</pubDate>
            <description><![CDATA[<p>안녕하세요. Marty입니다</p>
<p>Android 개발을 하면서 가장 많이 고민하는 부분 중 하나가 바로 <strong>상태 관리</strong>입니다. 특히 Jetpack Compose를 사용하면서 LiveData와 Flow 중 어떤 것을 사용해야 할지 고민하신 분들이 많을 것입니다.</p>
<p>오늘은 제가 WaveOn 앱을 개발하면서 겪은 경험을 바탕으로 두 라이브러리를 비교해보겠습니다.</p>
<h2 id="왜-이-글을-쓰게-되었나요">왜 이 글을 쓰게 되었나요?</h2>
<p>7년간 주로 LiveData를 사용해왔는데, WaveOn 앱에서 Compose를 도입하면서 Flow로 마이그레이션하게 되었습니다. 그 과정에서 두 라이브러리의 차이점을 정말 많이 느꼈습니다.</p>
<h2 id="livedata-vs-flow-기본-개념">LiveData vs Flow 기본 개념</h2>
<h3 id="livedata란">LiveData란?</h3>
<pre><code class="language-kotlin">class WeatherViewModel : ViewModel() {
    private val _weatherData = MutableLiveData&lt;WeatherData&gt;()
    val weatherData: LiveData&lt;WeatherData&gt; = _weatherData

    fun fetchWeather() {
        viewModelScope.launch {
            val data = repository.getWeatherData()
            _weatherData.value = data
        }
    }
}</code></pre>
<h3 id="flow란">Flow란?</h3>
<pre><code class="language-kotlin">class WeatherViewModel : ViewModel() {
    private val _weatherData = MutableStateFlow&lt;WeatherData?&gt;(null)
    val weatherData: StateFlow&lt;WeatherData?&gt; = _weatherData.asStateFlow()

    fun fetchWeather() {
        viewModelScope.launch {
            repository.getWeatherData().collect { data -&gt;
                _weatherData.value = data
            }
        }
    }
}</code></pre>
<h2 id="compose에서의-사용법-비교">Compose에서의 사용법 비교</h2>
<h3 id="livedata-사용법">LiveData 사용법</h3>
<pre><code class="language-kotlin">@Composable
fun WeatherScreen(viewModel: WeatherViewModel) {
    val weatherData by viewModel.weatherData.observeAsState()

    weatherData?.let { data -&gt;
        Text(text = &quot;온도: ${data.temperature}°C&quot;)
        Text(text = &quot;날씨: ${data.weatherStatus}&quot;)
    }
}</code></pre>
<h3 id="flow-사용법">Flow 사용법</h3>
<pre><code class="language-kotlin">@Composable
fun WeatherScreen(viewModel: WeatherViewModel) {
    val weatherData by viewModel.weatherData.collectAsState()

    weatherData?.let { data -&gt;
        Text(text = &quot;온도: ${data.temperature}°C&quot;)
        Text(text = &quot;날씨: ${data.weatherStatus}&quot;)
    }
}</code></pre>
<h2 id="실제-개발-경험담">실제 개발 경험담</h2>
<h3 id="시행착오-1-livedata의-한계">시행착오 1: LiveData의 한계</h3>
<p>7년간 LiveData를 사용해왔는데, Compose와 함께 사용하면서 몇 가지 한계점을 발견했습니다:</p>
<pre><code class="language-kotlin">// 문제가 있던 코드
class ReservationRepository @Inject constructor() {
    private val _reservations = MutableLiveData&lt;List&lt;Reservation&gt;&gt;()
    val reservations: LiveData&lt;List&lt;Reservation&gt;&gt; = _reservations

    fun addReservation(reservation: Reservation) {
        val currentList = _reservations.value ?: emptyList()
        _reservations.value = currentList + reservation
    }
}

// Compose에서 사용할 때
@Composable
fun ReservationList(viewModel: ReservationViewModel) {
    val reservations by viewModel.reservations.observeAsState()
    // observeAsState()를 사용해야 하는 번거로움
}</code></pre>
<p><strong>문제점:</strong></p>
<ul>
<li>LiveData는 단일 값만 저장할 수 있어서 리스트 업데이트가 복잡합니다</li>
<li>데이터 변환 작업이 어렵습니다</li>
<li>여러 데이터 소스를 조합하기 어렵습니다</li>
<li>Compose에서 사용할 때 observeAsState() 변환이 필요합니다</li>
</ul>
<h3 id="시행착오-2-flow로-마이그레이션">시행착오 2: Flow로 마이그레이션</h3>
<p>Flow로 바꾸면서 정말 편해졌습니다:</p>
<pre><code class="language-kotlin">// 개선된 코드
class ReservationRepository @Inject constructor(
    private val reservationDao: ReservationDao
) {
    fun getUpcomingReservations(): Flow&lt;List&lt;Reservation&gt;&gt; {
        return reservationDao.getUpcomingReservations(Date()).map { entities -&gt;
            entities.map { it.toDomainModel() }
        }
    }
}

// Compose에서 사용할 때
@Composable
fun ReservationList(viewModel: ReservationViewModel) {
    val reservations by viewModel.reservations.collectAsState()
    // collectAsState()로 직접 사용 가능!
}</code></pre>
<p><strong>장점:</strong></p>
<ul>
<li>데이터 변환이 쉬워집니다 (map, filter, combine 등)</li>
<li>여러 데이터 소스를 쉽게 조합할 수 있습니다</li>
<li>실시간 데이터 스트림 처리에 최적화되어 있습니다</li>
<li>Compose에서 collectAsState()로 직접 사용 가능합니다</li>
</ul>
<h2 id="성능-비교">성능 비교</h2>
<h3 id="livedata">LiveData</h3>
<ul>
<li>메모리 효율적 (Observer 패턴)</li>
<li>생명주기 인식</li>
<li>단일 값만 처리 가능</li>
<li>복잡한 데이터 변환 어려움</li>
</ul>
<h3 id="flow">Flow</h3>
<ul>
<li>강력한 연산자들 (map, filter, combine, etc.)</li>
<li>여러 데이터 소스 조합 가능</li>
<li>코루틴과 완벽 호환</li>
<li>초기 학습 곡선이 있음</li>
</ul>
<h2 id="실제-사용-사례">실제 사용 사례</h2>
<h3 id="waveon-앱에서의-활용">WaveOn 앱에서의 활용</h3>
<p><strong>1. 날씨 데이터 실시간 업데이트</strong></p>
<pre><code class="language-kotlin">// Flow 사용
fun getWeatherData(): Flow&lt;WeatherData&gt; {
    return weatherApiService.getWeather()
        .map { response -&gt; response.toWeatherData() }
        .catch { error -&gt; 
            emit(WeatherData.error(error.message))
        }
}</code></pre>
<p><strong>2. 예약 내역 필터링</strong></p>
<pre><code class="language-kotlin">// Flow의 강력한 연산자 활용
fun getUpcomingReservations(): Flow&lt;List&lt;Reservation&gt;&gt; {
    return reservationDao.getAllReservations()
        .map { reservations -&gt;
            reservations.filter { it.date &gt;= Date() }
                .sortedBy { it.date }
        }
}</code></pre>
<p><strong>3. 여러 데이터 소스 조합</strong></p>
<pre><code class="language-kotlin">// 날씨 + 수온 데이터 조합
fun getCombinedData(): Flow&lt;CombinedData&gt; {
    return combine(
        weatherRepository.getWeatherData(),
        temperatureRepository.getTemperatureData()
    ) { weather, temperature -&gt;
        CombinedData(weather, temperature)
    }
}</code></pre>
<h2 id="언제-어떤-것을-사용할까요">언제 어떤 것을 사용할까요?</h2>
<h3 id="livedata를-사용하는-경우">LiveData를 사용하는 경우</h3>
<ul>
<li>간단한 UI 상태 관리</li>
<li>단일 값 업데이트</li>
<li>기존 View 시스템과 호환성 필요</li>
<li>빠른 프로토타이핑</li>
</ul>
<h3 id="flow를-사용하는-경우">Flow를 사용하는 경우</h3>
<ul>
<li>복잡한 데이터 변환 필요</li>
<li>여러 데이터 소스 조합</li>
<li>실시간 데이터 스트림</li>
<li>Compose와 함께 사용</li>
<li>장기적인 프로젝트</li>
</ul>
<h2 id="마이그레이션-팁">마이그레이션 팁</h2>
<h3 id="livedata-→-flow-전환">LiveData → Flow 전환</h3>
<pre><code class="language-kotlin">// 기존 LiveData
private val _data = MutableLiveData&lt;String&gt;()

// Flow로 변경
private val _data = MutableStateFlow&lt;String?&gt;(null)
val data: StateFlow&lt;String?&gt; = _data.asStateFlow()</code></pre>
<h3 id="compose에서-사용">Compose에서 사용</h3>
<pre><code class="language-kotlin">// LiveData
val data by viewModel.data.observeAsState()

// Flow
val data by viewModel.data.collectAsState()</code></pre>
<h2 id="결론">결론</h2>
<p>개인적인 추천: Flow를 사용하세요.</p>
<p>특히 Compose를 사용한다면 Flow가 훨씬 더 자연스럽고 강력합니다. 처음에는 조금 어려울 수 있지만, 한번 익숙해지면 정말 편리합니다.</p>
<h3 id="waveon-앱에서의-최종-선택">WaveOn 앱에서의 최종 선택</h3>
<p>저는 WaveOn 앱에서 Flow를 선택했습니다. 그 이유는:</p>
<ul>
<li>실시간 데이터 업데이트가 많아서</li>
<li>여러 API 응답을 조합해야 해서</li>
<li>Compose와 함께 사용하기 때문에</li>
<li>향후 확장성을 고려해서</li>
</ul>
<h3 id="배운-점">배운 점</h3>
<ul>
<li>Flow의 강력한 연산자들을 활용하면 코드가 훨씬 깔끔해집니다</li>
<li>데이터 변환과 조합이 정말 쉬워집니다</li>
<li>Compose와의 호환성이 완벽합니다</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Google AI Edge SDK: 기기 내 AI 모델로 앱을 더 스마트하게 만들기]]></title>
            <link>https://velog.io/@marty_on/Google-AI-Edge-SDK-%EA%B8%B0%EA%B8%B0-%EB%82%B4-AI-%EB%AA%A8%EB%8D%B8%EB%A1%9C-%EC%95%B1%EC%9D%84-%EB%8D%94-%EC%8A%A4%EB%A7%88%ED%8A%B8%ED%95%98%EA%B2%8C-%EB%A7%8C%EB%93%A4%EA%B8%B0</link>
            <guid>https://velog.io/@marty_on/Google-AI-Edge-SDK-%EA%B8%B0%EA%B8%B0-%EB%82%B4-AI-%EB%AA%A8%EB%8D%B8%EB%A1%9C-%EC%95%B1%EC%9D%84-%EB%8D%94-%EC%8A%A4%EB%A7%88%ED%8A%B8%ED%95%98%EA%B2%8C-%EB%A7%8C%EB%93%A4%EA%B8%B0</guid>
            <pubDate>Thu, 03 Jul 2025 04:22:48 GMT</pubDate>
            <description><![CDATA[<p>안녕하세요. Marty입니다.</p>
<p>오늘은 Google에서 최근에 발표한 <strong>AI Edge SDK</strong>와 <strong>Gemini Nano experimental access</strong>에 대해 소개해드리겠습니다. 이 SDK는 기기 내에서 AI 모델을 실행할 수 있게 해주는 혁신적인 도구로, 현재 실험적 접근 프로그램을 통해 개발자들이 테스트할 수 있습니다.</p>
<h2 id="ai-edge-sdk란">AI Edge SDK란?</h2>
<p>AI Edge SDK는 Android 기기에서 <strong>Gemini Nano</strong> 모델을 실행할 수 있게 해주는 Google의 새로운 SDK입니다. 기존의 클라우드 기반 AI 서비스와 달리, 이 SDK는 사용자의 기기에서 직접 AI 모델을 실행하므로 개인정보 보호와 응답 속도 면에서 큰 장점이 있습니다.</p>
<h3 id="주요-특징">주요 특징</h3>
<ul>
<li><strong>기기 내 실행</strong>: 인터넷 연결 없이도 AI 기능 사용 가능</li>
<li><strong>개인정보 보호</strong>: 데이터가 기기를 벗어나지 않음</li>
<li><strong>빠른 응답</strong>: 네트워크 지연 없이 즉시 응답</li>
<li><strong>오프라인 지원</strong>: 인터넷이 없는 환경에서도 사용 가능</li>
</ul>
<h2 id="실험적-접근-프로그램-참여하기">실험적 접근 프로그램 참여하기</h2>
<p>현재 Gemini Nano는 실험적 접근 프로그램을 통해 테스트할 수 있습니다. 참여하기 위해서는 다음 단계를 따라야 합니다:</p>
<h3 id="필수-요구사항">필수 요구사항</h3>
<ul>
<li><strong>Pixel 9 시리즈 기기</strong>가 필요합니다</li>
<li>테스트용으로 사용할 계정으로만 로그인되어 있어야 합니다</li>
</ul>
<h3 id="참여-단계">참여 단계</h3>
<ol>
<li><strong>aicore-experimental Google 그룹에 가입</strong></li>
<li><strong>Android AICore 테스팅 프로그램에 참여</strong></li>
<li><strong>Play 스토어에서 앱 이름이 &quot;Android AICore&quot;에서 &quot;Android AICore (Beta)&quot;로 변경되는지 확인</strong></li>
</ol>
<h3 id="apk-업데이트">APK 업데이트</h3>
<ol>
<li><p><strong>AICore APK 업데이트</strong>:</p>
<ul>
<li>프로필 아이콘 → 관리 앱 및 기기 → 관리</li>
<li>Android AICore → 업데이트 (가능한 경우)</li>
</ul>
</li>
<li><p><strong>Private Compute Service APK 업데이트</strong>:</p>
<ul>
<li>프로필 아이콘 → 관리 앱 및 기기 → 관리</li>
<li>Private Compute Services → 업데이트 (가능한 경우)</li>
<li>앱 정보 탭에서 버전이 1.0.release.658389993 이상인지 확인</li>
</ul>
</li>
<li><p><strong>기기 재시작</strong> 후 몇 분 대기하여 테스팅 등록이 적용되도록 함</p>
</li>
<li><p><strong>Play 스토어에서 AICore APK 버전 확인</strong> (앱 정보 탭에서 0.thirdpartyeap으로 시작하는지 확인)</p>
</li>
</ol>
<h2 id="사용-사례">사용 사례</h2>
<p>AI Edge SDK는 다음과 같은 특정 태스크에 최적화되어 있습니다:</p>
<h3 id="1-텍스트-문구-변경">1. 텍스트 문구 변경</h3>
<p>사용자의 메시지나 텍스트의 어조와 스타일을 변경할 수 있습니다.</p>
<pre><code class="language-kotlin">// 예시: 캐주얼한 메시지를 격식 있는 스타일로 변경
val casualMessage = &quot;안녕! 오늘 날씨 진짜 좋네&quot;
val formalMessage = aiEdgeSDK.changeTextStyle(casualMessage, Style.FORMAL)
// 결과: &quot;안녕하세요. 오늘 날씨가 정말 좋습니다.&quot;</code></pre>
<h3 id="2-스마트-답장">2. 스마트 답장</h3>
<p>채팅 대화에서 맥락에 맞는 응답을 생성합니다.</p>
<pre><code class="language-kotlin">// 대화 맥락을 기반으로 적절한 답장 생성
val conversation = listOf(
    &quot;안녕하세요&quot;,
    &quot;안녕! 오늘 뭐해?&quot;,
    &quot;서핑하러 갈까 해&quot;
)
val smartReply = aiEdgeSDK.generateSmartReply(conversation)
// 결과: &quot;와! 좋은 날씨에 서핑하기 딱이네요. 즐거운 시간 보내세요!&quot;</code></pre>
<h3 id="3-교정">3. 교정</h3>
<p>맞춤법과 문법 오류를 자동으로 수정합니다.</p>
<pre><code class="language-kotlin">val textWithErrors = &quot;나는 어제 친구와 함께 영화관에 갔어요.&quot;
val correctedText = aiEdgeSDK.correctText(textWithErrors)
// 결과: &quot;저는 어제 친구와 함께 영화관에 갔습니다.&quot;</code></pre>
<h3 id="4-요약">4. 요약</h3>
<p>긴 문서나 텍스트를 간결한 요약으로 압축합니다.</p>
<pre><code class="language-kotlin">val longDocument = &quot;매우 긴 문서 내용...&quot;
val summary = aiEdgeSDK.summarizeText(longDocument)
// 결과: &quot;주요 내용을 요약한 간결한 텍스트&quot;</code></pre>
<h2 id="실제-google-앱에서의-활용">실제 Google 앱에서의 활용</h2>
<p>Google은 이미 여러 앱에서 AI Edge SDK를 활용하고 있습니다:</p>
<h3 id="talkback">TalkBack</h3>
<p>Android의 접근성 앱인 TalkBack은 Gemini Nano의 다중 모드 입력 기능을 활용하여 시각 장애가 있는 사용자를 위한 이미지 설명을 개선했습니다.</p>
<h3 id="pixel-voice-recorder">Pixel Voice Recorder</h3>
<p>Pixel Voice Recorder 앱은 AI Edge SDK를 사용하여 기기 내 요약 기능을 지원합니다. 긴 녹음 파일을 자동으로 요약해주는 기능이 추가되었습니다.</p>
<h3 id="gboard">Gboard</h3>
<p>Gboard의 스마트 답장 기능은 AI Edge SDK를 통해 온디바이스 Gemini Nano를 활용하여 정확한 스마트 답장을 제공합니다.</p>
<h2 id="개발-환경-설정">개발 환경 설정</h2>
<h3 id="1-gradle-설정">1. Gradle 설정</h3>
<pre><code class="language-kotlin">// build.gradle.kts
dependencies {
    implementation(&quot;com.google.ai.edge.aicore:aicore:0.0.1-exp01&quot;)
}

android {
    defaultConfig {
        minSdk = 31  // 최소 SDK 31 이상 필요
        // ...
    }
}</code></pre>
<h3 id="2-generationconfig-생성">2. GenerationConfig 생성</h3>
<p>AI 모델의 추론 방식을 커스터마이징할 수 있는 설정 객체를 생성합니다:</p>
<pre><code class="language-kotlin">val generationConfig = generationConfig {
    context = ApplicationProvider.getApplicationContext() // 필수
    temperature = 0.2f        // 무작위성 제어 (높을수록 다양성 증가)
    topK = 16                // 고순위 토큰 중 고려할 토큰 수
    maxOutputTokens = 256    // 응답 길이
}</code></pre>
<h3 id="3-generativemodel-초기화">3. GenerativeModel 초기화</h3>
<pre><code class="language-kotlin">// 선택적 다운로드 콜백 생성 (디버깅용)
val downloadConfig = DownloadConfig(downloadCallback)

// GenerativeModel 객체 생성
val generativeModel = GenerativeModel(
    generationConfig = generationConfig,
    downloadConfig = downloadConfig // 선택사항
)</code></pre>
<h2 id="실제-사용-예제">실제 사용 예제</h2>
<h3 id="1-기본-추론-실행">1. 기본 추론 실행</h3>
<pre><code class="language-kotlin">scope.launch {
    // 단일 문자열 입력 프롬프트
    val input = &quot;&quot;&quot;
        I want you to act as an English proofreader. I will provide you texts, 
        and I would like you to review them for any spelling, grammar, or 
        punctuation errors. Once you have finished reviewing the text, provide me 
        with any necessary corrections or suggestions for improving the text: 
        These arent the droids your looking for.
    &quot;&quot;&quot;.trimIndent()

    val response = generativeModel.generateContent(input)
    println(response.text)
}</code></pre>
<h3 id="2-다중-문자열-입력">2. 다중 문자열 입력</h3>
<pre><code class="language-kotlin">scope.launch {
    val response = generativeModel.generateContent(
        content {
            text(&quot;I want you to act as an English proofreader. I will provide you texts and I would like you to review them for any spelling, grammar, or punctuation errors.&quot;)
            text(&quot;Once you have finished reviewing the text, provide me with any necessary corrections or suggestions for improving the text:&quot;)
            text(&quot;These arent the droids your looking for.&quot;)
        }
    )
    println(response.text)
}</code></pre>
<h3 id="3-텍스트-교정-기능">3. 텍스트 교정 기능</h3>
<pre><code class="language-kotlin">class TextCorrector {
    private val generativeModel: GenerativeModel = // 초기화

    suspend fun correctText(text: String): String {
        val prompt = &quot;&quot;&quot;
            I want you to act as an English proofreader. I will provide you texts, 
            and I would like you to review them for any spelling, grammar, or 
            punctuation errors. Once you have finished reviewing the text, provide me 
            with any necessary corrections or suggestions for improving the text: $text
        &quot;&quot;&quot;.trimIndent()

        val response = generativeModel.generateContent(prompt)
        return response.text ?: text
    }
}</code></pre>
<h3 id="4-스마트-답장-생성-이모지-예측">4. 스마트 답장 생성 (이모지 예측)</h3>
<pre><code class="language-kotlin">class SmartReplyGenerator {
    private val generativeModel: GenerativeModel = // 초기화

    suspend fun predictEmojis(message: String): String {
        val prompt = &quot;&quot;&quot;
            Predict up to 5 emojis as a response to a text chat message. 
            The output should only include emojis.

            input: $message
            output:
        &quot;&quot;&quot;.trimIndent()

        val response = generativeModel.generateContent(prompt)
        return response.text ?: &quot;👍&quot;
    }
}

// 사용 예시
val emojis = smartReplyGenerator.predictEmojis(&quot;The new visual design is blowing my mind 🤯&quot;)
// 결과: ➕,💘,❤‍🔥</code></pre>
<h2 id="프롬프트-설계-팁">프롬프트 설계 팁</h2>
<p>프롬프트 설계는 언어 모델로부터 최적의 응답을 이끌어내는 과정입니다. 잘 구조화된 프롬프트를 작성하는 것은 정확하고 고품질의 응답을 보장하는 필수적인 부분입니다.</p>
<h3 id="주의사항">주의사항</h3>
<ul>
<li><strong>Gemini Nano는 최대 12,000개의 입력 토큰을 허용</strong>합니다</li>
<li>명확하고 구체적인 지시사항을 제공하세요</li>
<li>원하는 출력 형식을 명시하세요</li>
</ul>
<h3 id="텍스트-교정용-프롬프트">텍스트 교정용 프롬프트</h3>
<pre><code class="language-kotlin">val correctionPrompt = &quot;&quot;&quot;
    I want you to act as an English proofreader. I will provide you texts, and I
    would like you to review them for any spelling, grammar, or punctuation errors.
    Once you have finished reviewing the text, provide me with any necessary
    corrections or suggestions for improving the text: $inputText
&quot;&quot;&quot;.trimIndent()</code></pre>
<h3 id="스마트-답장용-프롬프트">스마트 답장용 프롬프트</h3>
<pre><code class="language-kotlin">val smartReplyPrompt = &quot;&quot;&quot;
    Predict up to 5 emojis as a response to a text chat message. The output
    should only include emojis.

    input: $message
    output:
&quot;&quot;&quot;.trimIndent()</code></pre>
<h3 id="예시-결과들">예시 결과들</h3>
<pre><code class="language-kotlin">// 입력: &quot;The new visual design is blowing my mind 🤯&quot;
// 출력: ➕,💘,❤‍🔥

// 입력: &quot;Well that looks great regardless&quot;
// 출력: 💗,🪄

// 입력: &quot;Unfortunately this won&#39;t work&quot;
// 출력: 💔,😔

// 입력: &quot;sounds good, I&#39;ll look into that&quot;
// 출력: 🙏,👍</code></pre>
<h2 id="성능-최적화-팁">성능 최적화 팁</h2>
<h3 id="1-메시지-표시-전략">1. 메시지 표시 전략</h3>
<p>사용자에게 AI 처리 중임을 알리는 것이 중요합니다. 로딩 인디케이터를 표시하여 사용자 경험을 개선하세요.</p>
<pre><code class="language-kotlin">@Composable
fun AITextProcessor() {
    var isProcessing by remember { mutableStateOf(false) }
    var result by remember { mutableStateOf(&quot;&quot;) }

    Column {
        if (isProcessing) {
            CircularProgressIndicator()
            Text(&quot;AI가 처리 중입니다...&quot;)
        } else {
            Text(result)
        }

        Button(
            onClick = {
                isProcessing = true
                // AI 처리 로직
                isProcessing = false
            }
        ) {
            Text(&quot;텍스트 처리하기&quot;)
        }
    }
}</code></pre>
<h3 id="2-에러-처리">2. 에러 처리</h3>
<p>모델 로딩 실패나 추론 오류에 대한 적절한 에러 처리를 구현하세요.</p>
<pre><code class="language-kotlin">suspend fun processWithAI(text: String): Result&lt;String&gt; {
    return try {
        val response = generativeModel.generateContent(text)
        Result.success(response.text ?: &quot;&quot;)
    } catch (e: Exception) {
        Result.failure(e)
    }
}</code></pre>
<h2 id="주의사항-1">주의사항</h2>
<h3 id="1-실험적-접근-프로그램">1. 실험적 접근 프로그램</h3>
<ul>
<li>현재 Gemini Nano는 실험적 접근 프로그램을 통해만 사용 가능합니다</li>
<li>Pixel 9 시리즈 기기가 필수적으로 필요합니다</li>
<li>테스팅 계정으로만 로그인되어 있어야 합니다</li>
</ul>
<h3 id="2-기기-호환성">2. 기기 호환성</h3>
<ul>
<li>현재 지원되는 기기 목록을 확인하고, 지원되지 않는 기기에서는 대체 기능을 제공하세요</li>
<li>AICore APK와 Private Compute Service APK가 최신 버전인지 확인하세요</li>
</ul>
<h3 id="3-모델-크기와-성능">3. 모델 크기와 성능</h3>
<ul>
<li>Gemini Nano는 기기 내 실행을 위해 최적화되어 있지만, 여전히 상당한 메모리를 사용할 수 있습니다</li>
<li>최대 12,000개의 입력 토큰 제한이 있습니다</li>
</ul>
<h3 id="4-배터리-소모">4. 배터리 소모</h3>
<ul>
<li>AI 모델 실행은 배터리 소모를 증가시킬 수 있으므로, 적절한 사용 패턴을 권장하세요</li>
</ul>
<h2 id="미래-전망">미래 전망</h2>
<p>Google은 AI Edge SDK의 기능을 지속적으로 확장할 계획입니다:</p>
<ul>
<li><strong>추가 기기 지원</strong>: 더 많은 Android 기기에서 사용 가능</li>
<li><strong>새로운 모달리티</strong>: 이미지, 음성 등 다양한 입력 형태 지원</li>
<li><strong>향상된 성능</strong>: 더 빠르고 정확한 AI 모델</li>
</ul>
<h2 id="결론">결론</h2>
<p>Google AI Edge SDK와 Gemini Nano experimental access는 Android 앱 개발에 새로운 가능성을 열어주는 혁신적인 도구입니다. 기기 내 AI 실행을 통해 개인정보 보호와 성능을 모두 확보할 수 있어, 앞으로 많은 앱에서 활용될 것으로 예상됩니다.</p>
<p>현재는 실험적 접근 프로그램을 통해 Pixel 9 시리즈 기기에서만 테스트할 수 있지만, 이는 AI 기술의 미래를 미리 경험할 수 있는 좋은 기회입니다.</p>
<p>특히 서핑 앱 WaveOn과 같은 서비스에서는 사용자 메시지의 스마트 답장, 예약 정보 요약, 또는 서핑 팁 생성 등에 활용할 수 있을 것 같습니다.</p>
<p>새로운 기술에 대한 학습과 적용은 항상 도전적이지만, 이런 혁신적인 도구들을 활용하면 더욱 스마트하고 사용자 친화적인 앱을 만들 수 있습니다.</p>
<h3 id="피드백-제공">피드백 제공</h3>
<p>Google AI Edge SDK나 기타 피드백이 있으시면 <a href="https://support.google.com/">Google 개발자 지원팀에 티켓을 제출</a>하실 수 있습니다.</p>
<hr>
<p><strong>참고 자료:</strong></p>
<ul>
<li><a href="https://developer.android.com/ai/gemini-nano/ai-edge-sdk">Google AI Edge SDK 공식 문서</a></li>
<li><a href="https://developer.android.com/ai/gemini-nano/ai-edge-sdk">Gemini Nano Experimental Access 가이드</a></li>
</ul>
<hr>
<p><strong>참고 자료:</strong></p>
<ul>
<li><a href="https://developer.android.com/ai/gemini-nano/ai-edge-sdk">Google AI Edge SDK 공식 문서</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[WaveOn 앱 개발기 1편: "서핑 앱을 만들어보자! - 기획부터 첫 번째 화면까지"]]></title>
            <link>https://velog.io/@marty_on/WaveOn-%EC%95%B1-%EA%B0%9C%EB%B0%9C%EA%B8%B0-1%ED%8E%B8-%EC%84%9C%ED%95%91-%EC%95%B1%EC%9D%84-%EB%A7%8C%EB%93%A4%EC%96%B4%EB%B3%B4%EC%9E%90-%EA%B8%B0%ED%9A%8D%EB%B6%80%ED%84%B0-%EC%B2%AB-%EB%B2%88%EC%A7%B8-%ED%99%94%EB%A9%B4%EA%B9%8C%EC%A7%80</link>
            <guid>https://velog.io/@marty_on/WaveOn-%EC%95%B1-%EA%B0%9C%EB%B0%9C%EA%B8%B0-1%ED%8E%B8-%EC%84%9C%ED%95%91-%EC%95%B1%EC%9D%84-%EB%A7%8C%EB%93%A4%EC%96%B4%EB%B3%B4%EC%9E%90-%EA%B8%B0%ED%9A%8D%EB%B6%80%ED%84%B0-%EC%B2%AB-%EB%B2%88%EC%A7%B8-%ED%99%94%EB%A9%B4%EA%B9%8C%EC%A7%80</guid>
            <pubDate>Thu, 03 Jul 2025 02:12:22 GMT</pubDate>
            <description><![CDATA[<p>안녕하세요. Marty입니다. </p>
<p>오늘부터 WavePark 서핑 레저 시설을 위한 Android 앱 &quot;WaveOn&quot; 개발 과정을 공유하려고 합니다. </p>
<h2 id="왜-이-앱을-만들게-되었나요">왜 이 앱을 만들게 되었나요?</h2>
<p>서핑을 좋아해서 자주 WavePark에 가는데, 웹사이트가 불편했습니다. 모바일에서 접속하면 화면이 작고, 자동로그인 기능 미지원과 예약 정보를 확인하기도 번거로웠죠. </p>
<p>그래서 직접 앱을 만들어보기로 했습니다. </p>
<h2 id="새로운-도전-compose로-ui-구축하기">새로운 도전: Compose로 UI 구축하기</h2>
<p>7년간 XML로 UI를 만들어왔는데, 이번 프로젝트에서는 <strong>Jetpack Compose</strong>를 도입해보기로 했습니다. 새로운 기술을 배우는 건 항상 설레면서도 긴장되는 일이죠.</p>
<h3 id="시행착오-1-xml에서-compose로의-전환">시행착오 1: XML에서 Compose로의 전환</h3>
<ul>
<li>7년간 익숙했던 XML 레이아웃과 완전히 다른 패러다임</li>
<li>LinearLayout, ConstraintLayout 대신 Column, Row, Box 사용</li>
<li>findViewById() 대신 remember와 state 관리</li>
<li>처음에는 정말 어색했습니다</li>
</ul>
<h3 id="시행착오-2-선언형-ui-적응하기">시행착오 2: 선언형 UI 적응하기</h3>
<ul>
<li>XML에서는 &quot;어떻게 그릴까?&quot;를 생각했는데</li>
<li>Compose에서는 &quot;무엇을 보여줄까?&quot;를 생각해야 합니다</li>
<li>상태 변화에 따른 UI 업데이트가 자동으로 되는 게 신기하면서도 어려웠습니다</li>
</ul>
<h2 id="첫-번째-화면-만들기">첫 번째 화면 만들기</h2>
<h3 id="splash-화면부터-시작했습니다">Splash 화면부터 시작했습니다:</h3>
<pre><code class="language-kotlin">@AndroidEntryPoint
class SplashActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            WaveOnTheme {
                SplashScreen()
            }
        }
    }
}

@Composable
fun SplashScreen() {
    var startAnimation by remember { mutableStateOf(false) }

    LaunchedEffect(Unit) {
        startAnimation = true
        delay(3000)
        // 메인 화면으로 이동
    }

    Box(
        modifier = Modifier.fillMaxSize(),
        contentAlignment = Alignment.Center
    ) {
        // 로고 애니메이션
        Image(
            painter = painterResource(id = R.drawable.logo),
            contentDescription = &quot;Logo&quot;,
            modifier = Modifier
                .size(200.dp)
                .scale(if (startAnimation) 1f else 0.5f)
                .animateContentSize()
        )
    }
}</code></pre>
<p>XML로는 10줄이면 끝날 코드가 Compose로는 30줄이 되었습니다. 하지만 애니메이션과 상태 관리가 훨씬 직관적이었습니다.</p>
<h2 id="bottom-navigation-구현">Bottom Navigation 구현</h2>
<p>메인 화면에는 4개의 탭을 만들었습니다:</p>
<ul>
<li>홈 (이벤트, 날씨, 수온 정보)</li>
<li>대시보드 (세션 정보)</li>
<li>카풀 (커뮤니티)</li>
<li>알림 (예약 내역)</li>
</ul>
<pre><code class="language-kotlin">@Composable
fun MainScreen() {
    var selectedTab by remember { mutableStateOf(0) }

    Scaffold(
        bottomBar = {
            BottomNavigation {
                BottomNavigationItem(
                    icon = { Icon(Icons.Default.Home, &quot;Home&quot;) },
                    label = { Text(&quot;홈&quot;) },
                    selected = selectedTab == 0,
                    onClick = { selectedTab = 0 }
                )
                // ... 다른 탭들
            }
        }
    ) { paddingValues -&gt;
        when (selectedTab) {
            0 -&gt; HomeScreen()
            1 -&gt; DashboardScreen()
            2 -&gt; CarpoolScreen()
            3 -&gt; NotificationScreen()
        }
    }
}</code></pre>
<h2 id="첫-번째-성취감">첫 번째 성취감</h2>
<p>Splash 화면과 Bottom Navigation이 완성되었습니다. 7년간 XML로만 작업해왔는데, Compose로 UI를 만드는 새로운 경험을 했다는 게 신기했습니다.</p>
<h3 id="xml-vs-compose-비교">XML vs Compose 비교</h3>
<p><strong>XML 방식 (기존):</strong></p>
<pre><code class="language-xml">&lt;!-- 20줄의 XML 코드 --&gt;
&lt;LinearLayout&gt;
    &lt;ImageView /&gt;
    &lt;TextView /&gt;
    &lt;!-- 복잡한 레이아웃 구조 --&gt;
&lt;/LinearLayout&gt;</code></pre>
<p><strong>Compose 방식 (새로운):</strong></p>
<pre><code class="language-kotlin">// 10줄의 Compose 코드
Column {
    Image()
    Text()
    // 직관적인 구조
}</code></pre>
<h3 id="배운-것들">배운 것들:</h3>
<ul>
<li>Jetpack Compose의 선언형 UI 패러다임</li>
<li>remember와 mutableStateOf를 활용한 상태 관리</li>
<li>LaunchedEffect를 통한 사이드 이펙트 처리</li>
<li>XML에서 Compose로의 마이그레이션 전략</li>
</ul>
<h2 id="다음-편-예고">다음 편 예고</h2>
<p>다음 편에서는 실제 데이터를 가져와서 화면에 표시하는 작업을 해볼 예정입니다. API 연동부터 시작해서 날씨 정보, 수온 정보를 실시간으로 가져오는 기능을 구현해보겠습니다.</p>
<hr>
]]></description>
        </item>
        <item>
            <title><![CDATA[XML에서 Compose로: 7년 차 개발자의 선언형 UI 적응기]]></title>
            <link>https://velog.io/@marty_on/XML%EC%97%90%EC%84%9C-Compose%EB%A1%9C-7%EB%85%84-%EC%B0%A8-%EA%B0%9C%EB%B0%9C%EC%9E%90%EC%9D%98-%EC%84%A0%EC%96%B8%ED%98%95-UI-%EC%A0%81%EC%9D%91%EA%B8%B0</link>
            <guid>https://velog.io/@marty_on/XML%EC%97%90%EC%84%9C-Compose%EB%A1%9C-7%EB%85%84-%EC%B0%A8-%EA%B0%9C%EB%B0%9C%EC%9E%90%EC%9D%98-%EC%84%A0%EC%96%B8%ED%98%95-UI-%EC%A0%81%EC%9D%91%EA%B8%B0</guid>
            <pubDate>Thu, 19 Jun 2025 14:06:10 GMT</pubDate>
            <description><![CDATA[<p>안녕하세요. 저는 안드로이드 네이티브 앱 개발을 7년간 해오며 수많은 XML 레이아웃을 다뤄왔습니다. 익숙한 ConstraintLayout, 복잡한 RecyclerView Adapter, Fragment 간의 연결 등 이제는 손에 익었지만, Jetpack Compose의 등장은 그런 제 개발 습관에 큰 충격을 줬습니다.</p>
<p>이번 글에서는 <strong>XML 기반 개발에 익숙한 입장에서 Compose로 전환하며 겪었던 시행착오와 느낀 점</strong>을 공유하려 합니다.</p>
<hr>
<h2 id="🌱-compose로-첫-발을-내딛다-기존-xml-화면-이식기">🌱 Compose로 첫 발을 내딛다: 기존 XML 화면 이식기</h2>
<p>가장 먼저 시도한 건, 기존에 XML로 구성된 간단한 로그인 화면을 Compose로 옮겨보는 것이었습니다.</p>
<h3 id="✅-기존-xml-레이아웃-예시">✅ 기존 XML 레이아웃 예시</h3>
<pre><code class="language-xml">&lt;LinearLayout
    xmlns:android=&quot;http://schemas.android.com/apk/res/android&quot;
    android:orientation=&quot;vertical&quot;
    android:layout_width=&quot;match_parent&quot;
    android:layout_height=&quot;match_parent&quot;
    android:padding=&quot;24dp&quot;&gt;

    &lt;EditText
        android:id=&quot;@+id/etEmail&quot;
        android:layout_width=&quot;match_parent&quot;
        android:layout_height=&quot;wrap_content&quot;
        android:hint=&quot;이메일 입력&quot; /&gt;

    &lt;EditText
        android:id=&quot;@+id/etPassword&quot;
        android:layout_width=&quot;match_parent&quot;
        android:layout_height=&quot;wrap_content&quot;
        android:hint=&quot;비밀번호 입력&quot;
        android:inputType=&quot;textPassword&quot; /&gt;

    &lt;Button
        android:id=&quot;@+id/btnLogin&quot;
        android:layout_width=&quot;match_parent&quot;
        android:layout_height=&quot;wrap_content&quot;
        android:text=&quot;로그인&quot; /&gt;
&lt;/LinearLayout&gt;</code></pre>
<h3 id="🛠-jetpack-compose로-변환한-코드">🛠 Jetpack Compose로 변환한 코드</h3>
<pre><code class="language-kotlin">@Composable
fun LoginScreen() {
    var email by remember { mutableStateOf(&quot;&quot;) }
    var password by remember { mutableStateOf(&quot;&quot;) }

    Column(
        modifier = Modifier
            .fillMaxSize()
            .padding(24.dp),
        verticalArrangement = Arrangement.Center
    ) {
        OutlinedTextField(
            value = email,
            onValueChange = { email = it },
            label = { Text(&quot;이메일 입력&quot;) },
            modifier = Modifier.fillMaxWidth()
        )

        Spacer(modifier = Modifier.height(16.dp))

        OutlinedTextField(
            value = password,
            onValueChange = { password = it },
            label = { Text(&quot;비밀번호 입력&quot;) },
            visualTransformation = PasswordVisualTransformation(),
            modifier = Modifier.fillMaxWidth()
        )

        Spacer(modifier = Modifier.height(24.dp))

        Button(
            onClick = { /* 로그인 로직 */ },
            modifier = Modifier.fillMaxWidth()
        ) {
            Text(&quot;로그인&quot;)
        }
    }
}</code></pre>
<p>이처럼 XML을 Compose로 변환하는 과정은 처음엔 낯설지만, 상태 기반 UI 개념과 <code>Modifier</code>, <code>Column</code>, <code>TextField</code>, <code>Button</code> 등 핵심 컴포저블을 익히면서 빠르게 적응할 수 있었습니다.</p>
<hr>
<h2 id="🛠-xml의-익숙함-그리고-한계">🛠 XML의 익숙함, 그리고 한계</h2>
<p>XML 기반 UI는 선언은 구조적으로 명확하지만, 실제 동작과 상호작용까지 구현하려면 다음과 같은 어려움이 있었죠:</p>
<ul>
<li>복잡한 뷰 계층 구조</li>
<li>findViewById / ViewBinding 남용</li>
<li>동적 뷰 생성 시 불편함</li>
<li>RecyclerView의 ViewHolder 패턴 반복</li>
<li>ConstraintLayout 제약조건 관리의 피로감</li>
</ul>
<p>그럼에도 “<strong>눈에 보이는 UI와 코드의 분리</strong>”가 직관적이었기에 오랫동안 써왔습니다.</p>
<hr>
<h2 id="🚀-compose의-첫-인상-선언형의-낯섦과-자유로움">🚀 Compose의 첫 인상: 선언형의 낯섦과 자유로움</h2>
<p>Compose의 첫 인상은 다음과 같았습니다:</p>
<ul>
<li>XML이 없다?! 뷰는 다 코드로 만든다?</li>
<li><code>@Composable</code>이 도대체 뭐지?</li>
<li><code>remember</code>, <code>mutableStateOf</code> 같은 상태 변수?</li>
<li>레이아웃 프리뷰는 왜 가끔씩 안 뜨지?</li>
</ul>
<p>게다가 IDE 환경도 아직 완벽하지 않아서, 처음엔 막막했습니다. “이거 그냥 Flutter 아니야?”라는 생각도 들었습니다.</p>
<hr>
<h2 id="🧱-구조의-전환-선언형-ui-사고로의-전환">🧱 구조의 전환: 선언형 UI 사고로의 전환</h2>
<p>Compose는 <strong>선언형 UI</strong>입니다. 즉, &quot;어떻게 그릴지&quot;가 아니라 **&quot;어떤 상태에서 어떤 모습이어야 하는지&quot;**를 정의합니다.</p>
<p>가장 인상 깊었던 점은:</p>
<ul>
<li>UI 상태 = 데이터 상태</li>
<li>XML + Adapter + ViewHolder 없이 리스트 구현 (예: <code>LazyColumn</code>)</li>
<li><code>Modifier</code>를 통해 UI 속성을 유연하게 정의</li>
</ul>
<p>예전에는 버튼 클릭 시 텍스트뷰를 숨기려면 <code>textView.visibility = View.GONE</code> 같은 imperative 코드를 작성했지만, 이제는 <code>if (isVisible) { Text(...) }</code>처럼 <strong>상태가 UI를 주도</strong>합니다.</p>
<hr>
<h2 id="💥-시행착오-best-3">💥 시행착오 Best 3</h2>
<h3 id="1-상태가-꼬임">1. 상태가 꼬임</h3>
<pre><code class="language-kotlin">var count by remember { mutableStateOf(0) }</code></pre>
<p>state hoisting 개념을 몰랐을 땐, 상태가 Compose 함수 내에 갇혀버리기도 했습니다.</p>
<h3 id="2-recomposition-지옥">2. recomposition 지옥</h3>
<p>컴포저블이 재구성되면서 예상치 못한 동작이 발생하기도 했습니다. 특히, lambda 함수나 remember 설정이 잘못되면 무한 recomposition이...</p>
<h3 id="3-프리뷰에-너무-의존">3. 프리뷰에 너무 의존</h3>
<p>프리뷰가 항상 정확하지 않다는 점을 간과하고, 실제 디바이스에선 레이아웃 깨짐을 여러 번 경험했습니다.</p>
<hr>
<h2 id="🌿-지금은">🌿 지금은?</h2>
<p>처음엔 낯설었지만, 이제는 새로운 프로젝트를 시작할 땐 XML보다 Compose를 먼저 떠올립니다. 이유는:</p>
<ul>
<li>더 적은 코드로 더 많은 기능 구현 가능</li>
<li>커스텀 뷰 구현이 훨씬 간편</li>
<li>MVVM, Hilt 등과의 궁합도 좋음</li>
</ul>
<p>물론 XML이 필요한 레거시 프로젝트나 복잡한 뷰 특수 케이스는 여전히 존재합니다. 하지만 앞으로의 안드로이드 개발에서 Compose는 중심에 설 것입니다.</p>
<hr>
<h2 id="✍️-마치며">✍️ 마치며</h2>
<p>이 글은 선언형 UI에 대한 첫 걸음을 기록한 것이며, 다음 글에서는 Compose + Hilt + MVVM 구조를 실제 코드와 함께 다뤄보겠습니다.</p>
<p><strong>👉 혹시 당신은 Compose를 처음 접했을 때 어땠나요? 댓글로 공유해주세요.</strong></p>
<p>감사합니다!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Compose + Hilt 적응기]]></title>
            <link>https://velog.io/@marty_on/Compose-Hilt-%EC%A0%81%EC%9D%91%EA%B8%B0</link>
            <guid>https://velog.io/@marty_on/Compose-Hilt-%EC%A0%81%EC%9D%91%EA%B8%B0</guid>
            <pubDate>Thu, 19 Jun 2025 13:26:27 GMT</pubDate>
            <description><![CDATA[<p>본 시리즈는
MVC 로만 개발을 하던 개발자의</p>
<p>Compose , Hilt , Kotlin 적응기 입니다.</p>
]]></description>
        </item>
    </channel>
</rss>