์ฝ˜ํ…์ธ  ๋Œ€ํ‘œ ์ด๋ฏธ์ง€ - ๐Ÿ“ฑ ์•ฑ ์„ฑ๋Šฅ ์ตœ์ ํ™”

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

๋ฉ”๋ชจ๋ฆฌ ๊ด€๋ฆฌ & ๋ฐฐํ„ฐ๋ฆฌ ์†Œ๋ชจ ์ตœ์†Œํ™” ์™„์ „ ์ •๋ณต

๐Ÿš€ ์ง„์งœ ๊ฐœ๋ฐœ์ž๋ผ๋ฉด ์•Œ์•„์•ผ ํ•  ํผํฌ๋จผ์Šค ํŠœ๋‹์˜ ๋ชจ๋“  ๊ฒƒ

์•ผ ์†”์งํžˆ ๋งํ•ด๋ด ๐Ÿ˜…
์•ฑ ๋งŒ๋“ค๊ณ  ๋‚˜์„œ "์™œ ์ด๋ ‡๊ฒŒ ๋ฐฐํ„ฐ๋ฆฌ ๋นจ๋ฆฌ ๋‹ณ์•„?" "์™œ ์•ฑ์ด ๋ฒ„๋ฒ…๊ฑฐ๋ ค?" ์ด๋Ÿฐ ๋ง ๋“ค์–ด๋ณธ ์  ์žˆ์ง€ ์•Š์•„?
ใ…‹ใ…‹ใ…‹ ๊ฐœ๋ฐœ์ž๋ผ๋ฉด ํ•œ ๋ฒˆ์ฏค์€ ๋‹ค ๊ฒช์–ด๋ดค์„ ๊ทธ ์ฐํ•œ ๊ฒฝํ—˜... ์˜ค๋Š˜์€ ๊ทธ ๊ณ ํ†ต์—์„œ ํ•ด๋ฐฉ๋˜๋Š” ๋‚ ์ด์•ผ!

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


์•ฑ ์„ฑ๋Šฅ ์ตœ์ ํ™”์˜ 3๋Œ€ ํ•ต์‹ฌ ์˜์—ญ ์ด ์„ธ ๊ฐ€์ง€๋ฅผ ์žก์œผ๋ฉด ์•ฑ์ด ๋‹ฌ๋ผ์ง„๋‹ค ๐Ÿง  ๋ฉ”๋ชจ๋ฆฌ ๊ด€๋ฆฌ Memory Management โ€ข ๋ฉ”๋ชจ๋ฆฌ ๋ˆ„์ˆ˜ ๋ฐฉ์ง€ โ€ข ํž™ ์ตœ์ ํ™” โ€ข ๊ฐ์ฒด ํ’€๋ง โ€ข GC ํŠœ๋‹ ์•ฑ ์•ˆ์ •์„ฑ์˜ ํ•ต์‹ฌ ๐Ÿ”‹ ๋ฐฐํ„ฐ๋ฆฌ ์ตœ์ ํ™” Battery Optimization โ€ข ๋ฐฑ๊ทธ๋ผ์šด๋“œ ์ž‘์—… ์ œ์–ด โ€ข ๋„คํŠธ์›Œํฌ ์ตœ์ ํ™” โ€ข ์„ผ์„œ ์‚ฌ์šฉ ์ตœ์†Œํ™” โ€ข Doze ๋ชจ๋“œ ๋Œ€์‘ ์‚ฌ์šฉ์ž ๋งŒ์กฑ์˜ ํ•ต์‹ฌ โšก ๋ Œ๋”๋ง ์ตœ์ ํ™” Rendering Optimization โ€ข 60fps ์œ ์ง€ โ€ข ๋ ˆ์ด์•„์›ƒ ์ตœ์ ํ™” โ€ข ์ด๋ฏธ์ง€ ์บ์‹ฑ โ€ข ๋น„๋™๊ธฐ ์ฒ˜๋ฆฌ UX ํ’ˆ์งˆ์˜ ํ•ต์‹ฌ โ†’ โ†’

๐Ÿง  Part 1. ๋ฉ”๋ชจ๋ฆฌ ๊ด€๋ฆฌ โ€” ์•ฑ์ด ํ„ฐ์ง€๋Š” ์ง„์งœ ์ด์œ 

ใ…‹ใ…‹ใ…‹ ์•ฑ์ด ๊ฐ‘์ž๊ธฐ ๊บผ์ง€๋Š” ๊ฒฝํ—˜... ๊ฐœ๋ฐœ์ž๋„ ์œ ์ €๋„ ๋‹ค ์‹ซ์–ดํ•˜์ž–์•„.
๊ทผ๋ฐ ์ด๊ฒŒ ๋Œ€๋ถ€๋ถ„ ๋ฉ”๋ชจ๋ฆฌ ๋ฌธ์ œ์•ผ. ํŠนํžˆ ๋ชจ๋ฐ”์ผ ํ™˜๊ฒฝ์—์„œ๋Š” ๋ฐ์Šคํฌํƒ‘์ด๋ž‘ ๋‹ฌ๋ฆฌ ๋ฉ”๋ชจ๋ฆฌ๊ฐ€ ์ œํ•œ์ ์ด๊ฑฐ๋“ .
Android๋Š” ๋ณดํ†ต ์•ฑ๋‹น 256MB~512MB, iOS๋Š” ๋” ์—„๊ฒฉํ•˜๊ฒŒ ๊ด€๋ฆฌํ•ด. ์ด ํ•œ๊ณ„๋ฅผ ๋„˜์œผ๋ฉด? ์•ฑ์ด ๊ทธ๋ƒฅ ์ฃฝ์–ด๋ฒ„๋ ค ๐Ÿ˜ฑ

๐Ÿ“Œ ๋ฉ”๋ชจ๋ฆฌ ๋ˆ„์ˆ˜(Memory Leak)๋ž€ ๋ญ”๊ฐ€์š”?

๋ฉ”๋ชจ๋ฆฌ ๋ˆ„์ˆ˜๋Š” ์‰ฝ๊ฒŒ ๋งํ•˜๋ฉด ๋” ์ด์ƒ ํ•„์š” ์—†๋Š” ๊ฐ์ฒด๊ฐ€ ๋ฉ”๋ชจ๋ฆฌ์—์„œ ํ•ด์ œ๋˜์ง€ ์•Š๊ณ  ๊ณ„์† ๋‚จ์•„์žˆ๋Š” ํ˜„์ƒ์ด์•ผ.
GC(Garbage Collector)๊ฐ€ ์žˆ์–ด๋„ ์ฐธ์กฐ๊ฐ€ ์‚ด์•„์žˆ์œผ๋ฉด ๋ชป ์ง€์šฐ๊ฑฐ๋“ . ์ด๊ฒŒ ์Œ“์ด๋ฉด ์•ฑ์ด ์ ์  ๋А๋ ค์ง€๋‹ค๊ฐ€ ๊ฒฐ๊ตญ OOM(Out Of Memory) ํฌ๋ž˜์‹œ๊ฐ€ ๋ฐœ์ƒํ•ด.

๐Ÿง‘โ€๐Ÿ’ป "์•ผ ๊ทผ๋ฐ GC๊ฐ€ ์•Œ์•„์„œ ์ •๋ฆฌํ•ด์ฃผ๋Š” ๊ฑฐ ์•„๋‹ˆ์•ผ?"
๋งž์•„, ๊ทผ๋ฐ GC๋Š” ์ฐธ์กฐ๊ฐ€ ์—†๋Š” ๊ฐ์ฒด๋งŒ ์ •๋ฆฌํ•ด์ค˜.
์ฐธ์กฐ๊ฐ€ ์‚ด์•„์žˆ์œผ๋ฉด GC๋„ ์† ๋ชป ๋Œ€๊ฑฐ๋“  ใ…‹ใ…‹

๋Œ€ํ‘œ์ ์ธ ๋ฉ”๋ชจ๋ฆฌ ๋ˆ„์ˆ˜ ํŒจํ„ด๋“ค์„ ์•Œ์•„๋ณด์ž!

๐Ÿ”ด Android์—์„œ ํ”ํ•œ ๋ฉ”๋ชจ๋ฆฌ ๋ˆ„์ˆ˜ ํŒจํ„ด

ํŒจํ„ด 1

Activity/Fragment Context ์ž˜๋ชป ์‚ฌ์šฉ
๊ฐ€์žฅ ํ”ํ•œ ์‹ค์ˆ˜์•ผ. Static ๋ณ€์ˆ˜๋‚˜ ์‹ฑ๊ธ€ํ†ค์— Activity Context๋ฅผ ์ €์žฅํ•˜๋ฉด Activity๊ฐ€ ์ข…๋ฃŒ๋ผ๋„ ๋ฉ”๋ชจ๋ฆฌ์—์„œ ํ•ด์ œ๊ฐ€ ์•ˆ ๋ผ.

// โŒ ์ด๋ ‡๊ฒŒ ํ•˜๋ฉด ์•ˆ ๋จ - Activity ๋ฉ”๋ชจ๋ฆฌ ๋ˆ„์ˆ˜ ๋ฐœ์ƒ
class MySingleton {
    companion object {
        var context: Context? = null // Activity Context ์ €์žฅ ๊ธˆ์ง€!
    }
}

// โœ… ์ด๋ ‡๊ฒŒ ํ•ด์•ผ ํ•จ - Application Context ์‚ฌ์šฉ
class MySingleton {
    companion object {
        lateinit var appContext: Context
        
        fun init(context: Context) {
            appContext = context.applicationContext // Application Context ์‚ฌ์šฉ!
        }
    }
}
ํŒจํ„ด 2

Inner Class์™€ Handler ๋ฌธ์ œ
Handler๋ฅผ ์ต๋ช… ๋‚ด๋ถ€ ํด๋ž˜์Šค๋กœ ๋งŒ๋“ค๋ฉด ์™ธ๋ถ€ ํด๋ž˜์Šค(Activity)์— ๋Œ€ํ•œ ์•”๋ฌต์  ์ฐธ์กฐ๊ฐ€ ์ƒ๊ฒจ์„œ ๋ˆ„์ˆ˜๊ฐ€ ๋ฐœ์ƒํ•ด.

// โŒ ์œ„ํ—˜ํ•œ ์ฝ”๋“œ
class MyActivity : AppCompatActivity() {
    private val handler = object : Handler(Looper.getMainLooper()) {
        override fun handleMessage(msg: Message) {
            // ์—ฌ๊ธฐ์„œ Activity ์ฐธ์กฐ ๋ฐœ์ƒ โ†’ ๋ˆ„์ˆ˜!
            updateUI()
        }
    }
}

// โœ… WeakReference ์‚ฌ์šฉ์œผ๋กœ ํ•ด๊ฒฐ
class MyHandler(activity: MyActivity) : Handler(Looper.getMainLooper()) {
    private val weakActivity = WeakReference(activity)
    
    override fun handleMessage(msg: Message) {
        weakActivity.get()?.updateUI()
    }
}
ํŒจํ„ด 3

๋ฆฌ์Šค๋„ˆ/์ฝœ๋ฐฑ ๋“ฑ๋ก ํ›„ ํ•ด์ œ ์•ˆ ํ•จ
BroadcastReceiver, LocationListener, SensorListener ๋“ฑ๋กํ•˜๊ณ  ํ•ด์ œ ์•ˆ ํ•˜๋ฉด Activity๊ฐ€ ์ฃฝ์–ด๋„ ๊ณ„์† ์‚ด์•„์žˆ์–ด.

// โœ… ์˜ฌ๋ฐ”๋ฅธ ๋ฆฌ์Šค๋„ˆ ๊ด€๋ฆฌ
class MyActivity : AppCompatActivity() {
    private val locationListener = LocationListener { location ->
        // ์œ„์น˜ ์—…๋ฐ์ดํŠธ ์ฒ˜๋ฆฌ
    }
    
    override fun onResume() {
        super.onResume()
        locationManager.requestLocationUpdates(
            LocationManager.GPS_PROVIDER, 0, 0f, locationListener
        )
    }
    
    override fun onPause() {
        super.onPause()
        locationManager.removeUpdates(locationListener) // ๋ฐ˜๋“œ์‹œ ํ•ด์ œ!
    }
}

๐ŸŽ iOS์—์„œ ํ”ํ•œ ๋ฉ”๋ชจ๋ฆฌ ๋ˆ„์ˆ˜ ํŒจํ„ด

iOS๋Š” ARC(Automatic Reference Counting)๋ฅผ ์‚ฌ์šฉํ•˜๋Š”๋ฐ, ์ด๊ฒŒ ์™„๋ฒฝํ•œ ๊ฒŒ ์•„๋‹ˆ์•ผ.
๊ฐ•ํ•œ ์ฐธ์กฐ ์ˆœํ™˜(Strong Reference Cycle)์ด ์ƒ๊ธฐ๋ฉด ARC๋„ ํ•ด์ œ๋ฅผ ๋ชป ํ•ด.

// โŒ ๊ฐ•ํ•œ ์ฐธ์กฐ ์ˆœํ™˜ - ๋ฉ”๋ชจ๋ฆฌ ๋ˆ„์ˆ˜ ๋ฐœ์ƒ
class ViewController: UIViewController {
    var networkManager: NetworkManager?
    
    override func viewDidLoad() {
        super.viewDidLoad()
        networkManager = NetworkManager()
        networkManager?.completion = {
            self.updateUI() // self๋ฅผ ๊ฐ•ํ•˜๊ฒŒ ์ฐธ์กฐ โ†’ ์ˆœํ™˜ ์ฐธ์กฐ!
        }
    }
}

// โœ… [weak self] ๋˜๋Š” [unowned self] ์‚ฌ์šฉ
networkManager?.completion = { [weak self] in
    guard let self = self else { return }
    self.updateUI() // ์•ฝํ•œ ์ฐธ์กฐ๋กœ ์ˆœํ™˜ ์ฐธ์กฐ ๋ฐฉ์ง€
}

๐Ÿ’ก iOS ๋ฉ”๋ชจ๋ฆฌ ๊ด€๋ฆฌ ํŒ! weak vs unowned ์ฐจ์ด๋ฅผ ์•Œ์•„์•ผ ํ•ด!

weak: ์ฐธ์กฐ ๋Œ€์ƒ์ด nil์ด ๋  ์ˆ˜ ์žˆ์„ ๋•Œ ์‚ฌ์šฉ. Optional ํƒ€์ž….
unowned: ์ฐธ์กฐ ๋Œ€์ƒ์ด ์ ˆ๋Œ€ nil์ด ์•ˆ ๋œ๋‹ค๊ณ  ํ™•์‹ ํ•  ๋•Œ ์‚ฌ์šฉ. Non-optional. ์ž˜๋ชป ์“ฐ๋ฉด ํฌ๋ž˜์‹œ ๋ฐœ์ƒํ•˜๋‹ˆ ์ฃผ์˜!

๐Ÿ“Š ๋ฉ”๋ชจ๋ฆฌ ์ตœ์ ํ™” ํ•ต์‹ฌ ์ „๋žต๋“ค

1 ๊ฐ์ฒด ํ’€๋ง(Object Pooling)

์ž์ฃผ ์ƒ์„ฑ/์‚ญ์ œ๋˜๋Š” ๊ฐ์ฒด๋Š” ํ’€์—์„œ ์žฌ์‚ฌ์šฉํ•˜์ž. RecyclerView๊ฐ€ ๋ฐ”๋กœ ์ด ์›๋ฆฌ์•ผ!
๋งค๋ฒˆ ์ƒˆ ๊ฐ์ฒด๋ฅผ ๋งŒ๋“ค๋ฉด GC ์••๋ฐ•์ด ์‹ฌํ•ด์ง€๊ฑฐ๋“ .

// ๊ฐ์ฒด ํ’€ ๊ตฌํ˜„ ์˜ˆ์‹œ (Kotlin)
class ObjectPool<T>(
    private val maxSize: Int,
    private val factory: () -> T,
    private val reset: (T) -> Unit
) {
    private val pool = ArrayDeque<T>()
    
    fun acquire(): T {
        return if (pool.isEmpty()) {
            factory() // ํ’€์ด ๋น„์–ด์žˆ์œผ๋ฉด ์ƒˆ๋กœ ์ƒ์„ฑ
        } else {
            pool.removeFirst() // ํ’€์—์„œ ์žฌ์‚ฌ์šฉ
        }
    }
    
    fun release(obj: T) {
        if (pool.size < maxSize) {
            reset(obj) // ์ดˆ๊ธฐํ™” ํ›„ ํ’€์— ๋ฐ˜ํ™˜
            pool.addLast(obj)
        }
    }
}

2 ์ด๋ฏธ์ง€ ๋ฉ”๋ชจ๋ฆฌ ์ตœ์ ํ™”

์ด๋ฏธ์ง€๋Š” ๋ฉ”๋ชจ๋ฆฌ ๋จน๋Š” ํ•˜๋งˆ์•ผ ๐Ÿฆ› ใ…‹ใ…‹ใ…‹
4K ์ด๋ฏธ์ง€ ํ•˜๋‚˜๊ฐ€ ๋ฉ”๋ชจ๋ฆฌ์—์„œ ์ˆ˜์‹ญ MB๋ฅผ ์ฐจ์ง€ํ•  ์ˆ˜ ์žˆ์–ด. ๋ฐ˜๋“œ์‹œ ์ตœ์ ํ™”ํ•ด์•ผ ํ•ด!

// Android - BitmapFactory.Options๋กœ ์ด๋ฏธ์ง€ ๋‹ค์šด์ƒ˜ํ”Œ๋ง
fun decodeSampledBitmapFromResource(
    res: Resources,
    resId: Int,
    reqWidth: Int,
    reqHeight: Int
): Bitmap {
    return BitmapFactory.Options().run {
        inJustDecodeBounds = true
        BitmapFactory.decodeResource(res, resId, this)
        
        inSampleSize = calculateInSampleSize(this, reqWidth, reqHeight)
        inJustDecodeBounds = false
        
        BitmapFactory.decodeResource(res, resId, this)
    }
}

fun calculateInSampleSize(
    options: BitmapFactory.Options,
    reqWidth: Int,
    reqHeight: Int
): Int {
    val (height: Int, width: Int) = options.run { outHeight to outWidth }
    var inSampleSize = 1
    
    if (height > reqHeight || width > reqWidth) {
        val halfHeight: Int = height / 2
        val halfWidth: Int = width / 2
        
        while (halfHeight / inSampleSize >= reqHeight && 
               halfWidth / inSampleSize >= reqWidth) {
            inSampleSize *= 2
        }
    }
    return inSampleSize
}

3 ์บ์‹œ ์ „๋žต (LRU Cache)

LRU(Least Recently Used) ์บ์‹œ๋Š” ๊ฐ€์žฅ ์˜ค๋ž˜ ์‚ฌ์šฉ ์•ˆ ํ•œ ํ•ญ๋ชฉ๋ถ€ํ„ฐ ์ œ๊ฑฐํ•˜๋Š” ๋ฐฉ์‹์ด์•ผ.
๋ฉ”๋ชจ๋ฆฌ ํ•œ๊ณ„ ๋‚ด์—์„œ ์ตœ๋Œ€ํ•œ ํšจ์œจ์ ์œผ๋กœ ๋ฐ์ดํ„ฐ๋ฅผ ์บ์‹ฑํ•  ์ˆ˜ ์žˆ์–ด.

// Android LruCache ์‚ฌ์šฉ ์˜ˆ์‹œ
val maxMemory = (Runtime.getRuntime().maxMemory() / 1024).toInt()
val cacheSize = maxMemory / 8 // ์ „์ฒด ๋ฉ”๋ชจ๋ฆฌ์˜ 1/8 ์‚ฌ์šฉ

val memoryCache = object : LruCache<String, Bitmap>(cacheSize) {
    override fun sizeOf(key: String, bitmap: Bitmap): Int {
        return bitmap.byteCount / 1024
    }
}

// ์บ์‹œ์— ์ €์žฅ
fun addBitmapToMemoryCache(key: String, bitmap: Bitmap) {
    if (getBitmapFromMemCache(key) == null) {
        memoryCache.put(key, bitmap)
    }
}

// ์บ์‹œ์—์„œ ๊ฐ€์ ธ์˜ค๊ธฐ
fun getBitmapFromMemCache(key: String): Bitmap? {
    return memoryCache.get(key)
}

๋ฉ”๋ชจ๋ฆฌ ๋ˆ„์ˆ˜ ๋ฐœ์ƒ ๊ณผ์ • ์‹œ๊ฐํ™” ์ •์ƒ 50MB ๋ˆ„์ˆ˜์‹œ์ž‘ 70MB ๋ˆ„์ ์ค‘ 95MB ์œ„ํ—˜ 120MB ์ž„๊ณ„์  150MB ๐Ÿ’ฅ OOM! ํฌ๋ž˜์‹œ ์ตœ์ ํ™”ํ›„ 50MB โœ“ ์‹œ๊ฐ„ ํ๋ฆ„ โ†’ ์ •์ƒ ๋ฒ”์œ„ ์œ„ํ—˜ ๊ตฌ๊ฐ„ ์ตœ์ ํ™” ํ›„ ๋ฉ”๋ชจ๋ฆฌ ๋ˆ„์ˆ˜๋ฅผ ๋ฐฉ์น˜ํ•˜๋ฉด ์•ฑ์ด ์ฃฝ๋Š”๋‹ค. ๋ฐ˜๋“œ์‹œ ์žก์•„์•ผ ํ•ด!

๐Ÿ› ๏ธ ๋ฉ”๋ชจ๋ฆฌ ํ”„๋กœํŒŒ์ผ๋ง ๋„๊ตฌ

์ฝ”๋“œ๋งŒ ์ž˜ ์งœ๋Š” ๊ฒŒ ์•„๋‹ˆ๋ผ ์‹ค์ œ๋กœ ๋ฉ”๋ชจ๋ฆฌ๊ฐ€ ์–ด๋–ป๊ฒŒ ์‚ฌ์šฉ๋˜๋Š”์ง€ ์ธก์ •ํ•ด์•ผ ํ•ด.
๋„๊ตฌ ์—†์ด ๊ฐ์œผ๋กœ ์ตœ์ ํ™”ํ•˜๋Š” ๊ฑด ๋ˆˆ ๊ฐ๊ณ  ์šด์ „ํ•˜๋Š” ๊ฑฐ๋ž‘ ๊ฐ™์•„ ใ…‹ใ…‹ใ…‹

ํ”Œ๋žซํผ ๋„๊ตฌ๋ช… ์ฃผ์š” ๊ธฐ๋Šฅ
Android Android Studio Profiler ํž™ ๋คํ”„, ๋ฉ”๋ชจ๋ฆฌ ํƒ€์ž„๋ผ์ธ, ๋ˆ„์ˆ˜ ๊ฐ์ง€
Android LeakCanary ์ž๋™ ๋ฉ”๋ชจ๋ฆฌ ๋ˆ„์ˆ˜ ๊ฐ์ง€ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ
iOS Xcode Instruments Allocations, Leaks ๋ถ„์„
iOS Memory Graph Debugger ์ฐธ์กฐ ๊ด€๊ณ„ ์‹œ๊ฐํ™”
๊ณตํ†ต Firebase Performance ์‹ค์‚ฌ์šฉ ํ™˜๊ฒฝ ๋ฉ”๋ชจ๋ฆฌ ๋ชจ๋‹ˆํ„ฐ๋ง

๐Ÿ’ก LeakCanary ์„ค์ • ์ดˆ๊ฐ„๋‹จ! Android ๊ฐœ๋ฐœ์ž๋ผ๋ฉด LeakCanary๋Š” ํ•„์ˆ˜์•ผ. ์„ค์ •๋„ ์—„์ฒญ ์‰ฌ์›Œ.

// build.gradle์— ์ถ”๊ฐ€
dependencies {
    debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.12'
}

// ๋! ๋ณ„๋„ ์ดˆ๊ธฐํ™” ์ฝ”๋“œ ์—†์ด ์ž๋™์œผ๋กœ ๋ˆ„์ˆ˜ ๊ฐ์ง€ํ•ด์คŒ ใ„ทใ„ท

๐Ÿ”‹ Part 2. ๋ฐฐํ„ฐ๋ฆฌ ์†Œ๋ชจ ์ตœ์†Œํ™” โ€” ์œ ์ €๊ฐ€ ์•ฑ ์ง€์šฐ๋Š” ์ด์œ 

๋ฐฐํ„ฐ๋ฆฌ ๋งŽ์ด ๋จน๋Š” ์•ฑ์€ ์œ ์ €๊ฐ€ ๋ฐ”๋กœ ์ง€์›Œ๋ฒ„๋ ค ๐Ÿ˜ค
์‹ค์ œ๋กœ ๊ตฌ๊ธ€ ํ”Œ๋ ˆ์ด์Šคํ† ์–ด ๋ฆฌ๋ทฐ ๋ณด๋ฉด "๋ฐฐํ„ฐ๋ฆฌ ๋„ˆ๋ฌด ๋งŽ์ด ๋จน์–ด์š”" ์ด๋Ÿฐ ๋ฆฌ๋ทฐ ์—„์ฒญ ๋งŽ์ž–์•„.
๋ฐฐํ„ฐ๋ฆฌ ์ตœ์ ํ™”๋Š” ๋‹จ์ˆœํžˆ ์ฐฉํ•œ ๊ฐœ๋ฐœ์ž๊ฐ€ ๋˜๋Š” ๊ฒŒ ์•„๋‹ˆ๋ผ ์•ฑ ์ƒ์กด์˜ ๋ฌธ์ œ์•ผ!

โšก ๋ฐฐํ„ฐ๋ฆฌ๋ฅผ ๊ฐ€์žฅ ๋งŽ์ด ๋จน๋Š” ์ฃผ๋ฒ”๋“ค

๐Ÿฅ‡ 1์œ„: ๋„คํŠธ์›Œํฌ ํ†ต์‹  โ€” ๋ฌด์„  ๋ผ๋””์˜ค(WiFi/LTE) ํ™œ์„ฑํ™” ์ž์ฒด๊ฐ€ ์—„์ฒญ๋‚œ ์ „๋ ฅ ์†Œ๋ชจ
๐Ÿฅˆ 2์œ„: GPS/์œ„์น˜ ์„œ๋น„์Šค โ€” GPS ์นฉ ๊ตฌ๋™์€ ๋ฐฐํ„ฐ๋ฆฌ ํ‚ฌ๋Ÿฌ
๐Ÿฅ‰ 3์œ„: ํ™”๋ฉด ๋ฐ๊ธฐ/๋””์Šคํ”Œ๋ ˆ์ด โ€” ์•ฑ์ด ํ™”๋ฉด์„ ๊ณ„์† ์ผœ๋‘๋ฉด ๋ฐฐํ„ฐ๋ฆฌ ๊ธ‰๊ฐ
4์œ„: ๋ฐฑ๊ทธ๋ผ์šด๋“œ ์ž‘์—… โ€” ์•ฑ์ด ๊บผ์ง„ ๊ฒƒ ๊ฐ™์•„๋„ ๋’ค์—์„œ ๋Œ์•„๊ฐ€๋Š” ์ž‘์—…๋“ค
5์œ„: ์„ผ์„œ ๋‚จ์šฉ โ€” ๊ฐ€์†๋„๊ณ„, ์ž์ด๋กœ์Šค์ฝ”ํ”„ ๋“ฑ ๋ถˆํ•„์š”ํ•œ ์„ผ์„œ ์‚ฌ์šฉ

๐Ÿ“ก ๋„คํŠธ์›Œํฌ ์ตœ์ ํ™” โ€” ๋ฐฐํ„ฐ๋ฆฌ์˜ ์ตœ๋Œ€ ์ 

๋„คํŠธ์›Œํฌ ์š”์ฒญ ํ•˜๋‚˜ ๋ณด๋‚ผ ๋•Œ๋งˆ๋‹ค ๋ฌด์„  ๋ผ๋””์˜ค๊ฐ€ ์ผœ์ง€๋Š”๋ฐ, ์ด๊ฒŒ ๋ฐฐํ„ฐ๋ฆฌ๋ฅผ ์—„์ฒญ ๋จน์–ด.
๋” ์‹ฌ๊ฐํ•œ ๊ฑด ๋ผ๋””์˜ค๊ฐ€ ์ผœ์ง„ ํ›„ ์•ฝ 20~30์ดˆ ๋™์•ˆ ํ™œ์„ฑ ์ƒํƒœ๋ฅผ ์œ ์ง€ํ•œ๋‹ค๋Š” ๊ฑฐ์•ผ.
์ด๊ฑธ "Tail Time"์ด๋ผ๊ณ  ํ•˜๋Š”๋ฐ, ์ž‘์€ ์š”์ฒญ ์—ฌ๋Ÿฌ ๋ฒˆ ๋ณด๋‚ด๋Š” ๊ฒŒ ํฐ ์š”์ฒญ ํ•œ ๋ฒˆ๋ณด๋‹ค ํ›จ์”ฌ ๋ฐฐํ„ฐ๋ฆฌ๋ฅผ ๋” ๋จน์–ด!

// โŒ ๋น„ํšจ์œจ์ ์ธ ๋„คํŠธ์›Œํฌ ์‚ฌ์šฉ - ์—ฌ๋Ÿฌ ๋ฒˆ ๋‚˜๋ˆ ์„œ ์š”์ฒญ
fun loadUserData() {
    fetchUserProfile()      // ์š”์ฒญ 1
    fetchUserPosts()        // ์š”์ฒญ 2  
    fetchUserFollowers()    // ์š”์ฒญ 3
    // ๋ผ๋””์˜ค๊ฐ€ 3๋ฒˆ ์ผœ์กŒ๋‹ค ๊บผ์ง โ†’ ๋ฐฐํ„ฐ๋ฆฌ ๋‚ญ๋น„!
}

// โœ… ๋ฐฐ์น˜ ์š”์ฒญ์œผ๋กœ ์ตœ์ ํ™”
fun loadUserDataOptimized() {
    // GraphQL์ด๋‚˜ REST API ๋ฐฐ์น˜ ์—”๋“œํฌ์ธํŠธ ํ™œ์šฉ
    fetchAllUserData() // ํ•œ ๋ฒˆ์˜ ์š”์ฒญ์œผ๋กœ ๋ชจ๋“  ๋ฐ์ดํ„ฐ ๊ฐ€์ ธ์˜ค๊ธฐ
    // ๋ผ๋””์˜ค๊ฐ€ 1๋ฒˆ๋งŒ ์ผœ์ง โ†’ ๋ฐฐํ„ฐ๋ฆฌ ์ ˆ์•ฝ!
}

1 WorkManager๋กœ ๋ฐฑ๊ทธ๋ผ์šด๋“œ ์ž‘์—… ์ตœ์ ํ™” (Android)

WorkManager๋Š” Android์˜ ๊ณต์‹ ๋ฐฑ๊ทธ๋ผ์šด๋“œ ์ž‘์—… ์Šค์ผ€์ค„๋Ÿฌ์•ผ.
๋ฐฐํ„ฐ๋ฆฌ ์ตœ์ ํ™”, Doze ๋ชจ๋“œ, ์•ฑ ์ข…๋ฃŒ ๋“ฑ ๋‹ค์–‘ํ•œ ์ƒํ™ฉ์„ ์ž๋™์œผ๋กœ ์ฒ˜๋ฆฌํ•ด์ค˜.

// WorkManager ์‚ฌ์šฉ ์˜ˆ์‹œ
class SyncWorker(
    context: Context,
    params: WorkerParameters
) : CoroutineWorker(context, params) {
    
    override suspend fun doWork(): Result {
        return try {
            syncDataWithServer()
            Result.success()
        } catch (e: Exception) {
            Result.retry()
        }
    }
}

// ์ œ์•ฝ ์กฐ๊ฑด ์„ค์ • - ๋ฐฐํ„ฐ๋ฆฌ ์ตœ์ ํ™”์˜ ํ•ต์‹ฌ!
val constraints = Constraints.Builder()
    .setRequiredNetworkType(NetworkType.CONNECTED) // ๋„คํŠธ์›Œํฌ ์—ฐ๊ฒฐ ์‹œ์—๋งŒ
    .setRequiresBatteryNotLow(true)                // ๋ฐฐํ„ฐ๋ฆฌ ๋ถ€์กฑ ์‹œ ์‹คํ–‰ ์•ˆ ํ•จ
    .setRequiresCharging(false)                    // ์ถฉ์ „ ์ค‘ ์•„๋‹ˆ์–ด๋„ OK
    .build()

val syncRequest = PeriodicWorkRequestBuilder<SyncWorker>(
    repeatInterval = 15,
    repeatIntervalTimeUnit = TimeUnit.MINUTES
)
    .setConstraints(constraints)
    .build()

WorkManager.getInstance(context).enqueueUniquePeriodicWork(
    "data_sync",
    ExistingPeriodicWorkPolicy.KEEP,
    syncRequest
)

2 Doze ๋ชจ๋“œ์™€ App Standby ๋Œ€์‘ (Android)

โš ๏ธ Doze ๋ชจ๋“œ ์•Œ๊ณ  ์žˆ์–ด? Android 6.0(Marshmallow)๋ถ€ํ„ฐ ๋„์ž…๋œ ๊ธฐ๋Šฅ์œผ๋กœ, ๊ธฐ๊ธฐ๊ฐ€ ์ผ์ • ์‹œ๊ฐ„ ์‚ฌ์šฉ๋˜์ง€ ์•Š์œผ๋ฉด ๋ฐฐํ„ฐ๋ฆฌ ์ ˆ์•ฝ์„ ์œ„ํ•ด ๋ฐฑ๊ทธ๋ผ์šด๋“œ ์ž‘์—…์„ ์ œํ•œํ•ด.
์ด ๋ชจ๋“œ์—์„œ๋Š” ๋„คํŠธ์›Œํฌ ์ ‘๊ทผ, ์•Œ๋žŒ, GPS, ๋™๊ธฐํ™” ๋“ฑ์ด ๋ชจ๋‘ ์ œํ•œ๋ผ!
WorkManager๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ์ด๋Ÿฐ ์ œ์•ฝ์„ ์ž๋™์œผ๋กœ ์ฒ˜๋ฆฌํ•ด์ฃผ๋‹ˆ๊นŒ ๊ผญ ์‚ฌ์šฉํ•˜์ž.

3 iOS Background Fetch ์ตœ์ ํ™”

// iOS - Background App Refresh ์ตœ์ ํ™”
class AppDelegate: UIResponder, UIApplicationDelegate {
    
    func application(
        _ application: UIApplication,
        performFetchWithCompletionHandler completionHandler: 
        @escaping (UIBackgroundFetchResult) -> Void
    ) {
        // ๋ฐฑ๊ทธ๋ผ์šด๋“œ์—์„œ ๋ฐ์ดํ„ฐ ๊ฐ€์ ธ์˜ค๊ธฐ
        fetchLatestData { result in
            switch result {
            case .success(let hasNewData):
                // ์ƒˆ ๋ฐ์ดํ„ฐ๊ฐ€ ์žˆ์„ ๋•Œ๋งŒ newData ๋ฐ˜ํ™˜
                completionHandler(hasNewData ? .newData : .noData)
            case .failure:
                completionHandler(.failed)
            }
        }
    }
}

// Info.plist์— UIBackgroundModes ์„ค์ • ํ•„์š”
// fetch ๋ชจ๋“œ ์ถ”๊ฐ€ํ•ด์•ผ ํ•จ

๐Ÿ“ ์œ„์น˜ ์„œ๋น„์Šค ์ตœ์ ํ™” โ€” GPS๋Š” ๋ฐฐํ„ฐ๋ฆฌ ํ‚ฌ๋Ÿฌ

GPS๋Š” ์ •๋ง ๋ฐฐํ„ฐ๋ฆฌ๋ฅผ ๋งŽ์ด ๋จน์–ด. ๊ทผ๋ฐ ๋งŽ์€ ์•ฑ๋“ค์ด ํ•„์š” ์ด์ƒ์œผ๋กœ ์ •๋ฐ€ํ•œ ์œ„์น˜๋ฅผ ์š”์ฒญํ•˜๊ฑฐ๋“ .
์•ฑ์˜ ๋ชฉ์ ์— ๋งž๋Š” ์ตœ์†Œํ•œ์˜ ์ •ํ™•๋„๋งŒ ์š”์ฒญํ•˜๋Š” ๊ฒŒ ํ•ต์‹ฌ์ด์•ผ!

// Android - ์œ„์น˜ ์ •ํ™•๋„ ๋ ˆ๋ฒจ ์„ ํƒ
// ๋ฐฐํ„ฐ๋ฆฌ ์†Œ๋ชจ ์ˆœ์„œ: HIGH_ACCURACY > BALANCED > LOW_POWER > NO_POWER

// โŒ ํ•ญ์ƒ ์ตœ๊ณ  ์ •ํ™•๋„ ์š”์ฒญ (๋ฐฐํ„ฐ๋ฆฌ ๋‚ญ๋น„)
val locationRequest = LocationRequest.create().apply {
    priority = LocationRequest.PRIORITY_HIGH_ACCURACY
    interval = 1000 // 1์ดˆ๋งˆ๋‹ค ์—…๋ฐ์ดํŠธ
}

// โœ… ์šฉ๋„์— ๋งž๋Š” ์ •ํ™•๋„ ์„ ํƒ
val locationRequest = LocationRequest.Builder(
    Priority.PRIORITY_BALANCED_POWER_ACCURACY, // ๊ท ํ˜• ์žกํžŒ ์ •ํ™•๋„
    10000L // 10์ดˆ๋งˆ๋‹ค ์—…๋ฐ์ดํŠธ
).apply {
    setMinUpdateIntervalMillis(5000L) // ์ตœ์†Œ 5์ดˆ ๊ฐ„๊ฒฉ
    setMaxUpdateDelayMillis(30000L)   // ์ตœ๋Œ€ 30์ดˆ ์ง€์—ฐ ํ—ˆ์šฉ
}.build()
// iOS - ์œ„์น˜ ์„œ๋น„์Šค ์ตœ์ ํ™”
class LocationManager: NSObject, CLLocationManagerDelegate {
    private let locationManager = CLLocationManager()
    
    func setupLocationManager() {
        locationManager.delegate = self
        
        // ์ •ํ™•๋„ ์„ค์ • - ์•ฑ ๋ชฉ์ ์— ๋งž๊ฒŒ ์„ ํƒ
        // kCLLocationAccuracyBest โ†’ ๋ฐฐํ„ฐ๋ฆฌ ๋งŽ์ด ๋จน์Œ
        // kCLLocationAccuracyHundredMeters โ†’ ์ ๋‹นํ•œ ๊ท ํ˜•
        // kCLLocationAccuracyKilometer โ†’ ๋ฐฐํ„ฐ๋ฆฌ ์ ˆ์•ฝ
        locationManager.desiredAccuracy = kCLLocationAccuracyHundredMeters
        
        // ์ด๋™ ๊ฑฐ๋ฆฌ ํ•„ํ„ฐ - ์ตœ์†Œ ์ด๋™ ๊ฑฐ๋ฆฌ ์ด์ƒ์ผ ๋•Œ๋งŒ ์—…๋ฐ์ดํŠธ
        locationManager.distanceFilter = 50 // 50๋ฏธํ„ฐ ์ด์ƒ ์ด๋™ ์‹œ์—๋งŒ ์—…๋ฐ์ดํŠธ
    }
    
    // ๋ฐฑ๊ทธ๋ผ์šด๋“œ์—์„œ๋Š” significant location change๋งŒ ์‚ฌ์šฉ
    func startMonitoringSignificantChanges() {
        locationManager.startMonitoringSignificantLocationChanges()
        // ์ผ๋ฐ˜ ์—…๋ฐ์ดํŠธ๋ณด๋‹ค ํ›จ์”ฌ ๋ฐฐํ„ฐ๋ฆฌ ์ ˆ์•ฝ!
    }
}

๐ŸŒ™ ํ™”๋ฉด ๊ด€๋ จ ๋ฐฐํ„ฐ๋ฆฌ ์ตœ์ ํ™”

ํ™”๋ฉด์€ ์Šค๋งˆํŠธํฐ ๋ฐฐํ„ฐ๋ฆฌ ์†Œ๋ชจ์˜ ํฐ ๋ถ€๋ถ„์„ ์ฐจ์ง€ํ•ด.
์•ฑ์ด ํ™”๋ฉด์„ ๋ถˆํ•„์š”ํ•˜๊ฒŒ ์ผœ๋‘๊ฑฐ๋‚˜ ๋ฐ๊ฒŒ ์œ ์ง€ํ•˜๋ฉด ๋ฐฐํ„ฐ๋ฆฌ๊ฐ€ ๊ธ‰๊ฒฉํžˆ ์ค„์–ด๋“ค์–ด.

// Android - WakeLock ์˜ฌ๋ฐ”๋ฅธ ์‚ฌ์šฉ๋ฒ•
// WakeLock์€ ํ™”๋ฉด์ด ๊บผ์ ธ๋„ CPU๋ฅผ ๊นจ์–ด์žˆ๊ฒŒ ํ•จ โ†’ ๋ฐฐํ„ฐ๋ฆฌ ์†Œ๋ชจ!
class MyService : Service() {
    private var wakeLock: PowerManager.WakeLock? = null
    
    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
        val powerManager = getSystemService(POWER_SERVICE) as PowerManager
        
        // โœ… ํ•„์š”ํ•œ ์ตœ์†Œํ•œ์˜ WakeLock ํƒ€์ž… ์‚ฌ์šฉ
        wakeLock = powerManager.newWakeLock(
            PowerManager.PARTIAL_WAKE_LOCK, // CPU๋งŒ ๊นจ์›€ (ํ™”๋ฉด์€ ๋”)
            "MyApp::MyWakeLock"
        ).apply {
            acquire(10 * 60 * 1000L) // ์ตœ๋Œ€ 10๋ถ„์œผ๋กœ ์ œํ•œ!
        }
        
        return START_STICKY
    }
    
    override fun onDestroy() {
        wakeLock?.release() // ๋ฐ˜๋“œ์‹œ ํ•ด์ œ!
        super.onDestroy()
    }
}

๋ฐฐํ„ฐ๋ฆฌ ์†Œ๋ชจ ๋น„๊ต: ์ตœ์ ํ™” ์ „ vs ํ›„ โŒ ์ตœ์ ํ™” ์ „ 30% 4์‹œ๊ฐ„ ์‚ฌ์šฉ ํ›„ โ€ข 1์ดˆ๋งˆ๋‹ค ์œ„์น˜ ์—…๋ฐ์ดํŠธ โ€ข ์žฆ์€ ๋„คํŠธ์›Œํฌ ์š”์ฒญ โ€ข ๋ถˆํ•„์š”ํ•œ ๋ฐฑ๊ทธ๋ผ์šด๋“œ ์ž‘์—… โ€ข WakeLock ๋ฏธํ•ด์ œ โ€ข ์„ผ์„œ ์ƒ์‹œ ๊ตฌ๋™ ๊ฐ™์€ ๊ธฐ๋Šฅ, ๋‹ค๋ฅธ ๊ฒฐ๊ณผ! โœ… ์ตœ์ ํ™” ํ›„ 85% 4์‹œ๊ฐ„ ์‚ฌ์šฉ ํ›„ โ€ข 50m ์ด๋™ ์‹œ์—๋งŒ ์œ„์น˜ ์—…๋ฐ์ดํŠธ โ€ข ๋ฐฐ์น˜ ๋„คํŠธ์›Œํฌ ์š”์ฒญ โ€ข WorkManager ์Šค์ผ€์ค„๋ง โ€ข WakeLock ํƒ€์ž„์•„์›ƒ ์„ค์ • โ€ข ํ•„์š” ์‹œ์—๋งŒ ์„ผ์„œ ํ™œ์„ฑํ™” ๋ฐฐํ„ฐ๋ฆฌ 55% ์ ˆ์•ฝ!

โšก Part 3. ๋ Œ๋”๋ง ์ตœ์ ํ™” โ€” 60fps์˜ ์„ธ๊ณ„

์•ฑ์ด ๋ฒ„๋ฒ…๊ฑฐ๋ฆฌ๋Š” ๋А๋‚Œ... ์ด๊ฑฐ ์ง„์งœ ์ตœ์•…์ด์ž–์•„ ใ…‹ใ…‹ใ…‹
์‚ฌ๋žŒ ๋ˆˆ์€ ์ดˆ๋‹น 60ํ”„๋ ˆ์ž„(60fps) ์ด์ƒ์ด๋ฉด ๋ถ€๋“œ๋Ÿฝ๋‹ค๊ณ  ๋А๋ผ๋Š”๋ฐ,
์ด๊ฑธ ์œ ์ง€ํ•˜๋ ค๋ฉด ๊ฐ ํ”„๋ ˆ์ž„์„ 16.67ms ์•ˆ์— ์ฒ˜๋ฆฌํ•ด์•ผ ํ•ด.
์ด ์‹œ๊ฐ„์„ ๋„˜๊ธฐ๋ฉด "ํ”„๋ ˆ์ž„ ๋“œ๋กญ"์ด ๋ฐœ์ƒํ•˜๊ณ  ์•ฑ์ด ๋ฒ„๋ฒ…๊ฑฐ๋ฆฌ๋Š” ๊ฑฐ์•ผ!

๐ŸŽจ Android ๋ Œ๋”๋ง ์ตœ์ ํ™”

1 ๋ ˆ์ด์•„์›ƒ ๊ณ„์ธต ์ตœ์†Œํ™”

๋ ˆ์ด์•„์›ƒ์ด ๊นŠ์–ด์งˆ์ˆ˜๋ก ์ธก์ •(Measure)๊ณผ ๋ฐฐ์น˜(Layout) ์‹œ๊ฐ„์ด ๋Š˜์–ด๋‚˜.
ConstraintLayout์„ ์‚ฌ์šฉํ•˜๋ฉด ์ค‘์ฒฉ ์—†์ด ๋ณต์žกํ•œ ๋ ˆ์ด์•„์›ƒ์„ ๋งŒ๋“ค ์ˆ˜ ์žˆ์–ด!

<!-- โŒ ๊นŠ์€ ์ค‘์ฒฉ ๋ ˆ์ด์•„์›ƒ - ๋ Œ๋”๋ง ๋А๋ฆผ -->
<LinearLayout>
    <LinearLayout>
        <RelativeLayout>
            <LinearLayout>
                <TextView />
                <ImageView />
            </LinearLayout>
        </RelativeLayout>
    </LinearLayout>
</LinearLayout>

<!-- โœ… ConstraintLayout์œผ๋กœ ํ‰ํƒ„ํ™” - ๋ Œ๋”๋ง ๋น ๋ฆ„ -->
<ConstraintLayout>
    <TextView
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintStart_toStartOf="parent" />
    <ImageView
        app:layout_constraintTop_toBottomOf="@id/textView"
        app:layout_constraintStart_toStartOf="parent" />
</ConstraintLayout>

2 RecyclerView ์ตœ์ ํ™”

๋ฆฌ์ŠคํŠธ ํ™”๋ฉด์€ ์•ฑ์—์„œ ๊ฐ€์žฅ ๋งŽ์ด ์“ฐ์ด๋Š” UI์•ผ. RecyclerView๋ฅผ ์ œ๋Œ€๋กœ ์จ์•ผ ํ•ด!

// RecyclerView ์ตœ์ ํ™” ์„ค์ •
recyclerView.apply {
    // ์•„์ดํ…œ ํฌ๊ธฐ๊ฐ€ ๊ณ ์ •๋˜๋ฉด ์„ฑ๋Šฅ ํ–ฅ์ƒ
    setHasFixedSize(true)
    
    // ๋ฏธ๋ฆฌ ๋กœ๋“œํ•  ์•„์ดํ…œ ์ˆ˜ ์„ค์ •
    val layoutManager = LinearLayoutManager(context)
    layoutManager.initialPrefetchItemCount = 4
    this.layoutManager = layoutManager
    
    // ์•„์ดํ…œ ๋ณ€๊ฒฝ ์• ๋‹ˆ๋ฉ”์ด์…˜ ๋น„ํ™œ์„ฑํ™” (ํ•„์š” ์—†์„ ๋•Œ)
    (itemAnimator as? SimpleItemAnimator)?.supportsChangeAnimations = false
}

// ViewHolder์—์„œ ๋น„์šฉ ๋น„์‹ผ ์ž‘์—… ํ”ผํ•˜๊ธฐ
class MyViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
    private val textView: TextView = itemView.findViewById(R.id.text)
    private val imageView: ImageView = itemView.findViewById(R.id.image)
    
    fun bind(item: MyItem) {
        textView.text = item.title
        
        // โœ… Glide/Coil ๊ฐ™์€ ์ด๋ฏธ์ง€ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ์‚ฌ์šฉ
        // ์ž๋™์œผ๋กœ ์บ์‹ฑ, ๋‹ค์šด์ƒ˜ํ”Œ๋ง, ๋น„๋™๊ธฐ ๋กœ๋”ฉ ์ฒ˜๋ฆฌ
        Glide.with(itemView.context)
            .load(item.imageUrl)
            .placeholder(R.drawable.placeholder)
            .into(imageView)
    }
}

3 ์ปค์Šคํ…€ ๋ทฐ ์ตœ์ ํ™”

์ปค์Šคํ…€ ๋ทฐ๋ฅผ ๋งŒ๋“ค ๋•Œ onDraw()์—์„œ ๊ฐ์ฒด ์ƒ์„ฑํ•˜๋ฉด ์ ˆ๋Œ€ ์•ˆ ๋ผ!
๋งค ํ”„๋ ˆ์ž„๋งˆ๋‹ค ํ˜ธ์ถœ๋˜๋Š” ๋ฉ”์„œ๋“œ์—์„œ ๊ฐ์ฒด๋ฅผ ์ƒ์„ฑํ•˜๋ฉด GC ์••๋ฐ•์ด ์‹ฌํ•ด์ ธ์„œ ํ”„๋ ˆ์ž„ ๋“œ๋กญ์ด ๋ฐœ์ƒํ•ด.

// โŒ ์ž˜๋ชป๋œ ์ปค์Šคํ…€ ๋ทฐ
class BadCustomView(context: Context) : View(context) {
    override fun onDraw(canvas: Canvas) {
        val paint = Paint() // ๋งค ํ”„๋ ˆ์ž„๋งˆ๋‹ค ๊ฐ์ฒด ์ƒ์„ฑ! ์ ˆ๋Œ€ ๊ธˆ์ง€!
        val rect = RectF()  // ์ด๊ฒƒ๋„ ๊ธˆ์ง€!
        canvas.drawRect(rect, paint)
    }
}

// โœ… ์˜ฌ๋ฐ”๋ฅธ ์ปค์Šคํ…€ ๋ทฐ
class GoodCustomView(context: Context) : View(context) {
    // ํด๋ž˜์Šค ๋ ˆ๋ฒจ์—์„œ ๋ฏธ๋ฆฌ ์ƒ์„ฑ
    private val paint = Paint().apply {
        color = Color.BLUE
        style = Paint.Style.FILL
    }
    private val rect = RectF()
    
    override fun onDraw(canvas: Canvas) {
        rect.set(0f, 0f, width.toFloat(), height.toFloat())
        canvas.drawRect(rect, paint) // ์žฌ์‚ฌ์šฉ!
    }
    
    override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
        super.onSizeChanged(w, h, oldw, oldh)
        // ํฌ๊ธฐ ๋ณ€๊ฒฝ ์‹œ์—๋งŒ ๊ณ„์‚ฐ
        rect.set(0f, 0f, w.toFloat(), h.toFloat())
    }
}

๐ŸŽ iOS ๋ Œ๋”๋ง ์ตœ์ ํ™”

1 ์…€ ์žฌ์‚ฌ์šฉ๊ณผ ๋น„๋™๊ธฐ ์ฒ˜๋ฆฌ

// iOS UITableView ์ตœ์ ํ™”
class OptimizedTableViewController: UITableViewController {
    
    override func tableView(
        _ tableView: UITableView,
        cellForRowAt indexPath: IndexPath
    ) -> UITableViewCell {
        // โœ… ์…€ ์žฌ์‚ฌ์šฉ - dequeueReusableCell ๋ฐ˜๋“œ์‹œ ์‚ฌ์šฉ
        let cell = tableView.dequeueReusableCell(
            withIdentifier: "MyCell",
            for: indexPath
        ) as! MyCell
        
        let item = items[indexPath.row]
        cell.configure(with: item)
        
        return cell
    }
    
    // โœ… ์…€ ๋†’์ด ๋ฏธ๋ฆฌ ๊ณ„์‚ฐ (estimatedRowHeight ์‚ฌ์šฉ)
    override func viewDidLoad() {
        super.viewDidLoad()
        tableView.estimatedRowHeight = 80
        tableView.rowHeight = UITableView.automaticDimension
    }
}

// ์ด๋ฏธ์ง€ ๋น„๋™๊ธฐ ๋กœ๋”ฉ
class MyCell: UITableViewCell {
    private var imageLoadTask: URLSessionDataTask?
    
    func configure(with item: Item) {
        // ์ด์ „ ๋กœ๋”ฉ ์ž‘์—… ์ทจ์†Œ
        imageLoadTask?.cancel()
        
        // ๋น„๋™๊ธฐ๋กœ ์ด๋ฏธ์ง€ ๋กœ๋”ฉ
        imageLoadTask = URLSession.shared.dataTask(with: item.imageURL) { 
            [weak self] data, _, _ in
            guard let data = data, let image = UIImage(data: data) else { return }
            DispatchQueue.main.async {
                self?.imageView?.image = image
            }
        }
        imageLoadTask?.resume()
    }
    
    override func prepareForReuse() {
        super.prepareForReuse()
        imageLoadTask?.cancel() // ์žฌ์‚ฌ์šฉ ์‹œ ์ด์ „ ์ž‘์—… ์ทจ์†Œ
        imageView?.image = nil
    }
}

2 Core Animation ์ตœ์ ํ™”

// iOS ๋ ˆ์ด์–ด ์ตœ์ ํ™”
class OptimizedView: UIView {
    override func awakeFromNib() {
        super.awakeFromNib()
        
        // โœ… shouldRasterize - ์ž์ฃผ ๋ณ€ํ•˜์ง€ ์•Š๋Š” ๋ณต์žกํ•œ ๋ทฐ์— ์‚ฌ์šฉ
        layer.shouldRasterize = true
        layer.rasterizationScale = UIScreen.main.scale
        
        // โœ… drawsAsynchronously - ๋น„๋™๊ธฐ ๋“œ๋กœ์ž‰
        layer.drawsAsynchronously = true
        
        // โœ… ๋ถˆํˆฌ๋ช… ๋ทฐ๋Š” isOpaque = true ์„ค์ •
        // ํˆฌ๋ช…๋„ ๊ณ„์‚ฐ์„ ๊ฑด๋„ˆ๋›ฐ์–ด์„œ ๋ Œ๋”๋ง ๋นจ๋ผ์ง
        isOpaque = true
        backgroundColor = .white
    }
}

๐Ÿš€ Flutter ๋ Œ๋”๋ง ์ตœ์ ํ™”

์š”์ฆ˜ Flutter ๋งŽ์ด ์“ฐ์ž–์•„? Flutter๋„ ์ตœ์ ํ™” ํฌ์ธํŠธ๊ฐ€ ์žˆ์–ด!

// Flutter - const ์ƒ์„ฑ์ž ํ™œ์šฉ
// โœ… const๋กœ ์„ ์–ธํ•˜๋ฉด ์œ„์ ฏ ์žฌ๋นŒ๋“œ ์‹œ ์žฌ์ƒ์„ฑ ์•ˆ ํ•จ
class MyWidget extends StatelessWidget {
  const MyWidget({super.key}); // const ์ƒ์„ฑ์ž!
  
  @override
  Widget build(BuildContext context) {
    return const Column(
      children: [
        Text('Hello'), // const๋กœ ์„ ์–ธ ๊ฐ€๋Šฅ
        Icon(Icons.star), // const๋กœ ์„ ์–ธ ๊ฐ€๋Šฅ
      ],
    );
  }
}

// โœ… ListView.builder ์‚ฌ์šฉ - ํ™”๋ฉด์— ๋ณด์ด๋Š” ๊ฒƒ๋งŒ ๋ Œ๋”๋ง
ListView.builder(
  itemCount: items.length,
  itemBuilder: (context, index) {
    return ListTile(
      title: Text(items[index].title),
    );
  },
)

// โŒ ListView(children: [...]) - ๋ชจ๋“  ์•„์ดํ…œ์„ ํ•œ ๋ฒˆ์— ๋ Œ๋”๋ง
// ์•„์ดํ…œ์ด ๋งŽ์œผ๋ฉด ์ ˆ๋Œ€ ์‚ฌ์šฉ ๊ธˆ์ง€!
// Flutter - RepaintBoundary๋กœ ๋ถˆํ•„์š”ํ•œ ๋ฆฌํŽ˜์ธํŠธ ๋ฐฉ์ง€
class MyAnimatedWidget extends StatefulWidget {
  @override
  Widget build(BuildContext context) {
    return Row(
      children: [
        // โœ… ์• ๋‹ˆ๋ฉ”์ด์…˜ ๋ถ€๋ถ„๋งŒ RepaintBoundary๋กœ ๊ฐ์‹ธ๊ธฐ
        RepaintBoundary(
          child: AnimatedContainer(
            duration: Duration(milliseconds: 300),
            // ์ด ๋ถ€๋ถ„๋งŒ ๋ฆฌํŽ˜์ธํŠธ๋จ
          ),
        ),
        // ์ด ๋ถ€๋ถ„์€ ์• ๋‹ˆ๋ฉ”์ด์…˜๊ณผ ๋ฌด๊ด€ํ•˜๊ฒŒ ๋ฆฌํŽ˜์ธํŠธ ์•ˆ ๋จ
        const Text('์ •์ ์ธ ํ…์ŠคํŠธ'),
      ],
    );
  }
}

๐Ÿ”ง Part 4. ๋น„๋™๊ธฐ ์ฒ˜๋ฆฌ โ€” ๋ฉ”์ธ ์Šค๋ ˆ๋“œ๋ฅผ ์ง€์ผœ๋ผ

๋ฉ”์ธ ์Šค๋ ˆ๋“œ(UI ์Šค๋ ˆ๋“œ)์—์„œ ๋ฌด๊ฑฐ์šด ์ž‘์—…์„ ํ•˜๋ฉด ์•ฑ์ด ์–ผ์–ด๋ฒ„๋ ค.
์ด๊ฑธ "ANR(Application Not Responding)"์ด๋ผ๊ณ  ํ•˜๋Š”๋ฐ, Android์—์„œ๋Š” 5์ดˆ ์ด์ƒ ๋ฉ”์ธ ์Šค๋ ˆ๋“œ๊ฐ€ ๋ธ”๋กœํ‚น๋˜๋ฉด ANR ๋‹ค์ด์–ผ๋กœ๊ทธ๊ฐ€ ๋œจ๊ฑฐ๋“ .
๋ฌด๊ฑฐ์šด ์ž‘์—…์€ ๋ฐ˜๋“œ์‹œ ๋ฐฑ๊ทธ๋ผ์šด๋“œ ์Šค๋ ˆ๋“œ์—์„œ! ์ด๊ฒŒ ๊ธฐ๋ณธ ์ค‘์˜ ๊ธฐ๋ณธ์ด์•ผ.

๐Ÿ”„ Kotlin Coroutines ํ™œ์šฉ

// Kotlin Coroutines - ๋น„๋™๊ธฐ ์ฒ˜๋ฆฌ์˜ ์ •์„
class MyViewModel : ViewModel() {
    
    fun loadData() {
        viewModelScope.launch {
            try {
                // โœ… IO ์ž‘์—…์€ Dispatchers.IO์—์„œ
                val data = withContext(Dispatchers.IO) {
                    repository.fetchDataFromNetwork() // ๋„คํŠธ์›Œํฌ ์š”์ฒญ
                }
                
                // โœ… UI ์—…๋ฐ์ดํŠธ๋Š” Main์—์„œ (launch๋Š” ๊ธฐ๋ณธ์ด Main)
                _uiState.value = UiState.Success(data)
                
            } catch (e: Exception) {
                _uiState.value = UiState.Error(e.message)
            }
        }
    }
    
    // โœ… ๋ณ‘๋ ฌ ์ฒ˜๋ฆฌ๋กœ ์„ฑ๋Šฅ ํ–ฅ์ƒ
    fun loadMultipleData() {
        viewModelScope.launch {
            val userDeferred = async(Dispatchers.IO) { 
                repository.fetchUser() 
            }
            val postsDeferred = async(Dispatchers.IO) { 
                repository.fetchPosts() 
            }
            
            // ๋‘ ์š”์ฒญ์„ ๋™์‹œ์— ์‹คํ–‰ํ•˜๊ณ  ๋‘˜ ๋‹ค ์™„๋ฃŒ๋  ๋•Œ๊นŒ์ง€ ๋Œ€๊ธฐ
            val user = userDeferred.await()
            val posts = postsDeferred.await()
            
            _uiState.value = UiState.Success(user, posts)
        }
    }
}

โšก Swift Concurrency (async/await)

// iOS - Swift async/await
class DataViewModel: ObservableObject {
    @Published var data: [Item] = []
    @Published var isLoading = false
    
    func loadData() async {
        isLoading = true
        defer { isLoading = false }
        
        do {
            // โœ… async/await์œผ๋กœ ๊น”๋”ํ•œ ๋น„๋™๊ธฐ ์ฒ˜๋ฆฌ
            let result = try await networkService.fetchItems()
            
            // โœ… UI ์—…๋ฐ์ดํŠธ๋Š” ๋ฉ”์ธ ์•กํ„ฐ์—์„œ
            await MainActor.run {
                self.data = result
            }
        } catch {
            print("Error: \(error)")
        }
    }
    
    // โœ… ๋ณ‘๋ ฌ ์ฒ˜๋ฆฌ
    func loadMultipleData() async {
        async let users = networkService.fetchUsers()
        async let posts = networkService.fetchPosts()
        
        do {
            let (fetchedUsers, fetchedPosts) = try await (users, posts)
            // ๋‘ ์š”์ฒญ ๋™์‹œ ์‹คํ–‰!
        } catch {
            print("Error: \(error)")
        }
    }
}

๐Ÿ“Š Part 5. ์„ฑ๋Šฅ ์ธก์ •๊ณผ ๋ชจ๋‹ˆํ„ฐ๋ง

์ตœ์ ํ™”๋ฅผ ํ–ˆ์œผ๋ฉด ์‹ค์ œ๋กœ ์–ผ๋งˆ๋‚˜ ๊ฐœ์„ ๋๋Š”์ง€ ์ธก์ •ํ•ด์•ผ ํ•ด.
"๋А๋‚Œ์ƒ ๋นจ๋ผ์ง„ ๊ฒƒ ๊ฐ™์€๋ฐ?" ์ด๊ฑด ๊ฐœ๋ฐœ์ž๊ฐ€ ํ•  ๋ง์ด ์•„๋‹ˆ์•ผ ใ…‹ใ…‹ใ…‹
์ˆซ์ž๋กœ ์ฆ๋ช…ํ•ด์•ผ ์ง„์งœ ์ตœ์ ํ™”์•ผ!

๐Ÿ“ฑ Android ์„ฑ๋Šฅ ์ธก์ • ๋„๊ตฌ

๐Ÿ” Android Studio Profiler CPU, ๋ฉ”๋ชจ๋ฆฌ, ๋„คํŠธ์›Œํฌ, ์—๋„ˆ์ง€ ์‚ฌ์šฉ๋Ÿ‰์„ ์‹ค์‹œ๊ฐ„์œผ๋กœ ๋ชจ๋‹ˆํ„ฐ๋งํ•  ์ˆ˜ ์žˆ์–ด.
ํŠนํžˆ Energy Profiler๋Š” ๋ฐฐํ„ฐ๋ฆฌ ์†Œ๋ชจ๋ฅผ ์ง์ ‘ ์ธก์ •ํ•  ์ˆ˜ ์žˆ์–ด์„œ ์ตœ์ ํ™” ์ „ํ›„ ๋น„๊ต์— ๋”ฑ์ด์•ผ!

// Android - ์„ฑ๋Šฅ ์ธก์ • ์ฝ”๋“œ ์‚ฝ์ž…
// Systrace/Perfetto ๋งˆ์ปค ์ถ”๊ฐ€
class MyActivity : AppCompatActivity() {
    
    fun performHeavyOperation() {
        // ์„ฑ๋Šฅ ์ธก์ • ์‹œ์ž‘ ๋งˆ์ปค
        Trace.beginSection("MyHeavyOperation")
        
        try {
            // ์ธก์ •ํ•˜๊ณ  ์‹ถ์€ ์ฝ”๋“œ
            processLargeDataSet()
        } finally {
            // ์„ฑ๋Šฅ ์ธก์ • ์ข…๋ฃŒ ๋งˆ์ปค
            Trace.endSection()
        }
    }
}

// Benchmark ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋กœ ์ •๋ฐ€ ์ธก์ •
@RunWith(AndroidJUnit4::class)
class MyBenchmark {
    @get:Rule
    val benchmarkRule = BenchmarkRule()
    
    @Test
    fun benchmarkMyOperation() {
        benchmarkRule.measureRepeated {
            // ์ธก์ •ํ•  ์ฝ”๋“œ
            myOperation()
        }
    }
}

๐ŸŽ iOS ์„ฑ๋Šฅ ์ธก์ • ๋„๊ตฌ

// iOS - os_signpost๋กœ ์„ฑ๋Šฅ ๋งˆ์ปค ์ถ”๊ฐ€
import os.signpost

class MyViewController: UIViewController {
    private let log = OSLog(subsystem: "com.myapp", category: "Performance")
    
    func loadData() {
        let signpostID = OSSignpostID(log: log)
        
        os_signpost(.begin, log: log, name: "DataLoad", signpostID: signpostID)
        
        fetchData { [weak self] result in
            os_signpost(.end, log: log, name: "DataLoad", signpostID: signpostID)
            self?.processResult(result)
        }
    }
}

// XCTest Performance ์ธก์ •
class PerformanceTests: XCTestCase {
    func testMyOperationPerformance() {
        measure {
            // ์ธก์ •ํ•  ์ฝ”๋“œ
            myOperation()
        }
    }
}

๐ŸŒ Firebase Performance Monitoring

์‹ค์ œ ์‚ฌ์šฉ์ž ํ™˜๊ฒฝ์—์„œ์˜ ์„ฑ๋Šฅ์„ ์ธก์ •ํ•˜๋ ค๋ฉด Firebase Performance Monitoring์ด ์ตœ๊ณ ์•ผ.
๊ฐœ๋ฐœ ํ™˜๊ฒฝ์—์„œ๋Š” ์ž˜ ๋˜๋˜ ๊ฒŒ ์‹ค์ œ ๊ธฐ๊ธฐ์—์„œ ๋А๋ฆด ์ˆ˜ ์žˆ๊ฑฐ๋“ !

// Android - Firebase Performance ์ปค์Šคํ…€ ํŠธ๋ ˆ์ด์Šค
val myTrace = Firebase.performance.newTrace("my_custom_trace")
myTrace.start()

// ์ธก์ •ํ•˜๊ณ  ์‹ถ์€ ์ž‘์—…
performOperation()

myTrace.stop()

// ์ปค์Šคํ…€ ๋ฉ”ํŠธ๋ฆญ ์ถ”๊ฐ€
myTrace.incrementMetric("items_processed", itemCount.toLong())

๐Ÿ’ก Part 6. ์‹ค์ „ ์ตœ์ ํ™” ์ฒดํฌ๋ฆฌ์ŠคํŠธ

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

์•ฑ ์„ฑ๋Šฅ ์ตœ์ ํ™” ์ฒดํฌ๋ฆฌ์ŠคํŠธ ๐Ÿง  ๋ฉ”๋ชจ๋ฆฌ ๊ด€๋ฆฌ Activity Context ๋Œ€์‹  Application Context ์‚ฌ์šฉ WeakReference๋กœ ์ˆœํ™˜ ์ฐธ์กฐ ๋ฐฉ์ง€ ๋ฆฌ์Šค๋„ˆ/์ฝœ๋ฐฑ ๋“ฑ๋ก ํ•ด์ œ ํ™•์ธ ์ด๋ฏธ์ง€ ๋‹ค์šด์ƒ˜ํ”Œ๋ง ์ ์šฉ LRU ์บ์‹œ ์ „๋žต ์ ์šฉ LeakCanary๋กœ ๋ˆ„์ˆ˜ ๊ฒ€์‚ฌ ๊ฐ์ฒด ํ’€๋ง ์ ์šฉ (๋ฐ˜๋ณต ์ƒ์„ฑ ๊ฐ์ฒด) ๐Ÿ”‹ ๋ฐฐํ„ฐ๋ฆฌ ์ตœ์ ํ™” ๋„คํŠธ์›Œํฌ ์š”์ฒญ ๋ฐฐ์น˜ ์ฒ˜๋ฆฌ WorkManager๋กœ ๋ฐฑ๊ทธ๋ผ์šด๋“œ ์ž‘์—… ๊ด€๋ฆฌ ์œ„์น˜ ์ •ํ™•๋„ ์ตœ์†Œํ™” WakeLock ํƒ€์ž„์•„์›ƒ ์„ค์ • Doze ๋ชจ๋“œ ๋Œ€์‘ ํ™•์ธ ๋ถˆํ•„์š”ํ•œ ์„ผ์„œ ์‚ฌ์šฉ ์ œ๊ฑฐ Battery Historian์œผ๋กœ ๋ถ„์„ โšก ๋ Œ๋”๋ง ์ตœ์ ํ™” ๋ ˆ์ด์•„์›ƒ ๊ณ„์ธต ์ตœ์†Œํ™” RecyclerView setHasFixedSize ์„ค์ • onDraw()์—์„œ ๊ฐ์ฒด ์ƒ์„ฑ ๊ธˆ์ง€ ์ด๋ฏธ์ง€ ๋น„๋™๊ธฐ ๋กœ๋”ฉ ์ ์šฉ 60fps ์œ ์ง€ ํ™•์ธ (Profiler) ์˜ค๋ฒ„๋“œ๋กœ์šฐ ์ตœ์†Œํ™” Flutter const ์œ„์ ฏ ํ™œ์šฉ ๐Ÿ”„ ๋น„๋™๊ธฐ ์ฒ˜๋ฆฌ ๋ฉ”์ธ ์Šค๋ ˆ๋“œ ๋ธ”๋กœํ‚น ์ œ๊ฑฐ Coroutines/async-await ํ™œ์šฉ ๋ณ‘๋ ฌ ์ฒ˜๋ฆฌ๋กœ ์‘๋‹ต ์‹œ๊ฐ„ ๋‹จ์ถ• ANR ๋ฐœ์ƒ ์—ฌ๋ถ€ ํ™•์ธ ์ ์ ˆํ•œ Dispatcher ์‚ฌ์šฉ ์—๋Ÿฌ ํ•ธ๋“ค๋ง ์™„๋น„ ์ทจ์†Œ ์ฒ˜๋ฆฌ (Cancellation) ๊ตฌํ˜„ ๋ชจ๋“  ํ•ญ๋ชฉ ์ฒดํฌ ์™„๋ฃŒ = ํ€„๋ฆฌํ‹ฐ ์•ฑ ์™„์„ฑ! ๐ŸŽ‰

๐ŸŽฏ Part 7. ๊ณ ๊ธ‰ ์ตœ์ ํ™” ๊ธฐ๋ฒ•๋“ค

๐Ÿ“ฆ ์•ฑ ํฌ๊ธฐ ์ตœ์ ํ™” (APK/IPA ์ตœ์ ํ™”)

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

// Android - R8/ProGuard๋กœ ์ฝ”๋“œ ์ตœ์†Œํ™”
// build.gradle
android {
    buildTypes {
        release {
            minifyEnabled true          // ์ฝ”๋“œ ์ตœ์†Œํ™”
            shrinkResources true        // ๋ฏธ์‚ฌ์šฉ ๋ฆฌ์†Œ์Šค ์ œ๊ฑฐ
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'),
                         'proguard-rules.pro'
        }
    }
    
    // App Bundle ์‚ฌ์šฉ - ๊ธฐ๊ธฐ์— ๋งž๋Š” ๋ฆฌ์†Œ์Šค๋งŒ ๋‹ค์šด๋กœ๋“œ
    bundle {
        language {
            enableSplit = true  // ์–ธ์–ด๋ณ„ ๋ถ„๋ฆฌ
        }
        density {
            enableSplit = true  // ํ™”๋ฉด ๋ฐ€๋„๋ณ„ ๋ถ„๋ฆฌ
        }
        abi {
            enableSplit = true  // CPU ์•„ํ‚คํ…์ฒ˜๋ณ„ ๋ถ„๋ฆฌ
        }
    }
}

๐Ÿ—œ๏ธ ์ด๋ฏธ์ง€ ์ตœ์ ํ™” ์ „๋žต

์ด๋ฏธ์ง€๋Š” ์•ฑ ํฌ๊ธฐ์˜ ํฐ ๋ถ€๋ถ„์„ ์ฐจ์ง€ํ•ด. ์˜ฌ๋ฐ”๋ฅธ ํฌ๋งท ์„ ํƒ์ด ์ค‘์š”ํ•ด!

ํฌ๋งท ํŠน์ง• ์ถ”์ฒœ ์šฉ๋„
WebP PNG ๋Œ€๋น„ 26%, JPEG ๋Œ€๋น„ 25~34% ์ž‘์Œ ์ผ๋ฐ˜ ์ด๋ฏธ์ง€ (Android 4.0+, iOS 14+)
AVIF WebP๋ณด๋‹ค ๋” ์ž‘์Œ, ์ตœ์‹  ํฌ๋งท ์ตœ์‹  ๊ธฐ๊ธฐ ๋Œ€์ƒ ์•ฑ
SVG ๋ฒกํ„ฐ ๊ทธ๋ž˜ํ”ฝ, ํฌ๊ธฐ ๋ฌด๊ด€ ์„ ๋ช… ์•„์ด์ฝ˜, ๋กœ๊ณ , ๋‹จ์ˆœ ์ผ๋Ÿฌ์ŠคํŠธ
PNG ๋ฌด์†์‹ค ์••์ถ•, ํˆฌ๋ช…๋„ ์ง€์› ํˆฌ๋ช… ๋ฐฐ๊ฒฝ ํ•„์š”ํ•œ ์ด๋ฏธ์ง€
JPEG ์†์‹ค ์••์ถ•, ์ž‘์€ ํŒŒ์ผ ํฌ๊ธฐ ์‚ฌ์ง„ (ํˆฌ๋ช…๋„ ๋ถˆํ•„์š”)

๐Ÿ” ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์ตœ์ ํ™”

// Android Room DB ์ตœ์ ํ™”
@Dao
interface UserDao {
    // โœ… ํ•„์š”ํ•œ ์ปฌ๋Ÿผ๋งŒ ์กฐํšŒ (SELECT * ๊ธˆ์ง€)
    @Query("SELECT id, name, email FROM users WHERE id = :userId")
    suspend fun getUserBasicInfo(userId: Long): UserBasicInfo
    
    // โœ… ์ธ๋ฑ์Šค ํ™œ์šฉ
    @Query("SELECT * FROM users WHERE email = :email")
    suspend fun getUserByEmail(email: String): User?
    
    // โœ… ํŽ˜์ด์ง•์œผ๋กœ ๋Œ€์šฉ๋Ÿ‰ ๋ฐ์ดํ„ฐ ์ฒ˜๋ฆฌ
    @Query("SELECT * FROM posts ORDER BY created_at DESC")
    fun getPostsPaged(): PagingSource<Int, Post>
}

// ์ธ๋ฑ์Šค ์„ค์ •
@Entity(
    tableName = "users",
    indices = [
        Index(value = ["email"], unique = true), // ์ด๋ฉ”์ผ ์ธ๋ฑ์Šค
        Index(value = ["created_at"])             // ๋‚ ์งœ ์ธ๋ฑ์Šค
    ]
)
data class User(
    @PrimaryKey val id: Long,
    val name: String,
    val email: String,
    val createdAt: Long
)

๐ŸŒ ๋„คํŠธ์›Œํฌ ์บ์‹ฑ ์ „๋žต

// OkHttp ์บ์‹œ ์„ค์ • (Android)
val cacheSize = 10L * 1024 * 1024 // 10MB ์บ์‹œ
val cache = Cache(context.cacheDir, cacheSize)

val okHttpClient = OkHttpClient.Builder()
    .cache(cache)
    .addInterceptor { chain ->
        val request = chain.request()
        
        // ๋„คํŠธ์›Œํฌ ์—†์„ ๋•Œ ์บ์‹œ ์‚ฌ์šฉ
        val modifiedRequest = if (!isNetworkAvailable()) {
            request.newBuilder()
                .header("Cache-Control", "public, only-if-cached, max-stale=86400")
                .build()
        } else {
            request.newBuilder()
                .header("Cache-Control", "public, max-age=60") // 60์ดˆ ์บ์‹œ
                .build()
        }
        
        chain.proceed(modifiedRequest)
    }
    .build()

๐ŸŽ“ Part 8. ํ”Œ๋žซํผ๋ณ„ ํ•ต์‹ฌ ์ตœ์ ํ™” ์š”์•ฝ

๐Ÿง‘โ€๐Ÿ’ป "๊ทผ๋ฐ Android๋ž‘ iOS๋ž‘ Flutter ๋‹ค ๋ฐฐ์›Œ์•ผ ํ•ด? ๋„ˆ๋ฌด ๋งŽ์€ ๊ฑฐ ์•„๋‹ˆ์•ผ?"
ใ…‹ใ…‹ใ…‹ ๋งž์•„, ๋งŽ๊ธด ํ•ด!
๊ทผ๋ฐ ํ•ต์‹ฌ ์›๋ฆฌ๋Š” ๋‹ค ๋˜‘๊ฐ™์•„.
๋ฉ”๋ชจ๋ฆฌ ๊ด€๋ฆฌ, ๋ฐฐํ„ฐ๋ฆฌ ์ ˆ์•ฝ, ๋ Œ๋”๋ง ์ตœ์ ํ™”
์ด ์„ธ ๊ฐ€์ง€ ์›์น™๋งŒ ์ดํ•ดํ•˜๋ฉด ์–ด๋А ํ”Œ๋žซํผ์ด๋“  ์ ์šฉํ•  ์ˆ˜ ์žˆ์–ด!

์žฌ๋Šฅ๋„ท์—์„œ ์•ฑ ๊ฐœ๋ฐœ ๊ด€๋ จ ์žฌ๋Šฅ์„ ๊ณต์œ ํ•˜๊ฑฐ๋‚˜ ์˜๋ขฐํ•  ๋•Œ๋„, ์ด๋Ÿฐ ์„ฑ๋Šฅ ์ตœ์ ํ™” ์ง€์‹์„ ๊ฐ–์ถ˜ ๊ฐœ๋ฐœ์ž๋ฅผ ์ฐพ๋Š” ๊ฒŒ ์ค‘์š”ํ•ด.
๋‹จ์ˆœํžˆ ๊ธฐ๋Šฅ๋งŒ ๊ตฌํ˜„ํ•˜๋Š” ๊ฒŒ ์•„๋‹ˆ๋ผ ์„ฑ๋Šฅ๊นŒ์ง€ ๊ณ ๋ คํ•œ ์•ฑ์„ ๋งŒ๋“œ๋Š” ๊ฐœ๋ฐœ์ž๊ฐ€ ์ง„์งœ ์‹ค๋ ฅ์ž๊ฑฐ๋“ !

๐Ÿ“ฑ Android ํ•ต์‹ฌ ์ตœ์ ํ™” ํฌ์ธํŠธ

๋ฉ”๋ชจ๋ฆฌ LeakCanary ํ•„์ˆ˜ ์‚ฌ์šฉ
๋ฉ”๋ชจ๋ฆฌ ViewModel + LiveData/StateFlow ํŒจํ„ด
๋ฐฐํ„ฐ๋ฆฌ WorkManager๋กœ ๋ฐฑ๊ทธ๋ผ์šด๋“œ ์ž‘์—…
๋ฐฐํ„ฐ๋ฆฌ Doze ๋ชจ๋“œ ๋Œ€์‘
๋ Œ๋”๋ง ConstraintLayout์œผ๋กœ ๋ ˆ์ด์•„์›ƒ ํ‰ํƒ„ํ™”
๋ Œ๋”๋ง RecyclerView + DiffUtil
๋น„๋™๊ธฐ Kotlin Coroutines
ํฌ๊ธฐ App Bundle + R8 ์ตœ์ ํ™”

๐ŸŽ iOS ํ•ต์‹ฌ ์ตœ์ ํ™” ํฌ์ธํŠธ

๋ฉ”๋ชจ๋ฆฌ [weak self] / [unowned self] ์ ์ ˆํžˆ ์‚ฌ์šฉ
๋ฉ”๋ชจ๋ฆฌ Instruments Leaks ๋„๊ตฌ๋กœ ๊ฒ€์‚ฌ
๋ฐฐํ„ฐ๋ฆฌ Background App Refresh ์ตœ์†Œํ™”
๋ฐฐํ„ฐ๋ฆฌ Significant Location Change ํ™œ์šฉ
๋ Œ๋”๋ง dequeueReusableCell ๋ฐ˜๋“œ์‹œ ์‚ฌ์šฉ
๋ Œ๋”๋ง isOpaque = true ์„ค์ •
๋น„๋™๊ธฐ Swift async/await + MainActor
ํฌ๊ธฐ Asset Catalog + On-Demand Resources

๐Ÿฆ Flutter ํ•ต์‹ฌ ์ตœ์ ํ™” ํฌ์ธํŠธ

๋ Œ๋”๋ง const ์œ„์ ฏ ์ตœ๋Œ€ํ•œ ํ™œ์šฉ
๋ Œ๋”๋ง ListView.builder ์‚ฌ์šฉ (ListView ๊ธˆ์ง€)
๋ Œ๋”๋ง RepaintBoundary๋กœ ๋ฆฌํŽ˜์ธํŠธ ๋ฒ”์œ„ ์ œํ•œ
๋ฉ”๋ชจ๋ฆฌ dispose() ๋ฉ”์„œ๋“œ์—์„œ ๋ฆฌ์†Œ์Šค ํ•ด์ œ
๋น„๋™๊ธฐ Isolate๋กœ ๋ฌด๊ฑฐ์šด ๊ณ„์‚ฐ ์ฒ˜๋ฆฌ
์ด๋ฏธ์ง€ cached_network_image ํŒจํ‚ค์ง€ ํ™œ์šฉ
ํฌ๊ธฐ flutter build --split-per-abi
๋ถ„์„ Flutter DevTools ํ™œ์šฉ


๐Ÿš€ Part 9. ์‹ค๋ฌด์—์„œ ๋ฐ”๋กœ ์“ฐ๋Š” ์ตœ์ ํ™” ํŒ

๐Ÿ’ฌ ๊ฐœ๋ฐœ์ž๋“ค์ด ์ž์ฃผ ๋†“์น˜๋Š” ๊ฒƒ๋“ค

โš ๏ธ ์ž์ฃผ ๋†“์น˜๋Š” ์‹ค์ˆ˜ TOP 5
1. ๋””๋ฒ„๊ทธ ๋นŒ๋“œ๋กœ๋งŒ ํ…Œ์ŠคํŠธ
๋””๋ฒ„๊ทธ ๋นŒ๋“œ๋Š” ์ตœ์ ํ™”๊ฐ€ ๊บผ์ ธ์žˆ์–ด์„œ ์‹ค์ œ ์„ฑ๋Šฅ๊ณผ ๋‹ฌ๋ผ. ๋ฐ˜๋“œ์‹œ ๋ฆด๋ฆฌ์ฆˆ ๋นŒ๋“œ๋กœ๋„ ํ…Œ์ŠคํŠธํ•ด์•ผ ํ•ด!

2. ์ตœ์‹  ๊ธฐ๊ธฐ์—์„œ๋งŒ ํ…Œ์ŠคํŠธ
์‹ค์ œ ์‚ฌ์šฉ์ž๋Š” ๋‹ค์–‘ํ•œ ๊ธฐ๊ธฐ๋ฅผ ์จ. ์ €์‚ฌ์–‘ ๊ธฐ๊ธฐ์—์„œ๋„ ํ…Œ์ŠคํŠธ ํ•„์ˆ˜!

3. ๋„คํŠธ์›Œํฌ ์กฐ๊ฑด ๋ฌด์‹œ
๋น ๋ฅธ WiFi ํ™˜๊ฒฝ์—์„œ๋งŒ ํ…Œ์ŠคํŠธํ•˜์ง€ ๋ง๊ณ , 3G๋‚˜ ๋А๋ฆฐ ๋„คํŠธ์›Œํฌ ํ™˜๊ฒฝ๋„ ์‹œ๋ฎฌ๋ ˆ์ด์…˜ํ•ด๋ด.

4. ๋ฐฐํ„ฐ๋ฆฌ ์ตœ์ ํ™”๋ฅผ ๋งˆ์ง€๋ง‰์— ์ƒ๊ฐ
์ฒ˜์Œ ์„ค๊ณ„ ๋‹จ๊ณ„๋ถ€ํ„ฐ ๋ฐฐํ„ฐ๋ฆฌ ์ตœ์ ํ™”๋ฅผ ๊ณ ๋ คํ•ด์•ผ ํ•ด. ๋‚˜์ค‘์— ๊ณ ์น˜๋ฉด ํ›จ์”ฌ ํž˜๋“ค์–ด.

5. ์„ฑ๋Šฅ ์ธก์ • ์—†์ด ์ตœ์ ํ™”
"์ด๊ฒŒ ๋А๋ฆด ๊ฒƒ ๊ฐ™์•„์„œ" ๊ฐ์œผ๋กœ ์ตœ์ ํ™”ํ•˜์ง€ ๋ง๊ณ , ๋ฐ˜๋“œ์‹œ ํ”„๋กœํŒŒ์ผ๋Ÿฌ๋กœ ๋ณ‘๋ชฉ ์ง€์ ์„ ์ฐพ์•„์„œ ์ตœ์ ํ™”ํ•ด!

๐ŸŽฏ ์„ฑ๋Šฅ ๋ชฉํ‘œ ์„ค์ •ํ•˜๊ธฐ

์ตœ์ ํ™”๋ฅผ ํ•˜๋ ค๋ฉด ๋ชฉํ‘œ๊ฐ€ ์žˆ์–ด์•ผ ํ•ด. ๋ง‰์—ฐํ•˜๊ฒŒ "๋น ๋ฅด๊ฒŒ ๋งŒ๋“ค์ž"๊ฐ€ ์•„๋‹ˆ๋ผ ๊ตฌ์ฒด์ ์ธ ์ˆ˜์น˜ ๋ชฉํ‘œ๋ฅผ ์„ธ์›Œ์•ผ ํ•ด!

์ง€ํ‘œ ๋ชฉํ‘œ๊ฐ’ ์ธก์ • ๋ฐฉ๋ฒ•
์•ฑ ์‹œ์ž‘ ์‹œ๊ฐ„ (Cold Start) 2์ดˆ ์ด๋‚ด Android Vitals, Xcode Instruments
ํ”„๋ ˆ์ž„ ๋ ˆ์ดํŠธ 60fps ์œ ์ง€ GPU Profiler, Xcode Core Animation
๋ฉ”๋ชจ๋ฆฌ ์‚ฌ์šฉ๋Ÿ‰ 200MB ์ดํ•˜ Android Profiler, Instruments
๋ฐฐํ„ฐ๋ฆฌ ์†Œ๋ชจ ์‹œ๊ฐ„๋‹น 5% ์ดํ•˜ Battery Historian, Energy Gauge
๋„คํŠธ์›Œํฌ ์š”์ฒญ ์‹œ๊ฐ„ 3์ดˆ ์ด๋‚ด Charles Proxy, Network Profiler
ANR ๋ฐœ์ƒ๋ฅ  0.1% ์ดํ•˜ Firebase Crashlytics, Play Console

๐Ÿ”„ ์ง€์†์ ์ธ ์„ฑ๋Šฅ ๋ชจ๋‹ˆํ„ฐ๋ง

์ตœ์ ํ™”๋Š” ํ•œ ๋ฒˆ ํ•˜๊ณ  ๋์ด ์•„๋‹ˆ์•ผ!
์•ฑ์ด ์—…๋ฐ์ดํŠธ๋ ์ˆ˜๋ก ์ƒˆ๋กœ์šด ๊ธฐ๋Šฅ์ด ์ถ”๊ฐ€๋˜๊ณ , ๊ทธ ๊ณผ์ •์—์„œ ์„ฑ๋Šฅ์ด ์ €ํ•˜๋  ์ˆ˜ ์žˆ์–ด.
CI/CD ํŒŒ์ดํ”„๋ผ์ธ์— ์„ฑ๋Šฅ ํ…Œ์ŠคํŠธ๋ฅผ ํฌํ•จ์‹œ์ผœ์„œ ์ง€์†์ ์œผ๋กœ ๋ชจ๋‹ˆํ„ฐ๋งํ•ด์•ผ ํ•ด.

// GitHub Actions์— ์„ฑ๋Šฅ ํ…Œ์ŠคํŠธ ์ถ”๊ฐ€ ์˜ˆ์‹œ
# .github/workflows/performance.yml
name: Performance Tests

on:
  pull_request:
    branches: [ main ]

jobs:
  benchmark:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      
      - name: Run Benchmark Tests
        run: ./gradlew :app:connectedBenchmarkAndroidTest
        
      - name: Upload Results
        uses: actions/upload-artifact@v3
        with:
          name: benchmark-results
          path: app/build/outputs/connected_android_test_additional_output/

๐ŸŽ‰ ๋งˆ๋ฌด๋ฆฌ โ€” ์ตœ์ ํ™”๋Š” ์Šต๊ด€์ด๋‹ค

์ž, ์—ฌ๊ธฐ๊นŒ์ง€ ์™”์–ด! ์—„์ฒญ๋‚œ ์–‘์ด์—ˆ์ง€? ใ…‹ใ…‹ใ…‹
๊ทผ๋ฐ ์ด๊ฑธ ๋‹ค ์™ธ์šธ ํ•„์š”๋Š” ์—†์–ด. ์ค‘์š”ํ•œ ๊ฑด ํ•ต์‹ฌ ์›์น™์„ ์ดํ•ดํ•˜๊ณ  ์Šต๊ด€ํ™”ํ•˜๋Š” ๊ฑฐ์•ผ.

๐ŸŽฏ ์•ฑ ์„ฑ๋Šฅ ์ตœ์ ํ™”์˜ 3๊ฐ€์ง€ ํ™ฉ๊ธˆ ์›์น™
์›์น™ 1: ์ธก์ • ๋จผ์ €, ์ตœ์ ํ™” ๋‚˜์ค‘
๊ฐ์œผ๋กœ ์ตœ์ ํ™”ํ•˜์ง€ ๋ง๊ณ  ํ”„๋กœํŒŒ์ผ๋Ÿฌ๋กœ ๋ณ‘๋ชฉ์„ ์ฐพ์•„๋ผ.

์›์น™ 2: ๋ฉ”์ธ ์Šค๋ ˆ๋“œ๋Š” UI๋งŒ
๋ฌด๊ฑฐ์šด ์ž‘์—…์€ ๋ฌด์กฐ๊ฑด ๋ฐฑ๊ทธ๋ผ์šด๋“œ ์Šค๋ ˆ๋“œ์—์„œ ์ฒ˜๋ฆฌํ•ด๋ผ.

์›์น™ 3: ํ•„์š”ํ•œ ๊ฒƒ๋งŒ, ํ•„์š”ํ•  ๋•Œ๋งŒ
์œ„์น˜, ์„ผ์„œ, ๋„คํŠธ์›Œํฌ ๋“ฑ ๋ฆฌ์†Œ์Šค๋Š” ํ•„์š”ํ•  ๋•Œ๋งŒ ์‚ฌ์šฉํ•˜๊ณ  ์ฆ‰์‹œ ํ•ด์ œํ•ด๋ผ.

์•ฑ ๊ฐœ๋ฐœ์€ ๋‹จ์ˆœํžˆ ๊ธฐ๋Šฅ์„ ๋งŒ๋“œ๋Š” ๊ฒŒ ์•„๋‹ˆ์•ผ.
์‚ฌ์šฉ์ž๊ฐ€ ์พŒ์ ํ•˜๊ฒŒ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋Š” ๊ฒฝํ—˜์„ ๋งŒ๋“œ๋Š” ๊ฒƒ์ด ์ง„์งœ ๋ชฉํ‘œ๊ฑฐ๋“ .
๋ฉ”๋ชจ๋ฆฌ ๊ด€๋ฆฌ, ๋ฐฐํ„ฐ๋ฆฌ ์ตœ์ ํ™”, ๋ Œ๋”๋ง ์ตœ์ ํ™” โ€” ์ด ์„ธ ๊ฐ€์ง€๋ฅผ ์ž˜ ์žก์œผ๋ฉด ์•ฑ์˜ ํ’ˆ์งˆ์ด ์™„์ „ํžˆ ๋‹ฌ๋ผ์ ธ!

์˜ค๋Š˜ ๋ฐฐ์šด ๋‚ด์šฉ๋“ค์„ ํ•˜๋‚˜์”ฉ ์ ์šฉํ•ด๋ณด๋ฉด์„œ ์‹ค๋ ฅ์„ ํ‚ค์›Œ๋ด.
์ฒ˜์Œ์—” ์–ด๋ ต๊ฒŒ ๋А๊ปด์ ธ๋„ ํ•˜๋‹ค ๋ณด๋ฉด ์ž์—ฐ์Šค๋Ÿฝ๊ฒŒ ๋ชธ์— ๋ฐฐ๊ฑฐ๋“ .
๊ทธ๋ฆฌ๊ณ  ๋ง‰ํžˆ๋Š” ๋ถ€๋ถ„์ด ์žˆ์œผ๋ฉด ์žฌ๋Šฅ๋„ท์—์„œ ๊ฒฝํ—˜ ๋งŽ์€ ๊ฐœ๋ฐœ์ž๋“ค์˜ ๋„์›€์„ ๋ฐ›๋Š” ๊ฒƒ๋„ ์ข‹์€ ๋ฐฉ๋ฒ•์ด์•ผ! ๐Ÿš€

๐Ÿ’ก ๋งˆ์ง€๋ง‰ ๊ฟ€ํŒ! ์„ฑ๋Šฅ ์ตœ์ ํ™”๋ฅผ ๊ณต๋ถ€ํ•  ๋•Œ ๊ฐ€์žฅ ์ข‹์€ ๋ฐฉ๋ฒ•์€ ์‹ค์ œ ์•ฑ์„ ๋งŒ๋“ค๋ฉด์„œ ํ”„๋กœํŒŒ์ผ๋Ÿฌ๋ฅผ ์ผœ๋‘๋Š” ๊ฒƒ์ด์•ผ.
์ด๋ก ์œผ๋กœ๋งŒ ๊ณต๋ถ€ํ•˜๋Š” ๊ฒƒ๋ณด๋‹ค ์ง์ ‘ ์ˆซ์ž๊ฐ€ ๋ณ€ํ•˜๋Š” ๊ฑธ ๋ณด๋ฉด์„œ ๋ฐฐ์šฐ๋ฉด ํ›จ์”ฌ ๋นจ๋ฆฌ ์ดํ•ด๋ผ!
Android Studio Profiler๋‚˜ Xcode Instruments๋ฅผ ํ•ญ์ƒ ์ผœ๋‘๋Š” ์Šต๊ด€์„ ๋“ค์—ฌ๋ด. ์ง„์งœ ๋„์›€ ๋งŽ์ด ๋ผ ใ…‹ใ…‹ใ…‹

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

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

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

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