오늘 배운 것

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

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

 

 오늘 배운 것

그저께 마주한 문제에 대해서 팀원과 상의를 해 본 결과, 일단 안드로이드에서 애플 로그인 버튼을 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' 값을 대신 넣어주었다.

const { getDefaultConfig } = require('metro-config');

module.exports = (async () => {
  const {
    resolver: { sourceExts, assetExts },
  } = await getDefaultConfig();
  return {
    transformer: {
      babelTransformerPath: require.resolve('react-native-svg-transformer'),
    },
    resolver: {
      assetExts: assetExts.filter(ext => ext !== 'svg'),
      sourceExts: [...sourceExts, 'jsx', 'js', 'ts', 'tsx'],
      // 여기서 entry 파일을 _layout.jsx로 지정
      mainFields: ['app/_layout.jsx', 'react-native', 'browser', 'main'],
    },
  };
})();

 

2. package.json 파일에서 'main' 속성의 값을 기존의 'expo-router/entry' 대신 'app/_layout.jsx' 파일로 바꿔주었다.

{
  "name": "onestep",
  "version": "1.0.0",
  // "main": "expo-router/entry",	// 기존 코드
  "main": "app/_layout.jsx"		// 추가한 코드
}

 

여전히 똑같은 화면이 나왔다. 위에서 시도한 방법들에 대한 내용을 프롬프트로 넣고 다시 GPT에게 문제 해결 방법을 제시해달라고 요청해 봐야겠다. 

 

 궁금한 점

1. metro의 역할은 정확히 무엇일까? 프론트에서 앱이 어떻게 빌드되는지에 대한 이해가 부족한 것 같은데 꼬리질문을 해봐야겠다. 

 

 오늘 배운 것

우선 간단하게 애플 로그인 버튼부터 만들어 주자. 사실 엄청 간단하지는 않았다. 현재 프로젝트에서는 ui kitten의 eva 아이콘 팩을 쓰고 있었는데, 이 eva 팩에는 apple 아이콘이 없었다. 그래서 다른 서드파티 라이브러리에서 아이콘을 가져와야 했다. 다행히 이와 관련된 공식문서가 있어서 가이드를 참고하면서 따라해 보았다. 

 

내가 사용하려고 하는 아이콘은 react-native-vector-icons 라이브러리의 AntDesign 패키지에 있었다. 그래서 다음과 같은 파일을 만들어서 RN에서 사용하기 위한 IconsPack을 만들어 주었다. 

// antdesign_icons.js
import React from 'react';
import { StyleSheet } from 'react-native';
import Icon from 'react-native-vector-icons/AntDesign';

export const AntDesignIconsPack = {
  name: 'antDesign',
  icons: createIconsMap(),
};

function createIconsMap() {
  return new Proxy(
    {},
    {
      get(target, name) {
        return IconProvider(name);
      },
    },
  );
}

const IconProvider = name => ({
  toReactElement: props => AntDesignIcon({ name, ...props }),
});

function AntDesignIcon({ name, style }) {
  if (!style) {
    return null;
  }

  const { height, tintColor, ...iconStyle } = StyleSheet.flatten(style);
  return <Icon name={name} size={height} color={tintColor} style={iconStyle} />;
}

 

그리고 UI kitten에서 아이콘을 사용하기 위해서는 앱의 가장 루트 파일(여기서는 _layout.jsx)에서 IconRegistry라는 컴포넌트를 통해 특정 아이콘 팩을 사용할 것이라고 명시해 주어야 했다. 

 

기존에 이미 eva iconpack을 사용할 것이라고 명시해줬던 코드가 있어서, 그 다음과 같이 코드를 변경해 주었다. 

// _layout.jsx
import { AntDesignIconsPack } from '@/antdesign-icons';
import { EvaIconsPack } from '@ui-kitten/eva-icons';
import { IconRegistry } from '@ui-kitten/components';

<IconRegistry icons={[EvaIconsPack, AntDesignIconsPack]} />

 

그리고 버튼을 만들어 주었다. 현재는 뷰와 데이터 처리 로직을 조금 분리한 상태라서 데이터 로직에서 함수 하나를 import 하는 방식으로 코드가 짜여져 있었다. 

// index.jsx
const { signInWithGoogle } = useGoogleAuth();

 

여기서 'signInWithApple' 함수를 추가하고, 구현부를 빈 함수로 작성해 주었다. 

// index.jsx
const { signInWithGoogle, signInWithApple } = useGoogleAuth();
// useGoogleAuth.js
const useGoogleAuth = () => {
  const [androidClientId, setAndroidClientId] = useState('');
  const [iosClientId, setIosClientId] = useState('');

  const signInWithGoogle = async () => {
    if (androidClientId === '') {
    } else {
      await promptAsync();
    }
  };

  const signInWithApple = async () => {
    if (iosClientId === '') {
    }
  };

  return { signInWithGoogle, signInWithApple };
};

 

그리고 구글 아이콘처럼 등록에 사용될 애플 icon 파일을 만들어 주고, 버튼 하나를 추가해 주었다.

// AppleIcon.jsx
import { Icon } from '@ui-kitten/components';
import React from 'react';

export const AppleIcon = props => (
  <Icon name="apple1" {...props} pack="antDesign" />
);
// index.jsx
const Login = () => {
  const { t, i18n } = useTranslation();

  const { signInWithGoogle, signInWithApple } = useGoogleAuth();

  return (
    <View style={styles.container}>
      <StatusBar style="auto" />
      <View style={styles.iconContainer}>
        <Image source={imageSource} style={styles.icon} />
      </View>
      <View style={styles.buttonContainer}>
        <Text category="h2">OneStep</Text>
        <Button accessoryLeft={GoogleIcon} onPress={() => signInWithGoogle()}>
          {t('views.index.googleSignIn')}
        </Button>
        // 추가한 애플 로그인 버튼
        <Button accessoryLeft={AppleIcon} onPress={() => signInWithApple()}>
          {t('views.index.appleSignIn')}
        </Button>
      </View>
    </View>
  );
};

 

그랬더니 다음과 같은 화면을 볼 수 있었다. 

버튼 자체는 괜찮은데 버튼 사이의 간격이 너무 큰 것 같았다. 이 부분을 개선해 보자. 

현재는 텍스트와 두 개의 버튼이 모두 하나의 View 컨테이너 안에서 justifyContent: space-evenly 속성을 적용받고 있었다. 이 부분을 수정해 주었다. 그랬더니 잘 나오더라!


이제는 기능 구현으로 넘어가 보자. 구글 로그인 때와 마찬가지로, 프론트 앱에서 로그인을 아예 완료한 다음에 서비스에 액세스토큰(구글의 경우는 idToken)을 전달하면, 서버에서 이를 서버와 통신할 때 쓸 수 있는 JWT 토큰으로 바꿔 주는 작업이다. 

 

우선 처음 해 보는 작업이라 GPT의 도움을 받아서 시작하였다.

 

우선은 관련 패키지를 설치하고, Xcode 관련 iOS 설정을 해 준 다음에, 코드를 작성하면 된다고 한다. react-native-apple-authentication 이라는 라이브러리를 설치해 주었다. 

npm install --save @invertase/react-native-apple-authentication

 

그리고 XCode 관련 설정을 해보았다. 사실 이게 시간이 오래 걸렸다. 처음에 XCode를 시작하려는데 더 높은 맥 버전이 필요하대서 맥 업데이트를 하고, XCode를 설치하고, 에뮬레이터를 깔고... 이렇게 기다리는 시간들이 오래 걸렸다. 

 

그리고 마침내 iOS emulator를 얻어냈다. 

 

구현하는 과정에서 두 가지의 문제가 생겼다. 

  1. 현재 사용하려는 라이브러리의 appleauth 모듈을 안드로이드에서는 사용할 수 없다. -> 그러나 생각해보면 안드로이드에서도 애플 로그인을 지원할 수 있어야 하기 때문에 다른 라이브러리 등의 대안을 알아보는 것이 맞겠다. 
  2. 'npx pod-install' 명령어에서 오류가 난다. -> 이 부분은 팀원에게 물어보니 ios 관련해서 별도로 추가해줘야 하는 설정 파일들이 있는 것으로 보였다. 이것들을 추가해서 다시 해보자.

 

많은 세부 과제들이 남아있어서 막막한데, 쪼개보면 이렇게 되겠다. 

  1. iOS 에뮬레이터 실행 성공시키기
  2. iOS 에뮬레이터에서 애플로그인 실행 성공시키기
  3. 안드로이드 에뮬레이터에서 애플로그인 실행 성공시키기

우선 1번부터 해 보자. GPT와 티키타카를 거쳐서 'npx pod install' 명령어를 성공시켰다. 

 

나머지 오류는 내일 해결해 보자. 

 

 오늘 배운 것

오늘과 어제는 많은 일이 있어서 우선은 context 정리가 필요할 것 같다. 크게는 동기화 관련된 논의와 디자인 관련 논의가 있겠다. 

 

동기화의 경우, iOS 출시를 시작하면서 문제점이 발견되었다. 현재 안드로이드만 출시된 우리 앱은 rnfirebase의 messaging().getToken() 이라는 메소드를 통해 FCM(firebase cloud messaging) 토큰을 받고 있었다. 그런데 문제는 iOS의 경우 사용자가 알림을 띄우는 것을 허용하지 않으면 이 토큰값을 받아올 수 없었다. 그런데 백엔드 서버에서는 이 값을 기준으로 요청을 보낸 식별하기 때문에, 이 값이 없으면 클라이언트에서도, 서버에서도 에러가 나는 문제가 생겼다. 

 

처음에 팀원들과 논의해서 나온 방안은 두 가지였다. 

  1. 알림 표시를 허용하지 않으면 앱을 사용할 수 없게끔 하기
  2. 11월 전까지, 2주 안에 구현 가능한 동기화 방법(후보는 롱 폴링과 웹소켓이다)을 찾아서 푸시 알람 대신 다른 방법으로 동기화 구현하기

 

그러다가 어제 저녁 멘토님과 개인 면담을 했는데, 멘토님께서 또 다른 방향의 조언을 주셨다. 

  1. 현재 문제가 되는 것은 '한 사용자의 두 개 이상의 기기가 동시에 켜져 있고 업데이트를 받는 것'이며 일반적인 사용에는 fcm 토큰이 없어도 무리가 없다. 그렇다면 '알림을 허용하지 않으면 다중 기기 동기화에 문제가 있을 수도 있다'고 사전 고지하고 만약 알림에 동의하지 않으면 그냥 사용하게 할 수 있겠다. 이 방식이 가장 빠른 방식이므로, 우선은 이 방식대로 iOS를 출시한 뒤 추후 방법을 찾아보자. 
  2. 추후 방법 중 하나는 현재 프론트에서 사용하는 expo에서 제공하는 backgroundFetch 기능이다. 말은 backgroundFetch인데, foreground에서도 동기화를 지원할 수도 있겠다. 
  3. 구글 드라이브 동기화 방법도 있다. 이 경우 서비스에서 사용자의 데이터를 저장하는 별도의 DB가 필요하지 않으며 서버에서 API만 지원하면 된다. 대신에 초기에 공부는 좀 필요할 것이다. 

 

그래서 일단은 1번의 방향으로 진행하기로 했다. 다만 이 경우 현재 코드에서는 rnfirebase의 messaging().getToken()으로 fcm 토큰을 못 받아오면 클라이언트에서도, 서버에서도 에러가 날 여지가 있기 때문에 관련 로직을 수정해서 디바이스 토큰을 못 받아왔을 경우에도 잘 동작하도록 바꿔 주어야 하겠다. 

 

디자인 관련 논의의 경우, 원래는 디자인비를 쓸 생각이 없었는데 어제 프론트 멘토님께서 앱의 완성도를 높이거나 사용자를 더 효과적으로 유치하려면 디자인도 중요한 요소라고 말씀해 주셨던 것이 하나의 계기가 되었다. (또 다른 이유로는 프로젝트 활동비가 너무 많이 남아서이기도 했다.) 

 

처음에는 그런데 이걸 지금 맡기면 언제 완성하나 싶었는데(기말평가는 11월 중순이다), 생각해 보면 12월 이후에 사이드 프로젝트로 이것을 계속 이어갈 수도 있는 거였다. 그때는 디자인 외주를 우리의 돈으로 진행해야 하기에, 그럴 때 완성된 디자인을 보고 작업하려면 지금도 디자인 외주를 신청하는 것이 늦지 않았겠다는 생각이 들었다. 

 

아무튼 그래서 디자인 외주를 급하게 알아보기로 했고, 중간에 1시간의 회의를 통해 다음과 같은 사항을 논의해서 합의에 이르렀다. (노션 멘토링 페이지에 작성해 두었다)

  1. 디자인 외주로 무엇을 맡길 것인가
  2. 디자이너 컨택을 어떻게 할 것인가
  3. 작업 요청서에는 무슨 내용을 작성할 것인가
  4. 디자이너 컨택을 하고 작업물을 받아오는 동안 해 보면 좋을 것들은 무엇인가

그래서 방금 전까지는 디자이너 분께 우리가 피그마로 뚝딱뚝딱, 얼기설기 만든 화면에 대해서 각자 개선하면 좋을 점들을 코멘트로 달아두고 왔다. 그리고 디자이너 컨택 전까지도 할 일이 매우 많았는데, 그 중의 하나가 애플 로그인을 구현하는 것이었다. 

 

팀원이 보내준 자료에 의하면 iOS 앱 출시에서 기각되는 사유 중 하나가 다른 소셜로그인은 있는데 애플로그인이 없어서라고 한다. 그래서 원래 계획에는 없었지만 애플 로그인을 개발하게 된 것이다. 

 

사실 구글로그인을 몇 달 전에 프론트에 붙여 봤어서 애플 로그인도 크게 어려울까? 라는 생각이 들었다. 분명 관련 문서가 있을 것이고, js에서 하는 방법을 잘 찾기만 하면 될 터였다. 그래서 해야 할 일은 크게 두 가지다. 

  1. 애플 로그인 버튼 만들기
  2. 애플 로그인 로직 개발하기

1번보다는 2번이 시간이 훨씬 많이 들 것으로 예상된다. 

 

 오늘의 운동 인증

 

+ Recent posts