엔지니어링 실무

클라우드 Mac에서 simctl로 iOS 푸시 알림 자동 검증하기

클라우드 Mac에서 simctl로 iOS 푸시 알림 자동 검증하기

원격 파이프라인에서 설치 가능한 iOS 앱을 빌드했지만, 팀에 상시 연결된 테스트 기기가 없을 수 있습니다. 이때 놓치기 쉬운 것은 컴파일 오류가 아니라 알림 권한, 포그라운드 표시, 딥 링크 매개변수, Notification Service Extension이 맞물릴 때 발생하는 문제입니다. simctl push를 사용하면 APNs 형식의 페이로드를 iOS Simulator에 직접 주입할 수 있어, 클라우드 Mac에서 빠르고 반복 가능한 커밋 전 검증을 수행하기에 적합합니다.

이 방식이 실제 APNs 전송 테스트를 대체할 수는 없지만, 많은 클라이언트 측 문제를 빌드 단계에서 미리 차단할 수 있습니다. 핵심은 명령의 종료 코드만 확인하지 않고 앱 상태, 화면 캡처, 통합 로그를 함께 보존하는 것입니다.

이 테스트로 검증할 수 있는 범위부터 정의하기

simctl push는 빌드된 앱이 알림 페이로드를 어떻게 처리하는지 검증합니다. 알림 콘텐츠 파싱, 포그라운드 및 백그라운드 델리게이트 콜백, 카테고리 액션 버튼, 딥 링크 라우팅, Notification Service Extension의 처리 결과를 확인할 수 있습니다.

반면 서버 인증, 기기 토큰의 유효성, 외부 네트워크를 통한 전송, APNs 속도 제한, 실제 기기의 절전 정책은 검증할 수 없습니다. 시뮬레이터가 페이로드를 수신했다고 해서 프로덕션 푸시 전송 경로까지 정상이라는 뜻은 아닙니다.

명령 성공은 “로컬 주입 완료”로 해석해야 하며, “사용자가 알림을 수신함”으로 간주해서는 안 됩니다. 검증 결과는 반드시 화면, 앱 로그, 라우팅 결과를 기준으로 판단해야 합니다.

시작하기 전에 앱에 알림 기능이 선언되어 있고 테스트 경로에서 권한 승인이 완료되었는지 확인합니다. simctl push가 앱을 대신해 시스템 권한 팝업을 눌러 주지는 않습니다. 최초 권한 승인은 UI 테스트로 처리하거나 전용 테스트 시뮬레이터에서 미리 완료할 수 있지만, 장기간 사용해 온 특정 시뮬레이터의 상태에 의존해서는 안 됩니다.

시뮬레이터와 앱 입력값 고정하기

병렬 작업에서는 booted를 직접 사용하지 마십시오. 여러 시뮬레이터가 동시에 실행되면 다른 작업의 시뮬레이터로 페이로드가 전달될 수 있습니다. 작업마다 명시적인 UDID를 기록하고 앱 경로, Bundle ID, 산출물 디렉터리를 입력값으로 지정합니다.

set -euo pipefail

: "${SIMULATOR_UDID:?Set SIMULATOR_UDID}"
: "${APP_PATH:?Set APP_PATH}"
: "${BUNDLE_ID:?Set BUNDLE_ID}"

ARTIFACTS="${ARTIFACTS:-$PWD/.ci/push-check}"
mkdir -p "$ARTIFACTS"

xcrun simctl bootstatus "$SIMULATOR_UDID" -b
xcrun simctl install "$SIMULATOR_UDID" "$APP_PATH"
xcrun simctl launch "$SIMULATOR_UDID" "$BUNDLE_ID"
xcrun simctl get_app_container "$SIMULATOR_UDID" "$BUNDLE_ID"

bootstatus -b는 시스템 부팅이 실제로 완료될 때까지 기다리므로, 실행 후 몇 초 동안 고정 대기하는 방식보다 안정적입니다. get_app_container는 비용이 적게 드는 설치 확인 수단입니다. Bundle ID가 잘못되었거나 앱이 설치되지 않았다면 알림을 전송하기 전에 작업이 실패합니다.

최초 권한 승인 과정을 다시 테스트해야 한다면 공유 시뮬레이터의 모든 개인정보 보호 설정을 임의로 초기화하지 말고, 해당 작업을 위한 전용 시뮬레이터를 새로 만들거나 지우십시오. 이렇게 하면 병렬 테스트가 서로의 카메라, 사진 또는 알림 권한을 변경하는 문제를 방지할 수 있습니다.

APNs 페이로드 생성 및 검증하기

페이로드 파일은 JSON 형식을 사용합니다. 화면 캡처, 앱 로그, 파이프라인 작업을 서로 연결할 수 있도록 검색 가능한 테스트 식별자를 추가하는 것이 좋습니다.

PAYLOAD="$ARTIFACTS/foreground.apns"
TRACE_ID="push-check-${CI_RUN_ID:-local}"

cat > "$PAYLOAD" <<JSON
{
  "Simulator Target Bundle": "$BUNDLE_ID",
  "aps": {
    "alert": {
      "title": "Build completed",
      "body": "Open the result for details"
    },
    "sound": "default",
    "badge": 1,
    "category": "BUILD_RESULT"
  },
  "route": "build/result",
  "trace_id": "$TRACE_ID"
}
JSON

plutil -lint "$PAYLOAD"
PAYLOAD_BYTES="$(wc -c < "$PAYLOAD" | tr -d ' ')"
test "$PAYLOAD_BYTES" -le 4096
xcrun simctl push "$SIMULATOR_UDID" "$BUNDLE_ID" "$PAYLOAD"

4KB 검사는 비정상적인 페이로드를 조기에 발견하기 위한 것이지만, 시뮬레이터가 서버 측의 모든 검증을 완전히 재현하지는 않습니다. 앱에서도 route, 식별자, 사용자 정의 필드의 타입을 검사해야 하며, 테스트 페이로드를 신뢰할 수 있다는 이유로 강제 형 변환해서는 안 됩니다.

Notification Service Extension을 사용한다면 "mutable-content": 1을 추가하고 확장이 통일된 형식의 구조화 로그를 기록하도록 합니다. 확장의 처리 시간과 시스템에 의한 종료 동작도 실패 경로를 고려해 설계해야 하며, 다운로드나 콘텐츠 수정이 반드시 완료된다고 가정해서는 안 됩니다.

앱 상태별 검증 매트릭스 구성하기

동일한 페이로드를 최소한 포그라운드, 백그라운드, 미실행 상태에서 각각 테스트해야 합니다. 포그라운드 알림의 배너 표시 여부는 일반적으로 델리게이트 메서드가 결정합니다. 따라서 “배너가 표시되지 않음”은 의도된 제품 동작일 수도 있고 콜백 누락일 수도 있으므로, 상태별 예상 결과를 명시해야 합니다.

시나리오 작업 필수 확인 결과
포그라운드 앱을 실행한 후 주입 델리게이트 콜백, 앱 내 안내, 중복 이벤트
백그라운드 홈 화면으로 돌아간 후 주입 배너 내용, 배지, 탭 후 라우팅
미실행 앱을 종료한 후 주입 콜드 스타트 진입점, 매개변수 파싱, 대상 화면
콘텐츠 확장 mutable-content가 포함된 페이로드 주입 확장 로그, 수정된 콘텐츠, 시간 초과 시 폴백
비정상 필드 라우트 누락 또는 잘못된 필드 타입 안전한 폴백, 비정상 종료 없음, 진단 가능한 로그

전송 후에는 화면을 캡처해 파이프라인에 “명령 성공”이라는 빈 결론만 남지 않도록 합니다.

xcrun simctl io "$SIMULATOR_UDID" screenshot \
  "$ARTIFACTS/notification.png"

xcrun simctl spawn "$SIMULATOR_UDID" log show \
  --last 2m \
  --style compact \
  --predicate "process == '$BUNDLE_ID'" \
  > "$ARTIFACTS/application.log"

프로세스 이름이 항상 Bundle ID와 같지는 않습니다. 더 안정적인 방법은 앱과 확장에서 고정된 로그 subsystem을 사용하고 해당 subsystem을 기준으로 조회하는 것입니다. 로그에는 테스트 식별자, 상태, 오류 유형만 기록하고 알림 전문, 토큰 또는 사용자 데이터를 기록해서는 안 됩니다.

실패 조건을 파이프라인에 포함하기

신뢰할 수 있는 알림 검증 작업은 단순히 셸 명령만 실행해서는 안 됩니다. 산출물이 존재하는지, 앱이 비정상 종료되었는지, 예상한 라우팅이 기록되었는지도 판단해야 합니다. 테스트 빌드에서 앱이 처리한 trace_id와 결과를 전용 파일에 기록하게 한 뒤, simctl get_app_container로 컨테이너 위치를 찾아 해당 파일을 읽을 수 있습니다. 테스트 인터페이스는 내부 빌드 구성에서만 활성화하고 릴리스 구성에서는 반드시 비활성화해야 합니다.

흔한 오판 사례로는 이미 권한이 승인된 시뮬레이터를 재사용해 최초 팝업 테스트를 누락하는 경우, booted를 사용해 병렬 작업 사이에 요청이 뒤섞이는 경우, 포그라운드에서 배너가 표시되지 않는 것을 주입 실패로 판단하는 경우, 화면 캡처만 확인하고 탭 이후의 라우팅을 검증하지 않는 경우, 로그 조회 범위를 지나치게 넓게 설정해 이전 작업의 기록을 읽는 경우가 있습니다.

커밋하기 전에 다음 순서로 마무리합니다.

  1. Xcode, 런타임, 기기 모델, 시뮬레이터 UDID를 고정합니다.
  2. 이번 빌드 산출물을 설치하고 기존 앱 컨테이너를 재사용하지 않습니다.
  3. JSON 구문, 바이트 수, 필수 필드를 검증합니다.
  4. 포그라운드, 백그라운드, 미실행 상태 및 비정상 페이로드 테스트를 각각 실행합니다.
  5. 페이로드, 화면 캡처, 앱 로그, 고유 테스트 식별자를 보관합니다.
  6. 시뮬레이터 검증과 실제 APNs 엔드투엔드 테스트를 구분해 보고합니다.

이 단계를 완료하면 수동 관찰에 의존하던 푸시 클라이언트 로직을 반복 실행할 수 있고, 증거를 보존하며, 실패 단계를 정확히 식별할 수 있는 엔지니어링 검사로 전환할 수 있습니다.

자주 묻는 질문

simctl push 성공이 실제 APNs 전송 성공을 의미하나요?

아닙니다. Simulator에 로컬로 페이로드를 주입했다는 의미일 뿐입니다. 서버 인증, 기기 토큰, 네트워크 전송과 APNs 제한은 통제된 실물 기기에서 별도로 확인해야 합니다.

명령이 성공했는데 알림 배너가 보이지 않는 이유는 무엇인가요?

알림 권한, aps 내용과 앱 상태를 먼저 확인합니다. 앱이 포그라운드에 있으면 알림 델리게이트에서 배너 표시를 명시적으로 허용해야 합니다.

CI에서는 booted와 고정 Simulator UDID 중 무엇을 써야 하나요?

작업에 할당된 명확한 UDID를 사용해야 합니다. 병렬 실행에서 booted를 쓰면 다른 Simulator가 선택되어 알림, 캡처와 로그가 섞일 수 있습니다.

전용 물리 Mac mini

다음 개발 파이프라인을 위한 클라우드 Mac 선택

고정 구성 3종과 아시아 노드 4곳을 비교하고, 실제 워크로드에 맞춰 대여 기간을 선택하세요. 각 대여에는 가상 머신이 아닌 전용 물리 장비 1대가 제공됩니다.

구성 선택 후 대여하기