[iOS] WKWebView로 웹뷰(WebView) 구현하기

Hoojeong Kim·2022년 7월 11일
11
post-thumbnail

WebView

WebView란 프레임워크에 내장된 웹 브라우저 컴포넌트로 View의 형태로 앱에 임베딩하는 것을 말한다.

무슨 소리냐구요..?

예를 들어, API를 통해서 받은 URL을 호출을 해보면 응답 값으로 JSON, XML 같은 데이터 포맷이 아니라 HTML로 들어오게 된다.

만약 이렇게 데이터가 들어오게 되면 파싱을 해줄 수가 없다.

그래서 이러한 HTML 데이터를 처리해서 웹페이지로 보여주고 하는 게 WebView이다. 즉, WebView는 앱 내에 웹 페이지를 넣는 것을 의미한다.

WebView를 구현하는 방법으로 총 3가지가 있다.

  • UIWebView
  • WKWebView
  • SFSafariView

각각 하나씩 알아보자.

UIWebView

UIWebView는 ios 2.0에 출시됐다. 굉장히 오래됨.. 그런 탓에 성능적인 측면에서 많이 부족하고, 사용하지 않는 것을 추천한다. 하하.

WKWebView

WKWebView는 ios 8.0부터 가장 많이 사용되고 있다. 오래전에 나온 UIWebView 보다 성능이 좋다.

WKWebView에는 큰 장점이 하나 있는데,

웹 페이지에서 할당하는 메모리는 앱과 별도의 스레드에서 관리한다.

이 말은 즉슨, 웹 페이지는 앱의 메모리와 별도로 동작하기 때문에 웹 페이지의 메모리가 아무리 크더라도 앱이 죽지 않는다는 것을 의미한다.

SFSafariView

SFSafariView 는 ios 9.0부터 사용되었으며, Safari를 이용하는 웹 뷰이다. 사용자가 웹 뷰를 통해 웹 페이지에 들어가면, Safari와 동일한 화면이 구현된다.

WKWebView가 단순히 웹 페이지 하나만을 보여준다면, SFSafariView는 사파리의 기능을 이용할 수가 있어 다양한 동작이 가능해진다.

추가로 기존 아이폰의 Safari 쿠키, 데이터 등을 공유할 수 있다는 장점이 있다.

정리하자면, 일반적인 웹 뷰를 구현할 때 WKWebView를 사용하면 되고, 더 복잡한 기능을 사용하고 싶을 때는 SFSafariView를 사용하면 된다.
UIWebView는 걍 쓰지 말아요..


그래서 오늘은, WKWebView를 사용해 웹 뷰를 구현해보자.


초기 설정

WebKit를 사용하기 위해서는, 먼저 프로젝트에 추가해야 한다.

프로젝트 설정 파일에서 Build Phases > Link Binary With Libraries로 이동해 +를 눌러 WebKit.framework를 추가한다.


구현하기

기본 설정

먼저 스토리보드는 이렇게 구성되어 있다.

ViewController는 Navigation Controller와 연결되어 있고, 스택뷰로 감싼 TextField, Button이 있다.

WebViewController에는 screen을 채운 view를 두었다.
(잘 보이라고 회색으로 해둠 ㅎㅎ)

ViewController

import UIKit

class ViewController: UIViewController {

    @IBOutlet weak var tvSearch: UITextField!/** 검색어 텍스트필드 */
    
    /** life cycle */
    override func viewDidLoad() {
        super.viewDidLoad()
        
    }

    @IBAction func tappedSearch(_ sender: Any) {

    }
}

tappedSearch Action 함수에는, 입력한 검색어와 함께 WebViewController로 넘어가는 코드를 작성할 예정.

WebViewController

import UIKit

class WebViewController: UIViewController {

    @IBOutlet weak var webViewGroup: UIView!/** 배경 뷰 */
    
    /** life cycle */
    override func viewDidLoad() {
        super.viewDidLoad()
        
    }
}

ViewController와 WebViewController 모두 스토리보드와 컴포넌트 연결만 해둔 상태이다.


ViewController

먼저 검색어를 입력한 뒤, 검색 버튼을 누르면 웹뷰로 넘어가는 코드를 작성해보자.

ViewController의 tappedSearch 함수에 다음 코드를 추가하자.

let text: String = tvSearch.text!

text 변수에 입력받은 검색어를 넣어, WebViewController에 넘겨줄 거다.


그 다음, WebViewController로 넘어가는 코드를 작성해보자.

if let navigationController = self.navigationController {
            
	if !(navigationController.topViewController?.description.contains("WebViewController"))! {
    	let storyBoard: UIStoryboard = UIStoryboard(name: "Main", bundle: nil)
    	let viewController = storyBoard.instantiateViewController(withIdentifier: "WebViewController") as! WebViewController
                
    	viewController.search = text
    	viewController.url = "https://m.search.naver.com/search.naver?sm=mtp_hty.top&where=m&"
                
    	navigationController.pushViewController(viewController, animated: true)
	}
}

화면 이동 시에는 NavigationController를 사용한다.

if 문을 사용해, 만약 topViewController가 WebViewController가 아니라면 push하도록 구현했다.

if !(navigationController.topViewController?.description.contains("WebViewController"))! { }

navigationController의 rootViewController는 이 프로퍼티의 0번째고, 현재 보여주고 있는 화면은 마지막 인덱스에 해당한다.

이 마지막 인덱스에 해당하는 화면이 바로 topViewController이다.

검색어와 url은 WebViewController의 프로퍼티를 통해 넘겨준다.

viewController.search = text
viewController.url = "https://m.search.naver.com/search.naver?sm=mtp_hty.top&where=m&"

네이버에 특정 키워드를 입력해 검색하면 url이 https://m.search.naver.com/search.naver?sm=mtp_hty.top&where=m&query=검색어 와 같다.
query=검색어 에 입력한 검색어가 들어갈 것이고, 그 앞은 모두 동일하다.

검색어를 입력하면 단순하게 네이버에 검색한 결과를 띄워줄 예정이니, url은 위와 같이 설정한다.


제대로 동작하는 지 확인하기 위해 WebViewController에 다음 코드를 추가하자.

WebViewController

var search: String!/** 검색어 */
var url: String!/** url */
    
    
/** life cycle */
override func viewDidLoad() {
	super.viewDidLoad()
        
    /** 네비게이션 바 타이틀 */
    self.navigationItem.title = search
}

search, url 프로퍼티를 통해 ViewController로 부터 값을 전달받아 저장한다.

잘 동작하는 걸 확인했으니, 이제 WebView를 구현해보자.


WebViewController

Configuration

먼저 WebKit를 import 하고, 웹 뷰를 담을 프로퍼티를 선언한다.

WebViewController

import UIKit
import WebKit

class WebViewController: UIViewController {

    @IBOutlet weak var webViewGroup: UIView!/** 배경 뷰 */
    
    private var webView: WKWebView!/** 웹 뷰 */
    
    var search: String!/** 검색어 */
    var url: String!/** url */
    
    
    /** life cycle */
    override func viewDidLoad() {
        super.viewDidLoad()
        
        /** 네비게이션 바 타이틀 */
        self.navigationItem.title = search
	}
}

webView를 통해 웹 뷰를 그리고, 미리 그려둔 뷰인 webViewGroup에 추가할 예정이다.


이제 웹 뷰를 구현하기 위해, viewDidLoad()에 다음 코드를 추가하자.

let preferences = WKPreferences()
/** javaScript 사용 설정 */
preferences.javaScriptEnabled = true
/** 자동으로 javaScript를 통해 새 창 열기 설정 */
preferences.javaScriptCanOpenWindowsAutomatically = true

WKPreferences는 웹 뷰에 대한 기본 속성을 캡슐화한 클래스이다.
java나 javaScript 설정이 가능하다.


let contentController = WKUserContentController()
/** 사용할 메시지 등록 */
contentController.add(self, name: "bridge")

WKUserContentController는 웹 뷰와 javaScript 간의 상호작용을 관리하는 클래스이다. 즉, 웹 뷰와 javaScript 사이의 중간 다리인 셈.

javaScript에서 앱으로 메시지를 전달할 때는 add() 함수를 사용해 메시지의 이름을 설정한다.


let configuration = WKWebViewConfiguration()
/** preference, contentController 설정 */
configuration.preferences = preferences
configuration.userContentController = contentController

WKWebViewConfiguration는 웹 뷰가 맨 처음 초기화될 때 호출되며, 웹 뷰가 생성된 이후에는 이 클래스를 통해 속성을 변경할 수 없다.


이제 웹 뷰를 생성해보자.
webView = WKWebView(frame: self.view.bounds, configuration: configuration)

웹 뷰에 띄울 url을 설정해주자.

var components = URLComponents(string: url)!
components.queryItems = [ URLQueryItem(name: "query", value: search) ]

url을 추가할 때, 나와 같이

"https://m.search.naver.com/search.naver?sm=mtp_hty.top&where=m&query=\(search)"

라고 할 생각이었다면...
지금 바로 손을 들어 머리를 쓰다듬어주자. 머리를 때릴 순 없으니까


맨 처음에 ViewController에 이렇게 구현했다가 1시간 버렸다. 백날 옵셔널 바인딩 해도 UnWrapped Optional 에러가 떠서..

아무튼! 쿼리문을 작성할 때는 위와 같이 URLQueryItem() 클래스를 사용해야 한다.

저처럼 바보 같은 실수 하지 마시길.. 저 같은 멍청이가 있다면요.. (시무룩)

마지막으로 request 객체를 생성하면 url 끝!

let request = URLRequest(url: components.url!)

Delegate 등록

이제 Delegate를 추가해보자.

webView.uiDelegate = self
webView.navigationDelegate = self

위 코드만 추가하면,

Cannot assign value of the type 'WebViewController' to type '~~Delegate?' 

라는 에러가 뜨는데, 필수로 구현해야하는 함수가 빠졌기 때문이다.


클래스 외부에 extension을 추가하자.
extension WebViewController: WKNavigationDelegate {
    
    public func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
    
		print("\(navigationAction.request.url?.absoluteString ?? "")" )
        
        decisionHandler(.allow)
    }
}

extension WebViewController: WKUIDelegate {

    public func webView(_ webView: WKWebView, runJavaScriptAlertPanelWithMessage message: String, initiatedByFrame frame: WKFrameInfo, completionHandler: @escaping () -> Void) {
        
    }
}

decidePolicyFor 함수는 유효한 도메인인지 체크한다.


추가한 김에, WKScriptMessageHandler도 추가하자.
extension WebViewController: WKScriptMessageHandler {
    
    func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
        
        print(message.name)
    }
}

아까 위에서 add()함수를 사용해 메시지를 등록했다면, WKScriptMessageHandler 클래스를 통해 javaScript(즉, 웹 페이지)로 부터 메시지를 받아올 수 있다.


View 설정

이제 거의 다 왔다...

마지막으로 생성한 webView를 등록하기만 하면 된다.

webViewGroup.addSubview(webView)
setAutoLayout(from: webView, to: webViewGroup)

AutoLayout은 따로 함수를 구현해 설정해주었다.
/** auto leyout 설정 */
public func setAutoLayout(from: UIView, to: UIView) {
        
	from.translatesAutoresizingMaskIntoConstraints = false
    NSLayoutConstraint.init(item: from, attribute: .leading, relatedBy: .equal, toItem: to, attribute: .leading, multiplier: 1.0, constant: 0).isActive = true
    NSLayoutConstraint.init(item: from, attribute: .trailing, relatedBy: .equal, toItem: to, attribute: .trailing, multiplier: 1.0, constant: 0).isActive = true
    NSLayoutConstraint.init(item: from, attribute: .top, relatedBy: .equal, toItem: to, attribute: .top, multiplier: 1.0, constant: 0).isActive = true
    NSLayoutConstraint.init(item: from, attribute: .bottom, relatedBy: .equal, toItem: to, attribute: .bottom, multiplier: 1.0, constant: 0).isActive = true
    view.layoutIfNeeded()
}

부모 뷰인 webViewGroupwebView의 top, leading, trailing, bottom을 맞춰준다.


이제, webView에 requset 객체를 통해 웹 페이지를 로드해보자.
webView.load(request)

정말 마지막으로.. 애니메이션까지 곁들여 보자.
webView.alpha = 0
UIView.animate(withDuration: 0.5, delay: 0, options: .curveEaseIn, animations: {
	self.webView.alpha = 1
}) { _ in
            
}

정말루 다 했으니까 실행 해보자.

제대로 동작한다!!

번외 ㅋ

근데 이렇게 하면 입력 뒤에 키보드가 내려가지 않아서 불편하다.

빈 화면을 터치하면 키보드가 내려가는 코드를 ViewController에 추가하자.

override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
	self.view.endEditing(true)
}

편안..


전체 코드

ViewController

import UIKit

class ViewController: UIViewController {

    @IBOutlet weak var tvSearch: UITextField!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
    }
    
    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        self.view.endEditing(true)
    }

    /** 웹 뷰로 이동 */
    @IBAction func tappedSearch(_ sender: Any) {
        let text: String = tvSearch.text!
        
        if let navigationController = self.navigationController {
            
            if !(navigationController.topViewController?.description.contains("WebViewController"))! {
                let storyBoard: UIStoryboard = UIStoryboard(name: "Main", bundle: nil)
                let viewController = storyBoard.instantiateViewController(withIdentifier: "WebViewController") as! WebViewController
                
                viewController.search = text
                viewController.url = "https://m.search.naver.com/search.naver?sm=mtp_hty.top&where=m&"
                
                navigationController.pushViewController(viewController, animated: true)
            }
        }
    }
}

WebViewController

import UIKit
import WebKit

class WebViewController: UIViewController {

    @IBOutlet weak var webViewGroup: UIView!/** 배경 뷰 */
    
    private var webView: WKWebView!/** 웹 뷰 */
    
    var search: String!/** 검색어 */
    var url: String!/** url */
    
    
    /** life cycle */
    override func viewDidLoad() {
        super.viewDidLoad()
        
        /** 네비게이션 바 타이틀 */
        self.navigationItem.title = search
        
        let preferences = WKPreferences()
        preferences.javaScriptEnabled = true
        preferences.javaScriptCanOpenWindowsAutomatically = true
        
        let contentController = WKUserContentController()
        contentController.add(self, name: "bridge")
        
        let configuration = WKWebViewConfiguration()
        configuration.preferences = preferences
        configuration.userContentController = contentController
        
        webView = WKWebView(frame: self.view.bounds, configuration: configuration)
        
        var components = URLComponents(string: url)!
        components.queryItems = [ URLQueryItem(name: "query", value: search) ]
        
        let request = URLRequest(url: components.url!)
        
        webView.uiDelegate = self
        webView.navigationDelegate = self
        webViewGroup.addSubview(webView)
        setAutoLayout(from: webView, to: webViewGroup)
        webView.load(request)
        
        webView.alpha = 0
        UIView.animate(withDuration: 0.5, delay: 0, options: .curveEaseIn, animations: {
            self.webView.alpha = 1
        }) { _ in
            
        }
    }
    
    /** auto leyout 설정 */
    public func setAutoLayout(from: UIView, to: UIView) {
        
        from.translatesAutoresizingMaskIntoConstraints = false
        NSLayoutConstraint.init(item: from, attribute: .leading, relatedBy: .equal, toItem: to, attribute: .leading, multiplier: 1.0, constant: 0).isActive = true
        NSLayoutConstraint.init(item: from, attribute: .trailing, relatedBy: .equal, toItem: to, attribute: .trailing, multiplier: 1.0, constant: 0).isActive = true
        NSLayoutConstraint.init(item: from, attribute: .top, relatedBy: .equal, toItem: to, attribute: .top, multiplier: 1.0, constant: 0).isActive = true
        NSLayoutConstraint.init(item: from, attribute: .bottom, relatedBy: .equal, toItem: to, attribute: .bottom, multiplier: 1.0, constant: 0).isActive = true
        view.layoutIfNeeded()
    }

}

extension WebViewController: WKNavigationDelegate {
    
    public func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
        
        print("\(navigationAction.request.url?.absoluteString ?? "")" )
        
        decisionHandler(.allow)
    }
}

extension WebViewController: WKUIDelegate {
    
    public func webView(_ webView: WKWebView, runJavaScriptAlertPanelWithMessage message: String, initiatedByFrame frame: WKFrameInfo, completionHandler: @escaping () -> Void) {
        
    }
}

extension WebViewController: WKScriptMessageHandler {
    
    func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
        
        print(message.name)
    }
}

깃허브에도 있어유
Toy-Project-iOS/WebView


마무리

현업에서 웹 뷰를 정말 많이 사용한다고 한다. 앱을 보면 개인정보처리방침이나 서비스 이용약관 등은 다 웹 뷰로 나타내야 한다.

다음에는 javaScript와 연결해서 메시지를 주고받는 것까지 하고 싶은데... javaScript 코드는 내가 짜야겠지?.. ㅎㅎ
착잡

profile
나 애기 개발자 👶🏻

2개의 댓글

comment-user-thumbnail
2023년 5월 20일

덕분에 한 5시간은 아꼈습니다. 감사합니다!

답글 달기
comment-user-thumbnail
2023년 11월 25일

굳 정보👍

답글 달기