Skip to content

llimental/Raon

Folders and files

NameName
Last commit message
Last commit date

Latest commit

ย 

History

345 Commits
ย 
ย 
ย 
ย 
ย 
ย 

Repository files navigation

Raon README

1. ํ”„๋กœ์ ํŠธ ์†Œ๊ฐœ

2. ๊ตฌํ˜„ ํ™”๋ฉด

3. ํ”„๋กœ์ ํŠธ ๊ตฌ์กฐ

4. ํ”„๋กœ์ ํŠธ ๋””์ž์ธ

5. STEP๋ณ„ ๊ตฌํ˜„ ๋‚ด์šฉ

6. ์—…๋ฐ์ดํŠธ ๋‚ด์—ญ

7. ํŠธ๋Ÿฌ๋ธ” ์ŠˆํŒ…

8. ์ถ”ํ›„ ๊ณ„ํš


ํ”„๋กœ์ ํŠธ ์†Œ๊ฐœ

  • ๐Ÿƒ๐Ÿป๐Ÿƒ๐Ÿปโ€โ™‚๏ธ๐Ÿ’จ ํ”„๋กœ์ ํŠธ ๊ตฌ์ƒ: 24. 01. 08. ~ 24. 01. 15.

  • ๐Ÿƒ๐Ÿป๐Ÿƒ๐Ÿปโ€โ™‚๏ธ๐Ÿ’จ ํ”„๋กœ์ ํŠธ ๊ธฐ๊ฐ„(1์ฐจ): 24. 01. 16. ~ 24. 03. 21.(1.0.0 ๋ฒ„์ „ ๊ฐœ๋ฐœ ์™„๋ฃŒ)

    • [Step-1] 24. 01. 19. ~ 24. 01. 26.
    • [Step-2] 24. 01. 27. ~ 24. 02. 02.
    • [Step-3] 24. 02. 03. ~ 24. 02. 16.
    • [Step-4] 24. 02. 22. ~ 24. 03. 04.
    • [Step-5] 24. 03. 05. ~ 24. 03. 05.
    • [Step-6] 24. 03. 06. ~ 24. 03. 06.
    • [Step-7] 24. 03. 06. ~ 24. 03. 07.
    • [Step-8] 24. 03. 07. ~ 24. 03. 08.
    • [Step-9] 24. 03. 09. ~ 24. 03. 11.
    • [Step-10] 24. 03. 12. ~ 24. 03. 12.
    • [Step-11] 24. 03. 13. ~ 24. 03. 15.
    • [Step-12] 24. 03. 16. ~ 24. 03. 18.
    • [Step-13] 24. 03. 19. ~ 24. 03. 19.
    • [Step-14] 24. 03. 21. ~ 24. 03. 21.
  • ๐Ÿƒ๐Ÿป๐Ÿƒ๐Ÿปโ€โ™‚๏ธ๐Ÿ’จ ํ”„๋กœ์ ํŠธ ๊ธฐ๊ฐ„(2์ฐจ): 24. 04. 05. ~ 24. 04. 06.

    • [Step-15] 24. 04. 05. ~ 24. 04. 06.
    • [Issue-1] 24. 04. 08. ~ 24. 04. 08. (1.0.1 - ๋ฒ„๊ทธ ํ”ฝ์Šค)
  • ๐Ÿƒ๐Ÿป๐Ÿƒ๐Ÿปโ€โ™‚๏ธ๐Ÿ’จ ์œ ์ง€๋ณด์ˆ˜:

    • [refactor/filter] 24. 10. 16. (1.0.2 - ๋ฒ„๊ทธ ํ”ฝ์Šค)
    • [fix/AlertToggle] 24. 10. 17. (1.0.3 - ๋ฒ„๊ทธ ํ”ฝ์Šค)

  • ํ”„๋กœ์ ํŠธ ๊ตฌ์„ฑ์›(iOS Developer 1์ธ)
Lust3r
  • ์„œ๋น„์Šค : ์„œ์šธ์‹œ ๋ฌธํ™” ํ”„๋กœ๊ทธ๋žจ์— ๋Œ€ํ•œ ๋‹ค์–‘ํ•œ ์ •๋ณด ๋ฐ ํŽธ์˜ ๊ธฐ๋Šฅ ์ œ๊ณต

    • ํ˜„์žฌ ์šด์˜์ค‘์ธ, ์šด์˜ ์˜ˆ์ •์ธ ํ”„๋กœ๊ทธ๋žจ ์ •๋ณด ์ œ๊ณต
    • ํ”„๋กœ๊ทธ๋žจ ์ƒ์„ธ ์ •๋ณด ์ œ๊ณต
    • ํ”„๋กœ๊ทธ๋žจ ๊ฒ€์ƒ‰ ๋ฐ ํ•„ํ„ฐ๋ง ๊ธฐ๋Šฅ ์ œ๊ณต
    • ์บ˜๋ฆฐ๋”๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๋‚ ์งœ๋ณ„ ํ”„๋กœ๊ทธ๋žจ ๋ชฉ๋ก ์ œ๊ณต
    • ํ”„๋กœ๊ทธ๋žจ ์ฆ๊ฒจ์ฐพ๊ธฐ ๊ธฐ๋Šฅ ๋ฐ ํ‘ธ์‹œ ์•Œ๋ฆผ ๊ธฐ๋Šฅ ์ œ๊ณต
    • ์™ธ๋ถ€ ์•ฑ์„ ํ™œ์šฉํ•œ ํ”„๋กœ๊ทธ๋žจ ์œ„์น˜ ๊ธธ์ฐพ๊ธฐ ๊ธฐ๋Šฅ ์ œ๊ณต
    • ์›น๋ทฐ๋ฅผ ํ™œ์šฉํ•œ ํ”„๋กœ๊ทธ๋žจ ์•ˆ๋‚ด ํŽ˜์ด์ง€ ์—ฐ๊ฒฐ ๊ธฐ๋Šฅ ์ œ๊ณต
    • ์บ˜๋ฆฐ๋”์— ์ผ์ • ์ €์žฅ ๊ธฐ๋Šฅ ์ œ๊ณต
  • ๊ธฐ์ˆ  ์Šคํƒ : Swift

    • SwiftUI
    • Architecture
      • Model-View Architecture
    • Design Pattern
      • Observer(Swift Combine)
      • Singleton(CacheManager)
    • Combine(Framework)
    • SwiftData(Framework)
    • EventKit / EventKitUI(Framework)
      • (iOS 17) without prompting the user access
    • UserNotifications(Framework)
      • Local Push Notification
    • ShareLink
    • WKWebView(UIViewRepresentable)
    • AsyncImage
    • NSCache
      • NSCache<NSString, UIImage>
    • GeometryReader
    • Navigation
      • NavigationStack
      • NavigationLink
    • NWPathMonitor
    • custom sheet
    • DeepLink: External App API Use
      • Apple Map
      • Naver Map
      • Kakao Map
      • KakaoTalk
    • Debug
      • Instruments
      • Allocation, Leak, VM Tracker
      • Xcode Memory Graph
  • ํ”„๋กœ์ ํŠธ ํ›„๊ธฐ :

    • ๊ณต๊ณต๊ธฐ๊ด€ ์žฌ์ง ๊ฒฝํ—˜์„ ๋ฐ”ํƒ•์œผ๋กœ ํ•„์š”์„ฑ์„ ๋А๊ผˆ๋˜ ์„œ๋น„์Šค๋ฅผ ์ด๋ ‡๊ฒŒ ์›ํ•˜๋Š”๋Œ€๋กœ ๋งŒ๋“ค ์ˆ˜ ์žˆ์–ด ๋งŒ์กฑ์Šค๋Ÿฌ์› ๋‹ค.
    • ๊ฐ€๋ณ๊ฒŒ ๊ฐ€์ ธ๊ฐ€๋ ค ํ–ˆ๋˜ ํ”„๋กœ์ ํŠธ์˜€์œผ๋‚˜ ์ด์™• ํ•˜๊ณ  ์‹ถ์€ ํ”„๋กœ์ ํŠธ ํ•˜๋Š” ๊น€์— ๊ฐœ๋ฐœ ์—ญ๋Ÿ‰๊ณผ ๊ฒฝํ—˜์„ ๋ชจ๋‘ ์ฑ™๊ธฐ์ž๋Š” ๋ชฉํ‘œ๋ฅผ ์„ธ์› ๊ณ , ์ง„์งœ๋กœ ์ด๊ฒƒ์ €๊ฒƒ ์•ˆํ•ด๋ณธ ๊ฒƒ๋“ค๋กœ๋งŒ ๋‹ค ์ฑ„์›Œ๋„ฃ๋‹ค๋ณด๋‹ˆ ์•ฝ ๋‘ ๋‹ฌ์ด๋ผ๋Š” ์‹œ๊ฐ„์ด ํ˜๋ €๋‹ค.
    • ๊ทธ ๋•๋ถ„์— ์ด์ „ ํ”„๋กœ์ ํŠธ ๋•Œ '๋‚˜'์— ๋น„ํ•ด์„œ ์ง€๊ธˆ์˜ '๋‚˜'๋Š” ์•„๋Š” ๊ฒƒ๋„, ํ•  ์ˆ˜ ์žˆ๋Š” ๊ฒƒ๋„ ๋Š˜์—ˆ๊ธฐ์— ์ด '๋ฐœ์ „'์ด๋ผ๋Š” ์ธก๋ฉด์—์„œ ๋„ˆ๋ฌด ๋งŒ์กฑ์Šค๋Ÿฌ์šด ๊ธฐ๊ฐ„์ด์—ˆ๋‹ค.
    • ์ฒ˜์Œ ๋””์ž์ธํ–ˆ๋˜ ํ”ผ๊ทธ๋งˆ ํŒŒ์ผ์€ ์ˆ˜์‹œ๋กœ ๋ฐ”๋€Œ์—ˆ๊ณ , ์ด์ •๋„๋ฉด ์ƒ์„ธํžˆ ์ ์—ˆ๊ฒ ์ง€ ํ–ˆ๋˜ ๊ธฐ์ˆ ๋ช…์„ธ์„œ๋„ ์ˆ˜์‹œ๋กœ ๋ฐ”๋€Œ์—ˆ๊ธฐ์—(๊ธ์ •์ ์ธ ๋ฐฉํ–ฅ์œผ๋กœ) ์ด๋ž˜์„œ ์• ์ž์ผ ๋ฐฉ๋ฒ•๋ก ์ด ๋‚˜์™”๋‚˜ ์‹ถ์—ˆ๋‹ค. ๊ฐœ๋ฐœํ•˜๊ณ  ํ…Œ์ŠคํŠธํ•˜๋‹ค ๋ณด๋ฉด ๋” ์ข‹์€ ๋””์ž์ธ์ด ๋ณด์ด๊ณ , ํŽธ์˜์„ฑ์ด๋‚˜ ๊ธฐ๋Šฅ์ ์ธ ๋ฉด์—์„œ ๋” ์ œ๊ณตํ•˜๊ณ  ์‹ถ์€ ๊ฒƒ์ด ๊ณ„์†์ ์œผ๋กœ ๋‚˜์˜จ๋‹ค.
    • ๋˜ ํ•˜๋‚˜ ๋งŒ์กฑ์Šค๋Ÿฌ์šด ๊ฒฝํ—˜์€, ๊ทธ๋™์•ˆ์€ ์–ด์ฐจํ”ผ ๋ญ”๊ฐ€๋ฅผ ๋งŒ๋“ ๋‹ค๋Š” ๊ฒƒ์€ ๊ทธ๋™์•ˆ ์ž์ฃผ ํ•ด์™”๋˜ ๊ธฐ์ดˆ์ ์ธ ๊ฒƒ์ด ์•„๋‹ˆ๋ผ๋ฉด ์ฒ˜์Œ์ธ ๊ฒƒ์ด๊ธฐ ๋•Œ๋ฌธ์— ๊ทธ๊ฒƒ์„ ํ•™์Šตํ•˜๊ณ  ์ ์šฉํ•˜๋Š” ๊ณผ์ •์ด ์žˆ์—ˆ๋‹ค. ๊ทธ๋ฆฌ๊ณ  ๊ทธ ๊ณผ์ •์ด ๊ณง ํŠธ๋Ÿฌ๋ธ” ์ŠˆํŒ…์ด์—ˆ๊ณ , ๊ธฐ๋Šฅ์„ ๊ตฌํ˜„ํ•˜๋ฉด์„œ ์–ผ๋งˆ๋‚˜ ๋งŽ์€ ์ง€์‹์„ ์–ป์—ˆ๋А๋ƒ๊ฐ€ ์ค‘์ ์ด์—ˆ๋‹ค๊ณ  ์ƒ๊ฐํ•œ๋‹ค. ํ•˜์ง€๋งŒ ์ด๋ฒˆ ํ”„๋กœ์ ํŠธ๋Š” ๊ทธ๋Ÿฐ ์ธก๋ฉด์— ๋”ํ•ด์„œ ์•ฑ ์„œ๋น„์Šค์˜ ๊ทผ๋ณธ์ ์ธ ์ธก๋ฉด์„ ์ข€ ๋” ๋ณด๊ณ ์ž ํ–ˆ๋˜ ๊ฒƒ ๊ฐ™๋‹ค. ๊ฐ€๋ น ๋„คํŠธ์›Œํฌ ์ž‘์—…, ์›น ์„œ๋น„์Šค ์ž‘์—…, ์ด๋ฏธ์ง€ ์ž‘์—…, ์ „์ฒด ์„œ๋น„์Šค ๊ณผ์ •์—์„œ ๊ฐ€์ง€๋Š” ๋ฌธ์ œ์ , ๋ฆฌ์†Œ์Šค ์ ์œ ์œจ, ์•„ํ‚คํ…์ฒ˜ ๋“ฑ. ์ด์ œ ํ•œ ๋‹จ๊ณ„ ๋” ๋„“๊ฒŒ ๋ณด๋ ค ํ–ˆ๋‹ค๋Š” ์ ์—์„œ ๋†€๋ž๊ณ  ๊ธฐ๋ปค์œผ๋ฉฐ ๊ทธ ๊ณผ์ •์—์„œ ์–ป์€ ๋””๋ฒ„๊น… ๊ฒฝํ—˜์€ ๋„ˆ๋ฌด๋‚˜ ๊ฐ’์ง€๋‹ค๊ณ  ์ƒ๊ฐํ•œ๋‹ค.
    • ์ด ํ”„๋กœ์ ํŠธ์— ๋Œ€ํ•œ ์• ์ •๋„ ๋†’๊ณ , ๊ทธ ๊ณผ์ •์ด ๋„ˆ๋ฌด ํ–‰๋ณตํ–ˆ๊ธฐ ๋•Œ๋ฌธ์— ์„œ๋น„์Šค๊ฐ€ ๋„๋ฆฌ ์‚ฌ์šฉ๋˜์—ˆ์œผ๋ฉด ์ข‹๊ฒ ๊ณ  ์‚ฌ์šฉ์ž ๊ฒฝํ—˜์ด ๋งŒ์กฑ์Šค๋Ÿฌ์› ์œผ๋ฉด ํ•œ๋‹ค.(ํ”„๋กœ์ ํŠธ๋ช…์ด '๋ผ์˜จ' ์ธ ์ด์œ )

๊ตฌํ˜„ ํ™”๋ฉด

Onboarding_1 Onboarding_2 Onboarding_3
ProgramView(MainView) ProgramDetailView ProgramDetailView(Notification request)
ProgramDetailView(ShareLink) ProgramDetailView(Image Detail) ProgramDetailView(Webview-homepage)
ProgramDetailView(DeepLink-Map_1) ProgramDetailView(DeepLink-Map_2) ProgramDetailView(WebView-Map)
ProgramDetailView(add to Calendar) CalendarView SettingsView
FavoritesView_1 FavoritesView_2
SearchView SearchView_Search SearchView_Filter

ํ”„๋กœ์ ํŠธ ๊ตฌ์กฐ


STEP๋ณ„ ๊ตฌํ˜„ ๋‚ด์šฉ

Step-1: Onboarding View ๊ตฌํ˜„

  • Onboarding View ๊ตฌํ˜„
    • ์ƒํƒœ๋ฅผ ๊ฐ€์ง„ color ๋ณ€์ˆ˜๋ฅผ ๋งŒ๋“ค์–ด App ์ „์ฒด์—์„œ ์‚ฌ์šฉ
    • ์ƒํƒœ๋ฅผ ๊ฐ€์ง„ initial ๋ณ€์ˆ˜๋ฅผ ๋งŒ๋“ค์–ด App ์ฒซ ์‹คํ–‰ ๋•Œ๋งŒ Onboarding Page๋ฅผ ๋ณด์—ฌ์ฃผ๊ธฐ
    • ์ƒํƒœ๋ฅผ ๊ฐ€์ง„ region ๋ณ€์ˆ˜๋ฅผ ๋งŒ๋“ค์–ด ์„ ํƒํ•œ ์ง€์—ญ ์ €์žฅ ๋ฐ ํ™œ์šฉ
    • Picker๋ฅผ ํ™œ์šฉํ•˜์—ฌ ์ง€์—ญ์„ ์„ ํƒํ•  ์ˆ˜ ์žˆ๋„๋ก ๊ตฌํ˜„
    • TabView๋ฅผ ํ†ตํ•ด Onboarding ๊ฐ ํŽ˜์ด์ง€๋ฅผ ์ด๋™

Step-2: Program View ๊ตฌํ˜„

  • Program View ๊ตฌํ˜„
    • ๊ฐ€์šด๋ฐ ๋ณด์ด๋Š” ์ปจํ…์ธ  ๊ฐ•์กฐ(-> TabView๋กœ ๋ณ€๊ฒฝ๋˜์–ด ์ž๋™ ๊ฐ•์กฐ)
    • ์–‘์ชฝ ์ปจํ…์ธ  ํˆฌ๋ช…๋„ ์กฐ์ ˆ(-> TabView๋กœ ๋ณ€๊ฒฝ๋˜์–ด ์กฐ์ ˆ ํ•„์š” ์—†์Œ)
    • NavigationBar ๊ตฌ์„ฑ
    • TabView ์‚ฌ์šฉ
  • API Model ๊ตฌ์„ฑ
    • ๋„คํŠธ์›Œํฌ ๋ถ„๋ฆฌ
    • ์˜คํ”„๋ผ์ธ ํ™˜๊ฒฝ ๊ฐ์ง€ ๋ฐ ์•ˆ๋‚ด
    • ๋ชฉ์—…, ์‹ค ๋ฐ์ดํ„ฐ ํ…Œ์ŠคํŠธ

Step-3: Search View ๊ตฌํ˜„

  • Search View ๊ตฌํ˜„

    • ProgramView์—์„œ Search Icon์„ ํ„ฐ์น˜ํ•˜๋ฉด SearchView๋กœ ์ด๋™(NavigationLink ํ™œ์šฉ)
    • ํ”„๋กœ๊ทธ๋žจ ์ œ๋ชฉ์€ ๊ฒ€์ƒ‰ํ•˜์—ฌ ํ•„ํ„ฐ๋ง
    • ํ•„ํ„ฐ๋ง์ด ๋˜๋ฉด ์ฒซ ๊ฒฐ๊ณผ๋กœ ์œ„์น˜ ์ด๋™
  • Filter View ๊ตฌํ˜„

    • ์นดํ…Œ๊ณ ๋ฆฌ, ์ง€์—ญ์€ FilterView๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์ ์šฉ
    • ์•„๋ž˜์ชฝ์— sheet๋กœ ์ž‘๊ฒŒ ๋„์›Œ ํ˜„์žฌ context ์œ ์ง€
    • ํ•„ํ„ฐ ์ดˆ๊ธฐํ™”, ๋‹ซ๊ธฐ ๋ฒ„ํŠผ ๊ธฐ๋Šฅ ์ œ๊ณต
    • @State, @Binding์„ ํ†ตํ•œ ํ•„ํ„ฐ ์„ ํƒ๊ฐ’ ๋ฐ˜์˜

Step-4: Program Detail View ๊ตฌํ˜„

  • Program Detail View ๊ตฌํ˜„

    • ํฌ์Šคํ„ฐ๋ฅผ ๋ฐฐ๊ฒฝ์œผ๋กœ ์ž์„ธํ•œ ์ •๋ณด๋ฅผ ์Šคํฌ๋กคํ•˜๋ฉฐ ๋ณผ ์ˆ˜ ์žˆ๋„๋ก ๊ตฌํ˜„
    • ํ”„๋กœ๊ทธ๋žจ๋ช… ์šฐ์ธก์— ์ฆ๊ฒจ์ฐพ๊ธฐ์™€ ์•Œ๋ฆผ ๋ฒ„ํŠผ ์ถ”๊ฐ€
    • ๊ด€๋ จ ๋งํฌ๋กœ ์ด๋™ํ•˜๋Š” ๋ฒ„ํŠผ ์ถ”๊ฐ€
    • ํ”„๋กœ๊ทธ๋žจ ์ง„ํ–‰ ์žฅ์†Œ๋ฅผ ์ง€๋„๋กœ ์ œ๊ณต
  • ๋ถ€๊ฐ€ ๊ธฐ๋Šฅ ๊ตฌํ˜„

    • ์ฆ๊ฒจ์ฐพ๊ธฐ ๋ฒ„ํŠผ ์ฒดํฌ ์—ฌ๋ถ€์— ๋”ฐ๋ฅธ fill ์ƒํƒœ ๋ณ€๊ฒฝ
    • ์•Œ๋ฆผ ๋ฐ›๊ธฐ ๋ฒ„ํŠผ ์ฒดํฌ ์—ฌ๋ถ€์— ๋”ฐ๋ฅธ fill ์ƒํƒœ ๋ณ€๊ฒฝ
    • ๊ณต์œ ์‹œํŠธ ๊ธฐ๋Šฅ ๊ตฌํ˜„

Step-5: Project Maintenance

  • DTO

    • ์‚ฌ์šฉํ•˜์ง€ ์•Š๋Š” ํ•ญ๋ชฉ ์ œ๊ฑฐ
  • ContentView

    • fullScreenCover ๋Œ€์‹  isFirstLaunching ๊ธฐ์ค€์œผ๋กœ ๋ถ„๊ธฐ ์ฒ˜๋ฆฌ
    • @AppStorage ๋“ฑ์˜ ํ”„๋กœํผํ‹ฐ ์œ„์น˜ ๊ณ ๋ ค
  • ProgramView

    • ์„ค์ •, ๊ฒ€์ƒ‰ ์œ„์น˜ ๊ณ ๋ ค
    • networkManager ์œ„์น˜ ๊ณ ๋ ค
  • ProgramCardView

    • shadow, opacity ๊ฐ’ ์กฐ์ •
    • ์‚ฌ์šฉํ•˜์ง€ ์•Š๋Š” ํ”„๋กœํผํ‹ฐ ์ œ๊ฑฐ
  • SearchView

    • List ๋ณด์ผ๋Ÿฌ ํ”Œ๋ ˆ์ดํŠธ ์ฝ”๋“œ ์ œ๊ฑฐํ•˜์—ฌ ๋กœ์ง ๊ฐ„๊ฒฐํ™”
  • SearchCardView

    • ์ด๋ฏธ์ง€์™€ ํ…์ŠคํŠธ ์œ„์น˜ ์กฐ์ •
    • content๋ฅผ ์ „๋‹ฌํ•ด์„œ ๋‚ด๋ถ€์—์„œ ๋™์ž‘ํ•˜๋Š” ๊ฒƒ ๊ณ ๋ ค
      • ๋‚ด๋ถ€ ๋™์ž‘์— ๋Œ€ํ•œ ์บก์Аํ™” ๋ชฉ์  + ๋ฉ”๋ชจ๋ฆฌ Allocations์ƒ 10MiB ์ฐจ์ด ๋‚˜๋Š” ๊ฒƒ ํ™•์ธ
  • ProgramDetailView

    • ํ™ˆํŽ˜์ด์ง€ ์ด๋™ ์‹œ ToolBar ์ œ๊ณต
      • .toolbar ๊ด€๋ จ ์ฝ”๋“œ๊ฐ€ ์ค‘๋ณต๋˜์–ด WebView ๋‚ด์— CustomWebView๋ฅผ ๊ตฌํ˜„
      • ๋ณด์ผ๋Ÿฌ ํ”Œ๋ ˆ์ดํŠธ ์ฝ”๋“œ ์ œ๊ฑฐ ๋ฐ ์‚ฌ์šฉํ•˜๋Š” View ์ฝ”๋“œ ๊ฐ„๊ฒฐํ™”
      • ํ•„์š”ํ•œ WebView๋Š” ์™ธ๋ถ€์—์„œ ์ฃผ์ž…

Step-6: Image Caching

  • ์ด๋ฏธ์ง€ ์บ์‹ฑ
    • CacheManager ๊ตฌํ˜„
    • CacheManager๋ฅผ ํ™œ์šฉํ•˜์—ฌ ์ด๋ฏธ์ง€ ํ‘œํ˜„

Step-7: WebView Memory Optimization

  • WebView
    • WebView ์‚ฌ์šฉ ์‹œ ๋ฉ”๋ชจ๋ฆฌ๊ฐ€ ๊ธ‰์ฆํ•˜์—ฌ ์ค„์ง€ ์•Š๋Š” ์ด์Šˆ๋ฅผ ํ•ด๊ฒฐํ•˜์—ฌ ๋ฉ”๋ชจ๋ฆฌ ์ตœ์ ํ™”

Step-8: Settings View

  • Settings View ๊ตฌํ˜„
    • ์ €์ž‘๊ถŒ ํ‘œ์‹œ
    • ์•ฑ ํ…Œ๋งˆ ์„ค์ •
    • ๊ด€์‹ฌ ์ง€์—ญ ์„ค์ •

Step-9: SwiftData

  • SwiftData ์‚ฌ์šฉ
    • SwiftData์˜ Model ๊ตฌ์ƒ
    • SwiftData๋ฅผ ํ™œ์šฉํ•˜์—ฌ Favorites ํ•ญ๋ชฉ ๊ด€๋ฆฌ

Step-10: Favorites View

  • Favorites View ๊ตฌํ˜„
    • SwiftData์˜ FavoritePrograms ๋ฐ์ดํ„ฐ๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ View ๊ตฌ์„ฑ
    • toolbar ๋ฒ„ํŠผ์„ ํ™œ์šฉํ•œ ์ œ๊ฑฐ ๊ธฐ๋Šฅ ๊ตฌํ˜„

Step-11: User Notifications

  • User Notifications ๊ตฌํ˜„
    • Notification ๊ด€๋ฆฌ ์—ญํ• ์„ ๋งก์„ ๊ฐ์ฒด ๊ตฌํ˜„
    • Notification ์‚ฌ์šฉ ๊ถŒํ•œ ์š”์ฒญ
    • User Notification์„ ํ™œ์šฉํ•˜์—ฌ ์ฆ๊ฒจ์ฐพ๊ธฐ ํ”„๋กœ๊ทธ๋žจ ์‹œ์ž‘์ผ ์•Œ๋ฆผ ๊ตฌํ˜„
    • Date์˜ ํ•œ๊ตญ์‹œ๊ฐ ์„ค์ •
    • Notification ์ค‘๋ณต ๋“ฑ๋ก ๋ฐฉ์ง€
      • ์‹œ์ž‘์ผ ๊ธฐ์ค€ ์•Œ๋ฆผ ๋“ฑ๋ก ๋ถ„๊ธฐ์ฒ˜๋ฆฌ(์ด๋ฏธ ์‹œ์ž‘ํ•œ ํ”„๋กœ๊ทธ๋žจ์€ ์•Œ๋ฆผ ๋“ฑ๋ก ์•ˆํ•จ)
      • ์ด๋ฏธ identifier๊ฐ€ ๋“ฑ๋ก๋˜์–ด ์žˆ๋Š”์ง€ ํ™•์ธ
    • ์ฆ๊ฒจ์ฐพ๊ธฐ ํ•ด์ œ ์‹œ Notification ์ œ๊ฑฐ ๊ธฐ๋Šฅ ๊ตฌํ˜„
    • SettingsView์—์„œ ์•Œ๋ฆผ ํ† ๊ธ€ ๊ตฌํ˜„ ๋ฐ ์„ค์ • ์•ฑ ์•Œ๋ฆผ ์ƒํƒœ์™€ ์—ฐ๊ฒฐ

Step-12: Project Maintenance

  • WebView

    • ์ฝ”๋“œ ์ปจ๋ฒค์…˜ ์ค€์ˆ˜
  • SearchView

    • ๋ ˆ์ด์•„์›ƒ ์žฌ์กฐ์ •
  • ProgramDetailView

    • ๋ฐฐ๊ฒฝ ์ด๋ฏธ์ง€ ๋Š˜์–ด๋‚˜๋Š” ๋ฌธ์ œ ํ•ด๊ฒฐ
    • isFavorite ๋กœ์ง ๊ฐœ์„ 
  • SettingsView

    • ํ…Œ๋งˆ์— ํ˜„์žฌ ์ƒ‰์ƒ ํ‘œ์‹œ
    • ์ง€์—ญ Picker ์ƒ‰์ƒ ๋ฐ”์ธ๋”ฉ
  • DTO

    • NetworkManager์—์„œ ๋ฐ์ดํ„ฐ ๋งคํ•‘ ์‹œ ํƒ€์ž„์Šคํƒฌํ”„ ์ œ๊ฑฐ
    • SearchCardView, DescriptionView, NotificationManager์—์„œ ํ•ด๋‹น ์ž‘์—… ์ฝ”๋“œ ์ œ๊ฑฐ
  • ํ”„๋กœ์ ํŠธ ํด๋”๋ง

    • Model ํด๋” ๋‚ด ํŒŒ์ผ ๊ตฌ๋ถ„
  • ํ”„๋กœ์ ํŠธ ๊ด€๋ฆฌ

    • ์ „์ฒด ํŒŒ์ผ ์ฝ”๋“œ ์ปจ๋ฒค์…˜ ์ค€์ˆ˜
  • NetworkManager

    • API ๋ฐ์ดํ„ฐ ์ œ๊ณต์ธก ์ •๋ ฌ ์ด์Šˆ๋กœ ์ธํ•œ ํ˜ธ์ถœ ๋กœ์ง ์ˆ˜์ •(๊ธฐ์กด 1000๊ฐœ -> ์ „์ฒด ๋ฐ์ดํ„ฐ)

Step-13: Calendar View

  • Calendar View ๊ตฌํ˜„
    • CalendarView์— Calendar ๊ตฌํ˜„
    • Calendar์—์„œ ๋‚ ์งœ ์„ ํƒ ์‹œ ํ•˜๋‹จ์— ๋ชฉ๋ก์„ ๋ณด์—ฌ์ฃผ๋„๋ก ๊ตฌํ˜„
    • '์˜ค๋Š˜(Today)' ๋ฒ„ํŠผ์„ ํ†ตํ•ด ๋‹ค๋ฅธ ๋‚ ์งœ์ผ ๋•Œ ์˜ค๋Š˜ ๋‚ ์งœ๋กœ ๋ฐ”๋กœ ๋Œ์•„๊ฐ€๋Š” ๊ธฐ๋Šฅ ๊ตฌํ˜„
    • ํ•ญ๋ชฉ์„ ํ„ฐ์น˜ํ•˜๋ฉด ์ƒ์„ธ ํŽ˜์ด์ง€๋กœ ๋„˜์–ด๊ฐ€๋„๋ก ๊ตฌํ˜„
    • ํ•ญ๋ชฉ ์ขŒ์ธก์— ์ฆ๊ฒจ์ฐพ๊ธฐ ์—ฌ๋ถ€ ํ‘œ๊ธฐ
    • EventKit์„ ํ™œ์šฉํ•˜์—ฌ ProgramDetailView์˜ ์‹œ์ž‘์ผ, ์ข…๋ฃŒ์ผ ์˜†์— ์บ˜๋ฆฐ๋”์— ๋“ฑ๋กํ•˜๊ธฐ ๊ธฐ๋Šฅ ๊ตฌํ˜„

Step-14: Additional Feature

  • ๋ถ€๊ฐ€ ๊ธฐ๋Šฅ

    • ์นด์นด์˜คํ†ก ํ”Œ๋Ÿฌ์Šค์นœ๊ตฌ ์—ฐ๊ฒฐ๋กœ ๋ฌธ์˜ ์ฐฝ๊ตฌ ๊ฐœ์„ค
    • ์•ฑ ๋ฒ„์ „ ํ™•์ธ ๊ธฐ๋Šฅ ๊ตฌํ˜„
    • ์•ฑ ์•„์ด์ฝ˜ ์ ์šฉ
    • ์•ฑ ์„ค์น˜ ์ด๋ฆ„, ํƒ€๊ฒŸ ๋ณ€๊ฒฝ
    • (ProgramDetailView) ๋ฐฐ๊ฒฝ ์ด๋ฏธ์ง€ ํด๋ฆญํ•˜๋ฉด ์›๋ณธ ์ด๋ฏธ์ง€ ํ‘œํ˜„ ๊ธฐ๋Šฅ ์ œ๊ณต
  • ํ”„๋กœ์ ํŠธ ๊ฐœ์„ 

    • (SettingsView) ์„ ํ˜ธ์ง€์—ญ ํ…์ŠคํŠธ ์ƒ‰์ƒ์ด ๋‹คํฌ๋ชจ๋“œ์—์„œ ๋ณ€๊ฒฝ๋˜๋Š” ํ˜„์ƒ ํ•ด๊ฒฐ
    • (SettingsView) ์•Œ๋ฆผ ํ† ๊ธ€ Alert ๋‹ซ๊ธฐ ๋กœ์ง ๊ฐœ์„ 
    • (CopyRightView) ์ด์šฉ์•ฝ๊ด€ ๋งํฌ๋ฅผ ์˜ฌ๋ฆฌ๋Š” ๊ฒƒ์ด ์•„๋‹ˆ๋ผ ์›น๋ทฐ๋กœ ์—ฐ๊ฒฐ
    • (FilterView) ๋‹ซ๊ธฐ ๋ฒ„ํŠผ์˜ ๋‹คํฌ๋ชจ๋“œ ์‹œ์ธ์„ฑ ๊ฐœ์„ 
    • (ProgramDetailView) ์ฆ๊ฒจ์ฐพ๊ธฐ, ๊ณต์œ ํ•˜๊ธฐ ๋ฒ„ํŠผ ์ƒ‰์ƒ ์‹œ์ธ์„ฑ ๊ฐœ์„ 
    • (OnboardingView) ํ”„๋กœ๊ทธ๋žจ ์ด๋ฆ„ ๋ณ€๊ฒฝ
    • (ProgramView) ์ƒˆ๋กœ๊ณ ์นจ ์‹œ ๋ฐ์ดํ„ฐ๊ฐ€ ๋‚˜์˜ค์ง€ ์•Š๋Š” ๋ฌธ์ œ ํ•ด๊ฒฐ

Step-15: Reflect 1st feedback

- TestFlight ํ†ตํ•œ ์ด์šฉ์ž ํ”ผ๋“œ๋ฐฑ ๋ฐ App Store ์‹ฌ์‚ฌ ๋‚ด์šฉ ๋ณด์™„

  • Onboarding View

    • ์‚ฌ์šฉ๋ฒ• ์ถ”๊ฐ€ ์•ˆ๋‚ด
    • ์˜จ๋ณด๋”ฉ ํ™”๋ฉด์—์„œ ์šฐ์ธก์œผ๋กœ pan gesture์‹œ ์ขŒ์ธก์— ํฐ ํ™”๋ฉด์ด ๋‚˜์˜ค๋Š” ํ˜„์ƒ ํ•ด๊ฒฐ
  • Calendar View

    • ์ฆ๊ฒจ์ฐพ๊ธฐํ•œ ํ•ญ๋ชฉ์„ ๋ฆฌ์ŠคํŠธ ์ƒ๋‹จ์œผ๋กœ ์˜ฌ๋ผ๊ฐ€๋„๋ก ๊ตฌํ˜„
  • Program View

    • ์ƒˆ๋กœ๊ณ ์นจ ์‹œ ์ธ๋””์ผ€์ดํ„ฐ ํ‘œ์‹œ๋˜๋„๋ก ๊ตฌํ˜„
  • Favorites View

    • ํŽธ์ง‘ ๋ชจ๋“œ ์‹œ ProgramDetailView ์ด๋™์ด ์•ˆ๋˜๋„๋ก ๊ตฌํ˜„
  • ProgramDetail View

    • ์œ„์น˜ ํ…์ŠคํŠธ๋ฅผ ๋ฒ„ํŠผ์œผ๋กœ ๋ณ€๊ฒฝํ•˜์—ฌ ๋ˆ„๋ฅด๋ฉด ์•ก์…˜์‹œํŠธ๋กœ ํ•ญ๋ชฉ์„ ์„ ํƒํ•  ์ˆ˜ ์žˆ๋„๋ก ๋ณ€๊ฒฝ
    • ์• ํ”Œ ์ง€๋„ ์—ฐ๊ฒฐ
    • ๋”ฅ๋งํฌ ์—ฐ๊ฒฐ ์‹œ ๊ธธ์ฐพ๊ธฐ๊ฐ€ ์•„๋‹ˆ๋ผ ์œ„์น˜ ์ •๋ณด๋ฅผ ์ œ๊ณต
    • DescriptionView ์ƒ๋‹จ ์—ฌ๋ฐฑ ์ถ”๊ฐ€
  • Feature

    • ํƒญ๋ฐ”๋ฅผ ํ„ฐ์น˜ํ•˜์—ฌ ์ฒ˜์Œ ํ™”๋ฉด์œผ๋กœ ๊ฐ€๋„๋ก ๊ธฐ๋Šฅ ๊ตฌํ˜„(Tap to Root)

์—…๋ฐ์ดํŠธ ๋‚ด์—ญ

1.0.1

  • Issue-1: NavigationTitle's wrong position
    • ์„ค์ • ํ™”๋ฉด์—์„œ ์•ฑ ์ƒ‰์ƒ ํ˜น์€ ์ง€์—ญ ์„ ํƒ ์‹œ ํ”„๋กœ๊ทธ๋žจ ํƒญ ๋‚ด๋น„๊ฒŒ์ด์…˜ ์ œ๋ชฉ์ด ํ•˜๋‹จ์œผ๋กœ ๋‚ด๋ ค์˜ค๋Š” ํ˜„์ƒ ํ•ด๊ฒฐ

1.0.2

  • refactor/filter: ํ•„ํ„ฐ ํฌ๋ž˜์‹œ ์ด์Šˆ ํ•ด๊ฒฐ
    • ๊ฒ€์ƒ‰ ํ™”๋ฉด์—์„œ ์นดํ…Œ๊ณ ๋ฆฌ ํ˜น์€ ์ง€์—ญ์„ ์„ ํƒํ–ˆ์„ ๋•Œ ํ•ด๋‹นํ•˜๋Š” ํ”„๋กœ๊ทธ๋žจ์ด ์—†๋‹ค๋ฉด ์•ฑ์ด ์ถฉ๋Œ๋‚˜๋Š” ํ˜„์ƒ ํ•ด๊ฒฐ

1.0.3

  • fix/AlertToggle: ์•Œ๋ฆผ ํ† ๊ธ€ ๋น„์ •์ƒ์  ๋™์ž‘ ํ•ด๊ฒฐ
    • ์„ค์ • ํ™”๋ฉด์—์„œ ์•Œ๋ฆผ ํ† ๊ธ€์ด ์•ฑ ์•Œ๋ฆผ ๊ถŒํ•œ๊ณผ ๋™๊ธฐํ™”๋˜์ง€ ์•Š๊ณ  ์ž์ฒด์ ์œผ๋กœ On/Off ํ† ๊ธ€๋˜๋Š” ํ˜„์ƒ ํ•ด๊ฒฐ
    • ์ดˆ๊ธฐ ์˜๋„์™€ ๊ฐ™์ด ์•ฑ ์•Œ๋ฆผ ๊ถŒํ•œ๊ณผ ๋™๊ธฐํ™” ๋ฐ ๊ธฐ๋ณธ ์„ค์ • ์•ฑ์œผ๋กœ ์—ฐ๊ฒฐ๋˜๋Š” Alert ํ‘œํ˜„

ํŠธ๋Ÿฌ๋ธ” ์ŠˆํŒ…

1. ์•ฑ ๋ฐ์ดํ„ฐ ๊ตฌ์„ฑ์— ๋Œ€ํ•œ ๊ณ ๋ฏผ

๊ณ ๋ฏผํ•œ ์  :

  • Onboarding Page๋ฅผ ๋งŒ๋“œ๋Š” ๋ฐ ์žˆ์–ด์„œ, View๋Š” ๊ฐ ์ปดํฌ๋„ŒํŠธ๋ฅผ ๋งŒ๋“ค๊ณ  ๋ ˆ์ด์•„์›ƒ์„ ์žก์œผ๋ฉด ๋˜์ง€๋งŒ, ๊ทธ ๊ณผ์ •์—์„œ ์‚ฌ์šฉํ•  ๋ฐ์ดํ„ฐ, ์ €์žฅํ•  ๋ฐ์ดํ„ฐ๋Š” ์–ด๋–ป๊ฒŒ ๊ด€๋ฆฌํ•˜๊ณ  ์–ด๋–ป๊ฒŒ ๋ถ„๋ฆฌํ•˜๋ฉด ์ข‹์„์ง€ ๊ณ ๋ฏผํ•จ

๊ณผ์ • ๋ฐ ํ•ด๊ฒฐ :

  • WWDC20 Data Essentials in SwiftUI ์˜์ƒ์ด ๋งŽ์ด ์ฐธ๊ณ ๊ฐ€ ๋˜์—ˆ๋‹ค. ํ•ด๋‹น ์˜์ƒ์—์„œ, ๋ฐ์ดํ„ฐ๋ฅผ ์‚ฌ์šฉํ•  ๋•Œ ๊ณ ๋ คํ•ด์•ผ ํ•  3๊ฐ€์ง€ ์งˆ๋ฌธ์„ ์•Œ๋ ค์คฌ๋Š”๋ฐ, ๋‹ค์Œ๊ณผ ๊ฐ™๋‹ค.

    1) View๊ฐ€ ์ž‘์—…์„ ์ˆ˜ํ–‰ํ•˜๋ ค๋ฉด ์–ด๋–ค ๋ฐ์ดํ„ฐ๊ฐ€ ํ•„์š”ํ•œ๊ฐ€? 2) View๊ฐ€ ํ•ด๋‹น ๋ฐ์ดํ„ฐ๋ฅผ ์–ด๋–ป๊ฒŒ ์กฐ์ž‘ํ•˜๋Š”๊ฐ€? 3) ๋ฐ์ดํ„ฐ๋Š” ์–ด๋””์—์„œ ์˜ค๋Š”๊ฐ€?(Source of Truth)

  • ์ด์™ธ์—๋„ property wrapper์˜ ์‚ฌ์šฉ ํ™˜๊ฒฝ์— ๋Œ€ํ•œ ์„ค๋ช… ๋•์— ๊ณ ๋ฏผ์„ ํ•ด๊ฒฐํ•  ์ˆ˜ ์žˆ์—ˆ๋‹ค. ๊ฐ€๋ น Step 1์˜ ์ƒํƒœ๋ฅผ ๊ฐ€์ง„ ๋ฐ์ดํ„ฐ๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์ด ๋ณด์•˜๋‹ค.

    1. Color
    • ์•ฑ์˜ ๋ฉ”์ธ ์ƒ‰์ƒ์ด ๋ฌด์—‡์ธ์ง€ ์ •๋ณด๋ฅผ ๋‹ด๊ณ  ์žˆ๋Š” ๋ฐ์ดํ„ฐ๊ฐ€ ํ•„์š”
    • ์„ค์ •์—์„œ ๋ฉ”์ธ ์ƒ‰์ƒ์„ ๋ณ€๊ฒฝํ•  ์ˆ˜ ์žˆ๊ฒŒ ์ ‘๊ทผํ•  ์ˆ˜ ์žˆ์–ด์•ผ ํ•จ. ๋‚˜๋จธ์ง€ View๋Š” ๋‹จ์ˆœ ๋ฐ์ดํ„ฐ ์ฝ๊ธฐ
    • App ๋‚ด์—์„œ ๊ธ€๋กœ๋ฒŒํ•˜๊ฒŒ ์‚ฌ์šฉ๋˜๊ณ , ์ž์ฃผ ๋ฐ”๋€Œ์ง€ ์•Š๋Š” ์ž‘์€ ์„ค์ • ๊ฐ’์ด๊ธฐ์— AppStorage๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์ €์žฅํ•˜๊ณ  binding์„ ํ†ตํ•ด ํ™œ์šฉ

    1. Initial
    • App์„ ์„ค์น˜ํ•œ ์ดํ›„ ์ฒ˜์Œ ์‹คํ–‰ํ•œ ๊ฒƒ์ธ์ง€ ์ •๋ณด๋ฅผ ๋‹ด๊ณ  ์žˆ๋Š” ๋ฐ์ดํ„ฐ๊ฐ€ ํ•„์š”
    • ContentView์—์„œ๋Š” OnboardingTabView๋ฅผ ๋ณด์—ฌ์ฃผ๋Š” ์—ฌ๋ถ€์— ์‚ฌ์šฉํ•˜๊ธฐ ์œ„ํ•ด ๋‹จ์ˆœํžˆ ๊ฐ’์„ ์ฝ๋Š” ๊ฒƒ๋งŒ ์ˆ˜ํ–‰, OnboardingPage์˜ ๋งˆ์ง€๋ง‰์ธ ThirdView์—์„œ๋Š” ํ•ด๋‹น ๊ฐ’์„ ๋ณ€๊ฒฝํ•˜๊ธฐ ์œ„ํ•œ ์ ‘๊ทผ์ด ํ•„์š”
    • ์ด ๋ฐ์ดํ„ฐ๋Š” App ์ตœ์ดˆ ์‹คํ–‰ ์ดํ›„์—๋Š” ๋ณ€๊ฒฝ๋  ์ผ์ด ์—†์–ด State๋‚˜ Observable๋กœ ์ถ”์ ํ•  ํ•„์š”๊ฐ€ ์—†๊ณ , ์ž‘์€ Bool ๋ฐ์ดํ„ฐ์ด๊ธฐ ๋•Œ๋ฌธ์— AppStorage๋ฅผ ์‚ฌ์šฉํ•˜๊ณ , binding์„ ํ†ตํ•ด ํ™œ์šฉ

    1. Region
    • ์‚ฌ์šฉ์ž๊ฐ€ ์„ ํƒํ•œ ์ง€์—ญ์— ๋Œ€ํ•œ ์ •๋ณด๋ฅผ ๋‹ด๊ณ  ์žˆ๋Š” ๋ฐ์ดํ„ฐ๊ฐ€ ํ•„์š”
    • OnboardingSecondView์™€ ์„ค์ •์—์„œ ๊ฐ’์„ ๋ฐ”๊พธ๊ธฐ ์œ„ํ•œ ์ ‘๊ทผ์ด ํ•„์š”, ๋ฉ”์ธ ๋กœ์ง์—์„œ๋Š” ํ•ด๋‹น ๊ฐ’์„ ๋‹จ์ˆœํžˆ ์ฐธ๊ณ ํ•˜์—ฌ ์ž‘์—… ์ˆ˜ํ–‰
    • ์ฒ˜์Œ์—๋Š” ์ถ”์ ์„ ์œ„ํ•ด ObservableObject๋ฅผ ๊ณ ๋ คํ–ˆ์œผ๋‚˜, ์ด ์—ญ์‹œ ํฌ๊ธฐ๊ฐ€ ์ž‘์€ ์„ค์ • ๊ฐ’์ด๊ธฐ ๋•Œ๋ฌธ์— AppStorage๋ฅผ ์‚ฌ์šฉํ•˜๊ณ  ์ƒ์œ„ ๋ทฐ์—์„œ ํ•˜์œ„ ๋ทฐ๋กœ binding์„ ํ†ตํ•ด ํ™œ์šฉ

2. AppStorage์— ์ €์žฅํ•  ํƒ€์ž…

๊ณ ๋ฏผํ•œ ์  :

  • Initial์€ Bool๋กœ ๊ฐ’์„ ์ €์žฅํ•˜๋ฉด ๋˜๋Š”๋ฐ, Color, Region์€ Enumํƒ€์ž…์œผ๋กœ ๋งŒ๋“ค์—ˆ๊ธฐ์— String์œผ๋กœ ์ €์žฅํ•˜๊ณ  ๊ทธ๊ฑธ ๋ถˆ๋Ÿฌ์™€์„œ ๋งค์น˜ํ•˜๋Š” ๋ฐฉ๋ฒ•์ด ๋งž๋‚˜ ๊ณ ๋ฏผํ•จ

๊ณผ์ • ๋ฐ ํ•ด๊ฒฐ :

  • Enum์€ rawPresentable์„ ์ค€์ˆ˜ํ•˜๊ณ , @AppStorage๋Š” ๊ทธ ํƒ€์ž…์„ ์ง€์›ํ•˜๊ธฐ ๋•Œ๋ฌธ์— ๊ฐ€๋Šฅํ•˜๋‹ค. Color๋Š” ๊ฐ case๊ฐ€ asset์˜ ์ด๋ฆ„์„ rawValue๋กœ ๊ฐ–๊ณ  ์žˆ๊ณ , Region๋„ ๊ฐ ์ผ€์ด์Šค๋ช…์„ ๊ทธ๋Œ€๋กœ rawValue๋กœ ๊ฐ–๊ณ  ์žˆ๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค.

3. ATS policy ๋ฌธ์ œ

๊ณ ๋ฏผํ•œ ์  :

  • API ์š”์ฒญ์ด http๋กœ ์ œํ•œ๋˜์–ด ์žˆ๋Š”๋ฐ ๋ณด์•ˆ์˜ ๋ฌธ์ œ๋กœ https๋ฅผ ์จ์•ผ ํ•˜๋Š” ์ƒํ™ฉ(since it does not conform to ATS policy ์˜ค๋ฅ˜ ๋ฐœ์ƒ)

๊ณผ์ • ๋ฐ ํ•ด๊ฒฐ :

  • Info.plist ํŒŒ์ผ ์กฐ์ •์„ ํ†ตํ•ด ํ•ด๊ฒฐ์ด ๊ฐ€๋Šฅ
    • App Transport Security Settings Key ์‚ฌ์šฉ
    • Exception Domains์— ์‚ฌ์šฉํ•  ๋„๋ฉ”์ธ ๊ธฐ์ž…
    • ๋งŒ์•ฝ http์ธ ์ „์ฒด ๋„๋ฉ”์ธ์„ ํ—ˆ์šฉํ•˜๋Š” ๊ฒฝ์šฐ์—๋Š” NSExceptionDomains ๋Œ€์‹  NSAllowsArbitraryLoads ์‚ฌ์šฉ

4. CodingKeys ์˜ค๋ฅ˜

๊ณ ๋ฏผํ•œ ์  :

  • DTO๋ฅผ API ๋ฐ์ดํ„ฐ ํƒ€์ž…์— ๋งž๊ฒŒ ๊ตฌ์„ฑํ–ˆ๋Š”๋ฐ ๋””์ฝ”๋”ฉ ์˜ค๋ฅ˜ ๋ฐœ์ƒ

๊ณผ์ • ๋ฐ ํ•ด๊ฒฐ :

  • CodingKeys ๋งค์นญ์˜ ๋ฌธ์ œ์˜€์Œ. ์ผ๋ถ€ CodingKeys๊ฐ€ Codingkeys๋กœ ์˜ค๊ธฐ์ž… ๋˜์–ด์žˆ์—ˆ๋‹ค.

5. dataTaskPublisher์˜ ์ค‘๋ณต ์‹คํ–‰ ๋ฐฉ์ง€

๊ณ ๋ฏผํ•œ ์  :

  • URLSession์˜ dataTaskPublisher๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ API ํ˜ธ์ถœ์„ ํ•˜๋Š”๋ฐ, ์ฒ˜์Œ ํ•œ ๋ฒˆ๋งŒ ๋ถ€๋ฅด๋ฉด ์ƒ๊ด€์ด ์—†์œผ๋‚˜ ์ƒˆ๋กœ๊ณ ์นจ ํ˜น์€ ์˜ค๋ฅ˜๋กœ ์ธํ•ด ์ค‘๋ณต์œผ๋กœ ํ˜ธ์ถœ์ด ๋˜๋ฉด ๋ฆฌ์†Œ์Šค ๋‚ญ๋น„๊ฐ€ ์žˆ์„ ๊ฒƒ์œผ๋กœ ํŒ๋‹จ๋˜์–ด ๋ฐฉ๋ฒ•์„ ๊ณ ๋ฏผ

๊ณผ์ • ๋ฐ ํ•ด๊ฒฐ :

  • ํ•ด๋‹น Publisher๋Š” AnyCancellable ํƒ€์ž…์ด๊ธฐ์— ํ•ด๋‹น ๋น„๋™๊ธฐ ์ž‘์—…์„ ์ˆ˜ํ–‰ํ•  ๋•Œ NetworkManager์˜ programCancellable ๋ณ€์ˆ˜์— ์ €์žฅ
  • API ์š”์ฒญ ๋ฉ”์„œ๋“œ๊ฐ€ ํ˜ธ์ถœ๋˜๋ฉด programCancellable์„ cancel()ํ•˜๊ณ  ์ƒˆ๋กœ dataTaskPublisher๋ฅผ ๋งŒ๋“ค์–ด ๋น„๋™๊ธฐ ์ž‘์—… ์ˆ˜ํ–‰
  • ์ค‘๋ณต ํ˜ธ์ถœ์ด ์•„๋‹ˆ๋ผ๋ฉด ์ž‘์—… ์™„๋ฃŒ ํ›„ ํ• ๋‹น ํ•ด์ œ

6. AsyncImage ์˜คํ”„๋ผ์ธ ๋Œ€์‘

๊ณ ๋ฏผํ•œ ์  :

  • AsyncImage์—์„œ ์ด๋ฏธ์ง€๋ฅผ ๋ถˆ๋Ÿฌ์˜ค๋Š” ๋™์•ˆ ๋„คํŠธ์›Œํฌ๊ฐ€ ๋Š๊ธฐ๊ฒŒ ๋˜๋ฉด placeholder๋กœ ์ง€์ •ํ•œ ProgressView๋งŒ ๋ณด์ด๊ฒŒ ๋จ
  • ๋„คํŠธ์›Œํฌ๊ฐ€ ๋‹ค์‹œ ์—ฐ๊ฒฐ๋˜๋ฉด ๋ณด์ด๋„๋ก ํ•˜๊ณ  ์‹ถ์€๋ฐ ๋‹ค๋ฅธ TabView๋กœ ํŽ˜์ด์ง•ํ•˜๋ฉด ํ•ด๋‹น ๊ฐ์ฒด๋Š” ๋‹ค์‹œ ๊ทธ๋ ค์ง€๊ธฐ ๋•Œ๋ฌธ์— ์ด๋ฏธ์ง€๊ฐ€ ๋ณด์ด๋‚˜ ์ด๋ฏธ ๋กœ๋”ฉ์˜ค๋ฅ˜๊ฐ€ ๋‚œ AsyncImage๋Š” ์ด๋ฏธ์ง€๊ฐ€ ๋ณด์ด์ง€ ์•Š์Œ

๊ณผ์ • ๋ฐ ํ•ด๊ฒฐ :

  • API ์š”์ฒญ ๋ฉ”์„œ๋“œ๋ฅผ ํ˜ธ์ถœํ–ˆ์œผ๋‚˜ URL์ด ๋™์ผํ•˜๊ธฐ ๋•Œ๋ฌธ์— ์ƒˆ๋กœ ๊ทธ๋ ค์ง€์ง€ ์•Š์Œ์„ ํ™•์ธ
  • NetworkManager์—์„œ ํ•ด๋‹น ๋ฉ”์„œ๋“œ๋ฅผ ํ˜ธ์ถœํ•  ๋•Œ ๊ธฐ์กด contents๋ฅผ ๋น„์šฐ๋Š” ์ž‘์—…์„ ์ถ”๊ฐ€ํ•˜์—ฌ ๋ณ€ํ™”๋ฅผ ์คŒ์œผ๋กœ์จ ํ•ด๊ฒฐํ•  ์ˆ˜ ์žˆ์—ˆ์Œ

7. TabView ์ฒ˜์Œ์œผ๋กœ ์ด๋™ํ•˜๊ธฐ

๊ณ ๋ฏผํ•œ ์  :

  • ProgramView์—์„œ ์ƒˆ๋กœ๊ณ ์นจ ๋ฒ„ํŠผ์„ ํ†ตํ•ด ์•„์ดํ…œ์„ ์ƒˆ๋กœ ๋ฐ›์•„์™”์„ ๋•Œ, TabView Page์˜ ๋งจ ์ฒ˜์Œ์œผ๋กœ ๋Œ์•„๊ฐ€๊ฒŒ ํ•˜๊ณ  ์‹ถ์œผ๋‚˜ ๋‹ค์–‘ํ•œ ๋ฐฉ๋ฒ•์„ ์‹œ๋„ํ•ด๋„ ์ ์šฉ๋˜์ง€ ์•Š์Œ

๊ณผ์ • ๋ฐ ํ•ด๊ฒฐ :

  • @Stateํ”„๋กœํผํ‹ฐ๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ํ•ด๊ฒฐํ•˜์˜€๋‹ค. TabView์˜ selection ํŒŒ๋ผ๋ฏธํ„ฐ๋Š” @Stateํ”„๋กœํผํ‹ฐ์™€ ๊ฐ TabViewItem์˜ tag ์กฐํ•ฉ์œผ๋กœ๋งŒ ์จ์•ผํ•˜๋Š” ์ค„ ์•Œ์•˜๋Š”๋ฐ, ์ด๋Š” ๊ฐ ํŽ˜์ด์ง€๊ฐ„ ๋ฒ„ํŠผ์„ ํ†ตํ•ด ์›€์ง์ด๊ฑฐ๋‚˜ ํ•  ๋•Œ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๊ณ , ์ด ๋ฌธ์ œ์˜ ๊ฒฝ์šฐ์—๋Š” ๋‹จ์ˆœํžˆ ์ฒซ ํŽ˜์ด์ง€๋กœ๋งŒ ์ด๋™ํ•˜๋ฉด ๋˜๋Š” ๊ฒƒ์ด๋‹ค.
  • @State ํ”„๋กœํผํ‹ฐ๋กœ ์ž„์˜์˜ ๋ฌธ์ž์—ด ํ˜น์€ ์ •์ˆ˜๋ฅผ ์ƒ์„ฑํ•˜๊ณ , selection ํŒŒ๋ผ๋ฏธํ„ฐ์— ํ•ด๋‹น ํ”„๋กœํผํ‹ฐ๋ฅผ ์ „๋‹ฌํ•œ ํ›„, ์ƒˆ๋กœ๊ณ ์นจ์„ ํ•  ๋•Œ @State ํ”„๋กœํผํ‹ฐ์˜ ๊ฐ’์„ ์ฒ˜์Œ์— ์ง€์ •ํ•œ ๊ฐ’์œผ๋กœ ๋ณ€๊ฒฝํ•ด์ฃผ๋Š” ์ฝ”๋“œ๋ฅผ ์ˆ˜ํ–‰ํ•˜๋„๋ก ํ–ˆ๋‹ค.
  • TabView์—์„œ selection์„ ์‚ฌ์šฉํ•˜๋ฉด ์ฒซ ์•„์ดํ…œ์„ selection์— ๋“ค์–ด์˜จ @State์˜ ๊ฐ’์œผ๋กœ ์‚ผ๊ธฐ ๋•Œ๋ฌธ์— ์ด ๋ฐฉ๋ฒ•์„ ํ†ตํ•ด ๋Œ์•„๊ฐˆ ์ˆ˜ ์žˆ์—ˆ๋‹ค.

8. SearchView์—์„œ ํ‚ค๋ณด๋“œ ๋‚ด๋ฆฌ๊ธฐ

๊ณ ๋ฏผํ•œ ์  :

  • searchable๋กœ ๊ฒ€์ƒ‰๊ธฐ๋Šฅ์„ ๊ตฌํ˜„ํ–ˆ๋Š”๋ฐ, ์ผ๋ฐ˜์ ์œผ๋กœ ์•ฑ์„ ์‚ฌ์šฉํ•  ๋•Œ์ฒ˜๋Ÿผ ํ™”๋ฉด์„ ํ„ฐ์น˜ํ–ˆ์„ ๋•Œ ํ‚ค๋ณด๋“œ๋ฅผ ๋‚ด๋ฆฌ๊ณ ์ž ํ–ˆ์Œ

๊ณผ์ • ๋ฐ ํ•ด๊ฒฐ :

  • View+Extension์— hideKeyboard() ๋ฉ”์„œ๋“œ๋ฅผ ๊ตฌํ˜„ํ•˜์—ฌ ํ•ด๊ฒฐ
  • UIApplication ์ธ์Šคํ„ด์Šค๋Š” ์‹ฑ๊ธ€ํ†ค ๊ฐ์ฒด์ธ๋ฐ, ์‚ฌ์šฉ์ž ์ด๋ฒคํŠธ์˜ ๋ผ์šฐํŒ… ์ฒ˜๋ฆฌ, ์ œ์–ด ๊ฐ์ฒด๊ฐ€ ์ „๋‹ฌํ•œ ๋™์ž‘ ๋ฉ”์‹œ์ง€๋ฅผ ์ ์ ˆํ•˜๊ฒŒ ์ „์†กํ•˜๋Š” ๋“ฑ์˜ ์—ญํ• ์„ ํ•˜๋Š”๋ฐ, ์ด๋ฅผ ํ™œ์šฉํ•  ์ˆ˜ ์žˆ์—ˆ๋‹ค.
  • UIApplication.shared๋กœ ์‹ฑ๊ธ€ํ†ค ๊ฐ์ฒด์— ์ ‘๊ทผํ•˜๊ณ , sendAction ๋ฉ”์„œ๋“œ๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ UIResponder์˜ resignFirstResponder๋ฅผ ์ˆ˜ํ–‰ํ•˜๋„๋ก ํ–ˆ๋‹ค.

9. List์—์„œ ๋งจ ์œ„๋กœ ์ด๋™ํ•˜๊ธฐ

๊ณ ๋ฏผํ•œ ์  :

  • SearchView์—์„œ ์Šคํฌ๋กค์„ ๋‚ด๋ฆฐ ์ƒํƒœ๋กœ ํ•„ํ„ฐ๋ฅผ ์ ์šฉํ•˜๋ฉด ๊ทธ ์œ„์น˜์—์„œ ํ•ญ๋ชฉ์ด ๋ณ€๊ฒฝ๋˜๊ธฐ ๋•Œ๋ฌธ์— ๋งจ ์œ„๋กœ ๋‹ค์‹œ ์˜ฌ๋ ค์•ผ ํ•˜๋Š” ๋ถˆํŽธํ•จ์ด ์žˆ์–ด ์ด๋ฅผ ํ•ด๊ฒฐํ•˜๊ณ ์ž ํ–ˆ๋‹ค.

๊ณผ์ • ๋ฐ ํ•ด๊ฒฐ :

  • TabView๋•Œ์ฒ˜๋Ÿผ ์ฒซ ํ•ญ๋ชฉ์„ ์ง€์ •ํ•  ์ˆ˜ ์—†๊ธฐ์— ScrollViewReader๋กœ View๋ฅผ embedํ•˜๊ณ , proxy๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ scrollTo ๋ฉ”์„œ๋“œ๋ฅผ ์‚ฌ์šฉํ•˜์˜€๋‹ค.
  • ๋‹ค๋งŒ ๋ณ€๊ฒฝ๋˜๋Š” ํ•ญ๋ชฉ ์†์—์„œ ์–ด๋–ป๊ฒŒ ๋งจ ์œ„์˜ ๊ฐ’์„ ์–ป๋А๋ƒ๊ฐ€ ๊ด€๊ฑด์ด์—ˆ๋Š”๋ฐ, ํ•„ํ„ฐ ๊ฒฐ๊ณผ์— ์ƒ๊ด€์—†์ด ๊ณ ์ •์ ์œผ๋กœ ์ƒ๋‹จ์— ์กด์žฌํ•˜๋Š” EmptyView๋ฅผ ๋งŒ๋“ค๊ณ  ํ•ด๋‹น View์— id๋ฅผ ๋ถ€์—ฌํ•˜๋Š” ๋ฐฉ์‹์œผ๋กœ ํ•ด๊ฒฐํ•  ์ˆ˜ ์žˆ์—ˆ๋‹ค.

10. [WebKit] WebView ์‚ฌ์šฉํ•˜๊ธฐ

๊ณ ๋ฏผํ•œ ์  :

  • UIKit์—์„œ๋Š” WebView ํ˜น์€ SFSafariViewController๋ฅผ ๊ทธ๋ƒฅ ์‚ฌ์šฉํ•˜๋ฉด ๋˜์—ˆ์ง€๋งŒ SwiftUI์—์„œ ์›น๋ทฐ ๊ธฐ๋Šฅ์„ ๋„ค์ดํ‹ฐ๋ธŒ๋กœ ์ œ๊ณตํ•˜์ง€ ์•Š์•„ UIKit์˜ ๋ฐฉ์‹์„ ์จ์•ผ ํ–ˆ๋‹ค.

๊ณผ์ • ๋ฐ ํ•ด๊ฒฐ :

  • UIKit์˜ ๊ธฐ๋Šฅ์„ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•ด์ฃผ๋Š” UIViewRepresentable ํ”„๋กœํ† ์ฝœ์„ ์‚ฌ์šฉํ•˜์—ฌ ํ•ด๊ฒฐํ•  ์ˆ˜ ์žˆ์—ˆ๋‹ค.
  • makeUIView, updateUIView ๋‘ ๊ฐœ์˜ ํ•„์ˆ˜ ๋ฉ”์„œ๋“œ๋ฅผ ๊ตฌํ˜„ํ•ด์•ผ ํ•˜๋Š”๋ฐ, ์ „์ž๋Š” View๋ฅผ ๋งŒ๋“ค๊ณ  ์ดˆ๊ธฐ ์„ค์ •์„ ์ง„ํ–‰ํ•˜๋Š” ๋ฉ”์„œ๋“œ์ด๊ณ  ํ›„์ž๋Š” ์ƒˆ๋กœ์šด ์ •๋ณด๋กœ View๋ฅผ ์—…๋ฐ์ดํŠธ ํ•ด์ฃผ๋Š” ๋ฉ”์„œ๋“œ์ด๋‹ค.
  • makeUIView์—์„œ ์–ด๋–ค View ํƒ€์ž…์„ ๋ฐ˜ํ™˜ํ•  ๊ฒƒ์ธ์ง€ ๋ช…์‹œํ•ด์ฃผ๊ณ , updateUIView์—์„œ์˜ ํŒŒ๋ผ๋ฏธํ„ฐ ํƒ€์ž…๋„ ์ด์— ๋งž์ถฐ์„œ ๋ณ€๊ฒฝํ•ด์ค˜์•ผ ํ•œ๋‹ค.
  • makeUIView์—์„œ ๋นˆ WKWebView๋ฅผ ๋ฐ˜ํ™˜ํ•˜๊ณ , updateUIView์—์„œ ์ฃผ์–ด์ง„ url์„ Request๋กœ ๋งŒ๋“ค์–ด WKWebView์—์„œ ๋ถˆ๋Ÿฌ์˜ค๋„๋ก ํ–ˆ๋‹ค.
import SwiftUI
import WebKit

struct WebView: UIViewRepresentable {
    // MARK: - Public Properties
    let urlToConnect: String

    // MARK: - Private Properties
    private var webView: WKWebView

    // MARK: - Initializer
    init(urlToConnect: String) {
        self.urlToConnect = urlToConnect
        self.webView = WKWebView()
    }

    // MARK: - UIViewRepresentable
    func makeUIView(context: Context) -> WKWebView {
        return webView
    }

    func updateUIView(_ webView: WKWebView, context: Context) {
        guard let url = URL(string: urlToConnect) else { return }

        let urlRequest = URLRequest(url: url)

        webView.load(urlRequest)
    }

    // MARK: - Public Functions
    func goBack() {
        webView.goBack()
    }

    func goForward() {
        webView.goForward()
    }

    func reload() {
        webView.reload()
    }
}

11. TabBar ๋ฐฐ๊ฒฝ์ƒ‰ ๋ณ€๊ฒฝํ•˜๊ธฐ

๊ณ ๋ฏผํ•œ ์  :

  • ์ƒ์„ธ ํŽ˜์ด์ง€์— ๋“ค์–ด๊ฐ”์„ ๋•Œ TabBar์˜ ์ƒ‰์ƒ์ด ํŽ˜์ด์ง€ ๋ฐฐ๊ฒฝ ์ƒ‰์ƒ๊ณผ ์ผ์น˜ํ–ˆ์œผ๋ฉด ์ข‹๊ฒ ์œผ๋‚˜ ์ƒ‰์ƒ ๋ณ€๊ฒฝ ์ฝ”๋“œ๊ฐ€ ์ ์šฉ๋˜์ง€ ์•Š์•˜๋‹ค.

๊ณผ์ • ๋ฐ ํ•ด๊ฒฐ :

  • init()์—์„œ UITabBar.appearance().backgroundColor ์†์„ฑ์„ ํ†ตํ•ด ๋ณ€๊ฒฝํ•  ์ˆ˜ ์žˆ์—ˆ๋‹ค.
init() {
    UITabBar.appearance().backgroundColor = UIColor.systemBackground
}

12. ์œ„์น˜ ์ •๋ณด๋ฅผ ์ง€๋„๋กœ ๋ณด์—ฌ์ฃผ๊ธฐ

๊ณ ๋ฏผํ•œ ์  :

  • ํ”„๋กœ๊ทธ๋žจ์ด ์ง„ํ–‰๋˜๋Š” ์žฅ์†Œ๋ฅผ ์ง€๋„๋ฅผ ํ†ตํ•ด ๋ณด์—ฌ์ฃผ๊ณ  ์‹ถ์œผ๋‚˜ MapKit์„ ์‚ฌ์šฉํ•˜๋ฉด ์•ฑ์˜ UI๊ฐ€ ๊นจ์ง€๋Š” ํ˜„์ƒ์ด ๋ฐœ์ƒํ–ˆ๋‹ค(๋‚ด๋น„๊ฒŒ์ด์…˜ ๋ฐ”, ํƒญ๋ฐ” ์ƒ‰์ƒ ๋ณ€๊ฒฝ ๋ฐ ๋‚ด๋น„๊ฒŒ์ด์…˜ ํƒ€์ดํ‹€ ์ƒ‰ ์ดˆ๊ธฐํ™”).

๊ณผ์ • ๋ฐ ํ•ด๊ฒฐ :

  • ProgramDetailView์—์„œ DescriptionView์— Map์„ ๋‹ด์•„๋ณด๋Š” ๋ฐฉ๋ฒ•, MapView๋ฅผ ๋”ฐ๋กœ ๊ตฌํ˜„ํ•˜๊ณ  ์ธ์Šคํ„ด์Šค๋กœ ๋ถˆ๋Ÿฌ์™€ ์‚ฌ์šฉํ•˜๋Š” ๋ฐฉ๋ฒ•, ๋‚ด๋น„๊ฒŒ์ด์…˜ ๋ฐ”์™€ ํƒญ๋ฐ” ์ƒ‰์ƒ์„ ๊ฐ•์ œํ•˜๋Š” ๋ฐฉ๋ฒ• ๋“ฑ ๋‹ค๋ฐฉ๋ฉด์œผ๋กœ ์‹œ๋„ํ•˜์˜€์œผ๋‚˜ ์›ํ•˜๋Š”๋Œ€๋กœ ์ด๋ค„์ง€์ง€ ์•Š์•˜๋‹ค.
  • ์• ํ”Œ ๊ธฐ๋ณธ ์ง€๋„ ์•ฑ์œผ๋กœ ์—ฐ๊ฒฐ์‹œํ‚ค๊ณ ์ž ํ–ˆ์œผ๋‚˜ ๊ธฐ๋ณธ ์ง€๋„ ์•ฑ์—์„œ ์š”๊ตฌํ•˜๋Š” ์ •๋ณด์™€ ๋ฐ์ดํ„ฐ์—์„œ ์ œ๊ณตํ•˜๋Š” ์ •๋ณด๊ฐ€ ๋งž์ง€ ์•Š์•„ ์œ„์น˜ ์ •๋ณด๋ฅผ ํ‘œ์‹œํ•  ์ˆ˜ ์—†๋Š” ๋ฐ์ดํ„ฐ๊ฐ€ ์žˆ์—ˆ๋‹ค.
  • ์‚ฌ๋žŒ๋“ค์ด ๋ณดํ†ต ๋งŽ์ด ์‚ฌ์šฉํ•˜๋Š” ์™ธ๋ถ€ ์ง€๋„ ์•ฑ(๋„ค์ด๋ฒ„, ์นด์นด์˜ค)์„ ์‚ฌ์šฉํ•˜์—ฌ ์œ„์น˜ ์ •๋ณด๋ฅผ ์ œ๊ณตํ•˜๊ณ ์ž ํ–ˆ๋‹ค.
  • ๊ทธ๋Ÿฌ๋‚˜ ์•ฑ ๋‚ด์—์„œ ์กฐ์ž‘์„ ํ•˜๊ธฐ๋ณด๋‹ค ์‹ค์ œ ์•ฑ์—์„œ ๊ฒฝ๋กœ๋ฅผ ๋ณด์—ฌ์ฃผ๋Š” ๊ฒƒ์ด ๋” ํšจ์œจ์ ์ด๋ผ ํŒ๋‹จํ•˜์—ฌ ๊ฐ ์•ฑ์˜ API๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ํ”„๋กœ๊ทธ๋žจ์ด ์ง„ํ–‰๋˜๋Š” ์žฅ์†Œ์˜ ์œ„, ๊ฒฝ๋„๋ฅผ ์ „๋‹ฌํ•˜๊ณ  ํ˜„์žฌ ์œ„์น˜์—์„œ ๊ฐ€๋Š” ๊ฒฝ๋กœ๋ฅผ ๋Œ€์ค‘๊ตํ†ต์œผ๋กœ ๋ณด์—ฌ์ฃผ์—ˆ๋‹ค.
  • ์•ฑ์ด ์„ค์น˜๋˜์–ด ์žˆ์œผ๋ฉด ์‹คํ–‰ํ•˜์—ฌ ๊ฒฝ๋กœ๋ฅผ ๋ณด์—ฌ์ฃผ๊ณ , ์—†๋‹ค๋ฉด ์•ฑ์Šคํ† ์–ด์— ์—ฐ๊ฒฐํ–ˆ์ง€๋งŒ ์™ธ๋ถ€ ์•ฑ์„ ์‚ฌ์šฉํ•˜๊ณ  ์‹ถ์ง€ ์•Š์€ ์ด์šฉ์ž๋„ ์žˆ์„ ๊ฒƒ์ด๊ธฐ์— WebView๋ฅผ ํ†ตํ•ด ํฌํ„ธ์‚ฌ์ดํŠธ ์ฟผ๋ฆฌ์— ํ•ด๋‹น ์žฅ์†Œ๋ช…์„ ์ง‘์–ด๋„ฃ์–ด ์•ฑ ๋‚ด์—์„œ๋„ ์žฅ์†Œ ์ •๋ณด๋ฅผ ํ™•์ธํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ–ˆ๋‹ค.
struct MapButton: View {
    // MARK: - Public Properties
    var mapType: MapType
    var latitude: String
    var longitude: String

    // MARK: - Body
    var body: some View {
        switch mapType {
            case .naver:
                Button(action: {
                    LinkToNaverMap(
                        latitude: Double(latitude) ?? 0,
                        longitude: Double(longitude) ?? 0)
                }, label: {
                    Image("Naver_map_Icon")
                        .resizable()
                        .frame(width: 30, height: 30)
                })

            case .kakao:
                Button(action: {
                    LinkToKakaoMap(
                        latitude: Double(latitude) ?? 0,
                        longitude: Double(longitude) ?? 0)
                }, label: {
                    Image("Kakao_map_Icon")
                        .resizable()
                        .frame(width: 30, height: 30)
                })
        }
    }
}

13. ์ด๋ฏธ์ง€ ์บ์‹ฑ

๊ณ ๋ฏผํ•œ ์  :

  • ๋งค๋ฒˆ AsyncImage๋กœ ๋ถˆ๋Ÿฌ์˜ค๋ฉด ๋ฐ์ดํ„ฐ ๋‚ญ๋น„๊ฐ€ ์ผ์–ด๋‚˜๊ณ , List ๋˜๋Š” TabView์—์„œ lazy loading์„ ํ•˜๋ฉด์„œ ์ข…์ข… ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ๋‹ค. Caching์„ ํ†ตํ•ด ์ €์žฅ๋œ ์ด๋ฏธ์ง€๊ฐ€ ์—†๋‹ค๋ฉด ๋ถˆ๋Ÿฌ์˜ค๊ณ , ์žˆ๋‹ค๋ฉด ๊ทธ๊ฒƒ์„ ์‚ฌ์šฉํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ์ฐพ๊ณ ์ž ํ–ˆ๋‹ค.

๊ณผ์ • ๋ฐ ํ•ด๊ฒฐ :

  • ๊ธฐ์กด์— ์‚ฌ์šฉํ•œ AsyncImage๋ฅผ ์ตœ๋Œ€ํ•œ ํ™œ์šฉํ•ด๋ณด๋ ค ํ–ˆ์œผ๋‚˜ Image ํƒ€์ž…์„ UIImage๋กœ๋Š” ๋ณ€ํ™˜ํ•  ์ˆ˜ ์—†์–ด(๋ฐ˜๋Œ€๋Š” ๊ฐ€๋Šฅํ•˜์ง€๋งŒ) ์ƒˆ๋กญ๊ฒŒ ์ด๋ฏธ์ง€๋ฅผ ํ‘œํ˜„ํ•  ํ•„์š”๊ฐ€ ์žˆ์—ˆ๋‹ค.
  • ์šฐ์„  CacheManager๋ฅผ ์‹ฑ๊ธ€ํ†ค ํŒจํ„ด์œผ๋กœ ๊ตฌํ˜„ํ•˜๊ณ , ๊ทธ ์•ˆ์— ์žˆ๋Š” private cache๋ฅผ ํ™œ์šฉํ•˜๊ธฐ๋กœ ํ–ˆ๋‹ค.
  • CacheManager์˜ fetchImage(urlString: completion:)๋ฉ”์„œ๋“œ์— content์˜ imageURL์„ ๋ณด๋‚ด ํ˜ธ์ถœํ•˜๋ฉด, cache์— ํ•ด๋‹น string์„ NSString์œผ๋กœ ๋ณ€ํ™˜ํ•œ ๊ฐ’์— ์ด๋ฏธ์ง€๊ฐ€ ์žˆ๋Š”์ง€ ํ™•์ธ์„ ํ•˜๊ณ , ์žˆ๋‹ค๋ฉด ์ด๋ฏธ์ง€๋ฅผ completion์œผ๋กœ ๋ฐ˜ํ™˜, ์—†๋‹ค๋ฉด URLSession์„ ํ†ตํ•ด ๋ฐ›์€ ํ›„ ์บ์‹œ์— ์ €์žฅํ•˜๊ณ  ๋ฐ˜ํ™˜ํ•˜๋Š” ๋ฐฉ์‹์„ ์‚ฌ์šฉ
  • ์ด๋Ÿฌํ•œ ์ผ๋ จ์˜ ๋น„๋™๊ธฐ ๋ฐฉ์‹์„ ์‚ฌ์šฉํ•˜๊ธฐ ์œ„ํ•ด @escaping closure๋ฅผ ํ™œ์šฉ
  • ์ดํ›„ ์ปค์Šคํ…€ํ•œ View์ธ CachedAsyncImageView์—์„œ CacheManager์˜ fetchImage๋ฅผ ํ™œ์šฉ, ์ด๋ฏธ์ง€๋ฅผ ๋ณด์—ฌ์ฃผ๋Š” ๋ฐฉ์‹์œผ๋กœ ํ•ด๊ฒฐํ•˜์˜€๋‹ค.
final class CacheManager {
    // MARK: - Singleton Pattern
    static let shared = CacheManager()

    // MARK: - Initializer
    private init() { }

    // MARK: - Private Properties
    private var cache = NSCache<NSString, UIImage>()

    // MARK: - Public Functions
    func fetchImage(urlString: String, completion: @escaping (UIImage?) -> Void) {
        if let cachedImage = cache.object(forKey: urlString as NSString) {
            completion(cachedImage)
        } else {
            guard let url = URL(string: urlString) else {
                completion(nil)

                return
            }

            URLSession.shared.dataTask(with: url) { data, _, _ in
                guard let data = data, let image = UIImage(data: data) else {
                    completion(nil)

                    return
                }

                self.cache.setObject(image, forKey: urlString as NSString)

                completion(image)
            }.resume()
        }
    }
}
์บ์‹ฑ ์ „ ๋ฉ”๋ชจ๋ฆฌ ์บ์‹ฑ ์ „ ๋„คํŠธ์›Œํฌ
์บ์‹ฑ ํ›„ ๋ฉ”๋ชจ๋ฆฌ ์บ์‹ฑ ํ›„ ๋„คํŠธ์›Œํฌ
  • ๊ทธ๋ž˜ํ”„์™€ ๊ฐ™์ด ์บ์‹ฑ ์ „์—๋Š” ๋งค ํŽ˜์ด์ง•(์Šคํฌ๋กค) ๋•Œ๋งˆ๋‹ค ์ƒˆ๋กญ๊ฒŒ ์ด๋ฏธ์ง€๋ฅผ ๋ถˆ๋Ÿฌ์˜ค๊ธฐ ๋•Œ๋ฌธ์— ๋ฉ”๋ชจ๋ฆฌ ์‚ฌ์šฉ๋Ÿ‰์ด ๋“ค์ญ‰๋‚ ์ญ‰ํ•˜์ง€๋งŒ, ์บ์‹ฑ ํ›„์—๋Š” ๋ฉ”๋ชจ๋ฆฌ ์ž์ฒด ์‚ฌ์šฉ๋Ÿ‰์€ ๋†’์œผ๋‚˜ ํŽ˜์ด์ง•(์Šคํฌ๋กค)์‹œ์— ์ด๋ฏธ ๋ถˆ๋Ÿฌ์˜จ ์ด๋ฏธ์ง€๋ฅผ ์‚ฌ์šฉํ•˜๊ธฐ ๋•Œ๋ฌธ์— ์ดํ›„ ๋ฉ”๋ชจ๋ฆฌ ์‚ฌ์šฉ๋Ÿ‰์€ ์ƒˆ๋กญ๊ฒŒ ๋ถˆ๋Ÿฌ์˜ค์ง€ ์•Š๋Š” ์ด์ƒ ๊ณ ์ •๋จ์„ ๋ณผ ์ˆ˜ ์žˆ๋‹ค.

14. [WebKit] WebView ๋ฉ”๋ชจ๋ฆฌ ์ตœ์ ํ™”

๊ณ ๋ฏผํ•œ ์  :

  • WKWebView๋ฅผ UIViewRepresentable์„ ์‚ฌ์šฉํ•˜์—ฌ SwiftUI View๋กœ ์‚ฌ์šฉํ–ˆ๋Š”๋ฐ, Instruments์˜ Leak์„ ์ฒดํฌํ•˜๋ฉฐ ๋ฉ”๋ชจ๋ฆฌ๊ฐ€ ๋น„์ •์ƒ์ ์œผ๋กœ ๋†’์•„์ง€๋Š” ํ˜„์ƒ์„ ๋ฐœ๊ฒฌํ•˜์˜€๊ณ , ์ด๋ฅผ ํ•ด๊ฒฐํ•˜๊ณ ์ž ํ–ˆ๋‹ค.
  • Instruments ์ธก์ • ๋ฐ์ดํ„ฐ์ƒ Leak์€ ๋ฐœ์ƒํ•˜์ง€ ์•Š์ง€๋งŒ Allocation์—์„œ WebView๋ฅผ ์‹คํ–‰ํ•˜๋Š” ์ˆœ๊ฐ„ ๋ฉ”๋ชจ๋ฆฌ๊ฐ€ 4~50MiB์—์„œ 550MiB๋กœ ๊ธ‰์ฆ, ์•ฝ 500MiB์˜ ๋ฉ”๋ชจ๋ฆฌ ํ• ๋‹น์€ ๋‹จ์ผ VM Allocation์ž„์„ ํ™•์ธํ–ˆ๋‹ค.
  • VM Allocation ์ •๋ณด์— ๋”ฐ๋ฅด๋ฉด WKWebView์—์„œ ๋ฐœ์ƒํ•œ ํ• ๋‹น์ด๋ฉฐ, Responsible Library๋Š” JavaScriptCore์˜€๋‹ค.

๊ณผ์ • ๋ฐ ํ•ด๊ฒฐ :

  1. WKWebView์˜ ์‚ฌ์šฉ์ƒ ๋ฌธ์ œ์ธ๊ฐ€?
  • UIViewRepresentable ๊ตฌํ˜„์ƒ ๋ฌธ์ œ๋Š” ์—†์—ˆ๊ณ , ๋ณดํ†ต WKWebView๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด์„œ ํ”ํžˆ ๋ฐœ์ƒํ•˜๋Š” ๋ฉ”๋ชจ๋ฆฌ ์ด์Šˆ๋Š” ์ปค์Šคํ…€ ์ฝ”๋””๋„ค์ดํ„ฐ๋ฅผ ๋งŒ๋“ค๋ฉด์„œ self๋ฅผ ๋„˜๊ฒจ์ฃผ๋Š” ๊ณผ์ •์—์„œ ๋ฐœ์ƒํ•˜๋Š” ๊ฐ•ํ•œ ์ฐธ์กฐ์˜€๊ธฐ์— ์ด ์ผ€์ด์Šค์— ๋ฐ˜์˜ํ•  ์ˆ˜ ์—†์—ˆ๋‹ค.
  1. ์ง์ ‘ ํ• ๋‹น ํ•ด์ œ๋ฅผ ํ•ด์ค˜์•ผ ํ•˜๋Š”๊ฐ€?
  • UIViewRepresentable๋กœ ๊ตฌํ˜„ํ•œ WebView๋Š” ๊ฐ’ํƒ€์ž… struct์ด๊ธฐ ๋•Œ๋ฌธ์— ํด๋ž˜์Šค๋‚˜ ํด๋ž˜์Šค ๋ฐ”์šด๋“œ ํ”„๋กœํ† ์ฝœ์— ํ•ด๋‹นํ•˜์ง€ ์•Š์•„ deinit์„ ์‚ฌ์šฉํ•  ์ˆ˜ ์—†๊ณ , ๋ฐ”์šด๋“œ๋ฅผ ๋ฒ—์–ด๋‚˜๋ฉด ๋ฉ”๋ชจ๋ฆฌ๊ฐ€ ํ• ๋‹น๋  ๊ฒƒ์ด๋ผ ์ƒ๊ฐํ–ˆ๋‹ค.
  1. ์–ด๋–ค ์ ์ด ๋ฌธ์ œ์ธ๊ฐ€?
  • ์œ„์˜ 2๊ฐ€์ง€ ๊ด€์ ์œผ๋กœ ๋‹ค์–‘ํ•œ ์ •๋ณด๋ฅผ ์ฐพ๊ณ , ์‹œ๋„๋ฅผ ํ•ด๋ณธ ๋์— ์›์ ์œผ๋กœ ๋Œ์•„์™€ ์ด๊ฒŒ ์–ด๋–ค ๋ฌธ์ œ์ธ์ง€ ๋‹ค์‹œ๊ธˆ ์ƒ๊ฐ์„ ํ–ˆ๋‹ค.
  • Instruments์—์„œ๋Š” ๋ฉ”๋ชจ๋ฆฌ๊ฐ€ ๊ธ‰์ฆํ•œ ํ›„ ์œ ์ง€๋˜๋Š” ์–‘์ƒ์„ ๋ณด์ด๋Š” ๋ฐ˜๋ฉด, Xcode Memory Report์ƒ Memory Usage๋Š” ์ •์ƒ์ ์ธ ๋ฉ”๋ชจ๋ฆฌ ์‚ฌ์šฉ๋ฅ ์„ ๋ณด์˜€๋‹ค.
  • ๋งŒ์•ฝ ๋ฉ”๋ชจ๋ฆฌ์ƒ ๊ณ„์† WebKit์„ ์‚ฌ์šฉํ•จ์— ์žˆ์–ด ์ฆ๊ฐ€ ์ถ”์„ธ๋ฅผ ๋ณด์˜€๊ฑฐ๋‚˜, Leak์ด ๋ฐœ์ƒํ–ˆ๊ฑฐ๋‚˜, Xcode Memory Report์—์„œ๋„ ๊ธ‰์ฆํ•˜๋Š” ํ˜„์ƒ์„ ๋ณด์˜€๋‹ค๋ฉด ์‚ฌ์šฉ์ƒ์˜ ๋ฌธ์ œ๋กœ ๋ฉ”๋ชจ๋ฆฌ ์ตœ์ ํ™”๊ฐ€ ๋ฐ˜๋“œ์‹œ ํ•„์š”ํ•˜์ง€๋งŒ, ์ „๋ฐ˜์ ์œผ๋กœ ์ •์ƒ์ธ๋ฐ ๋ฐ˜ํ•ด Instruments์—์„œ๋งŒ ๋ฉ”๋ชจ๋ฆฌ๊ฐ€ ์ฆ๊ฐ€ํ•˜๋Š” ์ ์—์„œ ๋‹ค๋ฅธ ๋ฐฉํ–ฅ์œผ๋กœ ์ ‘๊ทผํ•ด์•ผ ํ•จ์„ ์•Œ์•˜๋‹ค.
  1. VM Tracker
  • ์™œ Instruments์—์„œ๋งŒ ์ผ์–ด๋‚ ๊นŒ? ์ผ๋‹จ ๋ฉ”๋ชจ๋ฆฌ ํ• ๋‹น์ด VM Allocation์ด๊ธฐ ๋•Œ๋ฌธ์— VM Tracker๋กœ ์ธก์ •์„ ํ–ˆ๋‹ค.
  • VM Tracker๋กœ ์ธก์ •ํ–ˆ์„ ๋•Œ, Dirty Size/Swapped Size/Resident Size ์„ธ ํŒŒํŠธ๋กœ ์ •๋ณด๋ฅผ ์–ป์„ ์ˆ˜ ์žˆ๋‹ค.
  • Resident Size 409 MiB / Dirty Size 67 MiB / Swapped 59 MiB
  • ๊ทธ๋Ÿฌ๋‚˜ Type์—์„œ ์ ˆ๋ฐ˜์„ ์ฐจ์ง€ํ•˜๋Š” __Text, *Dirty*๋ฅผ ๋ณด๋ฉด ๊ฐ๊ฐ์€ 220/0/0, 83/67/43์˜ ๋ฉ”๋ชจ๋ฆฌ ๋ถ„ํฌ๋ฅผ ๋ณผ ์ˆ˜ ์žˆ๋‹ค.
  1. ๊ฒฐ๋ก 
  • VM Tracker์ƒ์—์„œ๋„ ์•ฑ์—์„œ ์‚ฌ์šฉํ•œ Dirty Memory์˜ ํฌ๊ธฐ๊ฐ€ ์ž‘๊ณ , __Text์˜ Resident Size๋„ WebCore๋งŒ ์žˆ๋Š” ๊ฒƒ์ด ์•„๋‹ˆ๋ผ SwiftUI, UIKitCore, JavaScriptCore๊ฐ€ ๊ฐ๊ฐ ์กฐ๊ธˆ์”ฉ ๊ฐ–๊ณ  ์žˆ๋Š” ๊ฒƒ์œผ๋กœ ๋ณด์•„ WebView๋งŒ์˜ ๋ฌธ์ œ๋ผ๊ณ  ๋ณด๊ธฐ๊ฐ€ ์–ด๋ ค์› ๋‹ค.
  • Instruments์—์„œ ์‹œ๋ฎฌ๋ ˆ์ดํ„ฐ ์›น ํ™˜๊ฒฝ์„ ์ธก์ •ํ•˜๊ธฐ ์œ„ํ•ด ํ•„์š”ํ•œ ์ž์›์ด๊ฑฐ๋‚˜, __Text ํƒ€์ž…์—์„œ ๋ดค๋“ฏ์ด UIKit์˜ WebView๋ฅผ SwiftUI์˜ View๋กœ ์ „ํ™˜ํ•˜๋Š” ๊ณผ์ •์—์„œ ์ž์›์ด ํ•„์š”ํ–ˆ์„ ์ˆ˜๋„ ์žˆ๋‹ค๊ณ  ๋ณด์•˜๋‹ค.
  • Instruments์—์„œ์˜ ๋ฉ”๋ชจ๋ฆฌ ์ฆ๊ฐ€๋ฅผ ์ค„์ด๊ฑฐ๋‚˜ ์›์ธ์„ ์ฐพ์ง€๋Š” ๋ชปํ–ˆ์ง€๋งŒ, ๊ทธ ๊ณผ์ •์—์„œ Xcode์˜ ๋ฉ”๋ชจ๋ฆฌ ๊ด€๋ฆฌ ๊ธฐ๋ฒ•์„ ๋‹ค์–‘ํ•˜๊ฒŒ ์‚ฌ์šฉํ•ด๋ดค๋‹ค๋Š” ์ , WebView๋ฅผ ๋” ํšจ์œจ์ ์œผ๋กœ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋Š” ๋ฐฉํ–ฅ์„ ๋‹ค๋ฐฉ๋ฉด์œผ๋กœ ์‹œ๋„ํ•ด๋ณผ ์ˆ˜ ์žˆ์—ˆ๊ธฐ์— ์–ป์–ด๊ฐ„ ๊ฒƒ์ด ๋งŽ๋‹ค๊ณ  ์ƒ๊ฐํ•œ๋‹ค.
Instruments
ํˆด๋ฐ” ์—†์„ ๋•Œ ๋ฉ”๋ชจ๋ฆฌ ํˆด๋ฐ” ์žˆ์„ ๋•Œ ๋ฉ”๋ชจ๋ฆฌ

++ 6) ์ถ”๊ฐ€ ์˜๊ฒฌ

  • Xcode์˜ Instruments๊ฐ€ ์ธก์ •ํ•  ๋•Œ๋งˆ๋‹ค ์•ฝ๊ฐ„์€ ๋‹ค๋ฅธ ์–‘์ƒ์„ ๋ณด์ด๋Š” ๋“ฏํ•˜๋‹ค. 24. 03. 15. ๊ธฐ์ค€ WebKit์„ ์‚ฌ์šฉํ•  ๋•Œ ํ• ๋‹น๋˜์—ˆ๋˜ ๋ฉ”๋ชจ๋ฆฌ๊ฐ€ ์‚ฌ์šฉ์ด ๋๋‚œ ๋’ค ํ• ๋‹น์ด ํ•ด์ œ๋˜๋Š” ๋ชจ์Šต์„ ๋ณผ ์ˆ˜ ์žˆ๋‹ค.
WebKit ๋ฉ”๋ชจ๋ฆฌ๊ฐ€ ํ• ๋‹น๋˜์—ˆ๋‹ค๊ฐ€ ํ•ด์ œ๋˜๋Š” ๋ชจ์Šต

15. App Theme ๊ด€๋ฆฌ

๊ณ ๋ฏผํ•œ ์  :

  • ๊ธฐ์กด์—๋Š” App Theme์„ ์„ค์ •ํ•˜๊ธฐ ์œ„ํ•ด Assets์— Color Set์„ ๋งŒ๋“ค๊ณ , ThemeColors์— case๋ฅผ ์ถ”๊ฐ€ํ•ด์คฌ๋‹ค. ๊ทธ๋Ÿฌ๋‚˜ ์ƒˆ๋กœ์šด ์ƒ‰์ƒ์„ ์ถ”๊ฐ€ํ•˜๋ ค๊ณ  ๋ณด๋‹ˆ, ์ด๋ฏธ ๊ตฌํ˜„ํ•œ ์ƒ‰์ƒ์ด๋‚˜ ์ƒˆ๋กœ ์ถ”๊ฐ€ํ•˜๋ ค๋Š” ์ƒ‰์ƒ ๋ชจ๋‘ Apple์—์„œ ์ œ๊ณตํ•˜๋Š” SystemColor์ด๊ธฐ์— ์ด๋ ‡๊ฒŒ ๋ณต์žกํ•˜๊ฒŒ ์ง„ํ–‰ํ•  ํ•„์š”๊ฐ€ ์žˆ์„๊นŒ ์‹ถ์€ ์ƒ๊ฐ์ด ๋“ค์—ˆ๋‹ค.

๊ณผ์ • ๋ฐ ํ•ด๊ฒฐ :

  • AccentColor๋ฅผ ์ œ๊ฑฐํ•˜๊ณ  Color ๊ฐ’์„ ๊ฐ–๊ธฐ๋กœ ๊ฒฐ์ •์„ ํ•˜๊ณ  ์ž‘์—…์„ ์ง„ํ–‰ํ–ˆ๋‹ค.
  • ๊ทธ๋ ‡๋‹ค๋ฉด ํ•„์š”ํ•œ ์ ์ด ๋ฌด์—‡์ผ๊นŒ? ์ด๋ฏธ ๋“ฑ๋กํ•œ AccentColor์ด๋ฆ„์„ ๊ฐ–๊ณ  ์žˆ๋Š” Assets์˜ Color Set์„ ๋‹ค ์ง€์šฐ๊ณ , ThemeColors์—์„œ Color๊ฐ’์„ ๊ฐ€์ง€๋„๋ก ์ฝ”๋“œ๋ฅผ ๊ตฌ์„ฑํ•œ ๊ฒฐ๊ณผ ๋‹ค์Œ์˜ ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ–ˆ๋‹ค.
  • Color๋ฅผ ์‚ฌ์šฉํ•˜๊ธฐ ์œ„ํ•ด์„œ๋Š” SwiftUI์˜ import๊ฐ€ ํ•„์š”ํ–ˆ๊ณ , SettingsView์—์„œ ForEach๋กœ Button์„ ์ƒ์„ฑํ•˜๊ธฐ ์œ„ํ•ด์„œ๋Š” CaseIterable์ด, background()์˜ ์š”์†Œ๋กœ ์‚ฌ์šฉํ•˜๊ธฐ ์œ„ํ•ด์„œ๋Š” ShapeStyle์ด ํ•„์š”ํ–ˆ๋‹ค.
  • ThemeColors ํŒŒ์ผ์— Foundation ๋Œ€์‹  SwiftUI๋ฅผ importํ•˜๊ณ , color ํ”„๋กœํผํ‹ฐ๋ฅผ ์ถ”๊ฐ€ํ•˜์—ฌ ๊ฐ case์— ๋งž๋Š” Color๋ฅผ ์ง€์ •ํ•ด์ฃผ์—ˆ๋‹ค. ๋˜ํ•œ CaseIterable์„ ์ฑ„ํƒํ•˜์—ฌ ForEach์—์„œ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋„๋ก ํ–ˆ๊ณ , ShapeStyle์„ ์ฑ„ํƒํ•˜์—ฌ background modifier์—์„œ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ–ˆ๋‹ค.
  • themeColor.rawValue๋กœ ์ƒ‰์ƒ์„ ์ง€์ •ํ–ˆ๋˜ ๋ถ€๋ถ„์„ themeColor.color๋กœ ๋ฐ”๊พธ๋Š” ์ž‘์—… ๋“ฑ ๋งŽ์€ ์ฝ”๋“œ๋ฅผ ์ˆ˜์ •ํ•ด์•ผ ํ–ˆ์ง€๋งŒ, ์ด๋ ‡๊ฒŒ ๋Œ€๊ทœ๋ชจ๋กœ ์กฐ์ •ํ•œ ๊ฒฐ๊ณผ OCP์›์น™์„ ๋” ์ž˜ ์ค€์ˆ˜ํ•˜๊ฒŒ ๋˜์—ˆ๋‹ค๊ณ  ์ƒ๊ฐํ•œ๋‹ค.
  • ๊ธฐ์กด์—๋Š” ์ƒˆ๋กœ์šด ์ƒ‰์ƒ์„ ์ถ”๊ฐ€ํ•˜๋ ค๋ฉด Color Set์„ ๋งŒ๋“ค๊ณ , ThemeColors์— ์ƒˆ๋กœ์šด case๋ฅผ ์ถ”๊ฐ€ํ•ด์ค˜์•ผ ํ•˜๋Š” ๋ฐ˜๋ฉด ์ด์ œ๋Š” ์ƒˆ๋กœ์šด ์ƒ‰์ƒ์„ ์ถ”๊ฐ€ํ•˜๋Š” ๊ฒฝ์šฐ ThemeColors์—์„œ case์™€ switch๋งŒ ๊ด€๋ฆฌํ•ด์ฃผ๋ฉด ๋œ๋‹ค.
import SwiftUI

enum ThemeColors: String, ShapeStyle, CaseIterable {
    case pink = "Pink"
    case blue = "Blue"
    case purple = "Purple"
    case indigo = "Indigo"

    var color: Color {
        switch self {
            case .pink:
                return .pink
            case .blue:
                return .blue
            case .purple:
                return .purple
            case .indigo:
                return .indigo
        }
    }
}

16. NavigationBarTitle Color ๊ด€๋ฆฌ

๊ณ ๋ฏผํ•œ ์  :

  • ์•ฑ ์„ค์ •์„ ํ†ตํ•ด ์ƒ‰์ƒ์„ ๋ณ€๊ฒฝํ•˜์ง€ ์•Š๋Š” ์ƒํƒœ์—์„œ๋Š” NavigationBarTitleColor์˜ ์ง€์ •์ด View+Extension์— ์ •์˜ํ•œ ๋ฉ”์„œ๋“œ๋ฅผ ํ†ตํ•ด ์ž˜ ์ด๋ค„์กŒ๋‹ค.
  • ๊ทธ๋Ÿฌ๋‚˜ SettingsView์—์„œ ThemeColor๋ฅผ ์กฐ์ •ํ•˜๊ณ  ๋ณด๋‹ˆ NavigationBarTitleColor๋Š” View๊ฐ€ ์•„์˜ˆ ์ƒˆ๋กœ ๊ทธ๋ ค์ง€๊ธฐ ์ „๊นŒ์ง€ ๋‹ค๋ฅธ Binding๊ฐ’์„ ํ†ตํ•ด ๊ทธ๋ ค์ง€๋Š” ์•„์ดํ…œ๊ณผ๋Š” ๋‹ฌ๋ฆฌ ์ƒ‰์ด ๋ณ€ํ•˜์ง€ ์•Š์•˜๋‹ค.
  • ThemeColor์— ๋”ฐ๋ผ NavigationBarTitle๋„ ์ƒ‰์ด ๋ณ€ํ•˜๊ธธ ์›ํ–ˆ๋‹ค.

๊ณผ์ • ๋ฐ ํ•ด๊ฒฐ :

  • ๊ธฐ์กด NavigationBarTitle์˜ ์ƒ‰์ƒ์€ UIKit์˜ apperance ํ•ญ๋ชฉ์„ ์ด์šฉํ–ˆ๊ธฐ์— ์ข€ ๋” ์œ ๋™์ ์œผ๋กœ, SwiftUI๋‹ต๊ฒŒ ์‚ฌ์šฉํ•˜๊ณ ์ž ์•„์˜ˆ ๋ฐฉํ–ฅ์„ ๋‹ค๋ฅด๊ฒŒ ์ง„ํ–‰ํ–ˆ๋‹ค.
  • View์— NavigationBarTitle๊ณผ ๋™์ผํ•œ ์™ธ์–‘์˜ Text๋ฅผ ๋ณ‘ํ–‰ ๋ฐฐ์น˜ํ•˜์—ฌ ์ด๋ฅผ ํ•ด๊ฒฐํ–ˆ๊ณ , foregroundStyle()์— ThemeColor๋ฅผ ์ „๋‹ฌํ•˜์—ฌ ์•ฑ ์„ค์ •์— ๋งž๊ฒŒ ์œ ๋™์ ์œผ๋กœ ๋ณ€ํ•˜๊ฒŒ ๊ตฌํ˜„ํ–ˆ๋‹ค.
  • ProgramView, SettingsView์—์„œ๋Š” LargeTitle์— ๋งž๋Š” ๋””์ž์ธ์˜ View๋ฅผ ์‚ฌ์šฉ, SearchView์—์„œ๋Š” .searchable modifier๊ฐ€ ์žˆ๊ธฐ ๋•Œ๋ฌธ์— Toolbar๋ฅผ ํ™œ์šฉํ•˜์—ฌ topBarLeading์— title2 font๋กœ ์ง€์ •ํ•œ Text๋ฅผ ๋ฐฐ์น˜ํ–ˆ๋‹ค.
struct NavigationBarLargeTitleView: View {
    // MARK: - Public Properties
    var titleText: String
    var themeColor: ThemeColors

    // MARK: - Body
    var body: some View {
        Text(titleText)
            .font(.largeTitle)
            .bold()
            .foregroundStyle(themeColor.color)
            .padding(.leading)
    }
}
struct NavigationBarSmallTitleView: View {
    // MARK: - Public Properties
    var titleText: String
    var themeColor: ThemeColors

    // MARK: - Body
    var body: some View {
        Text(titleText)
            .font(.title2)
            .bold()
            .foregroundStyle(themeColor.color)
    }
}

17. [SwiftData] No exact matches in call to instance method 'setValue' ์˜ค๋ฅ˜

๊ณ ๋ฏผํ•œ ์  :

  • SwiftData์˜ Model class๋ฅผ ๊ตฌํ˜„ ํ›„ ๋นŒ๋“œ๋ฅผ ์ˆ˜ํ–‰ํ•˜๋‹ˆ ์œ„์™€ ๊ฐ™์€ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ๋‹ค.

๊ณผ์ • ๋ฐ ํ•ด๊ฒฐ :

  • https://developer.apple.com/documentation/swiftdata/preserving-your-apps-model-data-across-launches
  • SwiftData๋Š” ํ˜ธํ™˜ ๊ฐ€๋Šฅํ•œ ์œ ํ˜•์„ ์‚ฌ์šฉํ•˜๋Š” ํ•œ ํด๋ž˜์Šค์˜ ๋ชจ๋“  ๊ณ„์‚ฐ ํ”„๋กœํผํ‹ฐ๊ฐ€ ์•„๋‹Œ ํ”„๋กœํผํ‹ฐ๋ฅผ ํฌํ•จํ•œ๋‹ค.
  • Bool, Int, String๊ณผ ๊ฐ™์€ ๊ธฐ๋ณธ ์œ ํ˜•์€ ๋ฌผ๋ก  structure, enum ๋ฐ Codable ํ”„๋กœํ† ์ฝœ์„ ์ค€์ˆ˜ํ•˜๋Š” ๊ธฐํƒ€ ๊ฐ’ ์œ ํ˜•๊ณผ ๊ฐ™์€ ๋ณต์žกํ•œ ๊ฐ’ ์œ ํ˜•๋„ ์ง€์›ํ•œ๋‹ค.
  • ์ฆ‰, ์ € ์˜ค๋ฅ˜๊ฐ€ ๋‚˜๋Š” ๊ฒƒ์€ ํ•ด๋‹น ํ”„๋กœํผํ‹ฐ๊ฐ€ Codable์„ ์ค€์ˆ˜ํ•˜์ง€ ์•Š์•„ ๋ฐœ์ƒํ•˜๋Š” ๊ฒƒ์ž„์„ ์•Œ ์ˆ˜ ์žˆ์—ˆ๋‹ค.
  • ProgramData๋Š” ์ด์ „๊นŒ์ง€ ์ธ์ฝ”๋”ฉ์„ ํ•˜์ง€ ์•Š์•„ Decodable๋งŒ ์ค€์ˆ˜ํ–ˆ๊ธฐ ๋•Œ๋ฌธ์— Codable๋กœ ๋ณ€๊ฒฝํ•ด์„œ ํ•ด๊ฒฐํ•  ์ˆ˜ ์žˆ์—ˆ๋‹ค.
struct ProgramContentModel: Codable {
    let category: String
    let region: String
    let title: String
    let place: String
    let organization: String
    let target: String
    let fees: String
    let url: String
    let imageURL: String
    let startDate: String
    let endDate: String
    let longitude: String
    let latitude: String
    let isFree: String
}

18. [SwiftData] error: Row (pk = ) for entity 'Model' is missing mandatory text data for property 'property name' ์˜ค๋ฅ˜

๊ณ ๋ฏผํ•œ ์  :

  • setValue ์˜ค๋ฅ˜๋ฅผ ํ•ด๊ฒฐํ•˜๊ณ  ์‹ค์ œ ๋ฐ์ดํ„ฐ๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ SwiftData์—์„œ CRUD ํ…Œ์ŠคํŠธ๋ฅผ ํ•˜๋Š”๋ฐ ์œ„์™€ ๊ฐ™์€ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ•˜๋ฉฐ ๋ฐ์ดํ„ฐ๊ฐ€ ์ œ๋Œ€๋กœ ์ž‘์—…์ด ๋˜์ง€ ์•Š๋Š” ํ˜„์ƒ์ด ๋ฐœ์ƒํ–ˆ๋‹ค.
  • ์‹ค์ œ Model์— ๋ชจ๋“  ํ•ญ๋ชฉ์„ ํ•„์ˆ˜์ ์œผ๋กœ ๋„ฃ์–ด์คฌ๊ณ , ์˜ค๋ฅ˜ ์ƒํ™ฉ์—์„œ๋„ ๋ฐ์ดํ„ฐ๊ฐ€ ๋“ค์–ด๊ฐ”์„ ๋•Œ ๋ชจ๋“  ๊ฐ’์ด ์žˆ์Œ์„ ํ™•์ธํ–ˆ๋‹ค.

๊ณผ์ • ๋ฐ ํ•ด๊ฒฐ :

  • ์ฒ˜์Œ์—๋Š” ์˜ค๋ฅ˜์— property name์ด ๋ช…์‹œ๋˜์–ด ์žˆ์–ด ํ•ด๋‹น ํ”„๋กœํผํ‹ฐ์˜ ์ข…๋ฅ˜๋ฅผ ์ง€์ •ํ•œ enum์˜ ๋ฌธ์ œ๋ผ๊ณ  ํŒ๋‹จํ–ˆ๋‹ค.
  • ๊ทธ๋ž˜์„œ enum์— Codable๋„ ์ ์šฉํ•ด๋ณด๊ณ  ๊ฒ€์ƒ‰๋„ ํ•ด๋ณธ ๊ฒฐ๊ณผ ์•„๋ž˜์™€ ๊ฐ™์€ ์ •๋ณด๋ฅผ ์–ป์„ ์ˆ˜ ์žˆ์—ˆ๋‹ค.
  • https://developer.apple.com/documentation/ios-ipados-release-notes/ios-ipados-17-release-notes
  • iOS & iPadOS 17 Release Notes์˜ SwiftData Resolved Issues๋ฅผ ๋ณด๋ฉด ๋‹ค์Œ์˜ ๋‚ด์—ญ์ด ์žˆ๋‹ค.
  • Fixed: Case value is not stored properly for a string rawvalue enumeration. (108634193)
  • string์„ rawvalue๋กœ ๊ฐ–๋Š” enum์—์„œ Case ๊ฐ’์ด ์ ์ ˆํžˆ ์ €์žฅ๋˜์ง€ ์•Š๋Š” ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ–ˆ๋‹ค๋Š”๋ฐ ์ด๊ฒƒ ๋•Œ๋ฌธ์— ์ด ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ•˜๋Š” ๊ฒƒ์ด๋ผ ์ƒ๊ฐํ–ˆ๋‹ค.
  • ๊ทธ๋Ÿฌ๋‚˜ iOS 17.0์—์„œ ํ•ด๊ฒฐ๋œ ๋ฌธ์ œ์ด๊ณ , ํ˜„์žฌ ๋นŒ๋“œ ์ƒํ™ฉ์€ Xcode 15.3, ์‹œ๋ฎฌ๋ ˆ์ดํ„ฐ iOS 17.4, ํ”„๋กœ์ ํŠธ ์š”๊ตฌ์‚ฌํ•ญ 17.0๋กœ ๋ชจ๋‘ ํ•ด๋‹น ์กฐ๊ฑด์„ ๋งŒ์กฑํ–ˆ๊ธฐ์— ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ•˜๊ธฐ์—” ๋ฌด๋ฆฌ๊ฐ€ ์žˆ์—ˆ๋‹ค.
  • ์ด์™ธ์˜ ๋‹ค๋ฅธ ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ๋ฅผ ์–ป์ง€ ๋ชปํ•ด ์ง„์ „์ด ์—†๋˜ ๋„์ค‘, ํ”„๋กœ์ ํŠธ์˜ ๋ฌธ์ œ์ธ๊ฐ€ ์‹ถ์–ด Xcode์—์„œ ์ƒˆ๋กœ์šด ํ”„๋กœ์ ํŠธ๋ฅผ ๋งŒ๋“ค์–ด๋ณด์•˜๋‹ค.
  • ํ”„๋กœ์ ํŠธ ์„ค์ •์—์„œ SwiftData๋ฅผ ํฌํ•จํ•  ์ˆ˜ ์žˆ๋Š” ์˜ต์…˜์ด ์žˆ์–ด ์ฒดํฌํ•˜์˜€๊ณ  ๊ธฐ๋ณธ ๊ตฌ์กฐ๋ฅผ ๋ฉด๋ฐ€ํžˆ ์‚ดํŽด๋ณธ ๋’ค, ๊ธฐ์กด ํ”„๋กœ์ ํŠธ์ฒ˜๋Ÿผ ๋ชจ๋ธ์„ ๊ตฌ์„ฑํ•ด์„œ ํ…Œ์ŠคํŠธ ํ•ด๋ดค๋‹ค.
  • ์ƒˆ๋กœ์šด stub ํ”„๋กœ์ ํŠธ์—์„œ๋Š” ์ •์ƒ์ ์œผ๋กœ SwiftData๊ฐ€ ์ž‘๋™ํ•˜๊ธฐ์— ์–ด๋–ค ์ฐจ์ด์ ์ด ์žˆ๋Š”๊ฐ€ ๋‹ค์‹œ ์‚ดํŽด๋ณด๋‹ˆ enum์˜ ๋ฌธ์ œ๊ฐ€ ์•„๋‹ˆ์—ˆ๋‹ค. ์• ์ดˆ์— SwiftData์— ๋“ค์–ด๊ฐ€๋Š” ๋ฐ์ดํ„ฐ๋Š” ํŠน์ • enum์ด ์•„๋‹ˆ๋ผ DTO ๊ตฌ์กฐ์ฒด์˜€๋Š”๋ฐ ๋‹จ์ˆœํžˆ ๊ทธ ํ”„๋กœํผํ‹ฐ ์ด๋ฆ„์— ์ง‘์ค‘ํ•˜๋Š” ๋ฐ”๋žŒ์— ๊ทธ๊ฒƒ์„ ์ •์˜ํ•œ enum์ด ๋ฌธ์ œ์ผ ๊ฒƒ์ด๋ผ ์ƒ๊ฐํ•œ ๊ฒƒ์ด๋‹ค.
  • DTO ๊ตฌ์กฐ์ฒด ๋‚ด๋ถ€์— JSON ๋ฐ์ดํ„ฐ๋ฅผ ๋””์ฝ”๋”ฉํ•  ๋•Œ ์—ฐ๊ฒฐํ•ด์ฃผ๊ธฐ ์œ„ํ•ด CodingKey๋ฅผ ์‚ฌ์šฉํ–ˆ๋Š”๋ฐ ์ด๊ฒƒ์ด ๋ฌธ์ œ๊ฐ€ ๋œ ๊ฒƒ์ด๋‹ค.
  • ๋”ฐ๋ผ์„œ API ๋ฐ์ดํ„ฐ๋ฅผ ๋ฐ›์„ ๋•Œ, ๋ฐ์ดํ„ฐ๋ฅผ ๋ฐ›๊ณ  ๋‚˜์„œ ๋””์ฝ”๋”ฉ๋œ DTO ๋ชจ๋ธ์„ CodingKey๊ฐ€ ์—†๋Š” ๋ณ„๋„์˜ ๊ตฌ์กฐ์ฒด์— ๋งคํ•‘์‹œ์ผœ ์ €์žฅํ•จ์œผ๋กœ์จ ํ•ด๊ฒฐํ•  ์ˆ˜ ์žˆ์—ˆ๋‹ค.
struct ProgramContent: Decodable {
    let category: String
    let region: String
    let title: String
    let place: String
    let organization: String
    let target: String
    let fees: String
    let url: String
    let imageURL: String
    let startDate: String
    let endDate: String
    let longitude: String
    let latitude: String
    let isFree: String

    enum CodingKeys: String, CodingKey {
        case category = "CODENAME"
        case region = "GUNAME"
        case title = "TITLE"
        case place = "PLACE"
        case organization = "ORG_NAME"
        case target = "USE_TRGT"
        case fees = "USE_FEE"
        case url = "ORG_LINK"
        case imageURL = "MAIN_IMG"
        case startDate = "STRTDATE"
        case endDate = "END_DATE"
        case longitude = "LAT"
        case latitude = "LOT"
        case isFree = "IS_FREE"
    }
}

/// SwiftData ๋ชจ๋ธ์—์„œ๋Š” CodingKey ์š”์†Œ๊ฐ€ ์˜ค๋ฅ˜๋ฅผ ์œ ๋ฐœํ•˜์—ฌ ๋ณ„๋„์˜ ๋งค์นญ ๊ตฌ์กฐ์ฒด ์‚ฌ์šฉ
struct ProgramContentModel: Codable {
    let category: String
    let region: String
    let title: String
    let place: String
    let organization: String
    let target: String
    let fees: String
    let url: String
    let imageURL: String
    let startDate: String
    let endDate: String
    let longitude: String
    let latitude: String
    let isFree: String
}
programCancellable = URLSession.shared
    .dataTaskPublisher(for: url)
    .subscribe(on: DispatchQueue.global())
    .map(\.data)
    .decode(type: ProgramData.self, decoder: JSONDecoder())
    .receive(on: DispatchQueue.main)
    .sink { completion in
        print(completion)
    } receiveValue: { [weak self] value in
        self?.transformDTO(from: value.programInfo.programContents)
    }
private func transformDTO(from contents: [ProgramContent]) {
    let today = Date().getStringOfTodayDate()

    self.contents = contents
        .filter { $0.endDate > today }
        .sorted { $0.startDate < $1.startDate }
        .map { content in
            ProgramContentModel(
                category: content.category,
                region: content.region,
                title: content.title,
                place: content.place,
                organization: content.organization,
                target: content.target,
                fees: content.fees,
                url: content.url,
                imageURL: content.imageURL,
                startDate: content.startDate,
                endDate: content.endDate,
                longitude: content.longitude,
                latitude: content.latitude,
                isFree: content.isFree
            )
        }
}

19. [WebKit] target is not running or doesn't have entitlement com.apple.runningboard.assertions.webkit / acquireSync Failed to acquire RBS assertion 'XPCConnectionTerminationWatchdog' for process with PID= ์˜ค๋ฅ˜

๊ณ ๋ฏผํ•œ ์  :

  • ํ”„๋กœ์ ํŠธ๋ฅผ ์‹œ๋ฎฌ๋ ˆ์ดํ„ฐ์—์„œ ํ…Œ์ŠคํŠธํ•  ๋•Œ ๋กœ๊ทธ์ฐฝ์— ์œ„์™€ ๊ฐ™์€ ๋ฉ”์‹œ์ง€๊ฐ€ ์ง€์†์ ์œผ๋กœ ์ถœ๋ ฅ๋˜์—ˆ๋‹ค. ์–ด๋–ค ๋ฌธ์ œ์ธ์ง€ ์•Œ๊ณ  ํ•ด๊ฒฐํ•˜๊ณ ์ž ํ–ˆ๋‹ค.

๊ณผ์ • ๋ฐ ํ•ด๊ฒฐ :

  • ๊ด€๋ จํ•œ ์ •๋ณด๊ฐ€ ๋งŽ์ด ์žˆ์ง€ ์•Š์•˜์ง€๋งŒ Apple ํฌ๋Ÿผ์—์„œ ๋‹ต์„ ์–ป์„ ์ˆ˜ ์žˆ์—ˆ๋‹ค.
  • https://developer.apple.com/forums/thread/742739
  • 2์ฃผ ์ „(24. 03. 11. ๊ธฐ์ค€)์— ์˜ฌ๋ผ์˜จ ํ•ด๋‹น ๋‹ต๋ณ€์— ๋”ฐ๋ฅด๋ฉด Apple์— ํ”ผ๋“œ๋ฐฑ์„ ์˜ฌ๋ฆฐ ๊ฒฐ๊ณผ ์—๋Ÿฌ๊ฐ€ ์•„๋‹Œ ๋‹จ์ˆœ ๋กœ๊ทธ ๋ฉ”์‹œ์ง€์ด๊ณ , ์ดํ›„ ์—…๋ฐ์ดํŠธ์—์„œ ์ œ๊ฑฐ๋œ๋‹ค๊ณ  ์‘๋‹ต์„ ๋ฐ›์•˜๋‹ค๊ณ  ํ•œ๋‹ค.
  • ๋นŒ๋“œ ํ™˜๊ฒฝ(Xcode 15.3) ๊ธฐ์ค€ ์•„์ง ํ•ด๋‹น ๋ฉ”์„ธ์ง€๋Š” ์ถœ๋ ฅ๋˜๊ณ  ์žˆ๋‹ค

20. Toggle์˜ isOn ๊ฐ’๊ณผ ๊ธฐ๋ณธ ์„ค์ • ์•ฑ ์ƒํƒœ์˜ ๋ฐ”์ธ๋”ฉ

๊ณ ๋ฏผํ•œ ์  :

  • ์•Œ๋ฆผ ํ† ๊ธ€์˜ ๊ฐ’์„ ์•ฑ ๋‚ด์—์„œ ๋ณ€๊ฒฝํ•˜๋Š” ๊ฒƒ์ด ์•„๋‹ˆ๋ผ ๊ธฐ์กด ์„œ๋น„์Šค(์Šคํƒ€๋ฒ…์Šค, ์ด๋””์•ผ ๋“ฑ)์ฒ˜๋Ÿผ ๋””๋ฐ”์ด์Šค ์•Œ๋ฆผ ์„ค์ •์„ ๋ฐ˜์˜ํ•˜๊ณ ์ž ํ–ˆ๋‹ค.
  • ๊ทธ๋Ÿฌ๋‚˜ ์„ค์ • ์•ฑ์œผ๋กœ ๊ฐ”๋‹ค๊ฐ€ ๋Œ์•„์™”์„ ๋•Œ ์ƒํƒœ๊ฐ€ ์‹ค์‹œ๊ฐ„ ๋ฐ˜์˜์ด ๋˜์ง€ ์•Š์Œ์„ ํ™•์ธํ–ˆ๋‹ค.

๊ณผ์ • ๋ฐ ํ•ด๊ฒฐ :

  • UNUserNotificationCenter์—์„œ getNotificationSettings ๋ฉ”์„œ๋“œ๋ฅผ ํ˜ธ์ถœํ•˜์—ฌ ๊ฐ’์„ ๊ฐ€์ ธ์˜ค๋Š” ๊ฒƒ์€ ์‹ค์‹œ๊ฐ„์œผ๋กœ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์—†์—ˆ๋‹ค.
  • ๊ธฐ์กด์—๋Š” NotificationManager์˜ Publish ๊ฐ’์„ ํ† ๊ธ€์— Binding์„ ํ–ˆ์œผ๋‚˜ ํ† ๊ธ€๋กœ Manager์˜ ๊ฐ’์„ ๋ณ€๊ฒฝํ•˜๋ฉด ์•ˆ๋๊ธฐ์— Binding ๋Œ€์‹  ๋ณ„๋„์˜ State ํ”„๋กœํผํ‹ฐ๋กœ ๋งŒ๋“ค๊ณ , Manager์˜ ๊ฐ’์„ ๋Œ€์ž…ํ•˜๋Š” ์‹์œผ๋กœ ์ง„ํ–‰์„ ํ–ˆ์—ˆ๋‹ค.
  • ์ดํ›„ ์ž‘์—…์— ๋”ฐ๋ผ Manager์—์„œ ํ˜„์žฌ ๊ถŒํ•œ ์ƒํƒœ๋ฅผ ํ™•์ธํ•˜๋Š” ๋ฉ”์„œ๋“œ๋ฅผ ํ˜ธ์ถœํ•˜์—ฌ Manager์˜ status ๊ฐ’์„ ๋ฐ”๊พธ๊ณ , ๊ทธ ๊ฐ’์„ ๋‹ค์‹œ ํ† ๊ธ€ ์ƒํƒœ ๊ฐ’์œผ๋กœ ๋„˜๊ฒจ์ฃผ๋Š” ๋ฐฉ์‹์„ ์‚ฌ์šฉํ•œ ๊ฒƒ์ด๋‹ค.
  • ๊ทธ๋Ÿฌ๋‚˜ onReceive์™€ Combine์„ ์‚ฌ์šฉํ•ด์„œ ์•ฑ์ด ๋ฐฑ๊ทธ๋ผ์šด๋“œ์—์„œ ํฌ๊ทธ๋ผ์šด๋“œ๋กœ ๋„˜์–ด์˜ค๋Š” ๊ฒƒ์„ ํ™•์ธํ•˜๊ณ  ๋ฉ”์„œ๋“œ๋ฅผ ํ˜ธ์ถœํ–ˆ์Œ์—๋„ ๋‹น์žฅ ๋ฐ”๋€Œ์ง€ ์•Š๋Š” ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ–ˆ๋‹ค(์ง€์—ฐ ๋ณ€๊ฒฝ).
  • ๊ฐ™์€ ๋ฌธ์ œ๋ฅผ ๊ณ„์† ๋ณด๋‹ค๋ณด๋‹ˆ ๋ฌธ์ œ์ ์„ ์‰ฝ๊ฒŒ ๋ฐœ๊ฒฌํ•  ์ˆ˜ ์—†์—ˆ๊ณ , ์ž ์‹œ ํœด์‹์„ ๊ฐ€์ง€๋ฉฐ ์ƒ๊ฐ์„ ์ •๋ฆฌํ•˜๊ณ  ๋‹ค์‹œ ๋ณด๋‹ˆ ๋กœ์ง์˜ ๋ฌธ์ œ๋ฅผ ๋ฐœ๊ฒฌํ•  ์ˆ˜ ์žˆ์—ˆ๋‹ค.
  • ์• ์ดˆ์— ๋ฐ”์ธ๋”ฉํ•˜์ง€ ์•Š์„ ๊ฒƒ์ด๋ผ๋ฉด Manager์—์„œ Publish ํ•˜๋Š” ์ด์œ ๋„ ์—†์„ ๋ฟ๋”๋Ÿฌ Binding ํ•ด์„œ ๊ฐ’์„ ๋ณ€๊ฒฝ์‹œํ‚ค๋”๋ผ๋„ ๋Œ๋ ค๋†“์œผ๋ฉด ๋˜๋Š” ๊ฒƒ์ด๋‹ค.
  • ์ดํ›„ ํ† ๊ธ€ ๊ฐ’์„ Publish ํ”„๋กœํผํ‹ฐ์— ๋ฐ”์ธ๋”ฉํ•œ ํ›„, Alert์—์„œ ๋‹ซ๊ธฐ๋ฅผ ๋ˆ„๋ฅด๋ฉด ํ† ๊ธ€ ๊ฐ’์„ ๋‹ค์‹œ ์›์œ„์น˜๋กœ ๋Œ๋ ค๋†“๊ณ , ์„ค์ • ์ฐฝ์œผ๋กœ ์ด๋™ํ•œ๋‹ค๋ฉด .onReceive๋ฅผ ํ™œ์šฉํ•˜์—ฌ ๋‹ค์‹œ ์•ฑ์ด Foreground๋กœ ๋Œ์•„์˜ฌ ๋•Œ notificationManager์˜ ์ƒํƒœ ํ™•์ธ ๋ฉ”์„œ๋“œ๋ฅผ ํ˜ธ์ถœํ•˜๋Š” ๋ฐฉ์‹์„ ์‚ฌ์šฉํ–ˆ๋‹ค. ๋ฉ”์„œ๋“œ๊ฐ€ ์ƒํƒœ๋ฅผ ํ™•์ธํ•˜๊ณ  Publish ํ”„๋กœํผํ‹ฐ์— ๋ฐ˜์˜ํ•˜๋ฉด, ๊ทธ๊ฒƒ์„ Bindingํ•œ ํ† ๊ธ€์˜ ๊ฐ’๋„ ์ž๋™์œผ๋กœ ์—…๋ฐ์ดํŠธ๊ฐ€ ๋˜๋Š” ๊ฒƒ์ด๋‹ค. ์ด๋ ‡๊ฒŒ ๋ณ€๊ฒฝํ•œ ์ดํ›„ ์„ค์ • ์•ฑ์—์„œ ๋ณ€๊ฒฝํ•œ ๊ฐ’์— ๋งž๊ฒŒ ์‹ค์‹œ๊ฐ„์œผ๋กœ ๋ฐ˜์˜์ด ๋˜์—ˆ๋‹ค.
.onReceive(NotificationCenter.default.publisher(for: UIApplication.willEnterForegroundNotification)) { _ in
    DispatchQueue.main.async {
        notificationManager.setNotificationStatus()
    }
}
.onTapGesture {
    isAlertPresented.toggle()
}
.alert(isPresented: $isAlertPresented) {
    Alert(
        title: Text("์•Œ๋ฆผ ์„ค์ • ๋ณ€๊ฒฝ ์•ˆ๋‚ด"),
        message: Text("์•Œ๋ฆผ ์„ค์ • ๋ณ€๊ฒฝ์€ ์„ค์ • ์•ฑ์—์„œ ํ•  ์ˆ˜ ์žˆ์–ด์š”."),
        primaryButton: .default(Text("์„ค์ •"), action: {
            guard let settingsURL = URL(string: UIApplication.openSettingsURLString) else { return }

            UIApplication.shared.open(settingsURL)
        }),
        secondaryButton: .cancel(Text("๋‹ซ๊ธฐ"), action: {
            DispatchQueue.main.async {
                isToggleOn.toggle()
            }
        }))
}

21. ํ•œ๊ตญ ์‹œ๊ฐ ๊ตฌํ•˜๊ธฐ

๊ณ ๋ฏผํ•œ ์  :

  • ํ”„๋กœ๊ทธ๋žจ์˜ ์‹œ์ž‘์ผ๊ณผ ์˜ค๋Š˜ ๋‚ ์งœ๋ฅผ ๋น„๊ตํ•˜๊ณ ์ž ํ•˜๋Š”๋ฐ, ๊ธฐ์ค€ ์‹œ์ (UTC vs KST)์ด ๋‹ฌ๋ผ ๋งž์ถฐ์ฃผ๊ณ ์ž ํ–ˆ๋‹ค.

๊ณผ์ • ๋ฐ ํ•ด๊ฒฐ :

  • DateFormatter์— Locale, TimeZone์„ ๋‹ค์–‘ํ•œ ๊ฐ’์œผ๋กœ ๋ณ€๊ฒฝํ•ด๋„, String์œผ๋กœ๋Š” ์ •์ƒ์ ์œผ๋กœ ํ•œ๊ตญ ์‹œ๊ฐ์ด ๋‚˜์˜ค๋Š” ๋ฐ˜๋ฉด Date ๊ฐ’์œผ๋กœ๋Š” UTC๋งŒ ๋‚˜์˜ค๋Š” ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ–ˆ๋‹ค.
  • ์–ด๋– ํ•œ ๊ฐ’์„ ๋ฐ˜์˜ํ•ด๋„ ์›ํ•˜๋Š” ๊ฐ’์ด ๋‚˜์˜ค์ง€ ์•Š์•„ addingTimeInterval์„ ํ†ตํ•ด 9์‹œ๊ฐ„์„ ๋”ํ•ด ํ•ด๊ฒฐํ–ˆ๋‹ค.
extension Date {
    func getKSTDate() -> Date {
        return self.addingTimeInterval(60 * 60 * 9)
    }
}

22. API ํ˜ธ์ถœ ๋กœ์ง ๋™์‹œ ์‚ฌ์šฉ ๋ฌธ์ œ

๊ณ ๋ฏผํ•œ ์  :

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

๊ณผ์ • ๋ฐ ํ•ด๊ฒฐ :

  • dataTaskPublisher๋ฅผ ์—ฌ๋Ÿฌ ๋ฒˆ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์€ ๋ถˆํ•„์š”ํ•œ ์ž‘์—…์ด๋ผ๊ณ  ์—ฌ๊ฒจ์ ธ์„œ Combine์—์„œ ์ œ๊ณตํ•˜๋Š” ๋ฉ”์„œ๋“œ๋ฅผ ์ฐพ์•„๋ณด๊ฒŒ ๋˜์—ˆ๋‹ค.
  • CombineLatest, Merge๊ฐ€ ์ด๋Ÿฐ ์ž‘์—…์— ์ ํ•ฉํ–ˆ์ง€๋งŒ, ๊ฐ๊ฐ ๊ฐœ์ˆ˜๊ฐ€ ์ •ํ•ด์ ธ ์žˆ๋‹ค๋ฉด ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๊ฒ ์œผ๋‚˜ ์ด ํ”„๋กœ์ ํŠธ์—์„œ API ํ˜ธ์ถœ ํšŸ์ˆ˜๋Š” ์ฒ˜์Œ ํ…Œ์ŠคํŠธ ์ธ๋ฑ์Šค๋ฅผ ์ „์†กํ•ด ๋ฐ›์€ ์ „์ฒด ๋ฐ์ดํ„ฐ ๊ฐœ์ˆ˜์— ๋น„๋ก€ํ•˜๊ธฐ ๋•Œ๋ฌธ์— ์ ์šฉํ•˜๊ธฐ ์–ด๋ ค์› ๋‹ค.
  • ๋‹ค์Œ์œผ๋กœ ์ฐพ์€ ๋ฉ”์„œ๋“œ๋Š” MergeMany์˜€๋‹ค. ์ด๊ฒƒ์€ publisher์˜ ๋ฐฐ์—ด์„ ๋ฐ›์•„ ๋ณ‘ํ•ฉํ•ด์ฃผ๋Š” ๊ฒƒ์œผ๋กœ ์œ„ ๋‘ ๋ฉ”์„œ๋“œ์™€ ๋‹ค๋ฅด๊ฒŒ ๊ฐœ์ˆ˜๋ฅผ ๋ชฐ๋ผ๋„ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์—ˆ๋‹ค.
  • Publishers.MergeMany
  • ๊ทธ๋Ÿฌ๋‚˜ ๊ณต์‹๋ฌธ์„œ์˜ ์ •๋ณด๋Š” ํ•œ๊ณ„๊ฐ€ ์žˆ์—ˆ๊ณ , ์‹ค์‚ฌ์šฉ ์˜ˆ์ œ๋ฅผ ๋ด๋„ ๋‹ค ์‚ฌ์šฉํ•˜๋Š” ์ผ€์ด์Šค๊ฐ€ ๋‹ฌ๋ผ ๊ทธ๊ฒƒ๋“ค์„ ๊ด€ํ†ตํ•˜๋Š” ์ •๋ณด๋ฅผ ์–ป๊ธฐ ์–ด๋ ค์› ๋‹ค.
  • ๊ทธ๋ž˜์„œ ์šฐ์„  ํ•„์š”ํ•œ ๊ฒƒ์„ ํ•˜๋‚˜ํ•˜๋‚˜ ์ •๋ฆฌํ–ˆ๋‹ค. MergeMany์— ์ „๋‹ฌํ•  publisher ๋ฐฐ์—ด์„ ๋งŒ๋“ค์—ˆ๊ณ , ๊ทธ๊ฒƒ์— ๋“ค์–ด๊ฐˆ ํƒ€์ž…์„ ๊ฒฐ์ •ํ•ด์•ผํ–ˆ๋‹ค.
  • ์ด์ „์— ํ˜ธ์ถœํ•  ๋•Œ๋Š” AnyCancellable ํƒ€์ž…์˜ ๋ณ€์ˆ˜์— ์ €์žฅํ•ด ํ•„์š”์‹œ cancelํ•˜๋Š” ์‹์œผ๋กœ ํ–ˆ์ง€๋งŒ AnyCancellable์€ ์ด ์ƒํ™ฉ์—์„œ ๋งž์ง€ ์•Š๋Š” ํƒ€์ž…์ด๋ผ๊ณ  ์—ฌ๊ฒผ๊ณ (MergeMany์—์„œ ๋ฐ์ดํ„ฐ ํ•ธ๋“ค๋ง์ด ๋ถˆ๊ฐ€, ๊ณ ๋ฏผ ๋์— URLSession.DataTaskPublisher ํƒ€์ž…์„ ๋„ฃ๊ฒŒ ๋˜์—ˆ๋‹ค.
  • ์ดํ›„ ์ƒ์„ฑํ•ด์•ผ ํ•˜๋Š” ํšŸ์ˆ˜๋ฅผ ๊ตฌํ•œ ๋’ค, for๋ฌธ์„ ์‚ฌ์šฉํ•˜์—ฌ ํ˜ธ์ถœํ•˜๋Š” ํšŸ์ˆ˜์— ๋น„๋ก€ํ•œ ์ธ๋ฑ์Šค URL์„ ์ง€๋‹Œ DataTaskPublisher๋ฅผ ์ƒ์„ฑํ•˜๊ณ  ๋ฐฐ์—ด์— ์ถ”๊ฐ€ํ–ˆ๋‹ค.
  • ๊ทธ ๋’ค, Publishers.MergeMany์— ๋ฐฐ์—ด์„ ์ „๋‹ฌํ•˜๊ณ , ๊ธฐ์กด์— dataTaskPublisher์—์„œ ํ–ˆ๋˜ ๊ฒƒ์ฒ˜๋Ÿผ ์Šค๋ ˆ๋“œ ์„ค์ •๊ณผ ๋””์ฝ”๋”ฉ ํƒ€์ž… ์„ค์ •, sink/receiveValue ๊ด€๋ จ ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•˜์˜€๋‹ค.
  • ์ด ๊ณผ์ •์œผ๋กœ ๋„˜์–ด์˜ค๊ธฐ๊นŒ์ง€ ์˜ค๋žœ ๊ณ ๋ฏผ๊ณผ ๋งŽ์€ ํ…Œ์ŠคํŠธ๋ฅผ ํ–ˆ์—ˆ๋Š”๋ฐ, ๋ธŒ๋ ˆ์ดํฌ ํฌ์ธํŠธ๋ฅผ ์ฐ์–ด ํ•˜๋‚˜ํ•˜๋‚˜ ์‚ดํŽด๋ณธ ๊ฒฐ๊ณผ, MergeMany์—์„œ ํ•˜๋‹จ์˜ ์ž‘์—…์„ ๋ฐ˜๋ณตํ•˜๋Š” ๊ฒƒ์„ ์•Œ๊ฒŒ ๋˜์—ˆ๊ณ  ๊ทธ๋ ‡๋‹ค๋ฉด ๊ธฐ์กด dataTaskPublisher๋ฅผ ์ถ”์ƒํ™” ํ•˜๋Š” ๊ฒƒ๊ณผ ๊ฐ™์ด ์‚ฌ์šฉํ•˜๋ฉด ๋˜๊ฒ ๋‹ค๋Š” ํŒ๋‹จ์„ ํ•  ์ˆ˜ ์žˆ์—ˆ๋‹ค. URL์„ ์„ค์ •ํ•˜๋Š” ๋ถ€๋ถ„๊นŒ์ง€๋ฅผ ๋ถ„๋ฆฌํ•˜์—ฌ publishers ๋ฐฐ์—ด์— ์ถ”๊ฐ€ํ–ˆ๊ณ , MergeMany์—์„œ์˜ ๊ณตํ†ต ์ž‘์—…์— ๊ฐ dataTaskPublisher์— ์ ์šฉํ•  ๊ฐ์ข… ์ž‘์—…๋“ค์„ ๋„ฃ๋Š” ๊ฒƒ์ด ๊ทธ๊ฒƒ์ด๋‹ค.
  • ๊ทธ๋ฆฌํ•˜์—ฌ ๊ฐ publisher๋Š” MergeMany์—์„œ ์ •ํ•ด์ง„ ์Šค๋ ˆ๋“œ์™€ ๋””์ฝ”๋”ฉ, ๋ฐ์ดํ„ฐ ํ•ธ๋“ค๋ง ๋ฐฉ์‹์— ๋”ฐ๋ผ ๋ฐ์ดํ„ฐ๋ฅผ ์ฒ˜๋ฆฌํ•˜๊ฒŒ ๋œ๋‹ค. ๋ชจ๋“  publisher์— ๋Œ€ํ•ด์„œ ์ด ๊ณผ์ •์ด ๋๋‚˜๋ฉด, ๊ฐ€๊ณต๋˜์ง€ ์•Š์€ ๋ฐ์ดํ„ฐ๋ฅผ ์ €์žฅํ•œ rawContents ํ”„๋กœํผํ‹ฐ์—์„œ๋Š” totalCount์™€ ๋น„๊ตํ•˜์—ฌ ์ˆ˜๊ฐ€ ๋™์ผํ•  ์‹œ ๊ธฐ์กด์˜ transformDTO์— ๋ฐ์ดํ„ฐ๋ฅผ ๋„˜๊ฒจ ๊ธฐ์กด์ฒ˜๋Ÿผ ๋ฐ์ดํ„ฐ๋ฅผ ํ›„๊ฐ€๊ณตํ•  ์ˆ˜ ์žˆ๊ฒŒ ๋œ๋‹ค.
  • ๋‹คํ–‰์ด์—ˆ๋˜ ๊ฒƒ์€ ๊ธฐ์กด ๋ฉ”์„œ๋“œ๊ฐ€ ์ž˜ ๋ถ„๋ฆฌ๊ฐ€ ๋˜์–ด ์žˆ์–ด์„œ 1000๊ฐœ ๋‹จ์ผ ํ˜ธ์ถœ์—์„œ n๊ฐœ n๋ฒˆ ํ˜ธ์ถœ๋กœ ๋กœ์ง์ด ๋ณ€๊ฒฝ๋˜์—ˆ์Œ์—๋„ ๋ถˆ๊ตฌํ•˜๊ณ  ํ•˜๋‚˜์˜ ๋ฉ”์„œ๋“œ๋งŒ ์ถ”๊ฐ€ํ•  ๋ฟ ๊ธฐ์กด ํ”„๋กœํผํ‹ฐ๋‚˜ ๋กœ์ง์€ ๋ณ€๊ฒฝํ•  ํ•„์š”๊ฐ€ ์—†์—ˆ๋‹ค๋Š” ์ ์ด๋‹ค.
/// API์—์„œ ๋ฐ›์•„์™€์•ผ ํ•˜๋Š” ์ „์ฒด ๋ฐ์ดํ„ฐ ์ˆ˜
private var totalCount = 0
/// API์—์„œ ๋ฐ›์•„์˜จ ์ „์ฒด ๋ฐ์ดํ„ฐ. totalCount์™€ ์ˆ˜๊ฐ€ ๋งž์„ ๋•Œ transformDTO(from:)์— ๋ฐ์ดํ„ฐ ์ „๋‹ฌ
private var rawContents = [ProgramContent]() {
    didSet {
        if rawContents.count == totalCount {
            transformDTO(from: rawContents)
        }
    }
}

/// Publishers.MergeMany๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์œ ๋™์ ์ธ ํšŸ์ˆ˜๋งŒํผ API๋ฅผ ํ˜ธ์ถœํ•˜๋Š” ๋ฉ”์„œ๋“œ. ์ „๋‹ฌ๋ฐ›์€ ์ˆ˜์— ๋น„๋ก€ํ•œ Publisher๋ฅผ ์ƒ์„ฑํ•˜๊ณ  MergeMany๋กœ ๊ฒฐํ•ฉํ•˜์—ฌ ์ „์ฒด ๋ฐ์ดํ„ฐ ํ•ธ๋“ค๋ง
private func getTotalContents(of amount: Int) {
    let count = (amount + 999) / 1000

    var publishers = [URLSession.DataTaskPublisher]()

    /*
     API ํ˜ธ์ถœ ์‹œ ์ธ๋ฑ์Šค ๋ณ€ํ™” ๋กœ์ง
     1ํšŒ(0): 1...1000 = 0 * 1000 + 1...0 * 1000 + 1000
     2ํšŒ(1): 1001...2000 = 1 * 1000 + 1...1 * 1000 + 1000
     3ํšŒ(2): 2001...3000 = 2 * 1000 + 1...2 * 1000 + 1000

     => nํšŒ(n): (n * 1000) + 1 ~ (n * 1000) + 1000
     */

    for index in 0..<count {
        guard let url = makeURL(startIndex: (index * 1000) + 1, endIndex: (index * 1000) + 1000) else { return }

        let publisher = URLSession.shared
            .dataTaskPublisher(for: url)

        publishers.append(publisher)
    }

    programCancellable = Publishers.MergeMany(publishers)
        .subscribe(on: DispatchQueue.global())
        .map(\.data)
        .decode(type: ProgramData.self, decoder: JSONDecoder())
        .receive(on: DispatchQueue.main)
        .sink { completion in
            print(completion)
        } receiveValue: { value in
            self.rawContents.append(contentsOf: value.programInfo.programContents)
        }
}

23. UIResponsiveness Issue

๊ณ ๋ฏผํ•œ ์  :

  • CalendarView์—์„œ ๋‚ ์งœ๋ณ„ ํ”„๋กœ๊ทธ๋žจ ๋ชฉ๋ก๊ณผ ํ•จ๊ป˜ ์ฆ๊ฒจ์ฐพ๊ธฐ ์—ฌ๋ถ€๋ฅผ ๋ณด์—ฌ์ฃผ๊ธฐ ์œ„ํ•ด SwiftData์— ์ €์žฅํ•œ ํ•ญ๋ชฉ์„ ์‚ฌ์šฉํ–ˆ๋Š”๋ฐ, ๊ทธ ํ™”๋ฉด์—์„œ ProgramDetailView๋กœ ์ง„์ž…ํ•˜๋ฉด CPU, ๋ฉ”๋ชจ๋ฆฌ ๋ฆฌ์†Œ์Šค๋ฅผ ๊ณผ๋„ํ•˜๊ฒŒ ์‚ฌ์šฉํ•˜๋Š” ์ด์Šˆ ๋ฐœ์ƒ troubleshooting

๊ณผ์ • ๋ฐ ํ•ด๊ฒฐ :

  • ์—ฌ๋Ÿฌ ๋ฒˆ ์‹œ๋„ํ•œ ๊ฒฐ๊ณผ, SwiftData๋ฅผ ์ƒ์œ„ View์ธ CalendarView์—์„œ ์‚ฌ์šฉํ•˜๊ณ , ํ•˜์œ„ View์ธ ProgramOfTheDayView์—์„œ๋Š” ์ „๋‹ฌํ•œ ์ฟผ๋ฆฌ ๊ฐ’์œผ๋กœ ์ž‘์—…์„ ์ˆ˜ํ–‰ํ•˜๋‹ˆ ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•  ์ˆ˜ ์žˆ์—ˆ๋‹ค.
  • ์ด์Šˆ๋ฅผ ๋ณด์—ฌ์ค€ ๊ตฌ๋ฌธ์€ WebView์˜€๊ณ , ์›์ธ์€ SwiftData ์ฝ”๋“œ์˜€์œผ๋‚˜ ์ด๋งˆ์ €๋„ ์‹œ๋„ํ•  ๋•Œ๋งˆ๋‹ค ์˜ค๋ฅ˜ ๋ฐœ์ƒ ์—ฌ๋ถ€๊ฐ€ ๊ณ ์ •์ ์ด์ง€ ์•Š์•˜๊ณ , Instruments, Xcode Debugger๋„ ์ „๋ถ€ ํ”„๋กœ์„ธ์Šค๊ฐ€ ๋ป—์–ด ์›์ธ์„ ์ •ํ™•ํžˆ ์ฐพ์ง€ ๋ชปํ•œ ๊ฒƒ์ด ์•„์‰ฌ์› ๋‹ค.
  • ๋‹ค๋งŒ SwiftData์˜ Query๋ฅผ ์กฐ๊ธˆ ๋‹ค๋ฅด๊ฒŒ ์‚ฌ์šฉํ–ˆ๋”๋‹ˆ ํ•ด๊ฒฐ๋œ ๊ฒƒ์œผ๋กœ ๋ณด์•„ subview์™€ ํ•จ๊ป˜ ์“ธ ๋•Œ ์–ด๋–ค ์ž‘์šฉ์„ ํ•˜๋Š”์ง€ ์ข€ ๋” ์•Œ์•„๋ณผ ํ•„์š”์„ฑ์„ ๋А๊ผˆ๋‹ค.

24. Tap to Root

๊ณ ๋ฏผํ•œ ์  :

  • ์• ํ”Œ ๊ธฐ๋ณธ ์•ฑ์ฒ˜๋Ÿผ ํƒญ๋ฐ”๋ฅผ ํ„ฐ์น˜ํ•˜์—ฌ ์ด์ „ ํ™”๋ฉด์œผ๋กœ ๋Œ์•„๊ฐ€๊ณ  ์‹ถ์œผ๋‚˜ ๊ธฐ๋ณธ์œผ๋กœ ์ œ๊ณตํ•˜๋Š” ๊ธฐ๋Šฅ์ด ์—†๊ณ , ๊ธฐ์กด์— ์‚ฌ์šฉ๋˜๋˜ ๋ฐฉ์‹๋„ ๋ณต์žกํ•˜๊ฑฐ๋‚˜ ์ ์šฉ๋˜์ง€ ์•Š์•„ ์ด์šฉ์‹œ ๋ถˆํŽธํ•จ์ด ๋”ฐ๋ฆ„
  • NavigationStack๊ณผ ํ•จ๊ป˜ ๋‚˜์˜จ NavigationPath๋ฅผ ์‚ฌ์šฉํ•ด๋ณด๊ณ ์ž ํ–ˆ์œผ๋‚˜ ์ค‘๋ณต ํƒ€์ž… ์„ค์ •์˜ ๋ฌธ์ œ ๋“ฑ ์ œ๋Œ€๋กœ ์‚ฌ์šฉํ•˜๊ธฐ๊ฐ€ ์–ด๋ ค์› ์Œ

๊ณผ์ • ๋ฐ ํ•ด๊ฒฐ :

  • ๋ฉ”์„œ๋“œ๋‚˜ ๊ธฐ๊ตฌํ˜„ ์š”์†Œ๋ณด๋‹ค๋Š” ์›๋ฆฌ๋ฅผ ๋”ฐ๋ผ๊ฐ€๊ณ ์ž ํ–ˆ๋‹ค. ๊ฒฐ๊ตญ ์Šคํƒ์ด ์Œ“์ด๋Š” ๋ฐฐ์—ด์ด ํ•„์š”ํ•˜๊ณ , ํƒญ๋ฐ”๋ฅผ ๋ˆ„๋ฅด๋ฉด ๊ทธ ๋ฐฐ์—ด์„ ์ดˆ๊ธฐํ™” ํ•ด์•ผ ํ•œ๋‹ค.
  • enum์œผ๋กœ ์ด๋™ํ•  View์˜ ํƒ€์ž…์„ caseํ™” ํ–ˆ๊ณ , NavigationPath ๋Œ€์‹  ํ•ด๋‹น enum์„ ์š”์†Œ๋กœ ๊ฐ–๋Š” ๋ฐฐ์—ด์„ ํ™œ์šฉํ–ˆ๋‹ค.
  • ๊ฐ NavigationStack์„ ๊ฐ–๋Š” 3๊ฐœ์˜ ๋ฉ”์ธ ํ™”๋ฉด์—์„œ ๊ฐ path ๋ฐฐ์—ด์„ path๋กœ ์‚ฌ์šฉํ–ˆ๊ณ , ๊ธฐ์กด์˜ Destination์„ ํฌํ•จํ•œ NavigationLink ๋Œ€์‹  ์–ด๋””๋กœ ๊ฐˆ ๊ฒƒ์ธ์ง€ DestinationPath value๋งŒ์„ ๊ฐ€์ง€๋Š” NavigationLink๋ฅผ ์‚ฌ์šฉํ–ˆ๋‹ค.
  • ์ดํ›„ ํ•ด๋‹น Link๋ฅผ ์ ์ ˆํžˆ ์ด์šฉํ•˜๊ธฐ ์œ„ํ•ด navigationDestination์—์„œ Path์— ๋”ฐ๋ผ View๋ฅผ ์—ฐ๊ฒฐ์‹œ์ผœ์คฌ๋‹ค.
  • ์ตœ์ƒ๋‹จ TabView์—์„œ๋Š” ํ™œ์„ฑํ™” ๋˜์–ด ์žˆ๋Š” ํƒญ์—์„œ ๋‹ค์‹œ ํ•œ ๋ฒˆ ํƒญ์„ ํ•˜๋ฉด path ๋ฐฐ์—ด์ด ๋น„์–ด์žˆ๋Š”์ง€ ํ™•์ธํ•˜๊ณ , ๊ทธ๋ ‡์ง€ ์•Š๋‹ค๋ฉด ๋น„์šฐ๋Š” ๋ฐฉ์‹์œผ๋กœ ํ•ด๊ฒฐํ•  ์ˆ˜ ์žˆ์—ˆ๋‹ค.
enum DestinationPath: Hashable {
    case detail(ProgramContentModel), calendar, settings
}

// MARK: - @State Properties
@State private var selectedTab: Tab = .program
@State private var programPath: [DestinationPath] = []
@State private var favoritesPath: [DestinationPath] = []
@State private var searchPath: [DestinationPath] = []

// MARK: - Private Functions
private func tabSelection() -> Binding<Tab> {
    Binding {
        self.selectedTab
    } set: { touchedTap in
        if touchedTap == self.selectedTab {
            switch touchedTap {
                case .program:
                    if !programPath.isEmpty { programPath.removeAll() }
                case .favorites:
                    if !favoritesPath.isEmpty { favoritesPath.removeAll() }
                case .search:
                    if !searchPath.isEmpty { searchPath.removeAll() }
            }
        }

        self.selectedTab = touchedTap
    }
}

.navigationDestination(for: DestinationPath.self) { destination in
    switch destination {
        case .detail(let content): ProgramDetailView(themeColor: $themeColor, content: content)
        case .calendar: CalendarView(contents: $networkManager.contents, themeColor: $themeColor)
        case .settings: SettingsView(themeColor: $themeColor, selectedRegion: $selectedRegion)
    }
}

25. (1.0.1) ์„ค์ • ์ฐฝ ์ง„์ž… ์‹œ ํ™”๋ฉด ์ƒ๋‹จ์— ์—ฌ๋ฐฑ์ด ์ƒ๊ธฐ๋Š” ๋ฌธ์ œ

๊ณ ๋ฏผํ•œ ์  :

  • ์„ค์ • ํ™”๋ฉด์— ์ง„์ž…ํ•˜๊ฑฐ๋‚˜, ์„ค์ • ํ™”๋ฉด์—์„œ ์•ฑ ์ƒ‰์ƒ ํ˜น์€ ์ง€์—ญ Picker๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๊ฐ’์„ ๋ณ€๊ฒฝํ•˜๋ฉด ํ”„๋กœ๊ทธ๋žจ ํƒ€์ดํ‹€, ์„ค์ • ํƒ€์ดํ‹€ ์ƒ๋‹จ์— ์—ฌ๋ฐฑ์ด ์ƒ๊ธฐ๋Š” ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒ

๊ณผ์ • ๋ฐ ํ•ด๊ฒฐ :

  • ๋””๋ฒ„๊น… ํ™”๋ฉด์„ ํ†ตํ•ด ํ™•์ธํ•ด๋ณด๋‹ˆ, ์ƒ๋‹จ์— ์ƒ๊ธฐ๋Š” ๊ณต๋ฐฑ์€ NavigationBarLargeTitle์˜ ๋นˆ UILabel ๋•Œ๋ฌธ์— ์ƒ๊ธฐ๋Š” ๊ฒƒ์ด์—ˆ๋‹ค.
  • ๋ณธ ์•ฑ์—์„œ๋Š” NavigationBar Title ๋Œ€์‹  ์œ ์‚ฌํ•˜๊ฒŒ ๊ตฌ์‚ฌํ•œ View๋ฅผ ์‚ฌ์šฉํ•˜๊ธฐ ๋•Œ๋ฌธ์— ๋ชจ์ข…์˜ ์ด์œ ๋กœ ํ•ด๋‹น View ์œ„์ชฝ์— Title์ด ์ƒ๊ธฐ๊ณ , ๊ทธ ๊ณต๊ฐ„์„ ์ฐจ์ง€ํ•˜๊ฒŒ ๋œ ๊ฒƒ์ด๋‹ค.
  • Title์— ๋นˆ ๋ฌธ์ž์—ด์„ ๋„ฃ์–ด๋ดค์ง€๋งŒ ํ•ด๊ฒฐ๋˜์ง€ ์•Š์•˜๊ณ , toolbar์˜ Visibility๋ฅผ .hidden์œผ๋กœ ์„ค์ •ํ•ด ๋ดค์œผ๋‚˜ ํˆด๋ฐ” ์ „์ฒด๊ฐ€ ์ˆจ๊ฒจ์ ธ ์‚ฌ์šฉํ•  ์ˆ˜ ์—†์—ˆ๋‹ค.
  • ๋Œ€์‹ , navigationBarTitleDisplayMode ๋ฅผ .inline ์œผ๋กœ ์„ค์ •ํ•˜์—ฌ ์ƒ๋‹จ์œผ๋กœ ์˜ฎ๊น€์œผ๋กœ์จ ํ•ด๊ฒฐํ•  ์ˆ˜ ์žˆ์—ˆ๋‹ค.
  • ๋˜‘๊ฐ™์€ NavigationStack์„ ์‚ฌ์šฉํ•˜๋Š” ์ฆ๊ฒจ์ฐพ๊ธฐ, ๊ฒ€์ƒ‰ ํƒญ์—์„œ๋„ ๋™์ผํ•œ ์ฆ์ƒ์ด ๋ฐœ์ƒํ•˜๋Š”์ง€ ํ™•์ธํ–ˆ์œผ๋‚˜, ๋‘ ํƒญ์—์„œ๋Š” ์ž‘์—…์ด ํ•ด๋‹น ํƒญ์—์„œ๋งŒ ์ด๋ค„์ง€๊ณ , ์ด๋™ํ•˜๋”๋ผ๋„ ์ด๋ฏธ inline์œผ๋กœ ์„ค์ •๋œ ProgramDetailView๋กœ ๊ฐ€๊ธฐ ๋•Œ๋ฌธ์— ๋ฌธ์ œ๊ฐ€ ์—†์—ˆ๋‹ค.
KakaoTalk_Photo_2024-04-08-01-31-57

26. (1.0.2) ๊ฒ€์ƒ‰ ์ฐฝ์—์„œ ํ•„ํ„ฐ ์ ์šฉ์‹œ ์ผ๋ถ€ ์ผ€์ด์Šค์—์„œ ์ถฉ๋Œ ๋ฐœ์ƒ ๋ฌธ์ œ

๊ณ ๋ฏผํ•œ ์  :

  • ๊ฒ€์ƒ‰ ํ™”๋ฉด์—์„œ ์นดํ…Œ๊ณ ๋ฆฌ๋‚˜ ์ง€์—ญ์„ ์„ ํƒํ–ˆ์„ ๋•Œ ํ•ด๋‹น ์กฐ๊ฑด์— ๋งž๋Š” ํ”„๋กœ๊ทธ๋žจ์ด ์—†๋‹ค๋ฉด ์•ฑ์ด ์ถฉ๋Œ์ด ๋‚˜ ๊ฐ•์ œ์ข…๋ฃŒ ๋˜๋Š” ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒ

๊ณผ์ • ๋ฐ ํ•ด๊ฒฐ :

  • section์ด ์—†๋Š”๋ฐ ์Šคํฌ๋กคํ•˜๋ ค๋Š” ์‹œ๋„(๊ธฐ์กด onChange ๋‚ด ๋ฉ”์„œ๋“œ)๊ฐ€ ์žˆ์–ด ๋ฐœ์ƒํ•˜๋Š” ๋ฌธ์ œ๋กœ ํŒŒ์•…(Attempted to scroll the collection view to an out-of-bounds section (0) when there are only 0 sections)
  • SearchView์—์„œ body๋ฅผ ๊ตฌ์„ฑํ•  ๋•Œ filteredContents์— getFilteredContents ๋ฉ”์„œ๋“œ์˜ ๋ฐ˜ํ™˜๊ฐ’์„ ํ• ๋‹นํ•˜๊ณ , ์ด ๊ฐ’์— ๋”ฐ๋ผ View๋ฅผ ๊ตฌ์„ฑํ•˜๋„๋ก ๋ณ€๊ฒฝ
  • filteredContents๊ฐ€ ๋น„์–ด์žˆ๋‹ค๋ฉด(ํ•„ํ„ฐ ์กฐ๊ฑด์— ๋งž๋Š” ํ”„๋กœ๊ทธ๋žจ์ด ์—†๋‹ค๋ฉด) Text๋กœ ํ•ด๋‹น ๊ฒฐ๊ณผ๊ฐ€ ์—†์Œ์„ ์‚ฌ์šฉ์ž์—๊ฒŒ ์•Œ๋ฆผ
  • filteredContents๊ฐ€ ๋น„์–ด์žˆ์ง€ ์•Š๋‹ค๋ฉด(ํ•„ํ„ฐ ์กฐ๊ฑด์— ๋งž๋Š” ํ”„๋กœ๊ทธ๋žจ์ด ์žˆ๋‹ค๋ฉด) ๊ธฐ์กด๊ณผ ๊ฐ™์ด List๋กœ ํ‘œํ˜„

27. (1.0.3) ์„ค์ • ์ฐฝ์—์„œ ์•Œ๋ฆผ ํ† ๊ธ€ ํ„ฐ์น˜ ์‹œ ์ž์ฒด์ ์œผ๋กœ ๋™์ž‘ํ•˜๋Š” ๋ฌธ์ œ

๊ณ ๋ฏผํ•œ ์  :

  • ์„ค์ • ํ™”๋ฉด์˜ ์•Œ๋ฆผ ํ† ๊ธ€์€ ์•ฑ ์•Œ๋ฆผ ๊ถŒํ•œ๊ณผ ๋™๊ธฐํ™”๋˜์–ด ์žˆ์Œ
  • ์ด์— ํ† ๊ธ€์„ ํ„ฐ์น˜ํ•˜๋ฉด ์•„์ดํฐ ๊ธฐ๋ณธ ์„ค์ • ์•ฑ์œผ๋กœ ๋„˜์–ด๊ฐˆ ์ˆ˜ ์žˆ๋Š” Alert ์ฐฝ์ด ํ‘œ์‹œ๋˜์–ด์•ผ ํ•จ
  • ๊ทธ๋Ÿฌ๋‚˜ ํ˜„์žฌ ํ† ๊ธ€์„ ํ„ฐ์น˜ํ•˜๋ฉด ๋™๊ธฐํ™”๋˜์ง€ ์•Š๊ณ  ์•ฑ ๋‚ด์—์„œ ์ž์ฒด์ ์œผ๋กœ On/Off๋ฅผ ์กฐ์ •ํ•  ์ˆ˜ ์žˆ์Œ

๊ณผ์ • ๋ฐ ํ•ด๊ฒฐ :

  • SettingsView์—์„œ NotificationToggleView์— NotificationManager์˜ status๋ฅผ ๋ฐ”์ธ๋”ฉ ๊ฐ’์œผ๋กœ ๋„˜๊ฒจ์คฌ์œผ๋‚˜ ๋ฐ์ดํ„ฐ ๋™๊ธฐํ™”๊ฐ€ ์ด๋ฃจ์–ด์ง€์ง€ ์•Š๋Š” ๊ฒƒ์œผ๋กœ ์ถ”์ •๋˜์—ˆ๋‹ค.
  • ์ด์— SettingsView์—์„œ ๋„˜๊ฒจ์ฃผ๋Š” ๋Œ€์‹  NotificationToggleView์—์„œ ์ง์ ‘ ๋ฐ”์ธ๋”ฉํ•˜๊ธฐ๋กœ ํ–ˆ๋‹ค(SettingsView์—์„œ๋Š” ๋ณ„๋„์˜ NotificationManager๋ฅผ ์‚ฌ์šฉํ•˜์ง€ ์•Š์Œ).
  • ๊ธฐ์กด ์ฝ”๋“œ์—์„œ isToggleOn์„ ์ œ๊ฑฐํ•˜๊ณ , notificationManager์˜ status๋ฅผ ์ง์ ‘ ์‚ฌ์šฉํ•˜๊ณ , bindingForToggle ํ”„๋กœํผํ‹ฐ๋ฅผ ์ƒ์„ฑํ•˜์—ฌ set์—์„œ isAlertPresented๋ฅผ true๋กœ ๋ณ€๊ฒฝ์‹œ์ผœ์ฃผ๋Š” ์ž‘์—…์„ ์ถ”๊ฐ€ํ–ˆ๋‹ค.
  • ๋”๋ถˆ์–ด onAppear์™€ onReceive ํด๋กœ์ €๋Š” ๋ฉ”์ธ ์Šค๋ ˆ๋“œ์—์„œ ์‹คํ–‰๋˜๊ธฐ ๋•Œ๋ฌธ์— GCD ์ฝ”๋“œ๋ฅผ ์ œ๊ฑฐํ–ˆ๋‹ค.

์ถ”ํ›„ ๊ณ„ํš

  • SubView ๋ถ„๋ฆฌ ๋“ฑ ๋ชจ๋“ˆํ™”์— ์‹ ๊ฒฝ์„ ์ผ์ง€๋งŒ ์•„์ง ์ „์ฒด์ ์œผ๋กœ ๊ฐœ์„ ํ•  ์ ์ด ์žˆ์–ด์„œ ์ฝ”๋“œ ๊ฐœ์„ ์„ ์ตœ์šฐ์„  ๊ณผ์ œ๋กœ ์‚ผ๊ณ  ์žˆ๋‹ค.

About

Seoul Culture Information API Service

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published