본 포스팅은 Swift 5.3 기준으로 작성되었습니다.
Intro
저는 요새 Market Kurly 앱을 클론하는 프로젝트를 진행하고 있는데요. 회원가입 부분에서 Kakao 우편변호 찾기 기능을 지원하더라고요. 사용자로서는 분명히 사용해본 경험이 있는 것 같은데 막상 구현하는 방법은 배운적도 생각해본 적도 없었습니다 ㅎㅎ 어쨌든 중요한건 지금 어떻게든 구현을 해야한다는 것이고, 마켓컬리앱이 아니더라도 회원가입을 필요로하는 서비스라면 많이 지원하고 있는 기능이니까 잘 공부해서 정리해두면 나중에 쓸데가 많을 것 같아 포스팅을 남깁니다.
서비스 확인하기
Kakao 우편번호 서비스 웹페이지 에 접속해보면 평소 우리가 회원가입을 할 때 자주 볼 수 있었던 화면이 하나 보일거에요.
먼저 안내를 읽어보니 모든 조건에서 무료로 사용가능하고 별다른 요구사항도 없는 것 같네요. 그냥 가져다쓰고 WebView 형태로 앱 내에서 띄워주면 될 것 같습니다. 그럼 바로 구현을 시작해볼까요? ㅎㅎ
구현하기
저도 완전히 처음 시도해보는 과정이다보니 어디서부터 접근해야할지 감이 오지않아 검색할 수 있는만큼 최대한 찾아봤지만 Swift 로는 카카오 우편번호 서비스 구현방법을 상세히 정리해둔 곳을 찾지 못해 공부하는데 애를 정말 많이 먹었습니다.
아무튼 구현을 위한 순서를 정리해보면 우편번호 서비스가 구현된 웹페이지 주소가 있어야하고, 그 주소를 기반으로 Xcode 에서 WebView 를 이용해 웹페이지를 띄워주어야 합니다. 그래서 오늘 포스팅은 이런 순서로 진행될거에요.
- Github Pages 를 사용해 카카오 우편번호 서비스 웹페이지 구현
- Xcode 에서 WebView 를 사용해 웹페이지 띄우기
그럼 순서대로 차근차근 구현을 시작해볼게요.
Github Pages 를 사용해 카카오 우편번호 서비스 웹페이지 만들기
카카오 우편번호 서비스를 웹페이지에 구현할 수 있는 방법은 여러가지가 있겠지만 이번 포스팅에서는 Github Pages 를 사용해 웹페이지를 생성하겠습니다. Github 에 접속해서 Repo 를 하나 만들어주세요. 저는 Kakao-Postcode
라고 생성했습니다.
생성된 Repo 의 Setting 카테고리로 들어가 스크롤을 내리다보면 Github Pages 라는 항목이 보일거에요. 이곳에서 Source 로 master
branch 를 선택하고 save 버튼을 눌러줍니다.
Save 버튼을 누르면 페이지가 리프레시 되는데 다시 Github Pages 항목이 있는 곳으로 돌아가보면 우리가 생성한 웹페이지의 주소를 얻을 수 있습니다.
생성된 주소로 들어가보면 404 에러 페이지가 보일거에요. 지금은 우리가 아무 내용도 입력하지 않았으므로 이렇게 뜨는게 정상입니다.
다시 Github 으로 돌아와 Create new file
을 클릭합니다.
그리고 파일의 이름을 정확하게 index.html
이라고 입력해주세요. Github Pages 는 index.html
이라는 파일을 자동적으로 찾아서 웹페이지로 생성해줍니다.
이제 웹페이지 구현에 필요한 html 코드를 입력하면 됩니다. 포스팅 초반에 보았던 공식 카카오 우편번호 서비스 웹페이지를 보면 통합로딩방식 항목을 찾을 수 있고 이걸 갖다 붙여넣으면 바로 구현이 되긴하지만 POP-UP 방식으로 구현된다는 문제점이 있습니다.
그래서 주소를 입력했을 때 팝업 방식이 아닌 페이지가 바로 보일 수 있도록 구현된 코드가 필요합니다. index.html
본문에 아래 코드를 입력하고 저장해주세요.
<!DOCTYPE html>
<html lang="ko">
<head>
<title>주소 찾기</title>
<meta charset="UTF-8" />
<meta
name="viewport"
content="width=device-width,height=device-height,initial-scale=1.0"
/>
</head>
<body onload="execDaumPostcode()">
<div
id="layer"
style="display:block; position:absolute; overflow:hidden; z-index:1; -webkit-overflow-scrolling:touch; "
></div>
<script src="https://spi.maps.daum.net/imap/map_js_init/postcode.v2.js"></script>
<script>
window.addEventListener("message", onReceivedPostMessage, false);
function onReceivedPostMessage(event) {
//..ex deconstruct event into action & params
var action = event.data.action;
var params = event.data.params;
console.log("onReceivedPostMessage " + event);
}
function onReceivedActivityMessageViaJavascriptInterface(json) {
//..ex deconstruct data into action & params
var data = JSON.parse(json);
var action = data.action;
var params = data.params;
console.log("onReceivedActivityMessageViaJavascriptInterface " + event);
}
function postMessageToiOS(postData) {
window.webkit.messageHandlers.callBackHandler.postMessage(postData);
}
var element_layer = document.getElementById("layer");
function execDaumPostcode() {
new daum.Postcode({
oncomplete: function (data) {
var jibunAddress = "";
if (data.jibunAddress == "") {
jibunAddress = data.autoJibunAddress;
} else if (data.autoJibunAddress == "") {
jibunAddress = data.jibunAddress;
}
var postData = {
roadAddress: data.roadAddress,
jibunAddress: jibunAddress,
zonecode: data.zonecode,
};
window.postMessageToiOS(postData);
},
width: "100%",
height: "100%",
}).embed(element_layer);
element_layer.style.display = "block";
initLayerPosition();
}
function initLayerPosition() {
var width = window.innerWidth || document.documentElement.clientWidth;
var height =
window.innerHeight || document.documentElement.clientHeight;
element_layer.style.width = width + "px";
element_layer.style.height = height + "px";
element_layer.style.left =
((window.innerWidth || document.documentElement.clientWidth) -
width) /
2 +
"px";
element_layer.style.top =
((window.innerHeight || document.documentElement.clientHeight) -
height) /
2 +
"px";
}
</script>
</body>
</html>
적용되는데 시간이 조금 필요하기 때문에 잠시 기다리고나서 Github Pages 주소로 들어가보면 우리가 구현하고자 했던 카카오 우편번호 서비스 화면이 보일거에요!
혹시 계속 기다려봐도 안된다면 Repo 의 이름을 한번 변경해주세요. 분명히 잘못된 부분이 없는데 계속 페이지 오류가 뜨길래 Repo 이름을 바꿔보니까 이후부터 웹페이지가 정상적으로 뜨기 시작했습니다.
잘 나오나요~? ㅎㅎ 이제 Xcode 에서 WebKit 을 이용해 이 페이지를 띄워주면 됩니다.
WebView 로 카카오 우편번호 서비스 웹페이지 표시하기
Xcode Project 를 하나 만들어 실습을 시작해볼게요. 먼저 간단하게 뷰를 세팅하겠습니다.
Setup View
View 위에 Button 과 Label 을 하나씩 올려놓았어요. Button 을 누르면 카카오 우편번호 서비스 창이 뜨고, 이곳에서 주소를 입력받은 뒤 Label 에 표시될 수 있도록 데이터를 전달해 볼거에요.
import UIKit
class ViewController: UIViewController {
// MARK: - Properties
let button = UIButton(type: .system)
let label = UILabel()
// MARK: - Lifecycle
override func viewDidLoad() {
super.viewDidLoad()
configureUI()
}
// MARK: - UI
private func configureUI() {
setContraints()
setAttributes()
}
private func setAttributes() {
button.setTitle("Button", for: .normal)
button.addTarget(self, action: #selector(handleButton(_:)), for: .touchUpInside)
label.text = "Label"
label.font = UIFont.systemFont(ofSize: 20)
}
private func setContraints() {
[button, label].forEach {
view.addSubview($0)
$0.translatesAutoresizingMaskIntoConstraints = false
}
NSLayoutConstraint.activate([
button.centerXAnchor.constraint(equalTo: view.centerXAnchor),
button.centerYAnchor.constraint(equalTo: view.centerYAnchor),
label.centerXAnchor.constraint(equalTo: view.centerXAnchor),
label.bottomAnchor.constraint(equalTo: button.topAnchor, constant: -40),
])
}
// MARK: - Selectors
@objc
private func handleButton(_ sender: UIButton) {
print(#function)
}
}
코드를 작성하고 Simulator 를 실행해보면 이렇게 보입니다.
카카오 우편번호 서비스 구현하기
기본적인 뷰 세팅은 마쳤으니 본격적으로 우편번호 서비스 구현을 위한 코드를 작성해보도록 할게요. 서비스가 보여질 새로운 UIViewController
를 하나 생성해줍니다. 저는 KakaoZipCodeVC
라고 만들었어요~
이제 이곳에 조금 전 Github Pages 를 사용해 미리 만들어둔 페이지를 WebView 를 사용해서 띄워줄거에요. 그러니까 WebView 를 사용하기 위해 WebKit
을 import 해주도록 할게요.
import WebKit
그리고 WKWebView
인스턴스를 optional 로 미리 하나 생성하고, webView 가 로딩될 동안 보여줄 UIActivityIndicatorView
도 인스턴스를 생성하겠습니다. 나중에 사용자가 선택한 주소를 저장할 변수도 하나 생성할게요.
var webView: WKWebView?
let indicator = UIActivityIndicatorView(style: .medium)
var address = ""
인스턴스 생성까지 마쳤고 Java Script 를 읽을 수 있게 도와주는 WKUserContentController
를 생성해야합니다. 처음보는 클래스니까 공식문서를 먼저 한번 볼까요.
특별한 내용은 없고 Java Script 가 메세지를 Post 할 수 있게 도와준다고 나와있네요. 우리가 Github Pages 에 작성한 html 이 Java Script 를 불러오는 내용이니까 이걸 쓰는게 맞을 것 같아요.
let contentController = WKUserContentController()
인스턴스를 생성해줬으니 이제 Java Script 가 보내는 메세지를 읽을 수 있어야 합니다. 여기서 메세지란 사용자가 선택한 정보가 무엇인지 우리가 받는 것을 뜻하겠죠?
아무튼 이 메세지를 받으려면 WKUserContentController
가 제공하는 add(_:name:)
method 를 사용해야 하는데요.
이 method 를 사용하기 위해서는 WKScriptMessageHandler
프로토콜을 채택해주어야 합니다. 그리고 이 프로토콜을 채택하면 필수적으로 구현해야하는 userContentController(_:didReceive:)
method 도 구현하도록 할게요.
프로토콜을 채택하기 전에 add(_:name:)
method 의 공식문서를 잠시 살펴봤는데…
Script Message Handler 를 추가한다고 합니다. Hanlder 가 있어야 Java Script 가 보내는 Message 를 정상적으로 수신할 수 있는 것 같아요.
WKScriptMessageHandler 프로토콜 채택
WKScriptMessageHandler
를 채택하고 필수함수를 구현했습니다.
extension KakaoZipCodeVC: WKScriptMessageHandler {
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
}
}
직접 실험을 해보니 이 함수가 호출되는 타이밍은 유저가 주소를 검색하고 어떤 값을 최종적으로 선택했을 때 호출되게 됩니다.
프로토콜을 채택했으니 add(_:name:)
method 를 사용할 수 있게 되었어요. 다시 contentController
를 인스턴스화 한곳으로 돌아가 코드를 입력해주세요.
contentController.add(self, name: "callBackHandler")
이제 초반에 optional 로 생성해두었던 WKWebView
를 인스턴스화 해주어야하는데요. 그 전에 먼저 방금 생성한 contentController
를 WKWebView
와 연결할 수 있도록 도와주는 WKWebViewConfiguration
이 필요합니다.
let configuration = WKWebViewConfiguration()
configuration.userContentController = contentController
webView
를 인스턴스화 해줍니다. 레이아웃은 아래에서 오토레이아웃으로 잡을거니까 .zero
로 세팅하고 방금 만들어둔 configuration
을 연결합니다.
webView = WKWebView(frame: .zero, configuration: configuration)
webView
가 로드될 때 indicator 를 보여줄 수 있도록 WKNavigationDelegate
프로토콜을 채택하고 코드를 구현해보겠습니다.
extension KakaoZipCodeVC: WKNavigationDelegate {
func webView(_ webView: WKWebView, didStartProvisionalNavigation navigation: WKNavigation!) {
indicator.startAnimating()
}
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
indicator.stopAnimating()
}
}
함수의 Parameter 이름만 봐도 알 수 있듯이
webView(_:didStartProvisionalNavigation:)
는webView
의 로드가 시작될 때webView(_:didFinish:)
는webView
의 로드가 끝날 때
각각 호출되게 됩니다. 그러니까 indicator 는 각각의 함수에 시작과 끝을 세팅하면 되겠죠?
Delegate 위임도 잊지말고 해주세요.
webView?.navigationDelegate = self
이제 webView
가 우편번호 서비스 웹페이지를 띄울 수 있도록 URL 만 전달해주면 됩니다. guard 문을 사용해 optional 을 unwrapping 해주고 URLRequest
를 생성해 webView
가 해당 URL 을 load 할 수 있도록 넘겨줍니다.
guard let url = URL(string: "https://kasroid.github.io/Kakao-Postcode/"),
let webView = webView
else { return }
let request = URLRequest(url: url)
webView.load(request)
indicator.startAnimating()
마지막으로 오토레이아웃을 잡고 Simulator 를 실행시켜 보도록할게요.
guard let webView = webView else { return }
view.addSubview(webView)
webView.translatesAutoresizingMaskIntoConstraints = false
webView.addSubview(indicator)
indicator.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
webView.topAnchor.constraint(equalTo: view.topAnchor),
webView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
webView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
webView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
indicator.centerXAnchor.constraint(equalTo: webView.centerXAnchor),
indicator.centerYAnchor.constraint(equalTo: webView.centerYAnchor),
])
한가지 더!! ViewController
로 돌아가 handleButton
함수에 KakaoZipCodeVC
를 Present 할 수 있도록 구현합니다.
let nextVC = KakaoZipCodeVC()
present(nextVC, animated: true)
이제 Simulator 를 실행시키고 Button 을 눌러보세요.
Success!!
데이터 전달하기
정상적으로 우편번호 서비스를 띄우는데 성공했으니 이제 사용자가 입력한 데이터를 받아올 일만 남았습니다.
userContentController(_:didReceive:)
함수 내부에 코드를 구현할게요.
if let data = message.body as? [String: Any] {
address = data["roadAddress"] as? String ?? ""
}
guard let previousVC = presentingViewController as? ViewController else { return }
previousVC.label.text = address
self.dismiss(animated: true, completion: nil)
지금은 “roadAddress” 라는 항목만 추출했지만 message.body
를 print 해보면 다음과 같은 값들을 가져오는 것을 확인할 수가 있습니다.
{
jibunAddress = "\Uacbd\Uae30 \Uc131\Ub0a8\Uc2dc \Ubd84\Ub2f9\Uad6c \Uc6b4\Uc911\Ub3d9 1017-3";
roadAddress = "\Uacbd\Uae30 \Uc131\Ub0a8\Uc2dc \Ubd84\Ub2f9\Uad6c \Ud310\Uad50\Ub85c 35";
zonecode = 13467;
}
필요에 따라 지번 주소, 도로명 주소, 우편번호를 가져와서 사용할 수 있어요.
이제 Simulator 를 실행하고 원하는 주소를 검색한 뒤에 클릭하면 내가 선택한 데이터가 ViewController
의 label
로 전달되는 것을 확인할 수 있습니다.
Wrap Up
처음 시도할 떄는 조금 복잡하게 느껴질 수 있지만 한번만 잘 따라해보면 그렇게까지 어려운 내용은 아니에요. 워낙 어려운게 많아야지…
Github Pages 로 한번 구현해두면 계속해서 사용할 수 있으니 꼭 한번 만들어두고 앞으로 다양한 곳에서 활용해보세요. 그럼 오늘도 공부하느라 수고 많으셨습니다!
import UIKit
import WebKit
class KakaoZipCodeVC: UIViewController {
// MARK: - Properties
var webView: WKWebView?
let indicator = UIActivityIndicatorView(style: .medium)
var address = ""
// MARK: - Lifecycle
override func viewDidLoad() {
super.viewDidLoad()
configureUI()
}
// MARK: - UI
private func configureUI() {
view.backgroundColor = .white
setAttributes()
setContraints()
}
private func setAttributes() {
let contentController = WKUserContentController()
contentController.add(self, name: "callBackHandler")
let configuration = WKWebViewConfiguration()
configuration.userContentController = contentController
webView = WKWebView(frame: .zero, configuration: configuration)
self.webView?.navigationDelegate = self
guard let url = URL(string: "https://kasroid.github.io/Kakao-Postcode/"),
let webView = webView
else { return }
let request = URLRequest(url: url)
webView.load(request)
indicator.startAnimating()
}
private func setContraints() {
guard let webView = webView else { return }
view.addSubview(webView)
webView.translatesAutoresizingMaskIntoConstraints = false
webView.addSubview(indicator)
indicator.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
webView.topAnchor.constraint(equalTo: view.topAnchor),
webView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
webView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
webView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
indicator.centerXAnchor.constraint(equalTo: webView.centerXAnchor),
indicator.centerYAnchor.constraint(equalTo: webView.centerYAnchor),
])
}
}
extension KakaoZipCodeVC: WKScriptMessageHandler {
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
if let data = message.body as? [String: Any] {
address = data["roadAddress"] as? String ?? ""
}
guard let previousVC = presentingViewController as? ViewController else { return }
previousVC.label.text = address
self.dismiss(animated: true, completion: nil)
}
}
extension KakaoZipCodeVC: WKNavigationDelegate {
func webView(_ webView: WKWebView, didStartProvisionalNavigation navigation: WKNavigation!) {
indicator.startAnimating()
}
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
indicator.stopAnimating()
}
}