오늘은
우편번호 검색 페이지 무한정 호출해 보기 예제 수정편 입니다.

일상적인 업무에서 주소 검색은 언제든 진행 되어야 합니다. 다만, API 을 사용하게 되면 역시나, 비용 발생이 되는 관계로
다가 무한정 호출 가능한 우편번호/주소 검색 기능을 구현하기 위해... 카카오가 제공하는 주소 검색 스크립트을 이용하는 방법을
정리해 보려고 합니다.
예전 버전을 적용해 사용했던 기억이 있지만, 현재는 해당 기능도 업데이트 되어 수정이 필요 했습니다.
https://postcode.map.kakao.com/guide
Kakao 우편번호 서비스
우편번호 검색과 도로명 주소 입력 기능을 너무 간단하게 적용할 수 있는 방법. Kakao 우편번호 서비스를 이용해보세요. 어느 사이트에서나 무료로 제약없이 사용 가능하답니다.
postcode.map.kakao.com
위 페이지에 기술된 내용을 참고 했습니다.
아래 스크립트 부분은 blogger.com 의 페이지에서 사용할 수 있게 추가한 부분 입니다. html 코드을 지원하는 블로그 페이지는 어디든 지원이 될 것으로 판단 됩니다.
<!--카카오 우편번호 서비스 불러오기-->
<script src="//t1.kakaocdn.net/mapjsapi/bundle/postcode/prod/postcode.v2.js"></script>
<!--주소검색 버튼과 결과 표시 영역-->
<button onclick="execDaumPostcode()">주소 검색</button>
<div id="result"></div>
<script>
function execDaumPostcode() {
new kakao.Postcode({
oncomplete: function(data) {
// 선택된 주소 정보 표시
const roadAddr = data.roadAddress; // 도로명 주소
const jibunAddr = data.jibunAddress; // 지번 주소
const zonecode = data.zonecode; // 우편번호
const address = data.roadAddress || data.address ;
document.getElementById("result").innerHTML =
"<p><b>도로명 주소:</b> " + roadAddr + "</p>" +
"<p><b>지번 주소:</b> " + jibunAddr + "</p>" +
"<p><b>우편번호:</b> " + zonecode + "</p>";
// 이 부분은 안드로이드 에서 참조 하도록 하기 위함이라 web 에서는 에러 발생됨
window.Android.processDATA(address)
}
}).open();
}
</script>
아래 코드는 kotlin 코드로 작성된 주소 검색 페이지 activity 입니다. 이 페이지의 기능은 주소검색 script 을 androidview 로 호출하고, 결과을 받아 원래 호출 했던 위치로 return 해 주는 기능만 존재 합니다.
import android.annotation.SuppressLint
import android.content.Intent
import android.os.Bundle
import android.util.Log
import android.os.Message
import android.net.Uri
import android.webkit.JavascriptInterface
import android.webkit.WebChromeClient
import android.webkit.WebResourceRequest
import android.webkit.WebView
import android.webkit.WebViewClient
import android.widget.FrameLayout
import androidx.activity.ComponentActivity
import androidx.activity.compose.BackHandler
import androidx.activity.compose.setContent
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Close
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.em
import androidx.compose.ui.viewinterop.AndroidView
import com.billcoreatech.multichat416.R
import com.billcoreatech.multichat416.ui.theme.Multichat416Theme
import com.billcoreatech.multichat416.ui.theme.appColorScheme
import com.billcoreatech.multichat416.ui.theme.appTypography
class AddressFindActivity : ComponentActivity() {
inner class MyJavaScriptInterface {
@JavascriptInterface
fun processDATA(data: String?) {
returnAddress(data)
}
@JavascriptInterface
fun postMessage(data: String?) {
returnAddress(data)
}
}
fun returnAddress(data: String?) {
Log.e("TAG", "returnAddress ${data}")
val intent = Intent()
intent.putExtra("data", data)
setResult(AppCompatActivity.RESULT_OK, intent)
finish()
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
Multichat416Theme(dynamicColor = true) {
// A surface container using the 'background' color from the theme
Surface(
modifier = Modifier.fillMaxSize(),
) {
Scaffold(
modifier = Modifier
.fillMaxSize()
.padding(top = 20.dp), topBar = {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(5.dp),
horizontalArrangement = Arrangement.Start,
verticalAlignment = Alignment.CenterVertically
) {
IconButton(onClick = {
Log.e("", "backArrow")
finish()
}) {
Icon(
imageVector = Icons.Default.Close,
contentDescription = "Close"
)
}
Spacer(modifier = Modifier.padding(5.dp))
Text(
text = stringResource(R.string.foundAddress),
color = appColorScheme.primary,
lineHeight = 1.33.em,
style = appTypography.titleLarge,
modifier = Modifier
.fillMaxWidth()
.wrapContentHeight(align = Alignment.CenterVertically)
)
}
}, bottomBar = {
}
) { innerPadding ->
Column(modifier = Modifier.padding(innerPadding).fillMaxSize()) {
WebViewForAddress(this@AddressFindActivity,
doFinish = {
val intent = Intent()
setResult(AppCompatActivity.RESULT_CANCELED, intent)
finish()
},
)
}
}
}
}
}
}
}
@SuppressLint("JavascriptInterface", "SetJavaScriptEnabled")
@Composable
fun WebViewForAddress(addressFindActivity: AddressFindActivity, doFinish:() -> Unit) {
val blogspot = "https://billcoreatech.blogspot.com/2026/06/blog-post_480.html"; // "https://billcoreatech.blogspot.com/2022/06/blog-post.html"
var webViewContainer by remember { mutableStateOf<FrameLayout?>(null) }
var mainWebView by remember { mutableStateOf<WebView?>(null) }
var webView by remember { mutableStateOf<WebView?>(null) }
var canGoBack by remember { mutableStateOf(false) }
var isPostcodeRequested by remember { mutableStateOf(false) }
AndroidView(
modifier = Modifier.fillMaxSize(),
factory = { context ->
val container = FrameLayout(context)
webViewContainer = container
fun WebView.configureForAddressSearch() {
settings.run {
javaScriptEnabled = true
domStorageEnabled = true
javaScriptCanOpenWindowsAutomatically = true
setSupportMultipleWindows(true)
}
val javascriptInterface = addressFindActivity.MyJavaScriptInterface()
addJavascriptInterface(javascriptInterface, "Android")
addJavascriptInterface(javascriptInterface, "android")
addJavascriptInterface(javascriptInterface, "ReactNativeWebView")
webViewClient = object : WebViewClient() {
override fun shouldOverrideUrlLoading(
view: WebView,
request: WebResourceRequest
): Boolean {
return handleAddressCallbackUrl(
url = request.url,
addressFindActivity = addressFindActivity
)
}
override fun onPageFinished(view: WebView, url: String?) {
canGoBack = view.canGoBack()
if (!isPostcodeRequested) {
isPostcodeRequested = true
view.evaluateJavascript("execDaumPostcode();", null)
}
}
}
webChromeClient = object : WebChromeClient() {
override fun onCreateWindow(
view: WebView,
isDialog: Boolean,
isUserGesture: Boolean,
resultMsg: Message
): Boolean {
val popupWebView = WebView(view.context).apply {
configureForAddressSearch()
webChromeClient = object : WebChromeClient() {
override fun onCloseWindow(window: WebView) {
container.removeView(window)
window.destroy()
webView = view
canGoBack = view.canGoBack()
}
}
}
container.addView(
popupWebView,
FrameLayout.LayoutParams(
FrameLayout.LayoutParams.MATCH_PARENT,
FrameLayout.LayoutParams.MATCH_PARENT
)
)
webView = popupWebView
val transport = resultMsg.obj as WebView.WebViewTransport
transport.webView = popupWebView
resultMsg.sendToTarget()
return true
}
}
}
WebView(context).apply {
configureForAddressSearch()
loadUrl(blogspot, emptyMap())
mainWebView = this
webView = this
container.addView(
this,
FrameLayout.LayoutParams(
FrameLayout.LayoutParams.MATCH_PARENT,
FrameLayout.LayoutParams.MATCH_PARENT
)
)
}
container
},
update = {
canGoBack = webView?.canGoBack() == true
},
)
BackHandler(enabled = true) {
val view = webView
if (view != null && canGoBack) {
view.goBack()
} else if (view != null && view !== mainWebView) {
webViewContainer?.removeView(view)
view.destroy()
webView = mainWebView
canGoBack = mainWebView?.canGoBack() == true
} else {
doFinish()
}
}
DisposableEffect(Unit) {
onDispose {
webViewContainer?.let { container ->
for (index in container.childCount - 1 downTo 0) {
(container.getChildAt(index) as? WebView)?.destroy()
}
container.removeAllViews()
}
mainWebView = null
webView = null
webViewContainer = null
}
}
}
private fun handleAddressCallbackUrl(
url: Uri,
addressFindActivity: AddressFindActivity
): Boolean {
if (url.scheme != "multichat416" || url.host != "address") {
return false
}
val data = url.getQueryParameter("data").orEmpty()
addressFindActivity.returnAddress(data)
return true
}
코드 사용 예제는 훗날 추가해 보겠습니다. 개발을 마무리 하고 나서.
'그냥글쓰기' 카테고리의 다른 글
| 온 우주가 널 응원해 (0) | 2026.06.23 |
|---|---|
| 2026년 6월 4일 나의금전운에 대해서 (0) | 2026.06.04 |
| 국제정세 분석] 현재 미국과 이란의 전쟁상황 총정리 및 향후 전망 (1) | 2026.03.24 |
| **“개발자 전자책이 실제로 잘 팔리는 제목 20개”** (1) | 2026.03.15 |
| **“안드로이드 개발자가 전자책으로 월 100만 원 만들기 가장 쉬운 주제 TOP5”** (0) | 2026.03.13 |