
모바일 환경에서 본인 인증 플로우를 개선하며 겪었던 문제와 해결 과정을 정리합니다.
문제 상황: 모바일에서 작동하지 않는 팝업 기반 인증
초기 구현은 데스크톱 환경을 중심으로 설계되었습니다. window.open()으로 외부 인증 페이지를 팝업 창으로 띄우고, 인증이 완료되면 window.opener를 통해 부모 창으로 결과를 전달하는 방식이었습니다.
하지만 모바일 환경에서는 여러 문제가 발생했습니다:
- 모바일 브라우저:
window.open()이 팝업 대신 전체 페이지 리다이렉트로 동작하거나 차단됨 - 웹뷰(WebView) 환경: 카카오톡, 네이버 앱 등의 인앱 브라우저에서는
window.open()을 아예 지원하지 않거나 차단됨
결국 사용자는 인증을 완료했음에도 원본 페이지에서 아무 반응을 얻지 못하는 불편한 경험을 하게 되었습니다.
해결 방안: 리다이렉트 기반 인증 플로우 추가
이 문제를 해결하기 위해 모바일 환경에 최적화된 인증 플로우를 재설계했습니다. 핵심은 window.open 대신 페이지 리다이렉트를 사용하고, sessionStorage로 상태를 안전하게 유지하는 것이었습니다.
핵심 기술: sessionStorage를 활용한 상태 관리
리다이렉트 과정에서 데이터를 유지하기 위해 sessionStorage를 활용했습니다. sessionStorage는 각 브라우저 탭에 고유하게 데이터가 유지되며, 페이지 리다이렉트나 새로고침에도 보존되지만 탭을 닫으면 자동으로 삭제되는 특성이 있습니다.
- 인증 시작: 현재 페이지 URL을
sessionStorage에 저장 후 인증 페이지로 리다이렉트 - 인증 완료: 콜백 페이지에서 인증 결과를
sessionStorage에 저장 - 원본 페이지 복귀: 저장된 URL로 리다이렉트
- 결과 처리: 원본 페이지에서
sessionStorage의 인증 결과를 읽어 처리 후 즉시 삭제
구현 포인트: 모바일 친화적인 본인 인증 플로우 구축
이제 위에서 설명한 개념들이 실제 코드에 어떻게 적용되었는지, Before/After 코드를 통해 확인합니다.
1. useIdentityAuth 컴포저블: 환경에 따른 인증 시작 로직 분리
가장 먼저, 본인 인증 시작을 담당하는 useIdentityAuth 컴포저블을 개선했습니다. 기존에는 window.open만을 고려했지만, 이제는 모바일 환경을 감지하여 전체 페이지 리다이렉트 방식으로 전환하도록 했습니다.
useIdentityAuth (이전 코드)
<script setup>
export default function useIdentityAuth(generateAuthUrl) {
async function startAuth() {
const popupRef = window.open(
"",
"authPopup",
"width=420,height=550,resizeable,scrollbars"
);
if (popupRef && !popupRef.closed) {
const authUrl = await generateAuthUrl();
popupRef.location.href = authUrl;
return;
}
// 팝업 차단 경고 처리 로직 (생략)
...
}
return {
startAuth,
};
}
</script>이전 코드에서는 startAuth 함수가 오직 window.open을 사용한 팝업 방식에만 의존했습니다. 데스크톱 환경에서는 이 방식이 잘 작동했지만, 모바일에서는 팝업이 제대로 동작하지 않는 문제가 있었습니다.
useIdentityAuth (개선된 코드)
<script setup>
import { useBrowserSize } from "./browserSize";
export default function useIdentityAuth(generateAuthUrl) {
const { isDesktop } = useBrowserSize();
async function startAuth() {
const authUrl = await generateAuthUrl();
if (!isDesktop.value) {
// 모바일 환경: 페이지 리다이렉트
sessionStorage.setItem("auth_return_path", window.location.href);
window.location.href = authUrl;
return;
}
// 데스크톱 환경: 팝업 창
const popupRef = window.open(
"",
"authPopup",
"width=420,height=550,resizeable,scrollbars"
);
if (popupRef && !popupRef.closed) {
popupRef.location.href = authUrl;
return;
}
// 팝업 차단 경고 처리 로직 (생략)
...
}
return {
startAuth,
};
}
</script>개선된 useIdentityAuth 컴포저블은 useBrowserSize 훅을 통해 현재 환경이 데스크톱인지 모바일인지 감지합니다.
- 모바일 환경(
!isDesktop.value):sessionStorage.setItem()을 사용하여 현재 페이지의 URL을 저장합니다. 이 URL은 인증 완료 후 돌아와야 할 페이지의 주소입니다. 그 후,window.location.href를 인증 URL로 변경하여 전체 페이지 리다이렉트를 수행합니다. - 데스크톱 환경: 이전과 동일하게
window.open을 사용하여 팝업 창을 띄웁니다.
이 컴포저블은 인증 시작에 필요한 환경별 분기 처리 로직을 캡슐화하여 재사용성을 높여줍니다. 인증 결과와 돌아올 경로는 sessionStorage를 직접 사용하여 저장하고 조회합니다.
2. AuthCallbackView: 인증 결과 처리 로직의 이중화
외부 인증 서비스에서 인증을 완료하면, 결과는 애플리케이션 내의 콜백 페이지로 쿼리 파라미터와 함께 리다이렉트됩니다. 이 페이지는 데스크톱 팝업과 모바일 리다이렉트 두 가지 환경을 모두 처리할 수 있어야 합니다.
Before:AuthCallbackView (이전 코드)
<script setup>
import { useRoute } from "vue-router";
import { onMounted } from "vue";
const route = useRoute();
onMounted(() => {
const authResult = route.query.authResult;
if (authResult) {
// 무조건 window.opener에 의존하여 결과 전달
window.opener.handleAuthResult(authResult);
window.close();
}
});
</script>이전 콜백 페이지는 onMounted 훅에서 쿼리 파라미터의 인증 결과를 파싱한 후, 무조건 window.opener.handleAuthResult를 호출하여 부모 창에 결과를 전달하고 팝업 창을 닫았습니다. 이 방식은 모바일 환경에서는 제대로 동작하지 않았습니다.
AuthCallbackView (개선된 코드)
<script setup>
import { useRoute } from "vue-router";
import { onMounted } from "vue";
const route = useRoute();
onMounted(() => {
const authResult = route.query.authResult;
if (authResult) {
if (window?.opener?.handleAuthResult) {
// 데스크톱 팝업 환경
window.opener.handleAuthResult(authResult);
window.close();
} else {
// 모바일 리다이렉트 환경
sessionStorage.setItem("auth_result", authResult);
const returnPath = sessionStorage.getItem("auth_return_path");
if (returnPath) {
window.location.href = returnPath;
}
}
}
});
</script>
<template>
<div>인증 처리 중...</div>
</template>개선된 콜백 페이지는 window.opener의 존재 여부를 확인하여 데스크톱과 모바일 환경을 명확히 구분합니다.
- 데스크톱 팝업 환경:
window.opener가 존재하면, 기존처럼window.opener.handleAuthResult를 호출하고 팝업 창을 닫습니다. - 모바일 리다이렉트 환경:
window.opener가 없으면,sessionStorage.setItem()을 사용해 인증 결과를 저장합니다. 이후sessionStorage.getItem()으로 이전에 저장했던 원본 페이지 URL을 가져와 다시 리다이렉트합니다. 이로써 원본 페이지는sessionStorage에서 인증 결과를 읽어 처리할 수 있게 됩니다.
3. 인증 페이지: 사용자 입력 데이터 유실 방지 및 결과 처리
인증이 필요한 페이지(예: 회원가입, 본인 확인 등)에서는 사용자가 정보를 입력한 후 인증을 시작합니다. 모바일 환경에서 페이지 리다이렉트가 발생하면, 인증 전 입력했던 데이터가 유실될 수 있습니다. 이를 방지하고, 인증 완료 후 sessionStorage에 저장된 결과를 처리하는 로직을 추가했습니다.
<script setup>
import { useRoute } from "vue-router";
import useIdentityAuth from "@/composables/useIdentityAuth";
const route = useRoute();
const { startAuth } = useIdentityAuth(() => generateAuthUrl()); // 인증 시작 버튼 클릭 시 바로 호출
function handleAuthClick() {
startAuth();
}
</script>이전 코드에서는 startAuth 함수를 직접 호출하여 인증을 시작했습니다. 별도의 중간 처리 로직 없이 바로 인증 플로우로 넘어갔기 때문에, 모바일에서 리다이렉트 후 돌아왔을 때 사용자가 입력했던 데이터가 유실될 위험이 있었습니다.
<script setup>
import { ref, onMounted } from "vue";
import { useRoute } from "vue-router";
import { useBrowserSize } from "@/composables/browserSize";
import useIdentityAuth from "@/composables/useIdentityAuth";
const route = useRoute();
const { isDesktop } = useBrowserSize();
const userData = ref({});
const { startAuth } = useIdentityAuth(() => generateAuthUrl());
onMounted(() => {
// 모바일 리다이렉트 후 돌아온 경우 세션 확인
const authResult = sessionStorage.getItem("auth_result");
const returnPath = sessionStorage.getItem("auth_return_path");
if (authResult && returnPath) {
handleAuthResult(authResult);
}
// 세션 데이터 클리어
sessionStorage.removeItem("auth_result");
sessionStorage.removeItem("auth_return_path");
// 데스크톱 팝업에서 호출될 전역 함수 설정
window.handleAuthResult = (result) => {
handleAuthResult(result);
};
});
function handleAuthClick() {
if (!isDesktop.value) {
// 모바일: 입력 데이터를 임시 저장
saveUserData(userData.value);
}
startAuth();
}
function handleAuthResult(result) {
// 인증 결과 처리 로직
console.log("인증 완료:", result);
}
</script>개선된 인증 페이지에서는 여러 중요한 변경 사항이 적용되었습니다.
handleAuthClick함수: 인증 버튼 클릭 시 모바일 환경(!isDesktop.value)을 확인합니다. 모바일일 경우,startAuth를 호출하기 전에 사용자가 입력한 데이터를 임시 저장합니다. 이는 리다이렉트 후 데이터 유실을 방지합니다.onMounted훅에서의 인증 결과 처리: 컴포넌트가 마운트될 때getAuthResult()와getReturnPath()를 확인하여sessionStorage에 저장된 인증 결과가 있는지 확인합니다. 결과가 있다면handleAuthResult함수를 호출하여 처리하고,sessionStorage의 데이터는 즉시 클리어합니다.- 전역 함수 설정: 데스크톱 팝업 방식에서
window.opener를 통해 호출될handleAuthResult함수를 전역에 정의하여, 두 가지 인증 흐름 모두를 지원하도록 했습니다.
이러한 개선을 통해, 사용자는 모바일 환경에서도 입력 데이터 유실 걱정 없이 원활하게 인증을 진행하고 서비스를 이용할 수 있게 되었습니다.
결과 및 효과, 그리고 남은 과제
인증 성공률 Before/After
데스크톱 브라우저는 기존 팝업 방식을 그대로 유지했기에 95% 이상의 성공률에 변화가 없었습니다. 차이가 극적이었던 건 모바일 쪽입니다.
모바일 브라우저(Chrome, Safari)에서는 팝업 차단과 window.opener 참조 끊김 때문에 성공률이 30~50% 수준에 머물렀는데, 리다이렉트 방식을 추가한 뒤 90% 이상으로 올라갔습니다. 인앱 브라우저(카카오톡, 네이버 앱 등)는 더 극단적이었습니다. 팝업 자체를 지원하지 않아 사실상 인증 진행이 불가능했던 환경이 85~90% 성공률로 정상 동작하게 되었습니다.
사용자 입력 데이터 보존율도 개선이 컸습니다. 기존에는 모바일에서 리다이렉트가 발생하면 입력 데이터가 통째로 유실되었지만, sessionStorage 임시 저장을 도입한 뒤 95% 이상 보존되는 것을 확인했습니다.
환경별 테스트 결과
다양한 모바일 환경에서의 테스트 결과를 정리합니다.
| 테스트 환경 | window.open() 지원 | window.opener 지원 | 리다이렉트 방식 동작 | sessionStorage 유지 |
|---|---|---|---|---|
| Chrome (Android) | 팝업 차단 기본값 | 제한적 | 정상 | 정상 |
| Safari (iOS) | 팝업 차단 기본값 | 제한적 | 정상 | 정상 |
| Samsung Internet | 팝업 차단 기본값 | 제한적 | 정상 | 정상 |
| 카카오톡 인앱 브라우저 | 미지원 | 미지원 | 정상 | 정상 |
| 네이버 앱 인앱 브라우저 | 미지원 | 미지원 | 정상 | 정상 |
| Instagram 인앱 브라우저 | 미지원 | 미지원 | 정상 | 정상 |
핵심 발견 사항은, 대부분의 모바일 및 인앱 브라우저에서 window.open()과 window.opener는 신뢰할 수 없지만 sessionStorage와 페이지 리다이렉트는 안정적으로 동작한다는 점이었습니다. 이 테스트 결과가 리다이렉트 기반 인증 플로우를 채택하게 된 핵심 근거가 되었습니다.
이번 본인 인증 흐름 개선 작업을 통해 모바일 환경에서의 사용자 경험을 획기적으로 향상시켰습니다. 더 이상 팝업 차단이나 환경별 동작 차이로 인해 본인 인증이 중단되거나, 사용자가 다시 처음부터 시작해야 하는 불편함을 겪지 않게 되었습니다. sessionStorage를 활용하여 리다이렉트 간 상태를 안정적으로 유지하고, 사용자 입력 데이터 유실을 방지함으로써 서비스의 신뢰성을 높일 수 있었습니다.
남은 과제로는 sessionStorage에 저장되는 데이터의 보안성을 더욱 강화하는 방안을 지속적으로 모색하는 것입니다. 현재도 사용 후 즉시 삭제하는 로직을 적용하고 있지만, 중요한 정보의 경우 암호화 같은 추가적인 보안 조치를 고려해볼 수 있습니다. 또한, 다양한 모바일 브라우저 환경에서의 호환성 테스트를 더욱 면밀히 진행하여 모든 사용자에게 일관된 경험을 제공하는 것도 중요하다고 생각합니다.
회고
window.open과 같은 웹 표준 API도 환경에 따라 다르게 동작할 수 있으며, 개발 시 특정 플랫폼의 특성을 정확히 파악하는 것이 중요합니다.
복잡한 사용자 흐름에서는 상태를 안전하게 유지하는 전략이 핵심입니다. 리다이렉트나 백/포워드 버튼 사용 등 모든 시나리오를 고려하여 데이터 유실 없이 매끄러운 경험을 제공해야 합니다. sessionStorage와 같은 클라이언트 사이드 스토리지를 적재적소에 활용하는 것이 효과적이었습니다.
참고 자료
Related Posts

웹뷰 환경에서의 구글 소셜 로그인 구현
웹뷰의 팝업 차단과 리다이렉트 제약을 극복하고, JavaScript-Native Bridge와 Android Intent Deep Link를 활용하여 안정적인 소셜 로그인 플로우를 구현한 과정을 다룹니다.

통합 로그인 시스템 구축기: 소셜 로그인과 이메일 로그인을 하나로
네이버, 구글 소셜 로그인과 이메일 계정 시스템을 통합하여 일관된 사용자 경험을 제공하고 신규 회원 가입 플로우를 단일화한 과정을 다룹니다.

웹 애플리케이션의 외부 서비스 연동 보안 강화: postMessage 데이터 검증 사례
브라우저 확장 프로그램으로 인한 불완전한 데이터 유입 문제를 발견하고, postMessage 통신의 데이터 검증 로직을 강화하여 외부 서비스 연동의 안정성을 향상시킨 과정을 다룹니다.