카카오톡 링크로 온 친구가 막히던 네 곳: 랜선포차 모바일 브라우저 대응기
랜선포차는 단톡방 링크로 들어오는 사람이 많다. http 링크, 잠긴 소리, 아이폰 무음 스위치, 인앱 브라우저의 저장 막힘을 하루 동안 고친 기록이다.

랜선포차는 우유 벌칙 파티 게임이다. 친구방을 홈 화면의 주인공으로 올리면서, 방장이 단톡방에 초대 링크를 던지고 친구들이 그 링크를 눌러 들어오는 흐름을 기본으로 잡았다. 그러면 첫 화면을 여는 곳은 사파리나 크롬이 아니라 카카오톡 안의 브라우저다.
2026년 9월 28일 하루 동안 이 경로에서 막히는 곳 네 군데를 고쳤다. 커밋으로 치면 47fd4dd(http 이동), e404d1b(공유 카드의 인앱 처리), f137f16(소리 잠금 해제), c6aa65c(무음 모드 안내)다. 하나하나는 작은 수정이지만, 친구가 처음 눌렀을 때 막히면 그 판은 시작도 못 한다는 점에서 같은 문제였다.

카카오톡이 만든 http 링크
첫 번째는 아침 8시대에 고쳤다. 카카오톡이 게임 주소를 http:// 링크로 만들어 열었다. 페이지가 http로 뜨면 브라우저가 보내는 출처(origin)도 http가 된다. 게임 서버는 https 출처만 허용하도록 막아 두었기 때문에 요청이 거절됐고, 화면에는 “서버에 접속할 수 없다”가 떴다.
HSTS 헤더로는 해결되지 않는다. HSTS는 https로 한 번 들어와 본 브라우저에만 효과가 있다. 단톡방 링크를 처음 누르는 친구는 그 전에 우리 사이트를 본 적이 없으니 막을 수 없다. 그래서 정적 파일을 내주는 Cloudflare Worker 앞에 작은 스크립트를 붙여, http로 들어온 요청은 같은 주소의 https로 301 이동시키고 나머지는 그대로 파일을 돌려주게 했다.
const url = new URL(request.url);
if (url.protocol === "http:") {
url.protocol = "https:";
return Response.redirect(url.toString(), 301);
}
return env.ASSETS.fetch(request);
이렇게 하면 카카오톡이 어떤 형태로 링크를 만들든 첫 화면은 항상 https로 열린다. 서버의 출처 허용 목록은 그대로 https 하나만 둔다.
첫 터치로는 안 풀리던 소리
두 번째는 소리였다. 모바일 브라우저는 사용자가 화면을 건드리기 전에는 소리를 내지 못하게 막는다. 원래 코드는 방에 들어가는 버튼을 누를 때 잠금 해제 함수를 한 번 부르고, 오디오 컨텍스트가 suspended일 때만 resume()을 불렀다. 그런데 iOS 사파리와 카카오톡 인앱에서는 이것만으로 안 풀리는 경우가 있었다. 사용자 동작 안에서 실제로 소리를 한 번 내야 풀린다.
그래서 잠금을 풀 때 1샘플짜리 무음 버퍼를 한 번 재생하게 했다. 들리지 않는 소리지만 브라우저 입장에서는 “사용자 동작 안에서 재생했다”가 된다.
if (ctx.state !== "running") void ctx.resume().catch(() => {});
const b = ctx.createBuffer(1, 1, 22050);
const src = ctx.createBufferSource();
src.buffer = b;
src.connect(ctx.destination);
src.start(0);
입력 종류도 늘렸다. iOS는 pointerdown만으로는 안 풀리는 경우가 있어서 touchend, click, pointerup, keydown을 모두 받는다. 그리고 리스너를 한 번 쓰고 떼지 않는다. 앱을 내렸다가 돌아오면 iOS가 컨텍스트를 다시 멈추기 때문에, 입력이 들어올 때마다 상태를 보고 running이 아닐 때만 다시 깨운다. 화면이 다시 보이는 순간(visibilitychange)에도 한 번 재개를 시도한다. resume()이 거절되면 처리하지 않은 오류가 남으므로 catch로 받아 둔다.
| 항목 | 고치기 전 | 고친 뒤 |
|---|---|---|
| 재개 조건 | suspended일 때만 |
running이 아니면 모두 |
| 받는 입력 | 방 입장 버튼 누를 때 | touchend·click·pointerup·keydown 매번 |
| 잠금 해제 동작 | resume()만 |
resume() + 1샘플 무음 재생 |
| 앱 복귀 | 처리 없음 | 화면이 다시 보이면 재개 |
무음 스위치는 웹이 알 수 없다
세 번째는 코드로 해결할 수 없는 문제였다. 아이폰은 옆 스위치나 액션 버튼으로 무음 모드를 켜면 웹 페이지 소리가 전부 막힌다. 웹에서는 지금 무음 모드인지 알아낼 방법이 없다.
이날 정한 원칙은 “무음 모드는 존중하고, 안내만 한다”였다. 무음 스위치는 사용자가 직접 켠 설정이다. 게임이 그 뜻을 넘어서 소리를 내려고 애쓸 이유는 없다고 봤다.
안내는 최소한으로 했다. 아이폰·아이패드에서, 처음 방에 들어갈 때, 게임 안 음소거를 켜 두지 않은 경우에만 “소리가 안 들리면 폰 옆 무음 모드를 꺼 주세요” 한 줄을 6초 동안 띄운다. 누르면 바로 닫힌다. 한 번 보여 주면 기기에 기억해서 다시는 띄우지 않는다. 아이패드는 사용자 에이전트가 맥처럼 보이는 경우가 있어서, 맥 플랫폼인데 터치 지점이 2개 이상이면 아이패드로 본다.
인앱 브라우저에서 막히는 저장 버튼
네 번째는 결과 화면의 명장면 공유 카드다. 카드는 버튼을 누를 때만 휴대폰 안에서 1080×1350 이미지로 굽는다. 일반 브라우저에서는 공유 시트로 파일을 보내거나 이미지를 저장할 수 있다. 그런데 카카오톡, 네이버, 인스타그램, 페이스북, 라인 같은 앱 안의 브라우저에서는 파일 공유도 다운로드도 막힌다.
그래서 인앱 브라우저로 판별되면 저장 버튼 대신 “카드를 길게 눌러 사진으로 저장한 뒤, 링크와 같이 보내 주세요”라는 안내와 링크 복사를 보여 준다. 길게 눌러 저장하려면 미리보기 이미지가 저장 가능한 형태여야 한다. blob 주소 이미지는 길게 눌러도 저장이 안 되는 곳이 있어서, 미리보기만 data URL로 만들었다.
iOS 사파리에는 다른 제약도 있었다. 공유 시트는 사용자가 누른 동작 안에서 바로 열어야 한다. 카드를 굽는 동안 기다렸다가 공유를 부르면 시트가 열리지 않는다. 그래서 굽기와 공유를 두 번의 누름으로 나눴다. 창이 열릴 때 카드를 미리 구워 두고, 공유 버튼은 이미 만든 파일을 곧장 넘긴다. 카카오톡 공유 SDK를 쓰면 더 매끄럽겠지만 키를 페이지에 노출해야 하고 도메인 등록도 필요해서 쓰지 않기로 했다.
정리
네 가지 모두 내 데스크톱 크롬에서는 한 번도 보이지 않던 문제다. 친구 중심으로 방향을 바꾸면서 첫 화면이 열리는 곳이 단톡방 안으로 옮겨 갔고, 그 환경을 기준으로 다시 보니 막힌 곳이 줄줄이 나왔다. 공통점도 있다. 넷 다 “브라우저가 막는 것을 억지로 뚫기”보다 “막힌 자리에서 사람이 다음에 할 일을 알려 주기”로 풀었다. http는 https로 보내고, 소리는 사용자 동작에 붙이고, 무음 스위치와 저장 막힘은 한 줄 안내로 넘겼다.
친구방이 홈 화면 주인공이 된 과정은 모르는 사람보다 친구방: 랜선포차 첫 화면을 뒤집고 판 벌칙을 붙인 날에, 친구방 4자리 코드와 초대 구조는 빠른 매칭 15초와 친구방 4자리 코드에 적었다. 게임 소개와 바로 하기 버튼은 랜선포차 게임 페이지에 있다.


