[Flutter] 다음 우편번호 API(주소 검색) 사용하기 - 2편

Tyger·2023년 2월 12일
0

Flutter

목록 보기
19/57

다음 우편번호 API(주소 검색) 사용하기 - 2편

다음 우편번호 API(주소 검색) 사용하기 - 1편

flutter_inappwebview | Flutter Package

이번 글에서는 다음 주소 검색을 직접 개발해 보도록 하겠다.

개발에 앞서 구현해야 하는 기능에 대해서 간단하게 살펴보면, 먼저 다음 주소 관련 html 파일을 만들어야 하고, flutter_inappwebview의 localHost 구동에 대해서도 살펴보아야 한다.

Flutter

AndroidManifest

인터넷 관련 권한이 없다면 릴리즈 모드에서 작동을 하지 않기에 아래에 권한 추가를 해주자.

project > android > app > main > AndroidMenifest.xml

    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>

dependencies

dependencies:
    flutter_inappwebview: ^5.7.2+3

UI

이전 시간에 사용했던 UI 구조와 같게 만들었는데, 차이점은 데이터 관련 객체만 변경되었다.

검색을 통해 받아온 주소를 노출시켜주는 부분이다.

Expanded(
            child: ListView(
              children: [
                if (_dataModel != null) ...[
                  _text("Address", _dataModel!.address),
                  _text("Road Address", _dataModel!.roadAddress),
                  _text("Jibun Address", _dataModel!.jibunAddress),
                  _text("Sido", _dataModel!.sido),
                  _text("Sigungu", _dataModel!.sigungu),
                  _text("B Name", _dataModel!.bname),
                  _text("Road Name", _dataModel!.roadname),
                  _text("Building Name", _dataModel!.buildingName),
                  _text("Address(EN)", _dataModel!.addressEnglish),
                  _text("Road Address(EN)", _dataModel!.roadAddressEnglish),
                  _text("Jibun Address(EN)", _dataModel!.jibunAddressEnglish),
                  _text("Sido(EN)", _dataModel!.sidoEnglish),
                  _text("Sigungu(EN)", _dataModel!.sigunguEnglish),
                  _text("B Name(EN)", _dataModel!.bnameEnglish),
                  _text("Road Name(EN)", _dataModel!.roadnameEnglish),
                  _text("Zonecode", _dataModel!.zonecode),
                  _text("Sigungu Code", _dataModel!.sigunguCode),
                  _text("B Code", _dataModel!.bcode),
                  _text("Building Code", _dataModel!.buildingCode),
                  _text("Roadname Code", _dataModel!.roadnameCode),
                  _text("Address Type", _dataModel!.addressType),
                  _text("Apertment", _dataModel!.apartment),
                  _text("User Language Type", _dataModel!.userLanguageType),
                  _text("User Selected Type", _dataModel!.userSelectedType),
                ],
              ],
            ),
          ),
 Padding _text(String title, String expain) {
    return Padding(
      padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 6),
      child: Row(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          SizedBox(
              width: MediaQuery.of(context).size.width * 0.31,
              child: Center(
                  child: Text(
                title,
                style: const TextStyle(
                    fontSize: 12, color: Color.fromRGBO(195, 195, 195, 1)),
              ))),
          Flexible(
            child: Text(
              expain,
              style: const TextStyle(fontSize: 13, fontWeight: FontWeight.bold),
            ),
          ),
        ],
      ),
    );
  }

직접 구현할 다음 주소 검색 웹뷰 페이지로 이동하고 데이터를 받아오기 위한 버튼에 해당하는 부분이다.

 Padding(
            padding: const EdgeInsets.symmetric(vertical: 24, horizontal: 20),
            child: GestureDetector(
              onTap: () {
                HapticFeedback.mediumImpact();
                Navigator.of(context)
                    .push(MaterialPageRoute(builder: (context) {
                  return const WebviewWithDaumPostWebview();
                })).then((value) {
                  if (value != null) {
                    setState(() {
                      _dataModel = value;
                    });
                  }
                });
              },
              child: Container(
                width: MediaQuery.of(context).size.width,
                decoration: BoxDecoration(
                  color: Colors.amber,
                  borderRadius: BorderRadius.circular(12),
                ),
                child: const Padding(
                  padding: EdgeInsets.symmetric(vertical: 12),
                  child: Center(
                    child: Text(
                      "Daum 주소 검색",
                      style: TextStyle(
                          color: Color.fromRGBO(41, 41, 41, 1),
                          fontSize: 18,
                          fontWeight: FontWeight.bold),
                    ),
                  ),
                ),
              ),
            ),
          ),

DaumPostModel

다음 주소와 관련된 정보를 받아오기 위한 모델이다.

class DaumPostModel {
  final String address;
  final String roadAddress;
  final String jibunAddress;
  final String sido;
  final String sigungu;
  final String bname;
  final String roadname;
  final String buildingName;
  final String addressEnglish;
  final String roadAddressEnglish;
  final String jibunAddressEnglish;
  final String sidoEnglish;
  final String sigunguEnglish;
  final String bnameEnglish;
  final String roadnameEnglish;
  final String zonecode;
  final String sigunguCode;
  final String bcode;
  final String buildingCode;
  final String roadnameCode;
  final String addressType;
  final String apartment;
  final String userLanguageType;
  final String userSelectedType;

  DaumPostModel(
      this.address,
      this.roadAddress,
      this.jibunAddress,
      this.sido,
      this.sigungu,
      this.bname,
      this.roadname,
      this.buildingName,
      this.addressEnglish,
      this.roadAddressEnglish,
      this.jibunAddressEnglish,
      this.sidoEnglish,
      this.sigunguEnglish,
      this.bnameEnglish,
      this.roadnameEnglish,
      this.zonecode,
      this.sigunguCode,
      this.bcode,
      this.buildingCode,
      this.roadnameCode,
      this.addressType,
      this.apartment,
      this.userLanguageType,
      this.userSelectedType);
}

Html

다음 주소 검색에 사용될 html 코드이다. 해당 코드는 assets으로 관리하여 접근하여야 한다.

assets 폴더가 없으면 Poject 구조 아래 새로 생성하여 바로 넣어주셔도 되고 아래와 같이 html 폴더를 만들어서 해당 폴더 아래에 넣어주면 된다.

project > assets > html > daum_postcode.html

  assets:
    - assets/html/daum_postcode.html
<html>
  <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
  <head>
      <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
      <title>주소 검색 페이지</title>
  </head>
  <style>
  html,body{ margin:0; padding:0; height:100%; width:100%; }
  #full-size{
    height: 100%;
    width: 100%;
    display: none;
    overflow:hidden; /* or overflow:auto; if you want scrollbars */
  }
  </style>
  <body>
  <div id="full-size">
  </div>
  </body>
  <script src="https://t1.daumcdn.net/mapjsapi/bundle/postcode/prod/postcode.v2.js"></script>
  <script type="text/javascript">
      const element_layer = document.getElementById('full-size');

      daum.postcode.load(function() {
          new daum.Postcode({
            oncomplete: function(data) {
              window.flutter_inappwebview.callHandler('onSelectAddress', data);
            },
            width : '100%',
            height : '100%',
            maxSuggestItems : 5,
            alwaysShowEngAddr: false,
            hideMapBtn: true,
            hideEngBtn: false,
        }).embed(element_layer);
        
        element_layer.style.display = 'block';
      });
  </script>
</html>

DaumPostWebView

이젠 다음 주소 검색 웹뷰에서 사용할 페이지 코드를 작성해보자.

먼저 flutter_inappwebview에서 사용할 InAppLocalhostServer와 InAppWebViewController를 선언해주자.
InAppLocalhostServer가 바로 로컬 서버를 구동하기 위해 선언한 것이다.

final InAppLocalhostServer _localhostServer = InAppLocalhostServer();
late InAppWebViewController _controller;

에러 처리와 로딩 인디케이터를 구현하기 위해 사용하는 변수로 필요하지 않다면 사용하지 않아도 상관없다.

 bool isLoading = true;
 bool isError = false;

페이지가 처음 로드될 때 위에서 선언한 localhostServer를 구동 시키자. 그리고 해당 페이지가 디스포즈될 때에 localHostServer의 구동을 닫아주어야 한다.
만약에 닫지 않았을 경우 앱 실행 내내 로컬 서버가 구동되는 불상사가 발생하기에 반드시 닫아주자.

  
  void initState() {
    super.initState();
    _localhostServer.start();
  }

  
  void dispose() {
    _localhostServer.close();
    super.dispose();
  }

아래와 같은 구조로 개발을 진행할 예정이다.

body : Stack(
	children : [
    ... 인앱 웹뷰
    ... 로딩 위젯
    ... 에러 위젯
    ]
)

InAppWebView 부분은 코드 하나씩 살펴보면서 만들어보자.

먼저 InAppWebView 위젯을 넣어주고 아래 initialUrlRequest 부분에 로컬 서버로 구동하여 위에서 만들었던 html 코드를 웹뷰로 생성해주자.

InAppWebView(
      initialUrlRequest: URLRequest(
         url: Uri.parse(
            "http://localhost:8080/assets/html/daum_postcode.html")),
            ...
     ),

onWebViewCreated는 웹뷰가 생성될 때 호출 되는 부분으로 여기서 InAppWebViewController를 할당해주자.
addJavaScriptHandler는 웹과 앱간의 자바 스크립트 채널을 수신받기 위한 기능으로 html 코드 안에 카카오가 제공하는 핸들러를 수신받을 수 있게된다. 핸들러 네임이 onSelectAddress이며, 리턴되는 args는 배열 안에 맵 형태의 객체로 리턴된다.
여기서 오는 데이터가 바로 주소 관련된 정보를 담고 있는 부분이다.

해당 핸들러가 호출될 때 네비게이터를 닫아주면서 이전 화면에 새로 생성한 DaumPostModel 객체를 넘겨주자.

InAppWebView(
		...
       onWebViewCreated: (controller) {
              _controller = controller;
              _controller.addJavaScriptHandler(
                  handlerName: 'onSelectAddress',
                  callback: (args) {
                    Map<String, dynamic> _fromMap = args.first;
                    DaumPostModel _data = _dataSetting(_fromMap);
                    Navigator.of(context).pop(_data);
                  });
            },
            ...
     ),

onLoadStop은 웹뷰의 로딩이 끝나고 화면에 노출되는 순간에 호출 되는 부분이다. 여기서 로컬 호스트가 구동되고 있는지에 따라 구동되지 않았다면 웹뷰를 다시 실행해주고 있다.
InAppLocalhostServer 객체는 현재 구동되고 있는 정보를 .isRunning() 불리언 값으로 제공한다.

다양하게 커스텀하여 로컬 호스트 에러 관련된 처리를 진행하면 된다.

onLoadStop: (controller, url) {
              setState(() {
                if (_localhostServer.isRunning()) {
                  isLoading = false;
                } else {
                  _localhostServer.start().then((value) {
                    _controller.reload();
                  });
                }
              });
            },

웹뷰의 에러 핸들러 부분이다. 로컬 서버 구동에 대한 에러는 여기서 리턴 받을 수 없고, 웹뷰 자체의 에러에 대해서만 수신이 가능하다.

onLoadError: ((controller, url, code, message) {
              setState(() {
                isError = true;
              });
            }),
onLoadHttpError: (controller, url, statusCode, description) {
              setState(() {
                isError = true;
              });
            },

아래는 옵션 파라미터 부분으로 원하는 옵션을 따로 설정할 수 있다.

initialOptions: InAppWebViewGroupOptions(
              crossPlatform: InAppWebViewOptions(
                useShouldOverrideUrlLoading: true,
                mediaPlaybackRequiresUserGesture: false,
              ),
              android: AndroidInAppWebViewOptions(
                useHybridComposition: true,
              ),
              ios: IOSInAppWebViewOptions(
                allowsInlineMediaPlayback: true,
              ),
            ),

InAppWebView 전체 코드에 해당하는 부분이다.

 InAppWebView(
            initialUrlRequest: URLRequest(
                url: Uri.parse(
                    "http://localhost:8080/assets/html/daum_postcode.html")),
            initialOptions: InAppWebViewGroupOptions(
              crossPlatform: InAppWebViewOptions(
                useShouldOverrideUrlLoading: true,
                mediaPlaybackRequiresUserGesture: false,
              ),
              android: AndroidInAppWebViewOptions(
                useHybridComposition: true,
              ),
              ios: IOSInAppWebViewOptions(
                allowsInlineMediaPlayback: true,
              ),
            ),
            onWebViewCreated: (controller) {
              _controller = controller;
              _controller.addJavaScriptHandler(
                  handlerName: 'onSelectAddress',
                  callback: (args) {
                    Map<String, dynamic> _fromMap = args.first;
                    DaumPostModel _data = _dataSetting(_fromMap);
                    Navigator.of(context).pop(_data);
                  });
            },
            onLoadStop: (controller, url) {
              setState(() {
                if (_localhostServer.isRunning()) {
                  isLoading = false;
                } else {
                  _localhostServer.start().then((value) {
                    _controller.reload();
                  });
                }
              });
            },
            onLoadError: ((controller, url, code, message) {
              setState(() {
                isError = true;
              });
            }),
            onLoadHttpError: (controller, url, statusCode, description) {
              setState(() {
                isError = true;
              });
            },
            androidOnPermissionRequest: (controller, origin, resources) async {
              return PermissionRequestResponse(
                  resources: resources,
                  action: PermissionRequestResponseAction.GRANT);
            },
          ),

로딩 중일 때 노출하는 로딩 인디케이터이다.

 if (isLoading) ...[
            const SizedBox(
              child: Center(
                child: CircularProgressIndicator(),
              ),
            ),
          ],

에러가 발생했을 때 노출시켜 주는 부분의 UI이다.

if (isError) ...[
            Container(
              color: const Color.fromRGBO(71, 71, 71, 1),
              child: const Center(
                child: Text(
                  "페이지를 찾을 수 없습니다",
                  style: TextStyle(fontWeight: FontWeight.bold, fontSize: 18),
                ),
              ),
            ),
          ],

리턴 받은 주소 객체를 DaumPostModel로 변환하는 로직이다.

 DaumPostModel _dataSetting(Map<String, dynamic> map) {
    return DaumPostModel(
      map["address"],
      map["roadAddress"],
      map["jibunAddress"],
      map["sido"],
      map["sigungu"],
      map["bname"],
      map["roadname"],
      map["buildingName"],
      map["addressEnglish"],
      map["roadAddressEnglish"],
      map["jibunAddressEnglish"],
      map["sidoEnglish"],
      map["sigunguEnglish"],
      map["bnameEnglish"],
      map["roadnameEnglish"],
      map["zonecode"],
      map["sigunguCode"],
      map["bcode"],
      map["buildingCode"],
      map["roadnameCode"],
      map["addressType"],
      map["apartment"],
      map["userLanguageType"],
      map["userSelectedType"],
    );
  }

Result

Git

https://github.com/boglbbogl/flutter_velog_sample/tree/main/lib/webview/daum_post

마무리

이번 글에 대해서 이해가 안되시거나 내용이 헷갈리시는 분은 위에 공유드린 Git 저장소를 통해서 코드를 받아보신 후 직접 구동해보시면 이해가 되실 겁니다.

다음 주소 관련 daum_postcode_search 라이브러리도 사용해보고, 라이브러리 없이 직접 로컬 서버와 html 코드를 사용하여 해당 기능을 개발해봤다. 생각보다 그렇게 어렵지는 않은 기능이고, 로컬 서버로만 구동하는게 불안 하시거나 하면 서버에 html코드를 올려서 사용하는 것도 가능하다.

profile
Flutter Developer

7개의 댓글

comment-user-thumbnail
2024년 1월 2일

web에서 위에서 설명해주신걸 쓸 순 없을까요?
열심히 찾아봐도 안나오네요..

1개의 답글
comment-user-thumbnail
2024년 1월 22일

개발자님 안녕하세요 !
저도 막 신입 프론트로 플러터로 들어온 개발자입니다.

덕분에 ㅠ 유용한 정보 얻구 갑니다

감사합니다 !

1개의 답글
comment-user-thumbnail
1일 전

안녕하세요 작성하신 코드설명을 유익하게 잘읽었습니다.
올려주신 깃을 클론받아서 실행하고싶은데 혹시 flutter run 했을 때 나오는 Nuget.exe not found, trying to download or use cached version. 이부분에 대해서 아는게 있을까요?? 어떻게 실행하였는지가 궁금합니다.

1개의 답글