Ver3.0 ๐ฑ ์ฑ ์ฑ๋ฅ ์ต์ ํ

๐ฑ ์ฑ ์ฑ๋ฅ ์ต์ ํ
๋ฉ๋ชจ๋ฆฌ ๊ด๋ฆฌ & ๋ฐฐํฐ๋ฆฌ ์๋ชจ ์ต์ํ ์์ ์ ๋ณต
๐ ์ง์ง ๊ฐ๋ฐ์๋ผ๋ฉด ์์์ผ ํ ํผํฌ๋จผ์ค ํ๋์ ๋ชจ๋ ๊ฒ
์ผ ์์งํ ๋งํด๋ด ๐
์ฑ ๋ง๋ค๊ณ ๋์ "์ ์ด๋ ๊ฒ ๋ฐฐํฐ๋ฆฌ ๋นจ๋ฆฌ ๋ณ์?" "์ ์ฑ์ด ๋ฒ๋ฒ
๊ฑฐ๋ ค?" ์ด๋ฐ ๋ง ๋ค์ด๋ณธ ์ ์์ง ์์?
ใ
ใ
ใ
๊ฐ๋ฐ์๋ผ๋ฉด ํ ๋ฒ์ฏค์ ๋ค ๊ฒช์ด๋ดค์ ๊ทธ ์ฐํ ๊ฒฝํ... ์ค๋์ ๊ทธ ๊ณ ํต์์ ํด๋ฐฉ๋๋ ๋ ์ด์ผ!
์ฑ ์ฑ๋ฅ ์ต์ ํ๋ ๊ทธ๋ฅ "์ฝ๋ ์ ์ง๋ฉด ๋๋ ๊ฑฐ ์๋?" ์์ค์ด ์๋์ผ.
๋ฉ๋ชจ๋ฆฌ ๋์, ๋ฐฐํฐ๋ฆฌ ๋๋ ์ธ, ๋ ๋๋ง ์ง์ฐ โ ์ด ์ธ ๊ฐ์ง ์
๋น์ ์ ๋๋ก ์ดํดํ๊ณ ์ก์์ผ ์ง์ง ํ๋ฆฌํฐ ์๋ ์ฑ์ด ๋์ค๊ฑฐ๋ .
์, ๊ทธ๋ผ ์ง๊ธ๋ถํฐ ์ฐํ๊ฒ ํ๊ณ ๋ค์ด๋ณด์๊ณ ! ๐ฅ
๐ง Part 1. ๋ฉ๋ชจ๋ฆฌ ๊ด๋ฆฌ โ ์ฑ์ด ํฐ์ง๋ ์ง์ง ์ด์
ใ
ใ
ใ
์ฑ์ด ๊ฐ์๊ธฐ ๊บผ์ง๋ ๊ฒฝํ... ๊ฐ๋ฐ์๋ ์ ์ ๋ ๋ค ์ซ์ดํ์์.
๊ทผ๋ฐ ์ด๊ฒ ๋๋ถ๋ถ ๋ฉ๋ชจ๋ฆฌ ๋ฌธ์ ์ผ. ํนํ ๋ชจ๋ฐ์ผ ํ๊ฒฝ์์๋ ๋ฐ์คํฌํ์ด๋ ๋ฌ๋ฆฌ ๋ฉ๋ชจ๋ฆฌ๊ฐ ์ ํ์ ์ด๊ฑฐ๋ .
Android๋ ๋ณดํต ์ฑ๋น 256MB~512MB, iOS๋ ๋ ์๊ฒฉํ๊ฒ ๊ด๋ฆฌํด. ์ด ํ๊ณ๋ฅผ ๋์ผ๋ฉด? ์ฑ์ด ๊ทธ๋ฅ ์ฃฝ์ด๋ฒ๋ ค ๐ฑ
๐ ๋ฉ๋ชจ๋ฆฌ ๋์(Memory Leak)๋ ๋ญ๊ฐ์?
๋ฉ๋ชจ๋ฆฌ ๋์๋ ์ฝ๊ฒ ๋งํ๋ฉด ๋ ์ด์ ํ์ ์๋ ๊ฐ์ฒด๊ฐ ๋ฉ๋ชจ๋ฆฌ์์ ํด์ ๋์ง ์๊ณ ๊ณ์ ๋จ์์๋ ํ์์ด์ผ.
GC(Garbage Collector)๊ฐ ์์ด๋ ์ฐธ์กฐ๊ฐ ์ด์์์ผ๋ฉด ๋ชป ์ง์ฐ๊ฑฐ๋ . ์ด๊ฒ ์์ด๋ฉด ์ฑ์ด ์ ์ ๋๋ ค์ง๋ค๊ฐ ๊ฒฐ๊ตญ OOM(Out Of Memory) ํฌ๋์๊ฐ ๋ฐ์ํด.
์ฐธ์กฐ๊ฐ ์ด์์์ผ๋ฉด GC๋ ์ ๋ชป ๋๊ฑฐ๋ ใ ใ
๋ํ์ ์ธ ๋ฉ๋ชจ๋ฆฌ ๋์ ํจํด๋ค์ ์์๋ณด์!
๐ด Android์์ ํํ ๋ฉ๋ชจ๋ฆฌ ๋์ ํจํด
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 ์ฌ์ฉ!
}
}
}
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()
}
}
๋ฆฌ์ค๋/์ฝ๋ฐฑ ๋ฑ๋ก ํ ํด์ ์ ํจ
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)
}
๐ ๏ธ ๋ฉ๋ชจ๋ฆฌ ํ๋กํ์ผ๋ง ๋๊ตฌ
์ฝ๋๋ง ์ ์ง๋ ๊ฒ ์๋๋ผ ์ค์ ๋ก ๋ฉ๋ชจ๋ฆฌ๊ฐ ์ด๋ป๊ฒ ์ฌ์ฉ๋๋์ง ์ธก์ ํด์ผ ํด.
๋๊ตฌ ์์ด ๊ฐ์ผ๋ก ์ต์ ํํ๋ ๊ฑด ๋ ๊ฐ๊ณ ์ด์ ํ๋ ๊ฑฐ๋ ๊ฐ์ ใ
ใ
ใ
| ํ๋ซํผ | ๋๊ตฌ๋ช | ์ฃผ์ ๊ธฐ๋ฅ |
|---|---|---|
| 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()
}
}
โก 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. ์ค์ ์ต์ ํ ์ฒดํฌ๋ฆฌ์คํธ
์, ์ด์ ์ค์ ์ด์ผ! ์ฑ ์ถ์ ์ ์ ๋ฐ๋์ ํ์ธํด์ผ ํ ์ฒดํฌ๋ฆฌ์คํธ๋ฅผ ์ ๋ฆฌํ์ด.
์ฌ๋ฅ๋ท ๊ฐ์ ํ๋ซํผ์์ ์ฑ ๊ฐ๋ฐ ๊ด๋ จ ์ฌ๋ฅ์ ์ฐพ๊ฑฐ๋ ์ ๊ณตํ ๋๋ ์ด๋ฐ ์ต์ ํ ์ง์์ด ์์ผ๋ฉด ํจ์ฌ ๊ฒฝ์๋ ฅ ์์ด!
๐ฏ 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 ํต์ฌ ์ต์ ํ ํฌ์ธํธ
๋ฉ๋ชจ๋ฆฌ 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
์ง์์ธ์ ์ฒ - ์ง์ ์ฌ์ฐ๊ถ ๋ณดํธ ๊ณ ์ง
์ง์ ์ฌ์ฐ๊ถ ๋ณดํธ ๊ณ ์ง
- ์ ์๊ถ ๋ฐ ์์ ๊ถ: ๋ณธ ์ปจํ ์ธ ๋ ์ฌ๋ฅ๋ท์ ๋ ์ AI ๊ธฐ์ ๋ก ์์ฑ๋์์ผ๋ฉฐ, ๋ํ๋ฏผ๊ตญ ์ ์๊ถ๋ฒ ๋ฐ ๊ตญ์ ์ ์๊ถ ํ์ฝ์ ์ํด ๋ณดํธ๋ฉ๋๋ค.
- AI ์์ฑ ์ปจํ ์ธ ์ ๋ฒ์ ์ง์: ๋ณธ AI ์์ฑ ์ปจํ ์ธ ๋ ์ฌ๋ฅ๋ท์ ์ง์ ์ฐฝ์๋ฌผ๋ก ์ธ์ ๋๋ฉฐ, ๊ด๋ จ ๋ฒ๊ท์ ๋ฐ๋ผ ์ ์๊ถ ๋ณดํธ๋ฅผ ๋ฐ์ต๋๋ค.
- ์ฌ์ฉ ์ ํ: ์ฌ๋ฅ๋ท์ ๋ช ์์ ์๋ฉด ๋์ ์์ด ๋ณธ ์ปจํ ์ธ ๋ฅผ ๋ณต์ , ์์ , ๋ฐฐํฌ, ๋๋ ์์ ์ ์ผ๋ก ํ์ฉํ๋ ํ์๋ ์๊ฒฉํ ๊ธ์ง๋ฉ๋๋ค.
- ๋ฐ์ดํฐ ์์ง ๊ธ์ง: ๋ณธ ์ปจํ ์ธ ์ ๋ํ ๋ฌด๋จ ์คํฌ๋ํ, ํฌ๋กค๋ง, ๋ฐ ์๋ํ๋ ๋ฐ์ดํฐ ์์ง์ ๋ฒ์ ์ ์ฌ์ ๋์์ด ๋ฉ๋๋ค.
- AI ํ์ต ์ ํ: ์ฌ๋ฅ๋ท์ AI ์์ฑ ์ปจํ ์ธ ๋ฅผ ํ AI ๋ชจ๋ธ ํ์ต์ ๋ฌด๋จ ์ฌ์ฉํ๋ ํ์๋ ๊ธ์ง๋๋ฉฐ, ์ด๋ ์ง์ ์ฌ์ฐ๊ถ ์นจํด๋ก ๊ฐ์ฃผ๋ฉ๋๋ค.
์์ง ๋๊ธ์ด ์์ต๋๋ค.