[Feature] 카카오톡 링크 내 유니버셜 링크/앱링크 미동작 대응#1218
Conversation
- Android: intent URL 스킴으로 앱 실행 (미설치 시 Play Store 이동) - iOS: 카카오톡 외부 브라우저로 열어 Universal Link 트리거
- 앱 웹뷰 / 카카오톡 / 일반 브라우저 3단계 조건 분기 적용 - 카카오톡 환경에서만 앱열기 버튼 노출
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
Warning
|
| Cohort / File(s) | Summary |
|---|---|
ClubDetailTopBar 스타일 및 로직 업데이트 frontend/src/pages/ClubDetailPage/components/ClubDetailTopBar/ClubDetailTopBar.styles.ts, frontend/src/pages/ClubDetailPage/components/ClubDetailTopBar/ClubDetailTopBar.tsx |
AppOpenButton 스타일 컴포넌트 추가 및 카카오톡 브라우저 감지 시 앱 오픈 버튼을 조건부로 렌더링합니다. 로딩 상태 표시 포함. |
카카오톡 브라우저 감지 유틸리티 frontend/src/utils/isKakaoTalkBrowser.ts |
User Agent를 검사하여 현재 카카오톡 브라우저 내에 있는지 확인하는 유틸리티 함수를 추가합니다. |
앱 오픈 훅 frontend/src/hooks/useOpenAppFromKakao.ts |
안드로이드 Intent URL과 iOS 커스텀 스킴을 통해 플랫폼별 앱 실행을 관리하는 React 훅을 추가합니다. 앱 미응답 시 타임아웃 처리 및 가시성 변경 감지 포함. |
시퀀스 다이어그램
sequenceDiagram
participant User as 사용자
participant TopBar as ClubDetailTopBar
participant Detector as isKakaoTalkBrowser
participant Hook as useOpenAppFromKakao
participant OS as 플랫폼(안드로이드/iOS)
User->>TopBar: 페이지 렌더링 요청
TopBar->>Detector: isKakaoTalkBrowser() 호출
Detector-->>TopBar: 카카오톡 여부 반환
alt 카카오톡 브라우저 && 앱 외부
TopBar->>TopBar: AppOpenButton 표시
User->>TopBar: 앱 열기 버튼 클릭
TopBar->>Hook: openApp() 호출
Hook->>Hook: isLoading = true
alt 안드로이드
Hook->>OS: Intent URL 네비게이션
OS-->>Hook: 앱 실행 시도
else iOS
Hook->>OS: 커스텀 스킴 오픈
Hook->>Hook: 타임아웃 설정 (500ms)
OS-->>Hook: 앱 응답 확인
alt 타임아웃 내 앱 오픈
Hook->>Hook: 타임아웃 취소
else 타임아웃 발생
Hook->>OS: KakaoTalk 외부 URL로 리다이렉트
end
end
Hook->>Hook: isLoading = false
Hook-->>TopBar: 로딩 상태 업데이트
else 카카오톡 브라우저 아님
TopBar->>TopBar: 기존 알림 버튼 유지
end
예상 코드 리뷰 소요 시간
🎯 3 (보통) | ⏱️ ~20분
관련 가능성이 있는 PR
- PR
#1112: ClubDetailTopBar 파일들의 변경사항과 직접적으로 관련되어 있습니다 (이전 PR에서 제거한 구성요소를 본 PR에서 추가). - PR
#1090: 본 PR이 ClubDetailTopBar 컴포넌트와 스타일을 확장하여 AppOpenButton과 카카오 앱 오픈 기능을 추가합니다. - PR
#1106: ClubDetailTopBar 컴포넌트와 스타일을 함께 수정하며, 본 PR의 카카오 관련 로직과 관련됩니다.
제안 리뷰어
- oesnuj
- lepitaaar
- suhyun113
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
| Check name | Status | Explanation |
|---|---|---|
| Linked Issues check | ✅ Passed | PR의 코드 변경사항은 MOA-665 이슈의 목표인 '카카오톡 인앱 환경에서 Universal Link 미동작 문제 대응'을 구현합니다: 카카오톡 브라우저 감지 유틸, 앱 오픈 훅, UI 컴포넌트 추가로 카카오톡 사용자의 앱 오픈 기능을 제공합니다. |
| Out of Scope Changes check | ✅ Passed | 모든 변경사항은 카카오톡 브라우저 감지 및 앱 오픈 기능 구현으로 이슈 목표와 직접적으로 관련되며, 범위를 벗어난 변경은 없습니다. |
| Docstring Coverage | ✅ Passed | No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check. |
| Title check | ✅ Passed | PR 제목은 카카오톡 환경에서 유니버셜 링크/앱 링크 미동작 대응이라는 주요 변경 사항을 명확하게 요약하고 있으며, 실제 구현 내용(카카오톡 브라우저 감지, 앱 열기 기능)과 일치합니다. |
| Description Check | ✅ Passed | Check skipped - CodeRabbit’s high-level summary is enabled. |
✏️ Tip: You can configure your own custom pre-merge checks in the settings.
✨ Finishing Touches
- 📝 Generate docstrings (stacked PR)
- 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
- Create PR with unit tests
- Post copyable unit tests in a comment
- Commit unit tests in branch
feature/#1217-kakao-universal-link-fix-MOA-665
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.
Comment @coderabbitai help to get the list of available commands and usage tips.
- intent URL 스킴 대신 kakaotalk://web/openExternal로 외부 브라우저 열기 - Android App Links(assetlinks.json) 기반으로 바로 앱 실행되도록 개선 - iOS/Android 동일한 로직으로 단순화
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (2)
frontend/src/pages/ClubDetailPage/components/ClubDetailTopBar/ClubDetailTopBar.tsx (1)
47-48: UA 기반 파생 값useMemo로 메모이제이션 고려
isInApp과isKakao는navigator.userAgent를 기반으로 하며, 세션 중 변경되지 않습니다. 현재는 매 렌더마다 재계산되지만 연산 자체가 가볍기 때문에 실질적인 성능 영향은 없습니다. 그러나 의도를 명확히 하려면useMemo로 감싸는 것도 좋습니다.♻️ useMemo 적용 예시
+import { useMemo, useState } from 'react'; -import { useState } from 'react'; const isInApp = isInAppWebView(); -const isKakao = !isInApp && isKakaoTalkBrowser(); +const isInApp = useMemo(() => isInAppWebView(), []); +const isKakao = useMemo(() => !isInApp && isKakaoTalkBrowser(), [isInApp]);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@frontend/src/pages/ClubDetailPage/components/ClubDetailTopBar/ClubDetailTopBar.tsx` around lines 47 - 48, isInApp and isKakao are derived from navigator.userAgent and should be memoized; wrap their computation with React's useMemo to avoid recomputing each render. Import useMemo and replace the direct calls so const isInApp = useMemo(() => isInAppWebView(), []); and const isKakao = useMemo(() => !isInApp && isKakaoTalkBrowser(), [isInApp]); ensuring you reference the existing isInAppWebView and isKakaoTalkBrowser functions and update any dependent logic in ClubDetailTopBar accordingly.frontend/src/utils/isKakaoTalkBrowser.ts (1)
1-1:navigator접근 시 실행 환경 가드 추가 고려Jest 등 브라우저 환경이 아닌 곳에서 테스트 실행 시
navigator가 정의되지 않아ReferenceError가 발생할 수 있습니다.♻️ 방어적 가드 추가 예시
-const isKakaoTalkBrowser = () => /KAKAOTALK/i.test(navigator.userAgent); +const isKakaoTalkBrowser = () => + typeof navigator !== 'undefined' && /KAKAOTALK/i.test(navigator.userAgent);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@frontend/src/utils/isKakaoTalkBrowser.ts` at line 1, The isKakaoTalkBrowser function currently accesses navigator.userAgent unguarded which causes ReferenceError in non-browser environments (e.g., Jest); update isKakaoTalkBrowser to first check typeof navigator !== "undefined" and that navigator.userAgent is a string (or truthy) before running /KAKAOTALK/i.test, and return false when navigator or userAgent is not available so tests and server-side environments won't throw.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@frontend/src/utils/openAppFromKakao.ts`:
- Around line 15-18: The intent URL builder in openAppFromKakao constructs
intentUrl using URL(currentUrl) and url.hash directly, causing two bugs: new
URL() throws for relative paths and url.hash includes a leading '#' which breaks
Android intent parsing. Fix by normalizing currentUrl before creating the URL
(if currentUrl looks like a relative path—e.g. startsWith('/') or lacks a
protocol—use window.location.origin as the base when calling new URL) and strip
the leading '#' from url.hash (use an empty string when no hash) when composing
intentUrl (the intentUrl construction that uses url.host, url.pathname,
url.search, url.hash and ANDROID_PACKAGE should be updated accordingly).
---
Nitpick comments:
In
`@frontend/src/pages/ClubDetailPage/components/ClubDetailTopBar/ClubDetailTopBar.tsx`:
- Around line 47-48: isInApp and isKakao are derived from navigator.userAgent
and should be memoized; wrap their computation with React's useMemo to avoid
recomputing each render. Import useMemo and replace the direct calls so const
isInApp = useMemo(() => isInAppWebView(), []); and const isKakao = useMemo(() =>
!isInApp && isKakaoTalkBrowser(), [isInApp]); ensuring you reference the
existing isInAppWebView and isKakaoTalkBrowser functions and update any
dependent logic in ClubDetailTopBar accordingly.
In `@frontend/src/utils/isKakaoTalkBrowser.ts`:
- Line 1: The isKakaoTalkBrowser function currently accesses navigator.userAgent
unguarded which causes ReferenceError in non-browser environments (e.g., Jest);
update isKakaoTalkBrowser to first check typeof navigator !== "undefined" and
that navigator.userAgent is a string (or truthy) before running
/KAKAOTALK/i.test, and return false when navigator or userAgent is not available
so tests and server-side environments won't throw.
- intent URL에 S.browser_fallback_url 파라미터 추가 - 앱 설치 시 바로 실행, 미설치 시 Play Store로 자동 리다이렉트 - iOS는 Safari 외부 브라우저 방식 유지
There was a problem hiding this comment.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Duplicate comments:
In `@frontend/src/utils/openAppFromKakao.ts`:
- Line 7: currentUrl is set directly from path which allows relative paths like
'/club/123' to be passed into the kakaotalk://web/openExternal?url= scheme
(which expects an absolute URL), causing silent failures; in openAppFromKakao
convert path to an absolute URL when it's present by resolving it against
window.location.href (e.g., using new URL(path, window.location.href) or
equivalent) before assigning to currentUrl so the encoded url param is always an
absolute URL; update references to currentUrl in that function accordingly.
- iOS에서도 프로덕션 도메인(www.moadong.com) URL을 사용하도록 buildProductionUrl 추가 - kakaotalk://web/openExternal 실패 시 1.5초 후 App Store로 자동 이동 - ANDROID_HOST를 APP_HOST로 통합하여 양 플랫폼 공통 사용
- Safari에서 프로덕션 URL 대신 App Store 링크를 직접 열도록 수정 - 설치됨 → App Store에서 "열기", 미설치 → "받기"로 양쪽 케이스 대응 - 동작 불가능했던 document.hidden 기반 타임아웃 폴백 로직 제거
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@frontend/src/utils/openAppFromKakao.ts`:
- Line 14: Update the JSDoc for the openAppFromKakao function to accurately
describe its iOS behavior: it uses the kakaotalk://web/openExternal URL scheme
(not opening Safari/Universal Link) and falls back to the App Store when the
scheme fails; adjust the comment text (in Korean) to reflect the actual scheme,
the fallback behavior, and any timing/visibility details used in the
implementation so the doc matches the code.
- Around line 33-38: The time check `Date.now() - start < 2000` inside the
`setTimeout` callback is redundant and brittle; remove that condition and only
use `!document.hidden` to decide the fallback to `APP_STORE_LINKS.iphone`, or if
you want an explicit elapsed guard make it compare against a named constant
matching the timeout delay (e.g., `FALLBACK_TIMEOUT = 1500`) instead of a
hardcoded 2000. Update the `setTimeout` usage around `start`, `setTimeout(...)`,
`document.hidden`, and `APP_STORE_LINKS.iphone` to either drop the `Date.now()`
check entirely or replace it with `Date.now() - start >= FALLBACK_TIMEOUT` where
`FALLBACK_TIMEOUT` equals the timeout value passed to `setTimeout`.
---
Duplicate comments:
In `@frontend/src/utils/openAppFromKakao.ts`:
- Around line 16-21: The code in openAppFromKakao (and in buildProductionUrl)
calls new URL(currentUrl) which throws for relative paths like "/club/123";
change the logic to detect if path is relative (e.g., starts with "/") or
otherwise not an absolute URL and construct the URL using a base
(window.location.origin or window.location.href) or call new URL(path,
window.location.href) so new URL always receives a resolvable value; update the
branches that call new URL(currentUrl) (inside openAppFromKakao and
buildProductionUrl) to use this base-aware construction to avoid TypeError on
relative paths.
- Around line 23-25: The intent URL construction in intentUrl is producing a
double '#' because url.hash includes a leading '#', which breaks Android Intent
parsing; fix it by normalizing/encoding the fragment before concatenation: take
url.hash (from the same code building intentUrl), remove the leading '#' if
present and URL-encode the fragment (or encode the entire url.hash via
encodeURIComponent) and use that encoded value when building intentUrl so the
resulting string contains only the single '#Intent;' delimiter (refer to
intentUrl, url.hash, APP_HOST, ANDROID_PACKAGE, and fallback to locate the
code).
- Safari에서 프로덕션 URL 열기로 앱 설치 시 바로 실행되도록 변경 - 미설치 시 Smart App Banner를 통해 App Store 유도
- moadongapp:// 커스텀 스킴으로 앱 직접 실행 시도 - 2초 후에도 페이지가 visible이면 앱 미설치로 판단 → App Store 이동 - 앱이 열려 페이지가 hidden 되면 타이머 취소
This reverts commit 12a427e.
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In
`@frontend/src/pages/ClubDetailPage/components/ClubDetailTopBar/ClubDetailTopBar.styles.ts`:
- Line 125: Remove the hardcoded font-family declaration from the AppOpenButton
styled rule in ClubDetailTopBar.styles.ts: locate the styled component or CSS
rule for AppOpenButton (the line setting font-family: 'Pretendard', sans-serif;)
and delete that property so the button inherits the global font defined in
Global.styles.ts (matching how TabButton, IconButton, and NotificationButton
rely on global inheritance).
frontend/src/pages/ClubDetailPage/components/ClubDetailTopBar/ClubDetailTopBar.styles.ts
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (1)
frontend/src/pages/ClubDetailPage/components/ClubDetailTopBar/ClubDetailTopBar.tsx (1)
48-50:isKakaoTalkBrowser()/isInAppWebView()를useMemo로 메모이제이션 고려두 함수 모두
navigator.userAgent를 읽는 순수 함수로 세션 중에 변하지 않습니다. 매 렌더마다 재호출할 필요 없이useMemo로 한 번만 계산할 수 있습니다. 현재 성능 영향은 미미하나, 명시적으로 불변 값임을 나타낼 수 있습니다.♻️ 제안 수정안
+import { useMemo, useState } from 'react'; -import { useState } from 'react'; // ... - const isInApp = isInAppWebView(); - const isKakao = !isInApp && isKakaoTalkBrowser(); + const isInApp = useMemo(() => isInAppWebView(), []); + const isKakao = useMemo(() => !isInApp && isKakaoTalkBrowser(), [isInApp]);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@frontend/src/pages/ClubDetailPage/components/ClubDetailTopBar/ClubDetailTopBar.tsx` around lines 48 - 50, The code calls isInAppWebView() and isKakaoTalkBrowser() on every render though they read navigator.userAgent and are stable for the session; memoize their results using React.memoization (e.g., useMemo) so isInApp and isKakao are computed once per mount (or when relevant dependencies change) and then used with useOpenAppFromKakao(); update the ClubDetailTopBar component to wrap calls to isInAppWebView() and isKakaoTalkBrowser() in useMemo to return the same boolean values and keep the rest of the logic (isKakao, useOpenAppFromKakao usage) unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@frontend/src/hooks/useOpenAppFromKakao.ts`:
- Around line 11-13: The openApp function's path parameter can be a relative
path (e.g., "/clubs/123") which causes new URL(path) to throw; update openApp
(and its parameter name) to either accept only absolute URLs by renaming path to
url and validating with new URL(url) or to support relative paths by resolving
them against window.location.origin (use new URL(path, window.location.origin));
locate the openApp function referenced alongside detectPlatform and
window.location.href and ensure the code builds the URL using the chosen
approach so new URL never receives a bare relative path.
- Around line 30-46: The hook currently creates a new setTimeout on each openApp
call and relies on a visibilitychange listener that may not run before SPA
navigations, causing timer accumulation and stray redirects; update
useOpenAppFromKakao to store the timeout id in a ref (e.g., timerRef) and always
clearTimeout(timerRef.current) before creating a new timer in openApp, add a
useEffect cleanup that clears the timerRef and removes the visibilitychange
listener on unmount, and ensure the visibilitychange handler also clears
timerRef.current and sets isLoading appropriately to prevent redirects after
navigation.
---
Nitpick comments:
In
`@frontend/src/pages/ClubDetailPage/components/ClubDetailTopBar/ClubDetailTopBar.tsx`:
- Around line 48-50: The code calls isInAppWebView() and isKakaoTalkBrowser() on
every render though they read navigator.userAgent and are stable for the
session; memoize their results using React.memoization (e.g., useMemo) so
isInApp and isKakao are computed once per mount (or when relevant dependencies
change) and then used with useOpenAppFromKakao(); update the ClubDetailTopBar
component to wrap calls to isInAppWebView() and isKakaoTalkBrowser() in useMemo
to return the same boolean values and keep the rest of the logic (isKakao,
useOpenAppFromKakao usage) unchanged.
- 상대경로 제외
- useRef로 타이머를 추적해 중복 호출 시 이전 타이머 취소 - useEffect 정리 함수로 컴포넌트 언마운트 시 잔여 타이머 정리
Co-authored-by: Cursor <cursoragent@cursor.com>
lepitaaar
left a comment
There was a problem hiding this comment.
작업수고하셨습니다. fake spinner를 두어 사용자에게 로딩시간 피드백을 주는거 좋네요
#️⃣연관된 이슈
📝작업 내용
카카오톡 인앱 브라우저에서 동아리 상세 페이지를 열었을 때, 네이티브 앱이 설치된 경우 앱으로 바로 이동하고
미설치 시 앱스토어로 유도하는 기능을 추가했습니다.
앱 없을 시 동작
ScreenRecording_02-20-2026.23-53-25_1.MP4
앱 다운로드 시 동작
ScreenRecording_02-20-2026.23-54-55_1.MP4
트러블 슈팅
iOS 카카오톡 인앱 브라우저에서 앱 열기 트러블슈팅
Android intent URL 트러블슈팅
기능
ClubDetailTopBar에 카카오톡 인앱 브라우저 환경에서만 노출되는 앱열기 버튼 추가플랫폼별 동작
Android
intent://URL로 앱 직접 실행S.browser_fallback_url로 Play Store 이동iOS
moadongapp://)으로 앱 실행 시도kakaotalk://web/openExternal로 App Store 이동visibilitychange이벤트로 타이머 즉시 취소안정성
useRef로 타이머 추적 → 중복 호출 시 이전 타이머 취소useEffect정리 함수로 컴포넌트 언마운트 시 잔여 타이머 정리하여 의도치 않은 리디렉션 방지중점적으로 리뷰받고 싶은 부분(선택)
논의하고 싶은 부분(선택)
🫡 참고사항
Summary by CodeRabbit
릴리스 노트