영문 버튼은 개발 환경에서 여섯 글자에 불과해도 독일어로 바꾸면 한 줄을 가득 채울 수 있습니다. 왼쪽에 있던 뒤로 가기 버튼도 오른쪽에서 왼쪽으로 읽는 인터페이스에서는 여전히 잘못된 방향을 가리킬 수 있습니다. 번역을 모두 마친 뒤에야 이런 문제를 발견하면 레이아웃 수정뿐 아니라 테스트와 출시 일정까지 함께 영향을 받습니다. 더 안정적인 방법은 클라우드 Mac의 모든 병합 검사에서 문자열을 의도적으로 늘리고 레이아웃 방향을 전환한 뒤, 보이지 않거나 조작할 수 없는 컨트롤을 곧바로 실패로 처리하는 것입니다.
먼저 차단 기준에서 잡아낼 문제 정의하기
의사 현지화는 인터페이스를 읽기 어려운 문자로 바꾸는 작업이 아닙니다. 제어 가능한 변환을 적용해 레이아웃 위험을 두드러지게 만드는 기법입니다. 먼저 다음 두 가지 모드를 구성하는 것이 좋습니다.
expanded: 원문을 유지하면서 양쪽에 표식을 추가하고 문자열 길이를 약 40% 늘립니다.rtl: 식별 가능한 콘텐츠는 유지하되 오른쪽에서 왼쪽으로 흐르는 의미 방향을 적용해 배치와 아이콘을 검사합니다.- 일반 모드: 대조군으로 사용해 테스트 보조 코드가 제품 동작을 바꾸지 않았는지 확인합니다.
차단 기준에서는 네 가지 문제를 중점적으로 검사합니다. 텍스트 컨트롤의 표시 영역이 현저히 부족한 경우, 주요 버튼이 없거나 누를 수 없거나 창 밖으로 벗어난 경우, 현지화 진입점을 거치지 않은 하드코딩 문구가 인터페이스에 나타난 경우, 내비게이션·가로 목록·방향 아이콘이 RTL에 맞춰 바뀌지 않은 경우입니다.
의사 현지화는 구조적 문제를 드러내는 데 적합하지만 번역의 정확성을 판단하지는 않습니다. 실제 언어를 사용한 검수는 여전히 필요하지만, 고정 너비 같은 기본적인 결함을 찾는 비용까지 그 단계에 떠넘겨서는 안 됩니다.
제어 가능한 문자열 변환 계층 구축하기
테스트에서 뷰를 순회하며 레이블을 수정하지 마세요. 그렇게 하면 앱이 실제로 문자열을 가져오는 경로를 우회하므로 하드코딩도 찾아낼 수 없습니다. 더 신뢰할 수 있는 방법은 프로젝트의 기존 현지화 진입점에 디버그 빌드에서만 동작하는 변환기를 연결하는 것입니다.
enum PseudoMode: String {
case expanded
case rtl
}
#if DEBUG
func pseudoTransform(_ value: String, mode: PseudoMode?) -> String {
guard let mode else { return value }
switch mode {
case .expanded:
let padding = String(repeating: "·", count: max(2, value.count * 2 / 5))
return "[\(value)\(padding)]"
case .rtl:
return "\(value)"
}
}
#else
func pseudoTransform(_ value: String, mode: PseudoMode?) -> String {
value
}
#endif
앱이 시작될 때 PSEUDO_LOCALE을 읽고 명시된 열거형 값만 허용합니다. 사용자에게 표시되는 모든 문자열은 L10n.text("checkout.confirm") 같은 단일 진입점을 거친 다음 변환기로 전달되어야 합니다. 특정 제목에 경계 표식이 나타나지 않는다면 해당 문자열이 현지화 계층을 우회했다고 판단할 수 있습니다.
RTL 모드에서는 앱의 루트 뷰에도 의미 방향을 설정해야 합니다. UIKit에서는 테스트 창에 semanticContentAttribute = .forceRightToLeft를 설정할 수 있고, SwiftUI에서는 루트 뷰에 layoutDirection을 주입할 수 있습니다. 이미지 내용 자체를 뒤집거나 숫자, 경로, 코드 조각의 순서를 강제로 반전해서는 안 됩니다.
UI 테스트로 표시 여부와 잘림 검사하기
테스트에서 모든 픽셀을 비교할 필요는 없습니다. 먼저 로그인 후 홈 화면, 목록, 상세 화면, 폼, 오류 상태 같은 핵심 경로를 다루고 주요 컨트롤에 안정적인 accessibility identifier를 지정합니다. 테스트 프로세스는 환경 변수를 통해 해당 모드로 앱을 실행합니다.
func launchApp(mode: String) -> XCUIApplication {
let app = XCUIApplication()
app.launchEnvironment["PSEUDO_LOCALE"] = mode
app.launchArguments += ["-UI_TESTING", "1"]
app.launch()
return app
}
func testExpandedCheckoutLayout() {
let app = launchApp(mode: "expanded")
let confirm = app.buttons["checkout.confirm"]
XCTAssertTrue(confirm.waitForExistence(timeout: 8))
XCTAssertTrue(confirm.isHittable)
XCTAssertFalse(confirm.frame.isEmpty)
XCTAssertTrue(app.windows.firstMatch.frame.contains(confirm.frame))
}
isHittable은 단순한 exists보다 더 유용합니다. 버튼이 접근성 트리에는 존재하지만 팝업에 가려졌거나 화면 밖에 놓여 있을 수 있기 때문입니다. 레이블 계열 컨트롤은 identifier로 frame을 가져와 소속 컨테이너를 벗어나지 않았는지 검증할 수 있습니다. OCR로 텍스트 잘림 여부를 추측하지 마세요. 핵심 컴포넌트가 디버그 빌드에서 intrinsicContentSize와 실제 frame의 차이를 노출하도록 만드는 편이 우선입니다.
검사 범위를 체크리스트로 구성하기
| 화면 | expanded 검사 | RTL 검사 | 실패 증거 |
|---|---|---|---|
| 내비게이션 바 | 제목과 동작 요소가 겹치지 않음 | 뒤로 가기 방향과 순서가 올바름 | 인터페이스 계층, 컨트롤 frame |
| 폼 | 레이블이 줄바꿈되고 오류 문구가 완전함 | 레이블과 입력 축이 일치함 | 필드 identifier, 현재 값 |
| 목록 | 긴 제목이 주요 동작을 밀어내지 않음 | 첨부 요소와 화살표 위치가 올바름 | 실패한 행 인덱스, 스크린샷 |
| 팝업 | 버튼이 온전히 표시되고 누를 수 있음 | 동작 순서가 의미 구조에 맞음 | 팝업 frame, 요소 트리 |
클라우드 Mac에서 실행 환경 구성하기
각 작업마다 별도의 시뮬레이터 기기 세트를 생성해 병렬 작업이 시작 상태를 공유하지 않도록 합니다. 기기 세트 디렉터리는 작업의 임시 디렉터리에 두고, 성공 여부와 관계없이 작업 종료 시 정리해야 합니다. 테스트 전에 Xcode 경로, 시뮬레이터 모델, 시스템 버전을 고정하고 해당 값을 보고서에 기록합니다.
set -euo pipefail
export DEVELOPER_DIR="/Applications/Xcode.app/Contents/Developer"
export PSEUDO_LOCALE="${PSEUDO_LOCALE:-expanded}"
RESULT_DIR="$PWD/TestResults/$PSEUDO_LOCALE"
mkdir -p "$RESULT_DIR"
xcodebuild test \
-workspace App.xcworkspace \
-scheme AppUITests \
-destination "platform=iOS Simulator,name=iPhone 16,OS=latest" \
-resultBundlePath "$RESULT_DIR/LayoutTests.xcresult" \
PSEUDO_LOCALE="$PSEUDO_LOCALE"
expanded와 rtl은 동일한 시뮬레이터 세션에서 전환하지 말고 두 개의 독립된 작업으로 실행해야 합니다. 그래야 실패 원인을 특정 모드에 명확히 귀속할 수 있고, 재실행 시 이전 방향 모드가 남지 않습니다. 클라우드 Mac에서 여러 작업을 동시에 실행한다면 테스트 산출물이 서로 덮어쓰이지 않도록 작업마다 별도의 DerivedData 경로도 사용해야 합니다.
흔한 오탐과 실제 결함 처리하기
가장 흔한 오탐은 애니메이션이 아직 끝나지 않았거나 시스템 팝업이 닫히지 않았거나 테스트 데이터 길이가 일정하지 않을 때 발생합니다. 해결책은 일괄적인 대기 시간을 늘리는 것이 아니라 명확한 상태를 기다리는 것입니다. 로딩 표시기가 사라지고, 대상 요소가 나타나며, 창 개수가 안정될 때까지 기다려야 합니다. 사용자 이름, 날짜, 서버 오류 문구를 비롯한 테스트 데이터도 고정해야 합니다.
실제 결함은 대체로 고정 높이, 한 줄 레이블, 수동 너비 계산, 시각적 위치만으로 순서를 표현하는 컴포넌트에 집중됩니다. 수정할 때는 특정 언어를 위한 예외를 추가하기보다 제약을 제거하는 방식을 우선해야 합니다. 버튼 제목의 줄바꿈을 허용한 뒤에는 터치 영역이 전체 텍스트를 덮는지도 다시 확인해야 합니다.
실패가 발견되면 최소한 다음 내용을 보관해야 합니다.
- 현재 의사 언어 모드와 Xcode 및 시뮬레이터 버전.
- 실패한 컨트롤의 identifier, frame, label, 조작 가능 여부.
- 현재 화면의 접근성 계층.
- 해당
xcresult와 실패 시점의 스크린샷. - 해당 화면을 재현하는 데 필요한 고정 테스트 데이터.
병합 차단 기준을 설정하고 유출 방지하기
차단 기준은 작업 완료에 영향을 주는 문제를 막아야 합니다. 주요 동작이 보이지 않거나 실행할 수 없는 경우, 텍스트가 잘려 의미를 잃은 경우, RTL 순서가 잘못된 경우, 인터랙티브 컨트롤에 접근 가능한 이름이 없는 경우가 이에 해당합니다. 사소한 간격 변화는 재검토 항목으로 기록할 수 있으며, 모든 픽셀 차이가 병합을 막도록 만들 필요는 없습니다.
마지막으로 Release 검수 항목을 하나 추가합니다. 빌드 설정과 앱 리소스를 검사해 테스트 전용 의사 언어 파일이 없는지 확인하고, 정식 빌드가 PSEUDO_LOCALE을 무시하는지도 검증합니다. 일반 모드에서도 스모크 테스트를 실행해 변환 계층 자체가 문자열을 바꾸지 않는지 확인해야 합니다.
이 흐름의 가치는 단순히 낯설어 보이는 인터페이스를 만드는 데 있지 않습니다. 출시 전 수동 표본 검사에 머물던 다국어 레이아웃 검증을 모든 코드 변경에서 반복 실행할 수 있는 엔지니어링 제약으로 전환하는 데 있습니다. 먼저 주요 경로를 다룬 뒤 오류 상태와 사용 빈도가 낮은 팝업을 점진적으로 추가해야 차단 기준을 안정적이고 유지 관리하기 쉬운 상태로 운영할 수 있습니다.
자주 묻는 질문
의사 현지화 테스트가 실제 언어 검수를 대체할 수 있나요?
대체할 수 없습니다. 고정 너비와 미번역 문자열, 양방향 배치 오류를 빠르게 찾는 데 유용하지만 실제 긴 문자열 언어와 RTL 언어로 최종 검수해야 합니다.
의사 현지화 코드가 배포 빌드에 포함되지 않게 하려면 어떻게 하나요?
변환기를 DEBUG 조건 안에 두고 Release 빌드에서 PSEUDO_LOCALE 환경 변수나 테스트 전용 리소스가 발견되면 실패하도록 검사합니다.
어떤 레이아웃 오류가 병합을 차단해야 하나요?
핵심 동작이 보이지 않거나 클릭되지 않는 경우, 의미를 잃는 문자열 잘림, RTL 순서 오류, 접근성 레이블 누락은 병합을 차단해야 합니다.
다음 빌드를 클라우드 Mac mini에서 실행하세요
구성, 결제 주기와 노드 지역을 선택하고 전용 물리 컴퓨터에서 개발, 빌드와 자동화 작업을 실행하세요.