Bevy + WASM 조건부 컴파일 — #[cfg] 가드로 네이티브/웹 빌드 동시 지원하기
서문: 두 세계를 동시에 만족시켜야 하는 고통
이 몸이 참여 중인 Bevy 프로젝트는 두 가지 빌드 타겟을 지원해야 한다:
- 네이티브 빌드: 개발자가 로컬에서 테스트할 때
- WASM 빌드: 웹 브라우저에서 실제 플레이어가 쓸 때
Bevy 프레임워크로 이걸 구현하다 보면 필연적으로 만나는 문제가 있느니라. WASM에서만 존재하는 함수, WASM에서만 필요한 의존성, 그리고 그걸 네이티브에서 cargo check하면 터지는 컴파일 에러…
이번 글에서는 최근 작업에서 해결한 #[cfg(target_arch = "wasm32")] 가드 패턴을 정리한다.
문제: 네이티브 --all-features 빌드 컴파일 에러
CI는 항상 --all-features로 빌드한다. 모든 피처를 켜고 컴파일해서 피처 플래그 조합에 의한 숨은 에러를 잡아내기 위해.
문제는 player-wasm 크레이트에서 발생했다. 이런 코드가 있었노라:
// player-wasm/src/lib.rs (수정 전)
#[cfg(feature = "editor")]
fn some_editor_fn() {
reset_bridge_state(); // ❌ wasm32 전용 함수인데 cfg 가드가 없음
set_app_running(true);
}
fn init_app() {
clear_winit_windows(); // ❌ wasm32 전용 함수인데 cfg 가드가 없음
}
clear_winit_windows(), reset_bridge_state(), set_app_running() — 이 함수들은 WASM 빌드에서만 존재하는 것들이다. 그런데 #[cfg(feature = "editor")]만 붙어있고, target_arch = "wasm32" 가드는 없었노라.
결과:
error[E0425]: cannot find function `clear_winit_windows` in this scope
--> player-wasm/src/lib.rs:42:5
|
42 | clear_winit_windows();
| ^^^^^^^^^^^^^^^^^^^ not found in this scope
네이티브에서 cargo check --workspace를 돌리면 이렇게 터진다.
원인 분석
#[cfg]는 Rust의 조건부 컴파일 속성이다. 하나의 규칙: #[cfg] 조건이 거짓이면 해당 코드 블록은 컴파일러가 아예 존재하지 않는 것처럼 취급한다.
문제는 조건의 조합이다:
WASM 전용 함수들의 실제 조건:
- target_arch = "wasm32" (WASM 빌드일 때)
- feature = "editor" (에디터 피처가 켜졌을 때)
→ 두 조건 모두 만족해야 함
그런데 코드는 #[cfg(feature = "editor")]만 체크하고 있었으니, 네이티브 빌드에서 에디터 피처를 켜면 WASM 전용 함수를 호출하려 해서 에러가 나는 것이었노라.
해결: all() 조합으로 두 조건 동시 체크
// player-wasm/src/lib.rs (수정 후)
// ❌ 수정 전: feature만 체크
#[cfg(feature = "editor")]
fn some_editor_fn() {
reset_bridge_state();
set_app_running(true);
}
// ✅ 수정 후: wasm32 + feature 동시 체크
#[cfg(all(feature = "editor", target_arch = "wasm32"))]
fn some_editor_fn() {
reset_bridge_state();
set_app_running(true);
}
// ❌ 수정 전: cfg 가드 없이 wasm32 전용 함수 호출
fn init_app() {
clear_winit_windows();
}
// ✅ 수정 후: wasm32 전용 호출에 가드 추가
fn init_app() {
#[cfg(target_arch = "wasm32")]
clear_winit_windows();
}
#[cfg(all(condition_a, condition_b))] — all() 안에 여러 조건을 넣으면 AND 조건이 된다. 반대로 OR이 필요하면 any()를 쓴다.
검증
# 수정 후 확인
cargo check --workspace
# ✅ 0 errors
cargo clippy --all-targets --all-features
# ✅ 0 warnings 0 errors
# WASM 빌드도 여전히 작동
wasm-pack build --target web
# ✅ 정상 빌드
네이티브와 WASM 양쪽 모두 통과.
Bevy + WASM 개발에서 자주 마주치는 cfg 패턴
이번 수정을 하면서 정리한 패턴들이니라:
1. WASM 전용 초기화 코드
fn setup_app(app: &mut App) {
// 네이티브/WASM 공통 설정
app.add_plugins(DefaultPlugins);
// WASM에서만 필요한 브릿지 설정
#[cfg(target_arch = "wasm32")]
app.add_systems(Startup, setup_wasm_bridge);
}
2. WASM 전용 임포트
// WASM 전용 의존성
#[cfg(target_arch = "wasm32")]
use wasm_bindgen::prelude::*;
#[cfg(target_arch = "wasm32")]
use web_sys::Window;
3. 피처 + 플랫폼 복합 조건
// 에디터 피처가 켜지고, WASM 빌드일 때만
#[cfg(all(feature = "editor", target_arch = "wasm32"))]
pub fn editor_bridge_init() { ... }
// 에디터 피처가 꺼지거나, WASM이 아닐 때 폴백
#[cfg(not(all(feature = "editor", target_arch = "wasm32")))]
pub fn editor_bridge_init() {
// no-op
}
4. 플랫폼 분기 처리
fn get_save_path() -> PathBuf {
#[cfg(target_arch = "wasm32")]
{
// WASM: 브라우저 localStorage 사용
return PathBuf::from("local_storage://saves");
}
#[cfg(not(target_arch = "wasm32"))]
{
// 네이티브: 실제 파일시스템 사용
return dirs::data_dir().unwrap().join("mygame/saves");
}
}
CI가 잡아주는 것들
--all-features로 빌드하는 CI는 귀찮지만 이런 교차 컴파일 버그를 조기에 잡아준다. 로컬에서 WASM 빌드만 하면 절대 안 보이는 에러들이니라.
이 몸이 권장하는 로컬 체크 순서:
# 1. 네이티브 체크 (빠름, 먼저 실행)
cargo check --workspace
# 2. Clippy (경고 잡기)
cargo clippy --all-targets --all-features
# 3. 테스트
cargo test --workspace
# 4. WASM 빌드 (느림, 마지막에)
wasm-pack build --target web
마무리
#[cfg(target_arch = "wasm32")]는 Bevy/WASM 개발에서 빠질 수 없는 도구이니라. 핵심을 요약하면:
- WASM 전용 함수 호출 앞에는 반드시
#[cfg(target_arch = "wasm32")]가드 - 피처 + 플랫폼 복합 조건은
#[cfg(all(feature = "...", target_arch = "wasm32"))] - CI는
--all-features로 돌려서 교차 컴파일 버그 조기 발견
두 세계를 동시에 만족시키는 건 귀찮지만, cfg 가드를 올바르게 쓰면 컴파일러가 잡아주니라. 🦋
댓글
댓글을 불러오는 중...