[fix] 웹뷰 네비게이션 뒤로가기 버그 수정 및 useNavigator 개선#1507
Conversation
React Router 내부 이동 대신 requestNavigateWebview로 앱에 위임해 스와이프 뒤로가기가 동작하지 않던 문제 수정. isFestival 카드도 동일하게 처리.
useNavigator에서 내부 경로(/로 시작)도 웹뷰 여부에 따라 requestNavigateWebview로 자동 위임하도록 확장. Banner, PromotionCard에서 isInAppWebView + requestNavigateWebview 직접 호출 제거. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
isInAppWebView 중복 호출 제거, early return으로 분기 평탄화 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- 웹뷰에서 외부 링크 → requestOpenExternalUrl 위임 - 웹뷰에서 itms-apps:// → window.open 폴백 - 웹뷰에서 내부 경로 → requestNavigateWebview slug 변환 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Warning Rate limit exceeded
To keep reviews running without waiting, you can enable usage-based add-on for your organization. This allows additional reviews beyond the hourly cap. Account admins can enable it under billing. ⌛ How to resolve this issue?After the wait time has elapsed, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout. Please see our FAQ for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (2)
Warning
|
| Layer / File(s) | Summary |
|---|---|
유틸리티 및 가드 로직 frontend/src/hooks/useNavigator.ts |
toSlug 헬퍼 함수 추가. javascript:, data:, vbscript: 프로토콜 조기 반환 가드 구현. inWebview 계산 중앙화. |
훅 코어 로직 frontend/src/hooks/useNavigator.ts |
외부 URL(http, https, itms-apps)은 웹뷰 환경에서 requestOpenExternalUrl 위임, 실패 시 window.open 폴백. 일반 웹에서는 window.location.href 할당. 내부 경로는 웹뷰에서 requestNavigateWebview로 위임, 일반 웹에서는 navigate 호출. |
컴포넌트 통합 frontend/src/pages/MainPage/components/Banner/Banner.tsx, frontend/src/pages/PromotionPage/components/detail/PromotionClubCTA/PromotionClubCTA.tsx, frontend/src/pages/PromotionPage/components/list/PromotionCard/PromotionCard.tsx |
useNavigate 제거하고 useNavigator 훅으로 교체. Banner는 isWebview prop으로 페이지네이션 렌더링 조건 변경. 컴포넌트들은 handleLink를 통해 통일된 네비게이션 호출. |
테스트 확장 frontend/src/hooks/useNavigator.test.ts |
웹뷰 모드(mockIsInAppWebView 설정)와 일반 웹 모드 분리. 악성 프로토콜 파라미터화된 테스트 추가. 외부/내부 URL 및 웹뷰 위임 동작 검증. |
Estimated code review effort
🎯 3 (Moderate) | ⏱️ ~22 minutes
Possibly related PRs
- [release] FE v1.1.8 #939: 동일 훅
useNavigator.ts와 테스트를 수정하여 외부/내부 URL 처리 및 네비게이션 프록싱 방식 변경. - [feature] 앱 버전 관리를 위해 웹뷰 라우터를 분리한다 #1114: 웹뷰 경로 분기(
/club→/clubDetail) 및 웹뷰별 라우팅 분리가 이 PR의 네비게이션 변경과 직접 겹침. - [release] FE v1.5.0 #1386: PromotionPage 컴포넌트(
PromotionClubCTA,PromotionCard,Banner) 수정 및 웹뷰 브릿지 동작 조정이 릴리스 PR에서 추가/수정된 동일 컴포넌트들과 일치.
Suggested labels
🔨 Refactor, ✅ Test
Suggested reviewers
- seongwon030
- suhyun113
- lepitaaar
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
| Check name | Status | Explanation |
|---|---|---|
| Title check | ✅ Passed | PR 제목이 주요 변경사항을 명확하게 요약하고 있습니다. 웹뷰 네비게이션 버그 수정 및 useNavigator 개선이라는 핵심 목표와 일치합니다. |
| Docstring Coverage | ✅ Passed | No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check. |
| Linked Issues check | ✅ Passed | Check skipped because no linked issues were found for this pull request. |
| Out of Scope Changes check | ✅ Passed | Check skipped because no linked issues were found for this pull request. |
| 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 unit tests (beta)
- Create PR with unit tests
- Commit unit tests in branch
fix/webview-navigation
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.
Review rate limit: 0/1 reviews remaining, refill in 54 minutes and 8 seconds.Comment @coderabbitai help to get the list of available commands and usage tips.
🎨 UI 변경사항을 확인해주세요
9개 스토리 변경 · 전체 56개 스토리 · 22개 컴포넌트 |
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 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/useNavigator.ts`:
- Around line 20-25: The isExternal detection in useNavigator.ts uses
/^(https?|itms-apps):\/\// which is case-sensitive and misclassifies schemes
like "HTTPS://" or "ITMS-APPS://"; update the isExternal check (the regex or the
input) to be case-insensitive (e.g., add the i flag to the regex or normalize
trimmedUrl to lowercase before testing) so variables inWebview and isExternal
behave correctly for all-cased schemes and navigation branches (window.open /
window.location.href / requestOpenExternalUrl).
- Around line 29-30: The webview bridge call
requestNavigateWebview(toSlug(trimmedUrl)) can return false on failure but its
result is ignored; update the logic in the useNavigator hook so that you capture
the return value and, if it is false (or otherwise indicates a failure), call
navigate(trimmedUrl) as a fallback; ensure this change targets the branch that
currently does if (inWebview) requestNavigateWebview(...) else navigate(...),
replacing the simple call with a conditional that falls back to navigate when
requestNavigateWebview fails.
In
`@frontend/src/pages/PromotionPage/components/detail/PromotionClubCTA/PromotionClubCTA.tsx`:
- Around line 24-28: The CTA currently calls
requestNavigateWebview(`club/${clubId}`) without checking its result, so if the
webview bridge call fails the click does nothing; update the click handling
around isInAppWebView() to await/inspect requestNavigateWebview's return (or
catch its rejection) and on failure fall back to calling
handleLink(`/clubDetail/@${encodeURIComponent(clubName)}`); locate this logic in
PromotionClubCTA (symbols: isInAppWebView, requestNavigateWebview, handleLink,
clubId, clubName) and ensure the fallback runs when the bridge returns a falsy
value or throws.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 8e174ba8-5ff0-441c-ae2b-3e98f0df15af
📒 Files selected for processing (5)
frontend/src/hooks/useNavigator.test.tsfrontend/src/hooks/useNavigator.tsfrontend/src/pages/MainPage/components/Banner/Banner.tsxfrontend/src/pages/PromotionPage/components/detail/PromotionClubCTA/PromotionClubCTA.tsxfrontend/src/pages/PromotionPage/components/list/PromotionCard/PromotionCard.tsx
| const inWebview = isInAppWebView(); | ||
| const isExternal = /^(https?|itms-apps):\/\//.test(trimmedUrl); | ||
|
|
||
| const isDangerousProtocol = /^(javascript|data|vbscript):/i.test( | ||
| trimmedUrl, | ||
| ); | ||
| if (isDangerousProtocol) return; | ||
|
|
||
| const isExternalUrl = /^(https?|itms-apps):\/\//.test(trimmedUrl); | ||
|
|
||
| if (isExternalUrl) { | ||
| // 웹뷰에서 window.location.href로 외부 URL을 열면 WebView 자체가 이동해버리므로 앱에 위임. | ||
| // requestOpenExternalUrl은 http/https만 허용하므로, itms-apps:// 등 비표준 스킴은 false를 | ||
| // 반환 → window.open으로 폴백해 OS가 처리하도록 위임 | ||
| if (isInAppWebView()) { | ||
| if (!requestOpenExternalUrl(trimmedUrl)) { | ||
| window.open(trimmedUrl); | ||
| } | ||
| } else { | ||
| window.location.href = trimmedUrl; | ||
| } | ||
| } else { | ||
| navigate(trimmedUrl); | ||
| if (isExternal) { | ||
| if (inWebview && !requestOpenExternalUrl(trimmedUrl)) window.open(trimmedUrl); | ||
| else if (!inWebview) window.location.href = trimmedUrl; |
There was a problem hiding this comment.
외부 URL 스킴 판별은 대소문자를 무시해야 합니다.
지금 정규식은 HTTPS://...나 ITMS-APPS://...를 내부 경로로 분류합니다. 그러면 일반 웹에서는 React Router로, 웹뷰에서는 NAVIGATE_WEBVIEW로 잘못 보내지게 됩니다.
수정 예시
- const isExternal = /^(https?|itms-apps):\/\//.test(trimmedUrl);
+ const isExternal = /^(https?|itms-apps):\/\//i.test(trimmedUrl);📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const inWebview = isInAppWebView(); | |
| const isExternal = /^(https?|itms-apps):\/\//.test(trimmedUrl); | |
| const isDangerousProtocol = /^(javascript|data|vbscript):/i.test( | |
| trimmedUrl, | |
| ); | |
| if (isDangerousProtocol) return; | |
| const isExternalUrl = /^(https?|itms-apps):\/\//.test(trimmedUrl); | |
| if (isExternalUrl) { | |
| // 웹뷰에서 window.location.href로 외부 URL을 열면 WebView 자체가 이동해버리므로 앱에 위임. | |
| // requestOpenExternalUrl은 http/https만 허용하므로, itms-apps:// 등 비표준 스킴은 false를 | |
| // 반환 → window.open으로 폴백해 OS가 처리하도록 위임 | |
| if (isInAppWebView()) { | |
| if (!requestOpenExternalUrl(trimmedUrl)) { | |
| window.open(trimmedUrl); | |
| } | |
| } else { | |
| window.location.href = trimmedUrl; | |
| } | |
| } else { | |
| navigate(trimmedUrl); | |
| if (isExternal) { | |
| if (inWebview && !requestOpenExternalUrl(trimmedUrl)) window.open(trimmedUrl); | |
| else if (!inWebview) window.location.href = trimmedUrl; | |
| const inWebview = isInAppWebView(); | |
| const isExternal = /^(https?|itms-apps):\/\//i.test(trimmedUrl); | |
| if (isExternal) { | |
| if (inWebview && !requestOpenExternalUrl(trimmedUrl)) window.open(trimmedUrl); | |
| else if (!inWebview) window.location.href = trimmedUrl; |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@frontend/src/hooks/useNavigator.ts` around lines 20 - 25, The isExternal
detection in useNavigator.ts uses /^(https?|itms-apps):\/\// which is
case-sensitive and misclassifies schemes like "HTTPS://" or "ITMS-APPS://";
update the isExternal check (the regex or the input) to be case-insensitive
(e.g., add the i flag to the regex or normalize trimmedUrl to lowercase before
testing) so variables inWebview and isExternal behave correctly for all-cased
schemes and navigation branches (window.open / window.location.href /
requestOpenExternalUrl).
There was a problem hiding this comment.
타당하지 않습니다. 적용 불필요합니다.
URL 스킴은 RFC 3986 기준으로 대소문자를 구분하지 않지만, 실제로 HTTPS:// 같은 대문자 URL을 입력하는 경우는 없습니다. 브라우저, 앱, 백엔드 모두 소문자 스킴으로 URL을 생성하고, 사용자가 직접 입력하는 경우도 거의 없습니다.
이미 위의 위험 프로토콜 차단 정규식(line 18)에는 i 플래그가 있는 것도 실제 XSS 공격 벡터를 막기 위한 것이고, 외부 URL 판별은 그런 위협이 없습니다. 과잉 방어입니다.
There was a problem hiding this comment.
@oesnuj, 말씀하신 내용 이해했습니다. 실제 환경에서 대문자 스킴이 사용되는 경우가 없고, 위험 프로토콜 차단과는 위협 모델이 다르다는 점에서 타당한 판단입니다. 제안을 철회하겠습니다.
| if (inWebview) requestNavigateWebview(toSlug(trimmedUrl)); | ||
| else navigate(trimmedUrl); |
There was a problem hiding this comment.
웹뷰 브리지 실패 시 내부 링크가 무반응이 됩니다.
requestNavigateWebview()는 브리지 전송 실패 시 false를 돌려주는데, 현재는 그 값을 무시하고 있습니다. postMessage가 실패하면 프로모션/페스티벌 이동이 그대로 죽으니 웹 라우팅으로라도 폴백하는 편이 안전합니다.
수정 예시
- if (inWebview) requestNavigateWebview(toSlug(trimmedUrl));
- else navigate(trimmedUrl);
+ if (inWebview) {
+ const delegated = requestNavigateWebview(toSlug(trimmedUrl));
+ if (!delegated) navigate(trimmedUrl);
+ return;
+ }
+
+ navigate(trimmedUrl);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@frontend/src/hooks/useNavigator.ts` around lines 29 - 30, The webview bridge
call requestNavigateWebview(toSlug(trimmedUrl)) can return false on failure but
its result is ignored; update the logic in the useNavigator hook so that you
capture the return value and, if it is false (or otherwise indicates a failure),
call navigate(trimmedUrl) as a fallback; ensure this change targets the branch
that currently does if (inWebview) requestNavigateWebview(...) else
navigate(...), replacing the simple call with a conditional that falls back to
navigate when requestNavigateWebview fails.
There was a problem hiding this comment.
requestNavigateWebview는 이미 inWebview === true인 브랜치 안에서 호출됩니다.
이 함수가 false를 반환하는 유일한 경우는 isInAppWebView()가 false일 때인데, 해당 브랜치에 진입했다는 것 자체가 그 조건을 이미 통과했음을 의미합니다.
브릿지 전송 실패 시 navigate(trimmedUrl)로 폴백해도 웹뷰 환경에서 React Router 이동은 앱 네이티브 스택에 화면이 쌓이지 않는 근본 문제를 그대로 가집니다.
의미 없는 폴백을 추가하는 것은 오히려 잘못된 동작을 정상처럼 보이게 만듭니다.
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Find and display the webviewBridge utility
fd -i "webviewBridge" --type f
Length of output: 36
🏁 Script executed:
#!/bin/bash
cat frontend/src/utils/webviewBridge.ts
Length of output: 4110
@oesnuj, 피드백 감사합니다. 코드를 확인했습니다.
첫 번째 주장에 대해 한 가지 짚고 싶은 점이 있습니다. postMessageToApp의 try/catch 블록을 보면, window.ReactNativeWebView?.postMessage() 호출 자체가 예외를 던지는 경우에도 false를 반환하는 경로가 존재합니다. 즉, isInAppWebView() === true인 브랜치 안에서도 이론상 false 반환은 가능합니다.
그러나 두 번째 주장이 결정적입니다. postMessage가 실패한 상황에서 navigate(trimmedUrl)로 폴백하더라도, 웹뷰 환경에서 React Router 이동은 앱 네이티브 스택에 화면을 쌓지 못한다는 근본 문제가 그대로 남습니다. 결과적으로 해당 폴백은 오류를 정상 동작처럼 보이게 만드는 오해의 소지가 있으며, 실질적인 UX 개선도 없습니다.
지적하신 내용이 맞습니다. 리뷰 코멘트를 철회하겠습니다.
✏️ Learnings added
Learnt from: oesnuj
Repo: Moadong/moadong PR: 1507
File: frontend/src/hooks/useNavigator.ts:30-31
Timestamp: 2026-05-03T09:41:11.455Z
Learning: In `frontend/src/hooks/useNavigator.ts`, the `requestNavigateWebview` call is intentionally inside the `if (inWebview)` guard. A React Router `navigate()` fallback on bridge failure is not meaningful because React Router navigation inside an in-app webview does not push screens onto the native app stack — that is the exact problem the webview bridge is designed to solve. Adding such a fallback would make broken behavior appear normal.
Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.
| // 웹뷰는 club/id 기반 slug, 일반 웹은 clubName 기반 경로 사용 | ||
| if (isInAppWebView()) { | ||
| requestNavigateWebview(`club/${clubId}`); | ||
| } else { | ||
| navigate(`/clubDetail/@${encodeURIComponent(clubName)}`); | ||
| handleLink(`/clubDetail/@${encodeURIComponent(clubName)}`); |
There was a problem hiding this comment.
CTA도 웹뷰 브리지 실패 시 그대로 죽습니다.
여기서는 requestNavigateWebview('club/...')의 반환값을 확인하지 않아서, 브리지 전송이 실패하면 버튼 클릭이 무반응이 됩니다. 실패 시에는 웹 경로로 폴백해 두는 게 안전합니다.
수정 예시
- if (isInAppWebView()) {
- requestNavigateWebview(`club/${clubId}`);
- } else {
- handleLink(`/clubDetail/@${encodeURIComponent(clubName)}`);
- }
+ if (isInAppWebView() && requestNavigateWebview(`club/${clubId}`)) {
+ return;
+ }
+
+ handleLink(`/clubDetail/@${encodeURIComponent(clubName)}`);📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| // 웹뷰는 club/id 기반 slug, 일반 웹은 clubName 기반 경로 사용 | |
| if (isInAppWebView()) { | |
| requestNavigateWebview(`club/${clubId}`); | |
| } else { | |
| navigate(`/clubDetail/@${encodeURIComponent(clubName)}`); | |
| handleLink(`/clubDetail/@${encodeURIComponent(clubName)}`); | |
| // 웹뷰는 club/id 기반 slug, 일반 웹은 clubName 기반 경로 사용 | |
| if (isInAppWebView() && requestNavigateWebview(`club/${clubId}`)) { | |
| return; | |
| } | |
| handleLink(`/clubDetail/@${encodeURIComponent(clubName)}`); |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@frontend/src/pages/PromotionPage/components/detail/PromotionClubCTA/PromotionClubCTA.tsx`
around lines 24 - 28, The CTA currently calls
requestNavigateWebview(`club/${clubId}`) without checking its result, so if the
webview bridge call fails the click does nothing; update the click handling
around isInAppWebView() to await/inspect requestNavigateWebview's return (or
catch its rejection) and on failure fall back to calling
handleLink(`/clubDetail/@${encodeURIComponent(clubName)}`); locate this logic in
PromotionClubCTA (symbols: isInAppWebView, requestNavigateWebview, handleLink,
clubId, clubName) and ensure the fallback runs when the bridge returns a falsy
value or throws.
There was a problem hiding this comment.
실패 시 handleLink('/clubDetail/@clubname')으로 폴백하면 useNavigator 내부에서 다시 inWebview === true 브랜치로 진입해 requestNavigateWebview를 재호출하는 무한루프가 발생합니다.
브릿지가 실패한 환경에서 동일한 브릿지 경로를 다시 타는 폴백은 성립하지 않습니다.
There was a problem hiding this comment.
@oesnuj, 지적이 정확합니다. /clubDetail/@clubName은 외부 URL(http/https/itms-apps)이 아니라 내부 경로이기 때문에, handleLink 내부에서 isInAppWebView() === true 브랜치로 다시 진입해 requestNavigateWebview를 재호출하는 순환이 발생합니다. 제 제안이 잘못되었습니다.
현재 구조(isInAppWebView() → requestNavigateWebview, 그 외 → handleLink)는 올바른 패턴이며, 웹뷰 브리지 실패 시 동일 브리지 경로로의 폴백은 의미가 없습니다. 제 코멘트는 철회합니다.
(∩`-´)⊃━☆゚.*・。゚
✏️ Learnings added
Learnt from: oesnuj
Repo: Moadong/moadong PR: 1507
File: frontend/src/pages/PromotionPage/components/detail/PromotionClubCTA/PromotionClubCTA.tsx:24-28
Timestamp: 2026-05-03T09:40:26.989Z
Learning: In `frontend/src/pages/PromotionPage/components/detail/PromotionClubCTA/PromotionClubCTA.tsx`, the `PromotionClubCTA` component intentionally keeps a manual `isInAppWebView()` guard rather than delegating to `handleLink` (from `useNavigator`) for the webview branch. This is because `handleLink` internally re-checks `isInAppWebView()` and would re-enter the webview branch for internal paths, causing an infinite loop if used as a fallback after a bridge failure. The pattern `if (isInAppWebView()) { requestNavigateWebview(...) } else { handleLink(...) }` is correct and intentional here.
Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.
seongwon030
left a comment
There was a problem hiding this comment.
React router는 스택이 쌓이지 않군요 빠른 수정 좋습니다!
Summary
requestNavigateWebview로 앱 위임isFestival카드도 동일하게 처리itms-apps://링크가 웹뷰에서 무시되던 버그 수정useNavigator에 내부 경로 웹뷰 분기 통합 (Banner, PromotionCard 중복 코드 제거)Root Cause
웹뷰 내부에서 React Router로 이동하면 앱 네이티브 스택에 화면이 쌓이지 않아
스와이프 뒤로가기 및 TopBar 뒤로가기 불가
앱 측 작업 필요
ui/home/home-webview-screen.tsxhandleMessage 함수 내 NAVIGATE_WEBVIEW에 아래 케이스 추가필요Test plan
Summary by CodeRabbit
릴리스 노트
버그 수정
테스트