본문 바로가기
React & TypeScript

웹뷰 하이브리드 앱에서 뒤로가기 제대로 구현하기 (react-hammerjs + WebView BackHandler)

by 어느새벽 2026. 3. 12.
반응형

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 기본 제공, 별도 처리 불필요

웹뷰 하이브리드는 웹이랑 앱이 서로 히스토리를 공유하지 않기 때문에 각 환경에서 독립적으로 처리해줘야 한다는 게 핵심이다.

반응형