[트러블슈팅/회고] RN + Webview 구조로 이루어진 모바일 프로젝트에 서비스 점검 팝업 추가하기

김하연·2023년 11월 1일
0

우당탕탕

목록 보기
2/11

React Native Webview 기반 구조에 네트워크 응답 확인하여 503 에러 발생 시 서비스 점검 팝업 노출시키기

이번에 회사 특정 서비스에 사용되는 재화의 단위가 변경되면서 앱 내 재화단위의 변경과 함께 서버쪽에서는 DB 마이그레이션 작업이 필요하게 되었다.
DB 마이그레이션 시간은 유저들의 사용량이 가장 적은 새벽 시간으로 결정되었고, 아무리 유저의 사용량이 적어도 해당 시간동안 서버가 작동하지 않아 앱 또한 정상적으로 작동할 수 없기에 서비스 점검 안내 팝업을 띄워야했다.

작업 계획

  1. API Client에 Axios 인터셉터를 추가하여 네트워크 요청의 실패 여부를 확인하고, 해당 애러의 status 코드가 503인지 확인한다.
  2. 503일 경우 서비스 점검중임을 알리는 팝업을 띄운다.

위 틀을 토대로 구체적인 작업 계획을 세우던 중 깨달은 중요한 사실은,

  • API 클라이언트는 Webview 에 위치해있다.
  • 유저의 모든 인터랙션을 막으려면 RN부에 구현되어있는 바텀 네비게이션까지 가릴 수 있도록 서비스 점검 안내 팝업은 RN에 구현되어야 한다.

따라서 최종적으로 결정내린 작업 방식은 아래와 같다.

  1. API Client에 Axios 인터셉터를 추가하여 네트워크 요청의 실패 여부를 확인하고, 해당 애러의 status 코드가 503인지 확인한다.
  2. 503일 경우 브릿지를 활용해 RN부에 메세지를 전송해 서비스 점검 팝업을 띄워야함을 알린다.
  3. 해당 메세지를 요청받은 RN영역에서 서비스 점검 팝업을 띄운다.

기능 구현

1. axios 인스턴스에 추가할 인터셉터 코드 작성하기

// Add a request interceptor
axios.interceptors.request.use(function (config) {
  // Do something before request is sent
  return config;
}, function (error) {
  // Do something with request error
  return Promise.reject(error);
});

// Add a response interceptor
axios.interceptors.response.use(function (response) {
  // Any status code that lie within the range of 2xx cause this function to trigger
  // Do something with response data
  return response;
}, function (error) {
  // Any status codes that falls outside the range of 2xx cause this function to trigger
  // Do something with response error
  return Promise.reject(error);
});

위와 같이 axios 공식문서에 인터셉터를 추가하는 방법이 아주 친절하게 설명되어있다.

위 코드는 기본 기능을 파악하기위해 작업 전 참고하였고, 다행이게도 우리 프로젝트의 apiClient 클래스 내부에 이미 센트리에 에러 보고를 하기 위한 인터셉터가 위 공식사이트 코드와 비슷한 형태로 추가되어있어서 해당 코드를 참고하여 아래와 같이 interceptor 코드를 작성하였다.

addNetworkResponseInterceptor(
  onFulfilled?: (response: AxiosResponse) => AxiosResponse,
  onRejected?: (error: AxiosError) => void,
) {
  this.instance.interceptors.response.use(onFulfilled, onRejected);
}

api client 클래스 내부에 선언된 메서드로
다른 컴포넌트에서 아래와 같이 fulfilled일 시, Rejected일 시에 대한 이벤트를 자유롭게 지정할 수 있다.

apiClient.addNetworkResponseInterceptor(
  () => {...// 성공 시 실행할 이벤트},
  () => {...// 실패 시 실행할 이벤트}
)

2. _app.txs 파일에 전역으로 인터셉터 추가하기

프로젝트의 _app.txs 파일 내부에 NetworkResponseObserver 라는 컴포넌트를 추가하였다.

export const NetworkResponseObserver = ({ children }: PropsWithChildren) => {
  const isPopupOpened = useRef(false);
  const { networkErrorCapturer } = useBridge();

  useEffect(() => {
    apiClient.addNetworkResponseInterceptor(
      response => {
        // 네트워크 에러 없을 경우, 팝업이 이미 오픈상태일 경우에 대한 핸들링
        if (isPopupOpened.current) {
          // RN부에 다시 네트워크 에러 여부 체크하도록 요청 전송
          networkErrorCapturer?.checkNetworkError();
          isPopupOpened.current = false;
        }
        return response;
      },
      (error: any) => {
        // 실패하여 Network Error로 응답올 경우
        if (error?.message === 'Network Error') {
          // 아직 팝업이 노출된 상태가 아니라면 RN부에 네트워크 에러 여부 체크하도록 요청 전송
          if(!isPopupOpened.current) {
        	networkErrorCapturer?.checkNetworkError();
            isPopupOpened.current = true;
          }
        }
        throw error;
      },
    );
  }, []);

  return <>{children}</>;
};

그리고 해당 파일에는 위와 같이 이전에 작성한 인터셉터 이벤트를 호출하여 onFulfilled, onRejected 호출 시에 실행되어야 하는 이벤트 내용을 작성하였다.

위의 Webview 코드에서 networkErrorCapturer.checkNetworkError(); 이벤트를 호출하면 RN에서 실행되는 코드는 아래와 같다.

export const useUnderMaintenancePopup = () => {

  const {
    data: healthCheckResponse,
    refetch,
    isError,
  } = useQuery(
    ['HEALTH_CHECK'],
    () => axios.get('/api/enfpy/health-check'),
    {
      enabled: false,
    },
  );

  useEffect(() => {
    if (isError) {
      if (healthCheckResponse?.response?.data?.status_code === 503) {
        const description = healthCheckResponse?.response?.data?.content?.message;

        popup.open({
          title: '시스템 점검 중입니다',
          description,
          BodyComponent: <MaintenancePopupIcon />,
        });
      }
    }
  }, [healthCheckResponse?.response?.data?.status_code, isError, popup]);
  
  useEffect(() => {
    if (isSuccess) {
    	popup?.close();
    }
  }, [isSuccess, popup]);

  return { refetch };
};

[트러블슈팅] onFulfilled, onRejected에서 RN에 네트워크 에러 여부 확인 요청을 전송하는 이유

networkErrorCapturer?.checkNetworkError(); 코드는 RN에 메세지를 전송해 checkNetworkError 메서드를 실행시키고, 해당 메서드 실행을 통해 네트워크 상태를 다시 한 번 체크하는 로직이다.
이 로직이 추가된 이유는

  • 503 에러에 대한 응답을 Webview에서 확인할 경우 Error의 status값이 null로, 이벤트 메세지는 Network Error 로 응답오는 것을 확인했다.
  • 다른 타입의 네트워크 에러에 대해서는 얻고자 하는 정보와 백엔드 개발자가 설정한 응답 내역이 모두 노출되었지만, 503에러에 대해서만 위와 같이 응답이 와서 백엔드 개발자와 소통하며 원인을 찾으려 했지만 찾지 못했다.
  • RN에서 같은 API에 대해 테스트를 진행했을 때에는 503에러일 경우 status code가 503으로 Webview와 다르게 정상적으로 노출되었다.
  • 위 원인을 찾기 위해 하루 절반의 시간을 소모했으나, 찾지 못하고 작업이 지연되는 상황과 원인을 PM과 사수에게 상황을 보고했다.
    대체방법으로 웹뷰에서 503에러일 경우에만 이벤트 메세지를 Network Error으로 응답받으므로 웹뷰에서는 해당 에러가 503이라 추측하되, RN에서 바로 팝업을 오픈하기 전 다시 한 번 해당 에러가 503 에러인지 확인하는 절차를 추가하였다.

따라서 axios interceptor에서는 error.message === 'Network Error'일 경우를 503에러로 간주하고,
해당 에러가 status가 혹시라도 503이 아닐 수 있으므로 다시 한 번 확인하기 위해 웹뷰에서 networkErrorCapturer?.checkNetworkError(); 이벤트를 브릿지를 호출해 RN에서 이벤트를 전달받아 health-check를 위한 API 요청을 실행한다.

그리고 위 health-check API의 응답의 status_code가 최종 503일 경우 RN에 구현되어있는 팝업을 노출시키는 방식이었다.

그 외 고려한 사항들

  • 점검 중 팝업이 이미 노출되어 있는 상태에서는 어떠한 인터랙션도 불가능하지만, 어떠한 변수가 발생해 특정 API가 호출되어 팝업이 중복으로 쌓이는 상황을 방지하기 위해 isPopupOpened 라는 Ref를 생성해 값이 false일 경우에만 networkErrorCapturer 핸들러에 이벤트를 전송하도록 조건 추가
  • 팝업이 노출된 상태로 앱이 백그라운드로 전환됐다가 점검이 끝난 후 포그라운드로 전환되면, API 요청에 성공했을 때 팝업을 숨겨야 하는데 모든 API 성공시마다 이벤트를 호출하면 너무 많은 이벤트가 전송되므로 onFulfilled 호출 시 isPopupOpened.current값이 true일 때만, 즉 점검 중 팝업이 띄워져 있는 상황일때만 networkErrorCapturer.checkNetworkError() 메서드가 실행되도록 하고 해당 이벤트 실행 후 isPopupOpened.current 값을 false로 바꿔 중복 이벤트 실행을 방지하였다.
  • 팝업에 노출되는 시스템 점검 안내 문구와 점검 시간은 점검시마다 달라질 것이므로 이 값을 프론트에서 제어하면 최신 버전 뿐만이 아니라 하위 버전 코드에 코드푸시를 진행해야하는 번거로움이 예상됐다. 이에 대해 백엔드 개발자와 논의 후 팝업에 노출되는 안내 문구와 시간 또한 서버에서 에러 응답에 message로 값을 함께 전달해주기로 하였다.

최종 구현 내용

Webview 구현사항

  • Api Client에 인터셉터를 추가한다.
  • 인터셉터에 fulfilled, rejected 시 실행할 이벤트를 추가한다.
    • fulfilled일 경우
      • 서비스 점검 팝업이 노출되어 있는 경우
        RN에 '네트워크 에러 체크' 메세지를 전송해 네트워크 에러가 발생하지 않고 정상적인지 확인, 정상일 경우 서비스 점검 팝업을 닫기 처리한다.
      • 서비스 점검 팝업이 노출되어 있지 않은 경우
        처리할 것 없음
    • rejected일 경우 && 에러 메세지가 `'Network Error'일 경우
      • 서비스 점검 팝업이 노출되어 있는 경우
        처리할 것 없음
      • 서비스 점검 팝업이 노출되어 있는 않은 경우
        RN에 '네트워크 에러 체크' 메세지를 전송해 네트워크 에러가 발생하는지, 발생한다면 해당 에러의 status_code가 503인지 확인한다. 503이 맞다면 서비스 점검 팝업을 노출한다.

React Native 구현사항

  • Webview에서 '네트워크 에러 체크' 요청을 보낼 경우 health-check API 요청을 보내 네트워크 상태를 확인한다.
  • health-check API가 실패했고, status_code가 503일 경우 팝업을 노출시킨다.
    이 때, 팝업의 안내 문구와 점검 시간은 error 응답에 함께 포함되어있으므로 해당 값을 참조하여 ui에 뿌려주기만 한다.
  • health-check API 요청이 에러 없이 성공할 경우 popup을 닫는다.

배포 직후 작성한 회고 & 소감

이렇게 서비스의 서버를 내리고 그 시간 내에 진행되는! 온타임이 매우 중요한 이런 작업에 참여한 것은 처음인데다 혼자서 프론트 작업을 모두 맡아 책임지고 진행해야 하다보니 긴장이 많이 됐다.
막상 실제 시스템 점검 시작하는 시점에 팝업이 정상적으로 뜨지 않으면 어떡하지..?! 라는 막연한 불안감이 있었고.. 여러번 테스트 환경에서 테스트를 진행한 끝에 준비를 마쳤지만 그래도 혹시모르는 이슈가 튀어나올까봐 괜히 걱정됐다.
그래서.. 약속된 작업시간은 새벽 4시였는데, 새벽 2시에 기상하여 웹배포와 코드푸시에 반영되는 코드들이 문제 없이 잘 처리되어있나 확인하는 시간을 가졌다. 사실 테스트는 이미 마쳤기에 코드들을 하나씩 훑어보고 로컬로 돌려보면서 확인했는데 정확도를 높이기보단 해당 행위들을 통해 거의 기도하는 시간에 가까웠다고 볼 수 있을 것 같다. 🥺
새벽 4시, 작업이 시작되었고 백엔드 개발자가 503에러를 보내기 시작하자 아주 다행이게도 팝업이 정상적으로 잘 작동을 하였다...😭
"이제 503 응답 갈거에요 확인해보세요~" 라는 말을 듣고 앱을 켜서 팝업이 보이기 직전의 순간까지 긴장돼서 손땀이 어찌나 많이 나던지... 그제서야 안도의 한숨을 쉬고 미리 적어둔 작업 순서를 하나씩 실행해나갔다.
구현 작업 자체가 어려운 것은 아니었지만, 서비스를 중단시키고 혼자서 문제없이 작업을 진행시켜야 한다는 상황이 압박이 컸던 것 같다. 덕분에 실수하지 않기 위해 이슈가 발생할 수 있는 케이스를 작성하고 그것을 토대로 여러번의 테스트를 거친 보람이 있었다.
테스트 케이스, 발생할 수 있는 이슈 시나리오, 배포 시나리오 적는게 심신의 안정을 가져다줄 뿐만 아니라 작업의 정확도를 높이는 데에도 아주 아주 중요함을 다시 한 번 깨달았다.

아, 그리고 이 작업 중 가장 골치가 아팠던 것은 바로 하위버전에 코드푸시 코드 반영하기...!!!!!!! 😱😱😱
서비스의 가장 최근 버전이 1.3.0 이라면, 1.3.0 바이너리가 배포된 지 며칠 되지 않아 1.3.0버전의 유저 점유율은 약 7퍼센트 정도밖에 안되었고 1.2.0 버전의 유저 점유율이 약 90퍼센트 이상이었다. 그리고 최하위 버전인 1.1.0버전은 대략 3퍼센트 정도...?
때문에 하위 버전에도 시스템 점검 팝업을 띄우기 위해 총 3가지 버전에 코드푸시를 진행해야 했던 상황...!

우선 1.3.0 버전 기반으로 코드를 작성한 후,
1.2.0과 1.1.0 버전이 배포된 시점을 기준으로 베이스 브랜치를 따서 1.3.0버전에 적용된 내용들을 체리픽으로 가져오는 식으로 작업을 하였다.

다행이게도 유저 점유율이 가장 많은 1.2.0은 별 탈 없이 코드푸시가 롤백되지 않고 잘 반영되었는데, 1.1.0이 계속해서 코드푸시 업데이트가 롤백되는 바람에 시간을 많이 할애했다.
처음에 1.1.0버전의 마지막 코드푸시 버전인 1.1.6에서 베이스를 따서 작업을 진행했더니 계속해서 롤백 됐는데, 1.1.0에서부터 베이스를 따볼까? 하고 진행해보니 천만다행이게도 1.1.0 버전도 코드푸시 업데이트가 잘 반영되었다.

코드푸시..
최신 버전만 최신 버전에 배포하는 것이 최선이라 하고, 그렇게 기획하려고 PM분들도 노력하고 있지만 워낙 우리 서비스가 배포가 잦기도 하고 그만큼 버전 변경도 많고, 또 이슈 발생 시 하위버전까지 해소가 필요한 내용이라면 하위버전 코드푸시는 절대 피할 수 없는 일 중 하나이다.
명확한 해결 방법은 아니지만, 그래도 이후에 언제 있을지 모를 하위버전 코드푸시 작업을 위해 배포 시 릴리즈 노트 작성하기, 하위버전의 최신 브랜치가 다른 버전의 코드와 섞이지 않도록 잘 유지하기(RN 변경사하만 반영하기)를 절대 놓치지 않아야겠다.

어쨌든 이번 에픽 다방면으로 우당탕탕 시행착오가 많았는데, 결국에는 모든 기능 구현과 코드푸시 업데이트가 잘 반영이 되어서 속시원하다.
변경된 재화 단위로 더 다양하고 재밌는 유저 인터랙션 상품이 만들어지면 더더욱 뿌듯할 것 같다!


이번 에픽으로 얻은 부분 & 개선사항

  • 빠른 상황 보고와 빠른 방향 전환의 중요성
    웹뷰에서 503에러 응답이 잘 잡히지 않을 때, 잡힐 듯 잡히지 않아 시간을 많이 할애할 뻔 했는데 어느정도 도전했다 라는 생각이 들었을 때 '조금만 더 해볼까?' 라는 유혹을 떨쳐내고 PM과 사수에게 상황을 보고하고 차선책을 제안해서 작업 방향을 전환한 것은 좋은 선택이었던 것 같다.
    늘 작업하다보면 될 듯 안되는 상황에서 밑도 끝도 없이 파고들게되는 상황이 항상 생기려고 하는데, 이럴때마다 스스로를 환기하고 '내가 하고자 하는 것이 무엇인가?', '이게 가장 중요한것이 맞는가?', '다른 방법은 없는가?' 등을 항상 떠올리려는 습관은 계속해서 가져가야 할 중요한 액션이다.

  • 릴리즈 노트 꾸준히 작성하기 & 작업 완료 후 프로덕션 환경과 테스트 환경 일치시켜두기 (*프론트 클래스 논의 사항)
    이전에 테스트 환경에 배포되었던 바이너리와 코드푸시 버전이 실제 프로덕션 환경과 다르게 설정되어있어 테스트 하는데에 애를 먹었다. 이번 프로젝트부터는 내가 먼저 신경써서 배포 완료된 작업에 대해서는 다시 한 번 테스트 환경에 싱크를 맞추는 작업을 꼭 진행해야겠다 라는 생각이 들었다. 그리고 릴리즈 노트를 만들어서 귀찮더라도 히스토리 파악이 가능하도록 해야할 것 같다.
    - 최종 배포 후 프로덕션 환경, 테스트 환경 싱크 맞추기
    - 릴리즈 노트 작성하기 (공유 문서 생성)

  • 테스트 시나리오 / 이슈 발생 시 치명적인 시나리오 / 배포 시나리오 / 롤백 시나리오 항상 작성하기
    이슈는 언제나 예상치 못한 곳에서 터지기 마련이지만, 내가 예상할 수 있는 이슈는 최대한 예방해야하고 그래야 내가 배포할 때 마음이 덜 불안하다. 그러므로 테스트, 이슈, 배포, 롤백 시나리오를 항상 작성하자. 이게 있으면 무슨 일이 발생해도 따라갈 수 있는 모범답안을 가진 기분이 들어 든든하다.

0개의 댓글