J.
BLOG
글 목록소개구독
기술

Rust 공통 모듈을 크로스플랫폼에서 공유하기 - 7편. 테스트와 CI로 같은 결과 보장하기

2026.07.26·읽기 6분

Rust 공통 모듈을 쓰는 이유는 코드 줄 수를 줄이기 위해서만은 아니다.

진짜 장점은 같은 규칙을 한 곳에서 검증하고, iOS, Android, Web이 같은 결과를 쓰게 만드는 데 있다. 그런데 테스트와 CI가 받쳐주지 않으면 이 장점은 금방 약해진다.

Rust test는 통과했지만 iOS에서 link가 깨질 수 있다. Android debug build는 되지만 release AAB에 특정 ABI가 빠질 수 있다. Web dev server에서는 되지만 production build에서 wasm 초기화가 실패할 수 있다. 공통 모듈은 오히려 CI에서 더 엄격하게 다뤄야 한다.

이번 글에서는 Rust core 공유 구조에서 테스트와 CI를 어떻게 나누면 좋은지 정리한다.

가장 많은 테스트는 Rust core에 둔다

도메인 규칙은 Rust core에 있으므로, 가장 많은 테스트도 Rust core에 있어야 한다.

스도쿠 앱이라면 이런 테스트가 core에 들어간다.

  • puzzle generation
  • solver correctness
  • invalid move validation
  • note toggle reducer
  • daily seed determinism
  • difficulty classification
  • serialization round-trip
  • boundary value
  • error mapping

이 테스트들은 iOS simulator나 Android emulator 없이도 실행되어야 한다. cargo test만으로 도메인 규칙의 대부분을 검증할 수 있어야 한다.

여기서 중요한 것은 deterministic test다. 같은 seed와 같은 action sequence를 넣으면 항상 같은 결과가 나와야 한다. 시간, locale, random source, timezone이 결과에 숨어들면 플랫폼마다 다른 결과가 나올 수 있다.

그래서 Rust core에는 외부 상태를 직접 넣지 않는 편이 좋다. 현재 시각이 필요하면 timestamp를 인자로 받는다. random이 필요하면 seed를 명시한다. locale이 필요하면 locale code를 외부에서 넘긴다.

golden fixture로 플랫폼 간 결과를 고정한다

공통 Rust core에서는 fixture가 매우 중요하다.

fixture는 단순한 샘플 데이터가 아니다. “이 입력이면 이 결과가 나와야 한다”는 계약이다.

예를 들어 이런 fixture를 둘 수 있다.

fixtures/
  start-game-easy-seed-001.json
  apply-action-set-value-valid.json
  apply-action-set-value-conflict.json
  daily-puzzle-2026-06-24.json
  completed-game-score.json

Rust test는 이 fixture를 읽어 engine output을 검증한다. iOS test는 같은 fixture를 읽어 Swift adapter가 DTO를 올바르게 해석하는지 확인한다. Android test는 Kotlin adapter와 serialization 설정을 검증한다. Web test는 TypeScript wrapper가 wasm output을 맞게 변환하는지 확인한다.

이렇게 하면 각 플랫폼 테스트가 Rust core의 모든 알고리즘을 다시 검증할 필요가 없다. 각 플랫폼은 자신의 adapter 경계를 검증하면 된다.

역할 분리가 중요하다.

Rust test
  -> 도메인 규칙이 맞는가

iOS test
  -> Swift adapter가 Rust 결과를 맞게 해석하는가

Android test
  -> Kotlin adapter와 native library 연결이 맞는가

Web test
  -> wasm package와 TypeScript wrapper가 맞는가

CI는 “빌드된다”보다 “재현된다”를 봐야 한다

공통 Rust core 프로젝트에서 CI의 목표는 단순히 pass 표시를 받는 것이 아니다. fresh clone에서 모든 산출물이 재현되는지 확인해야 한다.

최소한 다음 단계는 나눠서 확인하는 편이 좋다.

Rust
  cargo fmt
  cargo clippy
  cargo test

UniFFI
  generate Swift binding
  generate Kotlin binding
  generated output validation

Apple
  build Rust static library
  create XCFramework
  xcodebuild build/test

Android
  build Rust .so per ABI
  generate Kotlin binding
  Gradle unit test
  assemble release or debug
  native library packaging check

Web
  wasm-pack build
  typecheck
  test
  production build

이 단계 중 하나라도 로컬에서만 되고 CI에서 안 되면 위험하다. 특히 generated binding과 binary artifact는 stale 상태가 되기 쉽다. CI에서 재생성했을 때 diff가 생기는지 확인하는 방식도 좋다.

플랫폼별 실패를 분리해서 봐야 한다

Rust core가 하나라고 해서 실패 원인도 하나가 되는 것은 아니다.

iOS 실패는 XCFramework slice 누락, Swift binding mismatch, Xcode version 차이, simulator/device target 차이에서 생길 수 있다. Android 실패는 ABI 누락, .so 이름 mismatch, NDK version, 16 KB page size, Gradle packaging에서 생길 수 있다. Web 실패는 wasm 초기화, bundler 설정, SSR 경계, TypeScript declaration mismatch에서 생길 수 있다.

그래서 CI job을 너무 크게 하나로 묶으면 디버깅이 어려워진다.

가능하면 job을 역할별로 나눈다.

  • rust-core
  • uniffi-bindings
  • apple-xcframework
  • android-native
  • web-wasm
  • contract-fixtures

이렇게 나누면 어떤 계층이 깨졌는지 빨리 보인다. 공통 fixture가 깨졌다면 도메인 계약 문제일 가능성이 높고, iOS job만 깨졌다면 Apple packaging 문제일 가능성이 높다.

release checklist가 필요하다

공통 Rust core는 앱 릴리즈의 일부다. Rust crate만 업데이트했다고 끝이 아니다.

release 전에 확인해야 할 항목이 있다.

  • Rust core version이 앱에 반영됐는가
  • UniFFI binding이 최신 Rust API와 일치하는가
  • iOS XCFramework가 device/simulator slice를 모두 포함하는가
  • Android release artifact에 필요한 ABI가 모두 포함되는가
  • Android 16 KB page size 요구사항을 만족하는가
  • Web production build에서 wasm package가 정상 초기화되는가
  • fixture가 모든 플랫폼 테스트에서 같은 결과를 내는가

이 체크리스트는 문서로만 남기면 잘 잊힌다. 가능하면 CI로 자동화하고, 자동화가 어려운 항목만 release checklist에 남긴다.

좋아 보이지만 팀 단위로 도입할 때는 기준이 필요하다. Rust core 변경 PR은 플랫폼 앱 build까지 요구할 것인지, 아니면 nightly나 release branch에서만 전체 matrix를 돌릴 것인지 정해야 한다. 모든 PR에서 full matrix를 돌리면 비용이 커질 수 있다.

현실적인 타협은 이렇다.

  • Rust core PR: Rust test, binding generation, fixture test는 항상 실행
  • 플랫폼 통합 PR: 해당 플랫폼 build/test 실행
  • release candidate: iOS, Android, Web 전체 matrix 실행

이렇게 하면 비용과 안정성 사이의 균형을 잡을 수 있다.

그래서 무엇부터 보면 좋을까

  • Rust core에 가장 많은 도메인 테스트를 둔다.
  • seed, 시간, locale 같은 외부 상태는 명시적으로 주입한다.
  • golden fixture를 만들어 세 플랫폼이 같은 결과를 검증하게 한다.
  • 플랫폼 테스트는 core 알고리즘보다 adapter 해석을 검증한다.
  • CI에서 binding generation과 binary artifact 생성을 재현한다.
  • iOS, Android, Web job을 분리해 실패 원인을 좁힌다.
  • Android는 release artifact와 16 KB page size를 체크한다.
  • Web은 production build 기준으로 wasm 초기화를 확인한다.
  • release checklist를 문서가 아니라 CI 중심으로 옮긴다.

마무리

Rust 공통 모듈은 잘 만들면 세 플랫폼의 도메인 규칙을 하나로 묶어준다. 하지만 그 신뢰는 테스트와 CI에서 나온다.

cargo test만 통과하는 것으로는 부족하다. Swift adapter, Kotlin adapter, wasm package, XCFramework, Android .so, Web production build까지 같은 흐름 안에서 검증해야 한다.

결국 핵심은 “같은 코드를 쓴다”가 아니라 “같은 결과를 증명한다”에 있다. 이 기준을 잡아두면 Rust 공통 모듈은 단순한 기술 실험이 아니라, 앱 품질을 지키는 중심 계층이 될 수 있다.

출처

광고Coupang Partners

이 포스팅은 쿠팡 파트너스 활동의 일환으로, 이에 따른 일정액의 수수료를 제공받습니다.

#Rust#CI-CD#Testing#UniFFI#FFI
이전 글Rust 공통 모듈을 크로스플랫폼에서 공유하기 - 6편. WebAssembly로 Web에 배포하기
© 2026 진재명 · blog.jaemyeong.com
iOS 소프트웨어 엔지니어 · 부산