iOS의 Swift/Objective-C 코드는 빌드할 때 이미 CPU가 바로 이해할 수 있는 기계어로 번역되어 배포된다. 그래서 앱을 실행하면 CPU가 바이너리를 곧바로 읽고 실행한다. 중간 통역사가 없는 셈이다.
Android의 Kotlin/Java 코드는 "바이트코드"라는 중간 형태로 변환되어 배포되고, ART(Android Runtime)라는 실행 엔진 위에서 돌아간다. 쉽게 말해 Android 앱은 "기계어로 바로 실행되는" 게 아니라, ART라는 중간 통역사 위에서 동작하는 구조이다. Android 7.0 이후로는 설치 시점에 일부를 미리 기계어로 바꿔두긴 하지만, 여전히 런타임이라는 중간 계층이 존재하기 때문에 클래스를 불러오고, 타입을 확인하고, 예외를 처리하는 추가 작업이 항상 발생한다.
앱을 막 켠 직후에는 이 통역 작업이 더 많이 돌아가기 때문에 초기 실행(cold start) 속도에서 iOS가 유리하다.
이 부분이 메모리 사용량 차이에 가장 큰 영향을 줍니다.
iOS는 ARC(Automatic Reference Counting)라는 방식을 쓴다. 각 객체가 "나를 몇 명이 참조하고 있는지"를 카운트하고, 그 숫자가 0이 되는 바로 그 순간 즉시 메모리가 해제된다. 컴파일러가 알아서 해제 코드를 넣어주기 때문에, 별도의 청소 담당 스레드가 없고 앱이 멈칫하는 현상도 없다.
Android는 GC(Garbage Collection) 방식을 쓴다. "쓰레기 수거차"가 주기적으로 돌아다니면서 더 이상 안 쓰는 객체를 찾아 정리하는 구조인 것이다. 그래서 이미 필요 없어진 객체라도 GC가 오기 전까지는 메모리에 그대로 남아 있다. 또 GC가 효율적으로 동작하려면 실제 쓰는 양보다 1.5~2배 정도 여유 공간을 확보해두어야 한다. 거기에 객체마다 GC가 관리용으로 붙이는 정보(헤더)까지 더해지므로, 똑같은 데이터를 다뤄도 Android 쪽이 메모리를 더 많이 차지하는 게 구조적 이유이다.
Apple은 칩(A/M 시리즈), OS(iOS), 개발 도구, 프레임워크를 전부 자기네가 직접 만든다. 그래서 모든 계층이 서로를 염두에 두고 최적화되어 있다. 예를 들어 OS가 "이 앱은 어떤 코어에서 돌리는 게 효율적인지"를 칩 구조에 딱 맞게 판단할 수 있다.
Android는 반대로 수천 종의 다양한 기기를 전부 지원해야 한다. Qualcomm, MediaTek, Samsung, Google 등 제조사마다 칩이 다르고, 삼성 One UI, 샤오미 MIUI 같은 제조사 커스텀 UI가 기본 Android 위에 또 한 겹 덮여 있다. 이 "여러 기기에 두루 맞추기 위한 추상화 계층"과 "제조사가 추가한 스킨"이 메모리와 CPU를 더 소모하게 만든다.
iOS는 화면을 그릴 때 Core Animation이라는 시스템이 GPU로 직접 합성합니다. 개발자가 다루는 UIView는 얇은 껍데기일 뿐이고, 실제 그림 작업은 별도 프로세스가 효율적으로 처리합니다. 그래서 60/120fps를 안정적으로 뽑아내기 쉽습니다.
Android의 전통적인 View 시스템은 XML을 읽어오고, 크기를 재고, 위치를 잡고, 그리고, 합성하는 여러 단계를 거칩니다. 단계마다 객체가 새로 만들어지다 보니 GC 부담이 늘어나고, 프레임이 끊기는 현상도 생기기 쉽습니다. 최근 나온 Jetpack Compose가 이 부분을 많이 개선하긴 했지만, 여전히 Android 런타임 위에서 돌아가는 건 동일합니다.
iOS는 매우 엄격하다. 홈 화면으로 나가면 대부분의 앱이 거의 즉시 얼려(frozen) 버려지고, 특별히 허가받은 작업만 백그라운드에서 돌 수 있다. 그래서 지금 사용 중인 앱이 시스템 자원을 거의 독차지할 수 있다.
Android는 훨씬 관대합니다. Service, WorkManager, JobScheduler 같은 다양한 백그라운드 실행 수단을 제공한다. 최근엔 Doze 모드로 많이 제약을 걸긴 했지만, 전체적으로 동시에 살아있는 프로세스가 더 많고, 그만큼 전경 앱이 쓸 수 있는 여유 메모리가 줄어든다.
RN 개발자 입장에서는 아래가 체감 차이를 만든다.
JS 엔진이 차지하는 비용이 다름
iOS는 시스템에 JavaScriptCore가 이미 내장되어 있어서 그냥 가져다 쓰면 된다. 추가 바이너리가 거의 안 붙는다. Android는 JSC든 Hermes든 엔진 자체를 앱 안에 함께 넣어서 배포해야 하므로 앱 크기도 커지고 초기 메모리도 더 듭니다. (Hermes가 이걸 많이 줄여주긴 합니다.)
JS에서 네이티브로 넘어가는 경로가 더 김
iOS는 JS → C++ → Objective-C로 바로 이어지는 짧은 경로인 반면, Android는 JS → C++ → JNI → Java(JVM) → Android 프레임워크로 한 단계가 더 낀다. 이 차이가 브리지 호출이 많을수록 누적된다. New Architecture(JSI, Fabric, TurboModules)에서 많이 줄어들긴 했지만, 구조적으로 Android 쪽 경로가 여전히 깊다.
뷰 하나의 무게도 다르다. RN의 <View> 하나가 iOS에서는 가벼운 UIView 하나로 매핑되는데, Android에서는 ViewGroup과 이런저런 속성 처리가 함께 붙어 실제 뷰 계층이 더 두꺼워지는 경향이 있다.
iOS는 Apple이 직접 설계한 스토리지와 파일 시스템(APFS) 덕분에 파일 읽기 속도가 매우 빠르고, 앱을 띄울 때 필요한 정보를 미리 캐싱해두는 최적화도 들어가 있다.
Android는 Zygote라는 공용 프로세스를 복제해서 앱을 띄우는 방식으로 JVM 초기화 비용을 많이 줄였지만, 그래도 DEX 파일 로딩, 클래스 검증, 리소스 파일 파싱 같은 추가 작업이 실행 시점에 발생한다.
iOS가 빠르고 가벼운 이유는 하나가 아니라, "미리 기계어로 번역된 코드 + 즉시 메모리 해제하는 ARC + 자사 하드웨어에 딱 맞춘 최적화 + 짧은 렌더링 경로 + 엄격한 백그라운드 정책"이 쌓인 결과이다. 반대로 Android는 수많은 기기를 한 번에 지원해야 하는 숙명 때문에 중간 계층과 GC 기반 메모리 관리라는 구조적 비용을 떠안고 있다.
다만 이 격차는 과거보다 많이 줄었다. 실무에서는 Hermes 활성화, R8 최적화, Baseline Profiles, 이미지 메모리 관리, New Architecture 도입 같은 RN 측 튜닝이 체감 차이를 좁히는 데 훨씬 큰 영향을 준다.