오늘 배운 것

현재 상황에 대해서 간략히 정리해 보자. 애플 로그인 프론트 작업은 1차적으로 완료되었다. 다만 서버와 통신할 때 콘솔에 찍히는 응답이 null인 문제를 해결하면 되겠다. 

그런데 오늘 추가적인 문제가 발생했다. 다른 팀원이 알려준 이슈인데, 앱을 지웠다가 다시 깔았을 때 messaging().getToken() 메소드에서 오류가 발생한다는 문제였다. (이 messaging은 RnFirebase 라이브러리의 기능이다.) 그리고 이 이슈는 아마도 내 apple developer 계정에 '프로비저닝 프로파일(Profile)'이 없는 것과 연관이 있다고 말해주었다. 

 

결론은 '프로비저닝 프로파일'을 새로 만들어줘야 했다. 문제는 이 프로파일을 만들기 위해서는 '새 디바이스'를 등록해야 한다는 점이었다. 그런데 이 새 디바이스의 경우, 이미 기존 apple developer 계정에 연동되어 있던 내 아이패드나 팀원의 아이폰은 '이미 등록된 디바이스'로 간주되어 새 디바이스로 간주하지 않는다는 문제가 있었다. 

 

그렇게 프로비저닝 프로파일 생성하는 난관에 부딪히고 있던 때, 다시 Device 탭에 들어가 봤더니 웬일로 잘 되는 것이 아닌가. 


이 Device(다른 팀원의 iphone 디바이스다)를 사용해서 apple developer 계정에서 프로비저닝 프로파일을 생성할 수 있었고, 생성한 프로파일을 XCode에 적용할 수 있었다. 

 

그렇게 프로파일을 무사히 적용하고, dev 브랜치와 rebase도 했다. 그리고 다시 'npm run ios:dev' 명령어를 실행하니, 다음과 같은 에러가 뜨는 게 아닌가. 

 

GPT의 조언과 다양한 블로그 글들을 참고해봤는데, GPT가 제시한 원인과는 달리 RN 환경이 아닌 곳에서도 많은 github issue들을 볼 수 있었다. 그에 비해 GPT는 다음과 같은 원인들을 제시했는데, 과연 이 원인들이 정말이었을지는 조금 의문스럽다. 

  1. Sentry 설정 문제
  2. Expo와 Sentry의 연동 문제
  3. Sentry 관련 패키지(@sentry/react-native) 설정 문제

3번의 경우 npm i 명령어로 버전 업그레이드를 해 주었다. 여전히 되지 않아서 1-2번 아니면 다른 github issue(찾아봤는데 내 경우와 완벽히 일치하는 issue는 잘 보이지 않았다)를 찾아서 공통점을 파악해봐야 할 것 같다. 

 

1번의 경우 sentry.properties 파일이 다른 경로에 있을 가능성을 제시해 주었는데, 문제는 나는 애초에 sentry.properties 파일이 없었다는 거였다. 

 

2번의 경우도 마찬가지로 설정 파일(app.config.js)에서 expo 관련 설정에서 특정 파라미터(ios.dangerous)의 경로 설정이 잘못되었을 가능성을 제시해 주는데, 문제는 애초에 해당 파라미터와 관련된 설정은 없었다. 아직 여기에서 막히고 있다... 내일 다시 해 봐야겠다. 

 

오픈 소스에 첫 PR을 남긴 기념으로 이를 기록하고자 한다. 나는 #34900번 티켓을 작업했다. 정확히는 #34900번 티켓을 참조하는 티켓을 작업했다. 무슨 말이냐면, #34900번 티켓의 주제는 'django와 python 3.13 버전의 호환성'이다. 호환성이 맞지 않는 경우는 무수히 많고 한 번에 해결되는 것이 아니니 #34900번 티켓과 관련된 이슈는 계속 나올 수밖에 없다. 

 

티켓 작업 순서는 처음이라 헷갈렸었는데, 앞으로 헷갈리지 않도록 정리해 보려고 한다. 

 

우선 django 공식 레포에 대해서 forked repository를 만들어 준다. django는 오픈 소스 레포지토리이기 때문에 일반 사용자들은 직접 브랜치를 만들거나 레포에 직접 push를 날릴 권한이 없다. 이럴 때는 git fork를 사용한다. 반면 특정 레포지토리에서 협업 프로젝트를 하고 있고, 해당 레포에 대해 브랜치를 만들거나 직접 로컬에서 변경 사항을 push할 권한이 있다면 git clone을 사용하는 것으로 알고 있다. 

 

즉 git fork는 원본을 그대로 복사해서 만든, 하지만 원본과 엄연히 다른 복사된 레포지토리를 만든다. 반면 원본을 그대로 로컬에 가져오는 것은 git clone이다. 마치 shallow copy와 deep copy와 유사하다는 생각도 든다. 

 

어쨌든 git fork를 만들어 주고, 해당 fork된 레포에서 또 브랜치를 따로 판다. 다른 사람들의 PR들을 봤을 때 브랜치 이름에 대한 특별한 규칙은 없는 것 같다. 

 

오늘 작업한 이슈는 deprecated warning과 관련된 이슈였다. django 레포에는 tests라는 디렉토리가 있고, 해당 디렉토리 하위에는 무수한 test 패키지들이 있다. 다 돌려보면 실제로 제법 시간이 많이 든다. 이 중에서 pyenv를 통해 python 3.14(아직 정식 릴리즈 버전은 아니다)를 사용하는 가상환경을 실행하고, 이 환경에서 cache 테스트를 돌려보면 다음과 같이 deprecation warning이 뜨는 것을 볼 수 있다. 

pyenv install 3.14-dev
pyenv virtualenv 3.14-dev django-3.14
pyenv activate django-3.14
python -Wall ./runtests.py cache

 

즉 python 3.13, python 3.14 버전에서 해당 함수가 deprecated 되었음을 알 수 있다. 

 

glob 모듈에서 glob1() 메소드를 사용하는 부분이 있는데, 해당 glob1 메소드는 deprecated 처리 되었고 이후 python 3.15 버전부터는 사용이 불가능하기 때문에 경고 메시지가 뜨는 것이었다. 

 

전체 검색으로 glob1()의 사용처를 찾아보니 딱 한 군데였다. 물론 이 오류를 내가 처음부터 바로 딱 찾아낸 것은 아니고, djangonaut의 navigator님의 도움을 받았다. 해당 glob1() 메소드의 호출부를 glob()으로 변환시켜 주면 되는 문제였다. 기존 코드는 다음과 같았다. 

 

그렇다고 단순히 함수 이름만 바꿔서는 안 되겠다. glob1()은 2개의 인자를 받는 반면, glob()은 1개의 인자만을 받고 있었다. 찾아보니 glob()와 glob1()의 역할은 거의 비슷했다. 두 메소드 모두 특정 디렉토리의 모든 하위 디렉토리에서 특정 패턴을 가진 파일들을 찾아주는 역할을 했다. 그리고 두 함수 모두 리스트를 리턴했다. 

 

glob1(a, b)의 경우 a라는 디렉토리의 하위 디렉토리에서 b라는 패턴으로 시작하는 파일들(문자열들)을 찾아냈다. 반면 glob(a)의 경우 인자를 하나만 받는데, 이때 인자 a는 디렉토리와 패턴을 모두 포함하는 문자열이다. 

 

그래서 위의 함수와 같은 역할을 하도록 glob1() 대신 glob()을 사용하여 함수를 고쳐주면 이런 식으로 바꿀 수 있다. 

 

이렇게 바꾸고, 반드시 테스트를 돌려서 위에서 나던 deprecation warning이 사라졌는지를 확인해야 하겠다. 

 

warning이 사라진 것을 볼 수 있다! 이제 forked된 브랜치에 git push를 하고, 해당 forked된 레포에서 django 레포로 PR을 올려보았다. 

 

code patch(수정한 코드 커밋들)도 무사히 잘 반영되었다!! 첫 PR을 무사히 마쳤으니, 앞으로 다른 이슈도 열심히 작업해봐야겠다ㅎㅎ

 

이번주의 나의 활동은 어땠는지를 점검하고 이를 KPT(keep-problem-try) 회고로 작성해 보자. 

 

뭘 했는가

애플 소셜로그인에서 막히던 것을 겨우겨우 해결한 한 주였다. 프로젝트 기준으로는 내가 한 게 애플 소셜로그인만 있었다. 생각해 보면 이슈가 참 많았는데 말이다. 

 

물론 가장 중요한 이슈는 애플 소셜로그인이 맞았다. 그리고 생각해 보면 프로젝트 외에서도 다양한 곳에서 interrupt가 들어왔어서 프로젝트를 잘 하지 못한 것인가도 싶었다. 실제로 방해되는 작업들은 아니고 모두 중요한 작업들이지만(Djangonaut과 취준 활동), 순수 프로젝트 입장으로만 보자면 interrupt가 맞았다. 

 

Keep - 이번 주에 잘 해온 것

모든 일을 100% 쳐내지는 못했지만, 우선순위 기반으로 다른 것들이 조금 밀리더라도 가장 중요한 것들에만 집중했다. 프로젝트 면에서는 가장 중요한 애플로그인만 잡고 있었고, 취준 면에서는 팔랑귀 때문에 여러 채용 공고에 솔깃하더라도 딱 두 개만 집중해서 원서를 썼다. (하나는 제출했고, 나머지 하나는 아직 쓰는 중이다.)

 

프로젝트와 취준에 비해 약간 우선순위가 밀린 것이 Djangonaut이다. 오픈 소스 컨트리뷰트를 하고 싶은데, 주에 겨우 최소 시간인 4시간만 할애하는 것 같아서 죄책감이 든다. 어떻게 하면 좋을까? (절대 그만두고 싶지는 않고, 균형 잡기가 필요한데 어떻게 할 수 있을지를 고민하게 되었다.) 즉 잘한 것은 어느 하나에만 몰빵하지 않고 나름 균형을 추구한 것, 그리고 각각의 영역 중에서도 가장 중요한 것에만 선택과 집중을 한 것 같다. 

 

Problem - 문제점

문제점은 프로젝트와 Djangonaut에만 있다. 취준은 선택과 집중을 하는 현 상태가 맞다고 느끼고, 아예 원서를 안 넣는 상황도, 너무 많이 넣어서 허덕이는 상황도 아니었기 때문에 만족 상태에 있다고 보았다. 그렇다면 취준에 그렇게 많은 시간을 쓴 것 같지는 않은데, 왜 남은 시간들이 프로젝트와 Djangonaut에 온전히 쓰일 수 없었는가에 대해 생각해봐야 하겠다. 

 

지금 현 상황은 고3 입시 상황과 비슷하다. 취준이 입시는 아니지만, 나는 종종 묘한 데자뷰를 느낀다. 항상 일 모드에 대한 스위치가 켜져 있고, 소마 센터에서 집까지 왔다갔다 하는 것도 하루에 2시간 이상이 걸리며, 집에서조차 잘 회복하고 있다는 생각은 잘 들지 않았다. (관심사가 제한되고, 뭔가 충분히 리프레시되는 느낌이 잘 없었다.) 

 

그리고 이러한 상황에서는 일을 추가적으로 늘리는 것보다 범위를 조금 줄여서라도 '어떻게 성취율을 높일 수 있을까'를 고민하는 것이 맞다고 보았다. 일을 늘린다고 완성하는 일이 그만큼 비례해서 늘지는 않기 때문이다.

 

시간을 효율적으로 쓰고 있지 않고, 그럭저럭 큰 문제는 없지만 잘 회복하고 있지는 않으며, 현재 목표로 한 것들에 비해 성취율은 다소 낮다는 것이 현재 상황의 문제점인 것 같다. 

 

Try - 시도할 것

Problem을 참고해서 시도할 점을 정해보자. 시간을 효율적으로 써야 하지만, 헤르미온느의 시간표처럼 빈 시간 없이 빡빡한 시간표를 쓰면 안 되겠다. 하루에 필수적으로 쉬는 시간과 잠 자는 시간을 정해두자. 그리고 그 나머지 시간에서만 효율을 찾아보자. 아무래도 10시 넘어서는 본격적으로 쉬고, 오전 8시에는 일어나는 삶이 제일 바람직하겠다. 그리고 10시에 쉬려면 적어도 8시 반에는 센터에서 집으로 돌아와야 하겠다. 

 

이렇게 된다면 쉬는 시간에는 일 스위치를 꺼서 회복에 집중할 수 있고, 나머지 시간에서는 조금 더 추진력을 얻어볼 수 있겠다. 나의 원인은 이도 저도 아닌 상태에서 에너지를 뺏겼던 것이었기 때문이다. 마치 고3 때 시험이 얼마 남지 않았어도 자기들의 멘탈 회복을 위해 짧게라도 코노를 가던 친구들처럼, 나도 매일 조금씩은 그런 시간을 내야 하겠다. 

 

그리고 프로젝트의 범위도 너무 무리해서 잡지 말자. 우선순위를 정하고, 11월 최종발표를 위해서 해야 할 최소한의 것을 정해보자. 그리고 그것을 낱낱이 주간별로 쪼개보자. 당연히 해야 할 것은 많은데, 어차피 11월 이후에도 어떻게든 이 프로젝트는 계속 이어나갈 것이기 때문에 굳이 완성을 11월 전까지 해야 할 필요는 없겠다. 그래야 프로젝트를 하면서 취준과 Djangonaut도 병행할 수 있겠다. 

 

Djangonaut의 경우도 마찬가지다. 프로젝트와 취준이 아무리 중요해도, 나는 여기에 컨트리뷰터로 참여하는 만큼 최소한의 시간은 내야 할 의무가 있다. 오늘부터 매일 한 시간씩 Djangonaut에 할당하자. 가령 7시에서 8시는 누가 뭐래도 Djangonaut에 기여하는 시간으로 빼 두자. 그래야 나머지 취준과 프로젝트에 우선순위를 아예 뺏기지 않을 것 같다. 

 

 오늘 배운 것

어제 많은 삽질을 통해서 애플로그인에서 액세스토큰을 발급받는 데는 성공하였다. 그러면 기존의 로직을 사용해서, 이렇게 얻어낸 값을 앱의 값으로 저장해주면 되겠다. 또한 어제 봤는데 애플 로그인에서 맨 처음에 이메일 값을 제대로 저장하지 못할 경우에 대해서 걱정이 많았었다. 그런데 그 밑의 내용을 읽어보니 계속적으로 이메일을 받아올 수 있는 방법이 있었다. (다만 이 경우는 테스트 환경에서만 가능하긴 했다...) 어쨌든 다음과 같은 두 가지 작업을 하면 되겠다. 

  1. 설정 바꿔서 다시 email 받아오도록 설정하고, 이메일 값 저장해두는 로직 작성하기
  2. 받아온 identityToken 값을 백엔드 서버에 보내는 API 작성하기

우선 1번의 경우 위의 사진에 나온 것처럼 에뮬레이터의 설정에 들어가서 기존에 Apple ID를 사용한다는 설정을 다시 취소해 주어야 했다. 취소해 주고 다시 해보니 아래처럼 email 값과 identityToken 값 모두가 잘 들어온다. 

 

그런데 여기서 두 가지의 잠재적 문제점을 발견했다. 

 

첫 번째는 어쨌든 테스트 환경이 아니면 유저가 굳이 폰에서 로그인이 된 설정을 다시 풀 리가 없다. 그러므로 만약 유저가 앱에 최초 로그인을 하는데 그 과정에서 실패가 일어나서 실수로 딱 한번 주어지는 이메일 값을 저장소에 저장하지 못한다면, 문제가 생긴다. 

 

두 번째는 유저가 본인의 이메일 정보를 제공하지 않는 옵션을 선택할 경우이다. 이 경우에는 최초 로그인이더라도 프론트에서 유저의 이메일 정보를 갖고 있을 수 없다. 

 

그래도 두 경우 모두 identityToken의 값(애플에서 로그인을 마치고 받은 액세스토큰과 같은 개념)은 얻을 수 있었다. 여기서 궁금한 점은 identityToken 값으로 유저의 이메일 정보를 얻을 수 있는지였다.

 

이메일 정보가 중요한 이유는 사실상 이메일 필드가 pk를 제외하고 유저를 유일하게 구분할 수 있는 속성이기 때문에 꼭 필요하다고 판단했기 때문이다. 또한 구글 로그인 유저는 email이 항상 있는 반면 apple 로그인 유저에서는 없다면 이 경우에도 나중에 여러 유저의 데이터를 공통적으로 다루기 어려울 것 같았다. 

 

그래서 찾아보니 pyJWT 라이브러리로 해당 Identitytoken을 분해해볼 수 있을 것 같았다. GPT의 도움을 받아 스크립트를 작성해봤다. 

import jwt
import requests
import urllib3
from jwt.algorithms import RSAAlgorithm


APPLE_PUBLIC_KEY_URL = "https://appleid.apple.com/auth/keys"


urllib3.disable_warnings()

def get_apple_public_key(kid):
    # SSL 검증 비활성화하고 Apple 공개 키를 가져옴
    response = requests.get(APPLE_PUBLIC_KEY_URL, verify=False)
    keys = response.json().get("keys", [])

    # kid에 맞는 키 찾기
    for key in keys:
        if key["kid"] == kid:
            return RSAAlgorithm.from_jwk(key)
    raise ValueError("Matching key not found")

def validate_identity_token(identity_token):
    try:
        # JWT의 헤더에서 kid 추출
        unverified_header = jwt.get_unverified_header(identity_token)
        kid = unverified_header["kid"]

        # kid에 맞는 Apple 공개 키 가져오기
        public_key = get_apple_public_key(kid)

        # 토큰 디코드 및 검증
        decoded_token = jwt.decode(
            identity_token,
            public_key,
            algorithms=["RS256"],
            audience="audience",  # 실제 Apple Login에서 설정한 client_id로 변경
            issuer="https://appleid.apple.com"
        )

        # 유효한 토큰인 경우 정보 반환
        user_id = decoded_token.get("sub")
        email = decoded_token.get("email", None)

        return {
            "status": "success",
            "user_id": user_id,
            "email": email
        }
    except jwt.ExpiredSignatureError:
        return {"status": "error", "message": "Token has expired"}
    except jwt.InvalidTokenError:
        return {"status": "error", "message": "Invalid token"}
    except Exception as e:
        return {"status": "error", "message": e}


identity_token = "identity_token"


result = validate_identity_token(identity_token)

 

그리고 그 과정에서 원래는 SSL 관련 에러가 났었다. 이 에러는 SSL 인증서가 없어서 나는 에러였고, SSL 인증서를 발급받거나 SSL 인증을 임시로 무효화하는 방법이 있었다. SSL 인증서 발급은 간단했지만, 그래도 SSL 인증서를 발급받는 방법은 해당 인증서를 서버에도 설치해줘야 했기 때문에 더 번거롭다고 판단했다. 그래서 위에 SSL 인증을 임시로 건너뛰는 코드를 추가해줬다. 

 

그랬더니 다음과 같이 user id와 email 값이 잘 나오더라. 물론 user id는 실제 pk와 같은 정수값은 아니었다. 그리고 emai도 토큰에 잘 포함되어 있어서, 서버에 잘 저장이 가능해 보였다. 

 

그렇다면 앞서 맨 처음에 걱정했던 이메일을 저장하는 문제를 해결해볼 수 있겠다. 이메일을 최초 시도에 바로 받아오는 방법만이 아니라, identityToken 값에서 이를 추출하는 방법이 있겠다. 이 방법을 프론트에서도 사용할 수 있도록 코드를 바꿔보자. js 코드로 애플로그인에서 받은 identityToken을 디코딩하는 방법은 생각보다 간단했다. 

import { jwtDecode } from 'jwt-decode';

const appleAuthRequestResponse = await appleAuth.performRequest({
    requestedOperation: appleAuth.Operation.LOGIN,
    requestedScopes: [appleAuth.Scope.FULL_NAME, appleAuth.Scope.EMAIL],
})

const decodedToken = jwtDecode(identityToken);

 

이로서 프론트 앱에서도 identityToken 값을 통해서 email 값을 얻을 수 있게 되었다. email 값뿐만 아니라 많은 정보를 볼 수 있었다. 

 

이제 구글 로그인에서 해당 identityToken을 가지고 서버에 요청을 보내는 로직을 참고해서, 애플 로그인에서도 이를 비슷하게 구현해 보자. 

 

그리고 아이폰에서도 구글로그인도 되어야한다... 이를 위해서는 iosClientId 값이 필요하다. 이것도 API를 통해 받아와 보자. 우선은 애플 로그인부터 구현해 보자. 

 

우선 애플 로그인을 시도했을 때 response를 잘 받아와야 하기 때문에, 이를 저장할 상태 변수를 선언해 준다. 이 appleResponse는 애플 로그인이 성공적으로 실행되면 해당 response를 저장하기 위한 변수이다. 또한 구글로그인과 애플로그인은 로직이 겹치는 부분이 많기 때문에 중복 코드를 줄이기 위해서 loginType 변수에 구글 로그인인지, 애플 로그인인지를 저장하도록 하였다. 

const GOOGLE_LOGIN = 0;
const APPLE_LOGIN = 1;

const [appleResponse, setAppleResponse] = useState(null);
const [loginType, setLoginType] = useState(null);

 

그리고 useEffect 훅을 사용해서 해당 appleResponse의 상태가 null이 아니게 되면 애플 로그인 후 그에 맞는 조치(애플로그인에서 받은 idToken을 jwtToken으로 바꾸고, AsyncStorage에 관련 값을 설정하는 일)를 하도록 한다. 

useEffect(() => {
  if (appleResponse !== null) {
    const appleToken = appleResponse.identityToken;
    if (appleToken) {
      setLoginType(APPLE_LOGIN);
      handleLogin(appleToken, loginType);
    }
  }
}, [appleResponse]);

 

그리고 해당 loginType 변수값에 따라서 googleLogin API 또는 appleLogin API를 호출하도록 설정해 주었다.

if (loginType === GOOGLE_LOGIN) {
  loginResponse = await Api.googleLogin({ token, deviceToken });
} else if (loginType === APPLE_LOGIN) {
  loginResponse = await Api.appleLogin({ token, deviceToken });
}

 

그리고 Api 클래스에 appleLogin 함수를 추가해 주었다. 

 

 오늘 배운 것

어제 처리하다가 막힌 SZ-457 이슈(RN 앱에서 iOS 에뮬레이터로 로그인하기)를 다시 처리해보자. 많은 에러가 나서 일일이 기록하지는 못했지만 수많은 에러들이 있었다. 

 

그러다 이런 화면이 뜨더니, 설정에서 기기에 로그인을 해 주니 '애플 로그인' 버튼을 눌렀을 때 반응이 나오기 시작했다. 

 

관련된 정보를 입력해주니, 아래와 같이 입력한 로그인 코드가 실행되는 것으로 보였다. 

const onAppleButtonPress = async () => {
  try {
    const appleAuthRequestResponse = await appleAuth.performRequest({
      requestedOperation: appleAuth.Operation.LOGIN,
      requestedScopes: [appleAuth.Scope.FULL_NAME, appleAuth.Scope.EMAIL],
    });

    const credentialState = await appleAuth.getCredentialStateForUser(
      appleAuthRequestResponse.user,
    );

    if (credentialState === appleAuth.State.AUTHORIZED) {
      // when user is authenticated
    }
  } catch (error) {
    console.error('Apple login error:', error);
  }
};

 

다만 걸리는 점은 credentialState의 값이 appleAuth.State.AUTHORIZED가 아니라 appleAuth.State.REVOKED였다는 점이다. 왜 revoked가 되었는지 원인을 찾아보려고 했는데, 하나같이 지금 상황에서는 해당되지 않는 원인들이었다. 

 

GPT가 제시한 방법으로는 apple login 관련 설정이 옳지 않아서였다. 그런데, 세상에. 라이브러리에 멀쩡하게 메시지가 나와 있는게 아닌가?

 

그렇다. 그냥 내가 까막눈이었던 것이다. 그래서 3시간 장장의 삽질은 이렇게 허무하게 끝이 났다.

 

 오늘 배운 것

어제 기나긴 삽질을 거친 이후 ios 에뮬레이터에 RN 앱을 띄울 수 있었다. 과정을 상세히 기억하지는 못해서 정확히 어떤 방법이 문제를 해결하였는지는 알 수 없지만, 크게 두 가지 과정을 거쳐서 RN 앱을 띄우는 데 성공했다. 

  1. 그동안 잘못된 명령어를 사용하고 있었다. npm run ios가 아니라, package.json 파일에 있는 안드로이드 버전 명령어를 그대로 android 부분만 ios로 바꿔서 입력해주었다. 
  2. 1번의 과정을 거쳤더니 또 다른 에러가 났는데(이번에는 xcode에서 모듈 설치하다가 난 오류로 기억한다), 이 부분은 xcode의 빌드 캐시를 삭제하고... 등등의 과정을 거쳤더니 해결되었던 것으로 기억한다. 
// package.json에 새로 추가한 부분
{
  "scripts": {
    "ios:local": "APP_MODE=local npx expo prebuild --platform ios --clean && APP_MODE=local expo run:ios",
    "ios:dev": "APP_MODE=development npx expo prebuild --platform ios --clean && APP_MODE=development expo run:ios",
    "ios:stage": "APP_MODE=staging npx expo prebuild --platform ios --clean && APP_MODE=staging expo run:ios",
    "ios:prod": "APP_MODE=production npx expo prebuild --platform ios --clean && APP_MODE=production expo run:ios",
  }, 
}

감동

이제는 지금은 동작하지 않는 애플 로그인 버튼을 누르면 창이 켜지도록 만들어주면 되겠다. 우선 사용하려는 라이브러리의 Mac 설정으로 진행해봤다. 애플 로그인 버튼을 누르면 다음과 같은 에러 로그가 찍힌다. 

 

GPT에게 물어봤더니 앱의 개발자 계정 설정이나 Xcode의 프로젝트 설정 문제 가능성이 있다고 하더라. 우선은 Xcode의 프로젝트 설정에 들어가서, 'apple sign in' 옵션을 추가해주었다. 그리고 Team의 값이 None으로 되어 있었는데, 이 값도 이름으로 바꿔 주었다. 

 

그리고 다시 앱을 실행시켰더니 애플 로그인 버튼을 누르면 동작하지 않고 관련 경고 로그가 떴다. 

 

스택을 보니 아까와 같은 로그가 그대로 찍히고 있었다. 그리고 아래에 보니 이런 로그도 함께 있었다. 무엇부터 해결해야 할지 감이 안 잡혔다. 우선 이것부터 해결해 보기로 했다. 

 

해당 에러는 'expo-splash-screen'이라는 패키지가 ios 앱에서 스플래시 화면을 찾지 못해서 발생하는 에러라고 한다. 이를 위해서는 우선 Xcode를 열고, 파일 설정에서 Storyboard 파일을 찾아서 파일 이름을 SplashScreen.storyboard로 지정하고 프로젝트의 ios 디렉토리에 저장해 주어야 한다고 한다. 일단 해보자.

 

그런데 이렇게 해 봤는데도 되지 않아서 깃허브 링크를 참고해서 진행해봤다. 

npx expo install expo-splash-screen
npx pod-install

 

여전히 되지 않았다. 그리고 생각해보니 이미 기본으로 제공되는 스플래시 스크린이 있는 것으로 알고 있는데, 왜 굳이 새로운 스크린을 명시적으로 등록해야 하는 것인지 모르겠다. 그래서 반발심이 들어 일단 이 에러를 잠깐 무시하고, 위의 에러를 보기로 했다. 

 

 마주한 오류들 정리

  1. XCode 설정 및 프로젝트 설정 오류
  2. CocoaPods 설정 오류
  3. expo 캐시 관련 오류
  4. npm cache 관련 오류

 궁금한 점

  1. 애플 개발자 계정에서 app ID와 service ID는 무엇이 다를까? 그 외에도 Certificates, Identifiers 등 종류가 많은데 각각이 왜 필요하고 어디에 쓰이는지 궁금하다.

 

 오늘 배운 것

그저께 발생한 부분에서 아직 막혀있다. 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'가 올바르게 설정되었는지를 확인하려고 했다.

 

다음과 같은 경로가 있는지를 확인해보자.

$(SRCROOT)/../node_modules/react-native/React/**
$(SRCROOT)/../node_modules/react-native/ReactCommon/**
$(SRCROOT)/../node_modules/react-native/Libraries/**

 

확인해 보니 ${} 변수 값 중에서 SRCROOT로 시작하는 경로들이 없었다. 그래서 위의 값들을 추가해 주었다. 

아래와 같이 설정해 주고, 설정도 'recursive'로 바꿔주었다. 

 

그리고 XCode의 빌드 캐시를 정리해 주었다. XCode에서 Product > Clean Build Folder를 클릭해 주었다. 

 

그리고 다시 ios 에뮬레이터에서 앱을 로드하는 명령어를 실행해 주었다.

npm run ios

한참 로그가 쭉 나오더니 이번에도 fail이 났다. 아까와 오류 코드는 똑같고 세부 메시지는 달랐다. 

 

GPT에게 도움을 요청했더니 또 세부적인 여러 방법을 알려주었다. 오류가 돌고 돌지는 않아서 다행인데, 언제까지 해야 되는지는 좀 의심스럽다. 암튼 진행해 보자. 

 

GPT가 제시한 원인은 두 가지였다. 

  1. reanimated 관련 라이브러리 설치에서 문제가 있었다. (오류 1과 관련)
  2. iOS 빌드 설정 및 환경설정 충돌 문제(또...?)의 가능성이 있다. (오류 2와 관련)

오류 1과 관련된 문제부터 해결해보자. 크게 다섯 가지의 단계가 있었다.

 

우선 npm install로 문제가 발생한 라이브러리를 설치해 주었다. 

npm install react-native-reanimated

 

그 후 ios의 pod를 업데이트했다. 

cd ios
pod install

 

 

루트 디렉토리의 babel.config.js 파일에 코드를 추가해서 방금 설치한 라이브러리와 관련된 플러그인을(무슨 역할을 하는 플러그인인지는 모르겠다) 프로젝트에서 사용할 수 있도록 명시해줬다. 

// babel.config.js
module.exports = function (api) {
  api.cache(true);
  return {
    presets: ['babel-preset-expo'],
    plugins: [
      ['@babel/plugin-proposal-decorators', { legacy: true }],
      ['react-native-reanimated/plugin'],	// 해당 코드를 추가했다
      [
        'module-resolver',
        {
          root: ['./'],
          alias: {
            '@': './',
          },
        },
      ],
    ],
  };
};

 

그리고 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과 직접적으로 연관된 것은 아닌 툴인지, 아님 연관된 어떤 툴인지 궁금했다. 

 

+ Recent posts