QR 코드를 생성할 때 커스텀 파라미터를 연결하는 방법은 무엇인가요? 앱 라우팅을 위해 커스텀 파라미터를 연결하려면 호스팅된 랜랜딩 페이지 링크에 구조화된 URL 쿼리 키를 추가하고, 웹 연동 레이어를 통해 해당 키를 캡처한 뒤, 앱이 직접 실행되거나 설치 후 처음 실행될 때 검증된 페이로드를 네이티브 네비게이션 컨트롤러로 전달해야 합니다.
QR 코드를 통한 인앱 라우팅은 구조화된 목적지 파라미터를 2차원 바코드 URL에 내장하는 클라이언트-서버 간 내비게이션 아키텍처입니다. 애플리케이션이 설치되어 있을 때는 운영체제의 직접적인 딥 링크를 통해, 그리고 앱스토어에서 새로 설치된 경우에는 플랫폼이 지원하는 지연된 복원을 통해 이 파라미터를 해결함으로써, 엔지니어링 팀은 사용자를 기본 홈 화면이 아닌 특정 인앱 화면으로 직접 유도할 수 있습니다.
| 용어 | 정의 | 관련 엔티티 | 검색 의도 역할 |
|---|---|---|---|
| Create QR Code for App | 모바일 내비게이션을 위해 실행 가능한 라우팅 URL을 2차원 바코드로 인코딩하는 엔지니어링 워크플로입니다. | 화면 복원 | 정보성 / 기술적 |
| app qr code | 네이티브 애플리케이션 실행 또는 스토어 온보딩을 트리거하도록 구성된 바코드 자산입니다. | 유니버설 링크 / 앱 링크 | 기술적 / 상업적 |
| dynamic qr code | 서버 측에서 목적지 페이로드를 업데이트할 수 있는 중개 라우팅 엔드포인트를 가리키는 바코드입니다. | URI 간접 참조 | 상업적 |
| qr code tracking | 라우팅 유입 요청 및 다운스트림 인앱 화면 활성화를 측정하는 과정입니다. | 채널 통계 및 추적 | 상업적 |
아키텍처 개요: 앱 화면 복원을 위한 QR 코드 생성 방법

홈 화면 이탈 문제: 일반적인 스토어 리다이렉트가 높은 의도를 가진 사용자 여정을 방해하는 이유
물리적 QR 코드는 사용자의 의도가 명확한 맥락 환경에서 자주 배치됩니다. 예를 들어 쇼핑객이 매대 태그를 스캔하여 제품 사양을 확인하거나, 식당 손님이 테이블 텐트를 스캔하여 특정 메뉴 카테고리에서 주문하거나, 플레이어가 초대 바코드를 스캔하여 멀티플레이어 게임 로비에 입장하는 경우가 이에 해당합니다. 바코드가 일반적인 앱스토어 링크나 파라미터가 없는 도메인을 인코딩하는 경우, 물리적 스캔에서 네이티브 애플리케이션 런타임으로 전환되는 과정에서 사용자의 맥락적 의도가 상실됩니다.
애플리케이션이 실행되면 사용자는 기본 최상위 홈 화면으로 진입하게 됩니다. 사용자가 복잡한 카테고리 트리를 수동으로 탐색하거나, 검색어를 다시 입력하거나, 방 식별자를 직접 입력해야 하는 상황은 상당한 인지적 및 물리적 마찰을 유발합니다. 인앱 화면 복원은 QR 코드 URL에 목적지 상태를 인코딩하고 네이티브 클라이언트 내에서 해당 타겟 뷰를 프로그래밍 방식으로 재구성함으로써 이러한 이탈 문제를 해결합니다.
이중 상태 라우팅 메커니즘: 직접 활성화 대 지연된 화면 복원

사용자가 앱 라우팅 QR 코드를 스캔할 때 모바일 기기는 설치 여부에 따라 두 가지 상태 중 하나에 놓이게 됩니다. 완성도 높은 라우팅 아키텍처는 각각의 기술적 경로를 통해 두 가지 라이프사이클을 모두 처리합니다.
- 직접 활성화 라우팅(애플리케이션이 이미 설치된 경우): 타겟 애플리케이션이 기기에 이미 설치되어 있는 경우, QR 코드 스캔은 운영체제 딥 링크—구체적으로 iOS의 Apple Universal Links 또는 Android의 Android App Links—를 트리거합니다. 두 메커니즘 모두 암호화 방식으로 검증된 도메인 연동 파일의 호스팅을 요구합니다. iOS는 Xcode의
Associated Domains권한과 결합된apple-app-site-association(AASA) 파일을 필요로 하며, Android는 애플리케이션의 서명 인증서 SHA-256 지문과 일치하고 검증된 매니페스트 인텐트 필터와 매칭되는/.well-known/assetlinks.jsonDigital Asset Links 파일을 필요로 합니다. 검증이 완료되면 OS는 애플리케이션을 깨우고 URI를 클라이언트의 라이프사이클 델리게이트로 직접 전달합니다. - 지연된 화면 복원(애플리케이션이 설치되지 않은 경우): 애플리케이션이 없는 경우, 사용자는 중개 웹 랜딩 페이지를 거쳐 플랫폼 앱 스토어로 이동하게 됩니다. 운영체제 인텐트를 수신할 네이티브 바이너리가 아직 설치되어 있지 않으므로, 라우팅 파라미터는 스토어로 리다이렉트되기 전에 임시 저장되어야 하며 설치 완료 후 애플리케이션의 최초 실행 시점(Day 0 콜드 스타트)에 복구되어야 합니다.
플랫폼별 복원 경계: Google Play Install Referrer, iOS Universal Links 및 SDK 어댑터
사용자가 앱 스토어에서 애플리케이션을 설치한 후, 유입 및 라우팅 파라미터 복원은 플랫폼과 애플리케이션 또는 어트리뷰션 SDK가 구현한 메커니즘에 따라 달라집니다. 웹 URL을 통해 route 또는 id 파라미터를 전달한다고 해서 그 자체로 해당 파라미터가 앱스토어 설치 과정을 거치면서 유지된다는 보장은 없습니다.
- Android 및 Google Play: Google Play Install Referrer API는 Play 스토어 설치와 연결된 지원 유입 메타데이터를 제공할 수 있습니다. 이 메커니즘은 임의의 타사 APK 사이드로드나 대체 스토어가 아닌, 공식 Google Play 스토어 경로를 통해 완료된 설치에만 구체적으로 적용됩니다. 애플리케이션은 실제 레퍼러 페이로드를 구성 및 테스트하고 예상 파라미터가 리다이렉트 및 설치 흐름 속에서 안전하게 유지되는지 확인해야 합니다.
- iOS 및 App Store: Apple App Store는 임의의 웹 랜딩 페이지 쿼리 파라미터를 새로 설치된 애플리케이션 바이너리로 자동 이전하지 않습니다. Universal Links는 애플리케이션이 이미 설치되어 있고 연동된 도메인 설정이 유효할 때 직접 라우팅을 지원하며, 신규 설치 후의 지연된 파라미터 복원을 단독으로 보장하지는 않습니다.
- 어트리뷰션 SDK: 배포 중인 정확한 SDK 버전과 플랫폼에 대해 문서화된 복원 메커니즘만 사용하십시오. SDK가 모든 브라우저와 설치 경로에서 모든 라우팅 파라미터를 복구할 수 있다고 가정하는 대신 구현 방식, 데이터 수집 및 개인정보 보호에 미치는 영향을 직접 검증하세요.
iOS 어트리뷰션 및 개인정보 보호 경계: Apple의 안티 핑거프린팅 규정 준수
iOS 라우팅 및 지연된 화면 복원 구현 시, 애플리케이션이나 웹사이트 전반에 걸쳐 사용자와 기기를 식별하기 위해 금지된 기기 핑거프린팅 방식을 사용해서는 안 됩니다. Apple 사용자 개인정보 보호 및 데이터 사용 지침 및 Apple 개발자 프로그램 라이선스 계약에 따라, 개발자는 이 목적을 위해 기기, 브라우저, 설정, 위치 또는 네트워크 신호로부터 고유 식별자를 유추하는 행위가 엄격히 금지됩니다. 앱 추적 투명성(ATT) 권한 부여는 이외의 방식으로 금지된 핑거프린팅을 허용 가능한 것으로 만들어주지 않습니다.
애플리케이션이 설치된 상태에서 검증된 직접 앱 라우팅을 수행하려면 Universal Links를 사용하십시오. iOS에서의 설치 후 화면 복원에는 배포된 SDK 버전 및 관련 Apple 요구사항에 대해 동작이 검토된 문서화된 메커니즘을 사용하세요. 해당하는 경우 SDK의 개인정보 보호 공지 및 필수 사유 API 선언을 검토하되, 개인정보 보호 매니페스트(PrivacyInfo.xcprivacy)의 존재를 어트리뷰션 구현이 규정을 준수한다는 증거로 간주하지 마십시오.
선택한 SDK가 허용된 것으로 검증할 수 없는 매칭 방법에 의존하는 경우, 해당 방법을 iOS 배포에 권장하지 마십시오. 지연된 라우팅 맥락을 안정적으로 확립할 수 있는 허용된 메커니즘이 없는 경우, 일반적인 온보딩 흐름을 유지하고 사용자를 안전한 기본 홈 화면으로 진입시키며, 검증되지 않은 매치를 억지로 생성하는 대신 해당 설치를 비어트리뷰션 처리하세요.
이중 상태 QR 화면 복원 파이프라인 시각화
아래 아키텍처 다이어그램은 매개변수화된 QR 코드가 클라이언트 측 라우터 및 인증 게이트에 수렴하기 전, OS 직접 활성화 또는 지연된 스토어 복원으로 분기되는 다음 단계들을 보여줍니다.
[사용자 QR 코드 스캔] ──> URL (?route=product_detail&id=sku_8842&channelCode=pos_01)
│
┌────┴────────────────────────────────────────┐
▼ (앱이 이미 설치된 경우) ▼ (앱이 설치되지 않은 경우)
[OS Universal Link / App Link] [중개 H5 랜딩 페이지]
│ │
▼ ▼
[핫/콜드 웨이크업 콜백] [스토어 다운로드 및 최초 실행]
(onNewIntent / continueUserActivity) (플랫폼 지원 지연 복원)
│ │
└────────────────────┬────────────────────────┘
▼
[클라이언트 라우트 검증기 및 인증 게이트]
│
┌────────────────────┴────────────────────────┐
▼ (허용 목록 등록 및 인증됨) ▼ (유효하지 않은 라우트 / 미매치)
[제품 상세 화면으로 이동] [기본 홈 화면으로 폴백]
1단계: 커스텀 라우팅 파라미터 스키마 설계
실행 가능한 라우팅 페이로드와 수동적 어트리뷰션 태그의 구조화
앱 라우팅용 QR 코드를 생성할 때 엔지니어링 팀은 외부의 간결한 URL 쿼리 키와 내부의 정규화된 JSON 페이로드를 분리하는 명확한 2계층 데이터 계약을 수립해야 합니다.
| 계약 계층 | 필드 명명 규칙 | 예시 표현 | 아키텍처 목적 |
|---|---|---|---|
| 외부 URL 쿼리 계층 | route, id, channelCode |
?route=product_detail&id=sku_8842&channelCode=pos_01 |
인쇄된 QR 매트릭스를 간결하게 유지하기 위해 URI 문자열 길이를 최소화 |
| 내부 정규화 계층 | route_key, target_id, channel_code, fallback_route |
{"route_key": "product_detail", "target_id": "sku_8842", ...} |
H5 및 네이티브 클라이언트 라우터에 의해 검증되는 표준화된 캐노니컬 객체 |
| 클라이언트 보안 레지스트리 | PROTECTED_ROUTES (내부 상수) |
setOf("room_lobby", "promo_campaign") |
내부적으로 인증 요구사항 결정; 외부 URL에서 절대 신뢰하지 않음 |

이 2계층 모델에서:
- 수동적 어트리뷰션 태그(
channelCodechannel_code)는 클라이언트 UI 상태를 변경하지 않고 마케팅 전환 볼륨을 측정하기 위해 분석 파이프라인에 의해 소비됩니다. - 실행 가능한 라우팅 파라미터(
routeroute_key및idtarget_id)는 네이티브 클라이언트의 내비게이션 컨트롤러에 어떤 화면을 마운트하고 어떤 엔티티를 가져와야 하는지 지시합니다. - 인증 정책 격리: 라우트에 인증이 필요한지 여부(
requires_auth)는 외부 QR 쿼리 파라미터에서 수락하는 것이 아니라, 클라이언트 애플리케이션의 내부 신뢰 라우트 레지스트리로부터 엄격하게 도출되어야 합니다.
IETF RFC 3986 퍼센트 인코딩 및 페이로드 크기 제약 조건 준수
QR 목적지 URL에 추가되는 모든 라우팅 파라미터는 IETF RFC 3986에 명시된 URI 일반 구문을 준수해야 합니다.
- 중첩된 구분자 회피: 원시 JSON 문자열(
?data={"route":"product","id":123})을 URL 쿼리 문자열에 직접 내장하지 마십시오. 이스케이프되지 않은 중괄호, 따옴표, 콜론은 인앱 브라우저 URL 파서의 오류를 유발하고 바코드 모듈 밀도를 높입니다. - 키-값 쌍 평탄화: 외부 라우팅 맥락을 평면적인 퍼센트 인코딩 쿼리 파라미터로 표현합니다(
https://promo.example.com/open?route=product_detail&id=sku_8842&channelCode=pos_01). - 엄격한 문자 범위 적용:
route및id값은 URL에 안전한 영숫자, 하이픈, 언더스코어(^[A-Za-z0-9_-]{1,64}$)로 제한합니다.
클라이언트 측 라우트 허용 목록 계약 정의
외부 URL 파라미터가 수집되어 내부 페이로드 구조로 정규화되면, 엄격한 스키마 계약을 준수해야 합니다. route_key를 인식할 수 없거나 엔티티별 라우트(product_detail, room_lobby, promo_campaign)에 필수적인 target_id가 누락된 경우, 정규화기는 페이로드를 target_id: null을 포함하는 home_default로 다운그레이드해야 합니다.
아래 JSON 스키마는 정규화된 화면 복원 페이로드의 표준 구조를 정의합니다.
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "QrSceneRestorationPayload",
"description": "Canonical, normalized routing payload. Authentication policy is derived separately from the application's trusted route registry.",
"type": "object",
"additionalProperties": false,
"required": [
"schema_version",
"route_key",
"target_id",
"fallback_route"
],
"properties": {
"schema_version": {
"const": "1.0.0"
},
"route_key": {
"type": "string",
"enum": [
"product_detail",
"room_lobby",
"promo_campaign",
"home_default"
]
},
"target_id": {
"type": ["string", "null"],
"pattern": "^[A-Za-z0-9_-]{1,64}$"
},
"channel_code": {
"type": "string",
"pattern": "^[A-Za-z0-9_.-]{1,64}$"
},
"fallback_route": {
"const": "home_default"
}
},
"allOf": [
{
"if": {
"properties": {
"route_key": {
"enum": [
"product_detail",
"room_lobby",
"promo_campaign"
]
}
},
"required": ["route_key"]
},
"then": {
"properties": {
"target_id": {
"type": "string"
}
}
}
},
{
"if": {
"properties": {
"route_key": {
"const": "home_default"
}
},
"required": ["route_key"]
},
"then": {
"properties": {
"target_id": {
"type": "null"
}
}
}
}
],
"examples": [
{
"schema_version": "1.0.0",
"route_key": "product_detail",
"target_id": "sku_8842",
"channel_code": "pos_01",
"fallback_route": "home_default"
}
]
}
2단계: 랜딩 페이지에서 라우팅 맥락 캡처 및 전달
H5 랜딩 페이지에서 허용된 라우팅 키 파싱
애플리케이션이 설치되지 않은 사용자가 QR 코드를 스캔하면 모바일 브라우저는 호스팅된 중개 H5 랜딩 페이지를 엽니다. 랜딩 페이지 스크립트는 외부 쿼리 키(route, id, channelCode)를 추출하고 모호한 중복 파라미터를 거부한 뒤, 1단계에서 정의된 표준 구조로 정규화합니다.
아래 JavaScript 정규화기는 단일 키 카디널리티, 정규식 범위, 조건부 home_default 다운그레이드를 강제하는 동시에 중복된 예약 파라미터를 유효하지 않은 것으로 처리합니다.
const query = new URLSearchParams(window.location.search);
const routeValues = query.getAll("route");
const idValues = query.getAll("id");
const channelValues = query.getAll("channelCode");
const allowedRoutes = new Set([
"product_detail",
"room_lobby",
"promo_campaign",
"home_default"
]);
const idPattern = /^[A-Za-z0-9_-]{1,64}$/;
const channelPattern = /^[A-Za-z0-9_.-]{1,64}$/;
function normalizeQrRoutingPayload() {
// Reject duplicate keys to defend against parameter pollution
if (routeValues.length > 1 || idValues.length > 1 || channelValues.length > 1) {
return {
schema_version: "1.0.0",
route_key: "home_default",
target_id: null,
fallback_route: "home_default"
};
}
const rawRoute = routeValues.length === 1 ? routeValues[0] : null;
const rawId = idValues.length === 1 ? idValues[0] : null;
const rawChannel = channelValues.length === 1 ? channelValues[0] : null;
let routeKey = allowedRoutes.has(rawRoute)
? rawRoute
: "home_default";
let targetId =
rawId && idPattern.test(rawId)
? rawId
: null;
if (routeKey === "home_default" || targetId === null) {
routeKey = "home_default";
targetId = null;
}
const payload = {
schema_version: "1.0.0",
route_key: routeKey,
target_id: targetId,
fallback_route: "home_default"
};
if (rawChannel && channelPattern.test(rawChannel)) {
payload.channel_code = rawChannel;
}
return payload;
}
const routingPayload = normalizeQrRoutingPayload();
버전이 검증된 어댑터를 통한 웹 SDK 연동
랜딩 페이지는 즉각적인 앱 웨이크업 시도와 지연된 파라미터 임시 저장을 모두 조율합니다. 어트리뷰션 웹 SDK(예: opoinstall.js)를 배포하기 전에 배포하려는 SDK 버전의 공식 문서를 참조하여 정확한 JavaScript 번들 URL, 초기화 서명, 준비 콜백, 웨이크업 메서드 및 파라미터 스키마를 확인하십시오.
정규화된 routingPayload 객체를 수용하는 소규모 애플리케이션 어댑터 뒤에 SDK 고유의 동작을 격리합니다. 웹 연동 및 채널 파라미터 구성에 대한 자세한 사양은 OpoInstall 개발자 문서를 참조하세요.
직접 스토어 폴백 링크 전반에 걸친 라우팅 파라미터 유지
웹 JS SDK를 사용할 수 없거나 브라우저 보안 제어에 의해 차단된 경우, 랜딩 페이지는 직접 스토어 리다이렉트 링크로 폴백합니다. Google Play를 활용하는 Android 배포의 경우, 공식 Play 스토어 흐름을 통해 설치될 때 네이티브 클라이언트가 Google Play Install Referrer API를 통해 라우팅 맥락을 복구할 수 있도록 폴백 링크가 정규화된 라우팅 키를 Play 스토어 referrer 파라미터에 인코딩할 수 있습니다.
// Example Android Play Store fallback URL preserving normalized routing context
const referrerString = `route_key=${routingPayload.route_key}&target_id=${routingPayload.target_id || ""}&channel_code=${routingPayload.channel_code || ""}`;
const playStoreFallbackUrl = `https://play.google.com/store/apps/details?id=com.example.app&referrer=${encodeURIComponent(referrerString)}`;
3단계: 네이티브 클라이언트 라우트 디스패치 및 화면 복원 구현
Android 액티비티 실행 모드 처리: singleTask에 onCreate와 onNewIntent가 모두 필요한 이유
Android 딥 링크 아키텍처에서 액티비티가 라우팅 인텐트를 수신하는 방식은 구성된 실행 모드 및 내비게이션 그래프 디자인에 따라 다릅니다. 개발자는 태스크 스택 요구사항에 따라 Jetpack Navigation, singleTop 또는 singleTask와 함께 표준 실행 모드를 사용할 수 있습니다.
딥 링크 웨이크업 중에 중복 인스턴스가 스택에 쌓이는 것을 방지하기 위해 액티비티가 singleTop 또는 singleTask로 구성된 경우, 두 가지 서로 다른 라이프사이클 경로를 처리해야 합니다.
- 콜드 스타트(
onCreate): 애플리케이션 프로세스가 실행되고 있지 않을 때 QR 코드를 스캔하면 액티비티가 처음부터 실행됩니다. 수신된Intent는onCreate(savedInstanceState: Bundle?)로 전달됩니다. - 웜 스타트(
onNewIntent): 구성된 실행 모드 및 태스크 스택 조건에 따라 운영체제가 기존 액티비티 인스턴스를 재사용하는 경우, 수신된 딥 링크 인텐트는onNewIntent()로 전달됩니다.singleTop의 경우 타겟 액티비티가 이미 관련 태스크 스택의 최상위에 있을 때만 재사용이 발생하며, 그렇지 않으면 새로운 액티비티 인스턴스가 생성될 수 있습니다.onCreate()와onNewIntent()처리를 모두 구현하고 애플리케이션의 내비게이션 아키텍처에 대한 실제 동작을 검증하세요.
세션 내에서 동일한 QR 코드를 나중에 정당하게 다시 스캔하는 사용자를 영구적으로 차단하지 않고 두 상태를 안정적으로 처리하기 위해, 액티비티는 처리된 각 Intent 인스턴스에 소비 플래그(EXTRA_ROUTE_CONSUMED)를 표시하고 기록 앱 스위처 실행(Intent.FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY)에 의해서만 재전달된 인텐트는 무시해야 합니다.
iOS 라이프사이클 진입점 처리: AppDelegate 대 UISceneDelegate 연속성 활동
iOS에서는 직접 Universal Link 라우팅과 지연된 파라미터 검색이 애플리케이션의 구성된 UI 라이프사이클과 일치해야 합니다.
- 장면 기반 라이프사이클(
UISceneDelegate):UIScene을 채택한 애플리케이션(iOS 13 이후 표준)에서 콜드 런치 Universal Link는connectionOptions.userActivities를 통해scene(_:willConnectTo:options:)로 도착하는 반면, 웜 리주룸 Universal Link는scene(_:continue:)로 도착합니다. - 애플리케이션 델리게이트 라이프사이클(
UIApplicationDelegate): 장면 델리게이트를 사용하지 않는 애플리케이션의 경우 Universal Link는application(_:continue:restorationHandler:)로 전달됩니다. - 지연된 최초 실행 복원: Universal Link 연속성 델리게이트와 무관하게, 루트 뷰 계층 구조가 준비된 후 초기 애플리케이션 시작 중에 지연된 설치 파라미터 쿼리가 한 번 실행됩니다.
개발자는 OpoInstall SDK 다운로드 센터를 통해 플랫폼 SDK 패키지를 다운로드하고 라이프사이클 연동 템플릿을 검토할 수 있습니다.
직접 웨이크업 파라미터와 최초 실행 지연 파라미터를 단일 내비게이션 라우터로 통합
일관된 보안 및 내비게이션 동작을 유지하기 위해 클라이언트 아키텍처는 직접 웨이크업 URI와 지연된 설치 JSON 페이로드를 UI 라우터로 디스패치하기 전에 단일 NavigationTarget 모델로 정규화합니다.
- 출처 검증(직접 인텐트): 수신된
Intent.ACTION_VIEWURI가https스키마를 사용하고, 애플리케이션의 검증된 연동 호스트(promo.example.com) 및 경로(/open)와 일치하며, 상충되는 중복 쿼리 키를 포함하지 않는지 확인합니다. - 스키마 정규화 및 허용 목록 확인: 외부
route/id키 또는 내부route_key/target_id필드를 표준 모델로 매핑하고, 허용되지 않은 라우트나 잘못된 형식의 ID를 거부합니다. 클라이언트 Kotlin 파싱 로직은 예시적인 방어적 파싱 골격으로 작동하며, 프로덕션 시스템에서는 파싱하기 전에 전체 표준 스키마 규칙에 따라 페이로드를 검증해야 합니다. - 단일 실행 재입실 방지 가드: 동시 콜백이 겹치는 프래그먼트 트랜잭션을 트리거하지 못하도록 원자적(atomic) 전달당 가드를 사용합니다.
인증 게이트 관리: 사용자 로그인이 완료될 때까지 보류 중인 딥 링크 라우트 대기열 처리

신규 사용자가 QR 코드를 통해 앱스토어에서 애플리케이션을 설치하고 처음 실행할 때, 사용자는 대개 인증되지 않은 상태입니다. QR 코드가 보호된 화면(예: room_lobby 또는 promo_campaign)을 타겟으로 하고 라우터가 즉시 이동을 시도하면, 인증 게이트가 사용자를 로그인 화면으로 리다이렉트하고 딥 링크 맥락을 잃게 됩니다.
인증 경계 전반에서 화면 복원을 유지하려면:
- 내부 라우트 정책 조회: 외부 페이로드 플래그를 신뢰하는 대신 클라이언트의 내부
PROTECTED_ROUTES레지스트리를 확인하여 라우트에 인증이 필요한지 여부를 결정합니다. - 원자적 임시 저장(Staging): 사용자가 인증되지 않은 경우, 검증된
NavigationTarget을 원자적 세션 범위의PendingRouteStore(AtomicReference)에 저장하고 사용자를 로그인 흐름으로 라우팅합니다. 메모리 내PendingRouteStore는 활성 애플리케이션 프로세스에 적용되며, 인증 흐름이 프로세스 종료에 걸쳐 있을 수 있는 경우 계정 전환 시 상태를 지우는 암호화된 시간 제한 로컬 저장소를 사용합니다. - 인증 후 재검증 및 디스패치: 로그인이 완료되면 임시 저장된 타겟을 원자적으로 소비(
getAndSet(null))하고, 세션이 이제 인증되었는지 확인하고, 라우트 파라미터를 다시 검증한 뒤 화면 전환을 실행합니다.
내비게이션 호스트 준비 상태 및 상태 복원
onNavigationGraphReady() 콜백은 애플리케이션의 실제 내비게이션 호스트 초기화에 연결되어야 합니다. 내비게이션 그래프가 XML로 호스팅된 NavHostFragment에 정의된 경우, 액티비티는 호스트와 해당 대상이 준비된 후에 이 연결을 설정할 수 있습니다. 그래프가 비동기적으로 설치되는 경우, 그래프 초기화가 완료된 후에만 콜백을 호출하십시오.
액티비티 필드에만 저장된 보류 중인 내비게이션 요청은 구성 변경 중에 손실될 수 있습니다. 프로덕션 구현의 경우 라이프사이클에 민감한 ViewModel에 소비되지 않은 라우팅 상태를 유지하고, 시스템 주도 액티비티 재생성 또는 프로세스 종료 전반에서 복원이 필요한 경우 SavedStateHandle을 사용하세요. 여기에 표시된 코드는 아키텍처 예시이며 애플리케이션별 내비게이션 및 세션 연동이 필요합니다.
아래 Kotlin 코드는 이러한 제어 기능을 시연하는 애플리케이션 소유 SDK 및 내비게이션 어댑터가 포함된 예시적인 Android 라우팅 골격을 제공합니다.
<!-- Android Manifest Configuration for App Link Verification -->
<!-- File: app/src/main/AndroidManifest.xml -->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.app">
<application
android:allowBackup="true"
android:label="@string/app_name"
android:theme="@style/Theme.AppCompat.Light.NoActionBar">
<activity
android:name=".SceneRestorationActivity"
android:exported="true"
android:launchMode="singleTask">
<!-- Verified Android App Link Intent Filter -->
<intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<!-- Origin must match /.well-known/assetlinks.json SHA-256 fingerprint -->
<data
android:scheme="https"
android:host="promo.example.com"
android:pathPrefix="/open" />
</intent-filter>
</activity>
</application>
</manifest>
// Android Routing Activity Implementation
// File: app/src/main/java/com/example/app/SceneRestorationActivity.kt
//
// INTEGRATION NOTE:
// This example is an architectural routing skeleton, not a drop-in production implementation.
// Bind AttributionRoutingAdapter and NavController initialization to your verified SDK release,
// session manager, and NavHostFragment before deployment.
package com.example.app
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.util.Log
import androidx.appcompat.app.AppCompatActivity
import androidx.navigation.fragment.NavHostFragment
import org.json.JSONObject
import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.atomic.AtomicReference
/**
* Validated navigation target model representing an allowlisted in-app destination.
* Authentication requirement is derived strictly from the internal route registry.
*/
data class NavigationTarget(
val eventKey: String,
val routeKey: String,
val targetId: String?,
val requiresAuth: Boolean
)
/**
* Process-scoped atomic store holding a pending route when an unauthenticated user
* triggers a protected deep link during cold start or first-launch onboarding.
*/
object PendingRouteStore {
private val pending = AtomicReference<NavigationTarget?>(null)
fun stage(target: NavigationTarget) {
pending.set(target)
}
fun consume(): NavigationTarget? {
return pending.getAndSet(null)
}
}
class SceneRestorationActivity : AppCompatActivity() {
companion object {
private const val TAG = "SceneRestorationRouter"
private const val TRUSTED_HOST = "promo.example.com"
private const val TRUSTED_PATH = "/open"
private const val EXTRA_ROUTE_CONSUMED = "com.example.app.EXTRA_ROUTE_CONSUMED"
private const val ROUTE_HOME = "home_default"
private const val ROUTE_PRODUCT = "product_detail"
private const val ROUTE_LOBBY = "room_lobby"
private const val ROUTE_PROMO = "promo_campaign"
private val ALLOWED_ROUTES = setOf(ROUTE_HOME, ROUTE_PRODUCT, ROUTE_LOBBY, ROUTE_PROMO)
private val PROTECTED_ROUTES = setOf(ROUTE_LOBBY, ROUTE_PROMO)
private val SAFE_ID_REGEX = Regex("^[A-Za-z0-9_-]{1,64}$")
// Process-level guard ensuring deferred install recovery is queried only once per cold start
private val hasQueriedDeferredInstall = AtomicBoolean(false)
}
// Per-delivery re-entrancy guard; allows legitimate subsequent scans of the same QR code
private val processingIntent = AtomicBoolean(false)
// Staging container for routes that arrive before the UI/NavController is mounted
private val pendingUiTarget = AtomicReference<NavigationTarget?>(null)
private var navigationReady = false
// Application-owned adapter isolating vendor SDK and session calls
private val attributionAdapter = AttributionRoutingAdapter()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_scene_restoration)
// Connect NavHost initialization when defined in layout
connectNavigationHost()
// Activity recreation (e.g., screen rotation) is not a new install or new QR scan
if (savedInstanceState != null) {
return
}
val launchIntent = intent
val isDirectLinkLaunch =
launchIntent?.action == Intent.ACTION_VIEW &&
launchIntent.data != null
if (isDirectLinkLaunch) {
processIncomingIntentOnce(launchIntent) { candidate ->
handleIncomingWakeupIntent(candidate)
}
// Do not start deferred recovery after a direct-link launch,
// even if the direct route was rejected.
return
}
// Only initial normal application launches may request deferred recovery
if (hasQueriedDeferredInstall.compareAndSet(false, true)) {
queryDeferredSceneParameters()
}
}
/**
* Connect this method to the actual NavHost initialization.
* Uses currentDestination check and destination listener to safely verify graph readiness
* without throwing IllegalStateException on unset controller.graph.
*/
private fun connectNavigationHost() {
val navHost = supportFragmentManager
.findFragmentById(R.id.nav_host_fragment) as? NavHostFragment
if (navHost == null) {
Log.w(TAG, "NavHost not attached; route remains queued.")
// For an asynchronously attached NavHost, call this
// method again after the host has been attached.
return
}
val controller = navHost.navController
// currentDestination is nullable before a destination has been established.
// Do not access controller.graph merely to check whether setGraph() has been called.
if (controller.currentDestination != null) {
onNavigationGraphReady()
return
}
// If the graph/destination becomes available later,
// mark navigation ready when a destination is established.
val listener = object : androidx.navigation.NavController.OnDestinationChangedListener {
override fun onDestinationChanged(
navController: androidx.navigation.NavController,
destination: androidx.navigation.NavDestination,
arguments: Bundle?
) {
navController.removeOnDestinationChangedListener(this)
onNavigationGraphReady()
}
}
controller.addOnDestinationChangedListener(listener)
}
/**
* Captures warm-start QR scans when the Activity is reused under singleTop or singleTask.
*/
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
setIntent(intent)
processIncomingIntentOnce(intent) { candidate ->
handleIncomingWakeupIntent(candidate)
}
}
private fun processIncomingIntentOnce(incomingIntent: Intent?, process: (Intent) -> Unit) {
val candidate = incomingIntent ?: return
// Ignore historical intents re-delivered from recent-apps switcher or configuration changes
if ((candidate.flags and Intent.FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY) != 0) return
if (candidate.getBooleanExtra(EXTRA_ROUTE_CONSUMED, false)) return
if (!processingIntent.compareAndSet(false, true)) return
try {
process(candidate)
candidate.putExtra(EXTRA_ROUTE_CONSUMED, true)
} finally {
processingIntent.set(false)
}
}
private fun handleIncomingWakeupIntent(incomingIntent: Intent?): Boolean {
if (incomingIntent?.action != Intent.ACTION_VIEW) return false
val uri: Uri = incomingIntent.data ?: return false
// 1. Verify trusted HTTPS origin and path
val isTrusted =
uri.scheme.equals("https", ignoreCase = true) &&
uri.host.equals(TRUSTED_HOST, ignoreCase = true) &&
uri.path == TRUSTED_PATH
if (!isTrusted) {
Log.w(TAG, "Rejected untrusted deep-link origin: $uri")
return false
}
// 2. Reject missing or conflicting duplicate query parameters
if (uri.getQueryParameters("route").size != 1 ||
uri.getQueryParameters("id").size != 1) {
Log.w(TAG, "Rejected missing or duplicate routing keys in URI")
return false
}
val rawRoute = uri.getQueryParameter("route")
val rawId = uri.getQueryParameter("id")
val target = validateAndBuildTarget("wakeup", rawRoute, rawId)
if (target.routeKey != ROUTE_HOME) {
dispatchNavigationTarget(target)
return true
}
return false
}
private fun queryDeferredSceneParameters() {
// Application-defined timeout configuration; tune for deployed service behavior
attributionAdapter.fetchDeferredInstallParams(timeoutMs = 5000L) { jsonPayload ->
if (jsonPayload == null) {
Log.d(TAG, "No deferred routing parameters recovered; remaining on default view.")
return@fetchDeferredInstallParams
}
val target = parseJsonRoutingPayload("deferred_install", jsonPayload)
runOnUiThread {
dispatchNavigationTarget(target)
}
}
}
private fun parseJsonRoutingPayload(sourcePrefix: String, rawJson: String): NavigationTarget {
return try {
val obj = JSONObject(rawJson)
// Defensive normalization: map canonical 'route_key'/'target_id' with external 'route'/'id' fallback
val rawRoute = obj.optString("route_key", "").ifEmpty { obj.optString("route", "") }
val rawId = if (obj.has("target_id") && !obj.isNull("target_id")) {
obj.optString("target_id", "")
} else {
obj.optString("id", "")
}
validateAndBuildTarget(sourcePrefix, rawRoute, rawId)
} catch (e: Exception) {
Log.w(TAG, "Malformed routing JSON payload: ${e.message}")
NavigationTarget(sourcePrefix, ROUTE_HOME, null, false)
}
}
/**
* Enforces strict allowlist and regex bounds on untrusted routing parameters.
*/
private fun validateAndBuildTarget(
sourcePrefix: String,
rawRoute: String?,
rawId: String?
): NavigationTarget {
val cleanRoute = if (rawRoute != null && ALLOWED_ROUTES.contains(rawRoute)) {
rawRoute
} else {
ROUTE_HOME
}
if (cleanRoute == ROUTE_HOME) {
return NavigationTarget(sourcePrefix, ROUTE_HOME, null, false)
}
// Entity-specific routes strictly require a valid, sanitized targetId
val cleanId = if (rawId != null && SAFE_ID_REGEX.matches(rawId)) {
rawId
} else {
null
}
if (cleanId == null) {
Log.w(TAG, "Route $cleanRoute rejected due to missing or invalid target_id.")
return NavigationTarget(sourcePrefix, ROUTE_HOME, null, false)
}
val needsAuth = PROTECTED_ROUTES.contains(cleanRoute)
return NavigationTarget("${sourcePrefix}_${cleanRoute}_${cleanId}", cleanRoute, cleanId, needsAuth)
}
/**
* Invoked when the application's NavHost / NavController graph is attached and ready.
*/
fun onNavigationGraphReady() {
navigationReady = true
flushPendingNavigation()
}
private fun flushPendingNavigation() {
if (!navigationReady || isFinishing || isDestroyed) return
val target = pendingUiTarget.getAndSet(null) ?: return
dispatchReadyNavigationTarget(target)
}
private fun dispatchNavigationTarget(target: NavigationTarget) {
if (isFinishing || isDestroyed) return
// Stage route if the UI navigation graph has not finished inflating
if (!navigationReady) {
pendingUiTarget.set(target)
return
}
dispatchReadyNavigationTarget(target)
}
private fun dispatchReadyNavigationTarget(target: NavigationTarget) {
if (target.routeKey == ROUTE_HOME) {
navigateToHomeScreen()
return
}
// Check authentication gate for protected destinations
if (target.requiresAuth && !attributionAdapter.isUserAuthenticated()) {
Log.d(TAG, "Protected route ${target.routeKey} staged pending user authentication.")
PendingRouteStore.stage(target)
navigateToLoginScreen()
return
}
executeScreenTransition(target.routeKey, target.targetId)
}
/**
* Invoked by the authentication controller after login or registration completes.
*/
fun onUserAuthenticationCompleted() {
val pending = PendingRouteStore.consume()
if (pending == null) {
navigateToHomeScreen()
return
}
if (isFinishing || isDestroyed) return
if (!attributionAdapter.isUserAuthenticated()) {
PendingRouteStore.stage(pending)
navigateToLoginScreen()
return
}
val validated = validateAndBuildTarget(
pending.eventKey,
pending.routeKey,
pending.targetId
)
if (validated.routeKey == ROUTE_HOME) {
navigateToHomeScreen()
return
}
executeScreenTransition(validated.routeKey, validated.targetId)
}
private fun executeScreenTransition(routeKey: String, targetId: String?) {
// Implementation: perform backend-authorized resource fetch and invoke NavController.navigate()
Log.d(TAG, "Dispatching verified screen transition: route=$routeKey, id=$targetId")
}
private fun navigateToLoginScreen() {
// Implementation: transition to LoginFragment / LoginActivity
}
private fun navigateToHomeScreen() {
// Implementation: display default home screen
}
}
/**
* Application-owned adapter skeleton. Replace stubbed methods with verified OpoInstall SDK
* callbacks, Google Play Install Referrer queries, and your production session manager.
*/
class AttributionRoutingAdapter {
fun fetchDeferredInstallParams(timeoutMs: Long, callback: (String?) -> Unit) {
// Bind to verified SDK install parameter API or Play Install Referrer in production
callback(null)
}
fun isUserAuthenticated(): Boolean {
// Bind to production authentication session state
return false
}
}
보안 및 진단: 라우트 주입 및 내비게이션 이탈 방지
OWASP 지침에 따른 딥 링크 라우트 주입 및 오픈 리다이렉트 취약점 방어
QR 코드나 지연된 어트리뷰션 콜백에서 추출된 파라미터는 신뢰할 수 없는 외부 입력에 해당합니다. OWASP 모바일 애플리케이션 보안 검증 표준 및 OWASP 모바일 Top 10 보안 위험 및 지침에 따라 클라이언트 라우터는 세 가지 방어 경계를 강제해야 합니다.
- 임의의 WebView URL 로딩 금지: 라우팅 파라미터(
?route=webview&url=https://...)가 내부 애플리케이션 WebView 내에서 검증되지 않은 외부 URL을 로드하도록 허용하지 마십시오. 공격자가 QR 코드에 피싱 URL을 인코딩하여 애플리케이션의 네이티브 UI 프레임을 악용하고 자격 증명을 탈취할 수 있습니다. - 중복 파라미터 오염 거부: 조작된 URL에 상충되는 여러 키(
?route=home_default&route=room_lobby)가 포함된 경우, 파서가 임의의 첫 번째 또는 마지막 요소를 선택하도록 허용하는 대신 해당 URI를 거부합니다. - 백엔드 리소스 권한 부여 강제:
target_id=sku_8842를 전달하는 것은 클라이언트에게 요청할 뷰를 지시할 뿐입니다. 화면이 백엔드 API를 쿼리할 때 서버는 현재 사용자 세션이 해당 엔티티에 액세스할 권한이 있는지 독립적으로 검증하여 IDOR(Insecure Direct Object Reference) 취약점을 방지해야 합니다.
콜드 스타트 대 웜 스타트 내비게이션 경쟁 상태 진단
콜드 스타트 중에는 딥 링크 인텐트나 캐시된 SDK 콜백이 메인 NavHostFragment 또는 루트 내비게이션 그래프가 인플루언싱을 완료하기 전에 해결될 수 있습니다. 뷰 계층 구조가 준비되기 전에 navController.navigate()를 호출하려고 하면 런타임 예외가 발생하거나 전환이 누락됩니다. 호스트 뷰 계층 구조가 첨부되고 활성 상태(!isFinishing && !isDestroyed)가 될 때까지 항상 내비게이션 실행을 지연시키십시오.
만료된 프로모션, 삭제된 SKU 및 누락된 파라미터의 우아한 처리
물리적 QR 코드는 디지털 프로모션이 종료되거나 제품 SKU가 은퇴한 후에도 소매점이나 인쇄 매체에 남아 있는 경우가 종종 있습니다. 타겟 화면이 target_id에 대한 데이터를 가져오고 백엔드에서 HTTP 404 Not Found 또는 410 Gone 응답을 수신하는 경우, 클라이언트는 오류를 포착하고 사용자에게 명확한 정보성 공지를 표시한 뒤 home_default로 깔끔하게 이동해야 합니다.
이중 상태 QR 라우팅을 위한 배포 전 검증 체크리스트
물리적 QR 홍보물을 배포하기 전에 엔지니어링 팀은 물리적 기기에서 다음 7가지 검증 프로토콜을 실행해야 합니다.
- 1단계: 설치된 상태의 콜드 스타트 웨이크업: 설치된 앱을 강제 종료하고 QR 코드를 스캔합니다. OS가 앱을 열어 타겟 화면(
product_detail)으로 바로 이동하는지 확인합니다. - 2단계: 설치된 상태의 웜 스타트 웨이크업 및 반복 스캔: 앱이 두 번째 탭에 열려 있는 상태에서 앱을 백그라운드에 두고 QR 코드를 스캔합니다.
onNewIntent가 타겟 화면으로 라우팅하는지 확인합니다. 다른 곳으로 이동한 후 정확히 동일한 QR 코드를 두 번째로 스캔하고, 과도한 중복 제거로 인해 차단되지 않고 다시 이동하는지 확인합니다. - 3단계: 미설치 상태의 지연 복원(공개 라우트): 앱을 제거하고, QR 코드를 스캔하고, 스토어 설치를 완료하고, 실행합니다. 지연된 파라미터 콜백이 페이로드를 정규화하고
product_detail로 라우팅하는지 확인합니다. - 4단계: 인증 게이트가 포함된 미설치 상태의 지연 복원(보호된 라우트): 앱을 제거하고,
room_lobby를 타겟으로 하는 QR 코드를 스캔하고, 설치하고, 로그아웃된 상태에서 실행합니다. 앱이 로그인을 먼저 유도하고, 로그인 완료 시PendingRouteStore를 소비하고room_lobby로 이동하는지 확인합니다. - 5단계: 신뢰할 수 없는 출처 및 중복 키 거부: 신뢰할 수 없는 호스트에서 또는 중복 쿼리 파라미터(
?route=product_detail&route=room_lobby)와 함께ACTION_VIEW인텐트를 트리거합니다.handleIncomingWakeupIntent가 인텐트를 거부하고 경고를 로깅하는지 확인합니다. - 6단계: 화면 회전 멱등성: 딥 링크된 화면에 도착한 후 기기를 회전하여 액티비티 재생성을 트리거합니다.
EXTRA_ROUTE_CONSUMED플래그가 원래 인텐트가 다시 실행되는 것을 방지하는지 확인합니다. - 7단계: 오프라인 / 일치하지 않는 경우의 우아한 폴백: 비행기 모드에서 깨끗한 설치를 실행합니다. 검색 타임아웃이 UI 스레드를 차단하지 않고 해결되며 사용자에게
home_default화면을 보여주는지 확인합니다.
인앱 라우팅 메커니즘 비교 분석
URL 스키마, Universal Links 및 App Links, 그리고 지연된 화면 복원 평가
모바일 플랫폼은 다양한 설치 상태 전반에서 사용자를 애플리케이션 뷰로 라우팅하기 위한 고유한 메커니즘을 제공합니다.
| 라우팅 메커니즘 | 타겟 앱 상태 | OS 검증 요구사항 | 스토어 설치 전반의 파라미터 유지 여부 | 주요 엔지니어링 제약 사항 |
|---|---|---|---|---|
| 커스텀 URL 스키마 | 설치된 경우에만 | 없음(매니페스트 선언만 해당) | 아니오(앱이 설치되지 않은 경우 실패) | 미설치 시 브라우저 오류 팝업 발생; 스키마 하이재킹에 취약함 |
| Universal Links / App Links | 설치된 상태가 기본 | iOS: AASA 파일 + Associated Domains 권한; Android: assetlinks.json + 일치하는 앱 서명 인증서 및 검증된 인텐트 필터 구성 |
아니오(신규 설치의 경우 지연 레이어 필요) | 특정 제한된 인앱 웹뷰 컨테이너에 의해 억제될 수 있음 |
| Google Play Install Referrer | 신규 Android 설치 | 공식 Google Play 스토어 배포 경로 | 예(지원되는 레퍼러 문자열) | Android Play 스토어 전용; iOS 또는 비-Play 스토어는 지원하지 않음 |
| SDK 지원 화면 복원 | 설치됨 및 신규 설치 | 도메인 연동 + 검증된 SDK 어댑터 | 지원되는 플랫폼 메커니즘에 따라 조건부 유지 | 라우트 허용 목록 검증 및 플랫폼 개인정보 보호 검토 필요 |
설치 상태 및 운영체제 전반의 아키텍처 트레이드오프
커스텀 URL 스키마(myapp://product/123)는 암호화된 도메인 소유권 검증이 부족하므로, 모든 애플리케이션이 동일한 스키마를 등록하고 링크를 가로챌 수 있습니다. 게다가 앱이 설치되지 않은 상태에서 원시 커스텀 스키마 QR 코드를 스캔하면 복구할 수 없는 브라우저 오류가 발생합니다.
Universal Links 및 Android App Links는 HTTPS를 통해 도메인 소유권을 검증하고 앱이 없을 때 웹 랜딩 페이지로 폴백하므로 QR 라우팅의 필수 기반이 됩니다. 설치된 상태의 웨이크업을 위해 Universal Links와 App Links를 결합하고, 플랫폼 지원 지연 복원(Android의 Google Play Install Referrer 및 개인정보 보호가 검토된 SDK 어댑터 등)과 결합하면 단일 QR 자산에서 이중 상태 화면 복원이 가능해집니다.
자주 묻는 질문(FAQ)
앱이 아직 설치되지 않은 경우 커스텀 QR 파라미터가 어떻게 특정 인앱 페이지를 열 수 있나요?
QR 코드를 스캔했는데 타겟 페이지 대신 홈 화면으로 앱이 열리는 이유는 무엇인가요?
악의적인 QR 코드가 사용자를 권한이 없는 내부 화면으로 라우팅하는 것을 어떻게 방지할 수 있나요?
요약 및 결정 프레임워크
커스텀 파라미터를 사용하여 앱 라우팅용 QR 코드를 생성하려면 엔지니어링 팀이 간결한 외부 URL 쿼리 문자열과 상태 인식 네이티브 내비게이션 컨트롤러를 연결해야 합니다. 완성도 높은 구현은 기존 사용자를 위한 운영체제 직접 웨이크업과 신규 앱스토어 설치를 위한 조건부 지연 파라미터 복원을 모두 처리합니다.
앱스토어 전반의 파라미터 연속성은 Android의 Google Play Install Referrer 및 iOS의 검증되고 개인정보 보호를 준수하는 워크플로와 같은 플랫폼별 메커니즘에 의존하므로, 애플리케이션은 복구된 라우팅 페이로드를 신뢰할 수 없는 후보로 취급해야 합니다. 허용 목록에 등록된 라우트 디스패처, 인증 상태 큐잉, 우아한 홈 화면 폴백을 결합함으로써 팀은 애플리케이션 안정성이나 플랫폼 개인정보 보호 규칙을 손상시키지 않으면서 안정적인 화면 복원을 제공할 수 있습니다.
OpoInstall의 SDK 및 개발자 문서는 웹-to-app 라우팅 및 파라미터 전달 구현을 위한 컴포넌트를 제공합니다. 프로덕션에 출시하기 전에 배포된 SDK 버전을 통해 지원되는 특정 API 시그니처, 콜백 스키마, 플랫폼 동작을 확인하세요.
연동 세부 정보에 대해서는 OpoInstall 개발자 문서를 참조하거나, OpoInstall SDK 다운로드 센터에 액세스하거나, OpoInstall 개발자 콘솔에서 애플리케이션 라우팅 도메인을 구성하세요.
관련 자료
-
개념: 화면 복원, 이중 상태 딥 링크, 라우트 허용 목록 등록, 지연된 파라미터 복원
-
기술: Universal Links, Android App Links, Google Play Install Referrer, 네이티브 내비게이션 컨트롤러
-
표준: IETF RFC 3986 Uniform Resource Identifier (URI) 일반 구문, OWASP 모바일 애플리케이션 보안 검증 표준, OWASP 모바일 Top 10 보안 위험 및 지침
-
연동 인터페이스: 애플리케이션 소유 딥 링크 라우트 검증기, 배포된 SDK 버전을 위한 문서화된 웨이크업 및 설치 파라미터 콜백, 플랫폼 네이티브 내비게이션 컨트롤러
-
공식 문서 및 참고 자료:
Share this article



