테마 선택

📚 지식 노트

AI·하드웨어·주변기기·게임·컴퓨터 시스템·이론 컴퓨터 과학·수학·개발과 운영·보안·응용 분야에 대해 알아 두면 좋은 내용을 정리한 노트다. 위에서 분야를 고르고 아래에서 노트를 고른다.

모바일 앱 개발

주머니 속 컴퓨터는 배터리·작은 화면·끊기는 네트워크·엄격한 권한이라는 조건 아래서 돈다. iOS와 안드로이드의 구조, OS가 쥔 앱 생명 주기, 크로스플랫폼의 선택지, 화면 단위와 프레임 예산, 오프라인 동기화와 푸시 알림, 서명과 심사까지 작은 예를 직접 계산한 그림으로 따라간다.

목차 (14)
  1. 모바일이 다른 점
  2. iOS의 구조: Swift, SwiftUI와 UIKit, 샌드박스
  3. 안드로이드의 구조: Kotlin, Jetpack Compose, ART
  4. 앱 생명 주기: 앱을 끄는 것은 OS다
  5. 네이티브, 크로스플랫폼, PWA
  6. 화면 밀도와 단위: pt와 dp
  7. 상태와 오프라인: 캐시와 동기화 충돌
  8. 푸시 알림: APNs와 FCM
  9. 권한과 개인정보: 앱 추적 투명성
  10. 딥 링크: 유니버설 링크와 앱 링크
  11. 성능: 프레임 예산과 배터리
  12. 빌드, 서명, 심사, 배포
  13. 테스트와 크래시 리포트
  14. 핵심 자료

모바일이 다른 점

휴대폰 앱도 결국 프로그램이지만, 데스크톱이나 웹과 같은 습관으로 만들면 곧 벽에 부딪힌다. 네 가지 조건이 다르다. 첫째, 배터리. CPU·화면·무선 칩이 쓰는 전기가 곧 사용자의 하루이고, 그래서 OS는 화면에 없는 앱의 실행을 적극적으로 멈추거나 끊는다. 둘째, 화면과 손가락. 화면은 작고 손끝은 마우스 포인터보다 훨씬 굵다. 세로·가로 회전, 접는 화면, 분할 화면도 있다. 셋째, 네트워크. 지하철·엘리베이터에서 연결은 수시로 끊기고, 이동 통신은 느리거나 요금이 든다. 넷째, 권한과 통제. 카메라·위치·연락처는 사용자의 허락을 받아야 하고, 배포는 앱 스토어의 심사를 거친다.

모바일 앱이 데스크톱·웹과 다른 네 가지 제약: 배터리, 작은 화면과 손가락, 끊기는 네트워크, 권한과 스토어 심사배터리모든 일이 전기OS가 배경 작업을 끊는다화면 · 손가락작은 화면, 큰 손끝세로·가로·접는 화면네트워크지하철에서 끊긴다느리고 비싼 회선권한 · 심사카메라·위치는 허락스토어가 배포를 쥔다
모바일 앱을 다르게 만드는 네 조건. 아래 절들은 대부분 이 넷 가운데 하나에 대한 답이다.

이 노트는 두 플랫폼이 이 조건을 어떻게 다루는지에 집중한다. 웹 기술 자체는 웹 개발 노트, 터치 목표 크기 같은 사용성 원리는 HCI와 UX 노트에 있다.

iOS의 구조: Swift, SwiftUI와 UIKit, 샌드박스

iOS 앱은 주로 Swift로 쓴다(오래된 코드에는 Objective-C가 남아 있다). 화면을 만드는 틀은 둘이다. 오래 써 온 UIKit은 뷰 객체를 만들고 속성을 바꾸며 화면을 직접 조작하는 명령형 방식이고, SwiftUI는 "상태가 이러면 화면은 이렇다"를 선언하면 상태가 바뀔 때 프레임워크가 다시 그리는 선언형 방식이다. 둘은 한 앱 안에서 섞어 쓸 수 있어, 새 화면은 SwiftUI로 짜고 오래된 화면은 UIKit으로 두는 일이 흔하다. 개발은 macOS의 Xcode에서 한다.

보안의 중심은 샌드박스다. 앱마다 자기 컨테이너 폴더가 있고, 앱은 그 밖의 데이터를 읽거나 쓸 수 없다. 앱 심사 지침도 앱이 정해진 컨테이너 밖에 읽고 쓰지 말라고 못 박는다. 다른 앱의 파일은 물론 사진·연락처 같은 공용 데이터도 파일로 직접 열지 못하고, 권한을 받아 시스템 API를 통해서만 얻는다. 그래서 iOS에서는 "파일을 아무 데나 저장하는" 데스크톱식 설계가 처음부터 안 된다.

왼쪽은 Swift 앱 코드, SwiftUI와 UIKit, 시스템 프레임워크, 커널이 위에서 아래로 쌓인 층. 오른쪽은 앱 A와 앱 B가 각자 컨테이너 폴더만 쓰고 서로의 폴더나 시스템 영역에는 손대지 못하는 샌드박스앱 코드 (Swift)SwiftUI · UIKit시스템 프레임워크iOS 커널 · 드라이버기기 저장 공간앱 A 컨테이너DocumentsLibrary · Cachestmp앱 B 컨테이너A는 못 읽는다밖의 것(사진·연락처)은 권한을 받아 API로만
iOS 앱의 층(왼쪽)과 샌드박스(오른쪽). 앱 A는 자기 컨테이너만 보고, 앱 B의 폴더나 시스템 영역에는 손대지 못한다.

안드로이드의 구조: Kotlin, Jetpack Compose, ART

안드로이드 앱의 주 언어는 Kotlin이다(Java 코드도 함께 돈다). 화면은 XML 레이아웃과 View 객체로 만드는 옛 방식과, 안드로이드가 권장하는 최신 UI 툴킷인 Jetpack Compose가 있다. Compose는 SwiftUI처럼 선언형이라, UI를 Kotlin 함수로 묘사하면 상태가 바뀔 때 화면이 저절로 갱신된다. 개발 도구는 Android Studio다.

실행 쪽은 다르다. 소스는 JVM 바이트코드를 거쳐 DEX(Dalvik 실행 파일) 형식으로 묶이고, 기기의 ART(Android Runtime)가 이를 실행한다. ART는 설치 때 dex2oat 도구로 DEX를 그 기기의 기계어로 미리 컴파일(AOT)해 실행을 빠르게 한다. 그 아래에는 안드로이드 프레임워크와 리눅스 커널이 있고, 앱마다 별도의 리눅스 사용자와 프로세스로 돌아 서로 격리된다. 제조사와 기기가 매우 다양하다는 점도 iOS와 다른 큰 조건이다. 화면 크기·밀도·OS 판이 제각각이라 테스트 범위가 넓어진다.

위는 빌드: Kotlin 소스가 컴파일되어 DEX가 되고 APK나 AAB로 묶인다. 아래는 기기: 앱 프로세스가 ART 위에서 돌고, ART는 안드로이드 프레임워크와 리눅스 커널 위에 있다개발자 컴퓨터에서Kotlin 소스Compose UI컴파일JVM 바이트코드DEXclasses.dex패키지APK · AAB설치기기에서앱 프로세스ART (AOT · JIT)안드로이드 프레임워크리눅스 커널dex2oat가 설치 때기계어로 미리 컴파일
안드로이드 앱의 길. 개발자 컴퓨터에서 DEX로 묶여 패키지가 되고, 기기에서는 ART가 기계어로 바꿔 리눅스 커널 위에서 돌린다.

앱 생명 주기: 앱을 끄는 것은 OS다

데스크톱 프로그램은 사용자가 닫을 때 끝난다. 모바일 앱은 아니다. 사용자는 홈으로 나갈 뿐이고, 앱을 언제 멈추고 언제 메모리에서 지울지는 OS가 정한다. 앱이 할 일은 상태가 바뀔 때 OS가 불러 주는 콜백에서 알맞게 저장하고 정리하는 것이다.

  • iOS: 화면 단위인 장면(scene)이 연결 전 → 포그라운드(비활성·활성) → 백그라운드 → 정지 상태를 오간다. 비활성은 화면에는 보이지만 입력을 받지 않는 잠깐의 상태(알림 센터를 내렸을 때 등)다. 사용자가 앱을 떠나면 장면은 백그라운드를 거쳐 정지 상태가 되고, 시스템은 자원이 필요하면 백그라운드나 정지 상태의 장면을 언제든 끊어 연결 전 상태로 돌린다.
  • 안드로이드: 액티비티가 onCreate → onStart → onResume로 화면에 나오고, onPause → onStop으로 물러난다. 메모리가 모자라면 시스템은 액티비티 하나가 아니라 프로세스를 통째로 정리하며, 백그라운드(Stopped)에 있는 앱이 먼저 대상이 된다. 사용자가 돌아오면 저장해 둔 인스턴스 상태로 액티비티를 다시 만든다.
위 줄은 iOS 장면 상태 다섯, 아래 줄은 안드로이드 액티비티 상태 여섯. 단계마다 사용자가 한 일과 현재 상태 칸이 색칠된다1. 아이콘을 눌러 실행2. 알림 센터를 내림3. 알림 센터를 닫음4. 홈으로 나감5. 조금 뒤6. 메모리가 모자라 OS가 정리7. 다시 아이콘을 누름사용자가 한 일iOS 장면(scene)연결전포그라운드비활성포그라운드활성백그라운드정지안드로이드 액티비티CreatedStartedResumedPausedStoppedDestroyed콜백: onCreate → onStart → onResume콜백: onPause콜백: onResume콜백: onPause → onStop콜백 없음 — 상태 그대로프로세스가 사라짐 — 콜백 없이 끝날 수 있다콜백: onCreate(저장된 상태) → onStart → onResumeiOS 쪽 콜백 예: sceneDidBecomeActive · sceneDidEnterBackground
1 / 8

사용자가 한 일에 따라 두 플랫폼의 상태가 어떻게 바뀌는지 본다. 색칠된 칸이 지금 상태다.

앱 생명 주기. 앱은 스스로 끝나는 게 아니라 OS가 상태를 옮긴다. 백그라운드에 들어갈 때 저장해야, OS가 말없이 프로세스를 정리해도 사용자가 돌아왔을 때 그대로 이어진다.

안드로이드에는 함정이 하나 더 있다. 화면 회전 같은 구성 변경이 일어나면 기본적으로 액티비티를 없애고 새로 만든다. 액티비티 객체에 든 상태는 그대로 사라지므로, 상태는 회전을 넘어 살아남는 ViewModel에 두고, 가벼운 UI 상태는 프로세스가 죽어도 복원되는 rememberSaveable에 둔다. onPause는 아주 짧게 끝나야 하므로 무거운 저장은 onStop에서 한다. 두 플랫폼 모두 결론은 같다. "앱이 언제든 말없이 죽을 수 있다"고 가정하고 만든다.

네이티브, 크로스플랫폼, PWA

두 플랫폼에 같은 앱을 내려면 코드를 두 벌 짜거나, 한 벌로 두 곳을 덮는 도구를 쓴다. 고르는 기준은 "무엇을 공유하고, 화면은 누가 그리는가"다. Flutter는 Dart로 쓰고 자체 엔진(네이티브 플랫폼에서는 Impeller)이 모든 픽셀을 직접 그리며, 출시 빌드는 기계어로 컴파일한다. React Native는 React로 쓰지만 실제 화면은 플랫폼의 네이티브 뷰다. Kotlin Multiplatform은 구글이 공식 지원하는, 안드로이드와 iOS 사이에 비즈니스 로직을 나눠 쓰는 방식으로, 로직만 공유하고 UI는 각자 네이티브로 둘 수 있다(JetBrains의 Compose Multiplatform으로 UI까지 공유할 수도 있다). PWA는 아예 앱 스토어 밖에서, 설치할 수 있고 오프라인에서도 도는 웹 앱이다.

다섯 칸: 네이티브는 Swift·Kotlin으로 플랫폼 UI, Flutter는 Dart로 자체 엔진이 그림, React Native는 JS로 네이티브 뷰를 씀, Kotlin Multiplatform은 로직을 공유하고 UI는 각자 네이티브, PWA는 웹 기술로 브라우저가 그림네이티브SwiftKotlin플랫폼 UIFlutterDart자체 엔진이픽셀을 그림React NativeJS · TS네이티브 뷰Kotlin MP공유 로직Kotlin각자 네이티브(또는 공유 UI)PWAHTML · JS브라우저위: 개발자가 쓰는 언어 · 아래: 화면을 실제로 그리는 쪽
다섯 길의 차이는 위(무엇으로 쓰나)보다 아래(화면을 누가 그리나)에서 더 크다. 화면을 직접 그리면 두 플랫폼이 똑같아지고, 네이티브 뷰를 쓰면 플랫폼다운 동작을 거저 얻는다.
방식언어화면강점약점
네이티브Swift(iOS), Kotlin(안드로이드)플랫폼 UI 툴킷새 OS 기능을 바로 쓰고, 플랫폼다운 느낌과 성능두 벌의 코드와 두 팀
FlutterDart자체 엔진(Impeller)이 모든 픽셀을 그림두 플랫폼에서 똑같은 화면, 핫 리로드플랫폼 컨트롤의 모양·동작을 따라 그려야 함
React NativeJavaScript·TypeScript(React)네이티브 뷰웹 React 경험과 생태계를 그대로JS와 네이티브 사이 경계를 이해해야 함
Kotlin MultiplatformKotlin(공유 로직)각 플랫폼 네이티브 UI(원하면 Compose Multiplatform으로 공유)로직만 나눠 쓰고 UI는 네이티브로, 조금씩 도입 가능iOS 쪽 팀도 Kotlin 코드를 다뤄야 함
PWAHTML·CSS·JavaScript브라우저스토어 없이 링크로 배포, 한 벌의 코드기기 기능과 백그라운드 동작이 브라우저가 허락하는 만큼

정답은 팀과 제품에 달려 있다. 카메라·AR·위젯처럼 OS 기능을 깊게 쓰거나 새 OS 기능을 첫날부터 써야 하면 네이티브가 편하다. 화면 위주의 업무 앱을 작은 팀이 두 플랫폼에 내야 한다면 크로스플랫폼이 경제적이다. 스토어 심사 없이 바로 고치고 링크로 나눠 주는 것이 중요하면 PWA를 검토한다. 서비스 워커·매니페스트 같은 PWA의 재료는 웹 개발 노트에 있다.

화면 밀도와 단위: pt와 dp

휴대폰 화면은 같은 크기에 픽셀 수가 제각각이다. 단추를 픽셀로 정하면 고밀도 화면에서는 손톱만 해진다. 그래서 두 플랫폼은 밀도와 무관한 단위로 레이아웃을 짠다. 안드로이드의 dp는 160dpi(mdpi)를 기준으로 1dp = 1px이 되는 단위이고, 실제 픽셀은 px = dp × (dpi / 160)이다. 48dp 단추는 xhdpi(약 320dpi)에서 96px, xxhdpi(약 480dpi)에서 144px가 된다. iOS의 pt(포인트)도 같은 생각으로, 기기의 배율(@2x·@3x)만큼 픽셀이 된다. 44pt는 @2x에서 88px, @3x에서 132px다.

안드로이드 48dp 단추가 mdpi에서 48픽셀, hdpi 72, xhdpi 96, xxhdpi 144, xxxhdpi 192픽셀이 되는 모습을 크기를 줄여 나란히 그린 그림과, iOS 44pt가 @2x에서 88픽셀, @3x에서 132픽셀이 된다는 줄같은 48dp를 밀도별로 그린 픽셀 수(그림은 1/2로 줄임)mdpi48pxhdpi72pxxhdpi96pxxxhdpi144pxxxxhdpi192pxiOS 44pt → @2x 88px · @3x 132pxpx = dp × dpi / 160
같은 48dp가 밀도 묶음마다 다른 픽셀 수가 된다. 손끝에 닿는 실제 크기는 거의 같고, 고밀도일수록 더 많은 픽셀로 더 선명하게 그릴 뿐이다.

그래서 비트맵 이미지는 밀도별로 여러 벌(안드로이드는 3:4:6:8:12:16 비율, iOS는 @2x·@3x)을 준비하거나, 크기에 상관없이 선명한 벡터로 둔다. 코드에서 dp를 px로 바꿀 때 공식을 직접 쓰지 말고 플랫폼이 주는 변환 함수를 쓰라는 것도 안드로이드 문서의 당부다. 화면 크기와 비율을 미리 보는 도구는 화면 비율 미리보기에 있다.

상태와 오프라인: 캐시와 동기화 충돌

네트워크가 끊길 수 있다면, 화면이 서버 응답을 직접 기다리게 짜서는 안 된다. 안드로이드의 오프라인 우선 지침은 로컬 저장소를 진실의 원천으로 둔다. 화면은 늘 로컬 데이터베이스(Room 등)를 읽고, 네트워크는 그 로컬 데이터를 갱신하는 일만 한다. 화면 위쪽 층이 네트워크와 직접 이야기하지 않으니, 연결 상태가 바뀌어도 화면이 보는 데이터는 일관된다.

쓰기는 성격에 따라 나눈다. 송금처럼 실시간으로 확인해야 하는 것은 온라인에서만, 분석 로그처럼 늦어도 되는 것은 큐에 쌓았다가, 할 일 목록처럼 잃으면 안 되는 것은 먼저 로컬에 쓰고 나중에 서버에 보낸다(안드로이드에서는 WorkManager가 연결을 기다렸다가 재시도한다). 마지막 경우에 충돌이 생긴다. 오프라인에서 고친 내용과 그사이 다른 기기가 고친 내용이 만나는 것이다. 모바일에서 흔한 해법은 쓰기마다 시각을 붙여 늦은 쪽을 남기는 마지막 쓰기 승리(last write wins)다.

휴대폰은 시각 10에 오프라인으로 '우유, 달걀'로, 태블릿은 시각 12에 '우유, 빵'로 같은 메모를 고쳤다. 서버의 마지막 쓰기 승리 규칙은 '우유, 빵'를 남기고 '우유, 달걀'는 버린다처음 메모: "우유"휴대폰(오프라인) · 시각 10"우유, 달걀"태블릿 · 시각 12"우유, 빵"다시 연결된 뒤서버늦은 쪽이 이긴다남는 것"우유, 빵"조용히 사라지는 것"우유, 달걀""달걀"을 지키려면 필드 단위 병합, 충돌 표시, 또는 CRDT 같은 다른 규칙이 필요하다
마지막 쓰기 승리는 단순하지만 진 쪽의 변경을 말없이 버린다. 아래 예에서 휴대폰이 더한 '달걀'은 사라진다. 결과는 동기화 함수로 계산했다.

잃으면 곤란한 데이터라면 필드 단위로 합치거나, 충돌을 사용자에게 보여 고르게 하거나, 순서와 무관하게 합쳐지는 자료 구조(CRDT)를 쓴다. 여러 복제본이 어긋나는 문제의 일반론은 분산 시스템 노트에서 다룬다.

푸시 알림: APNs와 FCM

앱이 꺼져 있을 때 소식을 전하려면 서버가 기기를 깨워야 한다. 그런데 앱마다 서버와 연결을 열어 두면 배터리가 버티지 못한다. 그래서 플랫폼이 하나의 연결을 대신 유지한다. 애플의 APNs(Apple Push Notification service)와 구글의 FCM(Firebase Cloud Messaging)이다. 앱 서버는 기기의 주소 대신 기기 토큰을 알고, 토큰과 메시지를 푸시 서비스에 맡긴다. FCM은 애플 기기로 보낼 때 각 플랫폼의 전송 서비스를 거친다.

앱, 기기 OS, 푸시 서비스(APNs·FCM), 앱 서버 네 세로줄 사이를 일곱 메시지가 위에서 아래로 오가는 순서도앱기기 OSAPNs · FCM앱 서버1. 알림 등록 요청1. 알림 등록 요청2. 기기 등록2. 기기 등록3. 기기 토큰3. 기기 토큰4. 토큰을 계정과 함께 저장4. 토큰을 계정과 함께 저장5. 토큰 + 메시지5. 토큰 + 메시지6. 전달6. 전달7. 표시 · 탭하면 앱 실행7. 표시 · 탭하면 앱 실행
1 / 8

네 참여자 사이를 오가는 메시지. 단계를 넘기면 한 번에 하나씩 지나간다.

푸시 알림. 앱 서버는 기기의 주소를 모르고 토큰만 안다. 배달은 기기와 연결을 유지하는 플랫폼 푸시 서비스가 맡는다 — 배터리를 아끼려고 앱마다 따로 연결을 열지 않는 것이다.

몇 가지 실무 규칙이 있다. 애플 문서는 토큰을 기기에 저장해 두지 말라고 한다. 백업에서 복원하거나, 새 기기에 설치하거나, OS를 다시 깔면 토큰이 바뀌므로 실행할 때마다 새로 받아 서버에 보낸다. 한 사용자가 기기를 여러 대 쓰니 서버는 계정마다 토큰을 여럿 보관한다. 메시지에는 사용자에게 보이는 알림과, 앱 코드가 처리하는 데이터 메시지가 있다. 실을 수 있는 양은 작아서(FCM 문서는 메시지당 4096바이트를 예로 든다) 보통 "새 소식이 있다"는 신호만 보내고 내용은 앱이 서버에서 가져온다. 알림은 사용자의 주의를 쓰는 일이라, 너무 잦으면 알림 권한을 끄거나 앱을 지운다.

권한과 개인정보: 앱 추적 투명성

안드로이드는 권한을 세 종류로 나눈다. 시스템과 다른 앱에 영향이 작은 설치 시 권한(인터넷 등)은 설치할 때 자동으로 주어진다. 위치·연락처처럼 개인 데이터에 닿는 런타임 권한(위험 권한)은 쓰기 전에 사용자에게 묻고, 이미 받았다고 가정하지 말고 쓸 때마다 확인해야 한다. 플랫폼과 제조사만 정의하는 특수 권한은 설정의 별도 화면에서 켠다. iOS도 카메라·위치·사진 같은 접근마다 시스템 대화상자로 묻고, 앱은 그 대화상자에 보일 사용 이유 문구를 적어 두어야 한다.

좋은 권한 요청은 필요한 순간에, 이유와 함께 묻는다. 앱을 켜자마자 권한 다섯 개를 연달아 묻는 대신, 사진 첨부를 누른 순간 사진 권한을 묻는다. 거부해도 앱이 멈추지 않게, 기능을 줄이고 나중에 설정에서 켤 길을 알려 준다. 애플 심사 지침도 사용자가 거부하면 대안(주소를 손으로 입력하기 등)을 주고, 꼭 필요한 데이터만 요청하라고 한다.

사용자가 사진 첨부를 누름, 앱이 왜 필요한지 설명, 시스템 권한 대화상자, 허용이면 기능 사용, 거부면 기능을 줄이고 설정으로 가는 길 안내. 아래에는 추적 허용 요청을 거부하면 광고 식별자가 모두 0이 된다는 줄사진 첨부를누른 순간왜 필요한지앱 화면으로 설명OS 대화상자허용 · 거부기능 사용기능만 줄이고설정으로 가는 길 안내(앱이 맥락을 먼저 준다)추적 허용(ATT) — iOS 14.5부터허용 안 함을 고르면 광고 식별자00000000-0000-0000-…추적을 거부했다는 이유로앱 기능을 막을 수는 없다
권한 요청의 흐름. 대화상자는 한 번 거부되면 다시 띄우기 어려우므로, 맥락 없는 첫 실행 때 묻지 않는다. 아래는 iOS의 추적 허용 요청.

광고 추적에는 따로 규칙이 있다. iOS·iPadOS 14.5부터 앱이 사용자를 추적하거나 기기의 광고 식별자(IDFA)에 접근하려면 앱 추적 투명성(ATT) 프레임워크로 허락을 받아야 한다. 애플의 정의로 추적은 내 앱에서 모은 사용자·기기 데이터를 다른 회사의 앱·웹사이트·오프라인 데이터와 이어 맞춤 광고나 광고 측정에 쓰는 것, 그리고 데이터 브로커와 나누는 것이다. 허락이 없으면 광고 식별자는 모두 0으로 채워지고, 이메일을 해시해 맞추는 식의 우회도 허락 없이는 안 된다. 추적에 동의해야 기능을 쓸 수 있게 막는 것도 지침 위반이다. 같은 회사 앱끼리의 분석에는 앱 공급자 식별자(IDFV)를 쓸 수 있다.

기기에 남기는 비밀도 개인정보다. 로그인 토큰이나 비밀번호를 일반 설정 저장소(iOS의 UserDefaults, 안드로이드의 SharedPreferences)에 평문으로 두지 않는다. iOS는 작은 비밀을 암호화해 담는 키체인을, 안드로이드는 암호 키를 기기에서 꺼내기 어렵게 보관하고 키 자체는 내보낼 수 없게 하는 Android Keystore를 둔다. 키스토어의 키로 토큰을 암호화해 저장하는 식으로 함께 쓴다.

딥 링크: 유니버설 링크와 앱 링크

딥 링크는 앱의 첫 화면이 아니라 특정 화면(상품 상세, 초대 수락)으로 바로 여는 링크다. 오래된 방식은 myapp://product/42 같은 사용자 정의 스킴인데, 다른 앱도 같은 스킴을 등록할 수 있고 앱이 없으면 링크가 그냥 실패한다. 그래서 두 플랫폼 모두 내 웹 주소(https://example.com/product/42)를 앱과 묶는 방식을 권한다. 앱이 깔려 있으면 앱이 열리고, 없으면 같은 주소의 웹 페이지가 열린다.

  • iOS 유니버설 링크: 앱의 연결 도메인(associated domains) 설정과 웹사이트의 /.well-known/apple-app-site-association 파일이 서로를 가리키는 양방향 연결을 만든다(Supporting universal links).
  • 안드로이드 앱 링크: 웹사이트의 /.well-known/assetlinks.json에 패키지 이름과 서명 인증서 지문을 적어 두면, 기기가 그 파일을 받아 검증한다. 검증된 링크는 어느 앱으로 열지 묻는 선택 창 없이 바로 내 앱을 연다. 안드로이드 6 이상, 구글 서비스가 있는 기기에서 동작한다(About deep links).
  • 링크는 외부 입력이다: 애플 문서는 유니버설 링크가 앱으로 들어오는 공격 경로가 될 수 있으니 URL 매개변수를 모두 검증하고, 링크만으로 사용자 데이터를 위험하게 하는 동작(결제, 삭제)을 실행하지 말라고 경고한다.

성능: 프레임 예산과 배터리

스크롤과 애니메이션이 매끄러우려면 매 화면 갱신마다 새 프레임이 준비되어야 한다. 60Hz 화면이면 프레임 하나에 16.7ms, 90Hz면 11.1ms, 120Hz면 8.3ms다. 안드로이드 문서의 표현대로, 이 창을 1ms만 넘겨도 프레임이 1ms 늦게 나오는 게 아니라 그 프레임을 통째로 놓친다. 안드로이드는 이렇게 느린 프레임과 별도로, 700ms 넘게 걸린 프레임을 멈춘 프레임으로 센다. 메인 스레드가 5초 넘게 막히면 "응답 없음"(ANR)이 된다.

위 줄은 60Hz의 예산 16.7ms 안에 10ms의 일이 들어가는 모습, 아래 줄은 120Hz의 예산 8.3ms를 같은 일이 넘쳐 넘친 부분이 빨갛게 칠해진 모습한 프레임의 일 10ms를 두 주사율의 예산에 넣어 본다60Hz예산 16.7ms — 남는 시간은 쉬며 배터리를 아낀다120Hz예산 8.3ms — 넘쳐서 이 프레임을 놓친다입력·상태 갱신 2 · 레이아웃 2.5 · 그리기 명령 3 · GPU 합성 2.5 (ms, 가상의 예)
같은 일의 양이 주사율에 따라 들어가기도 넘치기도 한다. 120Hz 화면에서 매끄러우려면 한 프레임의 일을 절반 가까이 줄여야 한다. 예산은 계산 함수로 구했다.

프레임을 지키는 기본은 메인(UI) 스레드에서 오래 걸리는 일을 하지 않는 것이다. 네트워크·디스크·큰 이미지 디코딩은 다른 스레드로 보내고, 목록은 화면에 보이는 칸만 만들어 재사용하며, 레이아웃을 깊게 겹치지 않는다. 화면이 멈춰 있을 때 일을 덜 하는 것은 곧 배터리 절약이기도 하다. 배터리를 가장 많이 먹는 것은 대개 화면, 무선 통신, 위치(GPS)다. 무선 칩은 한 번 깨면 한동안 켜져 있으므로 자잘한 요청 여러 번보다 모아서 한 번 보내는 편이 낫고, 정밀한 위치가 필요 없으면 대략의 위치만 요청하며, 백그라운드 작업은 OS의 작업 예약기(안드로이드의 WorkManager 등)에 맡겨 충전 중·와이파이 연결 때처럼 조건이 좋을 때 몰아서 하게 한다.

빌드, 서명, 심사, 배포

모바일 앱은 서명된 패키지로만 설치된다. 서명은 "이 앱은 이 개발자가 만들었고 중간에 바뀌지 않았다"는 증명이다. iOS는 애플 개발자 계정의 인증서와 프로비저닝 프로필로 서명하고, 안드로이드는 개발자의 키로 서명한다. 구글 플레이는 Play 앱 서명을 쓴다. 개발자는 업로드 키로 서명한 앱 번들(AAB)을 올리고, 구글은 신원을 확인한 뒤 자기 쪽에 보관한 앱 서명 키로 다시 서명해 배포한다. 2021년 8월 이후 만든 앱은 이 방식이 필수다. 업로드 키는 잃어도 재설정할 수 있지만, 직접 관리하던 서명 키를 잃으면 그 앱에 업데이트를 낼 수 없다.

올린 앱은 심사를 거친다. 애플의 앱 심사 지침은 안전·성능·비즈니스·디자인·법 다섯 장으로 되어 있다. 개발자가 자주 걸리는 곳은 개인정보(수집 전 동의, 개인정보 처리방침, 계정을 만들게 했다면 앱 안에서 계정 삭제), 디지털 상품의 인앱 결제, 그리고 앱의 기능을 바꾸는 코드를 내려받아 실행하지 말라는 2.5.2다. 로그인이 필요한 앱이라면 심사자가 모든 기능을 볼 수 있게 데모 계정이나 데모 모드를 함께 내야 하고, 충돌하거나 미완성인 빌드는 거절된다. 출시 전에는 TestFlight(iOS)나 테스트 트랙(구글 플레이)으로 일부 사용자에게 먼저 돌려 본다.

위는 빌드와 서명, 테스트 배포, 스토어 심사, 단계적 출시가 차례로 이어진 흐름. 아래는 개발자가 업로드 키로 서명해 올리면 Google Play가 확인한 뒤 앱 서명 키로 다시 서명해 사용자에게 배포하는 흐름빌드 · 서명릴리스 빌드테스트 배포TestFlight 등스토어 심사가이드라인 확인단계적 출시일부 → 전체문제가 보이면 출시 비율을 멈추고 고친 판을 다시 올린다 — 이미 깔린 판은 되돌릴 수 없다Play 앱 서명개발자업로드 키로 서명AABGoogle Play확인 후 앱 서명 키로 재서명사용자 기기서명이 같아야 업데이트업로드 키를 잃으면 재설정할 수 있지만, 직접 관리하던 서명 키를 잃으면 그 앱은 더 못 고친다
빌드에서 사용자 손까지(위)와 Play 앱 서명(아래). 웹과 달리 이미 설치된 판은 서버에서 고칠 수 없어서, 단계적 출시와 크래시 감시가 중요하다.

웹은 서버에 올리면 모두가 새 판을 받지만, 앱은 사용자가 업데이트하기 전까지 옛 판이 계속 돈다. 그래서 서버 API는 옛 앱 판도 한동안 받아 주도록 호환성을 지키고, 큰 변경은 원격 설정·기능 플래그로 켜고 끈다. 그래도 보안 문제처럼 옛 판을 더 둘 수 없을 때를 대비해, 앱이 켜질 때 서버에서 ‘최소 지원 판’을 받아 그보다 낮으면 업데이트를 안내하는 장치를 처음부터 넣어 둔다. 구글 플레이는 이를 위한 인앱 업데이트 API를 주는데, 업데이트하고 다시 시작해야만 계속 쓸 수 있는 전체 화면 ‘즉시’ 흐름과, 쓰면서 뒤에서 받는 ‘유연’ 흐름이 있다. 빌드·서명·업로드를 자동화하는 파이프라인은 CI/CD 노트와 같은 원리다.

테스트와 크래시 리포트

모바일 테스트는 층이 같다(테스트 노트). 로직은 기기 없이 도는 단위 테스트로 넓게 덮고, 화면은 플랫폼의 UI 테스트 도구(iOS의 XCTest UI 테스트, 안드로이드의 Espresso·Compose 테스트)로 핵심 흐름만 잡는다. 다른 점은 기기의 다양성이다. 에뮬레이터·시뮬레이터로 대부분을 돌리되, 느린 저가 기기, 작은 화면, 오래된 OS 판, 다른 언어 설정, 큰 글자 설정, 비행기 모드 같은 조건은 실제 기기 또는 클라우드 기기 농장에서 확인한다.

출시 뒤에는 크래시 리포트가 테스트의 연장이다. 앱이 죽으면 SDK가 스택 정보를 모아 다음 실행 때 서버로 보낸다. 출시 빌드는 최적화·난독화되어 스택에 주소만 남으므로, 빌드할 때 만든 기호 파일(애플 플랫폼은 dSYM)로 함수 이름과 줄 번호를 되살린다(기호화). Firebase Crashlytics 같은 도구는 스택과 예외 메시지를 보고 같은 실패 지점의 사건을 이슈 하나로 묶어, 영향받은 사용자가 많은 것부터 보여 준다.

왼쪽 위는 기기에서 온 주소뿐인 스택 세 줄, 기호 파일을 거치면 오른쪽 아래처럼 함수 이름과 파일 줄 번호가 보인다. 맨 아래는 같은 지점에서 난 크래시 수천 건이 이슈 하나로 묶인다는 줄기기에서 온 스택(주소만)0 MyApp 0x0000000104a1c3f01 MyApp 0x0000000104a1b2c82 UIKit 0x00000001a2f0c114기호 파일dSYM · 매핑 파일, 빌드마다 보관0 CartView.total(items:) CartView.swift:421 CartView.body.getter CartView.swift:182 UIKit …같은 지점에서 난 크래시는 이슈 하나로 묶인다 → 영향받은 사용자 수가 많은 것부터 고친다
크래시 리포트의 기호화. 빌드마다 기호 파일을 보관하지 않으면 그 판의 크래시는 주소 덩어리로만 남는다.

핵심 자료

웹 쪽은 웹 개발 노트, 화면 설계의 원리는 HCI와 UX 노트에 있다.

자료종류요지링크
The activity lifecycleAndroid 문서콜백 여섯 개와 상태, 화면 회전 때의 재생성, 메모리가 모자랄 때 프로세스를 정리하는 순서.Android Developers
Managing your app's life cycleApple 문서iOS 장면(scene)의 연결 전·포그라운드·백그라운드·정지 상태와 옮겨 가는 길.Apple Developer
Support different pixel densitiesAndroid 문서dp와 px의 관계, 밀도 묶음(mdpi~xxxhdpi)과 배율, 비트맵을 밀도별로 준비하는 법.Android Developers
Build an offline-first appAndroid 문서로컬 저장소를 진실의 원천으로 두는 구조, 쓰기 전략 셋, 끌어오기·밀어주기 동기화와 충돌 해결.Android Developers
Registering your app with APNsApple 문서기기 토큰을 받아 서버로 보내는 과정과, 토큰을 기기에 캐시하지 말아야 하는 이유.Apple Developer
User Privacy and Data UseApple 안내앱 추적 투명성(ATT)이 말하는 "추적"의 정의, 광고 식별자, 허락 없이 할 수 있는 것과 없는 것.Apple Developer
Slow renderingAndroid 문서주사율별 프레임 예산, 느린 프레임과 멈춘 프레임(700ms)의 기준, 원인을 찾는 도구.Android Developers
App Review GuidelinesApple 지침안전·성능·비즈니스·디자인·법 다섯 장. 개인정보(5.1)와 실행 코드 내려받기 금지(2.5.2)를 꼭 읽는다.Apple Developer