1. 작업 배경
이번 작업은 본인 인증 기능을 처음부터 설계한 사례가 아니라, 기존에 구현되어 있던 기능에서 발생한 오류를 분석하고 수정한 유지보수 사례입니다. 프로젝트는 React Native 앱의 공통 WebView에서 Vue로 구현된 웹 화면을 제공하고 있었으며, 해당 화면에는 N*** CheckPlus 본인 인증이 이미 연동되어 있었습니다.
웹 브라우저와 iOS 앱에서는 정상 동작했지만, React Native Android WebView에서만 인증이 실패했습니다. 담당 범위는 기존 연동 구조를 전면 변경하는 것이 아니라, 플랫폼별 차이를 기준으로 원인을 좁히고 공통 WebView의 다른 기능에 미치는 영향을 최소화하면서 인증 흐름을 정상화하는 것이었습니다.
2. 오류 발생과 분석 단서
2.1 확인된 오류
Android 앱의 WebView에서 N*** 본인 인증을 진행하면 인증이 완료되지 않고 실패 페이지로 이동했습니다. 당시 기록에는 다음 오류가 남아 있었습니다.
SecurityError: Failed to read a named property 'checkSuccess' from 'Window':
Blocked a frame with origin "https://n***.checkplus.co.kr"
from accessing a cross-origin frame.
이 메시지는 서로 다른 출처(Origin)의 Window 또는 Frame 사이에서 상대 창의 속성에 직접 접근하려다가 동일 출처 정책(Same-Origin Policy)에 의해 차단되었다는 의미입니다. 다만 checkSuccess는 자체 프로젝트에서 작성한 함수가 아니라 N*** 인증 화면 내부에서 사용된 이름으로 보였으므로, 자체 Vue 코드에서 해당 함수를 직접 수정하는 방식으로 접근할 수는 없었습니다.
또한 화면에 마지막으로 표시된 예외가 최초 실패 원인이라고 단정하지 않았습니다. 인증 흐름이 앞 단계에서 이미 실패한 뒤, 실패 페이지의 후속 스크립트가 실행되는 과정에서 교차 출처(Cross-Origin) 예외가 발생했을 가능성이 있었기 때문입니다.
2.2 실행 환경과 URL 이동 흐름 비교
동일 기능을 실행 환경별로 비교한 결과, 웹 브라우저, 같은 Android 기기의 일반 브라우저, iOS 앱에서는 정상적으로 진행되었고 React Native Android WebView에서만 실패했습니다. 이 차이를 통해 N*** 서비스 전체나 Vue 비즈니스 로직보다 Android WebView의 URL 처리 흐름을 우선 확인했습니다.
실패 직전 기록에서는 다음 URL 이동 흐름을 확인했습니다.
/cert/mobileCert/main
> /cert/mobileCert/method
> /cert/mobileCert/fail/applink
> SecurityError 발생
SecurityError보다 먼저 /fail/applink 경로를 거쳤다는 점이 핵심 단서였습니다. 이 기록만으로 N*** 내부의 정확한 실패 원인을 확정할 수는 없지만, 마지막 Cross-Origin 예외보다 앞선 외부 인증 앱 호출 단계가 먼저 실패했는지 확인할 필요가 있었습니다.
3. 기존 코드 확인과 원인 범위 좁히기
3.1 실제 수정 위치 확인
Android에서만 발생한 문제이므로 처음에는 Java 또는 Kotlin의 WebViewClient를 수정해야 하는지 검토했습니다. 그러나 프로젝트의 Android Activity는 WebView를 직접 생성하는 구조가 아니었고, 실제 화면은 react-native-webview를 감싼 TypeScript 공통 컴포넌트에서 구성하고 있었습니다.
따라서 수정 지점은 Android Activity가 아니라 React Native 공통 WebView 컴포넌트였습니다. Android에서 발생한 오류라도 해당 프로젝트가 네이티브 기능을 어느 계층에서 제어하는지 먼저 확인해야 불필요한 수정 범위를 줄일 수 있었습니다.
3.2 기존 설정 및 자체 콜백 흐름 분리
기존 공통 WebView에는 originWhitelist={['*']}, JavaScript, DOM Storage, 멀티윈도우 및 플랫폼별 쿠키 관련 설정이 이미 적용되어 있었습니다. Android 기준으로 thirdPartyCookiesEnabled, domStorageEnabled, setSupportMultipleWindows 등이 존재했기 때문에 단순한 쿠키나 저장소 옵션 누락을 우선 원인으로 보기는 어려웠습니다.
originWhitelist는 WebView가 허용할 탐색 대상의 범위를 지정하는 속성이며, 동일 출처 정책을 해제하거나 외부 앱 실행을 보장하는 설정은 아닙니다. 따라서 설정값만으로 원인을 단정하지 않고, 당시 확인한 공통 컴포넌트에 외부 앱 URL을 명시적으로 분기하는 처리기가 없었다는 사실을 기준으로 범위를 좁혔습니다.
프로젝트에는 PC 팝업 결과 페이지가 window.opener.callbackEncodeData()를 호출하는 별도 흐름도 있었습니다. 반면 모바일 앱은 폼을 _self로 제출한 뒤 돌아온 페이지에서 EncodeData를 처리하는 구조였고, 이번 오류에 표시된 함수명은 checkSuccess였습니다. 따라서 PC 팝업 콜백 문제와 Android WebView의 외부 앱 호출 문제를 동일한 원인으로 혼합하지 않았습니다.
멀티윈도우 설정 변경도 후보로 검토했지만 실제 해결에는 적용하지 않았습니다. 정상 동작을 확인한 조치만 해결 내용에 포함했습니다.
3.3 외부 앱 커스텀 스킴 처리 확인
N*** 본인 인증 과정에서 P*** 등 외부 인증 앱을 실행할 때는 일반적인 http:// 또는 https:// 주소가 아니라 intent: URI나 tauthlink: 같은 커스텀 스킴을 사용할 수 있습니다. 당시 작업 메모에도 민감한 파라미터를 제외한 tauthlink://sktauth?... 형태가 남아 있었습니다.
일반 모바일 브라우저는 이러한 URL을 운영체제의 앱 실행 흐름으로 넘길 수 있지만, 앱 내부 WebView에서는 URL 이동 요청을 가로채 React Native 또는 네이티브 계층으로 전달해야 하는 경우가 있습니다.
당시 확인한 공통 WebView 컴포넌트에는 쿠키와 창 관련 설정은 있었지만, HTTP가 아닌 인증 앱 URL을 명시적으로 구분해 외부 앱으로 전달하는 처리기는 없었습니다. 플랫폼별 재현 결과, /fail/applink 이동 기록, 기존 코드의 누락 지점을 종합해 Android 외부 인증 앱 URL 처리부터 보완했습니다.
4. 해결 방식 적용
4.1 처리 기준
onShouldStartLoadWithRequest를 사용해 인증 도중 발생하는 URL 이동 요청을 확인하고, WebView에서 계속 처리할 요청과 운영체제에 넘길 외부 앱 요청을 분리했습니다. 이번 문제의 URL은 최초 로드가 아니라 인증 과정 중 발생한 이동 요청이었으므로 이 처리 방식으로 대응할 수 있었습니다.
처리 기준은 다음과 같이 정리했습니다.
-
http://, https://, about:blank는 WebView에서 계속 처리합니다.
-
Android에서 확인한 인증 앱 URL(intent:, tauthlink:)만 외부 앱 실행 대상으로 허용합니다.
-
외부 앱으로 전달한 URL은 WebView가 중복 로드하지 않도록 false를 반환합니다.
-
허용 목록에 없는 스킴은 실행하지 않고 WebView 로드도 중단합니다.
-
iOS에서는 이 처리기에서 별도의 Linking 호출을 수행하지 않습니다.
4.2 핵심 코드
아래 코드는 당시 적용한 핵심 처리 구조를 이해하기 쉽도록 단순화한 예시입니다.
const handleShouldStartLoadWithRequest = (
{url}: {url: string},
): boolean => {
if (/^(https?:\/\/|about:blank(?:#.*)?$)/i.test(url)) return true;
if (Platform.OS !== 'android') return true;
const isIntentUrl = /^intent:/i.test(url);
const isTAuthUrl = /^tauthlink:/i.test(url);
if (!isIntentUrl && !isTAuthUrl) return false;
const targetUrl = isIntentUrl ? parseIntentUrl(url) : url;
if (!targetUrl) {
showAuthAppError();
return false;
}
void Linking.openURL(targetUrl).catch(showAuthAppError);
return false;
};
핵심은 일반 웹 URL과 허용된 Android 인증 앱 URL을 분리하고, Linking.openURL()로 외부 앱 실행을 요청한 뒤 WebView의 해당 이동을 중단하는 것입니다.
parseIntentUrl()은 당시 확인한 intent: 형식에서 실제 앱 스킴 URL을 구성하는 보조 함수입니다. 당시 메모에서는 앱 실행 실패 시 getFallbackUrl()로 S.browser_fallback_url을 확인하는 별도 분기도 두었습니다. 위 예시에는 URL 이동을 분리하는 핵심 구조만 남겼으며, 두 함수는 모든 Android Intent URI를 해석하는 범용 파서가 아니라 실제 서비스에서 확인한 형식에 맞춰 사용 범위를 제한해야 합니다.
순수 커스텀 스킴인 tauthlink:에는 앱 패키지나 대체 URL 정보가 없을 수 있으므로, 실행 실패 시 안내만 표시할지, 스킴-패키지 매핑을 관리해 스토어로 이동할지는 별도 정책으로 결정해야 합니다.
공통 컴포넌트가 ...rest로 외부 WebView 속성을 전달하는 구조라면 기존 onShouldStartLoadWithRequest가 있는지 확인해야 합니다. 단순히 속성 순서로 한쪽을 덮어쓰기보다, 기존 처리기와 공통 처리기의 반환 결과를 합성하는 방식이 안전합니다.
5. 재테스트 결과
당시 수정 후, 기존에 실패했던 Android 환경에서 N*** 본인 인증을 다시 진행했습니다. 일반 https:// 인증 페이지는 WebView에서 계속 열렸고, 인증 앱 커스텀 스킴은 WebView가 직접 로드하지 않고 외부 앱 실행 요청으로 전달되었습니다. 그 결과 P*** 등 외부 인증 앱 호출과 N*** 본인 인증 절차가 이어졌고, 기존의 /fail/applink 및 SecurityError가 이어지는 실패 흐름은 동일 시나리오에서 더 이상 재현되지 않았습니다.
이 결과는 커스텀 스킴 처리 로직이 동일 출처 정책을 변경했다는 의미가 아닙니다. 외부 앱 호출이 정상 경로로 이어지면서 /fail/applink의 후속 스크립트가 실행되는 실패 경로에 진입하지 않게 되었고, 그 결과 뒤따르던 Cross-Origin 예외도 나타나지 않은 것으로 해석했습니다.
다만 커스텀 스킴 처리 누락이 SecurityError의 유일한 내부 원인이었다고 단정하지는 않았습니다. N*** 내부에서 checkSuccess가 호출되는 전체 구조를 직접 확인한 것은 아니기 때문입니다. 당시 기록으로 확인할 수 있는 사실은 다음과 같습니다.
-
Android 앱의 WebView에서만 인증이 실패했습니다.
-
실패 시 /fail/applink 경로를 거친 뒤 SecurityError가 나타났습니다.
-
당시 확인한 공통 WebView에는 외부 인증 앱 URL을 명시적으로 처리하는 로직이 없었습니다.
-
해당 처리를 추가한 뒤 동일한 인증 시나리오가 정상화되었습니다.
따라서 이번 사례는 “Cross-Origin 정책을 우회해 오류를 제거했다”가 아니라, “Android WebView의 외부 인증 앱 URL 처리 로직을 보완한 뒤, 앞선 실패 경로와 후속 Cross-Origin 예외가 동일 시나리오에서 재현되지 않았다”로 정리하는 것이 정확합니다.
6. 적용 시 고려 사항
공통 WebView에 URL 이동 처리기를 추가하면 N*** 이외의 화면도 동일한 로직을 거칠 수 있습니다. HTTP가 아닌 모든 URL을 외부 앱으로 전달하면 의도하지 않은 앱 실행이나 기존 기능 변경이 발생할 수 있으므로, 인증 화면 또는 인증 진행 상태로 적용 범위를 제한하고 확인된 스킴만 허용하는 방식이 안전합니다.
intent: URI를 범용적으로 지원해야 한다면 내부의 실제 스킴, 패키지, 대체 URL 도메인도 허용 목록으로 검증해야 합니다. 인증 URL 전체를 로그에 남길 경우 식별자나 토큰이 포함될 수 있으므로, 스킴과 호스트 등 필요한 정보만 기록하고 민감한 파라미터는 마스킹해야 합니다.
7. 작업 과정에서 얻은 경험
7.1 마지막 오류보다 앞선 흐름을 확인해야 합니다
처음 확인한 메시지는 Cross-Origin SecurityError였지만, URL 이동 기록에서는 그 전에 /fail/applink 경로로 이동하고 있었습니다. 마지막 예외만 직접 수정하려 했다면 외부 앱 호출이 먼저 실패한 정황을 놓칠 수 있었습니다. 장애 분석에서는 예외에 도달하기 전의 URL 이동과 상태 변화도 함께 확인해야 합니다.
7.2 플랫폼 비교로 분석 범위를 줄일 수 있습니다
웹 브라우저와 iOS 앱에서는 정상이고 React Native Android WebView에서만 실패한다는 사실을 통해 분석 대상을 빠르게 줄일 수 있었습니다. 같은 Android 기기의 일반 브라우저와 앱 WebView를 비교한 것도 운영체제 자체와 WebView 통합 방식의 차이를 구분하는 데 도움이 되었습니다.
7.3 적용한 조치와 검토한 대안을 구분해야 합니다
분석 과정에서는 창 처리 방식, 멀티윈도우 설정 등 여러 후보를 검토했습니다. 그러나 정상 동작을 확인한 핵심 변경은 onShouldStartLoadWithRequest에서 허용된 외부 앱 URL을 구분해 Linking으로 전달한 처리였습니다. 기술 사례를 정리할 때는 실제 적용·검증한 조치와 가능성만 검토한 대안을 구분해야 결과를 과장하지 않을 수 있습니다.
8. 마무리
이번 작업은 기존 React Native 하이브리드 앱에서 Android WebView에만 발생한 N*** 본인 인증 오류를 분석하고 개선한 사례입니다. Cross-Origin 오류에서 출발했지만, 플랫폼별 재현 결과, 실패 직전 URL 이동 흐름, 기존 공통 WebView 설정과 자체 콜백 구조를 순서대로 확인하면서 외부 인증 앱 URL 처리로 분석 범위를 좁혔습니다.
당시 기록을 기준으로 일반 웹 URL과 Android 인증 앱 URL을 분리하고, 허용된 스킴을 React Native Linking으로 전달하도록 보완했습니다. 수정 후 동일한 실패 시나리오에서 외부 인증 앱 호출과 N*** 본인 인증이 정상적으로 이어졌으며, /fail/applink 및 후속 SecurityError는 더 이상 재현되지 않았습니다.
이 사례를 통해 화면에 마지막으로 나타난 오류만 직접 수정하기보다, 오류 이전의 URL 이동과 웹-네이티브 경계에서 실제로 어떤 처리가 누락되었는지 확인하는 것이 중요하다는 점을 배웠습니다. 또한 확인된 사실과 추론을 구분해 기술 사례의 결론을 과장하지 않는 것 역시 중요했습니다.
green