Skip to content

refactor: App에 컴포넌트 및 훅 분리#22

Merged
KwonDeaGeun merged 3 commits intomainfrom
refactor/extract-app-components-and-hooks
Sep 22, 2025
Merged

refactor: App에 컴포넌트 및 훅 분리#22
KwonDeaGeun merged 3 commits intomainfrom
refactor/extract-app-components-and-hooks

Conversation

@KwonDeaGeun
Copy link
Owner

@KwonDeaGeun KwonDeaGeun commented Sep 22, 2025

Summary by CodeRabbit

  • New Features

    • 새 지도 컨테이너(MapContainer) 도입 및 버스/정류장 오버레이 생성 기능 추가
    • 버스정류장 패널 추가: 정류장 선택 시 위치 이동, 노선·말풍선 연동
    • 접근성 지원 설정 버튼 추가(아이콘 토글)
  • Refactor

    • 지도 초기화·오버레이·이벤트 로직을 컴포넌트와 훅으로 분리하여 구조 개선
  • Bug Fixes

    • WebView/모바일 환경에서 메시지·제스처 처리 및 제스처 간섭 감소

@vercel
Copy link

vercel bot commented Sep 22, 2025

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Preview Comments Updated (UTC)
what-the-bus-web Ready Ready Preview Comment Sep 22, 2025 3:03pm

@coderabbitai
Copy link

coderabbitai bot commented Sep 22, 2025

Walkthrough

App.tsx에서 인라인 Kakao 지도 초기화/오버레이/이벤트/ RN WebView 후킹 및 수동 설정 버튼을 제거하고 MapContainer, BusStopsPanel, SettingsButton으로 대체했습니다. 새로운 훅들(useKakaoMap, useMapEventHandlers, useMapOverlays)과 유틸(createBusStopOverlays, createBusOverlays)이 지도 초기화, 이벤트, 오버레이 생성/정리 책임을 분리합니다. ReactQueryDevtools의 타입 캐스트 표기만 소폭 변경되었습니다.

Changes

Cohort / File(s) Summary
App shell refactor
src/App.tsx
인라인 Kakao 지도 초기화·오버레이·이벤트·RN WebView 훅 및 수동 설정 버튼 제거. MapContainer, BusStopsPanel, SettingsButton으로 대체. ReactQueryDevtools 타입 캐스트 표기 수정.
New UI components
src/components/BusStopsPanel.tsx, src/components/MapContainer.tsx, src/components/SettingsButton.tsx
BusStopsPanel 추가(정류장 선택 시 moveToLocation 호출), MapContainer 추가(지도 초기화·오버레이·이벤트 훅 연결, mapId prop), SettingsButton 추가(접근성 속성 포함 토글 버튼).
Map hooks
src/hooks/useKakaoMap.ts, src/hooks/useMapEventHandlers.ts, src/hooks/useMapOverlays.ts
useKakaoMap: SDK 로드, 지도 인스턴스 생성, RN WebView 브리지, 오류/토스트 처리. useMapEventHandlers: origin 검사, postMessage 수신, 제스처/터치 방지 리스너 등록/정리. useMapOverlays: createBusStopOverlays/createBusOverlays 호출 및 안전한 정리.
Overlay utilities
src/utils/mapOverlays.ts
OverlayHandle, BusStop, Bus 타입과 createBusStopOverlays, createBusOverlays 추가 — DOM 노드 생성, Kakao CustomOverlay 인스턴스 생성 및 setMap 연결, 런타임 가드 및 cleanup 보장.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor User
  participant App as App.tsx
  participant MC as MapContainer
  participant HK as useKakaoMap
  participant EH as useMapEventHandlers
  participant MO as useMapOverlays
  participant U as mapOverlays

  User->>App: 애플리케이션 로드
  App->>MC: <MapContainer mapId> 렌더
  MC->>HK: init(mapId, toast)
  HK-->>MC: map 인스턴스 반환
  MC->>EH: 이벤트/메시지 핸들러 등록
  MC->>MO: overlays 마운트(map, busStops, buses)
  MO->>U: createBusStopOverlays(map, busStops)
  MO->>U: createBusOverlays(map, buses)
  U-->>MO: OverlayHandle[]
  MO-->>MC: overlays 적용
  Note over HK,MC: RN WebView 브리지 + MAP_READY postMessage
Loading
sequenceDiagram
  autonumber
  actor User
  participant BSP as BusStopsPanel
  participant BS as BusStops
  participant Ctx as MapContainer Context
  participant Map as Kakao Map

  User->>BS: 정류장 클릭
  BS-->>BSP: onSelect(stop)
  BSP->>Ctx: moveToLocation(lat, lng)
  Ctx->>Map: setCenter(lat, lng)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Poem

탁— 버튼 톡, 지도가 춤추네
토스트 반짝, 스크립트 살포시 풀려나네
정류장 눌러 지도 휙— 이동하네
오버레이 둥실, 아이콘 길 비추네
토깽이 개발자: "깡총! 컴포넌트 완성!" 🐇✨

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed PR 제목 "refactor: App에 컴포넌트 및 훅 분리"은 App에서 컴포넌트와 훅을 분리한 핵심 변경을 간결하고 명확하게 요약하며 변경 내용과 일치합니다.
Docstring Coverage ✅ Passed No functions found in the changes. Docstring coverage check skipped.
✨ Finishing touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch refactor/extract-app-components-and-hooks

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 005f7f3 and 8d1b75e.

📒 Files selected for processing (1)
  • src/utils/mapOverlays.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/utils/mapOverlays.ts

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (8)
src/hooks/useMapEventHandlers.ts (1)

15-22: 메시지 핸들러가 수신한 메시지를 처리하지 않습니다.

현재 messageHandler 함수가 origin 검증만 수행하고 실제 메시지 처리 로직이 없습니다. 이벤트 데이터를 처리하거나 특정 동작을 수행해야 한다면 구현이 필요합니다.

TODO 주석이나 향후 구현 예정인 기능이 있다면 이슈로 등록하시겠습니까?

src/components/MapContainer.tsx (1)

18-18: 불필요한 배열 복사를 제거하세요.

busStopsbuses는 이미 배열이므로 spread 연산자를 사용한 복사가 불필요합니다.

-    useMapOverlays(map, [...busStops], [...buses]);
+    useMapOverlays(map, busStops, buses);
src/hooks/useKakaoMap.ts (2)

50-50: 전역 변수 사용에 대한 타입 안전성을 개선하세요.

이전 PR 리뷰에서 학습한 내용에 따르면, TypeScript에서 any 타입 사용을 피하고 더 타입 안전한 코드를 선호하신다고 알고 있습니다. window.map과 같은 전역 변수 사용 시 타입 정의를 추가하는 것이 좋겠습니다.

global.d.ts 파일을 생성하여 window 객체의 커스텀 속성들에 대한 타입을 정의하세요:

// global.d.ts
interface Window {
    map?: any; // Kakao Map 인스턴스 타입이 있다면 더 구체적으로 정의
    kakao?: {
        maps: {
            LatLng: new (lat: number, lng: number) => any;
            Map: new (container: HTMLElement, options: any) => any;
            CustomOverlay: new (options: any) => any;
            load: (callback: () => void) => void;
        };
    };
    __moveFromRN?: (lat: number, lng: number) => void;
    __pendingMove?: { lat: number; lng: number } | null;
    __onMapReady?: () => void;
    __panAnimationId?: number;
    ReactNativeWebView?: {
        postMessage: (message: string) => void;
    };
}

123-144: 스크립트 이벤트 핸들러 중복 할당 가능성이 있습니다.

이미 존재하는 스크립트 엘리먼트에 대해 onloadonerror 핸들러를 재할당하는 것은 예상치 못한 동작을 일으킬 수 있습니다. 스크립트가 이미 로드된 경우를 더 명확하게 처리하는 것이 좋겠습니다.

             if (script) {
-                if (!window.kakao?.maps?.load) {
-                    script.onload = () => {
-                        window.kakao.maps.load(initMap);
-                    };
-                    script.onerror = () => {
-                        try {
-                            toast({
-                                title: "지도 로드 실패",
-                                description:
-                                    "Kakao Maps API 스크립트를 불러오는 데 실패했습니다.",
-                                variant: "destructive",
-                            });
-                        } catch {
-                            // fallback to console
-                            // eslint-disable-next-line no-console
-                            console.error(
-                                "Kakao Maps API 스크립트를 로드하는데 실패했습니다."
-                            );
-                        }
-                    };
-                }
+                // 스크립트가 이미 존재하면 로드 상태를 확인
+                if (script.readyState === 'complete' || script.readyState === 'loaded') {
+                    if (window.kakao?.maps?.load) {
+                        window.kakao.maps.load(initMap);
+                    }
+                } else if (!window.kakao?.maps?.load) {
+                    // 아직 로드 중인 경우에만 이벤트 핸들러 설정
+                    const originalOnload = script.onload;
+                    script.onload = () => {
+                        if (originalOnload && typeof originalOnload === 'function') {
+                            (originalOnload as () => void)();
+                        }
+                        window.kakao.maps.load(initMap);
+                    };
+                    // onerror 핸들러도 동일하게 처리...
+                }
                 return;
             }
src/utils/mapOverlays.ts (4)

29-30: innerHTML 사용 시 보안 고려사항

정적 SVG 경로를 사용하고 있어 현재는 XSS 위험이 없지만, DOM API를 사용하여 더 안전하게 구현할 수 있습니다.

-        busIconDiv.innerHTML =
-            '<img src="/ic_busstop.svg" alt="Bus Icon" width="48" height="48" />';
+        const img = document.createElement("img");
+        img.src = "/ic_busstop.svg";
+        img.alt = "Bus Icon";
+        img.width = 48;
+        img.height = 48;
+        busIconDiv.appendChild(img);

38-38: 타입 캐스팅을 피하고 타입 가드나 assertion을 사용하세요.

여러 번의 타입 캐스팅보다 타입 가드를 사용하거나 Kakao Maps 타입 정의를 개선하는 것이 좋습니다.

-        (overlay as unknown as { setMap: (m: unknown) => void }).setMap(map);
+        // 타입 가드를 사용한 안전한 접근
+        if ('setMap' in overlay && typeof overlay.setMap === 'function') {
+            overlay.setMap(map);
+        }

Also applies to: 73-73


16-42: 함수 반환 타입을 명시적으로 선언하세요.

TypeScript의 타입 추론을 돕고 API 계약을 명확하게 하기 위해 반환 타입을 명시하는 것이 좋습니다.

 export const createBusStopOverlays = (
     map: unknown,
     busStops: BusStop[]
-): OverlayHandle[] => {
+): ReadonlyArray<OverlayHandle> => {

12-14: OverlayHandle.setMap의 map 파라미터를 구체 타입으로 변경 (kakao.maps.Map | null)

src/utils/mapOverlays.ts (12–14): 현재 setMap: (map: unknown) => void; 를 setMap: (map: kakao.maps.Map | null) => void; 로 변경하세요. 프로젝트에 src/types/kakao.d.ts가 존재하고 코드에서 window.kakao.maps.*를 사용하므로 전역 kakao 타입을 사용하면 됩니다.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 05df9ff and e38e4a1.

📒 Files selected for processing (8)
  • src/App.tsx (4 hunks)
  • src/components/BusStopsPanel.tsx (1 hunks)
  • src/components/MapContainer.tsx (1 hunks)
  • src/components/SettingsButton.tsx (1 hunks)
  • src/hooks/useKakaoMap.ts (1 hunks)
  • src/hooks/useMapEventHandlers.ts (1 hunks)
  • src/hooks/useMapOverlays.ts (1 hunks)
  • src/utils/mapOverlays.ts (1 hunks)
🧰 Additional context used
🧠 Learnings (1)
📓 Common learnings
Learnt from: KwonDeaGeun
PR: KwonDeaGeun/WhatTheBus-Web#21
File: src/App.tsx:16-23
Timestamp: 2025-09-22T04:45:54.433Z
Learning: KwonDeaGeun은 TypeScript에서 any 타입 사용을 피하고 더 타입 안전한 코드를 선호한다.
🧬 Code graph analysis (5)
src/components/MapContainer.tsx (6)
src/components/ui/use-toast.tsx (1)
  • useToast (83-89)
src/hooks/useKakaoMap.ts (1)
  • useKakaoMap (12-178)
src/hooks/useMapOverlays.ts (1)
  • useMapOverlays (19-42)
src/data/busStops.ts (1)
  • busStops (7-14)
src/data/bus.ts (1)
  • buses (8-39)
src/hooks/useMapEventHandlers.ts (1)
  • useMapEventHandlers (3-53)
src/hooks/useMapOverlays.ts (3)
src/data/busStops.ts (2)
  • BusStop (1-5)
  • busStops (7-14)
src/data/bus.ts (2)
  • Bus (1-6)
  • buses (8-39)
src/utils/mapOverlays.ts (2)
  • createBusStopOverlays (16-42)
  • createBusOverlays (44-77)
src/components/BusStopsPanel.tsx (3)
src/data/busStops.ts (2)
  • BusStop (1-5)
  • busStops (7-14)
src/components/BusStops.tsx (1)
  • BusStops (11-246)
src/hooks/useMapMovement.ts (1)
  • moveToLocation (1-89)
src/App.tsx (4)
src/components/SettingsButton.tsx (1)
  • SettingsButton (8-38)
src/components/MapContainer.tsx (1)
  • MapContainer (14-26)
src/components/Bubble.tsx (1)
  • Bubble (18-221)
src/components/BusStopsPanel.tsx (1)
  • BusStopsPanel (11-34)
src/utils/mapOverlays.ts (2)
src/data/busStops.ts (2)
  • BusStop (1-5)
  • busStops (7-14)
src/data/bus.ts (2)
  • Bus (1-6)
  • buses (8-39)
🪛 ast-grep (0.39.5)
src/utils/mapOverlays.ts

[warning] 28-29: Direct modification of innerHTML or outerHTML properties detected. Modifying these properties with unsanitized user input can lead to XSS vulnerabilities. Use safe alternatives or sanitize content first.
Context: busIconDiv.innerHTML =
'Bus Icon'
Note: [CWE-79] Improper Neutralization of Input During Web Page Generation [REFERENCES]
- https://owasp.org/www-community/xss-filter-evasion-cheatsheet
- https://cwe.mitre.org/data/definitions/79.html

(dom-content-modification)


[warning] 28-29: Direct HTML content assignment detected. Modifying innerHTML, outerHTML, or using document.write with unsanitized content can lead to XSS vulnerabilities. Use secure alternatives like textContent or sanitize HTML with libraries like DOMPurify.
Context: busIconDiv.innerHTML =
'Bus Icon'
Note: [CWE-79] Improper Neutralization of Input During Web Page Generation [REFERENCES]
- https://www.dhairyashah.dev/posts/why-innerhtml-is-a-bad-idea-and-how-to-avoid-it/
- https://cwe.mitre.org/data/definitions/79.html

(unsafe-html-content-assignment)

🔇 Additional comments (4)
src/components/SettingsButton.tsx (1)

1-38: LGTM! 설정 버튼 컴포넌트가 잘 분리되었습니다.

컴포넌트 분리와 접근성 속성(aria-label, aria-haspopup, aria-expanded) 구현이 잘 되어 있습니다.

src/components/BusStopsPanel.tsx (1)

1-34: LGTM! BusStopsPanel 컴포넌트가 잘 구성되었습니다.

컴포넌트가 명확한 책임을 가지고 있으며, props 전달이 적절하게 구현되었습니다.

src/App.tsx (2)

76-104: 컴포넌트 분리가 잘 수행되었습니다!

App.tsx에서 지도 관련 로직과 UI 컴포넌트들을 성공적으로 분리했습니다. 이로 인해 코드의 가독성과 유지보수성이 크게 향상되었습니다.


91-96: MapContainer의 children으로 Bubble 컴포넌트를 전달하는 구조가 적절합니다.

지도 컨테이너와 버블 UI의 관계를 명확히 표현하고 있습니다.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (4)
src/utils/mapOverlays.ts (4)

25-33: 불필요한 이중 캐스팅 제거 및 가독성 개선

setMap 호출 때마다 임시 캐스팅하지 말고, 생성 시 한 번만 OverlayHandle로 단언하세요.

-        const overlay = new window.kakao.maps.CustomOverlay({
+        const overlay = new window.kakao.maps.CustomOverlay({
             position: markerPosition,
             content: busIconDiv,
             yAnchor: 1,
-        });
-        (overlay as unknown as { setMap: (m: unknown) => void }).setMap(map);
-
-        return overlay as OverlayHandle;
+        }) as unknown as OverlayHandle;
+        overlay.setMap(map);
+        return overlay;

버스 오버레이도 동일하게:

-        const busOverlay = new window.kakao.maps.CustomOverlay({
+        const busOverlay = new window.kakao.maps.CustomOverlay({
             position: busPosition,
             content: busDiv,
             yAnchor: 1,
-        });
-        (busOverlay as unknown as { setMap: (m: unknown) => void }).setMap(map);
-
-        return busOverlay as OverlayHandle;
+        }) as unknown as OverlayHandle;
+        busOverlay.setMap(map);
+        return busOverlay;

Also applies to: 60-68


49-49: UX: 클릭 동작이 없다면 포인터 커서 제거

커서를 포인터로 두면 클릭 가능하다고 오해할 수 있습니다.

-        busDiv.style.cursor = "pointer";
+        // 클릭 동작을 추가할 때 커서를 설정하세요.

53-53: 대체 텍스트 구체화

shuttleId는 필수 필드이므로 fallback 불필요하며, 더 의미 있는 alt로 개선하세요.

-        img.alt = bus.shuttleId || "bus";
+        img.alt = `셔틀 ${bus.shuttleId}`;

4-6: 타입 개선 제안: Kakao 타입 선언 추가로 unknown/단언 줄이기

팀 선호(any 회피)에 맞춰 최소 d.ts를 추가하면 캐스팅을 크게 줄일 수 있습니다.

예시: 새 파일 types/kakao.maps.d.ts 추가

// types/kakao.maps.d.ts
declare namespace kakao.maps {
  class Map {}
  class LatLng {
    constructor(lat: number, lng: number);
  }
  class CustomOverlay {
    constructor(opts: { position: LatLng; content: HTMLElement | string; yAnchor?: number });
    setMap(map: Map | null): void;
  }
}

그 후 이 파일에서의 시그니처를 다음처럼 조정할 수 있습니다:

-export interface OverlayHandle {
-    setMap: (map: unknown) => void;
-}
+export interface OverlayHandle {
+    setMap: (map: kakao.maps.Map | null) => void;
+}

-export const createBusStopOverlays = (map: unknown, busStops: BusStop[]): OverlayHandle[] => { ... }
+export const createBusStopOverlays = (map: kakao.maps.Map | null, busStops: BusStop[]): OverlayHandle[] => { ... }

원하시면 전체 적용 패치를 만들어드리겠습니다.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e38e4a1 and 005f7f3.

📒 Files selected for processing (2)
  • src/hooks/useMapOverlays.ts (1 hunks)
  • src/utils/mapOverlays.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/hooks/useMapOverlays.ts
🧰 Additional context used
🧠 Learnings (1)
📓 Common learnings
Learnt from: KwonDeaGeun
PR: KwonDeaGeun/WhatTheBus-Web#21
File: src/App.tsx:16-23
Timestamp: 2025-09-22T04:45:54.433Z
Learning: KwonDeaGeun은 TypeScript에서 any 타입 사용을 피하고 더 타입 안전한 코드를 선호한다.
🧬 Code graph analysis (1)
src/utils/mapOverlays.ts (2)
src/data/busStops.ts (2)
  • busStops (7-14)
  • BusStop (1-5)
src/data/bus.ts (2)
  • buses (8-39)
  • Bus (1-6)
🪛 ast-grep (0.39.5)
src/utils/mapOverlays.ts

[warning] 20-21: Direct modification of innerHTML or outerHTML properties detected. Modifying these properties with unsanitized user input can lead to XSS vulnerabilities. Use safe alternatives or sanitize content first.
Context: busIconDiv.innerHTML =
'Bus Icon'
Note: [CWE-79] Improper Neutralization of Input During Web Page Generation [REFERENCES]
- https://owasp.org/www-community/xss-filter-evasion-cheatsheet
- https://cwe.mitre.org/data/definitions/79.html

(dom-content-modification)


[warning] 20-21: Direct HTML content assignment detected. Modifying innerHTML, outerHTML, or using document.write with unsanitized content can lead to XSS vulnerabilities. Use secure alternatives like textContent or sanitize HTML with libraries like DOMPurify.
Context: busIconDiv.innerHTML =
'Bus Icon'
Note: [CWE-79] Improper Neutralization of Input During Web Page Generation [REFERENCES]
- https://www.dhairyashah.dev/posts/why-innerhtml-is-a-bad-idea-and-how-to-avoid-it/
- https://cwe.mitre.org/data/definitions/79.html

(unsafe-html-content-assignment)

🔇 Additional comments (1)
src/utils/mapOverlays.ts (1)

1-2: 중복 타입 제거 반영 확인 (굿)

Bus/BusStop를 데이터 모듈에서 import하여 로컬 인터페이스 중복을 해소했습니다.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant