[tolli #4] AOS 애플로그인 트러블슈팅

2026. 6. 30. 18:00·프로젝트
728x90

💡 읽기 전 필요한 지식 💡
- OAuth 소셜 로그인이 "토큰을 주고받는 과정"이라는 정도의 큰 그림
- 딥링크(tolli:// 같은 주소로 앱을 여는 것)와 WebView가 메시지를 주고받는다는 개념
- Firebase 로그인과 id_token(신원을 증명하는 토큰)이 무엇인지에 대한 가벼운 감

📖 읽고 나면 얻을 수 있는 것 📖
- "로그인은 되는데 앱으로 돌아오지를 않는" 같은 미묘한 버그를 어떻게 잡아가는지 감을 잡게 된다
- 안드로이드에서 애플 로그인이 왜 까다로운지, 그리고 그걸 어떻게 우회했는지 이해하게 된다
- 에러 메시지가 아니라 "호출이 아예 안 일어난 것"을 단서로 원인을 찍는 디버깅 사고방식을 엿볼 수 있다

일단 이 글을 읽기 전에 이전 글들을 읽으면 도움이 될 것이다.

 

[tolli #1] 무거운 성경암송 앱이 싫다

💡 읽기 전 필요한 지식 💡- 모바일 앱이 "기획 -> 개발 -> 출시"의 단계를 거친다는 정도의 큰 그림- 듀오링고 같은 학습 앱을 한 번이라도 써본 경험- "타겟 사용자"나 "문제 정의"라는 말이 어떤

bbin-guuuu.tistory.com

 

[tolli #2] 왜 React Native + WebView 하이브리드였을까?

💡 읽기 전 필요한 지식 💡- React와 React Native(RN)가 서로 다른 것이라는 정도의 구분- SSR과 CSR이라는 말을 들어본 경험- 앱이 웹뷰(앱 안에 띄우는 브라우저 창)로 웹을 띄울 수 있다는 사실📖 읽

bbin-guuuu.tistory.com

 

[tolli #3] switch로 쪼갠 화면이 협업 단위가 됐다

💡 읽기 전 필요한 지식 💡- 컴포넌트라는 개념에 대한 가벼운 이해- switch문이 값에 따라 분기하는 문법이라는 정도의 지식- 여러 명이 하나의 레포에서 함께 개발한다는 상황에 대한 감📖 읽

bbin-guuuu.tistory.com

사실 안드로이드에서 애플로그인을 구현하면서 필자는 여러 시행착오를 거쳤다.

근데 이 의문점이 들 수 있다.

왜 안드로이드폰에서 굳이 애플로그인을 구현해?

답은 간단하다.

안드로이드폰 사용자도 아이패드나 맥북을 사용할 수 있는거 아닌가?

최대한 여러 옵션을 아무리 소수의 사용자일지라도 오픈하고 싶었다.

 

그럼 이제 하나씩 해결 과정을 풀어가고자 한다.


iOS에서 되는데 안드로이드에서는 왜 버튼만 눌러도 에러가 날까?

애플로그인은 당연히 iOS에서 깔끔하게 동작했다.

그래서 그런지 안드로이드에서도 당연히 될 줄 알았다. 하지만 버튼을 누르는 순간부터 에러가 났다.

 

원인을 찾아보니 당시 쓰던 expo-apple-authentication 라이브러리가 iOS 전용이라는 것을 알 수 있었다.

애플은 안드로이드에서 네이티브 SDK 자체를 제공하지 않아서, 웹 OAuth 플로우를 사용해야만 한다.

즉, iOS와 안드로이드의 애플 로그인 방식 자체가 근본적으로 다른 것이라고 보면된다.

 

그래서 하나의 라이브러리로 억지로 통합하기보다는, 플랫폼별로 파일을 나누기로 했다. (추후에 관리하기도 편할거라고 예상했다.)

iOS는 이미 좋은 UX로 동작하니 그대로 두고, 안드로이드만 다른 방식을 가져가도록 했다.

auth/appleSignIn.ts          # 플랫폼 분기 진입점
├── appleSignIn.ios.ts        # expo-apple-authentication
└── appleSignIn.android.ts    # 웹 OAuth 플로우

여기서 꿀팁은 위처럼 파일 이름을 .ios.ts와 .android.ts로 만들어 두면,

Metro 번들러가 빌드할 때 확장자를 보고 플랫폼에 맞는 파일을 자동으로 골라준다!!!

(if문으로 런타임 분기를 직접 쓰지 않아도 되고, 번들에 불필요한 플랫폼 코드가 들어가지도 않는다는 이점!)

 

다만, 안드로이드의 웹 OAuth는 애플 정책상 무조건 https://로 시작하는 Return URL이 필요해서, 배포 없이는 테스트조차 어렵다는 것을 이때 알게 됬다... (이전에 Electron으로 데스크탑 웹앱을 만들 때에도 동일한 이슈가 있었다.)


expo-auth-session을 사용해볼까?

처음 안드로이드 쪽을 expo-auth-session으로 풀어보려 했다.

response_type=id_token

으로 시도했는데, 애플의 웹 OAuth는 이걸 지원하지 않아서 invalid_request 에러가 계속해서 났다.

 

그러니까 이런거다.

일반적인 OAuth 플로우에서는 id_token만 요청하는 게 가능한 provider도 있지만, 애플로그인의 웹 엔드포인트는 response_type의 조합에 까다롭다. (id_token외의 token이라던지 code라던지의 조합을 사용하는 방식)

하지만 id_token의 단독으로는 애플 스펙에서 지원하지를 않는다...

response_mode=fragment

이것도 name/email scope가 있으면 애플이 form_post를 강제해서 막혔다...

 

이게 진짜 애플의 악명 높은 제약이라고 한다.

더 쉽게 설명하자면 scope에 name이나 email, 즉 사용자 정보를 같이 받아오려고 하면, 보안상의 이유로 반드시 response_mode=form_post로 강제한다. 그래서 fragment나 query 방식의 redirect를 허용하지 않는다.

 

왜냐하면 id_token이나 user정보를 URL fragment(#...)이나 query string으로 리다이렉트하면 브라우저 히스토리나 서버 로그 등에 민감 정보가 노출될 위험이 당연히 높다. 따라서 POST body로만 응답을 돌려주도록 강제한다고 보면된다.

 

이 라이브러리로는 애플이 요구하는 방식을 맞춰줄 수 없다고 판단했다.

왜냐하면 expo-auth-session은 기본적으로 브라우저 리다이렉트 기반(주로 fragment/query 파싱)으로 동작하도록 설계되어 있어서,애플이 요구하는 POST 기반의 콜백 응답을 받아서 처리하는 흐름을 기본적으로 지원하지 않기 때문...

 

결국 필자는 react-native-app-auth로 전환해보았다.

이 라이브러리는 Chrome Custom Tab을 열어서 OAuth를 처리하고 딥링크로 앱에 결과를 돌려준다.

전체 흐름은 이렇게 정리된다고 보면된다.

1. 앱에서 react-native-app-auth로 Apple OAuth Custom Tab 열기
2. Apple -> https://tolli-fe-web.vercel.app/api/auth/apple/callback 으로 code POST
3. 서버에서 Apple token endpoint 호출해서 id_token 교환 (JWT client_secret 생성 필요)
4. 서버 -> tolli://auth/apple/callback?id_token=... 딥링크로 앱에 전달
5. App.tsx 딥링크 리스너 -> WebView에 APPLE_TOKEN으로 postMessage
6. 기존 로그인 로직 그대로 처리

위 흐름은 애플이 넘겨준 인가 코드를 서버가 받아서 id_token으로 교환하고,

그걸 딥링크로 다시 앱에 돌려주는 원리라고 보면된다.

앱과 서버, 그리고 WebView가 토큰을 이어달리는 구조이다.

여기서 서버는 next.js Api Routes이다!

로그인은 된다.. 근데 앱으로 돌아오지를 않는다.. (405에러)

그런데 callback 라우트를 Vercel에 배포했는데 405에러가 났다.

처음엔 서버 라우트가 POST를 안 받는가 싶었는데 진짜 원인은 따로 있었다...

 

애플이 form_post로 callback을 때려주는데,

필자는 처음에 openBrowserAsync를 쓰고 있었어서 앱이 아예 결과를 받을 수 없는 구조였다.

즉, 로그인 자체는 됐는데, 앱으로의 복귀가 안 됐던 거다...

 

여기서 찾아낸게 openAuthSessionAsync이다!!!

테이블로 정리해보겠다.

  openBroswerAsync openAuthSessionAsync
브라우저 외부 브라우저가 완전히 열림 인앱 Custom Tab
앱 복귀 불가 (그냥 나가버림) 딥링크 감지 시 자동 복귀
결과 반환 없음 redirect URL 반환
두 번째 인자 없음 감지할 딥링크의 prefix

그래서 필자는 두 가지를 같이 바꿔보았다.

  1. openBrowserAsync를 openAuthSessionAsync로 교체하기
    1. 이렇게 되면 두 번째 인자로 tolli://를 넘기면, 서버가 tolli://으로 리다이렉트할 때 이걸 감지해서 브라우저를 닫고 URL을 앱으로 돌려준다!
  2. 서버 callback에서 쿠키에 id_token을 담아서 웹 로그인 페이지로 리다이렉트하는 방식을 버리고,
    tolli://auth?id_token=... 딥링크로 바로 리다이렉트하도록 바꾸기

이렇게 두 가지를 바꾸니까 전체 플로우가 돌아가기 시작했다.

생각보다 단순한 문제였는데 이 두 함수의 차이를 몰라서 한참을 돌아갔던 것 같다...

AOS에서 애플로그인 성공하고 회원가입 페이지로 정상 이동되는 모습


여기서 끝이 아니다... 이제는 401에러...

위의 사진처럼 당연히 로그인을 성공하고 끝인 줄 알았다...

그런데 닉네임을 설정하고 나서 또 에러가 났다. (tolli에서는 닉네임을 설정한 후에 회원가입이 완료된다.)

Vercel로그를 보니 /api/auth/register 호출이 아예 없었다.

setNickname 페이지에서 fireAuth.currentUser가 null이라 idToken을 못 가져오고,

register API 호출을 건너뒤면서 createUser만 호출하고 있었다.

 

Network 탭을 열어보니 그래서 createUser을 호출할 때 401에러가 났다.

{
  "error": {
    "code": 401,
    "message": "unauthenticated: this operation requires a signed-in user",
    "status": "UNAUTHENTICATED"
  }
}

위 응답은 Firebase 로그인이 안된 상태로 DataConnect를 호출하고 있다는 뜻이다.

즉, 에러가 난 게 아니라, 인증되지 않은 채로 호출이 들어갔다"는게 핵심 포인트다.

 

원인은 두 가지였다.

 

1. rawNance 누락

안드로이드 애플 로그인은 앱에서 nonce라는걸 생성해서 SHA256 해시 후에 애플에 보낸다.

하지만 Firebase는 signInWithCredential 시에 raw nonce, 즉 해시 전의 원본을 같이 넘겨야 검증을 통과한다.

 

기존 코드는 idToken만 postMessage로 전달하고 rawNonce는 빠져 있었다.

Firebase가 nonce 검증에 실패하면서 로그인이 조용히 실패하고 다음 페이지로 넘어가고 있었던 것....

// 수정 전
postToken("APPLE_TOKEN", idToken)

// 수정 후
webviewRef.current?.postMessage(
  JSON.stringify({ type: "APPLE_TOKEN", token: idToken, rawNonce })
)

위 코드는 idToken만 보내던 걸 rawNonce까지 함께 웹뷰로 전달하도록 바꾼 코드이다!

 

서버의 fireAuth 쪽도 rawNonce를 받아 credential에 넘기도록 같이 고쳐보았다.

// 수정 전
provider.credential({ idToken })

// 수정 후
provider.credential({ idToken, rawNonce: rawNonce || undefined })

2. auth 상태 타이밍 이슈

rawNonce를 고쳐도, setNickname 페이지로 이동한 직후엔 Firebase auth 상태가 아직 초기화 중일 수도 있다.

그 순간에 fireAuth.currentUser를 그냥 읽어버리면 이전처럼 그냥 null이 되는거다.

그래서 onAuthStateChanged로 유저가 세팅될 때까지 기다리도록 바꿔보았다.

const idToken = await new Promise<string | null>((resolve) => {
  if (fireAuth.currentUser) {
    fireAuth.currentUser.getIdToken().then(resolve).catch(() => resolve(null));
    return;
  }
  const unsub = onAuthStateChanged(fireAuth, (user) => {
    unsub();
    if (user) user.getIdToken().then(resolve).catch(() => resolve(null));
    else resolve(null);
  });
});

 

위 코드는

  1. currentUser가 이미 있으면 바로 idToken을 가져오고,
  2. 아직 없으면 onAuthStateChanged로 유저가 세팅될 때까지 기다렸다가 그 때 토큰을 가져오는 부분이다.

이 둘을 고치고 나서야 /api/auth/register가 정상 호출되었고, 닉네임 설정 후 정상적으로 다음 화면으로 넘어갔다!!!


트러블슈팅을 통해 느낀 것

이 문제를 풀어나가며 가장 크게 느낀 건,

가장 골치 아픈 버그는 에러 메세지를 뚝 띄우는 버그가 아니라

"조용히 실패하고 호출이 건너뛰어지는"버그라는 점이라고 생각한다.

 

405도 서버 문제가 아니라 앱이 결과를 받는 방식이 틀렸던거고

401도 권한 문제가 아니라 로그인이 안 된 채로 호출이 들어간 것이었다...

 

그래서 해결의 실마리는 항상 "어느 단계에서 필자가 기대한 호출이 아예 안 일어났는가"를 쫓는 데에 있다고 생각한다.

그저 메트로 로그나 console.log가 아닌, Vercel 로그와 네트워크 탭을 번갈아 보면서

호출의 존재 여부부터 확인한 게 결국 원인을 찾는 열쇠가 되지 않았나 싶다!

 

다음 글에서는 이 애플 로그인 말고도 필자가 고생했던 웹뷰에서의 구글 로그인 트러블슈팅을 다뤄보고자 한다.
 

[tolli #5] 웹뷰에서 구글로그인 구현하기

💡 읽기 전 필요한 지식 💡- 웹뷰(앱 안에 띄우는 브라우저 창)에서 웹 페이지가 돌아간다는 정도의 감 (2편 참고)- Firebase 로그인, idToken(구글이 서명한 임시 토큰)이라는 개념에 대한 가볍운 이

bbin-guuuu.tistory.com

 

728x90

'프로젝트' 카테고리의 다른 글

[tolli #5] 웹뷰에서 구글로그인 구현하기  (0) 2026.07.01
[tolli #3] switch로 쪼갠 화면이 협업 단위가 됐다  (0) 2026.06.22
[tolli #2] 왜 React Native + WebView 하이브리드였을까?  (0) 2026.06.20
[tolli #1] 무거운 성경암송 앱이 싫다  (0) 2026.06.16
'프로젝트' 카테고리의 다른 글
  • [tolli #5] 웹뷰에서 구글로그인 구현하기
  • [tolli #3] switch로 쪼갠 화면이 협업 단위가 됐다
  • [tolli #2] 왜 React Native + WebView 하이브리드였을까?
  • [tolli #1] 무거운 성경암송 앱이 싫다
프론트 개발자 김현중
프론트 개발자 김현중
👋반갑습니다 저는 나눔을 실천하는 개발자 꿈나무 김현중입니다⌨️🚀
  • 프론트 개발자 김현중
    삥구의 개발블로그
    프론트 개발자 김현중
  • 전체
    오늘
    어제
    • 분류 전체보기 (116) N
      • React (39)
      • JavaScript (5)
      • TypeScript (5)
      • Next.js (13)
      • React Native (1)
      • 프로젝트 (5) N
      • 오픈소스 (1)
      • 우아한테크코스 (9)
      • 알고리즘 (5)
      • Swift (3)
      • 컴퓨터네트워크 (1)
      • Docker (1)
      • SQL (8)
      • Database (2)
      • 배포 (1)
      • Spring (9)
      • Git (1)
      • 회고 (1)
      • 컴퓨터그래픽스 (2)
      • Python (1)
      • Brew (1)
      • LangChain (2)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    회고
    우아한테크코스
    Next.js
    typescript
    데이터베이스
    ReactHooks
    springboot
    nextjs
    백준
    웹개발
    appRouter
    database
    javascript
    java
    Spring
    frontend
    프론트엔드
    react
    우테코
    Backend
  • 최근 댓글

  • 최근 글

  • 250x250
  • hELLO· Designed By정상우.v4.10.1
프론트 개발자 김현중
[tolli #4] AOS 애플로그인 트러블슈팅
상단으로

티스토리툴바