Expo + React Native Skia + Reanimated 4 + Gesture Handler + Zustand 기반의 모바일 앱 게임 개발 가이드. 타워 디펜스, 룰렛 뽑기, 퍼즐, 아케이드 등 다양한 장르의 2D 모바일 게임을 만들 때 반드시 이 스킬을 사용하라. 사용자가 "React Native 게임", "Expo 게임 만들기", "Skia 게임", "모바일 게임 개발", "타워 디펜스", "룰렛 시스템", "게임 루프", "useFrameCallback", "SharedValue 애니메이션" 등을 언급할 때 즉시 트리거한다.
Expo + React Native Skia + Reanimated 4 + Gesture Handler + Zustand 스택으로 모바일 게임을 구축하는 완전한 가이드다.
이 스택의 핵심은 어느 스레드에서 무엇을 처리하느냐다:
| 레이어 | 라이브러리 | 담당 | 스레드 |
|---|---|---|---|
| 렌더링 | React Native Skia | GPU 가속 2D 그래픽 | GPU |
| 게임 루프/애니메이션 | Reanimated 4 | 60fps 루프, 실시간 위치 | UI Thread |
| 제스처 | Gesture Handler | 터치, 스와이프, 탭 | UI Thread |
| 게임 메타 상태 | Zustand | 웨이브, 골드, 스킬, 점수 | JS Thread |
절대 규칙:
useSharedValue (UI 스레드)runOnJS → Zustand (JS 스레드)새 게임 프로젝트를 시작할 때 순서대로 따른다:
references/setup.md 참고references/rendering.md 참고references/game-loop.md 참고references/gestures.md 참고references/state.md 참고references/architecture.md 참고references/optimization.md 참고import { useSharedValue } from 'react-native-reanimated';
import { Canvas, Circle } from '@shopify/react-native-skia';
// SharedValue를 Skia props에 직접 전달 → bridge 없이 UI 스레드에서 렌더링
const EnemySprite = ({ startX, startY }) => {
const x = useSharedValue(startX);
const y = useSharedValue(startY);
return (
<Canvas style={{ flex: 1 }}>
<Circle cx={x} cy={y} r={15} color="crimson" />
</Canvas>
);
};
import { runOnJS } from 'react-native-reanimated';
// worklet 안에서 Zustand 업데이트가 필요할 때
const tick = (frameInfo) => {
'worklet';
if (enemy.hp.value <= 0) {
enemy.isAlive.value = false;
// Zustand는 JS 스레드 → runOnJS 필수
runOnJS(onEnemyKilled)(enemy.id.value, enemy.goldReward.value);
}
};
import { Gesture, GestureDetector } from 'react-native-gesture-handler';
import { useSharedValue, withDecay, runOnJS } from 'react-native-reanimated';
import { Canvas, Group } from '@shopify/react-native-skia';
const RouletteWheel = ({ onSpinComplete }) => {
const rotation = useSharedValue(0);
const panGesture = useMemo(() =>
Gesture.Pan()
.onUpdate((e) => { 'worklet'; rotation.value += e.changeY * 0.01; })
.onEnd((e) => {
'worklet';
rotation.value = withDecay(
{ velocity: e.velocityY * 0.005, deceleration: 0.997 },
(finished) => { 'worklet'; if (finished) runOnJS(onSpinComplete)(rotation.value); }
);
}),
[rotation, onSpinComplete]);
return (
<GestureDetector gesture={panGesture}>
<Canvas style={{ width: 300, height: 300 }}>
<Group transform={[{ rotate: rotation }]} origin={{ x: 150, y: 150 }}>
{/* 룰렛 세그먼트 */}
</Group>
</Canvas>
</GestureDetector>
);
};
| 게임 장르 | 핵심 시스템 | 참고 파일 |
|---|---|---|
| 타워 디펜스 | 게임 루프, 적 이동, 충돌 | game-loop.md, architecture.md |
| 룰렛/뽑기 | Pan 제스처, withDecay | gestures.md, rendering.md |
| 퍼즐/보드게임 | Tap 제스처, 상태 관리 | gestures.md, state.md |
| 아케이드 | 게임 루프, 파티클 이펙트 | game-loop.md, rendering.md |
{
"react-native": ">=0.79",
"react": ">=19",
"@shopify/react-native-skia": ">=2.0.0",
"react-native-reanimated": ">=4.0.0",
"react-native-gesture-handler": ">=2.0.0",
"expo": ">=52"
}
React Native < 0.79라면
@shopify/[email protected] 이하를 사용해야 한다.
runOnJS로 감싸야 한다useMemo로 메모이제이션 필수runOnUI, scheduleOnRuntime은 react-native-worklets로 이동됨timeSincePreviousFrame / 1000을 반드시 곱한다