Web

첫 클릭만 실패하는 공유 버그: WebKit User Activation과 CORS Preflight의 함정

Joonfluence 2026. 3. 15.

“웹에서는 잘 되는데 앱 웹뷰에서만 안 돼요.
Staging에서는 잘 되고, Production에서는 두 번째 클릭부터는 됩니다.”

프론트엔드 개발자라면 등골이 서늘해지는 전형적인 환경 의존 버그입니다.
로컬에서도 잘 되고 Staging에서도 잘 되는데 Production에서만 실패합니다.

게다가 더 이상한 점이 있습니다.

첫 번째 클릭 → 실패  
두 번째 클릭 → 성공

처음에는 WebView 문제라고 생각했습니다. 하지만 테스트를 진행하면서 예상과 다른 사실을 발견했습니다.

iOS Safari → 실패  
iOS WebView → 실패  
Android Chrome → 간헐적 실패

즉 이 문제는 단순한 WebView 버그가 아니었습니다.
문제의 핵심은 WebKit의 User Activation 정책과 초기 네트워크 지연이었습니다.

발단: 공유 버튼과 비동기 API

문제가 발생한 기능은 레퍼럴 공유였습니다.
사용자가 공유 버튼을 누르면 레퍼럴 코드를 생성한 뒤 카카오톡 공유 UI를 호출하는 구조였습니다.

async function shareReferral() {
  const { code } = await api.post("/v1/referral/codes")

  Kakao.Share.sendDefault({
    objectType: "feed",
    content: {
      title: `${customerName}님이 공유한 혜택 🎁`,
      description: "와이즐리가 처음이라면\n🎁 10,000원 쿠폰 + 첫 달 100원",
      imageUrl: SHARE_IMAGE_URL,
      link: { mobileWebUrl: shareUrl, webUrl: shareUrl },
    },
    buttons: [
      {
        title: "쿠폰 받으러 가기",
        link: { mobileWebUrl: shareUrl, webUrl: shareUrl },
      },
    ],
  })
}

겉보기에는 단순한 JavaScript 함수 호출처럼 보입니다.

하지만 Kakao.Share.sendDefault()는 실제로 카카오톡 공유 UI를 트리거하는 privileged action입니다.
브라우저 입장에서는 다음과 같은 동작과 같은 범주에 속합니다.

window.open
외부 앱 실행
공유 UI 호출
clipboard 접근

이런 동작은 사용자의 직접적인 액션 컨텍스트 안에서만 허용됩니다.

WebKit User Activation 정책

최신 브라우저는 보안을 위해 User Activation이라는 개념을 사용합니다.

사용자가 화면을 터치하면 브라우저는 잠깐 동안 Transient Activation 상태가 됩니다.

User Click

Transient Activation 시작

약 1초 내외 타이머

이 시간 안에서만 공유 UI / 팝업 / 외부 앱 호출 허용

문제는 이 타이머가 비동기 작업을 기다려주지 않는다는 점입니다.
우리 코드의 실제 흐름은 다음과 같았습니다.

click  
↓  
API 호출  
↓  
응답 대기  
↓  
Kakao.Share.sendDefault()

API 응답이 늦어지면 브라우저는

User Activation Timeout

이 발생했다고 판단하고 공유 UI 호출을 조용히 차단합니다.

에러도 로그도 남지 않습니다.

미스터리: 왜 첫 클릭만 실패했을까

그렇다면 왜 첫 클릭만 실패하고 두 번째 클릭부터 성공했을까요?

원인을 찾기 위해 OpenTelemetry 트레이스와 Network 탭을 분석했습니다.

범인은 초기 네트워크 지연(Initialization Latency)이었습니다.

우리 API 요청은 다음 헤더를 사용했습니다.


Content-Type: application/json  
Authorization: Bearer ...

이 조합은 브라우저 스펙상 Non-simple request입니다.
그래서 브라우저는 실제 요청 전에 CORS Preflight 요청을 보냅니다.


OPTIONS /v1/referral/codes

즉 첫 클릭의 실제 네트워크 흐름은 다음과 같습니다.


User Click  
↓  
OPTIONS (Preflight)  
↓  
TLS Handshake  
↓  
POST /v1/referral/codes  
↓  
API Response  
↓  
Kakao Share 호출

이 과정에서 다음 초기화 비용이 발생합니다.

  • DNS Lookup
  • TCP Handshake
  • TLS Handshake
  • Preflight RTT

실제 트레이스에서는 다음과 같은 지연이 확인됐습니다.


OPTIONS /v1/referral/codes → 180ms  
TLS Handshake → 120ms  
POST /v1/referral/codes → 210ms

총 지연은 약 500~700ms였습니다.

WebKit 환경에서는 이 정도 지연만으로도 User Activation 타이머가 만료될 수 있습니다.

두 번째 클릭이 성공한 이유

두 번째 클릭부터 정상 동작한 이유도 설명됩니다.

브라우저는 첫 요청 이후 다음 정보를 캐싱합니다.


Preflight 결과  
TCP Connection (Keep-Alive)  
TLS Session

그래서 두 번째 요청은 훨씬 가벼워집니다.


POST /v1/referral/codes  
↓  
응답 (약 40ms)

이제 Activation 타이머가 끝나기 전에
Kakao.Share.sendDefault()가 실행됩니다.

해결 방법: 기다림 자체를 제거하기

이 문제를 해결하는 가장 확실한 방법은 클릭 시점에 API를 기다리지 않는 것이었습니다.

기존 구조는 다음과 같았습니다.

User Click  
↓  
코드 생성 API  
↓  
응답 대기  
↓  
공유 UI 호출

우리는 플로우를 이렇게 변경했습니다.

페이지 진입  
↓  
코드 사전 발급

User Click  
↓  
공유 UI 즉시 호출  
↓  
백그라운드 상태 업데이트

프론트엔드에서는 페이지 로딩 시 코드를 미리 발급합니다.

const referralCode = await api.post("/v1/referral/codes")

클릭 시에는 공유 UI를 바로 실행합니다.

function share() {
  Kakao.Share.sendDefault({...})
  api.patch(`/v1/referral/codes/${referralCode}/share`)
}

이제 클릭 시점에는 네트워크 지연이 플로우에 영향을 주지 않습니다.

WebView 공유 기능 디버깅 팁

이 문제를 조사하면서 얻은 몇 가지 교훈을 정리합니다.

1. WebView만 테스트하면 안 된다

공유 기능은 반드시 다음 환경에서 확인해야 합니다.


iOS Safari  
iOS WebView  
Android Chrome  
Android WebView

특히 Safari는 WebKit 정책 때문에 동작이 달라질 수 있습니다.

2. Network 탭에서 Preflight를 확인하라

다음 요청이 보이면 Preflight가 발생한 것입니다.

OPTIONS /api

이 요청 하나만으로도 수백 ms 지연이 발생할 수 있습니다.

3. 첫 요청의 초기화 비용을 의심하라

다음 항목을 확인하면 숨은 병목이 보입니다.

DNS Lookup
TCP Handshake
TLS Handshake
TTFB

특히 첫 요청에서는 이 비용이 모두 발생합니다.

마치며

이번 문제는 단순한 버그가 아니었습니다.
다음 세 가지가 동시에 얽혀 있었습니다.

  • WebKit User Activation 정책
  • CORS Preflight
  • 네트워크 초기화 지연

이 문제를 해결한 핵심은 감이 아니라 데이터였습니다.

  1. 브라우저 Spec 확인
  2. 트레이싱으로 병목 분석
  3. 플로우 재설계

그리고 가장 중요한 교훈은 이것입니다.

좋은 설계는 더 많은 코드를 추가하는 것이 아니라
기다림 자체를 제거하는 설계다.

 

반응형

댓글