Naver_Maps_Study - boostcampwm-2024/and04-Nature-Album GitHub Wiki

โ“ Compose์—์„œ Naver Maps๋ฅผ ํ™œ์šฉํ•˜๊ธฐ ์œ„ํ•œ ๋ฐฉ๋ฒ•

  • MapFragment๋ฅผ Compose๋กœ ๋ž˜ํ•‘ํ•œ๋‹ค

    Naver Maps๋Š” Fragment๋กœ ๊ตฌํ˜„ํ•˜๋Š” ๊ฒƒ์„ ๊ถŒ์žฅํ•˜๊ธฐ ๋•Œ๋ฌธ์— MapFragment๋ฅผ ํ™œ์šฉํ•ด ์ง€๋„ ๊ธฐ๋Šฅ์„ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค. MapFragment๋ฅผ Compose ๋‚ด์—์„œ ์‚ฌ์šฉํ•˜๋„๋ก ๋ž˜ํ•‘ํ•จ์œผ๋กœ์จ ํ•„์š”ํ•œ ์œ„์น˜์— ๋ฐฐ์น˜ํ•  ์ˆ˜ ์žˆ์œผ๋ฉฐ, ์ง€๋„ API์˜ ๋ชจ๋“  ๊ธฐ๋Šฅ์„ ํ™œ์šฉํ•˜๋ฉด์„œ๋„ Compose UI ๊ณ„์ธต๊ณผ ํ†ตํ•ฉํ•  ์ˆ˜ ์žˆ๋‹ค.

  • AndroidView๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ MapFragment๋ฅผ ์‚ฝ์ž…ํ•œ๋‹ค

    AndroidView๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ MapFragment๋ฅผ Compose์— ์‚ฝ์ž…ํ•  ์ˆ˜ ์žˆ๋‹ค. ์ด ๋ฐฉ์‹์œผ๋กœ ์ง€๋„ ๋ทฐ ์œ„์— Compose ์ปดํฌ๋„ŒํŠธ๋ฅผ ์˜ค๋ฒ„๋ ˆ์ดํ•  ์ˆ˜ ์žˆ์œผ๋ฉฐ, ์ด๋ฅผ ํ†ตํ•ด BottomSheet, FloatingActionButton, Marker ๋“ฑ๊ณผ ๊ฐ™์€ ์ธํ„ฐ๋ ‰์…˜ ์š”์†Œ๋ฅผ ์‰ฝ๊ฒŒ ์ถ”๊ฐ€ํ•  ์ˆ˜ ์žˆ๋‹ค. AndroidView ๋‚ด์— FragmentContainerView๋ฅผ ๊ฐ์‹ธ ์„ค์ •ํ•˜๊ณ  Compose ์ƒ์œ„์—์„œ ํ•„์š”ํ•œ ๋งˆ์ปค ๋ฐ ์˜ค๋ฒ„๋ ˆ์ด ์š”์†Œ๋ฅผ ๋ฐฐ์น˜ํ•œ๋‹ค.

  • BottomSheet์™€ Marker๋ฅผ ์ง€๋„์— ์˜ค๋ฒ„๋ ˆ์ดํ•œ๋‹ค

    Scaffold๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ํ™”๋ฉด์„ ๊ตฌ์„ฑํ•˜๊ณ , BottomSheet๋ฅผ ์ง€๋„ ์œ„์— ์˜ค๋ฒ„๋ ˆ์ด ํ˜•ํƒœ๋กœ ๋ฐฐ์น˜ํ•œ๋‹ค. ์ด๋•Œ BottomSheetScaffold๋ฅผ ์‚ฌ์šฉํ•ด ์ง€๋„์™€ ๋…๋ฆฝ์ ์ธ ์ƒํƒœ ๊ด€๋ฆฌ๊ฐ€ ๊ฐ€๋Šฅํ•˜๋ฉฐ, ์‚ฌ์šฉ์ž ์ž…๋ ฅ์ด๋‚˜ ์ •๋ณด ํ‘œ์‹œ์™€ ๊ฐ™์€ ๊ธฐ๋Šฅ์„ ์ง€๋„์™€ ๋ณ„๋„๋กœ ์ˆ˜ํ–‰ํ•  ์ˆ˜ ์žˆ๋‹ค.

  • Compose Navigation์œผ๋กœ ํ™”๋ฉด ์ „ํ™˜์„ ๊ด€๋ฆฌํ•œ๋‹ค

    Compose์˜ NavHost์™€ NavController๋ฅผ ์‚ฌ์šฉํ•ด ํ™”๋ฉด ์ „ํ™˜์„ ๊ด€๋ฆฌํ•˜๊ณ , ์ง€๋„๊ฐ€ ํฌํ•จ๋œ ํ™”๋ฉด์„ ํ•„์š”ํ•œ ์‹œ์ ์—๋งŒ ํ‘œ์‹œํ•˜๋„๋ก ํ•œ๋‹ค. ์ด๋ฅผ ํ†ตํ•ด ์„ฑ๋Šฅ ํšจ์œจ์„ ๋†’์ด๊ณ  ์ง€๋„๊ฐ€ ํ™œ์„ฑํ™”๋  ๋•Œ๋งŒ MapFragment๋ฅผ ์‚ฌ์šฉํ•˜๊ฒŒ ํ•˜์—ฌ ๋ฆฌ์†Œ์Šค๋ฅผ ์ตœ์ ํ™”ํ•  ์ˆ˜ ์žˆ๋‹ค.

โœ… And06 ๋ฏผ์ฃผ๋‹˜๋„ค ๋ฐœ์ƒ ๋ฌธ์ œ ์ฐธ๊ณ 

  • ์ง€๋‚œ ํ”ผ์–ด์„ธ์…˜์—์„œ ๋ฏผ์ฃผ๋‹˜๋„ค๊ฐ€ ๋จผ์ € MapFragment ๋„์ž…์„ ์‹œ๋„ํ–ˆ๊ณ  ์ƒ๋ช…์ฃผ๊ธฐ ๋ฌธ์ œ๊ฐ€ ์žˆ์—ˆ๋‹ค๋Š” ๊ฒƒ์„ ์•Œ๊ฒŒ ๋˜์—ˆ๋‹ค. ์šฐ๋ฆฌ ํŒ€๋„ Naver Maps๋กœ XML์„ Compose์—์„œ ์‚ฌ์šฉํ•˜๋ฉด์„œ ๋ฐœ์ƒํ•˜๋Š” ๋ฌธ์ œ๋“ค์„ ํ•ด๊ฒฐํ•ด๋ณด๊ณ  ์ƒ๋ช…์ฃผ๊ธฐ์— ๋งž๊ฒŒ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์„ ๋„์ „ํ•ด๋ณด๋Š” ๊ฒƒ์ด ๋ชฉํ‘œ์˜€๋‹ค. ๊ทธ๋ž˜์„œ ๋‚˜๋„ ๋จผ์ € Fragment๋กœ ์‹คํ—˜ํ•ด๋ณด๋‹ค๊ฐ€ ๋ฏผ์ฃผ๋‹˜๊ป˜ DM์œผ๋กœ ํ˜น์‹œ๋‚˜ ์ƒ๋ช…์ฃผ๊ธฐ ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ–ˆ๋Š”์ง€ ๋จผ์ € ์—ฌ์ญˆ์–ด๋ณด์•˜๊ณ  ์•„์ง ํ•ด๊ฒฐ๋˜์ง€ ์•Š์•„ ์šฐ๋ฆฌ ํŒ€์ด ํ•ด๊ฒฐ๋˜๋ฉด ๋งํ•ด๋‹ฌ๋ผ๊ณ  ํ–ˆ๋‹ค. ์•„๋ž˜๋Š” ๋ฏผ์ฃผ๋‹˜์„ ํ†ตํ•ด ์•Œ๊ฒŒ ๋œ ๋ฏผ์ฃผ๋‹˜๋„ค ์ง„ํ–‰ ์ƒํ™ฉ์„ ์ •๋ฆฌํ•ด๋‘” ๊ฒƒ์ด๋‹ค.

  • naver map์„ fragment๋กœ ํ•˜์—ฌ android view ๋กœ compose์— ์‚ฝ์ž…

  • ์ด๋กœ ์ธํ•ด fragment ์œ„ ๋งˆ์ปค ๋“ฑ์€ custom view๋กœ ๋งŒ๋“ค์–ด fragment์—์„œ ํ•˜๋˜ ๊ฒƒ์ฒ˜๋Ÿผ ์ง„ํ–‰ํ•ด์•ผ ํ•จ

    [[๋„ค์ด๋ฒ„ ์ง€๋„ API] ๋งˆ์ปค ์•„์ด์ฝ˜ ์ปค์Šคํ…€ ๋ทฐ ๋งŒ๋“ค๊ธฐ](https://www.notion.so/API-13aea5e5fc1780e3a497fb5cdc4e2e9c?pvs=21)

    • Fragment ์œ„์— composable์„ ๋„์›Œ์„œ ํ‘œ์‹œํ•˜๋Š” ๊ฒƒ ์‹คํŒจํ•˜์…จ๋‹ค๊ณ  ํ•จ. ์ปค์Šคํ…€ ๋ทฐ๋กœ ์ถฉ๋ถ„ํžˆ ๊ฐ€๋Šฅํ•ด์„œ ๊ผญ ์•ˆํ•ด๋„ ๋  ๊ฒƒ ๊ฐ™๋‹ค๊ณ  ๋ง์”€ํ•˜์‹ฌ. โ‡’ ์‹œ๊ฐ„ ๋˜๋ฉด ์‹œ๋„ํ•ด๋ณด๊ธฐ
  • Fragment์™€ Compose ๊ฐ„์˜ ์ƒ๋ช…์ฃผ๊ธฐ ๋ฌธ์ œ

https://github.com/boostcampwm-2024/and06-musicroad/pull/85

  • ์œ„์˜ PR ๋งˆ์ง€๋ง‰ Comment ์ฐธ๊ณ 

์ €๋„ ๋‹ค๋ฅธ ํ™”๋ฉด์œผ๋กœ ์ด๋™ํ–ˆ๋‹ค๊ฐ€ ๋Œ์•„์˜ค๋ฉด ๋งˆ์ปค ์ƒ์„ฑ์ด ์—ฌ๋Ÿฌ ๊ฐœ๊ฐ€ ๋˜๋Š” ๊ฒƒ์€ ๋กœ๊ทธ๋ฅผ ํ†ตํ•ด ์ธ์ง€ํ•˜๊ณ  ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค. ๋‹ค๋ฅธ ํ™”๋ฉด์„ n๋ฒˆ ๋‹ค๋…€์˜ค๋ฉด ์ค‘์•™ ๋ฒ„ํŠผ์„ ํ•œ ๋ฒˆ ํด๋ฆญํ–ˆ์„ ๋•Œ ๋งˆ์ปค ์ƒ์„ฑ ๋กœ๊ทธ๊ฐ€ n๋ฒˆ ์ฐํž™๋‹ˆ๋‹ค. ์ด๋ฒคํŠธ ๋ฐฉ์ถœ์€ ํ•œ ๋ฒˆ ๋˜๋Š”๋ฐ ์ด๋ฒคํŠธ ๋ฌธ์ œ๋ผ๊ธฐ๋ณด๋‹จ ํ”„๋ž˜๊ทธ๋จผํŠธ ๋ฌธ์ œ๋ผ๊ณ  ์ƒ๊ฐํ•˜๊ณ  ์žˆ์—ˆ๋Š”๋ฐ ์œค๊ฒธ๋‹˜์ด ์–ธ๊ธ‰ํ•ด์ฃผ์…”์„œ ๋‹ค์‹œ ์ด ๋ฌธ์ œ์— ๋Œ€ํ•ด ์•Œ์•„๋ดค์Šต๋‹ˆ๋‹ค.

์ €ํฌ๋Š” ์ง€๊ธˆ MapScreen ์•ˆ์—์„œ AndroidView๋กœ MapFragment๋ฅผ ๋„์šฐ๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค.

๋‹ค๋ฅธ ํ™”๋ฉด์„ ๋‹ค๋…€์™”์„ ๋•Œ ๋ฒ„ํŠผ์„ ํด๋ฆญํ•˜๋ฉด ์ด๋ฒคํŠธ ๋ฐฉ์ถœ์€ ํ•œ ๋ฒˆ์ธ๋ฐ ์™œ ๋งˆ์ปค ์ƒ์„ฑ์ด ๋‘ ๋ฒˆ ๋˜๋Š”์ง€ ์ด๊ฒƒ์ €๊ฒƒ ๊ฐ€์„ค์„ ์„ธ์›Œ๋ณด๊ณ  ๋กœ๊ทธ๋ฅผ ์ฐ์–ด๋ณด๋ฉฐ ์›์ธ์„ ์ฐพ์•˜์Šต๋‹ˆ๋‹ค.

AndroidView(
    modifier=Modifier.fillMaxSize(),
    factory= { context->
FragmentContainerView(context).apply {
            id=View.generateViewId()

            (contextasFragmentActivity).supportFragmentManager.beginTransaction()
                .replace(id,MapFragment())
                .commitAllowingStateLoss()
        }
    }
)

ํ˜„์žฌ ์œ„์ฒ˜๋Ÿผ AndroidView๊ฐ€ ์ž‘์„ฑ๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค. AndroidView์˜ factory๋Š” composable์ด ์ปดํฌ์ง€์…˜๋  ๋•Œ ์‹คํ–‰๋ฉ๋‹ˆ๋‹ค. ํ™”๋ฉด์„ ์ด๋™ํ–ˆ๋‹ค๊ฐ€ ๋Œ์•„์˜ค๋ฉด ์ƒˆ๋กœ ์ปดํฌ์ง€์…˜ ๋˜๊ธฐ ๋•Œ๋ฌธ์— ์ƒˆ๋กœ์šด ํ™”๋ฉด์œผ๋กœ ์ด๋™ํ–ˆ๋‹ค๊ฐ€ ๋Œ์•„์˜ฌ ๋•Œ๋งˆ๋‹ค factory๊ฐ€ ํ˜ธ์ถœ๋ฉ๋‹ˆ๋‹ค.

factory ์•ˆ์—์„œ id๋ฅผ View.generateViewId()๋กœ ์ƒ์„ฑํ•˜๊ณ  ์žˆ๋Š”๋ฐ ์ด๋Š” factory๊ฐ€ ์‹คํ–‰๋  ๋•Œ๋งˆ๋‹ค ๋‹ค๋ฅธ ๊ฐ’์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. ๋”ฐ๋ผ์„œ replace๊ฐ€ ๋˜๋Š” ๊ฒƒ์ด ์•„๋‹ˆ๋ผ ์ƒˆ๋กญ๊ฒŒ ์ถ”๊ฐ€๊ฐ€ ๋ฉ๋‹ˆ๋‹ค.

๋‹ค๋ฅธ ํ™”๋ฉด์œผ๋กœ ๊ฐ”๋‹ค๊ฐ€ ๋‹ค์‹œ ๋Œ์•„์™”์„ ๋•Œ factory์—์„œ ์ƒˆ๋กœ์šด ํ”„๋ž˜๊ทธ๋จผํŠธ๊ฐ€ ๋งŒ๋“ค์–ด์ง€๊ณ  ์ด์ „ ํ”„๋ž˜๊ทธ๋จผํŠธ๋„ ์—ฌ์ „ํžˆ ์‚ด์•„์žˆ๋Š” ์ƒํƒœ์ž…๋‹ˆ๋‹ค. ํ”„๋ž˜๊ทธ๋จผํŠธ๋ผ๋ฆฌ ๋ทฐ๋ชจ๋ธ์€ ๊ณต์œ ํ•˜๊ณ  ์žˆ๊ธฐ ๋•Œ๋ฌธ์— ๋ฒ„ํŠผ์„ ํ•œ ๋ฒˆ ๋ˆŒ๋ €์„ ๋•Œ ๋‘ ํ”„๋ž˜๊ทธ๋จผํŠธ์—์„œ ์ด๋ฒคํŠธ๋ฅผ ๋ฐ›๊ณ  ๊ฐ์ž ๋งˆ์ปค๋ฅผ ์ƒ์„ฑํ•˜์—ฌ ๋กœ๊ทธ๊ฐ€ ๋‘ ๋ฒˆ ์ฐํžˆ๋Š” ๊ฒƒ์ด์—ˆ์Šต๋‹ˆ๋‹ค.

image

์œ„ ์ฝ”๋“œ์—์„œ id ๊ฐ’์„ ๊ณ ์ • ๊ฐ’(ex. R.id.fragment_map)์œผ๋กœ ๋ฐ”๊พธ๋ฉด replace์—์„œ ํ”„๋ž˜๊ทธ๋จผํŠธ๊ฐ€ ๊ต์ฒด๋˜๊ธฐ ๋•Œ๋ฌธ์— ๋‹ค๋ฅธ ํ™”๋ฉด์„ ๊ฐ”๋‹ค์™€๋„ ํ”„๋ž˜๊ทธ๋จผํŠธ๊ฐ€ ํ•˜๋‚˜๋งŒ ์žˆ๊ฒŒ ๋ฉ๋‹ˆ๋‹ค. ๋”ฐ๋ผ์„œ ํ™”๋ฉด์„ ์ด๋™ํ•˜๊ณ  ๋Œ์•„์™€๋„ ๋ฒ„ํŠผ์„ ๋ˆŒ๋ €์„ ๋•Œ ๋งˆ์ปค๊ฐ€ ์ค‘๋ณต์œผ๋กœ ์ƒ์„ฑ๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

๊ทธ๋Ÿฐ๋ฐ ์œ„์ฒ˜๋Ÿผ ๋ณ€๊ฒฝํ–ˆ์„ ๋•Œ ์™œ ๋‹ค๋ฅธ ํ™”๋ฉด์œผ๋กœ ๊ฐ€๋„ MapScreen์ด ์‹คํ–‰๋˜๋Š”์ง€(๋กœ๊ทธ์—์„œ ํƒœ๊ทธ๊ฐ€ MapScreen์ธ ๊ฒƒ์ด MapScreen์ด ์‹คํ–‰๋œ ๊ฒƒ์ž…๋‹ˆ๋‹ค), ๋‹ค๋ฅธ ํ™”๋ฉด์œผ๋กœ ๊ฐ”์„ ๋•Œ ๊ธฐ์กด ํ”„๋ž˜๊ทธ๋จผํŠธ๊ฐ€ onPause() ๋˜๋Š” ๊ฒŒ ์•„๋‹ˆ๋ผ ๋‹ค์‹œ ๋Œ์•„์™”์„ ๋•Œ onPause()๋ถ€ํ„ฐ ์†Œ๋ฉธ๊นŒ์ง€ ์ˆ˜ํ–‰ํ•˜๋Š” ๊ฒƒ ๋“ฑ ์•„์ง ์ดํ•ดํ•  ์ˆ˜ ์—†๋Š” ๋ถ€๋ถ„๋“ค์ด ์žˆ์Šต๋‹ˆ๋‹ค ใ… 

image

๊ฝค ์•Œ์•„๋ด์•ผ ํ•  ๋ถ€๋ถ„์ด ์žˆ์„ ๊ฒƒ ๊ฐ™์•„ ์ด PR์„ ๊ณ„์† ์—ด์–ด๋‘๋Š” ๊ฒƒ๋ณด๋‹ค๋Š” bug ์ด์Šˆ๋ฅผ ์ƒ์„ฑํ•˜๊ณ  ๊ฑฐ๊ธฐ์„œ ํ•ด๊ฒฐํ•ด๋ณด๋Š” ๊ฒƒ๋„ ์ข‹์„ ๊ฒƒ ๊ฐ™์Šต๋‹ˆ๋‹ค.

compose navigation๊ณผ fragment๋ฅผ ํ•จ๊ป˜ ์‚ฌ์šฉํ•˜๋‹ˆ ์ƒ๋ช…์ฃผ๊ธฐ ๊ด€๋ฆฌ๊ฐ€ ์–ด๋ ต๋„ค์š” ใ… ใ… 


๐Ÿ’ก ์•Œ๊ณ  ๋„˜์–ด๊ฐ€์•ผ ํ•  ๊ฒƒ๋“ค

  • Fragment์™€ Compose์˜ ์ƒ๋ช…์ฃผ๊ธฐ ์ฐจ์ด
    • AndroidView๋ฅผ ํ†ตํ•ด Fragment๋ฅผ ์‚ฝ์ž…ํ•˜๋ฉด, Fragment์˜ ์ƒ๋ช…์ฃผ๊ธฐ์™€ Compose ์ƒ๋ช…์ฃผ๊ธฐ๊ฐ€ ์„œ๋กœ ๋‹ค๋ฅธ ๋ฐฉ์‹์œผ๋กœ ๋™์ž‘ํ•˜๊ฒŒ ๋˜์–ด, ํ™”๋ฉด ์žฌ์ง„์ž… ์‹œ Fragment๊ฐ€ ์ค‘๋ณต ์ƒ์„ฑ๋˜๋Š” ๋ฌธ์ œ๋ฅผ ์ผ์œผํ‚ฌ ์ˆ˜ ์žˆ์Œ
  • AndroidView์˜ factory ์†์„ฑ ์ž‘๋™ ๋ฐฉ์‹
    • factory๋Š” Composable์ด ์ฒ˜์Œ ์ƒ์„ฑ๋˜๊ฑฐ๋‚˜ ์žฌ์ƒ์„ฑ๋  ๋•Œ ์‹คํ–‰
    • ํ™”๋ฉด์„ ์ด๋™ํ•˜๊ฑฐ๋‚˜ ๋Œ์•„์˜ฌ ๋•Œ๋งˆ๋‹ค factory๊ฐ€ ํ˜ธ์ถœ๋˜๋ฉด, View.generateViewId()๋กœ ์ƒ์„ฑ๋œ ID๊ฐ€ ๋ณ€๊ฒฝ๋˜์–ด ์ƒˆ๋กœ์šด Fragment๊ฐ€ ์ถ”๊ฐ€๋  ์ˆ˜ ์žˆ์Œ
      • custom view๋กœ ๋งˆ์ปค ์ƒ์„ฑํ•  ๋•Œ id๋ฅผ ์ง€์ •ํ•ด๋‘๊ณ  ์‚ฌ์šฉํ•ด์•ผ๊ฒ ์Œ
      • id ๊ฐ’์„ ๊ณ ์ •๋œ ๋ฆฌ์†Œ์Šค ID (์˜ˆ: R.id.fragment_map)๋กœ ์„ค์ •ํ•˜์—ฌ Fragment๊ฐ€ ๊ต์ฒด๋˜๋„๋ก ํ•˜๊ธฐ
  • Compose์™€ Fragment๋ฅผ ํ•จ๊ป˜ ์‚ฌ์šฉํ•  ๋•Œ์˜ Navigation ๋ฌธ์ œ
    • Compose Navigation๊ณผ Fragment Navigation์€ ๊ฐ๊ฐ ๋ณ„๋„์˜ ์Šคํƒ์„ ๊ด€๋ฆฌ
    • Compose Navigation์œผ๋กœ ํ™”๋ฉด์„ ์ด๋™ํ•˜๋ฉด Fragment์˜ ์ƒ๋ช…์ฃผ๊ธฐ๊ฐ€ ์ ์ ˆํ•˜๊ฒŒ ๊ด€๋ฆฌ๋˜์ง€ ์•Š๊ฑฐ๋‚˜ ์˜ˆ์ƒ์น˜ ๋ชปํ•œ ๋™์ž‘์„ ์œ ๋ฐœ(ํ™”๋ฉด ์ „ํ™˜์ด๋‚˜ ์žฌ์ง„์ž… ์‹œ ๋” ๋ณต์žก)
  • Fragment ์ƒ๋ช…์ฃผ๊ธฐ์™€ ViewModel ๊ณต์œ 
    • ํ™”๋ฉด ์ด๋™ ํ›„ ๋Œ์•„์™”์„ ๋•Œ ๋‘ ๊ฐœ์˜ Fragment๊ฐ€ ๊ฐ™์€ ViewModel์„ ๊ณต์œ ํ•˜๊ณ  ์žˆ๋‹ค๋ฉด, ์ด๋ฒคํŠธ๊ฐ€ ์ค‘๋ณต ์ฒ˜๋ฆฌ๋  ๊ฐ€๋Šฅ์„ฑ์ด ์žˆ์Œ( Fragment๊ฐ€ ์—ฌ๋Ÿฌ ๊ฐœ ์ค‘๋ณต ์ƒ์„ฑ๋˜์—ˆ๊ธฐ ๋•Œ๋ฌธ)
      • Fragment๋ฅผ ๊ณ ์ •๋œ ID๋กœ ์„ค์ •ํ•˜์—ฌ ์ค‘๋ณต ์ƒ์„ฑ์„ ๋ง‰์•„์•ผ ํ•จ

๐Ÿ’ก Naver Maps ๊ณต๋ถ€ํ•˜๊ณ  ์‹œ์ž‘ํ•˜๊ธฐ

Naver Maps๋ฅผ Compose์— MapView๋กœ ์ ์šฉํ•œ ์‚ฌ๋ก€

์˜ค๋ฒ„๋ ˆ์ด ํ•  ๋•Œ ์œ ์˜ํ•  ์ 

https://navermaps.github.io/android-map-sdk/guide-ko/5-1.html

๋ฉ€ํ‹ฐ์Šค๋ ˆ๋”ฉ

์˜ค๋ฒ„๋ ˆ์ด ๊ฐ์ฒด๋Š” ์•„๋ฌด ์Šค๋ ˆ๋“œ์—์„œ๋‚˜ ์ƒ์„ฑํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๊ทธ๋Ÿฌ๋‚˜ ์˜ค๋ฒ„๋ ˆ์ด์˜ ์†์„ฑ์€ ์Šค๋ ˆ๋“œ ์•ˆ์ „์„ฑ์ด ๋ณด์žฅ๋˜์ง€ ์•Š์œผ๋ฏ€๋กœ ์—ฌ๋Ÿฌ ์Šค๋ ˆ๋“œ์—์„œ ๋™์‹œ์— ์ ‘๊ทผํ•ด์„œ๋Š” ์•ˆ๋ฉ๋‹ˆ๋‹ค. ํŠนํžˆ ์ง€๋„์— ์ถ”๊ฐ€๋œ ์˜ค๋ฒ„๋ ˆ์ด์˜ ์†์„ฑ์€ ๋ฉ”์ธ ์Šค๋ ˆ๋“œ์—์„œ๋งŒ ์ ‘๊ทผํ•ด์•ผ ํ•˜๋ฉฐ, ๊ทธ๋ ‡์ง€ ์•Š์œผ๋ฉด [CalledFromWrongThreadException](https://navermaps.github.io/android-map-sdk/reference/com/naver/maps/map/CalledFromWrongThreadException.html)์ด ๋ฐœ์ƒํ•ฉ๋‹ˆ๋‹ค. ๋‹จ, ์˜ค๋ฒ„๋ ˆ์ด๊ฐ€ ์ง€๋„์— ์ถ”๊ฐ€๋˜์ง€ ์•Š์•˜๋‹ค๋ฉด ๋‹ค๋ฅธ ์Šค๋ ˆ๋“œ์—์„œ ์˜ค๋ฒ„๋ ˆ์ด์˜ ์†์„ฑ์— ์ ‘๊ทผํ•ด๋„ ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

๋”ฐ๋ผ์„œ ๋Œ€๋Ÿ‰์˜ ์˜ค๋ฒ„๋ ˆ์ด๋ฅผ ๋‹ค๋ฃฐ ๊ฒฝ์šฐ ๊ฐ์ฒด๋ฅผ ์ƒ์„ฑํ•˜๊ณ  ์ดˆ๊ธฐ ์˜ต์…˜์„ ์ง€์ •ํ•˜๋Š” ์ž‘์—…์€ ๋ฐฑ๊ทธ๋ผ์šด๋“œ ์Šค๋ ˆ๋“œ์—์„œ ์ˆ˜ํ–‰ํ•˜๊ณ  ์ง€๋„์— ์ถ”๊ฐ€ํ•˜๋Š” ์ž‘์—…๋งŒ์„ ๋ฉ”์ธ ์Šค๋ ˆ๋“œ์—์„œ ์ˆ˜ํ–‰ํ•˜๋ฉด ๋ฉ”์ธ ์Šค๋ ˆ๋“œ๋ฅผ ํšจ์œจ์ ์œผ๋กœ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๋‹ค์Œ์€ 1000๊ฐœ์˜ ๋งˆ์ปค๋ฅผ ๋ฐฑ๊ทธ๋ผ์šด๋“œ ์Šค๋ ˆ๋“œ์—์„œ ์ƒ์„ฑํ•˜๊ณ  ์†์„ฑ์„ ์ง€์ •ํ•œ ํ›„ ๋ฉ”์ธ ์Šค๋ ˆ๋“œ์—์„œ ์ง€๋„์— ์ถ”๊ฐ€ํ•˜๋Š” ์˜ˆ์ œ์ž…๋‹ˆ๋‹ค.

image

DisposableEffect์™€ SideEffect

  • DisposableEffect๋Š” ํŠน์ • Composable์ด ํ™”๋ฉด์— ์ง„์ž…ํ•˜๊ฑฐ๋‚˜ ๋– ๋‚  ๋•Œ ํ•œ ๋ฒˆ์”ฉ ์‹คํ–‰๋˜์–ด์•ผ ํ•˜๋Š” ์ž‘์—…์„ ์ฒ˜๋ฆฌํ•˜๋Š” ๋ฐ ์œ ์šฉ
  • SideEffect๋Š” ์ปดํฌ์ง€์…˜์ด ์„ฑ๊ณต์ ์œผ๋กœ ์™„๋ฃŒ๋œ ํ›„ ํ•œ ๋ฒˆ ์‹คํ–‰๋˜์–ด์•ผ ํ•˜๋Š” ์ž‘์—…์„ ์ฒ˜๋ฆฌ
  • ๊ธฐ์กด์˜ Fragment๋‚˜ Activity์—์„œ์˜ onResume/onPause์™€ ๊ฐ™์€ ์—ญํ• ์„ ์ˆ˜ํ–‰

์ฐธ๊ณ  ๋งํฌ

https://www.youtube.com/watch?v=G67sUjEpsGQ

https://github.com/mapbox/mapbox-maps-android/blob/main/extension-compose/src/main/java/com/mapbox/maps/extension/compose/internal/MapViewLifecycle.kt

๐Ÿ’ก MapView๋กœ ์ƒ๋ช… ์ฃผ๊ธฐ ๊ด€๋ฆฌ

์ฐธ๊ณ 

image

๐Ÿ’ก ์ง„ํ–‰ํ•ด๋ณด๊ธฐ

  • ์•„๋ž˜์˜ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์ฐธ๊ณ ํ•ด์„œ MapView์˜ ์ƒ๋ช…์ฃผ๊ธฐ๋ฅผ ์กฐ์ ˆํ•ด๋ณด์ž.

https://github.com/mapbox/mapbox-maps-android/blob/main/extension-compose/src/main/java/com/mapbox/maps/extension/compose/internal/MapViewLifecycle.kt

package com.and04.naturealbum.ui.maps

import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.viewinterop.AndroidView
import com.naver.maps.map.MapView
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver
import androidx.lifecycle.LifecycleOwner
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.platform.LocalLifecycleOwner

@Composable
fun MapScreen(modifier: Modifier = Modifier) {
    // ํ˜„์žฌ Context๋ฅผ ๊ฐ€์ ธ์˜ต๋‹ˆ๋‹ค. ์ด๋Š” MapView ์ดˆ๊ธฐํ™” ์‹œ ์‚ฌ์šฉ
    val context = LocalContext.current
    // ํ˜„์žฌ MapScreen์˜ LifecycleOwner ๊ฐ€์ ธ์˜ด. ์ด๋Š” ์ƒ๋ช…์ฃผ๊ธฐ๋ฅผ ๊ด€๋ฆฌํ•˜๋Š” ๋ฐ ์‚ฌ์šฉ
    val lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current

    // MapView๋ฅผ Compose์—์„œ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋„๋ก rememberSaveable์„ ์‚ฌ์šฉํ•˜์—ฌ ์ƒํƒœ๋ฅผ ๊ด€๋ฆฌ
    // Configuration change์—๋„ MapView๊ฐ€ ์žฌ์ƒ์„ฑ๋˜์ง€ ์•Š๊ณ  ์œ ์ง€
    val MAP_VIEW_ID = 12345 // ์ดํ›„ ids.xml๋กœ ๊ณ ์ • id ๋นผ๊ธฐ

    val mapView = remember {
        MapView(context).apply {
            id = MAP_VIEW_ID // MapView์— ๊ณ ์œ ํ•œ ID๋ฅผ ์„ค์ •
        }
    }

    // MapView์˜ ์ƒ๋ช…์ฃผ๊ธฐ๋ฅผ ๊ด€๋ฆฌํ•˜๊ธฐ ์œ„ํ•ด DisposableEffect๋ฅผ ์‚ฌ์šฉ
    DisposableEffect(lifecycleOwner) {
        // ํ˜„์žฌ LifecycleOwner์˜ Lifecycle์„ ๊ฐ€์ ธ์˜ด
        val lifecycle = lifecycleOwner.lifecycle
        // Lifecycle ์ด๋ฒคํŠธ๋ฅผ ๊ด€์ฐฐํ•˜๋Š” Observer๋ฅผ ์ƒ์„ฑ
        val observer = LifecycleEventObserver { _, event ->
            when (event) {
                Lifecycle.Event.ON_CREATE -> mapView.onCreate(null)
                Lifecycle.Event.ON_START -> mapView.onStart()
                Lifecycle.Event.ON_RESUME -> mapView.onResume()
                Lifecycle.Event.ON_PAUSE -> mapView.onPause()
                Lifecycle.Event.ON_STOP -> mapView.onStop()
                Lifecycle.Event.ON_DESTROY -> mapView.onDestroy()
                else -> Unit
            }
        }
        // Lifecycle์— Observer๋ฅผ ์ถ”๊ฐ€ํ•˜์—ฌ ์ƒ๋ช…์ฃผ๊ธฐ๋ฅผ ๊ด€์ฐฐ
        lifecycle.addObserver(observer)

        // DisposableEffect๊ฐ€ ํ•ด์ œ๋  ๋•Œ Observer๋ฅผ ์ œ๊ฑฐํ•˜๊ณ  MapView์˜ ๋ฆฌ์†Œ์Šค๋ฅผ ํ•ด์ œ
        onDispose {
            lifecycle.removeObserver(observer)
            mapView.onDestroy() // MapView์˜ ๋ฆฌ์†Œ์Šค๋ฅผ ํ•ด์ œํ•˜์—ฌ ๋ฉ”๋ชจ๋ฆฌ ๋ˆ„์ˆ˜๋ฅผ ๋ฐฉ์ง€
        }
    }

    // AndroidView๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ Compose ๋‚ด์— MapView๋ฅผ ํ‘œ์‹œ
    AndroidView(factory = { mapView }, modifier = modifier) {
        // ํ•„์š”ํ•œ ๊ฒฝ์šฐ ์ถ”๊ฐ€์ ์ธ ์„ค์ •์„ ์—…๋ฐ์ดํŠธ
        it.getMapAsync { naverMap ->
            // ์ง€๋„๊ฐ€ ์ค€๋น„๋œ ํ›„ ์ถ”๊ฐ€ ์„ค์ •์„ ์ˆ˜ํ–‰
            naverMap.minZoom = 10.0
            naverMap.maxZoom = 18.0
        }
    }
}

image

๐Ÿ’ก OverlayImage ์—ด์–ด๋ณด๊ธฐ

  • ๋„ค์ด๋ฒ„ ์ง€๋„ API์—์„œ ์ง€๋„์— ์‚ฌ์šฉํ•  ์ด๋ฏธ์ง€ ์˜ค๋ฒ„๋ ˆ์ด๋ฅผ ์ƒ์„ฑํ•˜๊ธฐ ์œ„ํ•œ OverlayImage ์ถ”์ƒ ํด๋ž˜์Šค
  • ๋‹ค์–‘ํ•œ ์†Œ์Šค(๋ทฐ, ๋น„ํŠธ๋งต, ๋ฆฌ์†Œ์Šค ID, ์—์…‹ ํŒŒ์ผ, ์•ฑ ๋‚ด๋ถ€ ํŒŒ์ผ, ํŒŒ์ผ ๊ฒฝ๋กœ ๋“ฑ)์—์„œ ์ด๋ฏธ์ง€๋ฅผ ๊ฐ€์ ธ์™€ ์˜ค๋ฒ„๋ ˆ์ด ์ด๋ฏธ์ง€๋กœ ๋ณ€ํ™˜ํ•˜๋Š” ํด๋ž˜์Šค

์ฃผ์š” ๊ตฌ์„ฑ ์š”์†Œ

  1. ํ•„๋“œ
    • id: ์˜ค๋ฒ„๋ ˆ์ด ์ด๋ฏธ์ง€์˜ ๊ณ ์œ  ์‹๋ณ„์ž์ด๋‹ค. ์†Œ์Šค์— ๋”ฐ๋ผ ID๋Š” ๋‹ค๋ฅด๊ฒŒ ์„ค์ •๋œ๋‹ค.
  2. ์ •์  ํŒฉํ† ๋ฆฌ ๋ฉ”์„œ๋“œ
    • ๋‹ค์–‘ํ•œ ์†Œ์Šค์—์„œ OverlayImage ๊ฐ์ฒด๋ฅผ ์ƒ์„ฑํ•˜๊ธฐ ์œ„ํ•ด ์—ฌ๋Ÿฌ ์ •์  ๋ฉ”์„œ๋“œ๋ฅผ ์ œ๊ณตํ•œ๋‹ค:
      • fromView(View view): ๋ทฐ๋ฅผ ํ†ตํ•ด ์˜ค๋ฒ„๋ ˆ์ด ์ด๋ฏธ์ง€๋ฅผ ์ƒ์„ฑํ•œ๋‹ค.
      • fromBitmap(Bitmap bitmap): ๋น„ํŠธ๋งต์œผ๋กœ๋ถ€ํ„ฐ ์˜ค๋ฒ„๋ ˆ์ด ์ด๋ฏธ์ง€๋ฅผ ์ƒ์„ฑํ•œ๋‹ค.
      • fromResource(int resourceId): ๋ฆฌ์†Œ์Šค ID๋ฅผ ํ†ตํ•ด ์˜ค๋ฒ„๋ ˆ์ด ์ด๋ฏธ์ง€๋ฅผ ์ƒ์„ฑํ•œ๋‹ค.
      • fromAsset(String assetName): ์—์…‹ ํŒŒ์ผ ์ด๋ฆ„์„ ํ†ตํ•ด ์˜ค๋ฒ„๋ ˆ์ด ์ด๋ฏธ์ง€๋ฅผ ์ƒ์„ฑํ•œ๋‹ค.
      • fromPrivateFile(String fileName): ์•ฑ์˜ ๋‚ด๋ถ€ ํŒŒ์ผ์„ ํ†ตํ•ด ์˜ค๋ฒ„๋ ˆ์ด ์ด๋ฏธ์ง€๋ฅผ ์ƒ์„ฑํ•œ๋‹ค.
      • fromPath(String absolutePath): ํŒŒ์ผ์˜ ์ ˆ๋Œ€ ๊ฒฝ๋กœ๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์˜ค๋ฒ„๋ ˆ์ด ์ด๋ฏธ์ง€๋ฅผ ์ƒ์„ฑํ•œ๋‹ค.
      • fromFile(File file): File ๊ฐ์ฒด๋ฅผ ์‚ฌ์šฉํ•ด ์˜ค๋ฒ„๋ ˆ์ด ์ด๋ฏธ์ง€๋ฅผ ์ƒ์„ฑํ•œ๋‹ค.
  3. ์ถ”์ƒ ๋ฉ”์„œ๋“œ
    • getBitmap(Context context): ๊ตฌ์ฒด์ ์ธ ์˜ค๋ฒ„๋ ˆ์ด ์ด๋ฏธ์ง€์—์„œ ๋น„ํŠธ๋งต์„ ๊ฐ€์ ธ์˜ค๋Š” ๋ฉ”์„œ๋“œ๋กœ, ๊ฐ ์†Œ์Šค๋ณ„ ํด๋ž˜์Šค์—์„œ ๊ตฌํ˜„ํ•ด์•ผ ํ•œ๋‹ค.
  4. equals์™€ hashCode ๋ฉ”์„œ๋“œ
    • ๋‘ OverlayImage ๊ฐ์ฒด๊ฐ€ ๋™์ผํ•œ id๋ฅผ ๊ฐ€์ง€๋Š”์ง€ ํ™•์ธํ•˜๊ธฐ ์œ„ํ•ด equals์™€ hashCode ๋ฉ”์„œ๋“œ๋ฅผ ์žฌ์ •์˜ํ•œ๋‹ค.
AndroidView(factory = { mapView }, modifier = modifier) {
        // ํ•„์š”ํ•œ ๊ฒฝ์šฐ ์ถ”๊ฐ€์ ์ธ ์„ค์ •์„ ์—…๋ฐ์ดํŠธ
        it.getMapAsync { naverMap ->
            // TODO: ์ง€๋„๊ฐ€ ์ค€๋น„๋œ ํ›„ ์ถ”๊ฐ€ ์„ค์ •์„ ์ˆ˜ํ–‰
            naverMap.minZoom = 10.0
            naverMap.maxZoom = 18.0
                photos.value.map { photoDetail ->
                    Marker().apply {
                        position = LatLng(photoDetail.latitude, photoDetail.longitude)
                        icon = OverlayImage.fromView(ImageMarker(context, photoDetail.photoUri))
                        width = 72
                        height = 100
                        map = naverMap
                    }
                }

        }

    }
import android.content.Context
import android.util.AttributeSet
import android.view.LayoutInflater
import android.widget.ImageView
import androidx.constraintlayout.widget.ConstraintLayout
import coil3.load
import coil3.request.crossfade
import coil3.request.error
import coil3.request.placeholder
import com.and04.naturealbum.R

class ImageMarker @JvmOverloads constructor(
    context: Context,
    uri: String,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0
) : ConstraintLayout(context, attrs, defStyleAttr) {
    init {
        LayoutInflater.from(context).inflate(R.layout.image_marker, this, true)
        findViewById<ImageView>(R.id.iv_marker_image).load(uri) {
            crossfade(true)
            placeholder(R.drawable.cat_dummy)
            error(R.drawable.ic_launcher_background)
        }
    }
}
โš ๏ธ **GitHub.com Fallback** โš ๏ธ