8 min read

[RN] 코드 인젝션으로 웹페이지 동작 수정하기

[RN] 코드 인젝션으로 웹페이지 동작 수정하기
[RN] 코드 인젝션으로 웹페이지 동작 수정하기
Code Injection with Webview(mock 이미지 출처:https://github.com/react-native-webview/react-native-webview/blob/master/docs/Reference.md)

RN , AI Tools, react-native-webview

글의 순서

  • 문제 상황
  • 요구사항 파악하기
  • 괜찮은 해결 방법 찾기
  • 몰랐던 사실 & 느낀 점

문제상황

어느 날 앱 개발자로부터 도착한 메세지

  • 👨‍💻(앱 개발자): Next routing 사용해서 화면 전환 하면 client-side 화면 이동만 되나요?
  • 👨‍💻(앱 개발자): Exhibition 페이지에서 router.push로 화면이동할때 캐치해야하는데 캐치가 안되어서요.
  • 사전 설명
    • 앱에서 일부 페이지를 웹뷰로 사용하고 있습니다.
      • 그 동안은 Privacy Policy와 같이 사용자 상호작용이 없는 페이지만 웹뷰로 사용했고, Header나 Footer 등은 가려서 사용자가 웹뷰 안에서 페이지 이동할 우려가 없었습니다.
      • 그런데 이번에 작품들을 전시 형식으로 보여주는 Exhibition 페이지를 웹뷰로 사용하게 되면서 사용자가 웹뷰내에서 페이지이동을 시도하면 그것을 막아야할 필요가 생겼습니다.
    • 따라서 앱 개발자가 페이지 이동을 감지하여 막으려고 한 것이죠. 그런데 생각대로 되지 않았습니다. 이유는, Next.js의 라우터는 클라이언트 사이드 렌더링을 하기 때문이죠.
      • 결론: 웹뷰내에서 페이지 이동을 막을 방법이 필요합니다.
  • 앱 개발자의 요구사항
    • 전시(Exhibition) 페이지에서 라우팅시 페이지를 Reload 해줄 수 없나요?

요구사항 파악하기

문제가 되는 상황을 모두 파악하기

  • Exhibition에서 문제가 되는 상황, 즉 화면 전환을 캐치 해야 하는 상황은 아래와 같습니다.
  • 페이지 이동이 일어나는 경우1: 작품 클릭
    • 작품을 누르면 작품 상세 모달이 뜨고, 거기서 버튼을 누르면 화면이 이동됩니다.
        • Buy 버튼: 구매 페이지로 이동 -> 웹 페이지 이동을 막고, 앱의 구매 페이지로 이동해야함
        • Detail 버튼: 작품 상세 페이지로 이동 -> 웹 페이지 이동을 막고, 앱의 작품 상세 페이지로 이동해야함
  • 페이지 이동이 일어나는 경우2: Exit 버튼 클릭
    • Exit 버튼을 누르면 뒤로가기 처리된다. -> 웹 페이지 뒤로가기를 막고, 앱내에서의 이전 위치로 이동해야 합니다.

문제와 관련하여 요구사항 분석하기

저에게 처음 주어진 요구사항은 "앱에서 페이지 변경을 감지할 수 있도록 Reload 하기"로 보였습니다.

그런데 페이지 이동을 캐치하는 이유를 다시 생각해보면

  • 웹뷰의 페이지 이동을 다른 행동으로 대체하기 위해(ex: 앱의 상품 디테일 화면으로 이동)

앱에서 Routing을 핸들링 할 수 있으면 문제가 해결 된다는 사실을 알 수 있습니다.

괜찮은 해결 방법 찾기

해결책 1: JavaScript Injection

  • 익숙한 문제가 아니기 때문에 Claude AI에게 일반적인 해결책을 물어보았습니다.
  • Claude AI가 알려준 방법은 JavaScript Injection을 사용하는 것이었습니다.
  • 알려준 방법을 그대로 사용할 수는 없었지만 버튼 클릭 동작을 인터셉트 할 수 있으면 좋겠다는 생각이 들어 앱 개발자에게 공유하였습니다.

해결책 2: Query Params

  • 사실 가장 먼저 머리속에 떠오른 방법은 header와 footer를 가리는 데 사용했던 Query Params로 fullReload를 컨트롤 하는 방법입니다.
    • Query Params에 fullReload가 true인 경우에만 router.push대신 window.location.href를 사용하여 전체 리로드 일어나게 분기하는 방법입니다.
const fullReload = queryParams.fullReload === 'true';
    
if (fullReload) {
  // fullReload가 true인 경우 전체 페이지 리로드
  window.location.href = path;
} else {
  // 그 외의 경우 router.push 사용
  router.push(path);
};
  • 동작은 하지만 좋은 방법이라고 생각되지 않아서 사용하지 않았습니다.
    • 좋은 방법이 아닌 이유1: front에서 처리해야하는 쿼리 파라미터가 추가되어 복잡도가 높아짐
    • 좋은 방법이 아닌 이유2: 웹 개발자가 실수로 분기를 삭제하거나 쿼리 파라미터를 무시하면 쉽게 깨짐
    • 좋은 방법이 아닌 이유3: 웹뷰 진입시 해당 쿼리를 달고 들어오면 무조건 페이지 변경 시 Full Reload가 일어나게 됨(경우에 따라 Full Reload를 선택 적용하는 것이 불가)
    • 좋은 방법이 아닌 이유4: 프론트에서 코드만 보고는 무슨 의도로 작성된 코드인지 한번에 알아채기 힘듦.(명확하지 않음)

해결책2(JavaScript Injection)의 구현

  • 끌로드가 일반적인 해결책이라고 알려준 JavaScript Injection으로 구현해보기로 합니다.
  • 다시 한번 경우의 수를 생각해보면,
    • 작품 클릭 > 버튼 클릭 > 페이지 이동
    • exit 버튼 클릭 > 페이지 이동
  • 모든 경우의 수가 Router(페이지 이동)와 관련되어 있다.
  • 그러면 Code Injection을 통해서, Router의 동작을 변경해보는 것은 어떨까요?
  • 가능여부를 Claude에게 물어보고 아래와 같은 결과물을 도출할 수 있었습니다.
// router의 동작을 컨트롤 하기 위한 code injection.
// 아래의 함수를 WebView 컨포넌트의 
// injectedJavaScript,injectedJavaScriptBeforeContentLoaded
// props에 넘겨주어야 합니다.

const injectedJavaScript = `
    (function() {
      const overrideRouterPush = function() {
        window.isNativeApp = true;
        const originalPush = window.next.router.push;
        window.next.router.push = function(url, as, options) {
          const shouldInterceptPush = [
            '/buy',
            '/artwork'
          ].some(pattern => url?.includes(pattern));

          if (shouldInterceptPush) {
            window.ReactNativeWebView.postMessage(JSON.stringify({
              event: 'routerPush',
              data: {
                url: url,
              },
            }));
            return;
          }

          originalPush.call(this, url, as, options);
        };
      };

      window.onload = overrideRouterPush;
    })();
  `;
  • 그러면 Query Params를 사용하는 방법에서는 불가능했던 선택적용(경우에 따라 기존의 router.push 사용 or 페이지 이동 막음)을 앱에서 컨트롤 가능합니다.
    • Step1: Javascript Injection으로 Next.js의 router.push를 modify(메세지 전송, 위의 코드 참조)
    • Step2: onMessage에서 메시지를 받아서 분기처리(아래 코드 참조)
// WebView 컴포넌트에 넘기는 onMessage props의 예시
// 여기서 javascript codeinjection에서 넘긴 메세지를 받아 처리할 수 있다.
const onMessage = (event) => {
  const message = JSON.parse(event.nativeEvent.data) as Message;
  if (message.event === 'routerPush') {
    const url = getUrl(message.data.url);

    if (url === 'artwork') {
      // 앱의 아트워크 디테일 페이지로 이동
      return false;
    };

    if (url === 'buy') {
      // 앱의 구매 페이지로 이동
      return false;
    };
  };
  return true;
};

해결책 1과 해결책 2의 차이

  • Code Injection을 사용하는 방법에서는 Query Params를 이용하는 방법(해결책2)과 비교해보면 앱에서 필요한 요구사항을 앱에서 작성하므로 혼란의 여지가 적습니다.
  • 프론트 개발자와 소통하지 않고 직접 컨트롤이 가능하며 프론트에서 실수하여 버그가 생길 우려가 비교적 적습니다.

몰랐던 사실 & 느낀 점

  • 안드로이드는 injectedJavaScriptBeforeContentLoaded를 붙여야 합니다.
    • Claude AI 에게 RN 관련 코드를 물어볼때는 '안드로이드에서 작동하게 하려면 추가할 것은 없습니까?'라고 한번 더 물어보는 것이 좋을 것 같습니다.
  • 입사 후 1-2개월간 앱과 웹을 모두 작업했는데, 그 이후로는 웹쪽만 작업하여 오랜만에 앱 작업을 했습니다. 처음에는 테스트 용으로 작성했는데 그 코드가 실제로 적용되어 뿌듯하기도 했고요.
  • IOS와 안드로이드를 각각 시뮬레이터와 에뮬레이터로 세팅하는 것도 오랜만에 하다보니 시간이 꽤 걸렸습니다.
  • 요구사항이 있다고 해서 그대로 작업하는 것이 아니라 분석을 해보는 자세가 좋은 것 같고, 시간은 더 걸렸지만 더 나은 방법으로 작업한 것 같습니다.