앱 스토어 링크용 QR 코드는 어떻게 만드나요? 앱 스토어 링크용 QR 코드를 만들려면 User-Agent 검사를 통해 사용자의 운영 체제를 감지하는 동적 라우팅 URL을 인코딩해야 합니다. 이를 통해 iOS 기기는 Apple App Store로, Android 기기는 Google Play로, 데스크톱 기기는 정보 제공용 랜딩 페이지로 자동 연결할 수 있습니다.
앱 QR 코드는 실시간 기기 운영 체제 감지를 바탕으로 스캔하는 사용자를 해당 네이티브 앱 스토어로 동적으로 연결해 주는 중앙 집중식 간접 URL을 인코딩하는 적응형 모바일 라우팅 바코드입니다. 고정된 플랫폼 종속성을 줄여 Apple App Store와 Google Play 생태계 전반에서 오프라인 인쇄 홍보물을 하나로 통합합니다.
| 용어 | 정의 | 관련 항목 | 검색 의도 역할 |
|---|---|---|---|
| 앱 QR 코드 | 스캔한 사용자를 플랫폼별 앱 스토어로 라우팅하는 통합 바코드입니다. | 유니버설 링크 / 인텐트 | 정보성 / 기술적 |
| 동적 QR 코드 | 인쇄 후에도 목적지를 변경할 수 있는 관리형 간접 URL을 인코딩하는 QR 코드입니다. | HTTP 302 리디렉션 | 기술적 |
| 앱용 QR 코드 생성 | 적응형 멀티 스토어 바코드를 생성하는 프로그래밍 방식 또는 콘솔 워크플로입니다. | 빠른 설치 및 공유 | 기술적 / 상업적 |
멀티 플랫폼의 문제점: 단일 QR 코드에서 직접 앱 스토어 링크가 실패하는 이유

직접 스토어 URL의 비호환성: Apple App Store 대 Google Play
제품 포장, 소매점 판매 시점(POS) 디스플레이, 옥외 빌보드, 이벤트 배너 등의 오프라인 마케팅 홍보물은 물리적 공간 제약을 받습니다. 모바일 애플리케이션 퍼블리셔가 홍보물을 인쇄할 때, 현실 세계의 사용자 관심을 디지털 애플리케이션 설치로 전환해 줄 디지털 관문이 필요합니다. 이러한 온·오프라인 연계를 위한 확립된 표준이 바로 ISO/IEC 18004 표준을 따르는 QR 코드입니다.
그러나 표준 앱 배포 아키텍처는 서로 분리된 두 개의 마켓플레이스 생태계로 나뉘어 있습니다.
- Apple App Store 인프라: iOS 기기에서 배포되는 애플리케이션은
https://apps.apple.com/app/id1234567890과 같은 Apple 전용 엔드포인트를 통해 카탈로그화됩니다. Apple 모바일 운영 체제는 이러한 URL을 네이티브로 가로채어 사용자를 로컬 App Store 애플리케이션으로 라우팅합니다. - Google Play 스토어 인프라: Android 기기에서 배포되는 애플리케이션은
https://play.google.com/store/apps/details?id=com.example.app과 같은 Google Play 엔드포인트를 통해 카탈로그화됩니다. Android 기기는 네이티브 Google Play 스토어 클라이언트를 통해 이러한 URL을 해결합니다.
퍼블리셔가 Apple App Store로 향하는 직접적이고 고정된 링크를 QR 코드에 인코딩하면, 해당 바코드를 스캔한 Android 기기에서는 설치 인터페이스가 실행되지 않습니다. Google Play 서비스는 Apple App Store 제품 식별자를 처리할 수 없기 때문입니다. 반대로 직접 Google Play URL을 인코딩하면 iOS 사용자는 네이티브 IPA 설치를 실행할 수 없는 웹 기반 Google Play 인터페이스로 유입되게 됩니다. 직접 스토어 방식의 QR 코드는 하드코딩된 목적지가 클라이언트 하드웨어 기능을 평가하거나 타겟 URL을 조정할 수 없기 때문에 인쇄 후 라우팅 유연성이 제한적입니다.
양쪽에 나란히 두 개의 QR 코드를 인쇄할 때 발생하는 운영상의 리스크
플랫폼 간 격차를 우회하기 위해 마케팅 팀은 종종 “iPhone용 스캔” 및 “Android용 스캔”이라는 라벨이 붙은 두 개의 바코드를 물리적 자산에 나란히 인쇄하는 방식을 사용합니다.
이러한 접근 방식은 중간 웹 개발 단계를 피할 수 있지만 다음과 같은 심각한 운영, 인체 공학적, 시각적 리스크를 초래합니다.
- 시각적 혼잡 및 스캔율 저하: 경쟁하는 두 개의 바코드를 인쇄하면 소중한 물리적 표면 공간을 낭비하게 되며 사용자에게 인지적 마찰을 주어 스캔 전에 라벨을 확인하도록 강요합니다.
- 사용자 오류 및 이탈: 모바일 사용자가 실수로 잘못된 바코드를 스캔하여 호환되지 않는 스토어 페이지나 플랫폼 오류를 마주하고 온보딩 과정을 완전히 포기하게 됩니다.
- 자산별 기여도 측정의 한계: 직접 스토어 방식의 QR 코드는 지원되는 스토어 어트리뷰션 파라미터를 통해 기본적인 캠페인 수준의 측정은 지원할 수 있지만, 중앙에서 관리되는 스캔 로그나 인쇄 후 라우팅 유연성을 자체적으로 제공하지는 않습니다.
인쇄 후 취약성: 번들 ID나 마케팅 목적지가 변경될 때 발생하는 문제
물리적 인쇄에는 엄격한 운영 일정 타이밍이 따릅니다. 패키징 인쇄물, 인쇄 잡지, 컨퍼런스 브로셔, 옥외(OOH) 빌보드는 소프트웨어 캠페인이 라이브되기 몇 주 또는 몇 달 전에 기획되고 인쇄되는 경우가 많습니다.
직접 스토어 방식의 바코드는 제품 라이프사이클 전반에 걸쳐 심각한 취약성을 유발합니다.
- 출시 전 인쇄 마감일: 모바일 애플리케이션이 Apple이나 Google로부터 공식 검토 승인을 받기 전에 마케팅 홍보물을 인쇄해야 하는 경우, App Store 검수자가 앱의 번들 식별자나 기본 앱 식별자 수정을 요구하면 직접 링크 인쇄로 인해 문제가 발생할 수 있습니다.
- 캠페인 종료 후 재활용 불가: 일회성 프로모션 캠페인이 종료되면, 업데이트된 프로모션이나 보조 랜딩 페이지를 가리키도록 서버에서 직접 스토어 링크를 수정할 수 없습니다.
- 깨진 인쇄 자산: 관리되지 않는 단축 링크 리디렉션 서비스가 API를 폐기하거나 도메인 구조를 변경하면 인쇄된 패키징과 배너가 영구적으로 손상되어, 비용이 많이 드는 물리적 재인쇄 없이는 해결할 수 없는 HTTP 404 오류가 발생합니다.
클라이언트 및 서버 측 User-Agent 감지는 앱 스토어를 어떻게 라우팅하는가
리디렉션 모델 평가: HTTP 302 엣지 서버 리디렉션 대 클라이언트 측 웹 트랜짓 페이지

멀티 플랫폼 딜레마를 해결하기 위해 앱 QR 코드는 중앙 집중식 라우팅 게이트웨이를 가리키는 간접 URL을 인코딩해야 합니다. 라우팅 게이트웨이는 들어오는 HTTP 요청을 평가하고 사용자에게 적절한 목적지를 전달합니다.
아키텍처 관점에서 엔지니어링 팀은 캠페인 추적 요구사항에 따라 두 가지 주요 라우팅 전략을 구분해야 합니다.
- 직접 스토어 리디렉션 (HTTP 302 서버 측): 스캔하는 기기가 라우팅 서버에 요청을 보내고, 서버는 들어오는
User-Agent헤더를 검사한 후 즉시 브라우저를 대상 스토어로 안내하는 HTTP 302 Found 응답을 발행합니다. 이 모델은 지연 시간이 가장 짧습니다. 그러나 사용자가 중간 웹 페이지를 렌더링하지 않기 때문에 이 경로에서는 클라이언트 측 JavaScript SDK가 실행될 수 없습니다. iOS 기기의 경우 App Store에 도달하기 전에 웹 스크립트를 통해 파라미터를 캐시할 수 없으며, Android의 경우 Google Playreferrer=URL 파라미터를 통해서만 파라미터가 순수하게 전달되어야 합니다. - 웹 트랜짓 라우팅 (H5 중간 페이지): 라우팅 서버가 스캔 기기를 적응형 모바일 랜딩 페이지(
universal_app_qr_transit.html)로 전달합니다. 이 트랜짓 페이지는 OpoInstall Web JS SDK를 초기화하여 캠페인 쿼리 파라미터를 수집하고, 터치 속성을 로컬에서 평가하며, 접근 가능한 네이티브 스토어 버튼을 표시합니다. 이 모델은 캠페인에 풍부한 브랜딩, 맞춤형 QR 코드 스타일링, 또는 스토어 탐색 전 클라이언트 측 파라미터 캐싱이 필요할 때 필수적입니다.
최신 모바일 브라우저에서의 User-Agent 문자열 구문 분석 및 클라이언트 힌트 보안
엣지 서버에서 실행되든 클라이언트 측 트랜짓 스크립트내에서 실행되든, 운영 체제 식별은 브라우저 환경 토큰 검사에 의존합니다.
- iOS 평가:
User-Agent문자열에서iPhone,iPad, 또는iPod같은 토큰을 검사합니다. 최신 터치 기기의 경우, 데스크톱 모드로 작동하는 iPadOS 하드웨어를 정확히 감지하기 위해 데스크톱으로 보고하는 플랫폼에서 클라이언트 스크립트가navigator.maxTouchPoints > 1도 평가해야 합니다. - Android 평가:
User-Agent문자열에서Android토큰을 검사합니다. 최신 Chromium 기반 모바일 브라우저에서는 서버가 레거시 문자열 일치에만 의존하지 않고 플랫폼 ID를 확인하기 위해 User-Agent 클라이언트 힌트(Sec-CH-UA-Platform: "Android")를 평가할 수도 있습니다. - 데스크톱 평가: iOS나 Android 지표가 감지되지 않으면 시스템은 방문자를 데스크톱 또는 워크스테이션 사용자(예: Windows, macOS, Linux)로 분류합니다.
데스크톱 스캐너 처리: 대체 랜딩 페이지 및 웹 쇼케이스 제공
사용자는 종종 노트북 웹캠, 데스크톱 미리보기 도구 또는 웹 브라우저 확장 프로그램을 사용하여 실제 QR 코드를 스캔합니다. 또한 사용자는 기본 라우팅 링크를 데스크톱 브라우저에 복사하여 붙여넣을 수도 있습니다.
모든 트래픽이 모바일 기기에서 발생한다고 가정하는 단순한 라우팅 스크립트는 데스크톱 사용자 에이전트를 만났을 때 실패합니다. Windows의 데스크톱 Chrome 브라우저를 모바일 market:// URI 스킴이나 iOS App Store 제품 URL로 라우팅하면 처리되지 않는 프로토콜 오류가 발생합니다.
프로덕션 등급의 앱 QR 코드 아키텍처는 명시적인 데스크톱 폴백(Fallback)을 구현합니다.
- 데스크톱 웹 쇼케이스: 데스크톱 방문자를 제품 기능을 강조하고, 데스크톱 웹앱 대안을 표시하거나, 모바일 스캔을 위한 고해상도 QR 코드를 임베드한 정보성 데스크톱 랜딩 페이지로 안내합니다.
- SMS 또는 이메일 발송기: 데스크톱 방문자가 모바일 전화번호나 이메일 주소로 설치 링크를 직접 보낼 수 있는 입력 필드를 제공합니다.
기기 오인식을 방지하기 위한 iPadOS 데스크톱급 브라우징 헤더 관리

iPadOS 13부터 Apple은 iPad의 Safari가 기본적으로 데스크톱 웹사이트를 요청하도록 구성했습니다. 결과적으로 브라우저는 macOS의 Safari와 동일한 User-Agent 문자열을 전송합니다.
Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Safari/605.1.15
앱 QR 코드 라우팅 스크립트가 iPad 문자열만 단순하게 확인하는 경우, iPad를 데스크톱 컴퓨터로 오인하여 터치 지원 태블릿을 App Store에서 멀어지게 라우팅하게 됩니다.
서버 측 HTTP 요청 핸들러는 클라이언트 힌트 없이는 터치 하드웨어를 직접 관찰할 수 없는 들어오는 헤더만 검사합니다. 따라서 web-transit 아키텍처의 서버 측 로직이 Macintosh 사용자 에이전트를 만나면 요청을 클라이언트 측 랜딩 페이지로 라우팅하며, 이곳에서 브라우저 JavaScript가 레거시 토큰과 터치 기능을 모두 평가합니다.
// 견고한 iOS 및 iPadOS 감지
var isIOS = /iPhone|iPad|iPod/.test(navigator.userAgent) ||
(navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1);
플랫폼이 MacIntel을 보고하는 동시에 navigator.maxTouchPoints > 1임을 확인하여, 클라이언트 스크립트는 iPadOS 태블릿을 실제 데스크톱 Mac 컴퓨터와 정확히 구별하며 iPad 사용자에게는 App Store 목적지를 제시하고 데스크톱 Mac 방문자는 제품 쇼케이스로 라우팅합니다.
유니버설 앱 QR 코드 라우팅 엔진의 기술 아키텍처
간접 토폴로지: 단일 물리적 매트릭스 ──> 중앙 집중식 라우터 ──> 플랫폼 목적지
유니버설 앱 QR 코드 시스템은 데이터의 물리적 표현을 디지털 라우팅 로직과 분리합니다.
이 아키텍처는 세 가지 핵심 계층으로 구성됩니다.
- 물리적 매트릭스: 패키징, 빌보드 또는 전단지에 인쇄된 영구적인 단축 URL(예:
https://trk.example.com/app)을 인코딩하는 표준화된 ISO/IEC 18004 바코드입니다. 기업 로고 삽입, 브랜드 색상 선택, 모서리 눈 모양 조정 등의 맞춤형 시각적 브랜딩은 순수한 물리적 매트릭스 렌더링에만 적용되며 기본 간접 프로토콜을 변경하지 않습니다. - 중앙 라우팅 게이트웨이: 스캔 메타데이터를 기록하고, 수신 클라이언트 헤더를 검사하며, 캠페인 쿼리 파라미터를 살균(Sanitize)하고, 타겟 엔드포인트를 결정하는 고가용성 웹 서비스입니다.
- 타겟 목적지: Apple App Store, Google Play Store, 맞춤형 APK 배포 서버, 또는 반응형 데스크톱 랜딩 페이지를 포함한 플랫폼별 목적지입니다.
[오프라인 홍보물] ──> 사용자가 유니버설 앱 QR 코드 스캔
│
▼
[중앙 집중식 라우팅 엔드포인트]
│
┌───────────────────┼───────────────────┐
▼ ▼ ▼
[iOS 기기] [Android 기기] [데스크톱 브라우저]
│ │ │
▼ ▼ ▼
[Apple App Store] [Google Play Store] [웹 랜딩 페이지]
(앱 ID 타겟) (리퍼러 메타데이터) (기능 쇼케이스)
운영 체제 디스패치 전반에 걸쳐 마케팅 및 캠페인 추적 파라미터 보존
기능적인 앱 QR 코드 단순히 사용자 올바른 스토어로 라우팅하는 것에 그쳐서는 안 되며, 스토어 전환 경계 전반에 걸쳐 캠페인 어트리뷰션 컨텍스트를 유지해야 합니다.
마케팅 팀이 인쇄된 홍보물을 배포할 때 라우팅 URL에 추적 파라미터를 추가합니다.
https://trk.example.com/app?channelCode=poster_chicago_01&utm_source=offline&utm_campaign=summer2026
중앙 집중식 라우팅 엔진은 초기 스캔 이벤트 동안 이러한 쿼리 파라미터를 캡처합니다. 다운스트림 분석 시스템이 이 데이터를 수신하도록 보장하기 위해:
- Android 처리: 라우팅 엔진 또는 트랜짓 스크립트가 살균된 파라미터를 공식
referrer파라미터에 인코딩하는 Google Play 스토어 URL을 구축합니다. 사용자가 앱을 설치하고 실행하면 Android 애플리케이션이 Google Play Install Referrer API를 통해 이러한 파라미터를 검색합니다.
getInstallParam)가 최초 실행 시 채널 컨텍스트를 복구할 수 있게 됩니다.개인정보 보호 및 보안 경계: 오픈 리디렉션 취약점 방지

동적 리디렉션 엔드포인트를 배포하면 아키텍처 보안 의무가 발생합니다. 유효성 검증되지 않은 리디렉션 및 포워드에 관한 OWASP 지침에 따라, 검증되지 않은 리디렉션 스크립트는 악의적인 행위자가 기업의 신뢰할 수 있는 도메인을 사용하여 피해자를 악성 웹사이트로 유도하는 피싱 링크를 구성하는 데 악용될 수 있습니다.
안전한 앱 QR 코드 라우팅 아키텍처는 엄격한 URL 유효성 검사를 시행합니다.
- 목적지 허용 목록(Allowlist): 라우팅 엔진은 승인된 목적지 호스트 이름(예:
apps.apple.com,play.google.com또는 검증된 기업 하위 도메인)으로만 리디렉션해야 합니다. 쿼리 파라미터를 통해 지정된 임의의 외부 리디렉션 타겟은 거부되어야 합니다. - 입력 살균(Sanitization): 스토어 목록이나 트랜짓 스크립트에 전달되는 쿼리 파라미터 키와 값은 엄격한 영숫자 허용 목록에 대해 유효성이 검사되어야 하며, 스크립트 태그, 줄 바꿈 및 프로토콜 수정자는 제거되어야 합니다.
- 개인정보 보호 중심 어트리뷰션: 시스템 라우팅은 명시적인 HTTP 요청 헤더(User-Agent), 승인된 스토어 API(Play Install Referrer) 및 투명한 세션 컨텍스트에 의존해야 합니다. 하드웨어 엔트로피를 사용하여 지속적인 기기 지문(Signature)을 생성하려는 시도는 플랫폼 개인정보 보호 규칙을 위반합니다.
단계별 구현: 멀티 스토어 트랜짓 리디렉터 구축
접근 가능한 네이티브 폴백 앵커를 갖춘 유니버설 멀티 스토어 랜딩 페이지 구성
앱 QR 코드를 배포할 때 성장 조직(Growth Team)은 접근 가능한 네이티브 폴백 앵커를 유지하면서 휴리스틱 환경 검사를 수행하는 경량 클라이언트 측 트랜짓 랜딩 페이지를 호스팅할 수 있습니다.
[CODE_BLOCK_01]의 구현 예시는 반응형 HTML/JS 트랜짓 페이지를 보여줍니다. 이 스크립트는 허용 목록에 대해 들어오는 쿼리 파라미터의 유효성을 검사하고, 클라이언트 기능(레거시 iPad 토큰 및 iPadOS 터치 포인트 포함)을 검사하며, 유효한 Google Play 및 Apple App Store 목적지 URL을 동적으로 구축하고, 네이티브 앵커를 기본 내비게이션 소유자로 유지하면서 OpoInstall Web JS SDK를 초기화합니다.
구성 파라미터 및 파라미터 구문 분석에 대한 기술 사양은 SDK 통합 참조를 참조하세요.
<!-- 폴백이 포함된 적응형 듀얼 플랫폼 리디렉션 스크립트 -->
<!-- 파일: universal_app_qr_transit.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>앱 스토어로 계속 이동하기</title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
background-color: #f8f9fa;
color: #333;
text-align: center;
padding: 48px 16px;
margin: 0;
}
.transit-card {
background: #ffffff;
border-radius: 16px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.06);
max-width: 440px;
margin: 0 auto;
padding: 36px 24px;
box-sizing: border-box;
}
.app-icon {
width: 76px;
height: 76px;
border-radius: 18px;
margin-bottom: 20px;
object-fit: cover;
}
h2 {
font-size: 20px;
margin: 0 0 10px 0;
color: #111;
}
p {
font-size: 14px;
color: #666;
margin: 0 0 24px 0;
line-height: 1.5;
}
.btn-download {
display: inline-block;
background-color: #007aff;
color: #ffffff;
font-size: 16px;
font-weight: 600;
padding: 14px 32px;
border-radius: 24px;
text-decoration: none;
cursor: pointer;
border: none;
transition: background-color 0.2s ease;
}
.btn-download:hover {
background-color: #0062cc;
}
.footer-note {
font-size: 12px;
color: #999;
margin-top: 18px;
}
</style>
</head>
<body>
<div class="transit-card">
<img src="https://cdn.example.com/assets/app-icon.png" alt="앱 아이콘" class="app-icon">
<h2 id="card-title">앱 스토어로 계속 이동하기</h2>
<p id="card-desc">기기에 맞는 앱 스토어를 열려면 아래 버튼을 선택하세요.</p>
<!-- 접근 가능한 기본 내비게이션 앵커 링크 -->
<!-- 프로덕션 참고: 기본 href를 라이브 반응형 랜딩 페이지 또는 데스크톱 쇼케이스 URL로 교체하세요 -->
<a id="store-action-btn" class="btn-download" href="https://app.example.com/landing">응용 프로그램 다운로드</a>
<p class="footer-note">올바른 목적지로 계속 이동하려면 위 버튼을 탭하세요.</p>
</div>
<!-- OpoInstall Web JS SDK 포함 (배포된 프로덕션 릴리스에 맞는지 확인하세요) -->
<script src="https://web.cdn.opoinstallcloud.com/opoinstall.js"></script>
<script>
(function() {
// 1단계: 물리적 QR 스캔에서 수신된 캠페인 파라미터 구문 분석 및 살균
function getSanitizedQueryParams() {
var allowedKeys = new Set([
"channelCode",
"utm_source",
"utm_medium",
"utm_campaign",
"inviter_id"
]);
var result = Object.create(null);
try {
var query = new URLSearchParams(window.location.search);
// 파라미터 값 유효성 검사: 영숫자, 하이픈, 언더바, 점 (최대 64자)
var validPattern = /^[A-Za-z0-9_.-]{1,64}$/;
query.forEach(function(value, key) {
if (allowedKeys.has(key) && validPattern.test(value)) {
if (!Object.prototype.hasOwnProperty.call(result, key)) {
result[key] = value;
}
}
});
} catch (e) {
return Object.create(null);
}
return result;
}
var campaignData = getSanitizedQueryParams();
// 2단계: 레거시 iPad 및 iPadOS 데스크톱 모드 처리가 포함된 클라이언트 측 User-Agent 검사
var ua = navigator.userAgent || "";
var isIOS = /iPhone|iPad|iPod/.test(ua) || (navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1);
var isAndroid = /Android/.test(ua);
var actionBtn = document.getElementById("store-action-btn");
var cardTitle = document.getElementById("card-title");
var cardDesc = document.getElementById("card-desc");
// 3단계: 플랫폼별 스토어 목적지 URL 구축
if (actionBtn) {
if (isIOS) {
// Apple App Store로 라우팅 (숫자 앱 스토어 ID)
// 참고: 표준 iOS App Store 링크는 임의의 쿼리 문자열을 구문 분석하지 않습니다.
// 파라미터는 Web JS SDK 지연 캐싱을 통해 보존됩니다.
actionBtn.href = "https://apps.apple.com/app/id1234567890";
actionBtn.innerText = "App Store에서 다운로드";
} else if (isAndroid) {
// 올바르게 구조화된 'referrer' 페이로드와 함께 Google Play 스토어로 라우팅
try {
var playUrl = new URL("https://play.google.com/store/apps/details");
playUrl.searchParams.set("id", "com.example.app");
var referrerParams = new URLSearchParams();
["channelCode", "utm_source", "utm_medium", "utm_campaign", "inviter_id"].forEach(function(paramKey) {
if (campaignData[paramKey]) {
referrerParams.set(paramKey, campaignData[paramKey]);
}
});
if (referrerParams.toString()) {
playUrl.searchParams.set("referrer", referrerParams.toString());
}
actionBtn.href = playUrl.toString();
actionBtn.innerText = "Google Play에서 다운로드";
} catch (e) {
actionBtn.href = "https://play.google.com/store/apps/details?id=com.example.app";
}
} else {
// 비모바일 데스크톱 폴백: 데스크톱 방문자를 위한 카드 UI 제목 업데이트
actionBtn.href = "https://app.example.com/desktop-showcase";
actionBtn.innerText = "제품 개요 보기";
if (cardTitle) cardTitle.innerText = "데스크톱 환경 살펴보기";
if (cardDesc) cardDesc.innerText = "웹 제품 쇼케이스를 둘러보거나 모바일 기기에서 스캔하세요.";
}
}
// 4단계: 단일 소유자 내비게이션 원칙으로 Web JS SDK 초기화
// 위에서 구축된 actionBtn.href는 기본 접근 가능한 목적지로 유지됩니다.
// SDK는 앵커 내비게이션을 덮어쓰지 않고 파라미터를 수집하고 캐시하도록 초기화됩니다.
if (typeof OpoInstall !== "function") {
console.warn("OpoInstall 웹 SDK를 사용할 수 없습니다. 직접 스토어 내비게이션 앵커로 계속 진행합니다.");
} else {
try {
new OpoInstall({
appKey: "OPOINSTALL_APPKEY_PLACEHOLDER",
data: campaignData,
onready: function() {
// SDK가 준비 상태를 보고합니다.
// 파라미터 지속성 및 후속 설치 검색은 배포된 SDK 계약 및 플랫폼 매칭 지원에 따라 달라집니다.
// 내비게이션은 actionBtn.href가 단독 소유합니다.
}
});
} catch (error) {
console.error("OpoInstall SDK 초기화 오류; 네이티브 스토어 href는 활성 상태로 유지됩니다:", error);
}
}
})();
</script>
</body>
</html>
프로그래밍 방식 앱 QR 코드 생성: 고해상도 ISO/IEC 18004 매트릭스 자산 제작
수백 개의 인쇄 버전을 관리하는 엔터프라이즈 캠페인의 경우, 바코드는 디자인 소프트웨어에서 수동으로 내보내는 대신 프로그래밍 방식으로 생성되어야 합니다.
[CODE_BLOCK_02]의 Node.js 구현은 유니버설 앱 QR 코드 시스템의 두 가지 구성 요소를 모두 보여줍니다. 실시간으로 수신 요청 헤더를 평가하는 실행 가능한 서버 측 HTTP 302 리디렉션 핸들러(엄격한 ROUTING_MODE 유효성 검사: direct-store 대 web-transit)와 트랜짓 URL의 유효성을 검사하고 ISO/IEC 18004 사양을 준수하는 표준 기반 QR 코드 generator를 사용하여 Level Q 오류 정정 기능이 포함된 인쇄용 PNG 바코드 이미지 파일을 렌더링하는 자동화 유틸리티입니다.
1024픽셀 PNG를 생성하면 고해상도 디지털 마스터가 제공되므로, 실제 물리적 인쇄 치수는 실제 스캔 거리를 기준으로 계산하고 인쇄 교정본(Proof)으로 검증해야 합니다.
// 서버 측 HTTP 302 리디렉션 및 QR 코드 생성 스크립트
// 파일: server_qr_router.js
// 종속성: npm install qrcode
const http = require('http');
const QRCode = require('qrcode'); // ISO/IEC 18004 사양을 준수하는 표준 기반 QR 생성기
/**
* 서버 구성 객체:
* 프로덕션에서는 포트 및 라이브 엔드포인트 주소에 대한 환경 변수를 구성하세요.
* routingMode: 'direct-store'(스토어로 즉시 302) 또는 'web-transit'(SDK 캐싱을 위해 H5 트랜짓 페이지로 라우팅)
*/
const SERVER_CONFIG = {
port: process.env.PORT || 3000,
routingMode: process.env.ROUTING_MODE || "web-transit",
iosStoreUrl: "https://apps.apple.com/app/id1234567890",
androidPackageId: "com.example.app",
webTransitUrl: process.env.WEB_TRANSIT_URL || "https://app.example.com/universal_app_qr_transit.html",
desktopFallbackUrl: "https://app.example.com/desktop-overview"
};
/**
* 호스트 이름이 연속된 점이나 잘못된 라벨 형식 없이
* RFC 1035 / RFC 1123 DNS 사양을 준수하는지 확인합니다.
*/
function isValidHostname(host) {
if (typeof host !== "string" || host.length > 253) {
return false;
}
const labels = host.split(".");
if (labels.length < 2) {
return false;
}
return labels.every(label =>
label.length >= 1 &&
label.length <= 63 &&
/^[A-Za-z0-9](?:[A-Za-z0-9-]*[A-Za-z0-9])?$/.test(label)
);
}
/**
* 환경 구성에서 승인된 호스트 허용 목록을 엄격하게 가져옵니다.
*/
function getApprovedHosts(envVarName) {
const raw = process.env[envVarName];
if (!raw) {
throw new Error(`${envVarName} 환경 변수는 명시적으로 구성되어야 합니다.`);
}
const hosts = new Set(
raw.split(",")
.map(host => host.trim().toLowerCase())
.filter(Boolean)
);
if (hosts.size === 0) {
throw new Error(`${envVarName} 허용 목록은 비워둘 수 없습니다.`);
}
return hosts;
}
/**
* 서버 시작 전 구성 스키마 및 엄격한 routingMode 열거형 값 유효성 검사
*/
function validateConfig() {
// ROUTING_MODE에 대한 엄격한 열거형 유효성 검사 강제
const allowedModes = new Set(["direct-store", "web-transit"]);
if (!allowedModes.has(SERVER_CONFIG.routingMode)) {
throw new Error(`잘못된 ROUTING_MODE: "${SERVER_CONFIG.routingMode}". 반드시 "direct-store" 또는 "web-transit"여야 합니다.`);
}
const portNum = Number(SERVER_CONFIG.port);
if (!Number.isInteger(portNum) || portNum < 1 || portNum > 65535) {
throw new Error(`잘못된 PORT 구성: "${SERVER_CONFIG.port}". 유효한 TCP 포트 번호여야 합니다.`);
}
// 라우터 호스트 허용 목록 환경 변수가 명시적으로 설정되었는지 확인
getApprovedHosts("APPROVED_API_HOSTS");
if (SERVER_CONFIG.routingMode === "web-transit") {
const transitUrl = new URL(SERVER_CONFIG.webTransitUrl);
if (transitUrl.protocol !== "https:") {
throw new Error("WEB_TRANSIT_URL 구성은 HTTPS 프로토콜을 사용해야 합니다.");
}
}
}
/**
* 명시적 허용 목록에 대해 수신 URL 파라미터 살균
*/
function sanitizeQueryParams(searchParams) {
const allowedKeys = ["channelCode", "utm_source", "utm_medium", "utm_campaign", "inviter_id"];
const validPattern = /^[A-Za-z0-9_.-]{1,64}$/;
const sanitized = new URLSearchParams();
allowedKeys.forEach(key => {
const val = searchParams.get(key);
if (val && validPattern.test(val)) {
sanitized.set(key, val);
}
});
return sanitized;
}
/**
* 수신 요청 헤더를 평가하고 라우팅 모드에 따라 목적지 URL을 반환합니다.
*/
function resolveStoreDestination(userAgentString, searchParams) {
const ua = userAgentString || "";
const sanitizedParams = sanitizeQueryParams(searchParams);
// 활성 모드는 서버 시작 중에 엄격하게 검증됩니다.
const activeMode = SERVER_CONFIG.routingMode;
// 모드: web-transit (모바일 및 모호한 데스크톱급 UA를 H5 트랜짓 페이지로 라우팅)
if (activeMode === "web-transit") {
const isMobileOrTablet = /iPhone|iPad|iPod|Android/.test(ua) || /Macintosh/.test(ua);
if (isMobileOrTablet) {
const transitUrl = new URL(SERVER_CONFIG.webTransitUrl);
sanitizedParams.forEach((value, key) => {
transitUrl.searchParams.set(key, value);
});
return transitUrl.toString();
}
return SERVER_CONFIG.desktopFallbackUrl;
}
// 모드: direct-store (앱 스토어 목록으로 즉시 HTTP 302)
const isIOS = /iPhone|iPad|iPod/.test(ua);
const isAndroid = /Android/.test(ua);
if (isIOS) {
return SERVER_CONFIG.iosStoreUrl;
} else if (isAndroid) {
const playUrl = new URL("https://play.google.com/store/apps/details");
playUrl.searchParams.set("id", SERVER_CONFIG.androidPackageId);
// 살균된 파라미터를 Google Play의 공식 referrer 파라미터에 인코딩
if (sanitizedParams.toString()) {
playUrl.searchParams.set("referrer", sanitizedParams.toString());
}
return playUrl.toString();
}
return SERVER_CONFIG.desktopFallbackUrl;
}
/**
* 경량 HTTP 302 리디렉션 서버
* 참고: 프로덕션에서는 HTTPS를 종료하는 인증 리버스 프록시(예: Nginx, Cloudflare) 뒤에서 실행하세요.
* 신뢰할 수 있는 업스트림 리버스 프록시에서 직접 호스트 헤더를 검사합니다.
*/
function startRoutingServer() {
// 네트워크 소켓을 열기 전에 엄격한 구성 유효성 검사 강제
validateConfig();
const server = http.createServer((req, res) => {
try {
// 리디렉션 엔드포인트에 대한 HTTP 메서드를 GET 및 HEAD로 제한
if (req.method !== "GET" && req.method !== "HEAD") {
res.writeHead(405, { "Content-Type": "text/plain", "Allow": "GET, HEAD" });
res.end("Method Not Allowed");
return;
}
// 신뢰할 수 있는 리버스 프록시에서 직접 호스트 헤더 읽기
// 참고: .split(":")[0]은 표준 호스트 허용 목록 평가를 위해 선택적 포트 번호를 스트립합니다.
const rawHost = req.headers.host || "";
const hostHeader = rawHost.split(":")[0];
const approvedRouterHosts = getApprovedHosts("APPROVED_API_HOSTS");
if (!isValidHostname(hostHeader) || !approvedRouterHosts.has(hostHeader.toLowerCase())) {
res.writeHead(403, { "Content-Type": "text/plain" });
res.end("Forbidden: Access via unauthorized host header.");
return;
}
const requestUrl = new URL(req.url, `https://${hostHeader}`);
// 리디렉션 처리를 승인된 경로 엔드포인트로 제한
if (requestUrl.pathname !== "/app") {
res.writeHead(404, { "Content-Type": "text/plain" });
res.end("Not Found");
return;
}
const destination = resolveStoreDestination(req.headers["user-agent"], requestUrl.searchParams);
// HTTP 302 Found 리디렉션 실행
res.writeHead(302, {
"Location": destination,
"Cache-Control": "no-cache, no-store, must-revalidate"
});
res.end();
} catch (err) {
res.writeHead(500, { "Content-Type": "text/plain" });
res.end("Internal Routing Error");
}
});
server.listen(SERVER_CONFIG.port, () => {
console.log(`[라우터 활성] 포트 ${SERVER_CONFIG.port}에서 ${SERVER_CONFIG.routingMode} 모드로 수신 대기 중인 HTTP 302 서버`);
});
return server;
}
/**
* 검증된 유니버설 트랜짓 URL에 연결되는 ISO/IEC 18004 규격 고해상도 PNG 바코드 이미지를 생성합니다.
*/
async function generateUniversalAppQRCode(transitUrlString, outputFilePath = "universal_app_qr.png") {
let parsedUrl;
try {
parsedUrl = new URL(transitUrlString);
} catch (err) {
throw new Error(`잘못된 URL 문자열이 제공되었습니다: "${transitUrlString}". 유효한 URI여야 합니다.`);
}
if (parsedUrl.protocol !== "https:") {
throw new Error(`안전하지 않은 프로토콜 감지됨: ${parsedUrl.protocol}. 앱 QR 코드 목적지는 HTTPS를 사용해야 합니다.`);
}
if (!isValidHostname(parsedUrl.hostname)) {
throw new Error(`잘못된 형식의 DNS 호스트 이름 감지됨: "${parsedUrl.hostname}".`);
}
const approvedQrHosts = getApprovedHosts("APPROVED_QR_HOSTS");
if (!approvedQrHosts.has(parsedUrl.hostname.toLowerCase())) {
throw new Error(`승인되지 않은 호스트 도메인: "${parsedUrl.hostname}". APPROVED_QR_HOSTS에 호스트가 없습니다.`);
}
console.log(`[유효성 검사 통과] 다음을 위한 유니버설 앱 QR 코드 생성 중: ${parsedUrl.toString()}`);
const qrOptions = {
errorCorrectionLevel: 'Q', // 인쇄 홍보물에 최적화된 레벨 Q (25% 손상 복구)
type: 'image/png',
margin: 4, // 규격 준수 4모듈 여백(Quiet Zone)
width: 1024 // 인쇄 제작 스케일링에 적합한 고해상도 디지털 마스터
};
try {
await QRCode.toFile(outputFilePath, parsedUrl.toString(), qrOptions);
console.log(`[성공] 다음 위치에 기록된 고해상도 바코드 이미지: ${outputFilePath}`);
return outputFilePath;
} catch (err) {
throw new Error(`QR 코드 이미지 자산 생성 실패: ${err.message}`);
}
}
// 독립 실행 진입점
if (require.main === module) {
if (process.argv.includes("--serve")) {
startRoutingServer();
} else {
const sampleUrl = process.env.TARGET_TRANSIT_URL || "https://trk.example.com/app?channelCode=poster_01";
generateUniversalAppQRCode(sampleUrl, "app_store_qr.png")
.then((file) => console.log(`빌드 완료: ${file}`))
.catch((err) => {
console.error(`빌드 실패: ${err.message}`);
process.exit(1);
});
}
}
module.exports = { startRoutingServer, generateUniversalAppQRCode, resolveStoreDestination };
멀티 스토어 앱 QR 코드 전략 비교 분석
단일 직접 링크, 듀얼 인쇄 코드, 정적 웹 리디렉터, 동적 앱 QR 코드 비교
최적의 QR 배포 전략을 선택하려면 사용자 마찰, 기술적 복잡성, 크로스 플랫폼 도달 범위, 인쇄 후 유지보수 리스크 간의 균형을 맞춰야 합니다.
아래 비교 표에서는 일반적인 구현 모델별 운영상의 트레이드오프를 설명합니다.
| 배포 방식 | 크로스 플랫폼 지원 | 인쇄 후 편집 가능 여부 | 사용자 스캔 마찰 | 유지보수 리스크 |
|---|---|---|---|---|
| 직접 앱 스토어 링크 | 단일 플랫폼 전용 | 아니오 (고정 URL) | 제외된 OS의 경우 높음 | 높음 (URL 변경 시 인쇄물 폐기됨) |
| 나란히 인쇄된 듀얼 코드 | 사용자 수동 선택 | 아니오 (고정 URL) | 높음 (스캔 오류 및 시각적 혼잡) | 높음 (인쇄 공간 낭비 및 링크 유실 리스크) |
| 일반 정적 HTML 페이지 | 스크립트 의존적 | 부분적 (도메인 수준만 가능) | 보통 (중간 페이지 클릭 필요) | 보통 (수동 웹사이트 호스팅 오버헤드) |
| 동적 유니버설 앱 QR 코드 | 자동화된 OS 디스패치 | 예 (콘솔/API 제어) | 최소화 (자동화된 스토어 라우팅) | 가변적 (라우팅 도메인 및 간접 서비스 가용성에 따라 다름) |
유지보수 오버헤드, 스캔 마찰, 크로스 플랫폼 호환성 평가
직접 마켓플레이스 링크는 오프라인 매체에 있어서는 구시대적인 접근 방식입니다. App Store 링크를 하드코딩하면 지원되지 않는 플랫폼에서의 설치를 막게 되며, 나란히 배치된 두 개의 코드는 오프라인 홍보물의 미관을 떨어뜨리고 사용자 스캔 오류를 유발합니다.
동적 유니버설 앱 QR 코드를 배포하면 이러한 제약 사항이 해결됩니다. 관리되는 QR 링크는 iOS 및 Android 방문자를 플랫폼에 적합한 목적지로 안내하고 물리적 배치 식별자와 스캔 활동을 연결할 수 있습니다. 설치 어트리뷰션은 지원되는 SDK나 스토어 메커니즘 및 성공적인 매칭에 따라 유지됩니다. 마케팅 팀이 목적지를 직접 스토어 목록에서 인터랙티브 홀리데이 이벤트 랜딩 페이지로 변경하기로 결정한 경우, 중앙 관리 콘솔에서 목적지 URL만 업데이트하면 되므로 재인쇄가 전혀 필요 없습니다.
크로스 플랫폼 스캔 및 리디렉션 실패 문제를 해결하는 방법
인앱 소셜 브라우저의 스토어 URL 제한 우회하기
오프라인 사용자의 상당수는 네이티브 모바일 카메라 앱 대신 소셜 메시징 애플리케이션(WeChat, Line, WhatsApp 등)의 통합 카메라를 사용하여 QR 코드를 스캔합니다.
모바일 컨테이너 연구에서 밝혀진 바와 같이, 소셜 웹뷰는 엄격한 샌드박스를 강제합니다.
- 스킴 억제: 인앱 웹뷰는 사용자 정의 프로토콜 스킴(
market://,itms-apps-ios://등)을 자주 가로채고 차단하며 직접 패키지 다운로드를 억제합니다. - 트랜짓 핸드오프 실행: 막다른 길을 방지하기 위해 트랜짓 페이지는 User-Agent 검사를 통해 소셜 컨테이너를 감지하고, 사용자가 시스템 브라우저에서 목적지를 열도록 안내하는 접근 가능한 HTTPS 폴백 버튼이나 명확한 시각적 지침을 제공해야 합니다.
앱 스토어와 플레이 스토어 핸드오프 간의 파라미터 유실 진단
설치 후 애플리케이션이 연결된 캠페인 파라미터 없이 실행되는 경우(channelCode가 비어 있는 경우), 엔지니어링 팀은 다음 핸드오프 검사점을 감사해야 합니다.
- Google Play 레퍼러 포맷팅: Android 스토어 URL이 단일
referrer=쿼리 파라미터 내에 파라미터를 올바르게 인코딩하는지 확인하세요. 레퍼러 페이로드 내에 임베드하지 않고 Play 스토어 URL에 느슨한 파라미터를 직접 추가하면 Google Play가 메타데이터를 기록하는 것을 막게 됩니다. - 룩백 윈도우 제한: 사용자가 구성된 어트리뷰션 윈도우 외부에 앱을 설치하거나 실행하는 경우 적격 캠페인 컨텍스트가 더 이상 매칭되지 않을 수 있습니다. 배포된 SDK 및 플랫폼 구성에 대해 해당 윈도우를 확인하세요.
iPad 데스크톱 모드 처리: Mac 데스크톱과 터치 지원 태블릿 구분
테스트 결과 앱 QR 코드를 스캔한 iPad 사용자가 App Store 대신 데스크톱 랜딩 페이지로 리디렉션되는 것으로 나타나면 User-Agent 평가 로직을 점검하세요.
라우팅 스크립트가 iPad 문자열에만 독점적으로 의존하지 않는지 확인하세요. 데스크톱급 브라우징 모드로 작동하는 iPadOS 하드웨어를 정확히 감지하기 위해 클라이언트 로직이 MacIntel을 보고하는 플랫폼에서 navigator.maxTouchPoints > 1을 평가하는지 확인하세요.
물리적 인쇄 검사점: 경험적 거리 대 크기 비율 및 여백(Quiet Zone) 유효성 검사
많은 스캔 실패는 소프트웨어 라우팅 로직이 아니라 물리적 그래픽 제작 과정에서 발생합니다.
- 경험적 거리 대 크기 비율: 안정적인 카메라 초점을 보장하기 위해 경험적 크기 조정 휴리스틱을 따르세요:
. 2미터 떨어진 곳에서 스캔하도록 설계된 포스터에는 최소 20센티미터 크기의 QR 매트릭스가 필요합니다. 인쇄 제작을 승인하기 전에 반드시 실제 조명 조건에서 물리적 테스트 스캔을 수행하세요. - 여백(Quiet Zone) 보존: 바코드는 네 가지 바깥쪽 가장자리 주변으로 최소 4개 모듈 너비의 인쇄되지 않은 테두리(여백)를 유지해야 합니다. 이 여백과 겹치는 그래픽 테두리, 타이포그래피 또는 배경 텍스처는 카메라 가장자리 감지 알고리즘이 매트릭스 경계를 식별하는 것을 방해합니다.
- 오류 정정 튜닝: 야외 날씨, 환경적 마모 또는 곡선 패키징 표면에 노출되는 인쇄물은 ISO/IEC 18004 표준에 따라 레벨 M(15% 복구) 또는 레벨 Q(25% 복구) 오류 정정을 사용하여 생성해야 합니다.
실무 멀티 플랫폼 검증 매트릭스 (모듈 M2 모델)
앱 QR 코드 배포를 물리적 인쇄 생산용으로 승인하기 전에 엔지니어링 팀은 선택된 라우팅 모드를 고려하여 타겟 테스트 환경 전반에서 검증을 실행해야 합니다.
| 테스트 기기 / 컨테이너 | 라우팅 모드: direct-store | 라우팅 모드: web-transit | 임계 검사 체크포인트 |
|---|---|---|---|
| iPhone Safari | App Store로 즉시 302 | H5 트랜짓 페이지 로드 ──> 스토어 앵커 | 숫자 app-id 해상도 확인 |
| iPad Safari (레거시 UA) | App Store로 즉시 302 | H5 트랜짓 페이지 로드 ──> 스토어 앵커 | iPad 토큰 평가 확인 |
| iPadOS (데스크톱 모드) | 데스크톱 폴백으로 302 (UA 제한) | 터치 평가 ──> App Store로 라우팅 | MacIntel + maxTouchPoints > 1 분기 확인 |
| macOS 데스크톱 Safari | 데스크톱 폴백으로 302 | H5 로드 ──> 0개 터치 평가 ──> 데스크톱 CTA | 데스크톱 쇼케이스를 깔끔하게 표시 |
| Android Chrome | Play Store로 302 (referrer= 추가됨) |
H5 트랜짓 페이지 로드 ──> 플레이 앵커 | referrer= 인코딩 및 패키지 id 확인 |
| 데스크톱 워크스테이션 | 데스크톱 웹 쇼케이스로 302 | 데스크톱 레이아웃 / 웹 쇼케이스 로드 | 처리되지 않은 모바일 프로토콜 오류 방지 |
| 소셜 웹뷰 (WeChat/Line) | 컨테이너 종속적 (302 차단될 수 있음) | 지침이 포함된 H5 트랜짓 페이지 로드 | 수동 브라우저 핸드오프 프롬프트 유효성 검사 |
| 물리적 인쇄 교정본(Proof) | 카메라 스캔 시 즉시 리디렉션 | 카메라 스캔 시 트랜짓 URL 해결 | 여백(Quiet Zone) 및 레벨 Q 복구 확인 |
자주 묻는 질문 (FAQ)
단일 QR 코드로 iOS 및 Android 기기를 자동으로 감지할 수 있나요?
데스크톱 사용자가 앱 QR 코드를 스캔하거나 클릭하면 어떻게 되나요?
앱 QR 코드를 통해 캠페인 추적 파라미터를 전달하려면 어떻게 해야 하나요?
요약 및 구현 결정 프레임워크
모바일 애플리케이션 다운로드를 단일 유니버설 앱 QR 코드로 통합하면 물리적 인쇄 매체가 가진 근본적인 멀티 플랫폼 한계가 해결됩니다. 직접 마켓플레이스 링크를 하드코딩하면 홍보물이 조각나고 인쇄 비용이 증가하며, 제외된 운영 체제를 사용하는 사용자의 경우 전환 실패가 보장됩니다.
엔지니어링 및 마케팅 팀은 반응형 트랜짓 라우팅을 기반으로 하는 중앙 집중식 동적 앱 QR 코드를 구현해야 합니다. 자동화된 User-Agent 감지, 접근 가능한 네이티브 폴백 앵커, OpoInstall을 통한 파라미터 보존을 결합하여, 팀은 물리적 프로모션 자산이 모든 모바일 플랫폼에서 줄어든 마찰력으로 플랫폼에 적합한 라우팅을 지원하도록 보장할 수 있습니다.
유니버설 멀티 스토어 QR 코드를 만들고 자동화된 모바일 리디렉션을 구성하려면 OpoInstall 개발자 콘솔에서 애플리케이션 엔드포인트를 설정하거나 SDK 통합 참조를 검토하세요. 클라이언트 SDK 다운로드에 대해서는 OpoInstall SDK 다운로드를 방문하거나 모바일 어트리뷰션 참조에서 광범위한 모바일 라우팅 메커니즘을 살펴보세요.
관련 자료
-
개념: 앱 QR 코드, 동적 QR 코드, User-Agent 감지, 웹투앱 리디렉션
-
기술: ISO/IEC 18004 바코드 표준, HTTP 302 리디렉션, Apple App Store, Google Play Store
-
표준: IETF RFC 3986 URI(Uniform Resource Identifier) 구문, ISO/IEC 18004:2024 정보 기술
-
API: Google Play Install Referrer API, OpoInstall Web JS SDK
-
공식 문서 및 참고 자료:
Share this article



