[rn] 코드 인젝션으로 웹뷰 컨트롤하기

5 min read
Cover Image for [rn] 코드 인젝션으로 웹뷰 컨트롤하기

#RN , #AI Tools, #react-native-webview

글의 순서

  • 문제 상황

  • 요구사항 파악하기

  • 괜찮은 해결 방법 찾기

  • 몰랐던 사실 & 느낀 점

문제상황

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

분홍색 배경에 흰색 물음표

  • 👨‍💻앱 개발자 ==> ✉️ ==> 👩‍💻웹 개발자

    • Next routing 사용해서 화면 전환 하면 client-side 화면 이동만 되나요?

    • Exhibition 페이지에서 화면 이동할때 캐치해야하는데 캐치가 안되어서요.

    • 전시(Exhibition) 페이지에서 라우팅시 페이지를 Reload 해줄 수 없나요?

👨‍💻앱 개발자가 이런 부탁을 한 이유

  • 앱 일부 페이지가 웹뷰로 되어있음

    • Before: 그 동안은 Privacy Policy와 같이 사용자 상호작용이 없는 페이지만 웹뷰로 사용했고, Header나 Footer 등은 가려서 사용자가 웹뷰 안에서 페이지 이동할 우려가 없었습니다.

    • After: 그런데 작품들을 전시 형식으로 보여주는 Exhibition 페이지를 앱에서 웹뷰로 보여주게 되었습니다. Exhibition페이지 내부에서는 페이지 이동을 트리거하는 인터랙션 요소가 많아서, 해당 요소를 통해 다른 페이지로 이동하지 않도록 막아야할 필요가 생겼습니다

  • 이 때문에 앱 개발자가 페이지 이동을 감지해 막으려고 한 것이죠.

    • 👨‍💻💭 “View Details 클릭 > 페이지 이동 > 감지해서 막아야지”
  • 그런데 생각대로 되지 않았습니다.

  • 서버사이드 렌더링일때만 페이지 이동 감지 가능한데, Next.js의 라우터는 클라이언트 사이드 렌더링을 하기 때문이죠.

    • 👨‍💻❓”그러면 어떻게 감지하지?”

    • 👨‍💻❗️”Exhibition 페이지에서만 서버사이드 렌더링으로 해달라고 할까?”

    • 👨‍💻✉️ ”Exhibition 페이지에서 라우팅할 때, 페이지를 Reload 해줄 수 없나요?”

문제 해결

👩‍💻웹 개발자: 요구사항 파악 => 문제 해결 방향 잡기

  • 👩‍💻💭 “문제가 되는 시나리오를 모두 생각해보자”

    • Exhibition 페이지에서 문제가 되는 상황, 즉 화면 전환을 캐치 해야 하는 상황은 아래와 같다.

    • 페이지 이동이 일어나는 시나리오1: 작품 클릭

      • 작품을 누르면 작품 상세 모달이 뜨고, 거기서 버튼을 누르면 화면이 이동됩니다.

        • 1. Detail 버튼: 작품 상세 페이지로 이동 -> 웹 페이지 이동

        • 2. Buy 버튼: 구매 페이지로 이동 -> 웹 페이지 이동

    • 페이지 이동이 일어나는 시나리오2: Exit 버튼 클릭

      • Exhibition 페이지 내부의 Exit 버튼 클릭 -> 웹 페이지 뒤로가기 발생

👩‍💻💭 “요구사항을 다시 분석해보자”

  • 나에게 주어진 요구사항: “앱에서 페이지 변경을 감지할 수 있도록 Reload 하기"

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

    • 웹뷰의 페이지 이동을 다른 행동으로 대체하기 위해서 임

      • ex: (본래 동작) 웹 상품 디테일 화면으로 이동 => (대체) 앱의 상품 디테일 화면으로 이동
  • 앱에서 Routing을 핸들링 할 수 있으면 문제가 해결 된다는 사실을 알 수 있습니다.

이제 방향은 잡았으니, 해결 방법을 탐구해봅니다.

괜찮은 해결 방법 찾기

해결책 1: JavaScript Injection

  • 👩‍💻💭 “익숙한 문제가 아니니까 Claude AI에게 일반적인 해결책을 물어보자”

  • 👩‍💻❓ “JavaScript Injection을 사용하라고?”

  • 👩‍💻💭 “알려준 답안을 좀 고치면, 버튼 클릭 동작을 인터셉트 할 수 있겠어“

해결책 2: Query Params

  • 사실 가장 먼저 머리속에 떠오른 방법은 Privacy Policy 페이지에서 header와 footer를 가리는 데 사용했던 Query Params로 fullReload를 컨트롤 하는 방법입니다.

    • Query Params fullReloadtrue인 경우에만 router.push대신 window.location.href를 사용하여 전체 리로드 일어나게 분기하는 방법입니다

const fullReload = queryParams.fullReload === 'true';

if (fullReload) {
  // fullReload가 true인 경우 전체 페이지 리로드 w;
  window.location.href = path; 
} else {
  // 그 외의 경우 router.push 사용
  router.push(path);
};
  • 동작은 하지만 좋은 방법이 아닌 것 같아 사용하지 않았습니다.

    • 좋은 방법이 아닌 이유1: 웹 프론트엔드에서 처리해야하는 쿼리 파라미터가 추가되어 복잡도가 높아짐

    • 좋은 방법이 아닌 이유2: 웹뷰 진입시 해당 쿼리를 달고 들어오면 무조건 페이지 변경 시 Full Reload가 일어나게 됨(Full Reload를 섬세하게 선택 적용하는 것이 불가)

    • 좋은 방법이 아닌 이유3: 웹 프론트엔드에서 이 코드가 왜 작성되었으며 어디에서 쓰이는지 코드만 보고 파악이 어려움.(앱 이슈를 웹 단에서 해결)

    • 좋은 방법이 아닌 이유4: 3과 같은 이유로 웹 개발자가 실수로 분기를 삭제하거나 쿼리 파라미터를 무시하면 쉽게 깨짐

해결방법 상의하기

👩‍💻✉️ “ 👨‍💻님, 말씀하신 대로 Exhibition 페이지에서 리로드하는 방식을 사용해도 동작은 되는데 더 괜찮은 방법이 있는 것 같아서요.”

👩‍💻✉️ “ JavaScript Injection을 사용하면 버튼 클릭했을때의 동작을 앱에서 컨트롤 할 수 있을 것 같아요! 제가 조금 구현해서 올려놨는데 한번 확인해주실 수 있나요?”

(생략: 리로드보다 Javascript Injection이 더 좋은 이유 4가지 설명)

👨‍💻✉️ “👩‍💻님, 코드 확인해보았는데 말씀하신 방법이 더 좋은 방법인 것 같아요! 👍 이 방법으로 가시죠”

👩‍💻✉️ “좋습니다.👍 그런데 앱팀은 다른 태스크로 바쁘신 것 같은데, 웹팀은 지금 한가해서요. 제가 구현까지 해볼까요?”

👨‍💻✉️ “그래주시면 감사하죠!”

구현하기

해결책2(JavaScript Injection)의 구현

  • Claude AI가 일반적인 해결책이라고 알려준 JavaScript Injection으로 구현해보기로 합니다.

  • Claude AI가 알려준 것은 버튼 클릭의 동작을 인터셉트 하는 것입니다.

    • 작품 클릭 > 버튼 클릭 > 페이지 이동

    • exit 버튼 클릭 > 페이지 이동

  • 👩‍💻💭 “그런데 다시 한번 생각해보면”

    • 작품 클릭 > 버튼 클릭 > 페이지 이동

    • exit 버튼 클릭 > 페이지 이동

  • 👩‍💻💭 “버튼 클릭을 인터셉트 하는 것보다, 페이지 이동을 관장하는 Router의 동작을 인터셉트 하는 것이 더 적절할 것 같네”

    • “이것이 가능할까?”

    • 가능여부를 Claude에게 물어보고 아래와 같은 결과물을 도출할 수 있었습니다.

    const injectedJavaScript = `
      (function() {
        const overrideRouterPush = function() {
          // app인지 확인
          window.isNativeApp = true; 

          // router.push 함수를 저장
          const originalPush = window.next.router.push;

          // 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;
            } else {
              // 컨트롤 대상 아닐때 원래 함수 사용
              originalPush.call(this, url, as, options);
            }
          };
        };

        window.onload = overrideRouterPush; // 오버라이드 코드가 window 로드 시점에 실행되도록
      })();
    `;

    // 위 injectedJavaScript함수를 Webview 컴포넌트에 넘겨주기
    // props: injectedJavaScript, injectedJavaScriptBeforeContentLoaded
    <WebView
      {...생략}
      injectedJavaScript={injectedJavaScript}
      injectedJavaScriptBeforeContentLoaded={injectedJavaScript}
    />
  • 그러면 Query Params를 사용하는 방법에서는 불가능했던 선택적용(경우에 따라 기존의 router.push 사용 or 페이지 이동 막음)을 앱에서 컨트롤 가능합니다.

    • Step1: Javascript Injection으로 Next.js의 router.push를 modify(메세지 전송, 위의 코드 참조)

    • Step2: onMessage에서 메시지를 받아서 분기처리(아래 코드)

// WebView 컴포넌트에 넘기는 onMessage props의 예시 
// 여기서 javascript code injection에서 넘긴 메세지를 받아 처리할 수 있다. 
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을 사용하는 방법에서는 앱에서 필요한 요구사항을 앱에서 직접 작성하므로 혼란의 여지가 적음

  • 프론트 개발자와 소통하지 않고 직접 컨트롤 가능

  • 웹 프론트엔드에서의 실수로 앱에 버그가 생길 우려 적음

몰랐던 사실 & 느낀 점

  • 안드로이드는 injectedJavaScriptBeforeContentLoaded를 붙여야 합니다.

    • Claude AI 에게 RN 관련 질문할때는 '안드로이드에서 작동하게 하려면 추가할 것은 없어?'라고 한번 더 물어보는 것이 좋을 것 같습니다.
  • 입사 후 1-2개월간 앱과 웹을 모두 작업했는데, 그 이후로 웹 프론프엔드 쪽만 건드렸기 때문에 오랜만에 앱 작업을 했습니다.

    • 그러다보니 IOS와 안드로이드를 각각 시뮬레이터와 에뮬레이터로 세팅하는 것도 오랜만에 하다보니 시간이 꽤 걸렸습니다.
  • 요구사항이 있다고 해서 그대로 작업하는 것이 아니라 분석을 해보는 자세가 좋은 것 같고, 시간은 더 걸렸지만 더 나은 방법으로 작업한 것 같습니다.