같은 iOS 페이지가 로컬에서는 정상적으로 동작하지만, 원격 빌드 작업에서는 웹페이지가 보낸 “준비 완료” 메시지를 간혹 받지 못할 수 있습니다. 이런 실패를 곧바로 클라우드 Mac의 네트워크 문제로 단정하지 마세요. 페이지와 네이티브 코드가 WKWebView 브리지로 통신한다면 로딩 순서, 메시지 필드, 핸들러 수명주기가 원인일 수 있습니다. 저장소에 함께 커밋한 로컬 HTML 파일을 사용하면 외부 페이지에 의존하지 않고 양방향 통신을 계약 테스트로 검증할 수 있습니다.
브리지 계약부터 고정하기
최소한의 프로토콜을 정합니다. 웹페이지는 네이티브 코드에 {kind: "ready", version: 1}을 보내고, 네이티브 코드는 웹페이지에 {kind: "sync", value: "ok"}를 보냅니다. 전자는 window.webkit.messageHandlers.bridge.postMessage를, 후자는 evaluateJavaScript를 사용합니다. 테스트에서는 페이지가 표시되는지만 확인할 것이 아니라, 메시지가 도착하고 필드 타입이 올바른지 각각 검증해야 합니다.
다음 내용을 테스트 target에서 읽을 수 있는 BridgeFixture.html로 저장하고, 해당 target의 리소스 목록에 포함됐는지 확인합니다.
<!doctype html>
<html lang="en">
<meta charset="utf-8">
<body>
<script>
window.bridgeReceive = function (message) {
if (message.kind !== "sync" || typeof message.value !== "string") {
throw new Error("Invalid native message");
}
document.body.dataset.received = message.value;
};
window.webkit.messageHandlers.bridge.postMessage({
kind: "ready",
version: 1
});
</script>
</body>
</html>
dataset.received는 테스트에서 결과를 관찰하기 위한 지점일 뿐, 프로덕션 프로토콜의 일부가 아닙니다. 프로덕션 코드와 픽스처는 같은 필드 정의를 공유해야 합니다. 필드를 변경할 때는 양쪽을 함께 업데이트하고, 호환되지 않는 변경은 테스트에서 명확히 실패하도록 하세요. 페이지 로딩이 끝났는지 고정된 시간만큼 기다려 추측해서는 안 됩니다.
시뮬레이터에서 양방향 검증하기
테스트 코드에서 WKUserContentController를 만들고 WKScriptMessageHandler를 bridge라는 이름으로 등록한 뒤, 컨트롤러를 WKWebViewConfiguration에 설정합니다. 테스트 번들에서 HTML을 읽어 loadHTMLString(_:baseURL:)을 호출합니다. ready를 받으면 먼저 message.body가 딕셔너리인지 확인하고, 이어서 kind가 문자열인지, version이 숫자이며 값이 1인지 검사합니다. 알 수 없는 메시지는 단언에 실패해야 합니다.
네이티브 코드에서 웹페이지로 보내는 메시지는 WKNavigationDelegate가 webView(_:didFinish:)를 받은 뒤에 전송해야 합니다. 보낼 딕셔너리를 JSONSerialization으로 JSON 텍스트로 인코딩하고, 이를 window.bridgeReceive(...)에 넣어 evaluateJavaScript를 호출합니다. 사용자 입력을 스크립트 문자열에 직접 삽입하지 마세요. 따옴표와 줄바꿈이 스크립트의 의미를 바꿀 수 있습니다. 호출이 완료되면 document.body.dataset.received를 읽고 결과가 ok인지 단언합니다.
loadHTMLString이 반환됐다는 것은 로딩이 시작됐다는 뜻일 뿐입니다. 웹페이지가 이미ready를 보냈더라도 반환 시점을 탐색 완료 이벤트 대신 사용해서는 안 됩니다. 방향마다 XCTest expectation을 하나씩 두면 어느 쪽에서 응답이 없는지 구분할 수 있습니다.
로딩 실패와 스크립트 실행 오류에도 실패 경로를 마련해야 합니다. expectation에 명확한 제한 시간을 설정하고, 실패 출력에서 “페이지 로딩 미완료”, “네이티브 코드가 메시지를 받지 못함”, “웹페이지가 네이티브 메시지를 수락하지 않음”을 구분하세요. 그렇지 않으면 CI에는 포괄적인 시간 초과 결과만 남습니다.
클라우드 빌드에 테스트 연결하기
먼저 ArmMesh 클라우드 Mac 콘솔에서 현재 선택 가능한 구성을 확인합니다. 노드를 선택한 다음 Xcode 버전과 시뮬레이터 런타임을 고정합니다. xcrun simctl list devices available로 사용 가능한 기기를 찾고 선택한 시뮬레이터 UUID를 작업 설정에 기록하세요. 기기 이름의 모호한 일치에 의존하지 마세요. 아래 명령의 프로젝트, scheme, UUID는 저장소의 실제 값으로 바꿉니다.
xcodebuild test \
-project Client.xcodeproj \
-scheme Client \
-destination 'platform=iOS Simulator,id=<SIMULATOR_UUID>' \
-only-testing:ClientTests/BridgeContractTests \
-resultBundlePath "$PWD/BridgeContract.xcresult"
실행 전 scheme이 공유되어 있고 테스트 target에 HTML 리소스가 포함되어 있으며 결과 경로가 아직 존재하지 않는지 확인합니다. xcodebuild는 기존 결과 번들을 빈 디렉터리처럼 덮어쓰지 않습니다. 실패하면 .xcresult와 빌드 로그를 보관하고 Xcode 버전과 시뮬레이터 UUID를 기록해 동일한 환경에서 다시 실행할 수 있도록 합니다.
이 테스트에는 원격 웹페이지가 필요하지 않으므로 네트워크 장애가 결론에 영향을 줘서는 안 됩니다. 실제 페이지를 사용하는 통합 테스트를 별도로 두어야 “브리지 계약 오류”와 “페이지 배포 또는 요청 실패”를 구분할 수 있습니다.
로딩 순서와 정리 과정의 함정 다루기
핸들러를 등록하기 전에 페이지가 postMessage를 실행하면 첫 메시지가 유실될 수 있습니다. 따라서 구성과 등록을 마친 뒤 WebView를 생성하고 로드하세요. 테스트에서 이전 핸들러가 남아 있는 컨트롤러를 재사용하지 마세요. 테스트 케이스가 끝나면 removeScriptMessageHandler(forName: "bridge")를 호출하고 WebView의 탐색 델리게이트 참조를 해제합니다.
거부 경로도 테스트해야 합니다. 픽스처에서 version이 빠진 메시지와 version을 문자열로 지정한 메시지를 보내고, 네이티브 코드가 이를 유효한 ready로 취급하지 않는지 확인합니다. 프로덕션 프로토콜에 요청 번호가 있다면 응답을 해당 번호와 대조해야 합니다. 이전 요청에 대한 지연 메시지가 이번 expectation을 완료하게 해서는 안 됩니다. 사용자 콘텐츠가 관련된 경우 테스트 로그에는 필드 구조와 민감 정보를 제거한 오류만 남깁니다.
마지막으로 검증합니다. 이 테스트를 단독으로 실행한 뒤 전체 테스트 스위트에도 포함해 실행하세요. 연속 실행 시 콜백이 중복 발생하지 않아야 하며, 같은 버전의 다른 시뮬레이터에서도 동일한 단언 결과가 나와야 합니다. 이렇게 하면 로컬 픽스처는 안정적인 인터페이스 계약을, 실제 페이지 테스트는 배포 후의 통합 동작을 담당하며 각 실패의 원인도 명확해집니다.
자주 묻는 질문
운영 중인 웹 페이지를 그대로 테스트하면 안 되나요?
웹 페이지 배포와 네트워크 상태가 테스트 입력을 바꿉니다. 계약 테스트는 로컬 픽스처로 고정하고 실제 페이지 검증은 별도로 진행하세요.
네이티브에서 JavaScript를 언제 호출해야 하나요?
WKNavigationDelegate의 didFinish 이후 evaluateJavaScript를 호출해야 페이지 함수가 준비됐음을 전제로 테스트할 수 있습니다.
고정 구성의 클라우드 Mac을 워크플로에 연결하세요
일·주·월·분기 단위로 전용 물리 노드를 임대하고, 고정된 하드웨어 리소스로 빌드, 자동화 및 MLX 추론 작업을 실행하세요.