하루 만에 Android 앱 만들기

하루 만에 Android 앱 만들기

Google Trends를 보여주는 간단한 앱을 만들고 싶었다. “지금 뭐가 핫해?”를 빠르게 확인하는 앱.

Jetpack Compose를 처음 써봤는데, LLM(Claude)의 도움으로 생각보다 빠르게 만들 수 있었다. Compose 문법을 몰라도 “이런 UI 만들어줘”라고 하면 코드가 나온다.


목표

  1. Google Trends RSS 파싱
  2. 트렌드 키워드 목록 표시
  3. 관련 뉴스 표시
  4. 북마크 기능
  5. AdMob 광고

하루 안에 끝내기.


Jetpack Compose 시작

XML 레이아웃 대신 Kotlin 코드로 UI를 작성한다.

@Composable
fun TrendItem(trend: TrendItem, onClick: () -> Unit) {
    Card(
        modifier = Modifier
            .fillMaxWidth()
            .padding(8.dp)
            .clickable(onClick = onClick),
        elevation = CardDefaults.cardElevation(4.dp)
    ) {
        Column(modifier = Modifier.padding(16.dp)) {
            Text(
                text = trend.keyword,
                style = MaterialTheme.typography.titleMedium
            )
            Text(
                text = trend.approxTraffic,
                style = MaterialTheme.typography.bodySmall,
                color = MaterialTheme.colorScheme.secondary
            )
        }
    }
}

선언형 UI라서 직관적이다. React 경험이 있으면 금방 적응된다.


Google Trends는 RSS 피드를 제공한다.

class NewsApiImpl : NewsApi {
    override suspend fun fetchTrends(): List<TrendItem> = withContext(Dispatchers.IO) {
        val url = "https://trends.google.com/trending/rss?geo=KR"
        val factory = XmlPullParserFactory.newInstance()
        val parser = factory.newPullParser()

        URL(url).openStream().use { stream ->
            parser.setInput(stream, "UTF-8")
            parseTrends(parser)
        }
    }

    private fun parseTrends(parser: XmlPullParser): List<TrendItem> {
        val items = mutableListOf<TrendItem>()
        var item: TrendItem? = null

        while (parser.eventType != XmlPullParser.END_DOCUMENT) {
            when (parser.eventType) {
                XmlPullParser.START_TAG -> {
                    when (parser.name) {
                        "item" -> item = TrendItem()
                        "title" -> item?.keyword = parser.nextText()
                        "ht:approx_traffic" -> item?.approxTraffic = parser.nextText()
                    }
                }
                XmlPullParser.END_TAG -> {
                    if (parser.name == "item") {
                        item?.let { items.add(it) }
                    }
                }
            }
            parser.next()
        }
        return items
    }
}

ht:approx_traffic에 검색량이 들어있다. “100K+” 같은 형태.


Pull-to-Refresh

Compose에서 당겨서 새로고침:

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun TrendsScreen(viewModel: NewsViewModel) {
    val uiState by viewModel.trends.collectAsState()
    val isRefreshing by viewModel.isRefreshing.collectAsState()

    val pullRefreshState = rememberPullToRefreshState()

    PullToRefreshBox(
        isRefreshing = isRefreshing,
        onRefresh = { viewModel.refresh() },
        state = pullRefreshState
    ) {
        LazyColumn {
            items(uiState.trends) { trend ->
                TrendItem(trend = trend)
            }
        }
    }
}

Material 3의 PullToRefreshBox를 사용하면 간단하다.


Horizontal Pager (탭 대신)

여러 화면을 좌우 스와이프로 전환:

@Composable
fun MainScreen(viewModel: NewsViewModel) {
    val pagerState = rememberPagerState(pageCount = { 3 })

    HorizontalPager(state = pagerState) { page ->
        when (page) {
            0 -> TrendsScreen(viewModel)
            1 -> BookmarksScreen(viewModel)
            2 -> SettingsScreen()
        }
    }
}

하단 네비게이션 바와 연동:

NavigationBar {
    NavigationBarItem(
        icon = { Icon(Icons.Default.Whatshot, "Trends") },
        selected = pagerState.currentPage == 0,
        onClick = {
            coroutineScope.launch {
                pagerState.animateScrollToPage(0)
            }
        }
    )
    // ...
}

Room 데이터베이스 (북마크)

북마크를 로컬에 저장:

@Entity(tableName = "bookmarks")
data class BookmarkedArticle(
    @PrimaryKey val link: String,
    val title: String,
    val snippet: String,
    val imageUrl: String?,
    val source: String,
    val bookmarkedAt: Long = System.currentTimeMillis()
)

@Dao
interface NewsDao {
    @Query("SELECT * FROM bookmarks ORDER BY bookmarkedAt DESC")
    fun getAllBookmarks(): Flow<List<BookmarkedArticle>>

    @Insert(onConflict = OnConflictStrategy.REPLACE)
    suspend fun bookmark(article: BookmarkedArticle)

    @Delete
    suspend fun removeBookmark(article: BookmarkedArticle)
}

Room + Flow로 실시간 업데이트.


AdMob 네이티브 광고

트렌드 목록 사이에 광고 삽입:

class AdManager(private val context: Context) {
    private var adLoader: AdLoader? = null

    init {
        MobileAds.initialize(context)
    }

    fun loadNativeAd(onLoaded: (NativeAd) -> Unit) {
        adLoader = AdLoader.Builder(context, AD_UNIT_ID)
            .forNativeAd { nativeAd ->
                onLoaded(nativeAd)
            }
            .build()

        adLoader?.loadAd(AdRequest.Builder().build())
    }
}

광고를 TrendItem처럼 표시:

@Composable
fun AdTrendItem(nativeAd: NativeAd) {
    Card(
        modifier = Modifier.fillMaxWidth().padding(8.dp),
        colors = CardDefaults.cardColors(
            containerColor = MaterialTheme.colorScheme.surfaceVariant
        )
    ) {
        Column(modifier = Modifier.padding(16.dp)) {
            Text(
                text = nativeAd.headline ?: "",
                style = MaterialTheme.typography.titleMedium
            )
            Text(
                text = "광고",
                style = MaterialTheme.typography.labelSmall,
                color = MaterialTheme.colorScheme.secondary
            )
        }
    }
}

타임라인

시간 작업
01:10 프로젝트 생성, 기본 구조
15:44 Compose UI, ViewModel
15:47 Material 3 테마
15:50 Pull-to-refresh
16:28 Google Trends RSS 파싱
17:55 Horizontal Pager
21:06 AdMob 연동

총 작업 시간: 약 8시간 (점심, 저녁 제외)


LLM 활용

솔직히 Jetpack Compose를 제대로 공부한 적이 없다. LLM에게 물어보면서 만들었다.

나: "Google Trends RSS를 파싱해서 LazyColumn으로 보여주는 코드 만들어줘"
LLM: (코드 생성)
나: "여기에 pull-to-refresh 추가해줘"
LLM: (코드 수정)

이런 식으로 대화하면서 코드를 완성했다. 모르는 API도 물어보면 설명해주고, 에러가 나면 붙여넣으면 고쳐준다.

LLM 시대의 개발:

물론 LLM이 생성한 코드를 이해하고 검증하는 능력은 필요하다. 하지만 진입 장벽이 확실히 낮아졌다.


결론

Jetpack Compose + LLM으로 하루 만에 꽤 괜찮은 앱을 만들 수 있었다.

좋았던 점:

아쉬운 점:

XML 레이아웃 시절로 돌아가고 싶지 않다. 그리고 LLM 없이 개발하던 시절로도.

Back