[TIL] 지역 검색 앱 과제

청학동버블티·2024년 12월 10일

Flutter 공부

목록 보기
8/18

과제 수행방법을 참고하여 앱 구성순서를 아래와 같이 짜봤다.

1. UI구현
2. API key 암호화
3. 데이터 테스트
4. Location Class 생성
5. Repository 구현
6. HomeViewModel 구현
7. HomeViewModel 데이터 바인딩
8. 트러블 슈팅



1. UI구현

페이지별 파일을 아래와 같이 추가생성했다.

  • home page
  • detail page
  • home list view

홈페이지에는 책검색 앱 구현시에 사용했던 검색창코드를 가져와 사용했다.

TextField(
            maxLines: 1,
            controller: textEditingController,
            onSubmitted: search,
            decoration: InputDecoration(
              hintText: '검색어를 입력해 주세요',
              border: MaterialStateOutlineInputBorder.resolveWith(
                (states) {
                  if (states.contains(WidgetState.focused)) {
                    return OutlineInputBorder(
                      borderRadius: BorderRadius.circular(10),
                      borderSide: BorderSide(color: Colors.black),
                    );
                  }
                  return OutlineInputBorder(
                    borderRadius: BorderRadius.circular(10),
                    borderSide: BorderSide(color: Colors.grey),
                  );
                },
              ),
            ),
          ),

홈 리스트뷰에서 생성된 개체별 디자인을 정하고 디테일 페이지로 이동하게끔 구성했다.

import 'package:flutter/material.dart';
import 'package:flutter_local_search_app/ui/pages/detail/detail_page.dart';

class HomeListView extends StatelessWidget {
  
  Widget build(BuildContext context) {
    return Expanded(
      child: ListView.separated(
        itemCount: 50,
        separatorBuilder: (context, index) => SizedBox(
          height: 20,
        ),
        itemBuilder: (context, index) {
          return item();
        },
      ),
    );
  }

  Widget item() {
    return Builder(
      builder: (context) {
        return GestureDetector(
          onTap: () {
            Navigator.push(
              context,
              MaterialPageRoute(
                builder: (context) {
                  return DetailPage();
                },
              ),
            );
          },
          child: SizedBox(
            height: 120,
            width: double.infinity,
            child: Container(
              width: double.infinity,
              height: double.infinity,
              decoration: BoxDecoration(
                  color: Colors.white,
                  borderRadius: BorderRadius.circular(20),
                  border: Border.all(
                    color: Colors.black12,
                  )),
              padding: EdgeInsets.all(20),
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  Text(
                    '삼성1동 주민센터',
                    style: TextStyle(
                      fontWeight: FontWeight.bold,
                      fontSize: 18,
                    ),
                  ),
                  Spacer(),
                  Text(
                    '공공,사회기관>행정복지센터',
                    style: TextStyle(
                      color: Colors.black54,
                      fontSize: 14,
                    ),
                  ),
                  Spacer(),
                  Text(
                    '서울특별시 강남구 봉은사로 616 삼성1동 주민센터',
                    style: TextStyle(
                      color: Colors.black54,
                      fontSize: 14,
                    ),
                  )
                ],
              ),
            ),
          ),
        );
      },
    );
  }
}

이후 디테일 페이지에서 인앱웹뷰를 통해 링크로 이동할 수 있게끔 구성했다.

import 'package:flutter/material.dart';
import 'package:flutter_inappwebview/flutter_inappwebview.dart';

class DetailPage extends StatelessWidget {
  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(),
      body: InAppWebView(
        initialSettings: InAppWebViewSettings(
          mediaPlaybackRequiresUserGesture: true,
          javaScriptEnabled: true,
          userAgent:
              'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36',
        ),
        initialUrlRequest: URLRequest(
          url: WebUri("https://www.naver.com/"),
        ),
      ),
    );
  }
}


2. API key 암호화

참고사이트 : https://monosandalos.tistory.com/75

API key를 사용할 땐 github에 push하면 누구나 사용할 수 있기 때문에
암호화하여 저장하는것이 좋다.

터미널에서 아래 패키지를 추가하고 assets 폴더에 .env 파일을 생성한다.

flutter pub add flutter_dotenv

pubspec.yaml파일에도 .env를 사용할 수 있도록 설정한다.

.gitignore 파일에도 .env를 추가하면 github에 push되지 않는다.

그러고나서 원하는 key를 .env 파일 내부에 작성한다.

이제 이 값을 가져와 사용하면 된다.
비동기로 실행되는 값이기에 await를 사용하고 dotenv.load() 메서드를 통해 불러온다.

처음에 추가한 WidgetFlutterBinding.ensureInitialized() 메서드는 Flutter프레임워크가
잘 초기화되어있는지 확인해주는데 주로 비동기 작업을 수행하는경우 선언해준다고 한다.

fileName에 .env파일의 경로를 입력하고 dotenv.env['X-Naver-Client-Secret'] 을 통해 원하는 키값을 가져온 후
naverClientSecret이라는 변수를 만들어 불러온 키값을 넣어줬다.

이렇게 만들어 원하는 곳에 해당 변수를 넣으면 그대로 키값을 사용할 수 있다.
해당코드에서는 API key값을 확인할 수 없으며 깃허브에 공유되지 않기 때문에 유용하다.



3. 데이터 테스트

Thunder Client extension을 이용해 데이터 테스트를 진행했다.

이렇게 입력하여 받은 출력값중 하나를 가져와 Location 클래스 생성에 참고했다.



4. Location class 생성

위와같이 클래스를 만든 후 테스트를 진행했다.

String을 Decoding하여 Map타입 변수에 담고
User.fromJson 생성자를 이용해 이 맵구조로부터 새로운 인스턴스를 만들어낸다.



5. Repository 구현

앱 내에서 http 요청을 하려면 아래 패키지를 추가해야한다.

flutter pub add http

repository 클래스에서는 HomePage의 TextField onSubmitted 속성에서 호출할
검색기능인 search 메서드를 만들었다.

아까 암호화했던 API key값을 여기서 사용한다.

네트워크 통신시에는 반드시 try catch 문으로 감싸는 것이 좋다.
모바일에서 인터넷이 연결되지 않을 경우도 있고
서버가 응답하지 않는 경우 등 다양한 예외가 발생할 수 있기 때문이다.

응답코드의 경우 GET 요청이 성공하면 200을 반환한다.
(참고: https://developer.mozilla.org/ko/docs/Web/HTTP/Status/200)

이렇게 작성한 repository도 test를 진행했다.

통신은 비동기이므로 테스트할 함수에는 꼭 async를 달아줘야 한다.



6. HomeViewModel 구현

뷰모델 구현순서는 아래와 같다.

  1. 화면에서 필요한 상태를 (HomeState) 만든다.
  2. 상태를 관리할 뷰모델인 HomeViewModel을 만든다.
    이때 뷰모델이 관리할 상태 클래스는 Notifier이다.
  3. build함수를 재정의 하여 초기상태(null)를 리턴한다.
  4. Repository에서 데이터를 받아와서 상태를 업데이트해주도록 한다.
  5. 뷰모델 관리자를 만든다.
    -> NotifierProvider<HomeViewModel, HomeState> : HomeState 상태를 관리하는 HomeViewModel 관리.
    -> 부를땐 ref.watch 메서드로 homeViewModelProvider를 호출한다.

아래와 같이 테스트도 진행했다.

앱 내에서는 ProviderScope가 ViewModel을 관리하지만
테스트시에는 ProviderContainer가 관리한다.
HomeViewModel을 생성자로 생성하면 HomeViewModel이 관리하는 상태에 접근할 수 없다고 한다.



7. HomeViewModel 데이터 바인딩

이제 필요한 밑작업들이 끝났으니 각각의 데이터를 바인딩 해줘야 한다.

HomePage에서 ConsumerStatefulWidget으로 변경해줘야 하는데,
이는 State 클래스 내에서 ref 메서드를 사용가능하게 해준다.

검색작업이 실행되면 뷰모델의 search함수를 호출할 수 있게끔 해줬다.
그리고 build 위젯 안에서 HomeViewModel의 상태구독을 시작할 수 있도록
아래 명령어를 추가했다.

HomeState homeState = ref.watch(homeViewModelProvider);

(이때 위젯으로 분리해두었던 HomeListView 파일에 데이터 바인딩이 어려워 HomePage로 다시 가져왔다.
과제 해설에서도 따로 위젯분리를 하지 않으신 것을 보니 크게 상관 없는 부분인 것 같다.)

그 다음으로 DetailPage에서 link값을 필수로 전달하도록 설정했고

HomePage에서 각각의 item을 탭하면 DetailPage로 넘어가면서 link정보를 전달하도록 했다.



8. 트러블 슈팅

장소를 검색하면 title부분에 html코드가 같이 출력되는 경우가 종종 발생했다.
이는 아래 코드를 활용하면 간단하게 수정이 가능하다.

extension StringExtension on String {
  String stripHtml() => replaceAll(RegExp(r'<[^>]*>|&[^;]+;'), '');
}

마지막으로 앱 구동시험을 하는데 link가 열리지 않는 문제가 발생했다.
튜터님께 여쭤보니 InAppWebView initialSetting에 userAgent를 아래처럼 입력하여 발생한 것이었다.

이는 크롤링 접근 차단이 되어 있는 일부 웹사이트에도 접속하기 위해
PC환경에서 접속하는 것처럼 보이기 위한 명령어인데 안드로이드 환경에서는 필수 세팅이라고 한다.
그런데 이를 삭제했더니 정상적으로 웹뷰 화면이 출력되었다.

튜터님 설명으로는 위 설정이 혹시나 발생할 수 있는 차단을 막고자 접속환경을 제한하는 것인데
위 설정없이도 구동되는 경우라면 입력할 필요가 없다고 하셨다.



오늘은 지역검색앱을 제작한 과정들을 살펴보며
다시한번 복습해보는 시간을 가졌다.
도전기능들은 구현해보지 못해서 아쉽지만 시간이 될때 조금씩 도전해봐야겠다.

0개의 댓글