ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [iOS]WKWebView를 이용한 하이브리드 앱(Hybrid App) 제작하기
    카테고리 없음 2021. 5. 17. 17:09

    오피스 체크인 모바일 웹 개발 이후 팀 결정에 따라 앱으로 출시하게 되었습니다.

    모바일 웹은 이미 하이브리드 앱을 염두에 두고 개발되었고 저희 팀은 안드로이드와, iOS 개발이 가능하여
    Cordova 같은 모바일 개발 프레임워크를 이용하지 않고 직접 웹뷰를 사용하여 구현하였습니다.

     

    하이브리드 앱을 개발 시 제가 생각하는 장단점을 정리해보겠습니다.

     

    장점

    • 웹 개발이 완료되어 있는 경우 개발 소요시간이 적다.
      이미 웹으로 개발되어 있는 서비스를 앱으로 출시하기만 하면 되기 때문에 네이티브 앱에 비하여 좀 더 빠르게 제작할 수 있습니다.
    • 모바일 웹에 푸시 알림, 위치기반 기능을 확장할 수 있다.
      하이브리드로 제작된 앱은 네이티브와 마찬가지로 푸시 알림, 외부 앱 연동, 위치기반 기능을 구현할 수 있습니다.
    • 업데이트 시 매번 심사받을 필요가 없다.
      플랫폼 개발 시 초기에 발생하는 UI 변경 및 기능개편이 자주 발생하게 되는데
      하이브리드 앱은 네이티브와 관련된 기능이 변경된 것이 아니라면 매번 앱을 심사받을 필요가 없습니다.

    단점

    • 네이티브에 비해 매끄럽지 못한 UI
      하이브리드 앱은 내부 구현을 전부 웹으로 하기 때문에 네이티브 앱처럼 부드러운 UI전환을 구현하기가 힘듭니다.
    • 네트워크가 연결된 상태에서만 사용 가능하다.
      우리가 흔히 사용하는 앱들은 네트워크 연결 상태와는 별개로 앱을 사용할 수 있습니다.
      하이브리드 앱에 경우 네트워크가 연결되있지 않은 경우 앱에 일부 기능들을 이용하게끔 할 수는 있지만
      대부분에 기능들을 웹으로 구현되기 때문에 네트워크 상태에 따라 원활한 서비스 제공이 안될 수 있습니다.

     

    하이브리드 앱을 위한 WKWebView 설정 방법을 알아보겠습니다.

    WKWebView를 이용하여 하이브리드 앱 만들기

    개발환경

    • iOS 12.5
    • Swift5
    • Xcode12.4
    • React.js

    목차

    1. WKWebView 생성 및 기본 세팅
    2. 인터넷 연결 체크
    3. 브라우저 경고창 처리 (alert, confirm)
    4. javascript와 통신하는 방법
    5. window.open()을 처리하는 방법
    6. 카메라, 앨범 접근을 위한 권한 처리

    1. WKWebView 생성 및 기본 세팅

    오피스 체크인에 모바일 홈페이지를 하이브리드 앱으로 제작하는 과정을 작성하겠습니다.

    Url: https://m.officecheckin.com/

    우선 앱 설정 화면 General -> Freamworks, Libraries, and Embedded Content에

    WebKit.framework를 추가해준 후  프레임워크를 import 합니다.

    앱 설정 화면 General -> Freamworks, Libraries, and Embedded Content 에 추가합니다.

    import UIKit
    import WebKit
    
    class ViewController: UIViewController {
    
        override func viewDidLoad() {
            super.viewDidLoad()
        }
    
    }
    
    

    WebKit View를 화면에 추가한 뒤 여백 없이 화면을 꽉 채운 뒤 webView라는 이름으로 아웃렛을 연결한 후
    웹뷰에 초기 설정하는 함수를 작성하겠습니다.

    위에있는 Web View는 사용하시면 안됩니다.

    class ViewController: UIViewController {
    
        @IBOutlet weak var webView: WKWebView!
        
        override func viewDidLoad() {
            super.viewDidLoad()
            
            webViewInit()
            
        }
        
        func webViewInit(){
        
            WKWebsiteDataStore.default().removeData(ofTypes:
            [WKWebsiteDataTypeDiskCache, WKWebsiteDataTypeMemoryCache],
            modifiedSince: Date(timeIntervalSince1970: 0)) {
            }
            
            webView.allowsBackForwardNavigationGestures = true
            
            if let url = URL(string: "https://m.officecheckin.com") {
                let request = URLRequest(url: url)
                webView.load(request)
            }
            
        }
    
    }

    WKWebsiteDataStore.default()는 WKWebView에 쿠키, 세션, 로컬 스토리지, 캐시 등 데이터를 관리하는 객체입니다.
    저는 웹사이트 변경사항이 바로 앱에 반영되기를 원하므로 캐시 데이터는 앱 실행 시 제거되도록 코드를 추가하였습니다.

     

    allowsBackForwardNavigationGestures속성은 좌 우 스와이프 동작시 뒤로 가기 앞으로 가기 기능을 활성화해 줍니다.

     

    오피스 체크인 앱을 제작 예정이기 때문에 URL은 고정된 스트링 값을 전달하였습니다.

    해당 함수를 viewDidLoad()에서 호출한 뒤 실행하겠습니다.

     

    문제없이 webView가 로드되었습니다.

     

    2. 인터넷 연결 체크

    인터넷 연결이 안 된 경우 앱에 사용할 수 있는 기능이 없으므로 앱을 종료하도록 구현하겠습니다.

    우선 연결 체크를 할 수 있는 Reachability클래스를 만들겠습니다.

    import Foundation
    import SystemConfiguration
    
    class Reachability {
        
        class func networkConnected() -> Bool {
            
            var zeroAddress = sockaddr_in(sin_len: 0, sin_family: 0, sin_port: 0, sin_addr: in_addr(s_addr: 0), sin_zero: (0, 0, 0, 0, 0, 0, 0, 0))
            zeroAddress.sin_len = UInt8(MemoryLayout.size(ofValue: zeroAddress))
            zeroAddress.sin_family = sa_family_t(AF_INET)
            
            let defaultRouteReachability = withUnsafePointer(to: &zeroAddress) {
                $0.withMemoryRebound(to: sockaddr.self, capacity: 1) { zeroSockAddress in
                    SCNetworkReachabilityCreateWithAddress(nil, zeroSockAddress)
                }
            }
            
            var flags: SCNetworkReachabilityFlags = SCNetworkReachabilityFlags(rawValue: 0)
            
            if SCNetworkReachabilityGetFlags(defaultRouteReachability!, &flags) == false {
                return false
            }
            
            let isRachable = (flags.rawValue & UInt32(kSCNetworkFlagsReachable)) != 0
            let neddsConnection = (flags.rawValue & UInt32(kSCNetworkFlagsConnectionRequired)) != 0
    
            
            return (isRachable && !neddsConnection)
        }
        
    }

    참고 블로그

    - https://trendy00develope.tistory.com/19

    - https://ithoon.tistory.com/26

     

    인터넷 연결은 viewDidApper에서 체크하겠습니다.

     override func viewDidAppear(_ animated: Bool) {
            super.viewDidAppear(animated)
            
            guard Reachability.networkConnected() else {
                let alert = UIAlertController(title: "NetworkError", message: "네트워크가 연결되어있지 않습니다.", preferredStyle: .alert)
                let okAction = UIAlertAction(title: "종료", style: .default) { (action) in
                    exit(0)
                }
                alert.addAction(okAction)
                self.present(alert, animated: true, completion: nil)
                return
            }
            
     }

     

     

    모바일 비행기모드를 키고 앱 실행시 앱이 종료됩니다.

    3. 브라우저 경고창 처리

    브라우저에서 띄울 수 있는 경고창은 alert, confirm, prompt 세 가지 형태가 있습니다.

    우선 webView에 uiDelegate를 연결해준 뒤 extension으로 WKUIDelegate를 확장하겠습니다.

    override func viewDidLoad() {
            super.viewDidLoad()
            
            webView.uiDelegate = self
            webViewInit()
            
    }

    세가지 함수를 전부 구현해주셔야 합니다,

    extension ViewController: WKUIDelegate{
        
        func webView(_ webView: WKWebView, runJavaScriptAlertPanelWithMessage message: String, initiatedByFrame frame: WKFrameInfo, completionHandler: @escaping () -> Void) {
            let alert = UIAlertController(title: "", message: message, preferredStyle: .alert)
            let okAction = UIAlertAction(title: "확인", style: .default) { (action) in
                completionHandler()
            }
            alert.addAction(okAction)
            self.present(alert, animated: true, completion: nil)
        }
        
        func webView(_ webView: WKWebView, runJavaScriptConfirmPanelWithMessage message: String, initiatedByFrame frame: WKFrameInfo, completionHandler: @escaping (Bool) -> Void) {
            let alert = UIAlertController(title: "", message: message, preferredStyle: .alert)
            let okAction = UIAlertAction(title: "확인", style: .default) { (action) in
                completionHandler(true)
            }
            let cancelAction = UIAlertAction(title: "취소", style: .default) { (action) in
                completionHandler(false)
            }
            alert.addAction(okAction)
            alert.addAction(cancelAction)
            self.present(alert, animated: true, completion: nil)
        }
        
        func webView(_ webView: WKWebView, runJavaScriptTextInputPanelWithPrompt prompt: String, defaultText: String?, initiatedByFrame frame: WKFrameInfo, completionHandler: @escaping (String?) -> Void) {
            let alert = UIAlertController(title: "", message: prompt, preferredStyle: .alert)
            let okAction = UIAlertAction(title: "확인", style: .default) { (action) in
                if let text = alert.textFields?.first?.text {
                    completionHandler(text)
                } else {
                    completionHandler(defaultText)
                }
            }
            alert.addAction(okAction)
            self.present(alert, animated: true, completion: nil)
        }
        
    }

    구현은 UIAlertController에 delegate로 넘어온 메시지와 completionHandler을 이용하여 구현해주시면 됩니다.

     

    4. Javascript와 통신하기

    모바일 웹에서 핸드폰으로 좌표를 요청하는 기능을 만들고자 합니다.

    우선 웹 페이지에서 webView로 메시지를 전달해야 됩니다.

    window.webkit.messageHandlers로 접근하여 앱에 메시지를 보내실 수 있습니다.

    const appLocationSearch = () => {
    
    	const data = {
        		action: 'searchLocation',
    	        value: ''
            }
                    
        window.webkit.messageHandlers.locationSearch.postMessage(data)
                    
    }

    extension을 통해 WKScriptMessageHandler를 상속받으신 후 userContentController함수를 구현해줍니다.

    위 함수는 javascript에서 메시지를 보내는 경우 호출됩니다.

    extension ViewController: WKScriptMessageHandler{
        
        func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
            
            if(message.name == "locationSearch"){
                
                let data:[String:String] = message.body as! Dictionary
                //location Event
                //data["action"] = searchLocation
                
            }
        }
        
    }

    locationSearch로 메시지를 받을 경우 위치정보를 획득한 후 웹 페이지로 전달해야 합니다.

    (위치정보를 얻어오는 과정은 추후 별도 포스팅으로 다뤄보겠습니다.)

    window.OCAPP = {
    
      locationSearch : (latitude, longitude) => {
        if(!latitude && !longitude){
        alert('위치정보를 가져오지 못했습니다.')
        return false
        }
    
    	//location search
      }
      
    }

    우선 웹페이지 window객체에 OCAPP 객체를 만들어 앱에서 전달하는 이벤트들을 처리할 수 있도록 구현하겠습니다.

    func postLocationInfo(){
            
            if let latitude = locationManager.location?.coordinate.latitude,
               let longitude = locationManager.location?.coordinate.longitude {
                
                webView.evaluateJavaScript("OCAPP.locationSearch('\(latitude)','\(longitude)');")
                
            }
            
      }

    evaluateJavaScript 메서드는 첫 번째 매개변수로 전달된 javascript 코드를 webView에서 실행시켜줍니다.

     

    자세한 스팩은 여기를 참고해주세요
    https://developer.apple.com/documentation/webkit/wkwebview/1415017-evaluatejavascript

     

    Apple Developer Documentation

     

    developer.apple.com

    5.  window.open()을 처리하는 방법

    웹뷰로 앱을 띄운 뒤 앱에서 새로운 창이 열리는 버튼을 눌러보면 동작하지 않는 것을 확인할 수 있습니다.

    sns로그인이 동작하지 않습니다.

    새창이 뜰 경우 새로운 웹뷰를 만들어 새로운 창에 띄어주도록 처리해야 합니다.

    class ViewController: UIViewController, CLLocationManagerDelegate {
    
        @IBOutlet weak var webView: WKWebView!
        var popupView: WKWebView?
        ...    
    }
    
    
    extension ViewController{
        
        func webView(_ webView: WKWebView, createWebViewWith configuration: WKWebViewConfiguration, for navigationAction: WKNavigationAction, windowFeatures: WKWindowFeatures) -> WKWebView? {
            
            popupView = WKWebView(frame: UIScreen.main.bounds, configuration: configuration)
            popupView?.autoresizingMask = [.flexibleWidth, .flexibleHeight]
            popupView?.uiDelegate = self
      
            view.addSubview(popupView!)
            
            return popupView
        }
        
        func webViewDidClose(_ webView: WKWebView) {
            if webView == popupView {
                popupView?.removeFromSuperview()
                popupView = nil
            }
        }
        
    }

    createWebViewWith 함수는 새로운 인터넷 창이 떠야 될 경우 실행되는 함수입니다.

    popupView를 옵셔널 WKWebView타입으로 선언 한 뒤 createWebViewWith 함수가 실행될 경우

    새로운 웹뷰를 만들어 할당한 뒤 view에 붙이는 방식입니다.

     

    webViewDidClose는 웹뷰가 종료될 경우 실행되는 함수입니다.

    popupView가 종료될 경우 현재 뷰를 제거해주신 뒤 popupView에 nil을 할당합니다.

    sns로그인 클릭시 새창에 로그인화면이 표시됩니다.

    6. 카메라, 앨범 접근을 위한 권한 처리

    아이폰 앱에 경우 카메라, 마이크, 앨범에 접근 시 권한 동의가 이루어져야 합니다.

    info.plist에 해당 권한에 접근 정보를 기재하지 않을 경우 앱이 종료됩니다.

    파일 업로드시 앨범, 카메라에 접근 가능합니다.

    추가해야 될 속성은 세 가지 속성입니다.

    Privacy - Camera Usage Description
    Privacy - Microphone Usage Description

    Privacy - Photo Library Usage Description

    위부터 카메라, 카메라 음성, 사진앨범 권한에 대한 정보입니다.

     

    Privacy - Location Whene In Use Usage Description 은 위치정보에 대한 동의입니다.

     

    카메라 접근시 접근허용을 위한 경고창 표시됩니다.

     

    이상 하이브리드 앱을 구동하기 위해 필수적으로 필요하다고 생각하는 부분을 다뤄봤습니다.

    이 외에도 하이브리드 앱을 출시하는 데 있어서 신경 써야 할 부분들은 많습니다.

     

    웹이 업데이트되는 것에 따라 앱에 버전 관리가 필요하고

    웹이 앱으로 접속한 사용자를 구분할 수 있어야 합니다.

    또 로그인 상태를 지속시키기 위해서 쿠키 데이터를 어디에 저장할지도 고민해야 합니다.

    최초 사용자에게 튜토리얼 화면을 제공하고 앱이 사용하는 권한을 최초 한번 표시해주어야 합니다.

    위 내용들은 추후 오피스 체크인 기술 블로그에서 다뤄보도록 하겠습니다.

     

    이상 글을 마치며 지속적으로 개발 과정을 공유할 수 있도록 노력하겠습니다.

     

     

     

    댓글

Designed by Tistory.