React Native 테스트 계층: 어떤 실패를 어디에서 막는가
JavaScript 테스트 하나로 네이티브 연결과 실제 기기 기능까지 증명하려는 실패를 재현하고, 막으려는 위험에 따라 가장 작은 충분한 테스트 범위와 실행 기기를 고르는 법을 익힌다.
목차
표준·구현·측정·해석 표시는 무엇인가요?
- 표준웹 표준이나 언어 명세가 정한 동작
- 구현특정 기술이나 브라우저가 실제로 구현한 동작
- 측정명시한 환경에서 직접 실행해 관찰한 결과
- 해석앞선 근거에서 도출한 설계 판단
- 미확인아직 공식 근거나 재현 결과를 확인하지 못한 내용
30초 요약
테스트 범위(scope)와 실행 목적지(destination)는 다른 축이다. unit test는 작은 로직, integration test는 둘 이상의 실제 부분이 맞물리는 경계, E2E(end-to-end) test는 사용자가 보는 핵심 흐름을 확인한다. host는 개발 컴퓨터나 Continuous Integration(CI, 변경을 자동 검사하는 지속적 통합 환경) 서버다. Android Emulator와 iOS Simulator는 각각 컴퓨터 안에서 기기 환경을 흉내 내는 가상 기기이고, physical device는 손에 들고 쓰는 실제 물리 기기다. callback(콜백)은 플랫폼이 결과를 알릴 때 앱이 실행하도록 넘긴 함수이고, contract(계약)는 어떤 입력과 결과를 주고받을지 정한 규칙이다.
막으려는 실패 가장 작은 충분한 시작점 필요한 실행 위치
금액 계산·상태 전이 JavaScript Jest host
네이티브 모듈·권한 콜백 연결 Android 계측 / iOS 네이티브 통합 Emulator·Simulator 또는 기기
설치된 앱의 핵심 사용자 흐름 E2E Emulator·Simulator 또는 기기
실제 카메라·센서·생체 기능·성능 native integration 또는 E2E physical devicephysical device는 별도 테스트 종류가 아니다. 작은 네이티브 통합 test도, 긴 E2E test도 실제 기기에서 실행할 수 있다. 반복해서 기억할 문장은 이것이다.
막으려는 실패를 실제로 볼 수 있는 가장 작은 범위를 고르고, 필요한 실제 환경까지 넓힌다.
이 글을 관통하는 상황: 카메라 모듈을 가짜로 바꾼 Jest만 통과했다
영수증 촬영 기능의 pull request에 초록색 Jest 결과가 붙었다. 테스트는 네이티브 카메라 모듈을 고정 응답으로
바꾸고 total: 15900이 화면 상태에 들어오는지 확인했다. 팀은 이를 근거로 “영수증 촬영 검증 완료”라고
적었다.
하지만 실제 Android 기기에서는 권한 거절 콜백이 잘못 번역됐고, 지원 기기 한 모델에서는 가까운 영수증에 초점이 맞지 않았다. 테스트가 거짓말한 것이 아니다. Jest가 실제 네이티브 콜백과 물리 카메라를 실행한 적이 없었다.
이 글의 질문은 하나다.
JavaScript 테스트, 네이티브 테스트와 실기기 검증은 어떤 실패를 각각 맡아야 하는가?
고정된 세 위험을 모두 Jest에 맡기는 실패를 재현한다
빈 상위 디렉터리에서 다음 명령으로 독립 fixture를 만든다.
npx @react-native-community/[email protected] init TestLayerApp \
--version 0.86.2 \
--skip-install
cd TestLayerApp
npm installNode.js 22.11.0 이상인 지원 중인 Node 22 release를 사용한다. 파일은 다음 순서로 만든다.
TestLayerApp/
├─ test-ownership.ts
└─ __tests__/
└─ test-ownership.test.tstest-ownership.ts를 만든다. USE_TEST_MATRIX = false가 모든 실패를 Jest 한 종류로 덮는 잘못된
기준선이다.
const USE_TEST_MATRIX = false
export type RiskId =
| 'amount-rounding'
| 'permission-callback-contract'
| 'real-camera-capture'
export type CheckKind =
| 'javascript-jest'
| 'native-integration'
| 'user-flow-e2e'
export type Destination =
| 'host'
| 'android-emulator'
| 'ios-simulator'
| 'physical-device'
export type EvidenceCheck = {
check: CheckKind
destinations: Destination[]
}
export type TestPlan = {
evidence: EvidenceCheck[]
}
const PLAN_BY_RISK: Record<RiskId, TestPlan> = {
'amount-rounding': {
evidence: [
{ check: 'javascript-jest', destinations: ['host'] },
],
},
'permission-callback-contract': {
evidence: [
{ check: 'javascript-jest', destinations: ['host'] },
{
check: 'native-integration',
destinations: ['android-emulator', 'ios-simulator'],
},
],
},
'real-camera-capture': {
evidence: [
{ check: 'native-integration', destinations: ['physical-device'] },
{ check: 'user-flow-e2e', destinations: ['physical-device'] },
],
},
}
export function planEvidence(riskId: RiskId): TestPlan {
if (!USE_TEST_MATRIX) {
return {
evidence: [
{ check: 'javascript-jest', destinations: ['host'] },
],
}
}
return PLAN_BY_RISK[riskId]
}__tests__/test-ownership.test.ts를 만든다.
import { planEvidence, type RiskId, type TestPlan } from '../test-ownership'
const CASES: Array<{
name: string
riskId: RiskId
expected: TestPlan
}> = [
{
name: '금액 반올림',
riskId: 'amount-rounding',
expected: {
evidence: [
{ check: 'javascript-jest', destinations: ['host'] },
],
},
},
{
name: '권한 callback contract',
riskId: 'permission-callback-contract',
expected: {
evidence: [
{ check: 'javascript-jest', destinations: ['host'] },
{
check: 'native-integration',
destinations: ['android-emulator', 'ios-simulator'],
},
],
},
},
{
name: '실제 카메라 촬영',
riskId: 'real-camera-capture',
expected: {
evidence: [
{ check: 'native-integration', destinations: ['physical-device'] },
{ check: 'user-flow-e2e', destinations: ['physical-device'] },
],
},
},
]
describe.each(CASES)('$name', ({ riskId, expected }) => {
test('막으려는 실패를 볼 수 있는 범위와 실행 위치를 보존한다', () => {
expect(planEvidence(riskId)).toEqual(expected)
})
})실행 전에 예측한다.
- 금액 반올림은 통과할까?
- 권한 콜백 계약에 네이티브 통합 test가 남을까?
- 실제 카메라 촬영에 physical device 목적지가 남을까?
앱 루트에서 실행한다.
npm test -- --runInBand __tests__/test-ownership.test.ts첫 사례만 통과하고 뒤의 두 사례가 실패해야 한다.
Test Suites: 1 failed, 1 total
Tests: 2 failed, 1 passed, 3 total이 실패는 실제 카메라 버그를 재현한 결과가 아니다. 세 위험에 필요한 테스트 범위와 실행 위치를 고르는 정책이 모든 것을 host Jest로 덮었다는 가상 증거다.
JavaScript 테스트는 빠른 로직과 대역 뒤의 계약을 맡는다
영수증 사례에서 Jest가 잘 맡는 질문은 다음과 같다.
- OCR 결과
15900을 제품 금액 상태로 바꾸는가? - 음수·소수·누락 필드를 거절하는가?
permission-denied를 사용자에게 보일 상태와 재시도 행동으로 바꾸는가?- 같은
requestId응답을 정확한 요청에 연결하는가?
여기서 OCR(Optical Character Recognition, 이미지 속 글자를 읽는 광학 문자 인식)은 실제 카메라가 아니라
이미 검증된 입력값으로 고정할 수 있다. 반대로 테스트 대역이 반환한 permission-denied를 잘 처리한다는
사실은 Android·iOS 구현이 그 값을 실제로 반환한다는 증거가 아니다.
네이티브 integration test는 플랫폼 연결을 직접 실행한다
Android에서 권한 콜백, 현재 Android 화면 단위인 Activity의 생성·종료 흐름, 네이티브 모듈 변환을 확인하는
코드는 일반적으로 android/app/src/androidTest/ 아래에 둔다. AndroidJUnitRunner는 앱과 별도 테스트
묶음을 기기에 올려 계측 테스트를 실행하는 도구다. 제품이 AndroidX Test 실행 도구와 필요한 라이브러리를
구성한 뒤, 앱 루트에서 다음처럼 연결된 Emulator 또는 기기의 테스트를 실행한다.
cd android
./gradlew connectedDebugAndroidTest
cd ..React Native 0.86.2 Community template은 JavaScript Jest fixture는 제공하지만 AndroidX Test 설정과
제품별 계측 테스트를 자동으로 만들지는 않는다. 따라서 위 명령을 이 글의 새 TestLayerApp에 바로
실행하라는 뜻이 아니다. 제품 프로젝트의 defaultConfig.testInstrumentationRunner,
androidTestImplementation 의존성과 src/androidTest test가 준비된 뒤 사용하는 통합 절차다.
iOS에서는 Xcode의 File > New > Target에서 Unit Testing Bundle 또는 UI Testing Bundle이라는 테스트
빌드 대상을 제품에 추가한다. 전자는 플랫폼 값을 앱 입력으로 바꾸는 연결 코드와 콜백 변환을 직접 호출하는
데, 후자는 설치한 앱을 열고 권한 거절 뒤 안내 화면이 보이는지 확인하는 데 적합하다. Community template에는
이 테스트 대상이 없으므로 추가 뒤 Product > Test로 실행하고, Xcode가 만든 테스트 결과 묶음인
.xcresult를 보존한다.
Android 계측 테스트와 iOS 테스트 대상이 같은 파일이나 테스트 도구를 써야 한다는 뜻은 아니다. 공통 계약은 “네이티브 구현에 준 입력과 실제 콜백, 앱에 전달한 결과”를 같은 한 번의 시도로 기록하는 것이다.
E2E는 설치된 앱에서 핵심 사용자 흐름을 확인한다
영수증 촬영의 E2E 범위는 다음처럼 제품 결과로 고정한다.
앱 시작 → 촬영 열기 → 권한 선택 → 영수증 촬영 → 금액 검토 화면이 흐름은 “사용자가 끝까지 갈 수 있는가?”를 강하게 확인하지만 금액 반올림의 모든 경계값이나 어느 플랫폼 콜백이 잘못됐는지를 가장 빠르게 설명하지는 않는다. 핵심 성공 흐름·권한 거절·재시작 복구처럼 사용자 영향이 큰 소수 경로를 맡기고, 세부 조합은 더 좁은 테스트로 내린다.
실기기는 별도 테스트 종류가 아니라 실행 목적지다
실기기에서 확인할 조건은 제품 능력에서 역으로 고른다.
| 위험 | 가상 환경이 먼저 맡을 수 있는 것 | physical device에서 닫을 것 |
|---|---|---|
| 카메라 | 권한 상태 전이, 고정 이미지·가상 카메라 입력 | 실제 렌즈 초점·회전·조도·제조사 카메라 구현 |
| 생체 인증 | 성공·취소·잠금 상태의 가짜 응답 | 기기에 등록된 생체 정보·실제 인증 창·재시도 정책 |
| 센서 | 고정 측정값과 누적 계산 함수 | 지원 여부·전달 지연·실제 단위와 앱 상태 전환 |
| 성능 | 결정적 기능 흐름과 성능 기록 도구 연습 | 지원 기기의 실제 화면 갱신 시간·CPU·발열 조건 |
Android는 Android Debug Bridge(ADB, 개발 컴퓨터에서 Android 기기 연결 상태와 명령을 다루는 도구)의
adb devices에서 물리 기기가 device 상태인지 확인하고, 한 기기만 연결한 뒤 앱 루트에서
npm run android로 실행한다. iOS는 Xcode 도구 막대에서 등록한 물리 기기를 실행 목적지로 고른 뒤
Product > Run 또는 Product > Test를 사용한다. 개인 기기 연결은 사용자 승인을 받고, 계정·알림·영수증
원문이 screenshot·video·log에 들어가지 않게 테스트 전용 입력을 사용한다.
성공 기록에는 최소한 다음을 남긴다.
build JavaScript bundle ID, Android/iOS build ID와 설정
scope unit / integration / E2E 중 실제 실행한 테스트 범위
destination Emulator / Simulator / physical device 중 실행 목적지와 모델·운영체제
dependencies fake·mock으로 바꾼 경계와 실제로 사용한 서버·SDK
result 입력, 기대 결과, 실제 결과, screenshot·video·test result 위치
limits 실행하지 않은 플랫폼·권한·기기 능력가장 작은 충분한 경계부터 넓힌다
fixture의 상수를 교정한다.
const USE_TEST_MATRIX = true다시 실행한다.
npm test -- --runInBand __tests__/test-ownership.test.tsPASS __tests__/test-ownership.test.ts
Test Suites: 1 passed, 1 total
Tests: 3 passed, 3 total교정된 세 행은 서로를 대체하지 않는다.
금액 반올림 JavaScript Jest → host
권한 콜백 계약 JavaScript Jest → host
native integration → Android Emulator + iOS Simulator
실제 카메라 촬영 native integration → physical device
user-flow E2E → physical devicefixture가 증명한 것은 이 matrix 정책과 Jest 실행뿐이다. 실제 Android 계측 test, iOS test target, E2E 도구, 카메라와 실기기 결과는 제품 통합에서 별도로 실행해야 한다.
pull request에서 달라져야 할 행동
테스트 관련 pull request에는 테스트 이름 목록보다 위험→범위→실행 위치→증거 연결이 보여야 한다.
위험 실패했을 때 사용자 영향과 재현 입력
JavaScript pure logic·state·component 결과와 host Jest
Android native 실제 모듈·권한 콜백·Activity 경계와 계측 test 결과
iOS native 플랫폼 연결·콜백 또는 UI 사용자 흐름과 Xcode test 결과
E2E 설치 build의 핵심 사용자 흐름과 실패 screenshot·video
physical device 기기 모델·운영체제·권한·능력·build와 실제 결과
대역 fake·mock으로 바꾼 경계와 그 때문에 증명하지 못한 범위
실행 주기 commit / pull request / 매일 밤 자동 실행 / 공개 후보 build 검증검토자는 다음 질문을 실제 증거로 닫는다.
- 통과한 JavaScript test가 어떤 네이티브 구현을 대역으로 바꿨는가?
- native integration test는 어느 플랫폼 코드와 콜백을 실제로 실행했는가?
- E2E는 release에 가까운 설치 build의 사용자 결과를 확인했는가?
- Emulator·Simulator 결과를 physical device 결과로 잘못 확대하지 않았는가?
- 실제 기기 검증이 필요한 능력과 대표 기기·운영체제 조합은 무엇인가?
- 불안정한 큰 test를 재시도로 숨기지 않고, 더 좁은 결정적 test로 원인을 내릴 수 있는가?
한 장으로 다시 보기
문제 mocked native module을 쓴 Jest 통과를 전체 촬영 검증으로 확대
실패 금액 규칙만 확인하고 네이티브 콜백·실제 카메라 능력은 실행하지 않음
JavaScript 순수 로직·상태·component 계약 → host Jest
Native Android 계측 / Xcode unit·integration·UI → Emulator·Simulator 또는 기기
E2E 설치된 앱의 핵심 사용자 흐름 → 공개 설정에 가까운 build와 실행 목적지
실기기 테스트 범위가 아니라 실제 하드웨어·성능을 보존하는 실행 목적지
교정 위험마다 가장 작은 충분한 범위와 필요한 실행 목적지를 함께 기록
경계 fixture의 matrix 통과는 제품 native test와 실기기 결과를 증명하지 않음테스트 계층은 신뢰도 순위표가 아니다. 좁은 test는 빠르고 원인을 잘 설명하고, 넓은 test는 실제 부분이 맞물리는 실패를 본다. physical device는 그 test가 필요한 실제 능력까지 실행하게 하는 목적지다. 테스트 범위와 실행 목적지를 분리해야 초록색 결과가 무엇을 증명했고 무엇을 아직 모르는지 말할 수 있다.
스스로 확인할 질문
- native module을 fake로 바꾼 Jest가 실제 Android·iOS 구현을 증명하지 못하는 이유는 무엇인가?
- Android local test와 계측 test의 가장 중요한 실행 위치 차이는 무엇인가?
- iOS unit·integration test와 UI test는 각각 어떤 경계를 직접 관찰하는가?
- E2E test를 모든 입력 조합에 쓰지 않는 이유는 무엇인가?
- physical device가 테스트 범위가 아니라 실행 목적지라는 말은 무슨 뜻인가?
- 카메라 촬영 pull request에서 가장 작은 test부터 실제 기기까지 어떤 증거를 연결해야 하는가?
Active recall
기억에서 꺼내 보기
답을 쓰지 않아도 됩니다. 먼저 머릿속으로 답한 뒤 펼쳐서 정답·이유·흔한 오해를 비교하세요.
01네이티브 카메라 모듈을 테스트 대역으로 바꾼 Jest 테스트가 통과하면 Android·iOS 구현도 맞다고 결론 내릴 수 있을까?
정답
아니다. 그 테스트는 JavaScript가 정해진 대역 응답을 처리하는 방식만 확인하고, 바꿔 놓은 실제 네이티브 구현과 플랫폼 콜백은 실행하지 않는다.
관련 설명 다시 읽기왜 그런가
대역은 확인하려는 경계 바깥을 통제할 때 유용하지만 대체한 경계 자체의 증거가 되지는 않는다.
선택한 상태와 다음 복습일은 이 브라우저에만 저장됩니다.
02Android 계측 테스트가 Emulator에서 통과하면 실제 카메라 렌즈·제조사 구현까지 확인했다고 볼 수 있을까?
정답
아니다. 계측 테스트는 Android 플랫폼 기능과 앱 연결을 실행하지만, Emulator가 대신 만든 기능과 실제 기기의 물리 능력은 같은 증거가 아니다.
관련 설명 다시 읽기왜 그런가
같은 테스트를 physical device에서도 실행할 수 있으며, 테스트 범위와 실행 목적지를 따로 기록해야 한다.
선택한 상태와 다음 복습일은 이 브라우저에만 저장됩니다.
03실기기에서 영수증 한 장을 성공적으로 찍었으면 금액 반올림 경계값 Jest 테스트를 빼도 될까?
정답
아니다. 실기기 흐름은 실제 연결과 능력을 확인하지만 작은 입력 조합을 빠르고 결정적으로 모두 반복하는 역할은 좁은 JavaScript 테스트가 더 잘 맡는다.
관련 설명 다시 읽기왜 그런가
넓은 테스트는 좁은 테스트를 지우는 상위 인증서가 아니라 서로 다른 실패를 관찰하는 보완 증거다.
선택한 상태와 다음 복습일은 이 브라우저에만 저장됩니다.
출처와 검증 범위
아래 날짜는 링크를 마지막으로 열어 본 날입니다. 문서 상단의 검증일은 글의 설명과 적용 범위를 다시 확인한 날입니다.
- TestingReact Native · 공식 문서 · 확인 2026-08-09
- React Native 0.86 Community template package.jsonReact Native Community · 소스 코드 · 확인 2026-08-09
- React Native 0.86 Community template Jest configurationReact Native Community · 소스 코드 · 확인 2026-08-09
- Fundamentals of testing Android appsAndroid Developers · 공식 문서 · 확인 2026-08-09
- Build local unit testsAndroid Developers · 공식 문서 · 확인 2026-08-09
- AndroidJUnitRunnerAndroid Developers · 공식 문서 · 확인 2026-08-09
- Automate UI testsAndroid Developers · 공식 문서 · 확인 2026-08-09
- Run apps on a hardware deviceAndroid Developers · 공식 문서 · 확인 2026-08-09
- TestingApple Developer · 공식 문서 · 확인 2026-08-09
- Adding tests to your Xcode projectApple Developer · 공식 문서 · 확인 2026-08-09
- Running your app on simulated or physical devicesApple Developer · 공식 문서 · 확인 2026-08-09
- Running On DeviceReact Native · 공식 문서 · 확인 2026-08-09