Flutter (앱 개발)/Flutter 트랙

[Flutter 숙련 TIL] 책검색 앱 만들기 Part 01 - (TextField, GridView, BottomSheet, WebView)

Ize𓆜 2025. 4. 16. 14:51
728x90
728x90

 


Ⅰ. 시작하며

 

앱을 직접 구성해보기 전, 내가 막혔던 지점들

 

출처 : teamsparta

 

MVVM 구조를 Flutter에 

어떻게 적용해야 할지 막막했습니다. 

 

특히 구조를 나눌 때 

어느 기준으로 폴더를 구성해야 할지 애매했고,

 위젯 하나하나를

어디에 배치해야 효율적인지도

처음엔 헷갈렸습니다.

 

또한 API 없이

화면만 구성하는 것도

쉽지 않았어요.

 

GridView, showModalBottomSheet,

WebView 등

실무에서 자주 쓰이는 위젯이지만,

각각의 역할과 쓰임새를

제대로 익히는 데 시간이 걸렸습니다.

 


직접 UI부터 하나씩 만들며 구조를 익힌 시간

 

출처 : teamsparta

 

이번 실습은

 서버 통신 없이 

"UI"를 중심으로

 전체 앱 구조를 구성해보는 시간이었습니다.

 

출처 : teamsparta

 

 

 MVVM 패턴을

 Flutter 프로젝트에서

 어떻게 적용할 수 있는지,

 그리고 그 안에서

 각각의 위젯(TextField, GridView 등)을

 어떤 흐름으로 사용할 수 있는지를 

몸으로 익힐 수 있었습니다.

 


Ⅱ. 본론

 

MVVM 구조 잡기: 프로젝트 폴더 설계

 

 

MVVM 구조에 따라

 data, ui/pages, ui/widgets로

 폴더를 나누어 앱을 설계했습니다. 

 

각 페이지 별로 위젯과 

뷰모델을 폴더로 분리하여

 유지보수성을 높였습니다.

 

lib/
├── main.dart
├── data/
│   ├── model/         ← API 응답 데이터 클래스
│   └── repository/    ← API 호출 및 데이터 변환
├── ui/
│   ├── pages/
│   │   ├── home/
│   │   │   ├── widgets/
│   │   │   ├── home_page.dart
│   │   │   └── home_view_model.dart
│   │   └── detail/
│   │       ├── widgets/
│   │       ├── detail_page.dart
│   │       └── detail_view_model.dart
│   └── widgets/       ← 공통 위젯


검색창 UI 구현: TextField + AppBar

 

 

AppBar 안에 TextField를

 넣는 방식으로 검색창을 구성했습니다. 

controller를 통해 텍스트를 제어하고, 

onSubmitted, onChanged를 통해

 이벤트를 처리합니다.

 

 UX를 위해 

터치 영역을 넓히고, 

포커스 해제 처리를

 GestureDetector로 구현했습니다.

 

title: TextField(
  controller: textEditingController,
  onSubmitted: search,
  decoration: InputDecoration(
    hintText: '검색어를 입력해 주세요',
    border: MaterialStateOutlineInputBorder.resolveWith(...),
  ),
),
actions: [
  GestureDetector(
    onTap: () => search(textEditingController.text),
    child: Icon(Icons.search),
  ),
],


GridView 구현: 격자 형태로 책 리스트 구성하기

 

책 리스트는

 GridView.builder를 이용해 

구성했습니다. 

 

SliverGridDelegateWithFixedCrossAxisCount를 통해

 가로 열 수, 간격, 비율 등을 설정했습니다.

 

GridView.builder(
  itemCount: 10,
  gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
    crossAxisCount: 3,
    childAspectRatio: 3 / 4,
    crossAxisSpacing: 10,
    mainAxisSpacing: 10,
  ),
  itemBuilder: (context, index) {
    return GestureDetector(
      onTap: () {
        showModalBottomSheet(
          context: context,
          builder: (_) => HomeBottomSheet(),
        );
      },
      child: Image.network('https://picsum.photos/300/400'),
    );
  },
)

 


BottomSheet + WebView + DetailPage 연동

 

 

책 카드를 클릭하면 

바텀시트를 띄우고, 

바텀시트에서 "자세히 보기"를 누르면

 상세 페이지로 이동합니다.

 

상세 페이지에는 

InAppWebView를 사용하여 

외부 웹페이지(예: 네이버)도 

보여줄 수 있게 했습니다.

 

showModalBottomSheet(
  context: context,
  builder: (context) => HomeBottomSheet(),
);

Navigator.push(
  context,
  MaterialPageRoute(builder: (_) => DetailPage()),
);

 

 

body: InAppWebView(
  initialUrlRequest: URLRequest(url: WebUri("https://www.naver.com/")),
  initialSettings: InAppWebViewSettings(
    javaScriptEnabled: true,
    mediaPlaybackRequiresUserGesture: true,
  ),
)

 

 

아래 GITHUB 링크를 통해

저의 실제 연습 파일을 구경하실 수 있습니다. 

https://github.com/Linayoo01/-250416-flutter_book_search_app.git

 

GitHub - Linayoo01/-250416-flutter_book_search_app

Contribute to Linayoo01/-250416-flutter_book_search_app development by creating an account on GitHub.

github.com

 

 


Ⅲ. 정리하며

 

실제 구현하면서 느꼈던 실수들

 

 

  • TextField에 controller를 선언하고 dispose하지 않으면 메모리 누수가 발생하는 걸 놓친 적이 있었습니다.
  • GestureDetector 없이 앱 전체에 포커스를 해제하지 않아 키보드가 계속 떠 있는 UX 문제도 있었죠.
  • showModalBottomSheet 내부를 한 파일에 다 구현했더니 코드가 너무 길어져 가독성이 떨어졌습니다.

 

 


하나씩 쌓아가는 UI 구현 경험의 힘

 

 

 

이번 실습을 통해 

Riverpod, MVVM, WebView 등

 복잡한 기능이 없어도

 UI만으로도 꽤나 완성도 있는 앱을

 구성할 수 있다는 자신감이 생겼습니다. 

구조를 먼저 나누고, 

그 위에 각 위젯을

 배치하는 방식으로 접근하니 

확실히 개발 속도와 효율이 올라갔습니다.

728x90
728x90