๋ณธ๋ฌธ ๋ฐ”๋กœ๊ฐ€๊ธฐ

๐ŸŽ iOS/iOS Programming

[iOS, SwiftUI] ํ•˜์ด๋ธŒ๋ฆฌ๋“œ(Hybrid) ์•ฑ์ด๋ž€ ? ์›น๋ทฐ<->๋„ค์ดํ‹ฐ๋ธŒ ํ†ต์‹  ๊ตฌํ˜„ํ•˜๊ธฐ, (SwiftUI + WebView evaluateJavaScript + javascript messagehandler)

728x90

ํ•˜์ด๋ธŒ๋ฆฌ๋“œ ์•ฑ(Hybid Application) ?

๊ทธ์ „์— ๋จผ์ € ์•Œ์•„์•ผ ํ•  ๊ฒƒ:

 

1. ๋„ค์ดํ‹ฐ๋ธŒ ์•ฑ(Native App)

:  Android ๋˜๋Š” iOS ๊ฐ™์€ ์–ด๋–ค ๊ตฌ์ฒด์ ์ธ ํ”Œ๋žซํผ๋งŒ์„ ์œ„ํ•ด ๋งŒ๋“ค์–ด์ง„ ์‘์šฉ ํ”„๋กœ๊ทธ๋žจ์„ ๋„ค์ดํ‹ฐ๋ธŒ ์•ฑ(Native App)์œผ๋กœ ๋””๋ฐ”์ด์Šค์— ๋‹ค์šด๋กœ๋“œํ•˜์—ฌ ์„ค์น˜ํ•  ์ˆ˜ ์žˆ๋Š” ์•ฑ

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

 

2. ์›น ์•ฑ(Web App)

:  ๋ฐ์Šคํฌํ†ฑ ๋˜๋Š” ๋ชจ๋ฐ”์ผ ๋””๋ฐ”์ด์Šค์˜ ๋ธŒ๋ผ์šฐ์ €์—์„œ ์•ก์„ธ์Šค ํ•  ์ˆ˜ ์žˆ๋Š” ์•ฑ

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

 

ํ•˜์ด๋ธŒ๋ฆฌ๋“œ ์•ฑ์€ ์ด ๋‘ ๊ฐ€์ง€๋ฅผ ์งฌ๋ฝ•ํ•œ ๊ฒƒ, 100% ์›น์•ฑ, 100% ๋„ค์ดํ‹ฐ๋ธŒ ์•ฑ์ด ์•„๋‹Œ ์•ฑ์ด๋‹ค.

์ฆ‰, JavaScript, HTML ๋ฐ CSS์™€ ๊ฐ™์ด ์ž˜ ์•Œ๋ ค์ง„ ์–ธ์–ด์™€ ํ”„๋ ˆ์ž„ ์›Œํฌ๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๋‹ค์–‘ํ•œ ํ”Œ๋žซํผ์—์„œ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋Š” ํ•˜์ด๋ธŒ๋ฆฌ๋“œ ์•ฑ์„ ๋น ๋ฅด๊ฒŒ ๋งŒ๋“ค ์ˆ˜ ์žˆ๋‹ค.

๋น„์šฉ๋„ ์‹œ๊ฐ„๋„ ๋œ ๋“ค๊ณ  ์œ ์ง€ ๋ณด์ˆ˜๋„ ์‰ฝ๊ณ  ๊ธฐ๋ณธ API ๊ธฐ๋Šฅ๋„ ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•˜๋‚˜ ์˜คํ”„๋ผ์ธ(offline)์œผ๋กœ๋Š” ์ž‘๋™ํ•˜์ง€ ์•Š๊ณ  ๋˜‘๊ฐ™์€ ์ฝ”๋“œ๋ฅผ ๋ฒ ์ด์Šค๋กœ ํ•˜๊ธฐ ๋•Œ๋ฌธ์— ์•ˆ๋“œ๋กœ์ด๋“œ๋‚˜ ์•„์ดํฐ ๋“ฑ ๊ฐ ๋””๋ฐ”์ด์Šค์˜ ํŠน์ • ๊ธฐ๋Šฅ์„ ์‚ฌ์šฉํ•˜๋Š” ๋ฐ ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ•  ์ˆ˜๋„ ์žˆ๋‹ค.

 

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

ํ•˜์ด๋ธŒ๋ฆฌ๋“œ ์•ฑ(Hybid Application)  ์›น๋ทฐ<->๋„ค์ดํ‹ฐ๋ธŒ ํ†ต์‹  ๊ตฌ์กฐ


ํ•˜์ด๋ธŒ๋ฆฌ๋“œ ์•ฑ(Hybid Application) ๊ตฌํ˜„ GOAL

ํ•˜์ด๋ธŒ๋ฆฌ๋“œ ์•ฑ์„ ๊ฐœ๋ฐœํ•  ๋•Œ๋Š” ๋„ค์ดํ‹ฐ๋ธŒ(iOS)์™€ ์›น(JS) ๊ฐ„์˜ ํ†ต์‹ ์„ ํ†ตํ•ด ์›น์—์„œ ๋„ค์ดํ‹ฐ๋ธŒ์˜ ๊ธฐ๋Šฅ์„, ๋„ค์ดํ‹ฐ๋ธŒ์—์„œ ์›น์„ ํ˜ธ์ถœํ•  ์ˆ˜ ์žˆ๋Š” ๊ฒƒ์ด ํ•ต์‹ฌ์ด๋‹ค. ๋ฌผ๋ก  ๊ทธ ๋ฐ”ํƒ•์€ ์›น๋ทฐ์—์„œ ์ด๋ฃจ์–ด์ง„๋‹ค.

 

์•„๋ž˜ ์Šคํ…์— ๋”ฐ๋ผ js์™€ native ๊ฐ„ ๋ฐ์ดํ„ฐ๋ฅผ ์ฃผ๊ณ ๋ฐ›๋Š” ๊ณผ์ •์„ ์•Œ์•„๋ณด์ž~

๐Ÿ’š Step1. js -> native ๋ฐ์ดํ„ฐ ์ „์†ก

  1. webview์™€ js์— Bridge ์„ธํŒ…ํ•˜๊ธฐ
    • WKUserContentController ์‚ฌ์šฉํ•˜๊ธฐ
  2. js์—์„œ webview๋กœ ์ „์†กํ•œ ๋ฐ์ดํ„ฐ native์—์„œ ์ฒ˜๋ฆฌํ•˜๊ธฐ
    • messageHandlers ์‚ฌ์šฉํ•˜๊ธฐ

๐Ÿ’š Step2. native -> js ๋ฐ์ดํ„ฐ ์ „์†ก

  1. ์›น ๋ทฐ์—์„œ ์›ํ•˜๋Š” ์‹œ์ ์— js ํ•จ์ˆ˜ ํ˜ธ์ถœํ•˜๊ธฐ
    • evaluateJavaScript(_:) ์‚ฌ์šฉํ•˜๊ธฐ

๐Ÿ’š Step1. js -> webview ๋ฐ์ดํ„ฐ ์ „์†ก

js์—์„œ webview์— ๋ฐ์ดํ„ฐ๋ฅผ ์ „์†กํ•˜๊ธฐ ์œ„ํ•ด์„œ๋Š” swift์™€ javascipt ์‚ฌ์ด ์—ฐ๊ฒฐํ•ด ์ฃผ๋Š” Bridge๊ฐ€ ํ•„์š”ํ•˜๋‹ค. ํ•ต์‹ฌ!

Bridge๋ฅผ ํ†ตํ•ด ์›น ํŽ˜์ด์ง€์˜ ์ด๋ฒคํŠธ๋ฅผ ๋ฐ›์•„์„œ native์—์„œ ์ฒ˜๋ฆฌ๊ฐ€ ๊ฐ€๋Šฅํ•˜๋‹ค!!!!

1. webview์™€ js์— Bridge ์„ธํŒ…ํ•˜๊ธฐ

๋”ฐ๋ผ์„œ ๋จผ์ €, ์›น ๋ทฐ(WKWebView) ์ดˆ๊ธฐํ™” + Webview์™€ js์— Bridge ๋“ฑ๋ก์„ ํ•ด์ค€๋‹ค.

 

1) UIViewRepresentable์„ ์ค€์ˆ˜(conform)ํ•˜๊ณ  ์›น ๋ทฐ๋ฅผ ๊ฐ์‹ธ๋Š” WebView๋ฅผ ์ƒ์„ฑ + Webview์— Bridge ๋“ฑ๋ก

์›น ๋ทฐ(WKWebView) ์ƒ์„ฑํ•˜๋ฉฐ, ๋ฉ”์‹œ์ง€ํ•ธ๋“ค๋Ÿฌ๋ฅผ ๋ฐ›๊ธฐ ์œ„ํ•œ ๋ธŒ๋ฆฟ์ง€๋ฅผ ๋“ฑ๋กํ•ด ์ค€๋‹ค.

how? WKUserContentController์„ ์ƒ์„ฑํ•˜์—ฌ ๊ทธ ์•ˆ์— ์‚ฌ์šฉํ•  Bridge ์ด๋ฆ„์„ ์ถ”๊ฐ€ํ•ด ์ค€๋‹ค. 

๋‚ด๊ฐ€ ์—ฐ๊ฒฐํ•  ๋ธŒ๋ฆฟ์ง€ ์ด๋ฆ„ : WKBridge >> ์ด ๋ถ€๋ถ„์€ ์•ฑ๊ณผ ์›น์—์„œ ๋™์ผํ•˜๊ฒŒ ๋งž์ถ”์–ด์•ผ ์ •์ƒ ์ž‘๋™ํ•œ๋‹ค!*์ฃผ์˜*

 

WKUserContentController์— ํ•ด๋‹น ํ•ธ๋“ค๋Ÿฌ๋ฅผ ๋“ฑ๋กํ•ด ์ฃผ๊ณ  

WKWebViewConfiguration์„ ์ƒ์„ฑํ•˜์—ฌ WKUserContentController์„ userContentController๋กœ ๋„ฃ์–ด์ค€๋‹ค.

struct WebView: UIViewRepresentable {
    typealias UIViewType = WKWebView
    
    var url: URL?
    
    func makeUIView(context: Context) -> UIViewType {
        let contentController = WKUserContentController()
        contentController.add(self.makeCoordinator(),
                              name: "WKBridge")
        
        let configuration = WKWebViewConfiguration()
        configuration.preferences = preferences
        configuration.userContentController = contentController
        
        ...

์ƒ์„ฑํ•œ webview์— ์›ํ•˜๋Š” remote url๋กœ ํŽ˜์ด์ง€๋ฅผ ๋กœ๋“œํ•œ๋‹ค.

์ง€๋‚œ Firebase ๊ธฐ๋ฐ˜ ์›น ํ˜ธ์ŠคํŒ… ๋ฐฐํฌํ•œ ํ™”๋ฉด์œผ๋กœ ํ•˜์ด๋ธŒ๋ฆฌ๋“œ ์•ฑ์„ ๋งŒ๋“ค์—ˆ๋‹ค.

    let hostURL = "https://hostingdemo-a9104.web.app/"
    var body: some View {
        VStack {
            WebView(url: hostURL, viewModel: viewModel)

 

2) js์— Bridge ๋“ฑ๋ก

์›น์—์„œ๋Š” window.webkit.messageHandlers๋กœ ์ ‘๊ทผํ•˜์—ฌ ์•ฑ์— ๋ฉ”์‹œ์ง€๋ฅผ ๋ณด๋‚ผ ์ˆ˜ ์žˆ๋‹ค.

 

๋”ฐ๋ผ์„œ js์—์„œ๋Š” "MessageHanlder"๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๋“ฑ๋กํ•œ๋‹ค.

  • messageHandler๋ž€ javascript์—์„œ native๋กœ ์ด๋ฒคํŠธ๋ฅผ ๋ณด๋‚ด๊ณ  ์‹ถ์€ ๊ฒฝ์šฐ, javascript ์ชฝ์—์„œ ์ •์˜ํ•œ ๋ฉ”์†Œ๋“œ window.webkit.messageHandlers.{๋ฉ”์‹œ์ง€ ํ•ธ๋“ค๋Ÿฌ ์ด๋ฆ„}.postMessage("์ „๋‹ฌํ•  ๋ฉ”์‹œ์ง€ ์ž…๋ ฅ")

<. js ํŒŒ์ผ >

์›น ๋ทฐ์—์„œ ์„ค์ •ํ•œ ๋ธŒ๋ฆฟ์ง€ ์ด๋ฆ„๊ณผ ๋™์ผํ•œ "WKBridge"๋กœ Bridge ํ•จ์ˆ˜๋ฅผ sendScriptMessage ํ•จ์ˆ˜ ์•ˆ์— ์„ ์–ธํ–ˆ๋‹ค. 

function sendScriptMessage(data) {
  // ์›น๋ทฐ์—์„œ ๋„ค์ดํ‹ฐ๋ธŒ๋กœ ๋ฉ”์‹œ์ง€ ์ „๋‹ฌ
  const sendData = { message: data };
  window.webkit.messageHandlers.WKBridge.postMessage(sendData);
}

 

javascript ํ•จ์ˆ˜๊ฐ€ ์‹คํ–‰ํ•œ ๊ฒฐ๊ณผ๋ฅผ native์— ์ „๋‹ฌํ•˜์—ฌ ์ฒ˜๋ฆฌํ•˜๋Š” ๋กœ์ง์„ ์œ„ํ•ด

html์—์„œ ๊ทธ๋ฆฐ ํ•  ์ผ ์ถ”๊ฐ€ํ•˜๋Š” ๋ฒ„ํŠผ ํด๋ฆญ ์‹œ ์ˆ˜ํ–‰ํ•˜๋Š” ํ•จ์ˆ˜(addTodo()) ์•ˆ์—์„œ sendScriptMessage๋ฅผ ๋ถˆ๋Ÿฌ์ฃผ๊ณ  ์ž…๋ ฅํ•œ ๋ฐ์ดํ„ฐ๋ฅผ ํŒŒ๋ผ๋ฏธํ„ฐ๋กœ ๋„ฃ์–ด์ฃผ์—ˆ๋‹ค.

<. js ํŒŒ์ผ >

function addTodo(){
    ...
    sendScriptMessage(addValue.value)
    ...
}

<. html ํŒŒ์ผ >

<button type="button" id = "btn" onclick="addTodo()"><i class="xi-plus xi-2x"></i></button>
        </div>

 

2.  js์—์„œ webview๋กœ ๋ณด๋‚ธ ๋ฐ์ดํ„ฐ native code๋กœ ์ฒ˜๋ฆฌํ•˜๊ธฐ

๊ทธ๋Ÿผ js์—์„œ ๋ณด๋‚ธ ๋ฐ์ดํ„ฐ๊ฐ€ ์ž˜ ์™”๋Š”์ง€ ํ™•์ธํ•ด ๋ณด์ž~

 

์—ฐ๊ฒฐํ•œ ๋ฒ„ํŠผ์„ ํด๋ฆญํ•ด์„œ ์›น ํŽ˜์ด์ง€(js)์—์„œ sendScriptMessage ํ•จ์ˆ˜๊ฐ€ ํ˜ธ์ถœํ–ˆ์„ ๋•Œ Bridge๋Š” WKWebView์—์„œ ํ•ธ๋“ค๋งํ•œ๋‹ค.

WKWebView์—์„œ WKScriptMessageHandlerํ”„๋กœํ† ์ฝœ์„ ์œ„์ž„๋ฐ›์€ ํ›„ userContentController ๋ฉ”์„œ๋“œ๋ฅผ ์ƒ์„ฑํ•œ๋‹ค.

์›น๋ทฐ์—์„œ ํ˜ธ์ถœํ•œ sendScriptMessage์ด ์‹คํ–‰๋˜๋ฉด, ๋„ค์ดํ‹ฐ๋ธŒ์—์„œ ์‹คํ–‰๋˜๋Š” ๋ถ€๋ถ„์ด๋‹ค.

 

message.name์œผ๋กœ ์–ด๋–ค ๋ธŒ๋ฆฟ์ง€๊ฐ€ ๋“ค์–ด์™”๋Š”์ง€ ํ™•์ธ ๊ฐ€๋Šฅํ•˜๊ณ ,

๋ธŒ๋ฆฟ์ง€ ๋ณ„๋กœ ๋“ค์–ด์˜จ message.body๋ฅผ handling ํ•ด์ฃผ๋ฉด ๋œ๋‹ค!!!

๋‚˜๋Š” "WKBridge"๋กœ ๋“ค์–ด์˜จ ๋ธŒ๋ฆฟ์ง€๋ฅผ ๋ถ„๋ฆฌํ•˜์—ฌ receivedJsonValueFromWebView ํ•จ์ˆ˜๋กœ ๋ฐ์ดํ„ฐ๋ฅผ ์ „๋‹ฌํ–ˆ๋‹ค.

 

extension WebView.Coordinator: WKScriptMessageHandler {
    func userContentController(_ userContentController: WKUserContentController,
                                 didReceive message: WKScriptMessage) {
        if message.name == "WKBridge" {
            delegate?.receivedJsonValueFromWebView(value: message.body as! [String:Any?])
        } else if let body = message.body as? String {
            delegate?.receivedStringValueFromWebView(value: body)
        }
    }
}

 

receivedJsonValueFromWebView ํ•จ์ˆ˜์—์„œ ์ „๋‹ฌ๋ฐ›์€ ๋ฐ์ดํ„ฐ๋ฅผ ํ•ธ๋“ค๋งํ•ด ์ค€๋‹ค~~!!

 

    func receivedJsonValueFromWebView(value: [String : Any?]) {
        if let data = value["message"] {
            print("JSON ๋ฐ์ดํ„ฐ๊ฐ€ ์›น์œผ๋กœ๋ถ€ํ„ฐ ์˜ด: \(String(describing: data!))")
        }
    }

 

[๊ตฌํ˜„ ๊ฒฐ๊ณผ]

์›น๋ทฐ์—์„œ ์ž…๋ ฅํ•œ ๋ฐ์ดํ„ฐ๊ฐ€ ๋ธŒ๋ฆฟ์ง€ ํƒ€๊ณ  native์˜ message.body์— ๋„์ฐฉํ•œ ๋ฐ์ดํ„ฐ๋ฅผ ์ถœ๋ ฅํ•ด ๋ณด์•˜๋‹ค.


๐Ÿ’š Step2. webview -> js ๋ฐ์ดํ„ฐ ์ „์†ก

๋ฐ˜๋Œ€๋กœ native์—์„œ ์ฒ˜๋ฆฌํ•œ ๊ฐ’์„ js๋กœ ๋ณด๋‚ด๋ณด์ž !

 

๋ฐฉ๋ฒ•์€ native code์—์„œ javascript code์— ์ •์˜๋˜์–ด ์žˆ๋Š” ํ•จ์ˆ˜ ์‹คํ–‰ ์‹œํ‚ค๊ณ  ๋ฐ์ดํ„ฐ๋Š” ํ•ด๋‹น ํ•จ์ˆ˜์—์„œ ํ•ธ๋“ค๋งํ•˜๋ฉด ๋œ๋‹ค.

๊ทธ๋Ÿผ ๋จผ์ €,

js ์ฝ”๋“œ์˜ ํ•จ์ˆ˜๋ฅผ ์–ด๋–ป๊ฒŒ ๋ถ€๋ฅด๋ƒ ์›น ๋ทฐ์—์„œ evaluateJavaScript(_:)๋ฅผ ํ†ตํ•ด ํ˜ธ์ถœ ๊ฐ€๋Šฅํ•˜๋‹ค!!

์•„๋ž˜ js์—์„œ ์„ ์–ธํ•œ callFromNative(data) ํ•จ์ˆ˜๋ฅผ ํ˜ธ์ถœํ•˜๊ธฐ ์œ„ํ•ด์„œ

<. js ํŒŒ์ผ >

function callFromNative(data){
    alert("Received :", data);
    return "OK";
}

 

ํ•ด๋‹น ํ•จ์ˆ˜๋ฅผ ํŒŒ๋ผ๋ฏธํ„ฐ๋กœ ํ˜ธ์ถœํ•˜๋ฉด ๋œ๋‹ค.

let args: Any = "test"
webView.evaluateJavaScript("callFromNative('\(args)');") { result, error in
    if let error = error {
        print(error.localizedDescription)
    } else if let result = result {
        print(result)
    }
}

ํ•ด๋‹น evaluateJavaScript(_:)๋ฅผ ์›ํ•˜๋Š” ์‹œ์ ์— ํ˜ธ์ถœํ•˜๋ ค๋ฉด? (with SwiftUI)

UIKit ๋‹จ์ˆœํžˆ ๋ฒ„ํŠผ ์ด๋ฒคํŠธ๋กœ ํ˜ธ์ถœํ•˜๋ฉด ๋˜์ง€๋งŒ, SwiftUI์—์„œ๋Š” ๋ณต์žกํ•˜๋‹คใ… 

 

ContentView์˜ ๋ฒ„ํŠผ์„ ํด๋ฆญํ•ด์„œ js์—์„œ ์„ ์–ธํ•œ callFromNative(data) ํ•จ์ˆ˜๋ฅผ ํ˜ธ์ถœํ•ด ๋ณด์ž!

 

1) ObservableObject์„ ์ƒ์†๋ฐ›๋Š” WebViewModel ํด๋ž˜์Šค ์ƒ์„ฑ

  • functionCaller : send๋ฅผ ํ˜ธ์ถœํ•ด์„œ ๊ตฌ๋…์ž๋“ค์—๊ฒŒ ๊ฐ’์„ ์ „ํŒŒํ•˜๋Š” publisher 
  • shouldUpldateView : updateView๋ฅผ ์‹คํ–‰ํ•ด์•ผ ํ•˜๋Š”์ง€ ์—ฌ๋ถ€๋ฅผ ์•Œ๋ ค์ฃผ๋Š” Bool ๊ฐ’
import Combine

class WebViewModel: ObservableObject {
    var functionCaller = PassthroughSubject<String, Never>()
    var shouldUpdateView = true
}

 

2) WebView ๋‚ด์— ์œ„์—์„œ ๋งŒ๋“  WebViewModel์— ๋Œ€ํ•œ ์ƒํƒœ ๋ณ€์ˆ˜๋ฅผ ์ถ”๊ฐ€ํ•œ๋‹ค.

struct WebView: UIViewRepresentable {
// ... //
@StateObject var data: WebViewModel
// ... //
}


3) WebView ๋‚ด์— ์ฝ”๋””๋„ค์ดํ„ฐ๋ฅผ ์ถ”๊ฐ€

  • tieFunctionCaller(…)
    • WebViewData์˜ functionCalller๋ฅผ ์†Œํ™˜ํ•˜๋Š” ์—ญํ• 
    • functionCalller๋Š” PassthroughSubject์ด๋ฏ€๋กœ sink๋ฅผ ํ˜ธ์ถœ ๊ฐ€๋Šฅ
    • ์–ด๋Š ํŠน์ • ์กฐ๊ฑด์ด ๋˜๋ฉด(์˜ˆ: ๋ฒ„ํŠผ์„ ๋ˆ„๋ฅธ ๊ฒฝ์šฐ) String ๊ฐ’์ด ๋ฐฐ์ถœ
    • sink๋ฅผ ํ†ตํ•ด ๋ฐฐ์ถœ๋œ js๊ฐ’(String ํƒ€์ž…)์„ webView?.evaluateJavaScript(js)๋กœ ์‹คํ–‰
import Combine
// ... //
func makeCoordinator() -> Coordinator {
    return Coordinator(self)
}
class Coordinator: NSObject, WKNavigationDelegate {
    /// WebView Representable
    var parentWebView: WebView
    var webView: WKWebView? = nil
    
    private var cancellable: AnyCancellable?
    
    init(_ parentWebView: WebView) {
        self.parentWebView = parentWebView
        super.init()
    }
    
	//webViewmodel ์˜ functionCaller ๋ฅผ ์†Œํ™˜
    func tieFunctionCaller(data: WebViewModel) {
        print("Passthrough:", #function)
        //functionCaller ๋Š” PassthroughSubject์ด๋ฏ€๋กœ Sink๋ฅผ ํ˜ธ์ถœํ•  ์ˆ˜ ์žˆ๋‹ค
        cancellable = data.functionCaller.sink(receiveValue: { js in
            self.webView?.evaluateJavaScript(js)
			print("js ๋‚ด \(js) ํ˜ธ์ถœ ์™„๋ฃŒ")
        })
    }
}

 

4) WebView ๋‚ด์— updateUIView๋ฅผ ์ž‘์„ฑ

updateUIView๋Š” ์›น๋ทฐ๊ฐ€ ์‹คํ–‰๋œ ์‹œ์ ์— ๋ฐ”๋กœ ์‹คํ–‰๋˜๋ฉฐ, makeUIView ๋‹ค์Œ์— ์‹คํ–‰๋œ๋‹ค.

์ง€์ •๋œ ๋ทฐ์˜ ์ƒํƒœ๋ฅผ ๋‹ด์˜ ์ƒˆ ์ •๋ณด๋กœ ์—…๋ฐ์ดํŠธํ•˜๋Š” ๋ฉ”์†Œ๋“œ!

func updateUIView(_ uiView: UIViewType, context: Context) {
        guard data.shouldUpdateView else {
        data.shouldUpdateView = false
        return
    }
    context.coordinator.tieFunctionCaller(data: data)
    context.coordinator.webView = uiView
}

 

5) SwiftUI์˜ ๋ทฐ(ContentView ๋“ฑ) ๋‚ด๋ถ€์— WebViewModel๋ฅผ ์ถ”๊ฐ€

struct ContentView: View {
    // ... //
    @StateObject var webViewData = WebViewModel()
    // ... //
}

 

6) SwiftUI์˜ ๋ทฐ(ContentView ๋“ฑ) ๋‚ด๋ถ€์— WebView๋ฅผ ์ถ”๊ฐ€

์œ„์—์„œ ๋งŒ๋“  webViewData๋ฅผ WebView์˜ ํŒŒ๋ผ๋ฏธํ„ฐ๋กœ ์ถ”๊ฐ€

var body: some View {
	WebView(url: URL(string: "https://example.con"), data: webViewData)
}

 

7) webViewData๋ฅผ ์ด์šฉํ•˜์—ฌ ๋ฒ„ํŠผ์„ ๋ˆ„๋ฅด๋ฉด ํŠน์ • ์ž๋ฐ”์Šคํฌ๋ฆฝํŠธ ํ•จ์ˆ˜๊ฐ€ ์‹คํ–‰๋˜๋„๋ก ํ•œ๋‹ค!

let args: Any = "test"
...
Button {
    webViewData.functionCaller.send(
        """
        callFromNative('\(args)')
        """
    )
} label: {
    Image(systemName: "icloud.and.arrow.up.fill")
}

 

[๊ตฌํ˜„ ๊ฒฐ๊ณผ]

safari๋กœ ๋””๋ฒ„๊น…ํ•˜์—ฌ ๊ฐ’์ด js์— ์ œ๋Œ€๋กœ ๋“ค์–ด์™”์Œ์„ ํ™•์ธํ–ˆ๋‹ค.

์ด์ œ js <-> webview ์–‘๋ฑกํ–ฅ์œผ๋กœ ๋ฐ์ดํ„ฐ๋ฅผ ์ฃผ๊ณ ๋ฐ›๋Š” ๋ฐฉ๋ฒ•์„ ํ„ฐ๋“ํ–ˆ์Šต๋‹ˆ๋‹ค!!!!


*์›น๋ทฐ ์‚ฌํŒŒ๋ฆฌ๋กœ ๋””๋ฒ„๊น…ํ•˜๊ธฐ

์›น๋ทฐ ๋””๋ฒ„๊น…์ด ์‚ฌํŒŒ๋ฆฌ์—์„œ ์ž˜ ๋˜์—ˆ์—ˆ๋Š”๋ฐ, ์•ˆ๋ผ์„œ ํ™•์ธํ•ด ๋ณด๋‹ˆ

ios 16.4 ์ดํ›„  inspectable ํ•จ์ˆ˜๊ฐ€ ์ถ”๊ฐ€๋˜๋ฉด์„œ, webview์— ์„ค์ •ํ•ด ์ค˜์•ผ์ง€ ๋””๋ฒ„๊น…์ด ๊ฐ€๋Šฅํ•ด์กŒ๋‹ค.

https://developer.apple.com/documentation/safari-developer-tools/enabling-inspecting-content-in-your-apps

@property(nonatomic, getter=isInspectable) BOOL inspectable;

1. ์›น๋ทฐ ์„ค์ •ํ•˜๋Š” ๋ถ€๋ถ„์— ์•„๋ž˜ ์ฝ”๋“œ ์ถ”๊ฐ€ํ•ด ์ค€๋‹ค.

if #available(iOS 16.4, *) {
	#if DEBUG
	webView.isInspectable = true  // webview inspector ๊ฐ€๋Šฅํ•˜๋„๋ก ์„ค์ •
	#endif
}

 

์ด์ œ, ์‚ฌํŒŒ๋ฆฌ๋กœ ๋””๋ฒ„๊น… ๊ฐ€๋Šฅ~~

๋ฐฉ๋ฒ•์€

2. ๋นŒ๋“œ > ์‚ฌํŒŒ๋ฆฌ ์‹คํ–‰ > ์ƒ๋‹จ ๋ฉ”๋‰ด์˜ '๊ฐœ๋ฐœ์ž์šฉ' ํด๋ฆญ > ์‹œ๋ฎฌ๋ ˆ์ดํ„ฐ ํด๋ฆญ!!

 

ํด๋ฆญํ•˜๋ฉด ์•„๋ž˜์ฒ˜๋Ÿผ ์›น ๋””๋ฒ„๊น…ํ•  ์ˆ˜ ์žˆ๋Š” ํ™”๋ฉด์ด ๋‚˜์˜ต๋‹ˆ๋‹ค~! 

๋ฐ˜์‘ํ˜•