이제는 지금은 동작하지 않는 애플 로그인 버튼을 누르면 창이 켜지도록 만들어주면 되겠다. 우선 사용하려는 라이브러리의 Mac 설정으로 진행해봤다. 애플 로그인 버튼을 누르면 다음과 같은 에러 로그가 찍힌다.
GPT에게 물어봤더니 앱의 개발자 계정 설정이나 Xcode의 프로젝트 설정 문제 가능성이 있다고 하더라. 우선은 Xcode의 프로젝트 설정에 들어가서, 'apple sign in' 옵션을 추가해주었다. 그리고 Team의 값이 None으로 되어 있었는데, 이 값도 이름으로 바꿔 주었다.
그리고 다시 앱을 실행시켰더니 애플 로그인 버튼을 누르면 동작하지 않고 관련 경고 로그가 떴다.
스택을 보니 아까와 같은 로그가 그대로 찍히고 있었다. 그리고 아래에 보니 이런 로그도 함께 있었다. 무엇부터 해결해야 할지 감이 안 잡혔다. 우선 이것부터 해결해 보기로 했다.
해당 에러는 'expo-splash-screen'이라는 패키지가 ios 앱에서 스플래시 화면을 찾지 못해서 발생하는 에러라고 한다. 이를 위해서는 우선 Xcode를 열고, 파일 설정에서 Storyboard 파일을 찾아서 파일 이름을 SplashScreen.storyboard로 지정하고 프로젝트의 ios 디렉토리에 저장해 주어야 한다고 한다. 일단 해보자.
그저께 발생한 부분에서 아직 막혀있다. package.json 파일의 scripts 변수 안의 값들 중 ios 에뮬레이터에서 앱을 실행시키는 명령어인 'npm run ios'를 입력했다. (정확히는 이렇게 입력하면 그대로 실행되는 게 아니라, 해당 스크립트 파일에 key-value로 명시된 실제 값인 'npx expo run:ios' 명령어가 실행된다.
여러 로그가 쭉 나오더니 에러 로그가 나오면서 또 실행에 실패했다.
단순히 해당 파일을 만들어야 하는 오류는 아니라는 생각이 들어서, 또 어쩌면 Xcode 등과 관련된 문제 같아서 GPT 찬스를 썼다. GPT 피셜 React Native 버전과 패키지 사이의 종속성 오류거나, CocoaPods(이 친구의 정체를 아직도 모르겠다) 설정과 관련이 있을 수 있다고 한다. 그래서 일단 알려준 명령어들을 잘 따라해 보았다.
우선 Podfile을 업데이트 하고 종속성을 설치해 보았다. (여기서 말하는 '종속성'의 구체적인 의미는 무엇일지 모르겠다..)
cd ios
pod deintegrate
pod install
다시 deintegrate한 pod를 install 해 주었다. 이 과정에서 설정 에러가 해결될 가능성이 있을 수도 있나보다. 다시 'pod install'을 실행하니 웬걸 오류가 난다.
GPT 피셜, hermes-engine(js 엔진이라고만 알고 있다)의 버전이 Podfile.lock(podfile 의존성 패키지들을 관리하는 파일인 것으로 보인다)에 기록된 버전과 달라서 나는 오류였다고 한다. 그리고 이 오류가 난 원인은 CocoaPods에서 종속성 관리를 할 때 충돌이 생겨서일 것이라고 한다. (아니 왜...?) 근본 원인이 무엇인진 아직도 모르겠지만, 우선 알려준 명령어로 실행해 보았다.
오류 메시지에 나온 것처럼 뒤에 특정 옵션을 붙여주면 pod install을 할 때 hermes-engine 관련 종속성만 업데이트하고 cocoapods의 전체 repo 업데이트는 생략한다고 한다.
pod update hermes-engine --no-repo-update
미묘하게 또 다른 에러가 났다. 아까와 비슷하게 CocoaPods에서 Sentry/HybridSDK와 관련된 버전을 찾을 수 없다는 말이었다. 그런데 여기서 추천해 준 명령어를 다시 입력해 보았다.
pod update Sentry/HybridSDK
그랬더니 이번에는 'pod install'을 처음 실행할 때 났던 오류가 나면서 이미 실패했던 명령어를 다시 추천해 주는 게 아닌가. 오류가 돌고 돈다.
다시 한 스텝 이전으로 돌아가서 GPT가 알려준 또 다른 방법으로 접근해 보자. 아까 전에 'pod update hermes-engine --no-repo-update' 명령어 말고도 다른 명령어를 알려줬었다. Podfile.lock에서 계속 충돌이 발생하는 것 같으니 이걸 지우고 다시 'pod install'을 시도해 보라는 의미였다.
rm Podfile.lock
pod install
다행히도 해당 방법은 성공하였다. 그렇다면 다시 한 스텝 더 돌아가서, Podfile을 업데이트하고 종속성을 설치해 주었으니 그 다음 스텝을 진행해 보자. 그 다음 스텝은 패키지 의존성과 관련된 문제였는데, node_module이나 pods에서 일부 파일 누락의 가능성(대체 왜인진 모르겠다)이 있으므로 지웠다가 다시 설치하라는 답을 주었다. 일단 해보자.
rm -rf node_modules
rm -rf ios/Pods
npm install
cd ios
pod install
그 다음 방법은 Xcode의 설정을 확인하는 것이었다. 여기서 RN 관련 설정이 잘 되어있는지도 혹시 모르니 확인해 보자. ios 디렉토리에서 해당 프로젝트의 .xcworkspace 파일을 열어주자.
cd ios/onestep.xcworkspace
여기서 확인하려는 설정은 마치 SDK 경로가 잘 설정되어 있는지를 확인하는 것과 비슷했다. 다음과 같은 화면에서 'Header Search Paths'가 올바르게 설정되었는지를 확인하려고 했다.
그리고 XCode 툴을 열어서 위에서 한 것처럼 Build Settings > Header Search Path 설정에 다음과 같은 루트를 추가해 주었다. 해당 애니메이션 관련 라이브러리를 사용하기 위해서 필요한 모양이다.
마지막으로 캐시를 정리해 주었다.
watchman watch-del-all
npm start --reset-cache
cd ios && xcodebuild clean
그 과정에서 또 두 가지의 에러가 났다. 아직 앱을 iOS 에뮬에서 띄우지도 않았는데 말이다. 이쯤되면 발생한 에러들을 트리로 그려서 dependency를 파악하는 것도 어려울 정도로 히스토리가 복잡해진다...!^^
첫 번째는 'npm start --reset-cache' 명령어를 실행하면서 난 뉴 에러들이었다.
두 번째 캡처의 경우는 코드에서 뭔가 잘못된 설정이 되어있는 것 같아서 내가 직접 수정해야 할 것 같으니, 우선 첫 번째 오류(5-1-1)만 해결해보자. GPT 피셜 해당 라이브러리의 react native 코드와 js 코드가 서로 다른 버전으로 설치되어있을 때 나는 오류라고 한다. 나는 그냥 npm install을 한 것밖에 없는데 왜 저러는지, 그리고 이 말이 구체적으로 무슨 말인지는 정확히 이해하진 못했다.
npm list react-native-reanimated
해당 명령어를 입력하면 관련 라이브러리가 어떻게 설치되었는지를 보여주는 것 같다.
버전이 다른지를 확인하라는데, 내가 보기엔 버전이 모두 같다! 뭘 더 해야 하지 싶다. 일단은 GPT가 권장하는 대로 해당 라이브러리의 최신 버전으로 한번 바꿔보자.
npm install react-native-reanimated@latest
그리고 다시 문제의 명령어였던 'npm start --reset-cache'를 실행했다. 그런데 위에서와 똑같은 오류가 다시 나는 게 아닌가. 에러 메시지에서 관련 링크를 주길래 들어가봤다. 링크에서는 아래와 같은 솔루션을 주었다.
일단 저녁을 먹고와서 다시 해보자.
✅ 궁금한 점
1. cocoapods와 hermes-engine은 어떤 관계가 있을까? 위에서 pod install을 할 때 --no-repo-update를 붙이는 옵션이 왜 오류 해결에 영향을 주는지 의문이다.
2. pod deintegrate는 pod를 지우는 것과는 다른 명령어일까?
3. Xcode에서 header search path 설정할 때 recursive와 non-recursive 옵션의 차이는 무엇이고 왜 recursive로 설정해야 할까?
4. 'npm outdated'는 구체적으로 어떤 기능을 하는 명령어일까? 그냥 npm으로 설치된 라이브러리 중 outdated된 라이브러리들을 보여주는 용도인지 궁금하다.
5. 다른 팀원의 컴퓨터에서는 잘 동작하는데 이번에 내가 ios 에뮬레이터를 처음 설치하고 앱을 실행하려고 하면서 이런 많은 문제들이 발생한 상황이다. 이런 환경설정 dependency? 암튼 프론트에서 실행환경을 통일 혹은 통제할 수 있는 방법이 있을까? 이런 과정이 앞으로 틈틈이 있다고 생각하면 좀 많이 성가실 것 같다.
6. watchman을 직접 설치한 기억이 없는데, 물론 내 기억이 잘못되었을 수도 있다. 암튼 watchman은 RN과 직접적으로 연관된 것은 아닌 툴인지, 아님 연관된 어떤 툴인지 궁금했다.
그저께 마주한 문제에 대해서 팀원과 상의를 해 본 결과, 일단 안드로이드에서 애플 로그인 버튼을 disable 시키는 것도 하나의 방법이라는 생각이 들었다. 물론 '안드로이드에서도 당연히 애플 로그인 할 수 있어야 하는 거 아니야?' 라는 질문이 들어온다면 좀 많이 죄책감이 들겠지만... 일단 할 일이 많기에 어쩔 수 없다. 임시방편으로 덮어 쓰고 그 다음에 새로운 방법을 찾아보자.
그렇다면 문제의 그 라이브러리(사실 라이브러리가 문제인지 내가 문제인지는 확실하지 않다)를 사용해볼 수 있겠다. 설정을 차근차근히 따라해 보자.
빌드를 다시 하고 캐시를 삭제해준 뒤, 어제 막혔던 명령어를 실행해 주었더니 우선 expo 홈 화면까지 띄우는 것은 성공했다.
// 빌드 다시 하기
APP_MODE=development npx expo prebuild
// 캐시 삭제하기
rm-rf~/Library/Developer/Xcode/DerivedData
// 어제 막혔던 명령어 실행
APP_MODE=development npx react-native run ios
그러나 여기서 'Recently opened'를 누르면 다음과 같은 에러가 났다.
로그를 보니 특정 확장자로 시작하는 index 파일이 없어서 나는 에러 같았다. 보통 루트 디렉토리의 index 파일을 시작점으로 인식한다는 것은 알고 있었고, 아마도 이 ios emulator를 처음 런칭할 때에도 그렇게 생각하는 것 같았다. 그러나 현재 프로젝트에서는 app/_layout.jsx 파일을 진입점으로 사용하고 있었으므로, 이에 대한 설정이 필요한 것일수도 있다는 생각이 들었다.
GPT와 티키타카를 통해서 다음과 같은 방법들을 시도해봤고, 아쉽지만 모두 실패했다.
1. metro.config.js(metro의 설정파일)에서 기존 코드를 주석처리 한 다음에 다음과 같은 코드를 추가해주었다. return 값의 mainFields 값들 중 하나를 진입점으로 보는 것이라고 추측했고, 여기에 기본값인 'index.js/index.ts' 파일 대신에 'app/_layout.jsx' 값을 대신 넣어주었다.
오늘과 어제는 많은 일이 있어서 우선은 context 정리가 필요할 것 같다. 크게는 동기화 관련된 논의와 디자인 관련 논의가 있겠다.
동기화의 경우, iOS 출시를 시작하면서 문제점이 발견되었다. 현재 안드로이드만 출시된 우리 앱은 rnfirebase의 messaging().getToken() 이라는 메소드를 통해 FCM(firebase cloud messaging) 토큰을 받고 있었다. 그런데 문제는 iOS의 경우 사용자가 알림을 띄우는 것을 허용하지 않으면 이 토큰값을 받아올 수 없었다. 그런데 백엔드 서버에서는 이 값을 기준으로 요청을 보낸 식별하기 때문에, 이 값이 없으면 클라이언트에서도, 서버에서도 에러가 나는 문제가 생겼다.
처음에 팀원들과 논의해서 나온 방안은 두 가지였다.
알림 표시를 허용하지 않으면 앱을 사용할 수 없게끔 하기
11월 전까지, 2주 안에 구현 가능한 동기화 방법(후보는 롱 폴링과 웹소켓이다)을 찾아서 푸시 알람 대신 다른 방법으로 동기화 구현하기
그러다가 어제 저녁 멘토님과 개인 면담을 했는데, 멘토님께서 또 다른 방향의 조언을 주셨다.
현재 문제가 되는 것은 '한 사용자의 두 개 이상의 기기가 동시에 켜져 있고 업데이트를 받는 것'이며 일반적인 사용에는 fcm 토큰이 없어도 무리가 없다. 그렇다면 '알림을 허용하지 않으면 다중 기기 동기화에 문제가 있을 수도 있다'고 사전 고지하고 만약 알림에 동의하지 않으면 그냥 사용하게 할 수 있겠다. 이 방식이 가장 빠른 방식이므로, 우선은 이 방식대로 iOS를 출시한 뒤 추후 방법을 찾아보자.
추후 방법 중 하나는 현재 프론트에서 사용하는 expo에서 제공하는 backgroundFetch 기능이다. 말은 backgroundFetch인데, foreground에서도 동기화를 지원할 수도 있겠다.
구글 드라이브 동기화 방법도 있다. 이 경우 서비스에서 사용자의 데이터를 저장하는 별도의 DB가 필요하지 않으며 서버에서 API만 지원하면 된다. 대신에 초기에 공부는 좀 필요할 것이다.
그래서 일단은 1번의 방향으로 진행하기로 했다. 다만 이 경우 현재 코드에서는 rnfirebase의 messaging().getToken()으로 fcm 토큰을 못 받아오면 클라이언트에서도, 서버에서도 에러가 날 여지가 있기 때문에 관련 로직을 수정해서 디바이스 토큰을 못 받아왔을 경우에도 잘 동작하도록 바꿔 주어야 하겠다.
디자인 관련 논의의 경우, 원래는 디자인비를 쓸 생각이 없었는데 어제 프론트 멘토님께서 앱의 완성도를 높이거나 사용자를 더 효과적으로 유치하려면 디자인도 중요한 요소라고 말씀해 주셨던 것이 하나의 계기가 되었다. (또 다른 이유로는 프로젝트 활동비가 너무 많이 남아서이기도 했다.)
처음에는 그런데 이걸 지금 맡기면 언제 완성하나 싶었는데(기말평가는 11월 중순이다), 생각해 보면 12월 이후에 사이드 프로젝트로 이것을 계속 이어갈 수도 있는 거였다. 그때는 디자인 외주를 우리의 돈으로 진행해야 하기에, 그럴 때 완성된 디자인을 보고 작업하려면 지금도 디자인 외주를 신청하는 것이 늦지 않았겠다는 생각이 들었다.
아무튼 그래서 디자인 외주를 급하게 알아보기로 했고, 중간에 1시간의 회의를 통해 다음과 같은 사항을 논의해서 합의에 이르렀다. (노션 멘토링 페이지에 작성해 두었다)
디자인 외주로 무엇을 맡길 것인가
디자이너 컨택을 어떻게 할 것인가
작업 요청서에는 무슨 내용을 작성할 것인가
디자이너 컨택을 하고 작업물을 받아오는 동안 해 보면 좋을 것들은 무엇인가
그래서 방금 전까지는 디자이너 분께 우리가 피그마로 뚝딱뚝딱, 얼기설기 만든 화면에 대해서 각자 개선하면 좋을 점들을 코멘트로 달아두고 왔다. 그리고 디자이너 컨택 전까지도 할 일이 매우 많았는데, 그 중의 하나가 애플 로그인을 구현하는 것이었다.
팀원이 보내준 자료에 의하면 iOS 앱 출시에서 기각되는 사유 중 하나가 다른 소셜로그인은 있는데 애플로그인이 없어서라고 한다. 그래서 원래 계획에는 없었지만 애플 로그인을 개발하게 된 것이다.
사실 구글로그인을 몇 달 전에 프론트에 붙여 봤어서 애플 로그인도 크게 어려울까? 라는 생각이 들었다. 분명 관련 문서가 있을 것이고, js에서 하는 방법을 잘 찾기만 하면 될 터였다. 그래서 해야 할 일은 크게 두 가지다.
1주차 워크북을 보았다. 워크북에서는 해당 주차의 목표가 무엇인지 스스로 점검하고, 해당 주차의 목표를 지키기 위해서 보면 좋을 learning material들을 추천해 주고 있었다.
해당 learning material 중에서 Contributing to Django라는 문서가 있길래 클릭해 보았다. 해당 문서에서는 django에 어떻게 기여할 수 있을지를 알려주고 있었다. 그리고 나는 코드 패치를 쓰는 것만이 장고에 기여하는 것이라고 생각했는데, 그 외에도 문서화를 하는 등의 다양한 기여 방법이 있었다.
나의 우선적인 목표는 코드로 기여를 하는 것이었기에, 관련된 문서를 눌러서 해당 프로그램과 관련된 튜토리얼을 진행하였다.
중간에 'pip install pylibmc' 부분에서 에러가 났다. GPT 찬스를 써서 'brew install libmemcached' 명령어를 실행해도 해결되지 않았는데, 다행히 중간에 발견한 어떤 글을 보고 따라해 보았다. 그러다가도 에러가 계속되었는데, 원인상 'brew install libmemcached' 명령어로 설치한 libmemcached 패키지의 경로를 찾을 수 없어서, 즉 해당 패키지가 지정된 경로에 있지 않거나 설치되어 있지 않아서 발생하는 에러 같았다.
여러 블로그들을 탐방한 결과 django open forum에도 해당 이슈가 올라와 있었다. 해당 글을 읽고 커맨드를 따라해 보면서 오류를 해결할 수 있었다.
LIBMEMCACHED=/opt/homebrew pip install pylibmc
테스트에 필요한 패키지들을 설치하는 것 까지는 완료되었다. 그런데 다음 명령어를 실행하니 아래와 같은 오류가 발생하는 게 아닌가.
python3 runtests.py
이런 에러가 왜 나지 싶어서 소스 코드를 보았더니 의심가는 지점이 있었다. 실행한 runtests.py 파일의 46-48번째 라인이다. 일부러 이런 오류가 나도록 설정해 놓은 걸까?
추측해 보기로는 Django 6.0 버전 전까지 나는 에러 같기도 하다. DeprecationWarning은 이제 곧 제거될 예정인 기능을 사용할 때 나는 경고인 것으로 안다. 그런데 그렇다면 RemovedInDjango60Warning은 무엇일까? 60이 6.0을 말하는 것인지, 60번째 버전을 나타내는 것인지 잘 모르겠어서 모호했다. 그런데 6.0 버전은 한참 남았으니 60번째 버전이라는 해석도 가능할 것 같다.
어쨌든 위의 에러가 왜 나는지는 다시 생각해 보니 알 것 같았다. 현재 python 3.12(내가 사용하는 버전)에는 RemovedInDjango50Warning은 있지만 RemovedInDjango60Warning은 없어서 나는 문제였다. 왜냐하면 지금 실행시킨 코드는 정식 출시된 버전이 아니라 개발 버전의 코드이기 때문에 그런 것일 수도 있겠다.
그렇다면 어떻게 해야 할까? 일단 이 에러를 무시하는 것이 맞을까? 아니면 파이썬의 최신 버전(3.13)을 설치하면 에러가 해결되려는지는 잘 모르겠다. 우선은 보류해 보자.
Djangonaut의 첫 weekly meeting을 마쳤다!
영어 회의는 처음이라 자기소개할 때 진땀을 뺐지만 영어를 잘하지 못합니다... sorry! 하고 잘 넘어갔다. 회의의 분위기는 kind, charming, welcoming...했다. 뉴비를 환영하는 분위기였다.
질문 시간에는 '만약 티켓을 할당받았는데 그 티켓에 대한 배경지식이 없어서 잘 처리하지 못하면 어떻게 하죠' 라고 질문했는데, 매우 자연스러운 일이며 그럴 땐 도움을 요청하면 된다고 했다. 공식적으로는 django forum에 discussion을 올리고, 만약 공식적으로 밝히기 좀 망설여진다면 디스코드에 밝혀도 된다고 했다.
그리고 navigator와 captain의 역할이 나눠져 있는 점도 신기하면서 좋았다. navigator는 technical helper, supporter 같은 역할을 하고, captain은 emotional, mental helper, cheerleader 같은 역할을 한다고 하셨다.
그리고 티켓에 대해서 공식적인 deadline은 없다고 한다. 왜냐면 티켓별로 그 범위가 천차만별이므로... '기간 내에 못 끝내면 어떡하지?' 라는 걱정은 안 해도 된다고 해주셨다.
미팅은 다음 주부터 항상 같은 시간에 매주 진행하고, captain 분과는 격주로 1:1 미팅을 진행한다. 새삼 djangonaut들에게 신경을 많이 써주시는 것 같아서 마음이 따듯해졌고 훈훈한 시간이면서도, 이 활동을 잘 경험하고 기록해서 앞으로도 이어갈 수 있게끔 하고 싶었다.
이전에 작업하다가 미뤄두었던 RN-Django e2e test 이슈를 다시 가져와보았다. 매번 앱을 켜고 잘 동작하는지 확인하는 과정을 e2e test로 대신할 수 있기 때문에, 이 작업이 많이 필요하다고 느꼈고 이것만 잘 구축된다면 제법 테스트하기 쉬운 앱이 될 것 같았다.
기존에 사용하려던 라이브러리는 detox였다. 이 라이브러리는 여러 가지 시나리오를 구체적으로 테스트할 수 있다는 장점이 있어서 이걸 써보고 싶었다. 그러나 현재 프로젝트에서는 react native 위에 expo를 사용하고 있는데, detox 공식문서에서는 공식적으로 expo와 같이 사용하기 위한 별도의 지원을 하고 있지는 않다고 했다. 즉 만약 오류가 발생하면 알아서 해결해야 한다는 것이다.
그래서 고민했던 다른 옵션이 maestro였다. maestro는 detox와 달리 yaml 파일로 테스트 코드를 작성하며, 간편하게 작성할 수 있다는 장점이 있어 보였다. 그러나 detox와 달리 세부적인 케이스에 대해서 테스트를 하는 부분에서는 좀 약점이 있었다. 그리고 maestro로 테스트를 하려면 npm으로 패키지를 다운받는 게 아니라 CLI를 사용해야 한다는 것이 마음에 안 들었다.
하지만 마음에 안 든다고 expo dependency를 이겨내면서 detox로 테스트하기에는 괜한 시간을 쓰는 것 같았기에, 일단 maestro로 진행해 보기로 했다.
우선 아래와 같은 명령어로 CLI(iterm)에 maestro를 설치해 주자.
brew tap mobile-dev-inc/tap
brew install maestro
'maestro test'를 입력하면 CLI에서 자동으로 로컬에서 실행 중인 에뮬레이터를 찾는다고 한다.
그렇다면 .yaml 파일은 어디에다 작성해야 할까? 'maestro test' 명령어 뒤에 테스트하고 싶은 yaml 파일의 경로를 입력하면 된다고 한다. 우리 프로젝트의 경우, 테스트 폴더에다 별도로 모든 파일들을 넣어두어야 할지, 아니면 각 view나 component마다 test 파일을 두어야 할지 고민이다. 이 부분은 팀원들에게 물어보고 결정해야겠다.
그리고 구글링하다보니 도움이 되는 또 다른 글을 발견했다! react native를 사용해서 maestro로 end to end test를 하는 글이었다.
빼먹은 부분이 있었는데, CLI에서 maestro 명령어를 자유롭게 사용하기 위해서 환경변수 설정을 해 주어야 했다. '.zshrc' 파일에 다음과 같은 명령어를 추가해 주자.
export PATH="$PATH":"$HOME/.maestro/bin"
그리고 잘 동작하는지 확인해 보자. 우선 가장 간단하게는 로그인 화면을 테스트해볼 수 있겠다. 버튼을 누르면 구글 폼이 나타나는지를 테스트 해 보자. 그런데 아직 maestro yaml 파일 문법에 익숙하지 않아서, GPT 찬스로 예제 코드를 짜 달라고 했다.
그리고 생각해보니 구글 로그인에 써 있는 텍스트가 다국어 버전별로 달랐다. 그래서 테스트할 때 다국어 상황에서 언어를 판단해서 특정 텍스트를 감지할 수 있는지도 궁금했다.
이 부분은 로그인 버튼에 별도의 ID를 적용함으로써 알 수 있다고 했다. testID를 사용하면 버튼의 텍스트나 속성 등이 조금씩 달라지더라도 각 버튼을 고유하게 식별할 수 있다고 한다. 현재 앱에는 다국어 지원이 적용된 상황이라 텍스트만으로 버튼을 판단하기는 어려울 것 같아서, 해당 방법을 사용하기로 했다.
그런데 구체적으로 어떻게 하는지를 보니 testID를 적용하려면 코드나 컴포넌트에 testID라는 속성을 하나씩 추가해 주어야 한다. 이러면 테스트 코드를 위해서 코드를 또 다 바꿔줘야 한다... 이게 맞을까? 어떻게 하면 코드의 변경을 최소화하면서 테스트 코드를 작성할 수 있을까?
여러 가지 방법이 있었다. 두 가지로 나눌 수 있겠다.
HOC(high order component)를 통해 코드의 변경을 최소화하면서 testId 적용하기
계층 구조를 사용해서 testID나 text를 사용하지 않고, 예를 들면 'container의 0번째 자식 컴포넌트' 이런 식으로 지정할 수도 있다고 한다.
HOC를 사용하는 방법의 경우, withTestID라는 함수를 통해 WrappedComponent를 변환해서 로직을 좀 통일해 주기는 했지만 여전히 테스트를 위해서 일반 로직에 손을 대야 하는 건 똑같았다. 그렇다고 계층 구조를 사용하자니, 만약 화면이나 계층 구조가 변화한다면 그에 맞춰서 테스트 코드도 수정해 주어야 했다. testID처럼 고유하게 컴포넌트를 식별하고는 싶지만, 그걸 일일이 앱 코드에다가 testID를 넣어서 주입해 주고 싶지는 않았다.
다시 GPT 찬스를 써 보았다. 코드에 직접 testID라는 속성을 추가하지 않으면서도, 테스트 런타임에서만 해당 값을 추가해줄 수 있는 방법이 있었다. 바로 useEffect나 componentDidMount 훅을 사용해서, 테스트 환경에서 실행될 경우에만 testID 값을 추가해주는 방식이었다. 코드의 속성을 수작업으로 편집하지 않아서 괜찮은 방법 같았다.
다만 여전히 많은 컴포넌트들이 있어서 이 함수를 하나씩 추가해야 한다는 점에서는 부담이 되었다. 문득 여러 규모가 있는 기업들은 이런 테스트를 어떻게 하고 있을지 궁금했다. 토스의 FE 아티클 하나를 참고해보니 위에서 잠깐 고민했던 것처럼 TestID는 어쨌거나 코드에 직접 주입을 해 주어야 하기 때문에 단점이 있다는 내용이 있었다.
그리고 찾다 보니 토스페이먼츠 노션에서 어제 궁금한 점으로 적었던 mock과 stub의 차이점도 알 수 있었다. mock은 객체가 내부에서 외부로 나갈 때 상호작용을 모방하는 객체이고, stub은 외부에서 내부로 들어오는 객체의 상호작용을 모방하는 객체라고 한다. 예를 들면 mock은 이메일을 발송하는 등의 작업이고, stub은 데이터베이스에서 더미 데이터를 입력으로 받아오는 등의 작업이라고 한다.
그러면 어떻게 테스트를 짜야 맞을까?
다시 생각해보니 다국어 지원은 어쨌든 언어만 달라지는 것이므로 이 상황에서 크게 고려할 점은 아니었다. 만약 언어가 통일된다면, 기존과 같이 텍스트 방식으로 일단은 테스트를 작성해 보는것도 나쁘지 않겠다. 그러다가 추후에 변경을 해도 되겠다. 현재 i18n의 기본 설정으로는 fallbackLng(변환 실패했을 때의 기본 언어)가 영어로 되어 있어서, 영어를 기준으로 텍스트를 작성해주면 되겠다.
아주아주 간단한 테스트를 작성해봤다. 잘 돌아가는지를 테스트 해 보려고 한다.
// indexTest.yaml
appId: com.safezone.onestep
---
# 앱을 실행
- launchApp
# 로그인 버튼을 찾고 클릭
- tapOn:
text: 'Sign in with Google'
# Google 로그인 폼이 나타나는지 확인
- assertVisible:
text: 'onestep'
maestro test indexTest.yaml
다음과 같은 명령어를 실행했더니, 이런 화면이 나왔다. 앱을 시작하는 것은 성공했으나 'Sign in with Google'이 들어간 텍스트를 찾지 못한 것 같았다.
파란색 디렉토리로 가서 스크린샷을 확인해 보았다. 알고보니 앱을 런칭하면 바로 로그인 화면이 뜨는 게 아니라 다음과 같은 화면이 떠서, 'Sign in with Google' 텍스트를 찾지 못한 것 같았다.
이 경우는 어떻게 해야 할지 고민이다. 일단 GPT 찬스를 써 보았다.
이 부분은 Expo Go를 사용하는 것과 연관이 있다고 한다. 이 문제를 방지하려면 launchApp 명령어를 사용하기보다는 링크를 통해 직접 앱을 열어야 한다. 그런데 링크를 또 어떻게 찾나... GPT 피셜 exp://172.x.x.x 이런 링크를 찾으라는데, 아무리 봐도 콘솔에 그런 링크가 안 떴다.
그래서 나름의 편법을 발견했는데, 바로 앱을 띄워두고 터미널을 하나 더 열어서 maestro로 테스트를 해 보았다. 그랬더니 아까는 fail했던 테스트가 잘 되더라.
위의 코드로는 로그인 프로세스가 완료되지는 않았다. 아래와 같이 코드를 더 추가해서 진행해 주었고, 성공한 사진을 볼 수 있었다. 이제 이 테스트 코드를 로직의 첫 번째에 실행시켜서 나머지 시나리오들도 테스트해보면 되겠다.
appId: com.safezone.onestep
---
# 앱을 실행
- launchApp
# 로그인 버튼을 찾고 클릭
- tapOn:
text: 'Sign in with Google'
# Google 로그인 폼이 나타나는지 확인
- assertVisible:
text: 'onestep'
# 로그인 폼이 나타나면 구글 계정 클릭
- tapOn:
text: '.*gmail.com'
#
- tapOn:
text: '계속'
- assertVisible:
text: 'Today'
waitUntilVisible: true
pytest에서 그저께 작업한 내용을 바탕으로 pytest 명령어를 실행하였는데, 예상 외로 잘 되지 않았다. monkey patching을 통해서 해당 부분 코드를 스킵하는 부분이 잘 동작하지 않는 것 같았다. 나는 monkeypatch의 setattr 함수를 사용해서 해당 함수의 반환값을 None으로 바꾸려고 의도했었는데, 생각해보니 해당 함수를 호출하는 것 까지는 막을 수 없었을 수 있겠다.
즉 해당 함수의 리턴값은 None이 될 수 있다고 해도, 해당 함수를 호출하는 코드는 실행되고 있어서 해당 'request.auth.get("device")' 부분에서 에러가 나는 것 같다. 어떻게 하면 해당 코드를 아예 무시하고 호출되지 않도록 할 수 있을까? 그러니까 해당 함수(send_push_notification_device)의 인자로 들어가는 request.auth.get('device') 코드에서 문제가 발생하고 있었다.
그래서 해결 방법을 알아보았다. 팀원의 조언을 들어보니 모든 테스트에서는 create_user와 authenticated_client라는 pytest fixture를 사용하고 있었다. 그래서 해당 authenticated_client fixture의 내용을 다음과 같이 수정해 주었더니 문제가 해결되었다.
@pytest.fixture
def authenticated_client(create_user):
# 원래 token={'device': None} 부분이 없었는데 추가해 주었음
client.force_authenticate(user=create_user, token={"device": None})
yield client
client.force_authenticate(user=None) # logout
여기서 client의 force_authenticate 메소드를 사용하면 해당 인자로 주어진 user와 token 값으로 강제로 인증을 시도한다. 내가 이해한 바로는 request 객체의 request.user 값과 request.auth 값을 주어진 값으로 강제로 바뀌게끔 한다.
위와 같이 코드를 바꿔 주었더니 아래와 같이 기존엔 모두 fail하던 테스트 케이스들이 거의 다 통과되었다.
일부 fail이 난 테스트 케이스는 비동기 뷰를 호출하는 테스트 케이스였다. 구체적인 로그는 다음과 같았다. coroutine과 관련된 에러가 나는 이유는 동기 상황을 가정하면 httpRequest를 리턴하기를 예상하는데, 비동기 뷰에서는 coroutine을 리턴해서 에러가 나는 것 같았다.
팀원이 말해주길 'pytest asyncio'라는 라이브러리가 있다고 한다. 그래서 해당 라이브러리를 통해서 비동기 뷰를 테스트해보면 좋겠다. 일단 이 이슈는 따로 티켓을 파서 진행하자.
✅ 궁금한 점
비동기 뷰의 동작 원리가 궁금하다. 나는 지금까지 def를 async def로 바꾸면 모든 문제가 해결! 되는 줄 알았는데 비동기 뷰의 동작 원리는 생각보다 복잡하더라... Celery는 왜 써야 할까? 그리고 왜 비동기 뷰가 있으면 비동기 요청을 핸들링할 수 있는 미들웨어가 있어야 효율적으로 동작할까?
monkey patching의 원리가 궁금하다.
mock이랑 stup이 있다고 한다. stup은 뭘까
pytest에서는 DRF의 authentication backend를 지나지 않는 걸까? 흐름이 궁금하다.
현재 뷰의 코드 일부분을 가져와봤다. 여기서 send_push_notification_device는 알람을 보내는 함수이다. 이 로직은 FE 서버에서 요청이 들어오면 해당되는 device_token의 값이 있기에 정상적으로 호출되지만, 테스트 환경에서 호출되면 별도의 디바이스에서 호출되는 것이 아니므로 오류가 나는 문제가 있었다.
지금부터 할 일은 기존의 테스트 로직을 수정하여 해당 함수를 우회하도록 작성해 주는 것이다.
우선 테스트에서는 django.urls의 reverse를 사용해서 이런 식으로 뷰의 이름을 통해 url을 가져온다. 여기서 url은 해당 뷰의 url이고, 'todos'는 해당 뷰를 unique하게 구분짓는 식별자로 볼 수 있다.
from django.urls import reverse
url = reverse("todos")
맨 처음에 작성해 볼 todo의 create API를 보자. 해당 뷰의 이름은 'todos'로 되어 있다. 그렇다면 위와 같은 코드로 해당 API의 url을 얻을 수 있겠다. 이제 pytest의 patch 기능을 이용해서, 해당 뷰 함수 안에서 호출되는 함수가 리턴하는 값을 mock 객체로 바꿔 주면 되겠다.