์ฝ˜ํ…์ธ  ๋Œ€ํ‘œ ์ด๋ฏธ์ง€ - ๐Ÿš€ ์•ˆ๋“œ๋กœ์ด๋“œ Jetpack ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผํ™œ์šฉํ•œ ์•ฑ ๊ฐœ๋ฐœ ์ƒ์‚ฐ์„ฑ ํ–ฅ์ƒ
๐Ÿ“ฑ ๋ชจ๋ฐ”์ผ/์•ฑ ๊ฐœ๋ฐœ ์™„์ „์ •๋ณต

๐Ÿš€ ์•ˆ๋“œ๋กœ์ด๋“œ Jetpack ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ
ํ™œ์šฉํ•œ ์•ฑ ๊ฐœ๋ฐœ ์ƒ์‚ฐ์„ฑ ํ–ฅ์ƒ

๊ตฌ๊ธ€์ด ๋งŒ๋“  ์ตœ๊ฐ• ๋„๊ตฌ ๋ชจ์Œ์œผ๋กœ ๊ฐœ๋ฐœ ์†๋„ 10๋ฐฐ ์˜ฌ๋ฆฌ๊ธฐ

์•ผ, ์†”์งํžˆ ๋งํ•ด๋ณผ๊ฒŒ. ์•ˆ๋“œ๋กœ์ด๋“œ ์•ฑ ๊ฐœ๋ฐœํ•˜๋‹ค๊ฐ€ "์ด๊ฑฐ ์™œ ์ด๋ ‡๊ฒŒ ๋ณต์žกํ•ด?" ๋ผ๊ณ  ๋А๋‚€ ์  ์—†์–ด? ๐Ÿคฏ
์•กํ‹ฐ๋น„ํ‹ฐ ์ƒ๋ช…์ฃผ๊ธฐ ๊ด€๋ฆฌํ•˜๋‹ค๊ฐ€ ๋จธ๋ฆฌ ํ„ฐ์ง€๊ณ , ๋ฐฑ๊ทธ๋ผ์šด๋“œ ์ž‘์—… ์ฒ˜๋ฆฌํ•˜๋‹ค๊ฐ€ ์•ฑ ํฌ๋ž˜์‹œ ๋‚˜๊ณ , ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์ฟผ๋ฆฌ ์งœ๋‹ค๊ฐ€ ๋ฐค์ƒˆ๊ณ ...
๊ทผ๋ฐ ์žˆ์ž–์•„, ๊ตฌ๊ธ€์ด ์ด ๋ชจ๋“  ๊ณ ํ†ต์„ ํ•ด๊ฒฐํ•˜๋ ค๊ณ  ๋งŒ๋“  ๊ฒŒ ๋ฐ”๋กœ Jetpack์ด์•ผ! ๐ŸŽ‰

Jetpack์€ ๋‹จ์ˆœํ•œ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ํ•˜๋‚˜๊ฐ€ ์•„๋‹ˆ์•ผ. ์•ˆ๋“œ๋กœ์ด๋“œ ๊ฐœ๋ฐœ์˜ ๋ฒ ์ŠคํŠธ ํ”„๋ž™ํ‹ฐ์Šค๋ฅผ ๋ชจ์•„๋†“์€ ๊ฑฐ๋Œ€ํ•œ ๋„๊ตฌ ๋ชจ์Œ์ง‘์ด๋ผ๊ณ  ์ƒ๊ฐํ•˜๋ฉด ๋ผ.
2018๋…„ ๊ตฌ๊ธ€ I/O์—์„œ ์ฒ˜์Œ ๋ฐœํ‘œ๋๊ณ , ์ง€๊ธˆ์€ ์•ˆ๋“œ๋กœ์ด๋“œ ๊ฐœ๋ฐœ์ž๋ผ๋ฉด ๊ฑฐ์˜ ํ•„์ˆ˜๋กœ ์“ฐ๋Š” ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋“ค์ด ์—ฌ๊ธฐ ๋‹ค ๋“ค์–ด์žˆ์–ด.
์˜ค๋Š˜์€ ์ด Jetpack์˜ ํ•ต์‹ฌ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋“ค์„ ํ•˜๋‚˜ํ•˜๋‚˜ ๋œฏ์–ด๋ณด๋ฉด์„œ, ์–ด๋–ป๊ฒŒ ํ•˜๋ฉด ๊ฐœ๋ฐœ ์ƒ์‚ฐ์„ฑ์„ ์ง„์งœ๋กœ ์˜ฌ๋ฆด ์ˆ˜ ์žˆ๋Š”์ง€ ๊ฐ™์ด ์•Œ์•„๋ณด์ž! ๐Ÿ’ช

๐Ÿš€ Android Jetpack ๊ตฌ์„ฑ ์นดํ…Œ๊ณ ๋ฆฌ ๊ตฌ๊ธ€์ด ๊ณต์‹ ์ง€์›ํ•˜๋Š” 4๊ฐ€์ง€ ํ•ต์‹ฌ ์นดํ…Œ๊ณ ๋ฆฌ ๐Ÿ—๏ธ Architecture ViewModel LiveData Room Navigation WorkManager DataStore Paging Hilt (DI) Lifecycle SavedState ์•ฑ ๊ตฌ์กฐ ์„ค๊ณ„์˜ ํ•ต์‹ฌ ๐ŸŽจ UI Compose Fragment RecyclerView ViewPager2 ConstraintLayout CoordinatorLayout DrawerLayout Emoji2 Palette Window ํ™”๋ฉด ๊ตฌ์„ฑ์˜ ๋ชจ๋“  ๊ฒƒ โš™๏ธ Behavior CameraX DownloadManager Media3 Notifications Permissions Sharing Slices Biometric Print AppSearch ์‹œ์Šคํ…œ ๊ธฐ๋Šฅ ํ†ตํ•ฉ ๐Ÿ”ง Foundation AppCompat Android KTX Multidex Test Benchmark Security Startup Tracing Annotation Collection ๊ธฐ๋ฐ˜ ์ธํ”„๋ผ ์ง€์› ์ด 80๊ฐœ ์ด์ƒ์˜ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ยท ๊ตฌ๊ธ€ ๊ณต์‹ ์ง€์› ยท ์ง€์†์  ์—…๋ฐ์ดํŠธ

๐Ÿค” Jetpack์ด ๋ญ”๋ฐ? ์™œ ์จ์•ผ ํ•ด?

Jetpack์„ ํ•œ ๋งˆ๋””๋กœ ์ •์˜ํ•˜๋ฉด "์•ˆ๋“œ๋กœ์ด๋“œ ๊ฐœ๋ฐœ์ž์˜ ๊ณ ํ†ต์„ ์ค„์—ฌ์ฃผ๋Š” ๊ตฌ๊ธ€์˜ ์„ ๋ฌผ ์„ธํŠธ"์•ผ. ๐Ÿ˜„
๊ณต์‹์ ์œผ๋กœ๋Š” "์•ˆ๋“œ๋กœ์ด๋“œ ์•ฑ์„ ๋” ์‰ฝ๊ฒŒ ๊ฐœ๋ฐœํ•  ์ˆ˜ ์žˆ๋„๋ก ๋„์™€์ฃผ๋Š” ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ, ๋„๊ตฌ, ๊ฐ€์ด๋“œ์˜ ๋ชจ์Œ"์ด๋ผ๊ณ  ์ •์˜๋ผ ์žˆ์–ด.

๐Ÿ“Œ Jetpack์˜ ํ•ต์‹ฌ ํŠน์ง• 3๊ฐ€์ง€

1. ํ•˜์œ„ ํ˜ธํ™˜์„ฑ ๋ณด์žฅ โ€” ๊ตฌ๋ฒ„์ „ ์•ˆ๋“œ๋กœ์ด๋“œ์—์„œ๋„ ์ตœ์‹  ๊ธฐ๋Šฅ์„ ์“ธ ์ˆ˜ ์žˆ์–ด. AppCompat์ด ๋Œ€ํ‘œ์ ์ธ ์˜ˆ์•ผ.
2. ๋ณด์ผ๋Ÿฌํ”Œ๋ ˆ์ดํŠธ ์ฝ”๋“œ ๊ฐ์†Œ โ€” ๋ฐ˜๋ณต์ ์œผ๋กœ ์จ์•ผ ํ•˜๋Š” ์ฝ”๋“œ๋ฅผ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๊ฐ€ ๋Œ€์‹  ์ฒ˜๋ฆฌํ•ด์ค˜.
3. ๊ตฌ๊ธ€ ๊ณต์‹ ์ง€์› โ€” ๊ตฌ๊ธ€์ด ์ง์ ‘ ๋งŒ๋“ค๊ณ  ์œ ์ง€๋ณด์ˆ˜ํ•˜๋‹ˆ๊นŒ ์•ˆ์ •์„ฑ์ด ๋ณด์žฅ๋ผ.

Jetpack ์ด์ „์—๋Š” ์–ด๋• ๋ƒ๊ณ ? ๐Ÿคฆ
์˜ˆ๋ฅผ ๋“ค์–ด ํ™”๋ฉด ํšŒ์ „ํ•  ๋•Œ ๋ฐ์ดํ„ฐ ์œ ์ง€ํ•˜๋ ค๋ฉด onSaveInstanceState()์— ์ง์ ‘ ๋ฐ์ดํ„ฐ ์ €์žฅํ•˜๊ณ , onRestoreInstanceState()์—์„œ ๋ณต์›ํ•˜๋Š” ์ฝ”๋“œ๋ฅผ ๋งค๋ฒˆ ์จ์•ผ ํ–ˆ์–ด.
๊ทผ๋ฐ ์ง€๊ธˆ์€? ViewModel ํ•˜๋‚˜๋ฉด ๋์ด์•ผ. ํ™”๋ฉด์ด ํšŒ์ „ํ•ด๋„ ๋ฐ์ดํ„ฐ๊ฐ€ ์•Œ์•„์„œ ์‚ด์•„๋‚จ์•„. ๐ŸŽฏ

Jetpack์€ ํฌ๊ฒŒ 4๊ฐ€์ง€ ์นดํ…Œ๊ณ ๋ฆฌ๋กœ ๋‚˜๋‰˜์–ด:
๐Ÿ—๏ธ Architecture ๐ŸŽจ UI โš™๏ธ Behavior ๐Ÿ”ง Foundation

์˜ค๋Š˜์€ ์ด ์ค‘์—์„œ ๊ฐœ๋ฐœ ์ƒ์‚ฐ์„ฑ์— ๊ฐ€์žฅ ์ง์ ‘์ ์ธ ์˜ํ–ฅ์„ ์ฃผ๋Š” ํ•ต์‹ฌ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋“ค์„ ์ง‘์ค‘์ ์œผ๋กœ ํŒŒํ—ค์ณ๋ณผ ๊ฑฐ์•ผ!


๐Ÿง  ViewModel + LiveData: ์ƒ๋ช…์ฃผ๊ธฐ ์ง€์˜ฅ์—์„œ ํƒˆ์ถœ!

์•ˆ๋“œ๋กœ์ด๋“œ ๊ฐœ๋ฐœ์—์„œ ๊ฐ€์žฅ ์•…๋ช… ๋†’์€ ๊ฒŒ ๋ญ”์ง€ ์•Œ์•„? ๋ฐ”๋กœ ์ƒ๋ช…์ฃผ๊ธฐ(Lifecycle) ๊ด€๋ฆฌ์•ผ. ๐Ÿ˜ฑ
์•กํ‹ฐ๋น„ํ‹ฐ๊ฐ€ ์ƒ์„ฑ๋˜๊ณ , ์ผ์‹œ์ •์ง€๋˜๊ณ , ์žฌ๊ฐœ๋˜๊ณ , ํŒŒ๊ดด๋˜๋Š” ๊ณผ์ •์—์„œ ๋ฐ์ดํ„ฐ๋ฅผ ์–ด๋–ป๊ฒŒ ๊ด€๋ฆฌํ•˜๋А๋ƒ๊ฐ€ ์ง„์งœ ๊ณจ์น˜๊ฑฐ๋ฆฌ์˜€๊ฑฐ๋“ .
ํ™”๋ฉด ํšŒ์ „ ํ•œ ๋ฒˆ์— ์•ฑ์ด ๋ป—์–ด๋ฒ„๋ฆฌ๋Š” ๊ฒฝํ—˜... ๋‹ค๋“ค ํ•œ ๋ฒˆ์ฏค์€ ํ•ด๋ดค์„ ๊ฑฐ์•ผ. ๐Ÿ˜…

๐Ÿ“ฆ ViewModel์ด๋ž€?

ViewModel์€ UI ๊ด€๋ จ ๋ฐ์ดํ„ฐ๋ฅผ ์ €์žฅํ•˜๊ณ  ๊ด€๋ฆฌํ•˜๋Š” ํด๋ž˜์Šค์•ผ.
ํ•ต์‹ฌ์€ ํ™”๋ฉด ๊ตฌ์„ฑ ๋ณ€๊ฒฝ(ํ™”๋ฉด ํšŒ์ „ ๋“ฑ)์—์„œ ์‚ด์•„๋‚จ๋Š”๋‹ค๋Š” ๊ฑฐ์•ผ!
์•กํ‹ฐ๋น„ํ‹ฐ๋‚˜ ํ”„๋ž˜๊ทธ๋จผํŠธ๊ฐ€ ํŒŒ๊ดด๋˜๊ณ  ์žฌ์ƒ์„ฑ๋˜์–ด๋„ ViewModel์€ ๋ฉ”๋ชจ๋ฆฌ์— ๊ทธ๋Œ€๋กœ ์œ ์ง€๋ผ.

Kotlin - ViewModel ๊ธฐ๋ณธ ์‚ฌ์šฉ๋ฒ•
// ViewModel ํด๋ž˜์Šค ์ •์˜
class MainViewModel : ViewModel() {
    private val _count = MutableLiveData<Int>(0)
    val count: LiveData<Int> = _count
    
    fun increment() {
        _count.value = (_count.value ?: 0) + 1
    }
    
    override fun onCleared() {
        super.onCleared()
        // ViewModel์ด ์™„์ „ํžˆ ์†Œ๋ฉธ๋  ๋•Œ ํ˜ธ์ถœ
        // ๋ฆฌ์†Œ์Šค ์ •๋ฆฌ ์ฝ”๋“œ ์—ฌ๊ธฐ์—!
    }
}

// Activity์—์„œ ์‚ฌ์šฉ
class MainActivity : AppCompatActivity() {
    private val viewModel: MainViewModel by viewModels()
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        
        // LiveData ๊ด€์ฐฐ
        viewModel.count.observe(this) { count ->
            binding.tvCount.text = count.toString()
        }
        
        binding.btnIncrement.setOnClickListener {
            viewModel.increment()
        }
    }
}
๐Ÿ’ก ๊ฟ€ํŒ! by viewModels()๋Š” Kotlin ์œ„์ž„ ํ”„๋กœํผํ‹ฐ๋ฅผ ํ™œ์šฉํ•œ ๊ฑฐ์•ผ. ๋‚ด๋ถ€์ ์œผ๋กœ ViewModelProvider๋ฅผ ์‚ฌ์šฉํ•˜๋Š”๋ฐ, ์ด๊ฑธ ์“ฐ๋ฉด ์ฝ”๋“œ๊ฐ€ ํ›จ์”ฌ ๊น”๋”ํ•ด์ ธ. KTX ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๊ฐ€ ์žˆ์–ด์•ผ ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•ด!

๐Ÿ“ก LiveData๋ž€?

LiveData๋Š” ์ƒ๋ช…์ฃผ๊ธฐ๋ฅผ ์ธ์‹ํ•˜๋Š” ๊ด€์ฐฐ ๊ฐ€๋Šฅํ•œ ๋ฐ์ดํ„ฐ ํ™€๋”์•ผ.
์‰ฝ๊ฒŒ ๋งํ•˜๋ฉด, ๋ฐ์ดํ„ฐ๊ฐ€ ๋ฐ”๋€Œ๋ฉด ์ž๋™์œผ๋กœ UI์— ์•Œ๋ ค์ฃผ๋Š” ๋˜‘๋˜‘ํ•œ ๋ฐ์ดํ„ฐ ์ปจํ…Œ์ด๋„ˆ์•ผ.
๊ทธ๋ฆฌ๊ณ  ์ƒ๋ช…์ฃผ๊ธฐ๋ฅผ ์•Œ๊ณ  ์žˆ์–ด์„œ, ์•กํ‹ฐ๋น„ํ‹ฐ๊ฐ€ ๋ฐฑ๊ทธ๋ผ์šด๋“œ์— ์žˆ์„ ๋•Œ๋Š” ์—…๋ฐ์ดํŠธ๋ฅผ ๋ณด๋‚ด์ง€ ์•Š์•„. ๋ฉ”๋ชจ๋ฆฌ ๋ˆ„์ˆ˜ ๊ฑฑ์ • ์—†์Œ! ๐Ÿ™Œ

LiveData vs StateFlow ๋น„๊ต

์š”์ฆ˜์€ LiveData ๋Œ€์‹  StateFlow๋‚˜ SharedFlow๋ฅผ ์“ฐ๋Š” ์ถ”์„ธ์•ผ.
Kotlin Coroutines์™€ ๋” ์ž˜ ์–ด์šธ๋ฆฌ๊ฑฐ๋“ . ํ•˜์ง€๋งŒ LiveData๋„ ์—ฌ์ „ํžˆ ํ˜„์—ญ์ด๊ณ , ํŠนํžˆ Java ํ”„๋กœ์ ํŠธ์—์„œ๋Š” LiveData๊ฐ€ ๋” ํŽธํ•ด.

LiveData โ†’ Java ์นœํ™”์ , ์ƒ๋ช…์ฃผ๊ธฐ ์ž๋™ ๊ด€๋ฆฌ
StateFlow โ†’ Kotlin Coroutines ์นœํ™”์ , ๋” ์œ ์—ฐํ•œ ์ฒ˜๋ฆฌ

ViewModel๊ณผ LiveData๋ฅผ ํ•จ๊ป˜ ์“ฐ๋ฉด MVVM(Model-View-ViewModel) ์•„ํ‚คํ…์ฒ˜๋ฅผ ์ž์—ฐ์Šค๋Ÿฝ๊ฒŒ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ์–ด.
UI ๋กœ์ง๊ณผ ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง์ด ๋ถ„๋ฆฌ๋˜๋‹ˆ๊นŒ ํ…Œ์ŠคํŠธ๋„ ์‰ฌ์›Œ์ง€๊ณ , ์ฝ”๋“œ ์œ ์ง€๋ณด์ˆ˜๋„ ํ›จ์”ฌ ํŽธํ•ด์ ธ. ๐Ÿ‘


๐Ÿ—„๏ธ Room: SQLite๋ฅผ ์ธ๊ฐ„๋‹ต๊ฒŒ ์“ฐ๋Š” ๋ฒ•

SQLite ์ง์ ‘ ์จ๋ณธ ์‚ฌ๋žŒ? ๐Ÿ™‹
SQLiteOpenHelper ์ƒ์†๋ฐ›๊ณ , onCreate()์— CREATE TABLE ์ฟผ๋ฆฌ ์“ฐ๊ณ , Cursor๋กœ ๋ฐ์ดํ„ฐ ์ฝ๊ณ ...
์ƒ๊ฐ๋งŒ ํ•ด๋„ ์†๊ฐ€๋ฝ์ด ์•„ํŒŒ์˜ค์ง€ ์•Š์•„? ๐Ÿ˜ญ
Room์€ ์ด ๋ชจ๋“  ๊ณ ํ†ต์„ ์—†์• ์ฃผ๋Š” SQLite ์œ„์— ๊ตฌ์ถ•๋œ ์ถ”์ƒํ™” ๋ ˆ์ด์–ด์•ผ!

Room์˜ ํ•ต์‹ฌ ๊ตฌ์„ฑ์š”์†Œ๋Š” ๋”ฑ 3๊ฐ€์ง€์•ผ:

1
Entity โ€” ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ํ…Œ์ด๋ธ”์„ ๋‚˜ํƒ€๋‚ด๋Š” ํด๋ž˜์Šค. @Entity ์–ด๋…ธํ…Œ์ด์…˜์œผ๋กœ ์ •์˜ํ•ด.
2
DAO (Data Access Object) โ€” ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์— ์ ‘๊ทผํ•˜๋Š” ๋ฉ”์„œ๋“œ๋ฅผ ์ •์˜ํ•˜๋Š” ์ธํ„ฐํŽ˜์ด์Šค. @Dao ์–ด๋…ธํ…Œ์ด์…˜ ์‚ฌ์šฉ.
3
Database โ€” ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ํ™€๋” ํด๋ž˜์Šค. @Database ์–ด๋…ธํ…Œ์ด์…˜์œผ๋กœ ์ •์˜ํ•˜๊ณ  ์‹ฑ๊ธ€ํ†ค์œผ๋กœ ๊ด€๋ฆฌํ•ด.
Kotlin - Room ๊ธฐ๋ณธ ์„ค์ •
// 1. Entity ์ •์˜
@Entity(tableName = "users")
data class User(
    @PrimaryKey(autoGenerate = true)
    val id: Int = 0,
    @ColumnInfo(name = "user_name")
    val name: String,
    val email: String,
    val createdAt: Long = System.currentTimeMillis()
)

// 2. DAO ์ •์˜
@Dao
interface UserDao {
    @Query("SELECT * FROM users ORDER BY createdAt DESC")
    fun getAllUsers(): Flow<List<User>>  // Flow๋กœ ๋ฐ˜์‘ํ˜• ์ฒ˜๋ฆฌ!
    
    @Query("SELECT * FROM users WHERE id = :userId")
    suspend fun getUserById(userId: Int): User?
    
    @Insert(onConflict = OnConflictStrategy.REPLACE)
    suspend fun insertUser(user: User)
    
    @Update
    suspend fun updateUser(user: User)
    
    @Delete
    suspend fun deleteUser(user: User)
    
    @Query("DELETE FROM users WHERE id = :userId")
    suspend fun deleteUserById(userId: Int)
}

// 3. Database ์ •์˜
@Database(
    entities = [User::class],
    version = 1,
    exportSchema = false
)
abstract class AppDatabase : RoomDatabase() {
    abstract fun userDao(): UserDao
    
    companion object {
        @Volatile
        private var INSTANCE: AppDatabase? = null
        
        fun getDatabase(context: Context): AppDatabase {
            return INSTANCE ?: synchronized(this) {
                val instance = Room.databaseBuilder(
                    context.applicationContext,
                    AppDatabase::class.java,
                    "app_database"
                ).build()
                INSTANCE = instance
                instance
            }
        }
    }
}
โœ… Room์˜ ๊ฐ•์ !

์ปดํŒŒ์ผ ํƒ€์ž„ ๊ฒ€์ฆ โ€” SQL ์ฟผ๋ฆฌ ์˜ค๋ฅ˜๋ฅผ ์ปดํŒŒ์ผ ์‹œ์ ์— ์žก์•„์ค˜. ๋Ÿฐํƒ€์ž„์— ์•ฑ์ด ํ„ฐ์ง€๋Š” ์ƒํ™ฉ์„ ๋ฐฉ์ง€!
LiveData/Flow ์ง€์› โ€” DB ๋ณ€๊ฒฝ์‚ฌํ•ญ์„ ์ž๋™์œผ๋กœ UI์— ๋ฐ˜์˜ํ•  ์ˆ˜ ์žˆ์–ด.
Migration ์ง€์› โ€” DB ์Šคํ‚ค๋งˆ ๋ณ€๊ฒฝ ์‹œ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์ฝ”๋“œ๋ฅผ ์ฒด๊ณ„์ ์œผ๋กœ ๊ด€๋ฆฌ ๊ฐ€๋Šฅ.
ํ…Œ์ŠคํŠธ ์šฉ์ด์„ฑ โ€” ์ธ๋ฉ”๋ชจ๋ฆฌ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค๋กœ ๋‹จ์œ„ ํ…Œ์ŠคํŠธ ๊ฐ€๋Šฅ.

ํŠนํžˆ Flow<List<User>>๋ฅผ ๋ฐ˜ํ™˜ํ•˜๋Š” DAO ๋ฉ”์„œ๋“œ๋Š” ์ง„์งœ ๊ฐ•๋ ฅํ•ด!
๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์— ๋ณ€๊ฒฝ์ด ์ƒ๊ธฐ๋ฉด ์ž๋™์œผ๋กœ ์ƒˆ ๋ฐ์ดํ„ฐ๋ฅผ emitํ•ด์„œ UI๊ฐ€ ์•Œ์•„์„œ ์—…๋ฐ์ดํŠธ๋ผ.
์ด๊ฑธ ViewModel์˜ StateFlow์™€ ์—ฐ๊ฒฐํ•˜๋ฉด ์™„์ „ ์ž๋™ํ™”๋œ ๋ฐ์ดํ„ฐ ํŒŒ์ดํ”„๋ผ์ธ์ด ์™„์„ฑ๋˜๋Š” ๊ฑฐ์•ผ! ๐Ÿš€


๐Ÿ—บ๏ธ Navigation Component: ํ™”๋ฉด ์ „ํ™˜์˜ ์‹ ์„ธ๊ณ„

์˜ˆ์ „์— ํ”„๋ž˜๊ทธ๋จผํŠธ ์ „ํ™˜ ์ฝ”๋“œ ์งœ๋ณธ ์  ์žˆ์–ด? ๐Ÿ˜ค
FragmentManager์— beginTransaction()ํ•˜๊ณ , replace()ํ•˜๊ณ , addToBackStack()ํ•˜๊ณ ...
๋ฐฑ์Šคํƒ ๊ด€๋ฆฌํ•˜๋‹ค๊ฐ€ ๋’ค๋กœ๊ฐ€๊ธฐ ๋ฒ„ํŠผ ๋™์ž‘์ด ์ด์ƒํ•ด์ง€๊ณ , ํ™”๋ฉด ์ „ํ™˜ ์• ๋‹ˆ๋ฉ”์ด์…˜ ์ถ”๊ฐ€ํ•˜๋ ค๋ฉด ๋˜ ์ฝ”๋“œ๊ฐ€ ๋Š˜์–ด๋‚˜๊ณ .
Navigation Component๋Š” ์ด ๋ชจ๋“  ๊ฑธ ์‹œ๊ฐ์ ์ธ ๊ทธ๋ž˜ํ”„๋กœ ๊ด€๋ฆฌํ•˜๊ฒŒ ํ•ด์ค˜! ๐ŸŽจ

Navigation์˜ ํ•ต์‹ฌ ๊ฐœ๋…

๊ฐœ๋… ์„ค๋ช…
NavGraph ๋ชจ๋“  ํ™”๋ฉด๊ณผ ํ™”๋ฉด ๊ฐ„ ์ด๋™ ๊ฒฝ๋กœ๋ฅผ ์ •์˜ํ•˜๋Š” XML ํŒŒ์ผ
NavHost NavGraph์˜ ํ˜„์žฌ ๋ชฉ์ ์ง€๋ฅผ ํ‘œ์‹œํ•˜๋Š” ์ปจํ…Œ์ด๋„ˆ (NavHostFragment)
NavController NavHost ๋‚ด์—์„œ ์•ฑ ํƒ์ƒ‰์„ ๊ด€๋ฆฌํ•˜๋Š” ๊ฐ์ฒด
Destination NavGraph์—์„œ ์ด๋™ํ•  ์ˆ˜ ์žˆ๋Š” ๊ฐ ํ™”๋ฉด (Fragment, Activity, Dialog)
Action Destination ๊ฐ„์˜ ์ด๋™ ๊ฒฝ๋กœ ์ •์˜
Safe Args ํ™”๋ฉด ๊ฐ„ ๋ฐ์ดํ„ฐ ์ „๋‹ฌ์„ ํƒ€์ž… ์•ˆ์ „ํ•˜๊ฒŒ ์ฒ˜๋ฆฌํ•˜๋Š” ํ”Œ๋Ÿฌ๊ทธ์ธ
XML - nav_graph.xml ์˜ˆ์‹œ
<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/nav_graph"
    app:startDestination="@id/homeFragment">

    <fragment
        android:id="@+id/homeFragment"
        android:name="com.example.HomeFragment"
        android:label="ํ™ˆ">
        <action
            android:id="@+id/action_home_to_detail"
            app:destination="@id/detailFragment"
            app:enterAnim="@anim/slide_in_right"
            app:exitAnim="@anim/slide_out_left"/>
    </fragment>

    <fragment
        android:id="@+id/detailFragment"
        android:name="com.example.DetailFragment"
        android:label="์ƒ์„ธ">
        <argument
            android:name="itemId"
            app:argType="integer"/>
    </fragment>
</navigation>
Kotlin - Navigation ์‚ฌ์šฉ๋ฒ•
// ํ™”๋ฉด ์ด๋™ (Safe Args ์‚ฌ์šฉ)
val action = HomeFragmentDirections
    .actionHomeToDetail(itemId = 42)
findNavController().navigate(action)

// ๋’ค๋กœ๊ฐ€๊ธฐ
findNavController().navigateUp()

// BottomNavigationView์™€ ์—ฐ๋™
val navController = findNavController(R.id.nav_host_fragment)
binding.bottomNav.setupWithNavController(navController)
๐Ÿ’ก Safe Args ํ”Œ๋Ÿฌ๊ทธ์ธ์„ ๊ผญ ์จ๋ด!
ํ™”๋ฉด ๊ฐ„ ๋ฐ์ดํ„ฐ ์ „๋‹ฌํ•  ๋•Œ Bundle์— ์ง์ ‘ put/get ํ•˜๋Š” ๋ฐฉ์‹์€ ํƒ€์ž… ์˜ค๋ฅ˜๊ฐ€ ๋Ÿฐํƒ€์ž„์— ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ์–ด.
Safe Args๋Š” NavGraph์˜ argument ์ •์˜๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ํƒ€์ž… ์•ˆ์ „ํ•œ ์ฝ”๋“œ๋ฅผ ์ž๋™ ์ƒ์„ฑํ•ด์ค˜.
build.gradle์— ํ”Œ๋Ÿฌ๊ทธ์ธ ์ถ”๊ฐ€ํ•˜๋ฉด ๋ฐ”๋กœ ์‚ฌ์šฉ ๊ฐ€๋Šฅ!

Navigation Component์˜ ๋˜ ๋‹ค๋ฅธ ๊ฐ•์ ์€ ๋”ฅ๋งํฌ(Deep Link) ์ง€์›์ด์•ผ.
์™ธ๋ถ€์—์„œ ํŠน์ • ํ™”๋ฉด์œผ๋กœ ๋ฐ”๋กœ ์ง„์ž…ํ•˜๋Š” ๊ธฐ๋Šฅ์„ NavGraph์—์„œ ์„ ์–ธ์ ์œผ๋กœ ์ •์˜ํ•  ์ˆ˜ ์žˆ์–ด.
URL ํŒจํ„ด ๋งคํ•‘๋„ ์ง€์›ํ•˜๋‹ˆ๊นŒ ์›น ๋งํฌ์—์„œ ์•ฑ ํŠน์ • ํ™”๋ฉด์œผ๋กœ ์—ฐ๊ฒฐํ•˜๋Š” ๊ฒƒ๋„ ์‰ฝ๊ฒŒ ๊ตฌํ˜„ ๊ฐ€๋Šฅํ•ด! ๐Ÿ”—


โš™๏ธ WorkManager: ๋ฐฑ๊ทธ๋ผ์šด๋“œ ์ž‘์—…์˜ ์ตœ๊ฐ•์ž

๋ฐฑ๊ทธ๋ผ์šด๋“œ ์ž‘์—…... ์•ˆ๋“œ๋กœ์ด๋“œ ๊ฐœ๋ฐœ์—์„œ ์ง„์งœ ์•…๋ชฝ ๊ฐ™์€ ์˜์—ญ์ด์•ผ. ๐Ÿ˜ฐ
AsyncTask๋Š” deprecated๋๊ณ , Service๋Š” ๋ฐฐํ„ฐ๋ฆฌ ์žก์•„๋จน๋Š”๋‹ค๊ณ  ์‹œ์Šคํ…œ์ด ์ฃฝ์—ฌ๋ฒ„๋ฆฌ๊ณ ,
JobScheduler๋Š” API 21 ์ด์ƒ์—์„œ๋งŒ ๋˜๊ณ , AlarmManager๋Š” ๋„์ฆˆ ๋ชจ๋“œ์—์„œ ์•ˆ ๋˜๊ณ ...
์ด ๋ชจ๋“  ํ˜ผ๋ž€์„ WorkManager๊ฐ€ ํ•ด๊ฒฐํ•ด์ค˜! ๐Ÿฆธ

WorkManager๊ฐ€ ๋‚ด๋ถ€์ ์œผ๋กœ ์‚ฌ์šฉํ•˜๋Š” ๊ธฐ์ˆ ๋“ค

API 23+ โ†’ JobScheduler
API 14-22 โ†’ Firebase JobDispatcher ๋˜๋Š” AlarmManager + BroadcastReceiver

๊ฐœ๋ฐœ์ž๋Š” ๊ทธ๋ƒฅ WorkManager API๋งŒ ์“ฐ๋ฉด ๋ผ. ๋‚ด๋ถ€ ๊ตฌํ˜„์€ ์•Œ์•„์„œ ์ตœ์ ํ™”ํ•ด์คŒ! ๐ŸŽฏ

WorkManager๋Š” ๋ณด์žฅ๋œ ์‹คํ–‰(Guaranteed Execution)์ด ํ•ต์‹ฌ์ด์•ผ.
์•ฑ์ด ์ข…๋ฃŒ๋˜๊ฑฐ๋‚˜ ๊ธฐ๊ธฐ๊ฐ€ ์žฌ์‹œ์ž‘๋˜์–ด๋„ ์ž‘์—…์ด ์‹คํ–‰๋˜๋Š” ๊ฑธ ๋ณด์žฅํ•ด์ค˜.
๋‹จ, ์ฆ‰๊ฐ์ ์ธ ์‹คํ–‰์ด ํ•„์š”ํ•œ ์ž‘์—…๋ณด๋‹ค๋Š” ์ง€์—ฐ ๊ฐ€๋Šฅํ•˜๊ณ  ์‹ ๋ขฐ์„ฑ์ด ํ•„์š”ํ•œ ์ž‘์—…์— ์ ํ•ฉํ•ด.

Kotlin - WorkManager ๊ธฐ๋ณธ ์‚ฌ์šฉ๋ฒ•
// Worker ํด๋ž˜์Šค ์ •์˜
class ImageUploadWorker(
    context: Context,
    workerParams: WorkerParameters
) : CoroutineWorker(context, workerParams) {
    
    override suspend fun doWork(): Result {
        val imageUri = inputData.getString("image_uri") 
            ?: return Result.failure()
        
        return try {
            // ์ด๋ฏธ์ง€ ์—…๋กœ๋“œ ๋กœ์ง
            uploadImage(imageUri)
            Result.success()
        } catch (e: Exception) {
            if (runAttemptCount < 3) {
                Result.retry()  // ์ตœ๋Œ€ 3๋ฒˆ ์žฌ์‹œ๋„
            } else {
                Result.failure()
            }
        }
    }
}

// WorkRequest ์ƒ์„ฑ ๋ฐ ์‹คํ–‰
val uploadWorkRequest = OneTimeWorkRequestBuilder<ImageUploadWorker>()
    .setInputData(workDataOf("image_uri" to imageUri))
    .setConstraints(
        Constraints.Builder()
            .setRequiredNetworkType(NetworkType.CONNECTED)  // ๋„คํŠธ์›Œํฌ ์—ฐ๊ฒฐ ์‹œ์—๋งŒ
            .setRequiresBatteryNotLow(true)  // ๋ฐฐํ„ฐ๋ฆฌ ๋ถ€์กฑ ์‹œ ์‹คํ–‰ ์•ˆ ํ•จ
            .build()
    )
    .setBackoffCriteria(
        BackoffPolicy.EXPONENTIAL,
        WorkRequest.MIN_BACKOFF_MILLIS,
        TimeUnit.MILLISECONDS
    )
    .build()

WorkManager.getInstance(context).enqueue(uploadWorkRequest)

// ์ž‘์—… ์ƒํƒœ ๊ด€์ฐฐ
WorkManager.getInstance(context)
    .getWorkInfoByIdLiveData(uploadWorkRequest.id)
    .observe(this) { workInfo ->
        when (workInfo.state) {
            WorkInfo.State.SUCCEEDED -> showSuccess()
            WorkInfo.State.FAILED -> showError()
            WorkInfo.State.RUNNING -> showProgress()
            else -> {}
        }
    }

WorkManager์˜ ๊ฐ•๋ ฅํ•œ ๊ธฐ๋Šฅ๋“ค

1
์ฒด์ด๋‹(Chaining) โ€” ์—ฌ๋Ÿฌ ์ž‘์—…์„ ์ˆœ์„œ๋Œ€๋กœ ๋˜๋Š” ๋ณ‘๋ ฌ๋กœ ์‹คํ–‰ํ•  ์ˆ˜ ์žˆ์–ด. then()์œผ๋กœ ์ˆœ์ฐจ ์‹คํ–‰, combine()์œผ๋กœ ๋ณ‘๋ ฌ ์‹คํ–‰ ํ›„ ํ•ฉ์น˜๊ธฐ!
2
์ฃผ๊ธฐ์  ์ž‘์—… โ€” PeriodicWorkRequest๋กœ 15๋ถ„ ์ด์ƒ ๊ฐ„๊ฒฉ์˜ ๋ฐ˜๋ณต ์ž‘์—… ์„ค์ • ๊ฐ€๋Šฅ. ๋ฐ์ดํ„ฐ ๋™๊ธฐํ™”์— ๋”ฑ!
3
๊ณ ์œ  ์ž‘์—… โ€” enqueueUniqueWork()๋กœ ๊ฐ™์€ ์ž‘์—…์ด ์ค‘๋ณต ์‹คํ–‰๋˜๋Š” ๊ฑธ ๋ฐฉ์ง€ํ•  ์ˆ˜ ์žˆ์–ด.
4
์ง„ํ–‰ ์ƒํ™ฉ ๋ณด๊ณ  โ€” setProgress()๋กœ ์ž‘์—… ์ง„ํ–‰๋ฅ ์„ ์‹ค์‹œ๊ฐ„์œผ๋กœ UI์— ์ „๋‹ฌ ๊ฐ€๋Šฅ!
Kotlin - ์ž‘์—… ์ฒด์ด๋‹ ์˜ˆ์‹œ
// ์ด๋ฏธ์ง€ ์ฒ˜๋ฆฌ ํŒŒ์ดํ”„๋ผ์ธ: ์••์ถ• โ†’ ํ•„ํ„ฐ ์ ์šฉ โ†’ ์—…๋กœ๋“œ
WorkManager.getInstance(context)
    .beginWith(compressWorkRequest)
    .then(filterWorkRequest)
    .then(uploadWorkRequest)
    .enqueue()

๐Ÿ’‰ Hilt: ์˜์กด์„ฑ ์ฃผ์ž…์„ ์‰ฝ๊ฒŒ!

์˜์กด์„ฑ ์ฃผ์ž…(Dependency Injection, DI)... ์ฒ˜์Œ ๋“ค์œผ๋ฉด ๋ญ”๊ฐ€ ์–ด๋ ค์›Œ ๋ณด์ด์ง€? ๐Ÿ˜…
๊ทผ๋ฐ ๊ฐœ๋…์€ ๊ฐ„๋‹จํ•ด. "ํ•„์š”ํ•œ ๊ฐ์ฒด๋ฅผ ์ง์ ‘ ๋งŒ๋“ค์ง€ ๋ง๊ณ , ์™ธ๋ถ€์—์„œ ๋ฐ›์•„๋ผ"๋Š” ๊ฑฐ์•ผ.
์˜ˆ๋ฅผ ๋“ค์–ด Repository๊ฐ€ ํ•„์š”ํ•œ ViewModel์ด ์žˆ์„ ๋•Œ, ViewModel ์•ˆ์—์„œ Repository๋ฅผ ์ง์ ‘ ์ƒ์„ฑํ•˜๋Š” ๊ฒŒ ์•„๋‹ˆ๋ผ ์™ธ๋ถ€์—์„œ ์ฃผ์ž…๋ฐ›๋Š” ๋ฐฉ์‹์ด์•ผ.

Hilt๋Š” Dagger2 ๊ธฐ๋ฐ˜์˜ ์•ˆ๋“œ๋กœ์ด๋“œ ์ „์šฉ DI ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์•ผ.
Dagger2๊ฐ€ ๋„ˆ๋ฌด ๋ณต์žกํ•˜๋‹ค๋Š” ๋ถˆ๋งŒ์„ ๋ฐ˜์˜ํ•ด์„œ ๊ตฌ๊ธ€์ด ์•ˆ๋“œ๋กœ์ด๋“œ์— ์ตœ์ ํ™”๋œ ๋ฒ„์ „์œผ๋กœ ๋งŒ๋“  ๊ฑฐ์•ผ.
์–ด๋…ธํ…Œ์ด์…˜ ๋ช‡ ๊ฐœ๋งŒ ์ถ”๊ฐ€ํ•˜๋ฉด DI ์„ค์ •์ด ๋๋‚˜! ๐ŸŽ‰

Kotlin - Hilt ๊ธฐ๋ณธ ์„ค์ •
// Application ํด๋ž˜์Šค์— @HiltAndroidApp ์ถ”๊ฐ€
@HiltAndroidApp
class MyApplication : Application()

// Activity์— @AndroidEntryPoint ์ถ”๊ฐ€
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
    // ViewModel์— ์ž๋™์œผ๋กœ ์˜์กด์„ฑ ์ฃผ์ž…๋จ
    private val viewModel: MainViewModel by viewModels()
}

// ViewModel์— @HiltViewModel ์ถ”๊ฐ€
@HiltViewModel
class MainViewModel @Inject constructor(
    private val userRepository: UserRepository
) : ViewModel() {
    // userRepository๊ฐ€ ์ž๋™์œผ๋กœ ์ฃผ์ž…๋จ!
}

// Repository ์ •์˜
class UserRepository @Inject constructor(
    private val userDao: UserDao,
    private val apiService: ApiService
) {
    fun getUsers() = userDao.getAllUsers()
}

// Module๋กœ ์ธํ„ฐํŽ˜์ด์Šค ๋ฐ”์ธ๋”ฉ
@Module
@InstallIn(SingletonComponent::class)
abstract class RepositoryModule {
    @Binds
    @Singleton
    abstract fun bindUserRepository(
        impl: UserRepositoryImpl
    ): UserRepository
}

// ์™ธ๋ถ€ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ๊ฐ์ฒด ์ œ๊ณต
@Module
@InstallIn(SingletonComponent::class)
object NetworkModule {
    @Provides
    @Singleton
    fun provideRetrofit(): Retrofit {
        return Retrofit.Builder()
            .baseUrl("https://api.example.com/")
            .addConverterFactory(GsonConverterFactory.create())
            .build()
    }
}
โœ… Hilt ์‚ฌ์šฉ์˜ ์ด์ 

๋ณด์ผ๋Ÿฌํ”Œ๋ ˆ์ดํŠธ ๊ฐ์†Œ โ€” Dagger2 ๋Œ€๋น„ ์ฝ”๋“œ๋Ÿ‰์ด ๋Œ€ํญ ์ค„์–ด๋“ค์–ด.
ํ…Œ์ŠคํŠธ ์šฉ์ด์„ฑ โ€” ์˜์กด์„ฑ์„ ์‰ฝ๊ฒŒ ๊ต์ฒดํ•  ์ˆ˜ ์žˆ์–ด์„œ ๋‹จ์œ„ ํ…Œ์ŠคํŠธ๊ฐ€ ํŽธํ•ด์ ธ.
์ปดํŒŒ์ผ ํƒ€์ž„ ๊ฒ€์ฆ โ€” ์˜์กด์„ฑ ๊ทธ๋ž˜ํ”„ ์˜ค๋ฅ˜๋ฅผ ์ปดํŒŒ์ผ ์‹œ์ ์— ๋ฐœ๊ฒฌ ๊ฐ€๋Šฅ.
ํ‘œ์ค€ํ™” โ€” ๊ตฌ๊ธ€ ๊ณต์‹ ๊ถŒ์žฅ DI ์†”๋ฃจ์…˜์ด๋ผ ํŒ€ ๋‚ด ์ฝ”๋“œ ์ผ๊ด€์„ฑ ์œ ์ง€์— ์ข‹์•„.

Hilt๋ฅผ ์“ฐ๋ฉด ์ฝ”๋“œ ๊ตฌ์กฐ๊ฐ€ ํ›จ์”ฌ ๊น”๋”ํ•ด์ง€๊ณ , ํŠนํžˆ ํ…Œ์ŠคํŠธ ์ฝ”๋“œ ์ž‘์„ฑ์ด ์—„์ฒญ ํŽธํ•ด์ ธ.
ํ…Œ์ŠคํŠธํ•  ๋•Œ ์‹ค์ œ Repository ๋Œ€์‹  Mock Repository๋ฅผ ์ฃผ์ž…ํ•˜๋ฉด ๋˜๋‹ˆ๊นŒ, ๋„คํŠธ์›Œํฌ ์—†์ด๋„ ViewModel ํ…Œ์ŠคํŠธ๊ฐ€ ๊ฐ€๋Šฅํ•ด! ๐Ÿงช


๐Ÿ—๏ธ MVVM + Jetpack ์•„ํ‚คํ…์ฒ˜ ๊ตฌ์กฐ ๊ฐ ๋ ˆ์ด์–ด์™€ Jetpack ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์˜ ์—ญํ•  ๐ŸŽจ View Layer Activity / Fragment Jetpack Compose Navigation Component Data Binding UI ํ‘œ์‹œ ยท ์‚ฌ์šฉ์ž ์ž…๋ ฅ ์ฒ˜๋ฆฌ ๐Ÿง  ViewModel Layer ViewModel LiveData / StateFlow Kotlin Coroutines Hilt (DI) ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง ยท ์ƒํƒœ ๊ด€๋ฆฌ ๐Ÿ“ฆ Repository Layer Repository Pattern ๋ฐ์ดํ„ฐ ์†Œ์Šค ์ถ”์ƒํ™” ์บ์‹ฑ ์ „๋žต ๊ด€๋ฆฌ ๋‹จ์ผ ์ง„์‹ค ๊ณต๊ธ‰์›(SSOT) ๐Ÿ—„๏ธ Room DB Entity / DAO SQLite ์ถ”์ƒํ™” ๋กœ์ปฌ ๋ฐ์ดํ„ฐ ์ €์žฅ ๐ŸŒ Network Retrofit / OkHttp REST API ํ†ต์‹  ์›๊ฒฉ ๋ฐ์ดํ„ฐ ์†Œ์Šค ๊ด€์ฐฐ ํ˜ธ์ถœ โš™๏ธ WorkManager ๋ฐฑ๊ทธ๋ผ์šด๋“œ ์ž‘์—… ๋ณด์žฅ๋œ ์‹คํ–‰ ์ œ์•ฝ ์กฐ๊ฑด ์„ค์ • ์•ฑ ์ข…๋ฃŒ ํ›„์—๋„ ์‹คํ–‰ ๋ณด์žฅ ๊ฐ ๋ ˆ์ด์–ด๋Š” ๋‹จ๋ฐฉํ–ฅ ๋ฐ์ดํ„ฐ ํ๋ฆ„(UDF)์œผ๋กœ ์—ฐ๊ฒฐ ยท ๊ด€์‹ฌ์‚ฌ ๋ถ„๋ฆฌ(SoC) ์›์น™ ์ ์šฉ

๐ŸŽจ Jetpack Compose: UI ๊ฐœ๋ฐœ์˜ ํ˜๋ช…

์ž, ์ด์ œ ์ง„์งœ ํ•ซํ•œ ๋…€์„์„ ์†Œ๊ฐœํ•  ์‹œ๊ฐ„์ด์•ผ! ๐Ÿ”ฅ
Jetpack Compose๋Š” 2021๋…„์— ์•ˆ์ • ๋ฒ„์ „์ด ์ถœ์‹œ๋œ ์•ˆ๋“œ๋กœ์ด๋“œ์˜ ์ƒˆ๋กœ์šด UI ํˆดํ‚ท์ด์•ผ.
๊ธฐ์กด์˜ XML ๊ธฐ๋ฐ˜ UI ๊ฐœ๋ฐœ ๋ฐฉ์‹์„ ์™„์ „ํžˆ ๋’ค์ง‘์–ด๋ฒ„๋ฆฐ ํ˜๋ช…์ ์ธ ๋ณ€ํ™”์•ผ!

๊ธฐ์กด ๋ฐฉ์‹์€ XML๋กœ ๋ ˆ์ด์•„์›ƒ ์ •์˜ํ•˜๊ณ , ์ฝ”๋“œ์—์„œ findViewById()๋‚˜ ViewBinding์œผ๋กœ ๋ทฐ๋ฅผ ์ฐพ์•„์„œ ์กฐ์ž‘ํ•˜๋Š” ๋ฐฉ์‹์ด์—ˆ์ž–์•„.
Compose๋Š” ์„ ์–ธํ˜•(Declarative) UI ๋ฐฉ์‹์ด์•ผ. "์ด ์ƒํƒœ์ผ ๋•Œ UI๊ฐ€ ์ด๋ ‡๊ฒŒ ๋ณด์—ฌ์•ผ ํ•ด"๋ผ๊ณ  ์„ ์–ธํ•˜๋ฉด, ์ƒํƒœ๊ฐ€ ๋ฐ”๋€” ๋•Œ UI๊ฐ€ ์•Œ์•„์„œ ์—…๋ฐ์ดํŠธ๋ผ!

๐Ÿ“Š ๋ช…๋ นํ˜• vs ์„ ์–ธํ˜• UI ๋น„๊ต

๋ช…๋ นํ˜• (๊ธฐ์กด XML ๋ฐฉ์‹)
"๋ฒ„ํŠผ ์ฐพ์•„์„œ โ†’ ํ…์ŠคํŠธ ๋ณ€๊ฒฝํ•ด โ†’ ์ƒ‰์ƒ ๋ฐ”๊ฟ” โ†’ ๋ณด์ด๊ฒŒ ํ•ด"
โ†’ ์ƒํƒœ ๋ณ€ํ™”๋งˆ๋‹ค ์ง์ ‘ ๋ทฐ๋ฅผ ์กฐ์ž‘ํ•ด์•ผ ํ•จ

์„ ์–ธํ˜• (Compose ๋ฐฉ์‹)
"isLoading์ด true๋ฉด ๋กœ๋”ฉ ์Šคํ”ผ๋„ˆ ๋ณด์—ฌ์ค˜, false๋ฉด ์ฝ˜ํ…์ธ  ๋ณด์—ฌ์ค˜"
โ†’ ์ƒํƒœ๋งŒ ๋ฐ”๊พธ๋ฉด UI๊ฐ€ ์•Œ์•„์„œ ์žฌ๊ตฌ์„ฑ๋จ
Kotlin - Compose ๊ธฐ๋ณธ ์˜ˆ์‹œ
// Composable ํ•จ์ˆ˜ ์ •์˜
@Composable
fun UserCard(
    user: User,
    onFollowClick: (User) -> Unit
) {
    Card(
        modifier = Modifier
            .fillMaxWidth()
            .padding(horizontal = 16.dp, vertical = 8.dp),
        elevation = CardDefaults.cardElevation(4.dp),
        shape = RoundedCornerShape(12.dp)
    ) {
        Row(
            modifier = Modifier.padding(16.dp),
            verticalAlignment = Alignment.CenterVertically
        ) {
            // ํ”„๋กœํ•„ ์ด๋ฏธ์ง€
            AsyncImage(
                model = user.profileImageUrl,
                contentDescription = "ํ”„๋กœํ•„ ์ด๋ฏธ์ง€",
                modifier = Modifier
                    .size(56.dp)
                    .clip(CircleShape)
            )
            
            Spacer(modifier = Modifier.width(12.dp))
            
            // ์‚ฌ์šฉ์ž ์ •๋ณด
            Column(modifier = Modifier.weight(1f)) {
                Text(
                    text = user.name,
                    style = MaterialTheme.typography.titleMedium,
                    fontWeight = FontWeight.Bold
                )
                Text(
                    text = user.bio,
                    style = MaterialTheme.typography.bodySmall,
                    color = MaterialTheme.colorScheme.onSurfaceVariant,
                    maxLines = 2,
                    overflow = TextOverflow.Ellipsis
                )
            }
            
            // ํŒ”๋กœ์šฐ ๋ฒ„ํŠผ
            Button(
                onClick = { onFollowClick(user) },
                shape = RoundedCornerShape(20.dp)
            ) {
                Text("ํŒ”๋กœ์šฐ")
            }
        }
    }
}

// ์ƒํƒœ ๊ด€๋ฆฌ
@Composable
fun UserListScreen(viewModel: UserViewModel = hiltViewModel()) {
    val uiState by viewModel.uiState.collectAsStateWithLifecycle()
    
    when (uiState) {
        is UiState.Loading -> CircularProgressIndicator()
        is UiState.Success -> LazyColumn {
            items(uiState.users) { user ->
                UserCard(
                    user = user,
                    onFollowClick = viewModel::followUser
                )
            }
        }
        is UiState.Error -> ErrorScreen(message = uiState.message)
    }
}
๐Ÿ’ก Compose์˜ ํ•ต์‹ฌ ๊ฐœ๋… - Recomposition

Compose๋Š” ์ƒํƒœ๊ฐ€ ๋ณ€๊ฒฝ๋  ๋•Œ ํ•ด๋‹น Composable ํ•จ์ˆ˜๋ฅผ ๋‹ค์‹œ ์‹คํ–‰ํ•ด(์ด๊ฑธ Recomposition์ด๋ผ๊ณ  ํ•ด).
์ด๋•Œ ๋ณ€๊ฒฝ๋œ ๋ถ€๋ถ„๋งŒ ํšจ์œจ์ ์œผ๋กœ ์—…๋ฐ์ดํŠธํ•˜๋Š” ๊ฒŒ ํ•ต์‹ฌ์ด์•ผ.
remember์™€ rememberSaveable๋กœ ์ƒํƒœ๋ฅผ ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ๊ด€๋ฆฌํ•˜๋Š” ๊ฒŒ ์ค‘์š”ํ•ด!

Compose์˜ ์ฃผ์š” ์žฅ์ 

1
์ฝ”๋“œ ๊ฐ์†Œ โ€” XML + Kotlin ์ฝ”๋“œ ๋‘ ํŒŒ์ผ ๊ด€๋ฆฌ ๋Œ€์‹  Kotlin ์ฝ”๋“œ ํ•˜๋‚˜๋กœ UI ์™„์„ฑ. ์ผ๋ฐ˜์ ์œผ๋กœ ์ฝ”๋“œ๋Ÿ‰์ด 30-40% ๊ฐ์†Œํ•ด.
2
Preview ๊ธฐ๋Šฅ โ€” @Preview ์–ด๋…ธํ…Œ์ด์…˜์œผ๋กœ Android Studio์—์„œ ์‹ค์‹œ๊ฐ„ UI ๋ฏธ๋ฆฌ๋ณด๊ธฐ ๊ฐ€๋Šฅ. ์—๋ฎฌ๋ ˆ์ดํ„ฐ ์—†์ด๋„ ํ™•์ธ OK!
3
์žฌ์‚ฌ์šฉ์„ฑ โ€” Composable ํ•จ์ˆ˜๋Š” ์ผ๋ฐ˜ ํ•จ์ˆ˜์ฒ˜๋Ÿผ ์žฌ์‚ฌ์šฉ ๊ฐ€๋Šฅ. ์ปดํฌ๋„ŒํŠธ ๊ธฐ๋ฐ˜ ๊ฐœ๋ฐœ์ด ์ž์—ฐ์Šค๋Ÿฝ๊ฒŒ ๋จ.
4
์• ๋‹ˆ๋ฉ”์ด์…˜ โ€” animate*AsState, AnimatedVisibility ๋“ฑ์œผ๋กœ ๋ณต์žกํ•œ ์• ๋‹ˆ๋ฉ”์ด์…˜์„ ์‰ฝ๊ฒŒ ๊ตฌํ˜„ ๊ฐ€๋Šฅ.
5
Material3 ํ†ตํ•ฉ โ€” Material Design 3 ์ปดํฌ๋„ŒํŠธ๋“ค์ด ๊ธฐ๋ณธ ์ œ๊ณต๋ผ์„œ ์ผ๊ด€๋œ ๋””์ž์ธ ์‹œ์Šคํ…œ ๊ตฌ์ถ•์ด ์‰ฌ์›Œ.
โš ๏ธ Compose ๋„์ž… ์‹œ ์ฃผ์˜์‚ฌํ•ญ

๊ธฐ์กด XML ๊ธฐ๋ฐ˜ ํ”„๋กœ์ ํŠธ์— Compose๋ฅผ ์ ์ง„์ ์œผ๋กœ ๋„์ž…ํ•  ์ˆ˜ ์žˆ์–ด. ComposeView๋‚˜ AndroidView๋กœ ์ƒํ˜ธ ์šด์šฉ ๊ฐ€๋Šฅ.
ํ•˜์ง€๋งŒ ํŒ€ ์ „์ฒด๊ฐ€ Compose์— ์ต์ˆ™ํ•ด์ง€๋Š” ๋ฐ ์‹œ๊ฐ„์ด ํ•„์š”ํ•ด. ๋Ÿฌ๋‹ ์ปค๋ธŒ๊ฐ€ ์žˆ์œผ๋‹ˆ ์ถฉ๋ถ„ํ•œ ํ•™์Šต ์‹œ๊ฐ„์„ ํ™•๋ณดํ•˜๋Š” ๊ฒŒ ์ข‹์•„!

๐Ÿ’พ DataStore: SharedPreferences์˜ ์ง„ํ™”ํ˜•

SharedPreferences ์จ๋ณธ ์‚ฌ๋žŒ? ๐Ÿ™‹
๊ฐ„๋‹จํ•œ ์„ค์ •๊ฐ’ ์ €์žฅํ•  ๋•Œ ํŽธํ•˜๊ธด ํ•œ๋ฐ... ๋ฉ”์ธ ์Šค๋ ˆ๋“œ์—์„œ I/O ์ž‘์—…์ด ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ๊ณ , ํƒ€์ž… ์•ˆ์ „์„ฑ๋„ ์—†๊ณ , ํŠธ๋žœ์žญ์…˜ ์ง€์›๋„ ์•ˆ ๋˜๊ณ .
DataStore๋Š” ์ด ๋ชจ๋“  ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•œ SharedPreferences์˜ ํ˜„๋Œ€์  ๋Œ€์•ˆ์ด์•ผ! ๐Ÿš€

DataStore๋Š” ๋‘ ๊ฐ€์ง€ ๊ตฌํ˜„์ฒด๊ฐ€ ์žˆ์–ด:

๊ตฌ๋ถ„ Preferences DataStore Proto DataStore
๋ฐ์ดํ„ฐ ํ˜•์‹ ํ‚ค-๊ฐ’ ์Œ Protocol Buffers (ํƒ€์ž… ์ •์˜)
ํƒ€์ž… ์•ˆ์ „์„ฑ ์ œํ•œ์  ์™„์ „ํ•œ ํƒ€์ž… ์•ˆ์ „์„ฑ
์Šคํ‚ค๋งˆ ์ •์˜ ๋ถˆํ•„์š” .proto ํŒŒ์ผ ํ•„์š”
์‚ฌ์šฉ ๋‚œ์ด๋„ ์‰ฌ์›€ ์ค‘๊ฐ„
๊ถŒ์žฅ ์šฉ๋„ ๊ฐ„๋‹จํ•œ ์„ค์ •๊ฐ’ ๋ณต์žกํ•œ ๊ตฌ์กฐํ™” ๋ฐ์ดํ„ฐ
Kotlin - Preferences DataStore ์‚ฌ์šฉ๋ฒ•
// DataStore ์ƒ์„ฑ (Application ๋ ˆ๋ฒจ์—์„œ ์‹ฑ๊ธ€ํ†ค์œผ๋กœ)
val Context.dataStore: DataStore<Preferences> by preferencesDataStore(
    name = "user_settings"
)

// ํ‚ค ์ •์˜
object PreferencesKeys {
    val DARK_MODE = booleanPreferencesKey("dark_mode")
    val USER_NAME = stringPreferencesKey("user_name")
    val FONT_SIZE = intPreferencesKey("font_size")
}

// Repository์—์„œ DataStore ์‚ฌ์šฉ
class SettingsRepository(private val dataStore: DataStore<Preferences>) {
    
    // ๋ฐ์ดํ„ฐ ์ฝ๊ธฐ (Flow๋กœ ๋ฐ˜ํ™˜ - ๋ณ€๊ฒฝ ์‹œ ์ž๋™ emit)
    val darkModeFlow: Flow<Boolean> = dataStore.data
        .catch { exception ->
            if (exception is IOException) {
                emit(emptyPreferences())
            } else {
                throw exception
            }
        }
        .map { preferences ->
            preferences[PreferencesKeys.DARK_MODE] ?: false
        }
    
    // ๋ฐ์ดํ„ฐ ์“ฐ๊ธฐ (suspend ํ•จ์ˆ˜ - ์ฝ”๋ฃจํ‹ด์—์„œ ํ˜ธ์ถœ)
    suspend fun setDarkMode(enabled: Boolean) {
        dataStore.edit { preferences ->
            preferences[PreferencesKeys.DARK_MODE] = enabled
        }
    }
    
    suspend fun setUserName(name: String) {
        dataStore.edit { preferences ->
            preferences[PreferencesKeys.USER_NAME] = name
        }
    }
}
โœ… DataStore vs SharedPreferences ํ•ต์‹ฌ ์ฐจ์ด

๋น„๋™๊ธฐ ์ฒ˜๋ฆฌ โ€” DataStore๋Š” Kotlin Coroutines์™€ Flow๋ฅผ ์‚ฌ์šฉํ•ด์„œ ์™„์ „ ๋น„๋™๊ธฐ๋กœ ๋™์ž‘ํ•ด. ๋ฉ”์ธ ์Šค๋ ˆ๋“œ ๋ธ”๋กœํ‚น ์—†์Œ!
์˜ค๋ฅ˜ ์ฒ˜๋ฆฌ โ€” ๋ฐ์ดํ„ฐ ์ฝ๊ธฐ/์“ฐ๊ธฐ ์‹คํŒจ๋ฅผ Flow์˜ catch ์—ฐ์‚ฐ์ž๋กœ ์•ˆ์ „ํ•˜๊ฒŒ ์ฒ˜๋ฆฌ ๊ฐ€๋Šฅ.
ํŠธ๋žœ์žญ์…˜ โ€” edit ๋ธ”๋ก ๋‚ด์˜ ๋ณ€๊ฒฝ์‚ฌํ•ญ์€ ์›์ž์ ์œผ๋กœ ์ฒ˜๋ฆฌ๋ผ. ๋ถ€๋ถ„ ์—…๋ฐ์ดํŠธ๋กœ ์ธํ•œ ๋ฐ์ดํ„ฐ ๋ถˆ์ผ์น˜ ์—†์Œ!

๐Ÿ“„ Paging 3: ๋ฌดํ•œ ์Šคํฌ๋กค์„ ์šฐ์•„ํ•˜๊ฒŒ

SNS ํ”ผ๋“œ๋‚˜ ์ƒํ’ˆ ๋ชฉ๋ก์ฒ˜๋Ÿผ ๋ฐ์ดํ„ฐ๊ฐ€ ๋งŽ์„ ๋•Œ ์–ด๋–ป๊ฒŒ ์ฒ˜๋ฆฌํ•ด? ๐Ÿค”
ํ•œ ๋ฒˆ์— ๋‹ค ๋ถˆ๋Ÿฌ์˜ค๋ฉด ๋ฉ”๋ชจ๋ฆฌ ํ„ฐ์ง€๊ณ , ๋กœ๋”ฉ ์‹œ๊ฐ„๋„ ์—„์ฒญ ๊ฑธ๋ฆฌ์ž–์•„.
๊ทธ๋ž˜์„œ ํŽ˜์ด์ง€๋„ค์ด์…˜(Pagination)์ด ํ•„์š”ํ•œ๋ฐ, ์ด๊ฑธ ์ง์ ‘ ๊ตฌํ˜„ํ•˜๋ฉด ์ƒ๊ฐ๋ณด๋‹ค ๋ณต์žกํ•ด.
Paging 3๋Š” ์ด ๋ณต์žกํ•œ ํŽ˜์ด์ง€๋„ค์ด์…˜ ๋กœ์ง์„ ๊น”๋”ํ•˜๊ฒŒ ์ฒ˜๋ฆฌํ•ด์ฃผ๋Š” ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์•ผ!

Kotlin - Paging 3 ๊ธฐ๋ณธ ๊ตฌํ˜„
// PagingSource ์ •์˜
class UserPagingSource(
    private val apiService: ApiService
) : PagingSource<Int, User>() {
    
    override suspend fun load(params: LoadParams<Int>): LoadResult<Int, User> {
        val page = params.key ?: 1
        
        return try {
            val response = apiService.getUsers(
                page = page,
                pageSize = params.loadSize
            )
            
            LoadResult.Page(
                data = response.users,
                prevKey = if (page == 1) null else page - 1,
                nextKey = if (response.users.isEmpty()) null else page + 1
            )
        } catch (e: Exception) {
            LoadResult.Error(e)
        }
    }
    
    override fun getRefreshKey(state: PagingState<Int, User>): Int? {
        return state.anchorPosition?.let { anchorPosition ->
            state.closestPageToPosition(anchorPosition)?.prevKey?.plus(1)
                ?: state.closestPageToPosition(anchorPosition)?.nextKey?.minus(1)
        }
    }
}

// Repository์—์„œ Pager ์ƒ์„ฑ
class UserRepository(private val apiService: ApiService) {
    fun getUsersPagingData(): Flow<PagingData<User>> {
        return Pager(
            config = PagingConfig(
                pageSize = 20,
                enablePlaceholders = false,
                prefetchDistance = 5  // 5๊ฐœ ๋‚จ์œผ๋ฉด ๋ฏธ๋ฆฌ ๋กœ๋“œ
            ),
            pagingSourceFactory = { UserPagingSource(apiService) }
        ).flow
    }
}

// ViewModel
@HiltViewModel
class UserViewModel @Inject constructor(
    private val repository: UserRepository
) : ViewModel() {
    val users = repository.getUsersPagingData()
        .cachedIn(viewModelScope)  // ํ™”๋ฉด ํšŒ์ „ ์‹œ ์บ์‹œ ์œ ์ง€!
}

// Fragment์—์„œ PagingDataAdapter ์‚ฌ์šฉ
class UserAdapter : PagingDataAdapter<User, UserViewHolder>(USER_COMPARATOR) {
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): UserViewHolder {
        // ViewHolder ์ƒ์„ฑ
    }
    
    override fun onBindViewHolder(holder: UserViewHolder, position: Int) {
        getItem(position)?.let { holder.bind(it) }
    }
    
    companion object {
        val USER_COMPARATOR = object : DiffUtil.ItemCallback<User>() {
            override fun areItemsTheSame(oldItem: User, newItem: User) = 
                oldItem.id == newItem.id
            override fun areContentsTheSame(oldItem: User, newItem: User) = 
                oldItem == newItem
        }
    }
}
๐Ÿ’ก Paging 3์˜ ์ˆจ๊ฒจ์ง„ ๊ฐ•์ !

LoadStateAdapter๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ๋กœ๋”ฉ ์ค‘, ์˜ค๋ฅ˜ ์ƒํƒœ๋ฅผ ๋ฆฌ์ŠคํŠธ ์ƒ๋‹จ/ํ•˜๋‹จ์— ์ž๋™์œผ๋กœ ํ‘œ์‹œํ•  ์ˆ˜ ์žˆ์–ด.
RemoteMediator๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ๋„คํŠธ์›Œํฌ ๋ฐ์ดํ„ฐ๋ฅผ Room์— ์บ์‹ฑํ•˜๋ฉด์„œ ์˜คํ”„๋ผ์ธ ์ง€์›๋„ ๊ตฌํ˜„ ๊ฐ€๋Šฅ!
Compose์—์„œ๋Š” collectAsLazyPagingItems()๋กœ ๋” ๊ฐ„ํŽธํ•˜๊ฒŒ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์–ด.

๐Ÿ“ธ CameraX: ์นด๋ฉ”๋ผ ๊ฐœ๋ฐœ์˜ ๊ตฌ์›์ž

์•ˆ๋“œ๋กœ์ด๋“œ ์นด๋ฉ”๋ผ API... ์ง„์งœ ์•…๋ช… ๋†’์ง€. ๐Ÿ˜ค
Camera1 API๋Š” ๋„ˆ๋ฌด ๊ตฌ์‹์ด๊ณ , Camera2 API๋Š” ๋„ˆ๋ฌด ๋ณต์žกํ•˜๊ณ , ๊ธฐ๊ธฐ๋งˆ๋‹ค ๋™์ž‘์ด ๋‹ฌ๋ผ์„œ ํ…Œ์ŠคํŠธํ•˜๋‹ค๊ฐ€ ๋จธ๋ฆฌ ๋น ์ง€๊ณ ...
CameraX๋Š” ์ด ๋ชจ๋“  ๋ณต์žกํ•จ์„ ์ถ”์ƒํ™”ํ•ด์„œ ์ผ๊ด€๋œ ์นด๋ฉ”๋ผ ๊ฒฝํ—˜์„ ์ œ๊ณตํ•ด์ค˜!

CameraX๋Š” ๋‚ด๋ถ€์ ์œผ๋กœ Camera2 API๋ฅผ ์‚ฌ์šฉํ•˜์ง€๋งŒ, ๊ฐœ๋ฐœ์ž์—๊ฒŒ๋Š” ํ›จ์”ฌ ๊ฐ„๋‹จํ•œ API๋ฅผ ์ œ๊ณตํ•ด.
๊ทธ๋ฆฌ๊ณ  ๊ธฐ๊ธฐ ํ˜ธํ™˜์„ฑ ๋ฌธ์ œ๋ฅผ ์ž๋™์œผ๋กœ ์ฒ˜๋ฆฌํ•ด์ค˜์„œ ๋‹ค์–‘ํ•œ ์•ˆ๋“œ๋กœ์ด๋“œ ๊ธฐ๊ธฐ์—์„œ ์ผ๊ด€๋˜๊ฒŒ ๋™์ž‘ํ•ด.

Kotlin - CameraX ๊ธฐ๋ณธ ์‚ฌ์šฉ๋ฒ•
class CameraFragment : Fragment() {
    private lateinit var cameraExecutor: ExecutorService
    
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        cameraExecutor = Executors.newSingleThreadExecutor()
        startCamera()
    }
    
    private fun startCamera() {
        val cameraProviderFuture = ProcessCameraProvider.getInstance(requireContext())
        
        cameraProviderFuture.addListener({
            val cameraProvider = cameraProviderFuture.get()
            
            // Preview UseCase
            val preview = Preview.Builder().build().also {
                it.setSurfaceProvider(binding.viewFinder.surfaceProvider)
            }
            
            // ImageCapture UseCase
            val imageCapture = ImageCapture.Builder()
                .setCaptureMode(ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY)
                .build()
            
            // ImageAnalysis UseCase (์‹ค์‹œ๊ฐ„ ๋ถ„์„)
            val imageAnalyzer = ImageAnalysis.Builder()
                .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
                .build()
                .also {
                    it.setAnalyzer(cameraExecutor) { imageProxy ->
                        analyzeImage(imageProxy)
                    }
                }
            
            // ์นด๋ฉ”๋ผ ๋ฐ”์ธ๋”ฉ
            try {
                cameraProvider.unbindAll()
                cameraProvider.bindToLifecycle(
                    viewLifecycleOwner,
                    CameraSelector.DEFAULT_BACK_CAMERA,
                    preview,
                    imageCapture,
                    imageAnalyzer
                )
            } catch (e: Exception) {
                Log.e("CameraX", "์นด๋ฉ”๋ผ ๋ฐ”์ธ๋”ฉ ์‹คํŒจ", e)
            }
        }, ContextCompat.getMainExecutor(requireContext()))
    }
    
    override fun onDestroyView() {
        super.onDestroyView()
        cameraExecutor.shutdown()
    }
}
CameraX UseCase 3๊ฐ€์ง€

Preview โ€” ์นด๋ฉ”๋ผ ๋ฏธ๋ฆฌ๋ณด๊ธฐ. PreviewView์— ์‹ค์‹œ๊ฐ„ ์˜์ƒ ํ‘œ์‹œ.
ImageCapture โ€” ์‚ฌ์ง„ ์ดฌ์˜. ๊ณ ํ’ˆ์งˆ ์ด๋ฏธ์ง€๋ฅผ ํŒŒ์ผ์ด๋‚˜ ๋ฉ”๋ชจ๋ฆฌ ๋ฒ„ํผ๋กœ ์ €์žฅ.
ImageAnalysis โ€” ์‹ค์‹œ๊ฐ„ ์ด๋ฏธ์ง€ ๋ถ„์„. ML Kit์™€ ์—ฐ๋™ํ•ด์„œ QR์ฝ”๋“œ ์ธ์‹, ์–ผ๊ตด ๊ฐ์ง€ ๋“ฑ ๊ตฌํ˜„ ๊ฐ€๋Šฅ!

โœจ Android KTX: Kotlin์˜ ํž˜์„ ์ตœ๋Œ€๋กœ!

KTX๋Š” Kotlin Extensions์˜ ์•ฝ์ž์•ผ.
์•ˆ๋“œ๋กœ์ด๋“œ API๋ฅผ Kotlin ์นœํ™”์ ์œผ๋กœ ๋งŒ๋“ค์–ด์ฃผ๋Š” ํ™•์žฅ ํ•จ์ˆ˜ ๋ชจ์Œ์ด์•ผ.
์ฝ”๋“œ๊ฐ€ ์–ผ๋งˆ๋‚˜ ๊ฐ„๊ฒฐํ•ด์ง€๋Š”์ง€ ์ง์ ‘ ๋น„๊ตํ•ด๋ณผ๊ฒŒ! ๐Ÿ‘€

Kotlin - KTX ์‚ฌ์šฉ ์ „ํ›„ ๋น„๊ต
// โŒ KTX ์—†์ด
val sharedPreferences = context.getSharedPreferences("prefs", Context.MODE_PRIVATE)
val editor = sharedPreferences.edit()
editor.putString("key", "value")
editor.apply()

// โœ… KTX ์‚ฌ์šฉ
context.getSharedPreferences("prefs", Context.MODE_PRIVATE).edit {
    putString("key", "value")
}

// โŒ KTX ์—†์ด - Fragment ํŠธ๋žœ์žญ์…˜
supportFragmentManager.beginTransaction()
    .replace(R.id.container, fragment)
    .addToBackStack(null)
    .commit()

// โœ… KTX ์‚ฌ์šฉ
supportFragmentManager.commit {
    replace(R.id.container, fragment)
    addToBackStack(null)
}

// โŒ KTX ์—†์ด - Bundle ์ƒ์„ฑ
val bundle = Bundle()
bundle.putString("name", "ํ™๊ธธ๋™")
bundle.putInt("age", 25)

// โœ… KTX ์‚ฌ์šฉ
val bundle = bundleOf("name" to "ํ™๊ธธ๋™", "age" to 25)

// โŒ KTX ์—†์ด - View ๊ฐ€์‹œ์„ฑ
view.visibility = View.VISIBLE
view.visibility = View.GONE

// โœ… KTX ์‚ฌ์šฉ
view.isVisible = true
view.isGone = true

// โŒ KTX ์—†์ด - ์ƒ‰์ƒ ๊ฐ€์ ธ์˜ค๊ธฐ
ContextCompat.getColor(context, R.color.primary)

// โœ… KTX ์‚ฌ์šฉ
context.getColor(R.color.primary)  // API 23+์—์„œ ์ž๋™ ์ฒ˜๋ฆฌ
โœ… ์ฃผ์š” KTX ๋ชจ๋“ˆ๋“ค

core-ktx โ€” ํ•ต์‹ฌ ์•ˆ๋“œ๋กœ์ด๋“œ API ํ™•์žฅ
fragment-ktx โ€” Fragment ๊ด€๋ จ ํ™•์žฅ (by viewModels() ๋“ฑ)
lifecycle-ktx โ€” Lifecycle ๊ด€๋ จ ํ™•์žฅ (lifecycleScope ๋“ฑ)
room-ktx โ€” Room ์ฝ”๋ฃจํ‹ด ์ง€์›
navigation-ktx โ€” Navigation ํ™•์žฅ
work-runtime-ktx โ€” WorkManager ์ฝ”๋ฃจํ‹ด ์ง€์›

KTX๋ฅผ ์“ฐ๋ฉด ์ฝ”๋“œ๊ฐ€ ํ›จ์”ฌ ๊ฐ„๊ฒฐํ•˜๊ณ  ์ฝ๊ธฐ ์‰ฌ์›Œ์ ธ.
ํŠนํžˆ lifecycleScope์™€ viewModelScope๋Š” ์ฝ”๋ฃจํ‹ด์„ ์ƒ๋ช…์ฃผ๊ธฐ์— ๋งž๊ฒŒ ์ž๋™์œผ๋กœ ์ทจ์†Œํ•ด์ค˜์„œ ๋ฉ”๋ชจ๋ฆฌ ๋ˆ„์ˆ˜ ๊ฑฑ์ •์ด ์—†์–ด! ๐Ÿ™Œ


๐Ÿ—๏ธ ์‹ค์ „! Jetpack์œผ๋กœ ์™„์„ฑํ•˜๋Š” ์•ฑ ์•„ํ‚คํ…์ฒ˜

์ž, ์ด์ œ ๋ฐฐ์šด ๊ฒƒ๋“ค์„ ์ข…ํ•ฉํ•ด์„œ ์‹ค์ œ ํ”„๋กœ์ ํŠธ์— ์–ด๋–ป๊ฒŒ ์ ์šฉํ•˜๋Š”์ง€ ๋ณด์—ฌ์ค„๊ฒŒ! ๐ŸŽฏ
๊ตฌ๊ธ€์ด ๊ถŒ์žฅํ•˜๋Š” Clean Architecture + MVVM ํŒจํ„ด์— Jetpack ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋“ค์„ ๋…น์—ฌ๋‚ธ ๊ตฌ์กฐ์•ผ.

๐Ÿ“ ๊ถŒ์žฅ ํ”„๋กœ์ ํŠธ ๊ตฌ์กฐ

app/src/main/java/com/example/myapp/
โ”œโ”€โ”€ data/ โ€” ๋ฐ์ดํ„ฐ ๋ ˆ์ด์–ด
โ”‚ โ”œโ”€โ”€ local/ โ€” Room DB, DataStore
โ”‚ โ”œโ”€โ”€ remote/ โ€” Retrofit API ์„œ๋น„์Šค
โ”‚ โ””โ”€โ”€ repository/ โ€” Repository ๊ตฌํ˜„์ฒด
โ”œโ”€โ”€ domain/ โ€” ๋„๋ฉ”์ธ ๋ ˆ์ด์–ด (์„ ํƒ์ )
โ”‚ โ”œโ”€โ”€ model/ โ€” ๋„๋ฉ”์ธ ๋ชจ๋ธ
โ”‚ โ””โ”€โ”€ usecase/ โ€” ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง
โ”œโ”€โ”€ presentation/ โ€” ํ”„๋ ˆ์  ํ…Œ์ด์…˜ ๋ ˆ์ด์–ด
โ”‚ โ”œโ”€โ”€ home/ โ€” ํ™ˆ ํ™”๋ฉด (Fragment + ViewModel)
โ”‚ โ”œโ”€โ”€ detail/ โ€” ์ƒ์„ธ ํ™”๋ฉด
โ”‚ โ””โ”€โ”€ common/ โ€” ๊ณตํ†ต UI ์ปดํฌ๋„ŒํŠธ
โ””โ”€โ”€ di/ โ€” Hilt ๋ชจ๋“ˆ
Kotlin - ์™„์„ฑ๋œ ViewModel ์˜ˆ์‹œ
@HiltViewModel
class HomeViewModel @Inject constructor(
    private val getUsersUseCase: GetUsersUseCase,
    private val savedStateHandle: SavedStateHandle
) : ViewModel() {
    
    // UI ์ƒํƒœ๋ฅผ ํ•˜๋‚˜์˜ sealed class๋กœ ๊ด€๋ฆฌ
    sealed class UiState {
        object Loading : UiState()
        data class Success(val users: List<User>) : UiState()
        data class Error(val message: String) : UiState()
    }
    
    private val _uiState = MutableStateFlow<UiState>(UiState.Loading)
    val uiState: StateFlow<UiState> = _uiState.asStateFlow()
    
    // ๊ฒ€์ƒ‰์–ด ์ƒํƒœ
    private val _searchQuery = MutableStateFlow("")
    val searchQuery: StateFlow<String> = _searchQuery.asStateFlow()
    
    init {
        loadUsers()
    }
    
    fun loadUsers() {
        viewModelScope.launch {
            _uiState.value = UiState.Loading
            getUsersUseCase()
                .catch { e ->
                    _uiState.value = UiState.Error(e.message ?: "์•Œ ์ˆ˜ ์—†๋Š” ์˜ค๋ฅ˜")
                }
                .collect { users ->
                    _uiState.value = UiState.Success(users)
                }
        }
    }
    
    fun updateSearchQuery(query: String) {
        _searchQuery.value = query
    }
    
    fun refreshUsers() {
        loadUsers()
    }
}

์ด ๊ตฌ์กฐ์˜ ํ•ต์‹ฌ์€ ๋‹จ๋ฐฉํ–ฅ ๋ฐ์ดํ„ฐ ํ๋ฆ„(UDF, Unidirectional Data Flow)์ด์•ผ.
UI๋Š” ์ด๋ฒคํŠธ๋ฅผ ViewModel์— ์ „๋‹ฌํ•˜๊ณ , ViewModel์€ ์ƒํƒœ๋ฅผ ์—…๋ฐ์ดํŠธํ•˜๊ณ , UI๋Š” ์ƒํƒœ๋ฅผ ๊ด€์ฐฐํ•ด์„œ ํ™”๋ฉด์„ ์—…๋ฐ์ดํŠธํ•ด.
๋ฐ์ดํ„ฐ๊ฐ€ ํ•œ ๋ฐฉํ–ฅ์œผ๋กœ๋งŒ ํ๋ฅด๋‹ˆ๊นŒ ๋””๋ฒ„๊น…์ด ์‰ฝ๊ณ , ์˜ˆ์ธก ๊ฐ€๋Šฅํ•œ ๋™์ž‘์„ ๋ณด์žฅํ•  ์ˆ˜ ์žˆ์–ด! ๐ŸŽฏ

๐Ÿ’ก ์žฌ๋Šฅ๋„ท์—์„œ ์•ˆ๋“œ๋กœ์ด๋“œ ๊ฐœ๋ฐœ ๊ด€๋ จ ์žฌ๋Šฅ์„ ์ฐพ๊ฑฐ๋‚˜ ๊ณต์œ ํ•  ๋•Œ, ์ด๋Ÿฐ Jetpack ๊ธฐ๋ฐ˜ ์•„ํ‚คํ…์ฒ˜ ์ง€์‹์ด ์žˆ์œผ๋ฉด ํ›จ์”ฌ ๊ฒฝ์Ÿ๋ ฅ ์žˆ์–ด! ์‹ค์ œ๋กœ ๊ธฐ์—… ํ”„๋กœ์ ํŠธ์—์„œ๋„ ์ด ๊ตฌ์กฐ๋ฅผ ๋งŽ์ด ์‚ฌ์šฉํ•˜๊ฑฐ๋“ . ๐Ÿš€

๐Ÿ“Š Jetpack ๋„์ž… ์ „ํ›„ ๊ฐœ๋ฐœ ์ƒ์‚ฐ์„ฑ ๋น„๊ต ์ฃผ์š” ๊ฐœ๋ฐœ ์˜์—ญ๋ณ„ ์ฝ”๋“œ ๋ณต์žก๋„ ๊ฐ์†Œ์œจ (๊ตฌ๊ธ€ ๊ณต์‹ ์ž๋ฃŒ ๊ธฐ๋ฐ˜) 100% 80% 60% 40% 20% 0% 80% 40% UI ๊ฐœ๋ฐœ (Compose) 93% 33% ๋ฐ์ดํ„ฐ ๊ด€๋ฆฌ (Room) 73% 33% ํ™”๋ฉด ์ „ํ™˜ (Navigation) 87% 40% ๋ฐฑ๊ทธ๋ผ์šด๋“œ (WorkManager) Jetpack ๋„์ž… ์ „ Jetpack ๋„์ž… ํ›„

๐ŸŽฏ Jetpack ๋„์ž… ์‹ค์ „ ํŒ & ์ฃผ์˜์‚ฌํ•ญ

์ž, ์ด์ œ ์‹ค์ œ๋กœ Jetpack์„ ํ”„๋กœ์ ํŠธ์— ๋„์ž…ํ•  ๋•Œ ์•Œ์•„๋‘๋ฉด ์ข‹์€ ํŒ๋“ค์„ ์ •๋ฆฌํ•ด๋ณผ๊ฒŒ! ๐Ÿ’ช

โœ… ๋ฒ„์ „ ๊ด€๋ฆฌ: BOM(Bill of Materials) ํ™œ์šฉ

Jetpack ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋“ค์€ ์„œ๋กœ ์˜์กด์„ฑ์ด ์žˆ์–ด์„œ ๋ฒ„์ „ ์ถฉ๋Œ์ด ์ƒ๊ธธ ์ˆ˜ ์žˆ์–ด.
Compose BOM์„ ์‚ฌ์šฉํ•˜๋ฉด ๋ชจ๋“  Compose ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ๋ฒ„์ „์„ ํ•œ ๋ฒˆ์— ๊ด€๋ฆฌํ•  ์ˆ˜ ์žˆ์–ด!

Gradle - BOM ์‚ฌ์šฉ๋ฒ•
// build.gradle.kts (app)
dependencies {
    // Compose BOM - ๋ฒ„์ „ ํ•˜๋‚˜๋กœ ๋ชจ๋“  Compose ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ๊ด€๋ฆฌ
    val composeBom = platform("androidx.compose:compose-bom:2024.02.00")
    implementation(composeBom)
    
    // ๋ฒ„์ „ ๋ช…์‹œ ๋ถˆํ•„์š”!
    implementation("androidx.compose.ui:ui")
    implementation("androidx.compose.material3:material3")
    implementation("androidx.compose.ui:ui-tooling-preview")
    
    // Jetpack ํ•ต์‹ฌ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋“ค
    implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.7.0")
    implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.7.0")
    implementation("androidx.room:room-runtime:2.6.1")
    implementation("androidx.room:room-ktx:2.6.1")
    ksp("androidx.room:room-compiler:2.6.1")  // KSP ์‚ฌ์šฉ ๊ถŒ์žฅ!
    
    implementation("androidx.navigation:navigation-fragment-ktx:2.7.7")
    implementation("androidx.navigation:navigation-ui-ktx:2.7.7")
    
    implementation("androidx.work:work-runtime-ktx:2.9.0")
    
    implementation("com.google.dagger:hilt-android:2.51")
    ksp("com.google.dagger:hilt-compiler:2.51")
    
    implementation("androidx.datastore:datastore-preferences:1.0.0")
    implementation("androidx.paging:paging-runtime-ktx:3.2.1")
}
โš ๏ธ KAPT โ†’ KSP ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ๊ถŒ์žฅ!

Room, Hilt ๋“ฑ ์–ด๋…ธํ…Œ์ด์…˜ ํ”„๋กœ์„ธ์„œ๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋“ค์€ KAPT ๋Œ€์‹  KSP(Kotlin Symbol Processing)๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๊ฒŒ ์ข‹์•„.
KSP๋Š” KAPT๋ณด๋‹ค ๋นŒ๋“œ ์†๋„๊ฐ€ ์ตœ๋Œ€ 2๋ฐฐ ๋น ๋ฅด๊ณ , Kotlin ์นœํ™”์ ์ด์•ผ.
build.gradle.kts์—์„œ kapt ๋Œ€์‹  ksp๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ๋ผ!

๐Ÿ”„ ์ ์ง„์  ๋„์ž… ์ „๋žต

1
๊ธฐ์กด ํ”„๋กœ์ ํŠธ๋ผ๋ฉด โ€” ํ•œ ๋ฒˆ์— ๋‹ค ๋ฐ”๊พธ๋ ค ํ•˜์ง€ ๋งˆ. ์ƒˆ๋กœ์šด ๊ธฐ๋Šฅ ๊ฐœ๋ฐœํ•  ๋•Œ Jetpack์„ ์ ์šฉํ•˜๊ณ , ๊ธฐ์กด ์ฝ”๋“œ๋Š” ์ ์ง„์ ์œผ๋กœ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ํ•ด.
2
์šฐ์„ ์ˆœ์œ„ ์ •ํ•˜๊ธฐ โ€” ViewModel + LiveData/StateFlow โ†’ Room โ†’ Navigation โ†’ Hilt ์ˆœ์„œ๋กœ ๋„์ž…ํ•˜๋Š” ๊ฒŒ ํšจ๊ณผ์ ์ด์•ผ.
3
Compose๋Š” ์‹ ์ค‘ํ•˜๊ฒŒ โ€” ๊ธฐ์กด XML ๊ธฐ๋ฐ˜ ํ”„๋กœ์ ํŠธ์— Compose๋ฅผ ๋„์ž…ํ•  ๋•Œ๋Š” ํŒ€ ์ „์ฒด์˜ ํ•™์Šต ์ค€๋น„๊ฐ€ ๋๋Š”์ง€ ํ™•์ธํ•ด. ๋Ÿฌ๋‹ ์ปค๋ธŒ๊ฐ€ ์žˆ์–ด!
4
ํ…Œ์ŠคํŠธ ์ฝ”๋“œ ํ•จ๊ป˜ ์ž‘์„ฑ โ€” Jetpack ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋“ค์€ ํ…Œ์ŠคํŠธ ์ง€์›์ด ์ž˜ ๋ผ ์žˆ์–ด. ๋„์ž…ํ•˜๋ฉด์„œ ๋‹จ์œ„ ํ…Œ์ŠคํŠธ๋„ ํ•จ๊ป˜ ์ž‘์„ฑํ•˜๋ฉด ์žฅ๊ธฐ์ ์œผ๋กœ ํฐ ๋„์›€์ด ๋ผ.

๐Ÿ“š ๊ณต์‹ ํ•™์Šต ๋ฆฌ์†Œ์Šค

๐ŸŒ ๊ตฌ๊ธ€ ๊ณต์‹ ๋ฆฌ์†Œ์Šค

developer.android.com/jetpack โ€” Jetpack ๊ณต์‹ ๋ฌธ์„œ
developer.android.com/codelabs โ€” ๋‹จ๊ณ„๋ณ„ ์‹ค์Šต ์ฝ”๋“œ๋žฉ
github.com/android/architecture-samples โ€” ๊ณต์‹ ์•„ํ‚คํ…์ฒ˜ ์ƒ˜ํ”Œ
github.com/android/nowinandroid โ€” ์‹ค์ œ ์•ฑ ์ˆ˜์ค€์˜ ์™„์„ฑ๋„ ๋†’์€ ์ƒ˜ํ”Œ ์•ฑ
๐Ÿ’ก Now in Android ํ”„๋กœ์ ํŠธ๋Š” ๊ตฌ๊ธ€์ด ๋งŒ๋“  ์‹ค์ œ ์•ฑ ์ˆ˜์ค€์˜ ์˜คํ”ˆ์†Œ์Šค ํ”„๋กœ์ ํŠธ์•ผ.
Compose, Hilt, Room, Navigation, WorkManager ๋“ฑ ์ตœ์‹  Jetpack ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋“ค์ด ๋ชจ๋‘ ์ ์šฉ๋˜์–ด ์žˆ์–ด์„œ ์‹ค์ „ ์ฝ”๋“œ๋ฅผ ์ฐธ๊ณ ํ•˜๊ธฐ์— ์ตœ๊ณ ์•ผ!

โšก Jetpack์œผ๋กœ ์•ฑ ์„ฑ๋Šฅ๊นŒ์ง€ ์žก๊ธฐ

์ƒ์‚ฐ์„ฑ๋งŒ ์˜ฌ๋ฆฌ๋Š” ๊ฒŒ ์•„๋‹ˆ์•ผ! Jetpack ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋“ค์„ ์ œ๋Œ€๋กœ ํ™œ์šฉํ•˜๋ฉด ์•ฑ ์„ฑ๋Šฅ๋„ ํฌ๊ฒŒ ํ–ฅ์ƒ๋ผ. ๐Ÿš€

๐Ÿ” Benchmark ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ

Jetpack Benchmark๋Š” ์ฝ”๋“œ ์„ฑ๋Šฅ์„ ์ •ํ™•ํ•˜๊ฒŒ ์ธก์ •ํ•  ์ˆ˜ ์žˆ๋Š” ๋„๊ตฌ์•ผ.
์ผ๋ฐ˜ JUnit ํ…Œ์ŠคํŠธ์™€ ๋‹ฌ๋ฆฌ JIT ์ปดํŒŒ์ผ, ๊ฐ€๋น„์ง€ ์ปฌ๋ ‰์…˜ ๋“ฑ์˜ ์˜ํ–ฅ์„ ์ œ๊ฑฐํ•˜๊ณ  ์ •ํ™•ํ•œ ์ธก์ •๊ฐ’์„ ์ œ๊ณตํ•ด.

Kotlin - Benchmark ์‚ฌ์šฉ ์˜ˆ์‹œ
@RunWith(AndroidJUnit4::class)
class MyBenchmark {
    @get:Rule
    val benchmarkRule = BenchmarkRule()
    
    @Test
    fun benchmarkSomeWork() {
        benchmarkRule.measureRepeated {
            // ์„ฑ๋Šฅ ์ธก์ •ํ•  ์ฝ”๋“œ
            doSomeExpensiveWork()
        }
    }
}

๐Ÿ“Š Compose ์„ฑ๋Šฅ ์ตœ์ ํ™” ํŒ

Recomposition ์ตœ์†Œํ™” ์ „๋žต

1. ๋žŒ๋‹ค ์•ˆ์ •์„ฑ โ€” ๋žŒ๋‹ค๋ฅผ remember๋กœ ๊ฐ์‹ธ๊ฑฐ๋‚˜ ํด๋ž˜์Šค ๋ฉ”์„œ๋“œ๋กœ ์ „๋‹ฌํ•ด์„œ ๋ถˆํ•„์š”ํ•œ recomposition ๋ฐฉ์ง€.
2. ์•ˆ์ •์ ์ธ ํƒ€์ž… ์‚ฌ์šฉ โ€” @Stable, @Immutable ์–ด๋…ธํ…Œ์ด์…˜์œผ๋กœ Compose์—๊ฒŒ ํƒ€์ž… ์•ˆ์ •์„ฑ ์•Œ๋ ค์ฃผ๊ธฐ.
3. LazyList ์ตœ์ ํ™” โ€” key ํŒŒ๋ผ๋ฏธํ„ฐ๋ฅผ ํ•ญ์ƒ ์ง€์ •ํ•ด์„œ ์•„์ดํ…œ ์žฌ์‚ฌ์šฉ ํšจ์œจ ๋†’์ด๊ธฐ.
4. derivedStateOf โ€” ๋‹ค๋ฅธ ์ƒํƒœ์—์„œ ํŒŒ์ƒ๋œ ์ƒํƒœ๋Š” derivedStateOf๋กœ ๊ฐ์‹ธ์„œ ๋ถˆํ•„์š”ํ•œ recomposition ๋ฐฉ์ง€.
Kotlin - Compose ์„ฑ๋Šฅ ์ตœ์ ํ™” ์˜ˆ์‹œ
// โŒ ๋งค๋ฒˆ ์ƒˆ๋กœ์šด ๋žŒ๋‹ค ์ƒ์„ฑ - ๋ถˆํ•„์š”ํ•œ recomposition ์œ ๋ฐœ
@Composable
fun UserList(users: List<User>, onUserClick: (User) -> Unit) {
    LazyColumn {
        items(users) { user ->
            UserItem(
                user = user,
                onClick = { onUserClick(user) }  // ๋งค๋ฒˆ ์ƒˆ ๋žŒ๋‹ค ์ƒ์„ฑ!
            )
        }
    }
}

// โœ… key ํŒŒ๋ผ๋ฏธํ„ฐ + ์•ˆ์ •์ ์ธ ๋žŒ๋‹ค ์ „๋‹ฌ
@Composable
fun UserList(users: List<User>, onUserClick: (User) -> Unit) {
    LazyColumn {
        items(
            items = users,
            key = { user -> user.id }  // ๊ณ ์œ  ํ‚ค ์ง€์ •!
        ) { user ->
            UserItem(
                user = user,
                onClick = onUserClick  // ๋žŒ๋‹ค ์ง์ ‘ ์ „๋‹ฌ
            )
        }
    }
}

// derivedStateOf ํ™œ์šฉ
@Composable
fun SearchScreen(users: List<User>, query: String) {
    // query๋‚˜ users๊ฐ€ ๋ฐ”๋€” ๋•Œ๋งŒ ์žฌ๊ณ„์‚ฐ
    val filteredUsers by remember(users, query) {
        derivedStateOf {
            users.filter { it.name.contains(query, ignoreCase = true) }
        }
    }
    
    LazyColumn {
        items(filteredUsers, key = { it.id }) { user ->
            UserItem(user = user)
        }
    }
}

๐Ÿ—„๏ธ Room ์„ฑ๋Šฅ ์ตœ์ ํ™”

1
์ธ๋ฑ์Šค ํ™œ์šฉ โ€” ์ž์ฃผ ๊ฒ€์ƒ‰ํ•˜๋Š” ์ปฌ๋Ÿผ์— @Index ์–ด๋…ธํ…Œ์ด์…˜์œผ๋กœ ์ธ๋ฑ์Šค ์ถ”๊ฐ€. ์ฟผ๋ฆฌ ์†๋„๊ฐ€ ํฌ๊ฒŒ ํ–ฅ์ƒ๋ผ.
2
Flow ์‚ฌ์šฉ โ€” ๋ฐ์ดํ„ฐ ๋ณ€๊ฒฝ ๊ฐ์ง€๊ฐ€ ํ•„์š”ํ•œ ์ฟผ๋ฆฌ๋Š” Flow๋กœ ๋ฐ˜ํ™˜. ๋ถˆํ•„์š”ํ•œ ํด๋ง ์—†์ด ํšจ์œจ์ ์ธ ์—…๋ฐ์ดํŠธ ๊ฐ€๋Šฅ.
3
ํŠธ๋žœ์žญ์…˜ ํ™œ์šฉ โ€” ์—ฌ๋Ÿฌ DB ์ž‘์—…์„ @Transaction์œผ๋กœ ๋ฌถ์œผ๋ฉด ์„ฑ๋Šฅ๊ณผ ๋ฐ์ดํ„ฐ ์ผ๊ด€์„ฑ ๋ชจ๋‘ ํ–ฅ์ƒ.
4
ํ•„์š”ํ•œ ์ปฌ๋Ÿผ๋งŒ ์กฐํšŒ โ€” SELECT * ๋Œ€์‹  ํ•„์š”ํ•œ ์ปฌ๋Ÿผ๋งŒ ์„ ํƒํ•˜๋Š” ์ฟผ๋ฆฌ ์ž‘์„ฑ. ํŠนํžˆ ๋Œ€์šฉ๋Ÿ‰ ๋ฐ์ดํ„ฐ์—์„œ ํšจ๊ณผ์ .

๐Ÿงช Jetpack์œผ๋กœ ํ…Œ์ŠคํŠธ ์ฝ”๋“œ ์ž‘์„ฑํ•˜๊ธฐ

ํ…Œ์ŠคํŠธ ์ฝ”๋“œ... ๊ท€์ฐฎ๋‹ค๊ณ  ์•ˆ ์“ฐ๋Š” ์‚ฌ๋žŒ ๋งŽ์ง€? ๐Ÿ˜…
๊ทผ๋ฐ Jetpack ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋“ค์€ ํ…Œ์ŠคํŠธ๋ฅผ ์ •๋ง ์‰ฝ๊ฒŒ ๋งŒ๋“ค์–ด์ค˜.
ํŠนํžˆ ViewModel, Room, WorkManager๋Š” ์ „์šฉ ํ…Œ์ŠคํŠธ ๋„๊ตฌ๊ฐ€ ์žˆ์–ด!

Kotlin - ViewModel ํ…Œ์ŠคํŠธ
@OptIn(ExperimentalCoroutinesApi::class)
class HomeViewModelTest {
    
    @get:Rule
    val mainDispatcherRule = MainDispatcherRule()  // ํ…Œ์ŠคํŠธ์šฉ ๋””์ŠคํŒจ์ฒ˜
    
    private lateinit var viewModel: HomeViewModel
    private val fakeRepository = FakeUserRepository()  // ๊ฐ€์งœ Repository
    
    @Before
    fun setup() {
        viewModel = HomeViewModel(fakeRepository)
    }
    
    @Test
    fun `์‚ฌ์šฉ์ž ๋ชฉ๋ก ๋กœ๋“œ ์„ฑ๊ณต ์‹œ Success ์ƒํƒœ`() = runTest {
        // Given
        fakeRepository.setUsers(listOf(
            User(id = 1, name = "ํ™๊ธธ๋™"),
            User(id = 2, name = "๊น€์ฒ ์ˆ˜")
        ))
        
        // When
        viewModel.loadUsers()
        
        // Then
        val state = viewModel.uiState.value
        assertTrue(state is HomeViewModel.UiState.Success)
        assertEquals(2, (state as HomeViewModel.UiState.Success).users.size)
    }
    
    @Test
    fun `๋„คํŠธ์›Œํฌ ์˜ค๋ฅ˜ ์‹œ Error ์ƒํƒœ`() = runTest {
        // Given
        fakeRepository.setShouldThrowError(true)
        
        // When
        viewModel.loadUsers()
        
        // Then
        assertTrue(viewModel.uiState.value is HomeViewModel.UiState.Error)
    }
}

// Room ์ธ๋ฉ”๋ชจ๋ฆฌ DB ํ…Œ์ŠคํŠธ
@RunWith(AndroidJUnit4::class)
class UserDaoTest {
    private lateinit var database: AppDatabase
    private lateinit var userDao: UserDao
    
    @Before
    fun setup() {
        database = Room.inMemoryDatabaseBuilder(
            ApplicationProvider.getApplicationContext(),
            AppDatabase::class.java
        ).allowMainThreadQueries().build()
        userDao = database.userDao()
    }
    
    @After
    fun teardown() {
        database.close()
    }
    
    @Test
    fun insertAndGetUser() = runTest {
        val user = User(name = "ํ…Œ์ŠคํŠธ ์œ ์ €", email = "test@test.com")
        userDao.insertUser(user)
        
        val users = userDao.getAllUsers().first()
        assertEquals(1, users.size)
        assertEquals("ํ…Œ์ŠคํŠธ ์œ ์ €", users[0].name)
    }
}
โœ… Jetpack ํ…Œ์ŠคํŠธ ๋„๊ตฌ ๋ชจ์Œ

androidx.test โ€” ์•ˆ๋“œ๋กœ์ด๋“œ ํ…Œ์ŠคํŠธ ๊ธฐ๋ฐ˜ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ
androidx.test.espresso โ€” UI ํ…Œ์ŠคํŠธ ์ž๋™ํ™”
androidx.compose.ui.test โ€” Compose UI ํ…Œ์ŠคํŠธ
kotlinx-coroutines-test โ€” ์ฝ”๋ฃจํ‹ด ํ…Œ์ŠคํŠธ ์œ ํ‹ธ๋ฆฌํ‹ฐ
androidx.work.testing โ€” WorkManager ํ…Œ์ŠคํŠธ
hilt-android-testing โ€” Hilt ํ…Œ์ŠคํŠธ ์ง€์›

ํŠนํžˆ ์žฌ๋Šฅ๋„ท์—์„œ ์•ˆ๋“œ๋กœ์ด๋“œ ๊ฐœ๋ฐœ ํ”„๋ฆฌ๋žœ์„œ๋กœ ํ™œ๋™ํ•œ๋‹ค๋ฉด, ํ…Œ์ŠคํŠธ ์ฝ”๋“œ ์ž‘์„ฑ ๋Šฅ๋ ฅ์€ ํด๋ผ์ด์–ธํŠธ์—๊ฒŒ ์‹ ๋ขฐ๋ฅผ ์ค„ ์ˆ˜ ์žˆ๋Š” ์ค‘์š”ํ•œ ์—ญ๋Ÿ‰์ด์•ผ.
Jetpack ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋“ค์ด ํ…Œ์ŠคํŠธ๋ฅผ ์‰ฝ๊ฒŒ ๋งŒ๋“ค์–ด์ฃผ๋‹ˆ๊นŒ, ๊ผญ ํ…Œ์ŠคํŠธ ์ฝ”๋“œ ์ž‘์„ฑ ์Šต๊ด€์„ ๋“ค์—ฌ๋ด! ๐Ÿ’ช


๐Ÿ—บ๏ธ Jetpack ํ•™์Šต ๋กœ๋“œ๋งต ๋‹จ๊ณ„๋ณ„๋กœ ์ฐจ๊ทผ์ฐจ๊ทผ ๋งˆ์Šคํ„ฐํ•˜์ž! 1๋‹จ๊ณ„ ๊ธฐ์ดˆ ๋‹ค์ง€๊ธฐ ViewModel + LiveData/StateFlow ์ƒ๋ช…์ฃผ๊ธฐ ์ดํ•ด ยท MVVM ํŒจํ„ด ยท ๋‹จ๋ฐฉํ–ฅ ๋ฐ์ดํ„ฐ ํ๋ฆ„ ๐ŸŒฑ 2๋‹จ๊ณ„ ๋ฐ์ดํ„ฐ ๋ ˆ์ด์–ด Room + DataStore + Hilt ๋กœ์ปฌ DB ยท ์„ค์ • ์ €์žฅ ยท ์˜์กด์„ฑ ์ฃผ์ž… ยท Repository ํŒจํ„ด ๐ŸŒฟ 3๋‹จ๊ณ„ UI & ํƒ์ƒ‰ Navigation + Paging + WorkManager ํ™”๋ฉด ์ „ํ™˜ ยท ํŽ˜์ด์ง€๋„ค์ด์…˜ ยท ๋ฐฑ๊ทธ๋ผ์šด๋“œ ์ž‘์—… ๐ŸŒณ 4๋‹จ๊ณ„ ์ตœ์‹  ๊ธฐ์ˆ  Jetpack Compose + CameraX + ML Kit ์„ ์–ธํ˜• UI ยท ์นด๋ฉ”๋ผ ยท ๋จธ์‹ ๋Ÿฌ๋‹ ํ†ตํ•ฉ ๐Ÿš€ ๊ฐ ๋‹จ๊ณ„๋Š” 2-4์ฃผ ํ•™์Šต ๊ถŒ์žฅ ยท ๊ณต์‹ ์ฝ”๋“œ๋žฉ ํ™œ์šฉ ์ถ”์ฒœ

๐Ÿ”ฎ 2024๋…„ Jetpack ์ตœ์‹  ํŠธ๋ Œ๋“œ

Jetpack์€ ๊ณ„์† ๋ฐœ์ „ํ•˜๊ณ  ์žˆ์–ด! ์ตœ์‹  ํŠธ๋ Œ๋“œ๋ฅผ ์•Œ์•„๋‘๋ฉด ๋ฏธ๋ž˜ ์ค€๋น„๋„ ๋˜๊ณ , ๊ธฐ์ˆ  ๋ฉด์ ‘์—์„œ๋„ ๋น›๋‚  ์ˆ˜ ์žˆ์–ด. โœจ

1. Compose Multiplatform ํ™•์žฅ

JetBrains์™€ ๊ตฌ๊ธ€์ด ํ˜‘๋ ฅํ•ด์„œ Compose๋ฅผ iOS, ๋ฐ์Šคํฌํ†ฑ, ์›น๊นŒ์ง€ ํ™•์žฅํ•˜๊ณ  ์žˆ์–ด.
Kotlin Multiplatform Mobile(KMM)๊ณผ ๊ฒฐํ•ฉํ•˜๋ฉด ํ•˜๋‚˜์˜ ์ฝ”๋“œ๋ฒ ์ด์Šค๋กœ ์•ˆ๋“œ๋กœ์ด๋“œ์™€ iOS ์•ฑ์„ ๋™์‹œ์— ๊ฐœ๋ฐœํ•  ์ˆ˜ ์žˆ์–ด!
์•„์ง ์™„์ „ํžˆ ์•ˆ์ •ํ™”๋˜์ง„ ์•Š์•˜์ง€๋งŒ, 2024๋…„์— ๋น ๋ฅด๊ฒŒ ์„ฑ์ˆ™ํ•ด์ง€๊ณ  ์žˆ์–ด. ์ฃผ๋ชฉํ•ด์•ผ ํ•  ๊ธฐ์ˆ ์ด์•ผ! ๐Ÿ‘€

2. Credential Manager

๊ธฐ์กด์˜ ๋ถ„์‚ฐ๋œ ์ธ์ฆ API๋“ค(SmartLock, FIDO2, Google Sign-In)์„ ํ†ตํ•ฉํ•œ ์ƒˆ๋กœ์šด ์ธ์ฆ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์•ผ.
ํŒจ์Šคํ‚ค(Passkey) ์ง€์›์ด ํ•ต์‹ฌ์ด์•ผ. ๋น„๋ฐ€๋ฒˆํ˜ธ ์—†๋Š” ์ธ์ฆ์˜ ๋ฏธ๋ž˜! ๐Ÿ”

3. Health Connect

๊ฑด๊ฐ• ๋ฐ์ดํ„ฐ๋ฅผ ์•ˆ์ „ํ•˜๊ฒŒ ๊ณต์œ ํ•˜๊ณ  ๊ด€๋ฆฌํ•˜๋Š” ํ”Œ๋žซํผ์ด์•ผ.
ํ”ผํŠธ๋‹ˆ์Šค ์•ฑ, ๊ฑด๊ฐ• ๋ชจ๋‹ˆํ„ฐ๋ง ์•ฑ ๊ฐœ๋ฐœํ•  ๋•Œ ํ•„์ˆ˜๊ฐ€ ๋  ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์•ผ.

4. Macrobenchmark

์•ฑ ์‹œ์ž‘ ์‹œ๊ฐ„, ์Šคํฌ๋กค ์„ฑ๋Šฅ ๋“ฑ ์‹ค์ œ ์‚ฌ์šฉ์ž ๊ฒฝํ—˜์— ๊ฐ€๊นŒ์šด ์„ฑ๋Šฅ์„ ์ธก์ •ํ•˜๋Š” ๋„๊ตฌ์•ผ.
Baseline Profiles์™€ ํ•จ๊ป˜ ์‚ฌ์šฉํ•˜๋ฉด ์•ฑ ์‹œ์ž‘ ์†๋„๋ฅผ ์ตœ๋Œ€ 40%๊นŒ์ง€ ๊ฐœ์„ ํ•  ์ˆ˜ ์žˆ์–ด! โšก

Kotlin - Baseline Profiles ์ƒ์„ฑ
// Macrobenchmark๋กœ Baseline Profile ์ƒ์„ฑ
@RunWith(AndroidJUnit4::class)
class BaselineProfileGenerator {
    @get:Rule
    val rule = BaselineProfileRule()
    
    @Test
    fun generate() {
        rule.collect("com.example.myapp") {
            // ์•ฑ ์‹œ์ž‘ ๋ฐ ์ฃผ์š” ์‚ฌ์šฉ์ž ์—ฌ์ • ์‹œ๋ฎฌ๋ ˆ์ด์…˜
            pressHome()
            startActivityAndWait()
            
            // ์ฃผ์š” ํ™”๋ฉด ํƒ์ƒ‰
            device.findObject(By.text("ํ™ˆ")).click()
            device.waitForIdle()
        }
    }
}
๐Ÿ”ฅ 2024๋…„ ์ฃผ๋ชฉํ•  Jetpack ์—…๋ฐ์ดํŠธ

Compose 1.6+ โ€” ์„ฑ๋Šฅ ๊ฐœ์„ , ์ƒˆ๋กœ์šด ์• ๋‹ˆ๋ฉ”์ด์…˜ API, ์ ‘๊ทผ์„ฑ ํ–ฅ์ƒ
Room 2.6+ โ€” KSP ์™„์ „ ์ง€์›, ๋ฉ€ํ‹ฐ๋งต ์ฟผ๋ฆฌ ๊ฒฐ๊ณผ ์ง€์›
Lifecycle 2.7+ โ€” SavedStateHandle ๊ฐœ์„ , collectAsStateWithLifecycle ์•ˆ์ •ํ™”
Navigation 2.7+ โ€” Compose Navigation ๊ฐœ์„ , ํƒ€์ž… ์•ˆ์ „ ๋ผ์šฐํŒ…
Hilt 2.50+ โ€” KSP ์™„์ „ ์ง€์›, ์„ฑ๋Šฅ ๊ฐœ์„ 

๐ŸŽ‰ ๋งˆ๋ฌด๋ฆฌ: Jetpack์œผ๋กœ ๋” ๋‚˜์€ ๊ฐœ๋ฐœ์ž ๋˜๊ธฐ

์ž, ์˜ค๋Š˜ ์ •๋ง ๋งŽ์€ ๋‚ด์šฉ์„ ๋‹ค๋ค˜์–ด! ๐ŸŽŠ
Jetpack์˜ ํ•ต์‹ฌ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋“ค์„ ์ •๋ฆฌํ•ด๋ณด๋ฉด:

โœ“
ViewModel + LiveData/StateFlow โ€” ์ƒ๋ช…์ฃผ๊ธฐ ๊ด€๋ฆฌ์™€ UI ์ƒํƒœ ๊ด€๋ฆฌ์˜ ํ•ต์‹ฌ
โœ“
Room โ€” SQLite๋ฅผ ์ธ๊ฐ„๋‹ต๊ฒŒ ์“ธ ์ˆ˜ ์žˆ๊ฒŒ ํ•ด์ฃผ๋Š” ORM
โœ“
Navigation Component โ€” ํ™”๋ฉด ์ „ํ™˜์„ ์‹œ๊ฐ์ ์œผ๋กœ ๊ด€๋ฆฌ
โœ“
WorkManager โ€” ์‹ ๋ขฐ์„ฑ ์žˆ๋Š” ๋ฐฑ๊ทธ๋ผ์šด๋“œ ์ž‘์—… ์ฒ˜๋ฆฌ
โœ“
Hilt โ€” ์˜์กด์„ฑ ์ฃผ์ž…์„ ์‰ฝ๊ณ  ํ‘œ์ค€ํ™”๋œ ๋ฐฉ์‹์œผ๋กœ
โœ“
Jetpack Compose โ€” ์„ ์–ธํ˜• UI๋กœ ๊ฐœ๋ฐœ ํŒจ๋Ÿฌ๋‹ค์ž„์˜ ์ „ํ™˜
โœ“
DataStore โ€” SharedPreferences์˜ ํ˜„๋Œ€์  ๋Œ€์•ˆ
โœ“
Paging 3 โ€” ๋Œ€์šฉ๋Ÿ‰ ๋ฐ์ดํ„ฐ์˜ ํšจ์œจ์ ์ธ ํŽ˜์ด์ง€๋„ค์ด์…˜
โœ“
CameraX โ€” ๋ณต์žกํ•œ ์นด๋ฉ”๋ผ API๋ฅผ ๋‹จ์ˆœํ•˜๊ฒŒ
โœ“
Android KTX โ€” Kotlin์˜ ํž˜์„ ์ตœ๋Œ€ํ•œ ํ™œ์šฉ
โœ… Jetpack ๋„์ž…์˜ ์‹ค์งˆ์  ํšจ๊ณผ

๊ฐœ๋ฐœ ์†๋„ ํ–ฅ์ƒ โ€” ๋ฐ˜๋ณต์ ์ธ ๋ณด์ผ๋Ÿฌํ”Œ๋ ˆ์ดํŠธ ์ฝ”๋“œ ๊ฐ์†Œ๋กœ ํ•ต์‹ฌ ๊ธฐ๋Šฅ ๊ฐœ๋ฐœ์— ์ง‘์ค‘ ๊ฐ€๋Šฅ
๋ฒ„๊ทธ ๊ฐ์†Œ โ€” ์ปดํŒŒ์ผ ํƒ€์ž„ ๊ฒ€์ฆ, ํƒ€์ž… ์•ˆ์ „์„ฑ์œผ๋กœ ๋Ÿฐํƒ€์ž„ ์˜ค๋ฅ˜ ์‚ฌ์ „ ๋ฐฉ์ง€
์œ ์ง€๋ณด์ˆ˜์„ฑ ํ–ฅ์ƒ โ€” ํ‘œ์ค€ํ™”๋œ ์•„ํ‚คํ…์ฒ˜๋กœ ํŒ€ ํ˜‘์—…๊ณผ ์ฝ”๋“œ ์ดํ•ด๋„ ํ–ฅ์ƒ
ํ…Œ์ŠคํŠธ ์šฉ์ด์„ฑ โ€” ์˜์กด์„ฑ ์ฃผ์ž…, ์ธ๋ฉ”๋ชจ๋ฆฌ DB ๋“ฑ์œผ๋กœ ๋‹จ์œ„ ํ…Œ์ŠคํŠธ ์ž‘์„ฑ์ด ์‰ฌ์›Œ์ง
์„ฑ๋Šฅ ์ตœ์ ํ™” โ€” ์ƒ๋ช…์ฃผ๊ธฐ ์ธ์‹, ํšจ์œจ์ ์ธ ๋ฐ์ดํ„ฐ ์ฒ˜๋ฆฌ๋กœ ์•ฑ ์„ฑ๋Šฅ ํ–ฅ์ƒ

์•ˆ๋“œ๋กœ์ด๋“œ ๊ฐœ๋ฐœ์€ ๊ณ„์† ๋ฐœ์ „ํ•˜๊ณ  ์žˆ๊ณ , Jetpack๋„ ํ•จ๊ป˜ ์ง„ํ™”ํ•˜๊ณ  ์žˆ์–ด.
์ฒ˜์Œ์—๋Š” ๋ฐฐ์šธ ๊ฒŒ ๋งŽ์•„์„œ ๋ถ€๋‹ด์Šค๋Ÿฌ์šธ ์ˆ˜ ์žˆ์ง€๋งŒ, ํ•˜๋‚˜์”ฉ ์ตํ˜€๊ฐ€๋‹ค ๋ณด๋ฉด ์–ด๋А ์ˆœ๊ฐ„ "์ด๊ฒŒ ์—†์œผ๋ฉด ์–ด๋–ป๊ฒŒ ๊ฐœ๋ฐœํ–ˆ์ง€?"๋ผ๋Š” ์ƒ๊ฐ์ด ๋“ค ๊ฑฐ์•ผ. ๐Ÿ˜„

์ค‘์š”ํ•œ ๊ฑด ์™„๋ฒฝํ•˜๊ฒŒ ๋‹ค ์•Œ๊ณ  ์‹œ์ž‘ํ•˜๋ ค ํ•˜์ง€ ๋ง๊ณ , ์ผ๋‹จ ์จ๋ณด๋ฉด์„œ ๋ฐฐ์šฐ๋Š” ๊ฒƒ์ด์•ผ.
๊ณต์‹ ์ฝ”๋“œ๋žฉ๋ถ€ํ„ฐ ์‹œ์ž‘ํ•ด์„œ ์‹ค์ œ ํ”„๋กœ์ ํŠธ์— ํ•˜๋‚˜์”ฉ ์ ์šฉํ•ด๋ด. ๊ทธ๊ฒŒ ๊ฐ€์žฅ ๋น ๋ฅธ ํ•™์Šต ๋ฐฉ๋ฒ•์ด์•ผ! ๐Ÿš€

๐Ÿ’ก ๋งˆ์ง€๋ง‰ ๊ฟ€ํŒ!

Android Studio์˜ New Project ํ…œํ”Œ๋ฆฟ์„ ํ™œ์šฉํ•ด๋ด.
"Empty Compose Activity"๋‚˜ "Navigation Drawer Activity" ๊ฐ™์€ ํ…œํ”Œ๋ฆฟ๋“ค์ด Jetpack ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋“ค์„ ๊ธฐ๋ณธ์œผ๋กœ ํฌํ•จํ•˜๊ณ  ์žˆ์–ด์„œ, ์ฒ˜์Œ ์‹œ์ž‘ํ•  ๋•Œ ๊ตฌ์กฐ๋ฅผ ์ฐธ๊ณ ํ•˜๊ธฐ ์ข‹์•„.
๊ทธ๋ฆฌ๊ณ  Android Studio์˜ Gemini AI ๊ธฐ๋Šฅ๋„ Jetpack ์ฝ”๋“œ ์ž‘์„ฑ์— ํฐ ๋„์›€์ด ๋ผ! ๐Ÿค–

์ž, ์ด์ œ Jetpack์œผ๋กœ ๋” ๋น ๋ฅด๊ณ , ๋” ์•ˆ์ •์ ์ด๊ณ , ๋” ์œ ์ง€๋ณด์ˆ˜ํ•˜๊ธฐ ์‰ฌ์šด ์•ฑ์„ ๋งŒ๋“ค์–ด๋ด! ๐Ÿ’ช
๊ถ๊ธˆํ•œ ์ ์ด ์žˆ์œผ๋ฉด ๊ตฌ๊ธ€ ๊ณต์‹ ๋ฌธ์„œ๋‚˜ Android Developers YouTube ์ฑ„๋„์„ ์ฐธ๊ณ ํ•ด.
๊ทธ๋ฆฌ๊ณ  ๊ฐ™์€ ๊ณ ๋ฏผ์„ ํ•˜๋Š” ๊ฐœ๋ฐœ์ž๋“ค๊ณผ ์ง€์‹์„ ๋‚˜๋ˆ„๊ณ  ์‹ถ๋‹ค๋ฉด, ์žฌ๋Šฅ๋„ท ๊ฐ™์€ ํ”Œ๋žซํผ์—์„œ ์„œ๋กœ์˜ ๊ฒฝํ—˜์„ ๊ณต์œ ํ•˜๋Š” ๊ฒƒ๋„ ์ •๋ง ์ข‹์€ ๋ฐฉ๋ฒ•์ด์•ผ! ๐ŸŒŸ

๋Œ“๊ธ€ ์ž‘์„ฑ

์ด ๊ธ€์— ๋Œ€ํ•œ ์—ฌ๋Ÿฌ๋ถ„์˜ ์ƒ๊ฐ์„ ๋“ค๋ ค์ฃผ์„ธ์š”

๋Œ“๊ธ€ 0
๋Œ“๊ธ€์„ ๋ถˆ๋Ÿฌ์˜ค๋Š” ์ค‘์ž…๋‹ˆ๋‹ค.

์•„์ง ๋Œ“๊ธ€์ด ์—†์Šต๋‹ˆ๋‹ค.