본문 바로가기

TIL

[TIL-168] React Native : 안드로이드 BackHandler 앱 종료, Promise로 fetch 동기 처리, 세션

BackHandler

뒤로가기 버튼 -> 앱 종료

BackHandler는 RN의 API로, 안드로이드에 있는 네비게이션바의 뒤로가기 버튼을 눌렀을 때를 감지하여 특정 동작을 실행하게 해준다. 이벤트 구독은 역순, 즉 최신순으로 이뤄진다. 어떤 구독이 true를 반환하면 그보다 이전에 등록된 구독은 호출되지 않는다. 아무 구독도 true를 반환하지 않거나 등록된 구독이 없으면, 자동으로 기본 백 버튼 기능을 실행한다.(뒤로 갈 화면이 없으면 어플이 종료됨.)

※ 모달이 열려있으면 아무 이벤트도 인식하지 않음.

 

// Home.js

  const exit = () => {
    BackHandler.exitApp();
  };

  useEffect(() => {
    BackHandler.addEventListener('hardwareBackPress', exit);
    return () => BackHandler.removeEventListener('hardwareBackPress', exit);
  }, []);

 

웹에서 document처럼 addEventListener를 이용하여 (back button press) 이벤트를 감지할 수 있고, 이때 실행할 콜백함수를 두번째 인자로 담는다. 특징은 useEffect() 안에 코드를 작성하는 것. 그리고 removeEventListener 코드를 반환하면서 마치는 것. return 부분이 꼭 필요한가 해서 안 써봤는데 그럼 작동 안 됨.

 

 

문제 1. 다른 화면에서도 동작함

그런데 내 코드에서 잘 작동하지 않았다. 뒤로가기를 누르면 앱이 꺼지기는 하는데 Home 말고 다른 화면에서도 뒤로가기하면 앱이 꺼졌다.

 

  const exit = () => {
    BackHandler.exitApp();
  };

  useFocusEffect(
    useCallback(() => {
      BackHandler.addEventListener('hardwareBackPress', exit);
      return () => BackHandler.removeEventListener('hardwareBackPress', exit);
    }, []),
  );

같은 문제를 호소하는 질문글의 답변에 따라 코드를 고쳤더니 해결됐다. useFocusEffect()를 사용하면 해당 screen이 포커스되었을 때 실행되는 것 같다. 그리고 useFocusEffect()를 사용할 때는 반드시 useCallback()으로 콜백함수를 감싸야한다고 한다.

 

 

문제 2. 기본 동작(뒤로 가기)도 여전히 실행됨

백버튼을 눌렀을 때 앱이 꺼지기는 하는데, 꺼지기 전에 뒤로가기도 실행돼서 이전 화면이 보이면서 지저분하게 앱이 종료된다. 아무리 코드를 봐도 왜인지 모르겠어서 한참을 찾아봤다. BackHandler에 "hardwareBackPress" 이벤트를 붙여놓고, 이 이벤트(press back button)가 일어났을 때 실행될 함수(exit)를 전달해줬으니 이 함수 안의 동작만 실행될 것이라 생각했다. 그런데 기본 동작도 여전히 실행되는 것이다.

다른 코드들을 찾아보며 뭐가 다른지 비교해봤다. 스택오버플로우의 한 질문을 보니, back button을 눌렀을 때 뒤로가는 동작을 막은 코드로 올라온 것이 다음과 같다.

  useEffect(() => {
    BackHandler.addEventListener('hardwareBackPress', () => true);
    return () =>
      BackHandler.removeEventListener('hardwareBackPress', () => true);
  }, []);

true를 return하고 있었다. back button이 눌렸을 때 실행되는 로직을 잘 몰라서 이해는 되지 않지만 true를 반환해야 기본적으로 약속된 동작이 실행되지 않고 back button press에 대한 이벤트 처리가 완료되는 모양이다. 이 코드를 보고 나니, 다른 코드도 눈에 들어왔다.

let currentCount = 0;
export const useDoubleBackPressExit = (
  exitHandler: () => void
) => {
  if (Platform.OS === "ios") return;
  const subscription = BackHandler.addEventListener("hardwareBackPress", () => {
    if (currentCount === 1) {
      exitHandler();
      subscription.remove();
      return true;
    }
    backPressHandler();
    return true;			// <== 여기
  });
};

const backPressHandler = () => {
  if (currentCount < 1) {
    currentCount += 1;
    WToast.show({
      data: "Press again to close!",
      duration: WToast.duration.SHORT,
    });
  }
  setTimeout(() => {
    currentCount = 0;
  }, 2000);
};

back button 더블 press를 감지하여 앱을 종료하게 하는 이 코드에서도 원하는 동작인 backPressHandler()를 실행한 후 true를 return하고 있다.

 

 

두번 눌렀을 때 종료시키기

한 번만에 종료시키면 실수로 앱을 종료하는 경우가 있을 것 같아서, back button을 두번 눌렀을 때 앱이 종료되도록 해보았다. 한 번 눌렀을 때 알림창을 띄워서 앱을 종료할 건지 묻고 사용자가 누르는 버튼에 따라 앱을 종료하는 방법도 있지만 불편해보이기도 하고 옛날 어플에서 주로 쓰던 방식 같다. 그래서 홈 화면에서 버튼이 한 번 눌렸을 때 토스트로 메시지를 띄우고, 일정 시간 안에 두 번째로 버튼이 눌리면 앱이 종료되도록 했다.

참고한 코드는 위에 첨부한, return true를 확인한 코드다.

  let backPressCount = 0;

  const exit = () => {
    if (backPressCount < 1) {
      Toast.show('앱을 종료하려면 한 번 더 눌러주세요!', 2000);
      backPressCount++;
    } else {
      BackHandler.exitApp();
    }

    setTimeout(() => {
      backPressCount = 0;
    }, 2000);

    return true;
  };

  useFocusEffect(
    useCallback(() => {
      BackHandler.addEventListener('hardwareBackPress', exit);
      return () => BackHandler.removeEventListener('hardwareBackPress', exit);
    }, []),
  );

백 버튼이 몇 번째 눌렸는지 세는 backPressCount 변수를 선언하고, 0을 초기값으로 준다. 그리고 백 버튼이 눌렸을 때 이미 눌린 횟수가 1 미만(0)이라면 토스트를 띄우고 backPressCount를 1 증가시킨다. 그런데 백 버튼이 연달아 두번, 즉 빠르게 연속해서 눌렸을 때만 앱을 종료시키기 위해 setTimeout()을 이용해서 백 버튼이 눌린지 2초가 지나면 backPressCount를 0으로 다시 초기화시킨다.

 

 

 

비동기 fetch를 aync await Promise로 동기 처리하기

메모를 서버에 보내 저장한 뒤, 메모가 포함된 상세 내역 불러오기

  const saveMemo = () => {
    fetch(API.memo, {
      method: 'POST',
      headers: {
        Authorization: `Bearer ${token.access}`,
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        send: sendId,
        memo,
      }),
    })
      .then(res => res.json())
      .then(result => {
        console.log(result);
      });
  };

  const goHome = () => {
    saveMemo();
    navigation.navigate('Home');
  };

  const goDetail = async () => {
    await Promise.all([saveMemo()]);
    navigation.navigate('Detail', {sendId});
  };

코드가 길어지기도 하고, 메모를 서버로 보내 데이터베이스에 저장하는 작업이 세 가지 버튼이 눌릴 때마다(goHome, goDetail 등) 각각 실행되어야 하기 때문에 saveMemo라는 함수로 분리했다. 그런데 saveMemo()를 실행시키고 이어지길 원하는 동작을 이어서 작성했더니 saveMemo() 안의 fetch가 끝나기 전에 먼저 실행되어서, 이동한 화면에서 메모가 저장되지 않은 채 상세내역을 불러오는 등의 문제가 생겼다.

그래서 aync await를 이용해서 saveMemo의 수행 상태를 Promise 객체로 확인하여(?) 완료할 때까지 기다리게 했다. 하지만 여전히 메모가 저장되기 전에 다음 화면으로 navigate되었다.

 

  const saveMemo = async () => {
    await fetch(API.memo, {
      method: 'POST',
      headers: {
        Authorization: `Bearer ${token.access}`,
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        send: sendId,
        memo,
      }),
    })
      .then(res => res.json())
      .then(result => {
        console.log(result);
      });
  };

...

  const goDetail = async () => {
    await Promise.all([saveMemo()]);
    navigation.navigate('Detail', {sendId});
  };

saveMemo 함수 자체에서 fetch를 await하도록 처리해주었더니 해결되었다!! 이전에도 겪었던 문제와 비슷한 것 같았는데, saveMemo 안의 fetch는 특성상 비동기적으로 처리되지만 saveMemo 함수 자체는 일반 함수이기 때문에 동기적으로 처리된다는 것이다. 그래서 saveMemo를 await하든(예전에 비슷한 문제를 겪었을 때 시도해본 방법 -> 물론 실패), Promise로 받든, 그래봤자 달라질 게 없는 것이다.

 

 

 

Session

구현 목표

송금하기 전에 사용자에게서 한번 더 비밀번호를 입력받아 확인하는 절차를 넣었다. 이때, 세션을 이용해서 비밀번호 2차 확인을 통과한 사용자인지 확인하도록 했다.

 

개념

그런데 세션을 제대로 이해하지 못해서 혼란이 있었다. 세션이 서버에서 만들어진 것까진 알겠는데, 어떻게 클라이언트로 넘어가며, 프론트의 아무 조치 없이도 매 요청때마다 정보가 실려온다는 것인가. 토큰처럼 프론트와 백이 request와 response에 직접 담아서 주고 받는 것만 해보아서 세션이 서버와 클라이언트를 왔다갔다 하는 것이 이해되지 않았다. 하지만 너무나 기초적이고 당연한 것이라 그런지 검색해봐도 원하는 답변이 잘 나오지 않았다.

프론트는 "아무것도 하지 않아도 된다"는 팀장님의 이야기를 듣고, 정답만 안 상태로 그 과정을 찾아해맸다. 예전에 노마드코더의 유튜브 클론코딩 강의를 들을 때도 세션을 이용하여 로그인을 구현했는데, 그때 쓴 nodeJS 코드를 다시 살펴보았지만 프론트가 처리한 일은 없었다. 서버 쪽에서 세션을 생성하고 저장해주는 미들웨어를 설치한 후, 세션을 데이터베이스(몽고db)에 저장하는 세팅을 하고, 세션의 형태나 포함할 정보(사용자가 로그인에 성공하면 isLogin이라는 키의 값을 true로 한다든지)를 정해주기만 했다.

결국 구글링하면서 이해한 바로는, 세션을 생성해서 보내면 http 통신규약에 따라 요청과 응답을 주고받을 때 자동으로 세션이 담기는 것 같다. 1) 사용자가 보낸 요청을 처리할 때 서버에서 세션을 만들어 세션 DB에 저장하고 이 세션의 ID를 응답에 담아 보내면서 브라우저의 쿠키에 저장한다. 2) 해당 사이트에서 요청을 보낼 때마다 브라우저는 항상 쿠키에 저장된 세션을 담아서 보낸다. 3) 서버는 받은 요청에서 세션을 꺼내어 확인할 수 있다. 4) 이때 해당 세션에 원하는 값(value)을 넣거나 변경하여 저장할 수도 있다.

그런데 한가지 더 이해되지 않는 점은 브라우저와 달리 앱은 쿠키가 없는데 똑같이 적용할 수 있는가이다.

 

참고

[4/29] 세션 추가 공부 https://good-developer.tistory.com/175

 


공부할 것

RN 개념 : https://velog.io/@pks787/TIL-18%EC%9D%BC%EC%B0%A8-React-Native

'TIL' 카테고리의 다른 글

[TIL-170] React Native  (0) 2022.04.20
[TIL-169] React Native  (0) 2022.04.19
[TIL-167] React Native  (0) 2022.04.15
[TIL-166] React Native  (0) 2022.04.14
[TIL-165] React Native  (0) 2022.04.13