React 웹 + RN Expo 웹뷰 하이브리드 구조에서 삽질한 내용 정리
프로젝트 구조
이 글은 아래 구조를 전제로 한다.
RN+Expo 앱
└── WebView
└── React 웹앱 (react-router-dom)
웹이랑 앱 따로 개발하고, 앱에서 웹뷰로 웹을 띄우는 방식이다. 처음엔 단순해 보였는데 뒤로가기 하나 구현하는 데 꽤 헤맸다.
웹 쪽 - react-hammerjs로 스와이프 뒤로가기
모바일 웹에서 오른쪽 스와이프로 뒤로가기를 구현하려면 react-hammerjs를 쓰면 된다.
설치
npm install react-hammerjs
타입 에러가 날 텐데 npm i --save-dev @types/hammerjs 를 해도 안되면 아래처럼 세팅 해준다
// declarations.d.ts 생성
declare module 'react-hammerjs';
// tsconfig.json
// declarations.d.ts 추가
//... 생력
"include": ["src", ..., "global.d.ts", "declarations.d.ts"]
이후 provider로 감싸려고 아래처럼 만들었다.
// GestureProvider.tsx
// @ts-ignore
import Hammer from 'react-hammerjs';
import { useNavigate, useLocation } from 'react-router-dom';
interface HammerInput {
direction: number;
deltaX: number;
deltaY: number;
}
const DIRECTION_RIGHT = 4;
const BLOCKED_PATHS = ['/login'];
export default function GestureProvider({ children }: { children: React.ReactNode }) {
const navigate = useNavigate();
const location = useLocation();
const handleSwipe = (e: HammerInput) => {
if (e.direction === DIRECTION_RIGHT && !BLOCKED_PATHS.includes(location.pathname)) {
navigate(-1);
}
};
return (
<Hammer
onSwipe={handleSwipe}
options={{
recognizers: {
swipe: { threshold: 10, velocity: 0.3 },
},
}}
>
<div style={{ width: '100%', height: '100%' }}>{children}</div>
</Hammer>
);
}
방향 상수 정리
상수 값
| 왼쪽 | 2 |
| 오른쪽 | 4 |
| 위 | 8 |
| 아래 | 16 |
⚠️ 주의할 점 두 가지
1. Hammer는 단일 DOM 요소만 자식으로 받는다
// ❌ 이렇게 하면 안 됨
<Hammer>
<Routes>...</Routes>
</Hammer>
// ✅ div로 감싸야 함
<Hammer>
<div style={{ width: '100%', height: '100%' }}>
<Routes>...</Routes>
</div>
</Hammer>
2. GestureProvider는 반드시 Router 안에 있어야 한다
useNavigate는 Router 컨텍스트 안에서만 동작한다. Router 밖에 두면 useNavigate() may be used only in the context of a <Router> 에러 난다.
// App.tsx
function App() {
return (
<Router>
<GestureProvider> {/* ✅ Router 안에 */}
<Routes>
...
</Routes>
</GestureProvider>
</Router>
);
}
앱 쪽 - WebView BackHandler로 안드로이드 뒤로가기
안드로이드 하드웨어 백버튼은 RN에서 처리해야 한다.
웹뷰 하이브리드 구조에서 핵심은 웹뷰의 히스토리 스택을 RN이 모른다는 것이다. 그래서 router.back()이나 router.canGoBack() 같은 expo-router API로는 제대로 제어가 안 된다.
WebView ref의 goBack()으로 직접 웹뷰 히스토리를 제어해야 한다.
// _layout.tsx (expo-router)
import { useEffect, useRef } from 'react';
import { BackHandler } from 'react-native';
import { WebView } from 'react-native-webview';
export default function App() {
const webViewRef = useRef(null);
const canGoBackRef = useRef(false);
useEffect(() => {
const goBack = () => {
if (canGoBackRef.current) {
webViewRef.current?.goBack(); // ✅ 웹뷰 뒤로가기
return true;
}
return false; // 앱 종료
};
const subscription = BackHandler.addEventListener(
'hardwareBackPress',
goBack,
);
return () => subscription.remove();
}, []);
return (
<WebView
ref={webViewRef}
onMessage={handleMessage}
...생략
onNavigationStateChange={(navState) => {
// ✅ 웹뷰 히스토리 변경될 때마다 업데이트
canGoBackRef.current = navState.canGoBack;
}}
style={[
styles.container,
{ marginTop: inset.top, marginBottom: inset.bottom },
]}
/>
);
}
removeEventListener는 최신 RN에서 없어졌다. subscription.remove()로 써야 한다.
최종 정리
환경 방식
| 웹 스와이프 뒤로가기 | react-hammerjs → navigate(-1) |
| 안드로이드 백버튼 | BackHandler → webViewRef.goBack() |
| iOS | 기본 제공, 별도 처리 불필요 |
웹뷰 하이브리드는 웹이랑 앱이 서로 히스토리를 공유하지 않기 때문에 각 환경에서 독립적으로 처리해줘야 한다는 게 핵심이다.
'React & TypeScript' 카테고리의 다른 글
| 문자열 리터럴 타입이란? as const (0) | 2026.01.15 |
|---|---|
| 안 쓰는 import 자동으로 제거하기 (3) | 2025.07.28 |
| [React] 새 프로젝트 설치 + tailwindcss 설치 (0) | 2025.02.21 |
| Redux 사용법 (0) | 2025.01.12 |
| [React] React 라이프 사이클 이해하기 + useEffect의 동작 순서 (1) | 2024.12.17 |