범프 인터랙션으로 인식할 수 있는 데이터의 기준을 정하기 위해 react-native-sensors 라이브러리의 가속도계 데이터를 활용해 테스트를 진행했다.
각 디바이스별/OS별로 해당 라이브러리를 통해 추출하는 가속도계 데이터가 동일하게 나오는지, 특정 기준과 가설을 세워 범프 인터랙션이라고 정의할 데이터상의 기준점을 찾을수 있을지를 테스트해보았다.
- A와 B유저의 디바이스 가속도가 특정 속도 이상일 때
- A와 B유저가 각각 1번 조건에 부합하는 시점의 타임스탬프 차이가 1s 이내일 때
- A와 B유저가 1, 2번 조건을 모두 충족하면서 위치 좌표가 서로 3km 이내일 때
위와 같이 os별 디바이스를 다양한 세기로 범프 행위를 반복해 데이터를 출력했고, 그래프 형식으로 만들어 시각적으로 보기 편하게 만들었다.
하지만 데이터가 예상한것보다 일정하지 않은 모습을 보여줘서, 실시간으로 디바이스를 움직여보며 로컬로 테스트를 진행해보니 아래와 같이 해결해야할 주요 이슈 몇가지를 발견하게 되었다.
사례 1. 가속도계 데이터는 gyroscope의 영향을 받는다.
디바이스를 좌우로 이동시킬 때 뿐만이 아니라 좌우로 회전시키면 자이로스코프의 영향을 받아 가속도계의 X축 값이 증가된다.
이로인해 유저가 탈퇴하기 페이지에 진입 후 무의식적으로 핸드폰을 회전시킬경우 해당 행위가 범프로 인식될 가능성이 있다.
해결 방법 1
accelerometer의 X축 값의 변화를 감지하면서 동시에 gyroscope의 Z축과 Y축 방향의 회전도 어느정도 감지하되 gyroscope의 영향을 최소화하여 범프로 인식되도록 아래와 같은 공식을 범프 기준으로 세운다.
(
accelerometer의 X축으로의 변화
*gyroscope의 Z축으로의 회전의 변화
) /gyroscope의 Y축 방향의 변화
위의 방식으로 공식을 세운 이유는 유저들이 범프 동작을 실행할 때 디바이스를 어느정도 회전시키며 움직일것이라 예상하고 gyroscope의 영향도 어느정도 범프 행위에 포함하려고 하였으나, 위 공식을 활용해 특정 기준점을 찾기 위해 데이터를 미세하게 조정하면서 테스트하는 것이 시간도 많이 소요되고 쉽지 않았다. 따라서 다른 방법을 강구하게 되었다.
해결 방법 2
단순하게 가자!
gyroscope가 가속도계에 영향을 미친다면 gyroscope의 값이 특정 수치 이하여야 한다는 조건을 범프 인식 조건에 추가하면 된다!
그 결과 디바이스별로 범프행위를 여러번 재현했을 때 데이터를 모아서 디바이스를 부딪힐 때 어느정도의 회전은 허용하되 특정 수치 이상은 과한 회전으로 인식하여 범프로 인식되지 않도록 조건을 추가하였다.
1번의 방법으로 테스트를 진행했을 때보다 훨씬 쉽게 gyroscope의 영향을 줄이는데에 빠르게 성공했다.
사례 2. 휴대폰을 y축 방향으로 90도 회전한 상태로 멈춰있을 경우, 중력의 영향을 받아 가속도계 수치가 급격히 상승한다.
gyroscope에 대한 조건을 추가했음에도, 디바이스를 이리저리 돌려보고 테스트했을 때 디바이스가 움직이지 않음에도 y축으로 회전한 상태로 고정시켜두면 계속해서 범프로 인식되는 이슈가 있었다.
1번 사례에서 gyroscope에 대한 영향을 이미 최소화하였고, 추출되는 데이터상 자이로스코프 Y축의 값은 거의 제로에 가까운 상태인데, 가속도계 X축 값이 미친듯이 치솟았다. 도대체 어디서 영향을 받은 것일까? 하고 추측해보니 중력의 영향이 있을것이란 생각이 들었다.
위의 방법대로 적용하여 테스트해보니 더이상 가속도계 X축이 중력의 영향을 받지 않아 디바이스를 회전한상태로 두어도 값이 이상하게 튀지 않게 되었다!
사례 3. 가속도계 x,y,z에 대한 iOS, Android 데이터 값이 다르다.
처음에는 iOS, Android의 가속도계 데이터를 추출하여 둘의 값을 한쪽으로 기준삼을 수 있도록 통일시키는 로직을 추가할까 했지만, 오버엔지니어링이라는 판단이 들었다.
iOS, Android 두 OS에 대한 기준점을 각자 가져가는 것이 아래와 같은 이유로 개발상 더 효율적일것이라는 판단이 들었다.
따라서 결론은, iOS, Android의 가속도계 데이터가 다르므로 기준점을 각자 가져간다.
react-native-permission
라이브러리 사용 중 iOS에서 권한 확인 및 요청 모두 unavailabl 만 리턴되는 이슈pod 'Permission-LocationWhenInUse', :path => "#{permissions_path}/LocationWhenInUse"
추가 # React Native Permission
permissions_path = '../node_modules/react-native-permissions/ios'
pod 'Permission-PhotoLibrary', :path => "#{permissions_path}/PhotoLibrary"
pod 'Permission-PhotoLibraryAddOnly', :path => "#{permissions_path}/PhotoLibraryAddOnly"
pod 'Permission-Camera', :path => "#{permissions_path}/Camera"
// 새로 추가한 권한
pod 'Permission-LocationWhenInUse', :path => "#{permissions_path}/LocationWhenInUse"
static_frameworks = [
'RNPermissions',
'Permission-PhotoLibraryAddOnly',
'Permission-PhotoLibrary',
'Permission-Camera',
'Permission-LocationWhenInUse', // 추가한 코드
'vision-camera-code-scanner',
'VisionCamera',
'RNReanimated',
]
탈퇴하고자 하는 두 유저가 범프 실행 후, 서로 모든 조건을 충족하여 범프로 인식됐을 경우 마지막 장치로 서로 탈퇴를 최종으로 요청하는 버튼을 클릭하는 과정이 있다.
A와 B가 모두 해당 버튼을 클릭하여야만 최종 탈퇴가 완료되고, 두 유저가 모두 최종 탈퇴를 선택해야만 유저의 탈퇴 요청 상태가 done
이 되기 때문에 이를 확인하기 위하여 2초마다 상대 유저의 상태를 확인하는 로직이 있다.
해당 로직은 아래와 같이 구현하였다.
const { data: outTogetherRequestStatus } = useCheckOutTogetherRequestStatus({
enabled: isRegisterOutTogetherSuccess,
refetchInterval: response => {
return response?.data.status === OutTogetherStatusEnum.DONE
? false
: 2000;
},
});
조회한 상대와 나의 탈퇴 요청 상태가 done
이 되는 조건
done
으로 응답한다.위 조건을 충족하지 않을 경우 2초마다 refetchInterval을 실행하고, 상태가 done
이 되었을 경우에만 refetchInterval을 멈추고 다음 스텝으로 넘어간다.
근데 여기서 이슈가 발생한 테스트 케이스는 아래와 같다.
테스트를 위해 유저 A가 해당 스텝에서 최종 요청 버튼을 클릭하고 상대도 클릭하여 done
상태를 응답받은 후 바로 탈퇴하기 페이지를 이탈한다.
기획상 실제 탈퇴 요청이 완료되는 조건은, done
상태로 변한 후 탈퇴 엔딩 크레딧까지 봐야만 탈퇴요청 API가 실행된다. 따라서 A유저는 탈퇴 요청 상태는 done
이 되었으나 엔딩크레딧을 보지 않고 이탈해 탈퇴요청 API가 정상적으로 요청되지 않았다.
이 상태에서 다시 탈퇴하기 실행 후 범프까지 성공하여 최종 탈퇴 선택 버튼이 있는 페이지로 돌아올 경우 버튼을 클릭하지 않았음에도 탈퇴 요청 상태가 done
으로 인식되어 다음 스텝으로 넘어가는 현상이 발생했다.
원인을 찾느라 꽤 오래 코드를 들여다봤는데, 알고보니 너무나도 간단하고 어이없는 실수였다.
너무 바보같은 실수였지만 다시 같은 실수를 반복하지 않기 위해 기록하는 트러블슈팅..
문제는 아래와 같이 cacheTime 설정 추가만으로 해결되었다.
const { data: outTogetherRequestStatus } = useCheckOutTogetherRequestStatus({
enabled: isRegisterOutTogetherSuccess,
refetchInterval: response => {
return response?.data.status === OutTogetherStatusEnum.DONE
? false
: 2000;
},
cacheTime: 0, // 추가된 코드. cacheTime 0으로 설정하여 캐시된 유저 상태 제거
});
이슈 발생 원인은 아래와 같다.
done
으로 캐싱되었다.done
으로 상태를 변화시킨 후 탈퇴하기 페이지를 이탈하여도 해당 유저의 탈퇴 요청 상태에 대한 데이터는 캐싱되어 done
으로 남아있다.done
으로 인식되어 다음 스텝으로 넘어가게된다.위 이슈의 경우 흔하게 일어날 수 있는 경우는 아니고, 만약을 대비한 테스트 케이스 중 하나로 인해 찾게된 이슈이지만 발생했다면 기능상 매우 치명적인 이슈였다.
내가 이 이슈를 발생시키고 빠르게 해결하지 못한 이유 중 하나는, 보통 회사에서 진행했던 작업들은 항상 데이터가 캐싱되어있어도 문제가 없고 오히려 미리 데이터를 보여줄 수 있어 좋은 경우였어서 캐시타임을 깊게 고려하지 않았던 것 같다.
하지만 이번 작업은 유저가 같은 페이지를 진입하더라도 항상 새로운 데이터를 조회하여 참조해야하는 기능이었기에 평소에 놓치고 있었던 cacheTime에 발목을 잡힌 것이다.
문제는 항상 단순하면서 생각지 못한 부분에서 발생할 수 있다. 항상 내가 사용하는 기술의 기본 설정과 기본 기능을 염두에 두면서 작업해야함을 다시 한 번 깨달았다.
익숙해졌다 해도 무지성으로 사용하지 말자!
유저가 범프와 같이 디바이스와 디바이스를 부딪히는 행위를 했을 때,
해당 행위를 인식한 timestamp와 유저의 geolocation 데이터를 서버에 전송하여 두 유저가 같은 장소에서 서로 범프를 시도했음을 판단하는 기능을 작업했다.
예상 일정은 2주였지만...
일부 Native로 구현된 코드와 연동이 필요한 기능이 있었고, 모든 페이지들이 Animation으로 구성되어있는데 RN기반으로 애니메이션을 구현하려니 React로 할 때보다 골치아픈 부분이 많았고, 각 스텝별 에러핸들링도 다양하게 필요했고, QA엔지니어, PM이랑 QA다 돌고는 됐다!!! 스토어 심사 제출합시다! 하고 내부테스트 혹은 테스트플라이트 돌리다가 갑자기 이슈 발생해 심사가 미뤄진 날만 며칠이던지...ㅎ_ㅎ 세상이 우릴 억까한다면서 쓴웃음짓는 나날 끝에 마무리된 에픽이다.
약 3주간은 새벽까지 이어지는 작업에 눈뜨면 출근하고 머리가 더이상은 안돌아가겠다 싶을 때쯤 퇴근하는 일상이 이어졌지만, 힘들기보다는 함께 고생하는 QA엔지니어와 PM에게 미안한 마음이 크기도 했고, 그래도 시간을 투자한 만큼 불가능할 것 같았던 것들이 조금씩은 작동하고 완성되어가는 상황을 보면서 스스로를 위로하고 채찍질도 해가면서 끝끝내 완성할 수 있었던 외롭고 힘겨운 작업이었다! (그와중에 주말은 등산으로 힐링..)
마지막 1주일 동안은 끝날 듯 끝나지 않는 상황이 반복되자 새벽 퇴근길에 스스로 자책도 하고 자존감도 많이 떨어졌던 것 같다. 트러블슈팅도 일일이 기록하고 이슈 발생 가능한 지점도 미리 체크해서 QA까지 진행했는데, 그럼에도 불구하고 여기 저기서 하나씩 튀어나오는 예상외의 이슈들에 "나는 도대체 왜..."라는 생각에 많은 감정이 교차했던 것 같다. 하필 내가 단독으로 맡은 에픽에서 이렇게 지연이 발생하니까 마음은 급해지고 패배자가 된 것 같은 그런 기분도 잠깐 들었었다...ㅠㅠ
하지만 옆에서 응원해주고 걱정해주고, 잠을 못자 피곤해서 효율이 안나는 것이라며 나름 격려(?)ㅋㅋㅋ 해주는 팀원들 덕분에 웃으면서 힘내서 마무리할 수 있었던 것 같다. 정말 동료들에게 얻는 긍정적 기운과 에너지를 다시 한 번 체감할 수 있었다.
또 다른 문제는, Native 코드를 건드려야해서 많이 부담스러웠는데, GPT한테 부탁도 해보고 화도 내보고 하면서 "이게 되네?" 싶은 시간들을 지나다보니 '어떻게든 되긴 된다.'라는 개발에 대한 나의 신념(?)을 증명해내는 에픽이기도 했다.
엔딩크레딧 완료 후 세션 만료 + 로그아웃 실행 후 인트로 화면으로 보내기 기능이 Native코드를 건드려야했던 지점인데, 이게 뭐라고 세션 만료 + 로그아웃만 하루를 보내고 또 인트로 화면으로 보내기도 반나절이 걸렸다.. 네이티브 코드 정말 이해가 안되고 어려웠다..
게다가 ios랑 android 중 하나 끝내서 "오예!!!!!!!"를 외치고 정신차리고나면, 아직 또 하나의 산이 더 남았다는 현실을 직면했을 때의 그 허탈함이란..ㅋㅋㅋㅋ이래서 React Native로 한번에 개발하려고들 하는가보다를 아주 깊게 실감하기도 했다.
그치만 언어도 표현도 그리고 코드 위치 찾는것도 너무 생소하고 어려웠지만, 구현하고자 하는 기능의 키워드 하나 검색하기로 시작해서 관련된 함수를 찾고, 해당 함수가 임포트된 부분을 찾고, 그 긴 내용중에서 변수명이나 이벤트명으로 관련되어보이는 로직을 찾아서 차근차근 진행해보니 안될 것 같았던 것들도 조금씩 되기 시작해서 너무 신기했다..
아무튼, 나의 짧은 프론트엔드 개발 인생에서 가장 큰 고비라는 생각이 들었던 에픽..
지난 1월을 모두 이 에픽에 잠식당해버려 다른 기억은 없지만 그만큼 배운것도 많고 성장할 수 있는 시간이 되었던 것 같다.
그리고 정말, 안되란 법은 없다!! 어떻게든 되긴 된다.