Rutina를 진행하면서 소셜로그인에서 좀 크게 오래 걸렸습니다. 문제 해결하는 과정을 통해서 expo환경에서 Oauth2.0 소셜 로그인 적용방식을 알아보겠습니다.
1. 왜 안됨?
처음에는 백엔드 개발자와 소통하며 소셜로그인을 구현해보적이 없어서 어떤 방식으로 어떻게 해야하는 전혀 알지 못했습니다.
우선 백엔드에서 처음에는 클라인언트 ID와 시크릿을 알려주었습니다. 그래서 이를 이용해서 env 파일에 적용했는데 안되었습니다. 그래서 이후에 백엔드 개발자와 소통을 하니 Ouath2.0을 이용해서 Redirect를 설정해야한다고 알려주었습니다.
2. 안전한 OAuth2 인증 방식
이 문제를 해결하기 위해 OAuth2 흐름에서는 프론트엔드가 Secret을 직접 다루지 않도록 설계합니다. 가장 표준적인 Authorization Code Grant (인가 코드 승인 방식)의 흐름은 다음과 같습니다.
- 로그인 요청: 앱(Client)에서 사용자를 소셜 로그인 화면(카카오, 구글 등)으로 보냅니다.
- 인가 코드(Authorization Code) 발급: 사용자가 로그인을 성공하면, 인증 서버는 앱에 'Access Token'이 아닌, 임시 비밀번호 같은 '인가 코드'만 전달합니다.
- 백엔드로 코드 전달: 앱은 이 인가 코드를 우리 팀의 백엔드 서버로 보냅니다.
- 토큰 교환 (Secret 사용): 백엔드 서버는 안전하게 보관 중인 Client Secret과 방금 앱에서 받은 인가 코드를 합쳐서 소셜 인증 서버에 보냅니다.
- 토큰 발급: 소셜 인증 서버는 정보가 일치하면 백엔드 서버로 최종 Access Token을 발급해 줍니다.
💡 핵심: Client Secret은 오직 안전한 우리 백엔드 서버(환경변수)에만 존재하고, 앱은 구경조차 하지 못하게 만드는 것이 핵심입니다.
위의 방식은 현재 표준으로 자리잡은 방식으로 루티나에서도 같은 방식을 사용합니다. 그렇지만 문제가 된부분은 흐름을 잘 따라하지만 결국에 마지막 토큰이 넘어오지 않았다는 점입니다.
3. Expo에서의 설정 및 구현 팁 (디버깅 삽질기)
자 여기서 oauth2.0 방식의 흐름에 따라서 확인해봐야합니다.
- 우선 백엔드에서 테스트 할때는 url로는 로그인이 되는가?
- 웹에서 URL로 들어가서 로그인을 진행할때는 문제가 없이 잘 진행이 된다는 점에서 현재 URL 요청에는 문제가 없음을 확인
- 루티나 앱에서 로그인은 진행이 되는가?
- 소셜 로그인 화면을 통해서 로그인이 잘 진행은 됨
- 그 다음으로 로그인 진행 후 리다일렉트가 되지 않으니 데이터 베이스에서 로그인한 유저의 정보가 넘어가는 것을 보아 로그인 요청자체에는 문제가 없음을 화거인
- 리다이렉트가 되고 있는가?
- 로그인은 진행 되었지만 앱에서 로그인 확인하는 로직이 실행되지 않는 것을 보아서 리다이렉트 쪽에서 문제가 있음을 확인함
위의 순서를 거치며 결국 리다이렉트 URL에 문제가 있다는 것을 확신했습니다. 이 지점에서 백엔드 개발자분과 정말 수많은 시행착오를 겪으며 URL을 고치고 또 고쳤는데요. 저희가 정답을 찾아간 과정을 요약하면 이렇습니다.
❌ 1단계: 웹 환경용 리다이렉트의 늪
처음에는 백엔드 개발자분도, 저도 모바일 환경에 대한 이해가 부족했습니다. 백엔드에서 테스트할 때 잘 작동하던 http://localhost:3000/callback이나 https://api.rutina.com/oauth/callback 같은 일반적인 웹 URL을 리다이렉트 주소로 설정해 두고 앱에서 테스트를 하니, 로그인은 성공하지만 앱으로 돌아오지 못하고 웹 브라우저 공중에 붕 떠버리는 현상이 발생했습니다. 모바일 앱은 웹 브라우저가 아니기 때문에 도메인 주소(http://)를 해석해서 자신을 열어줄 수 없다는 걸 깨달았습니다.
❌ 2단계: 일반 모바일 환경 리다이렉트 시도
"아, 모바일은 딥링크(Deep Link)를 써야 하는구나!"를 깨닫고, 앱의 고유 주소인 '앱이름':// 형태의 스턴트를 시도했습니다. 네이티브 환경에 맞춰 리다이렉트 주소를 등록하고 백엔드와 맞췄지만, 여전히 Expo 앱에서는 반응이 없거나 에러가 났습니다.
⭕ 3단계: 드디어 해결! 'Expo 환경' 맞춤형 리다이렉트
결정적인 원인은 저희가 시뮬레이터나 테스트 기기에서 'Expo Go' 환경을 쓰고 있다는 점이었습니다. 최종 빌드된 앱에서는 rutina://라는 커스텀 스킴이 바로 작동하지만, 개발 단계에서 쓰는 Expo Go 앱은 일종의 '대리인' 역할을 하기 때문에 Expo가 주선해 주는 특수한 프록시 주소(https://auth.expo.io/@개인계정/rutina-app)를 리다이렉트 URI로 사용해야만 정상적으로 인증 코드를 들고 앱으로 복귀(Redirect)할 수 있었던 것입니다.
🛠️ 실제 루티나(Rutina) 프로젝트에 적용한 해결책
이 3단계의 시행착오 끝에 정립한 Expo 소셜 로그인 설정 및 코드 공유합니다.
① app.json에 Scheme(딥링크) 등록하기
우선 최종 빌드 및 내장 브라우저 식별을 위해 app.json에 우리 앱만의 고유한 스킴을 등록해 줍니다.
{
"expo": {
"name": "Rutina",
"slug": "rutina-app",
"scheme": "rutina", // 외부에서 "rutina://"로 호출하면 우리 앱이 열리도록 설정
...
}
}
② expo-auth-session 및 WebBrowser로 URI 동적 처리
개발 환경(Expo Go)과 실제 운영 환경의 리다이렉트 주소 차이를 매끄럽게 해결하기 위해 Expo의 인증 세션 라이브러리를 활용했습니다.
import * as WebBrowser from 'expo-web-browser';
import * as AuthSession from 'expo-auth-session';
import { useEffect } from 'react';
// 웹브라우저 창이 열리고 인증이 완료되면 세션을 닫아주는 필수 설정
WebBrowser.maybeCompleteAuthSession();
export default function LoginScreen() {
// 🔥 핵심: Expo가 현재 개발(Expo Go)인지 프로덕션인지를 감지하여
// 알맞은 Redirect URI를 알아서 동적으로 생성해 줍니다!
const redirectUri = AuthSession.makeRedirectUri({
scheme: 'rutina'
});
// 백엔드에 로그인 요청을 보낼 때, 위에서 만든 리다이렉트 주소를 파라미터로 함께 전송
const authUrl = `https://api.rutina.com/oauth2/authorization/kakao?redirect_uri=${redirectUri}`;
const handleLogin = async () => {
// 앱 내 내장 인앱브라우저(WebBrowser)로 소셜 로그인 창 오픈
const result = await WebBrowser.openAuthSessionAsync(authUrl, redirectUri);
if (result.type === 'success' && result.url) {
// 리다이렉트된 URL에서 백엔드가 최종 발급해준 토큰(JWT 등) 추출
const params = new URL(result.url).searchParams;
const userToken = params.get('token');
// 우리 서비스 로그인 성공 처리!
console.log("루티나 로그인 성공! 토큰:", userToken);
}
};
return (
<Button title="카카오로 시작하기" onPress={handleLogin} />
);
}
③ 백엔드 개발자 및 소셜 콘솔 설정 맞추기
마지막으로 백엔드 개발자분에게 앱에서 동적으로 생성되는 리다이렉트 URI(개발용 Expo 주소 및 실서버용 rutina:// 주소)를 전달하여, 백엔드 서버 환경변수와 카카오/구글 개발자 센터의 'Valid Redirect URIs' 목록에 빠짐없이 등록해 줌으로써 기나긴 삽질을 끝낼 수 있었습니다.
4. 마치며 (회고)
처음에는 백엔드 개발자분이 주신 Client Secret을 무작정 앱 화면에 넣고 "왜 안 되지?"라며 삽질을 거듭했습니다. 하지만 오류를 하나씩 뜯어보며 OAuth 2.0의 보안 구조를 이해하고, 특히 웹 ➡️ 모바일 ➡️ Expo 개발 환경으로 이어지는 리다이렉트 매커니즘의 차이를 직접 몸으로 부딪치며 배울 수 있었던 정말 값진 경험이었습니다.
'react-native > rutina' 카테고리의 다른 글
| 📅 [04.01] 타임테이블 동적 달력 연동 & 제스처 UI 구현 (메모용) (0) | 2026.04.01 |
|---|---|
| 🛠️ [03.25] 타임테이블 수학적 그리드 설계 & 설정 UI 컴포넌트화 (0) | 2026.04.01 |
| 프로젝트 라이브러리 정리(기록용) (0) | 2026.04.01 |