본문 바로가기
Flutter for Beginners

Flutter WebViewController를 이용한 블로그 앱 만들기

by Records that rule memory 2025. 3. 21.
728x90

Flutter

블로그앱 구성

  1. 상단 Appbar의 타이틀 "tistory", 색상은 f9e000, 굵은 글씨, 중앙정렬
  2. Appbar 우측에 '홈' 버튼 생성, 클릭하면 'https://bigdown.tistory.com/' 로 이동
  3. AppBar 좌측에 뒤로가기 버튼을 추가하여 웹 브라우저의 뒤로 가기 기능을 수행
  4. 스마트폰의 뒤로 가기 버튼을 누르면 이전 페이지로 이동하도록 WillPopScope을 활용하여 처리
  5. Webview에서는 'https://bigdown.tistory.com/' 로 접속, 자바스크립트 등의 기능 활성화
  6. Webview 좌측 하단에 '방명록' 플로팅 버튼을 추가하고 클릭하면 'https://bigdown.tistory.com/guestbook'으로 이동

pubspec.yam에 'webview_flutter' 플러그인 설치하기

dependencies:
  flutter:
    sdk: flutter

  # The following adds the Cupertino Icons font to your application.
  # Use with the CupertinoIcons class for iOS style icons.
  cupertino_icons: ^1.0.8
  webview_flutter: 4.10.0
  • 설정 변경후 'flutter pub get' 을 실행하여 플러그인을 설치합니다.

전체 코드

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

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      home: const WebViewScreen(),
    );
  }
}

class WebViewScreen extends StatefulWidget {
  const WebViewScreen({super.key});

  @override
  _WebViewScreenState createState() => _WebViewScreenState();
}

class _WebViewScreenState extends State<WebViewScreen> {
  late final WebViewController _controller;

  @override
  void initState() {
    super.initState();
    _controller = WebViewController()
      ..setJavaScriptMode(JavaScriptMode.unrestricted)
      ..setBackgroundColor(const Color(0xFFFFFFFF))
      ..setNavigationDelegate(
        NavigationDelegate(
          onPageStarted: (String url) {},
          onPageFinished: (String url) {},
        ),
      )
      ..loadRequest(Uri.parse('https://bigdown.tistory.com/'));
  }

  void _goHome() {
    _controller.loadRequest(Uri.parse('https://bigdown.tistory.com/'));
  }

  void _goGuestbook() {
    _controller.loadRequest(Uri.parse('https://bigdown.tistory.com/guestbook'));
  }

  Future<void> _goBack() async {
    if (await _controller.canGoBack()) {
      await _controller.goBack();
    }
  }

  @override
  Widget build(BuildContext context) {
    return PopScope(
      canPop: false,
      onPopInvokedWithResult: (didPop, result) async {
        if (!didPop && await _controller.canGoBack()) {
          _controller.goBack();
        } else {
          Navigator.of(context).maybePop(result);
        }
      },
      child: Scaffold(
        appBar: AppBar(
          title: const Text(
            'tistory',
            style: TextStyle(color: Colors.black, fontWeight: FontWeight.bold),
          ),
          centerTitle: true,
          backgroundColor: const Color(0xFFF9E000),
          leading: IconButton(
            icon: const Icon(Icons.arrow_back, color: Colors.black),
            onPressed: _goBack,
          ),
          actions: [
            IconButton(
              icon: const Icon(Icons.home, color: Colors.black),
              onPressed: _goHome,
            ),
          ],
        ),
        body: WebViewWidget(controller: _controller),
        floatingActionButton: FloatingGuestBookButton(onPressed: _goGuestbook),
      ),
    );
  }
}

class FloatingGuestBookButton extends StatelessWidget {
  final VoidCallback onPressed;

  const FloatingGuestBookButton({super.key, required this.onPressed});

  @override
  Widget build(BuildContext context) {
    return FloatingActionButton(
      onPressed: onPressed,
      backgroundColor: const Color(0xFFF9E000),
      child: const Icon(Icons.comment, color: Colors.black),
    );
  }
}
728x90

 

코드 상세 설명

import 'package:flutter/material.dart';
import 'package:webview_flutter/webview_flutter.dart';
  • flutter/material.dart: Flutter의 UI 구성 요소를 포함하는 패키지
  • webview_flutter/webview_flutter.dart: WebView 관련 기능을 제공하는 패키지

1. 애플리케이션 실행

void main() {
  runApp(const MyApp());
}
  • main() 함수에서 MyApp 클래스를 실행하여 앱을 시작합니다.
class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      home: const WebViewScreen(),
    );
  }
}
  • MyApp은 StatelessWidget을 상속받아 앱의 루트 위젯 역할을 합니다.
  • MaterialApp을 반환하며, debugShowCheckedModeBanner: false로 설정해 디버그 배너를 제거했습니다.
  • home: const WebViewScreen()으로 WebViewScreen을 초기 화면으로 설정했습니다.

2. WebView 화면 (WebViewScreen)

class WebViewScreen extends StatefulWidget {
  const WebViewScreen({super.key});

  @override
  _WebViewScreenState createState() => _WebViewScreenState();
}
  • WebViewScreen은 StatefulWidget으로 정의되어 상태를 관리할 수 있습니다.
  • createState()를 통해 _WebViewScreenState를 생성하여 상태 관리.

(1) 상태 변수 선언

class _WebViewScreenState extends State<WebViewScreen> {
  late final WebViewController _controller;
  • WebViewController를 late 키워드를 이용하여 선언했습니다.
  • late를 사용한 이유: WebView 컨트롤러는 initState()에서 초기화되므로, late를 사용해 초기화 전에는 메모리를 할당하지 않도록 함.

(2) WebViewController 초기화

  @override
  void initState() {
    super.initState();
    _controller = WebViewController()
      ..setJavaScriptMode(JavaScriptMode.unrestricted)
      ..setBackgroundColor(const Color(0xFFFFFFFF))
      ..setNavigationDelegate(
        NavigationDelegate(
          onPageStarted: (String url) {},
          onPageFinished: (String url) {},
        ),
      )
      ..loadRequest(Uri.parse('https://bigdown.tistory.com/'));
  }
  • JavaScriptMode.unrestricted: JavaScript 실행을 허용
  • setBackgroundColor(const Color(0xFFFFFFFF)): **배경색을 흰색(0xFFFFFFFF)**으로 설정
  • setNavigationDelegate: 페이지 이동 관련 이벤트를 처리하는 콜백 함수
  • loadRequest(Uri.parse('https://bigdown.tistory.com/')): 초기 웹페이지를 Tistory 블로그로 설정

3. 뒤로 가기 & 홈 버튼 기능 추가

(1) 홈으로 이동하는 함수

  void _goHome() {
    _controller.loadRequest(Uri.parse('https://bigdown.tistory.com/'));
  }
  • loadRequest() 메서드를 사용하여 홈 URL을 다시 로드합니다.

(2) 뒤로 가기 기능

  Future<void> _goBack() async {
    if (await _controller.canGoBack()) {
      await _controller.goBack();
    }
  }
  • _controller.canGoBack()을 통해 뒤로 갈 수 있는지 여부를 확인
  • 가능하면 _controller.goBack()을 실행하여 이전 페이지로 이동

4. UI 구성 (AppBar + WebView + FloatingActionButton)

return PopScope(
  canPop: false,
  onPopInvokedWithResult: (didPop, result) async {
    if (!didPop && await _controller.canGoBack()) {
      _controller.goBack();
    } else {
      Navigator.of(context).maybePop(result);
    }
  },
  • Flutter 3.22 이후 뒤로 가기 동작을 커스터마이징하기 위해 사용되는 PopScope 위젯입니다. 이전에는 WillPopScope를 사용했지만, predictive back navigation(예측 가능한 뒤로가기) 지원을 위해 PopScope이 도입되었고, 최근에는 onPopInvokedWithResult로 업그레이드된 상태입니다.
  • PopScope는 사용자 또는 시스템(뒤로 가기 버튼 등)이 현재 라우트를 종료(pop) 하려고 할 때, 그 동작을 가로채서 커스터마이징할 수 있게 해줍니다.
      child: Scaffold(
        appBar: AppBar(
          title: const Text(
            'tistory',
            style: TextStyle(color: Colors.black, fontWeight: FontWeight.bold),
          ),
          centerTitle: true,
          backgroundColor: const Color(0xFFF9E000),
          leading: IconButton(
            icon: const Icon(Icons.arrow_back, color: Colors.black),
            onPressed: _goBack,
          ),
          actions: [
            IconButton(
              icon: const Icon(Icons.home, color: Colors.black),
              onPressed: _goHome,
            ),
          ],
        ),

 

  • title: "tistory" 텍스트를 중앙 정렬 & 굵게 표시
  • backgroundColor: 노란색(0xFFF9E000)
  • leading: 좌측 뒤로 가기 버튼 추가 (_goBack() 호출)
  • actions: 우측 홈 버튼 추가 (_goHome() 호출)
        body: WebViewWidget(controller: _controller),
  • WebViewWidget(controller: _controller): WebView를 표시

5. FloatingActionButton (방명록 페이지 이동)

        floatingActionButton: FloatingGuestBookButton(onPressed: _goGuestbook),
  • floatingActionButton: FloatingGuestBookButton 위젯을 사용하여 우측 하단에 방명록 이동 버튼 추가
  • _goGuestbook()을 클릭하면 방명록 페이지(https://bigdown.tistory.com/guestbook)로 이동

6. 방명록 이동 버튼 (FloatingGuestBookButton)

class FloatingGuestBookButton extends StatelessWidget {
  final VoidCallback onPressed;

  const FloatingGuestBookButton({super.key, required this.onPressed});

  @override
  Widget build(BuildContext context) {
    return FloatingActionButton(
      onPressed: onPressed,
      backgroundColor: const Color(0xFFF9E000),
      child: const Icon(Icons.comment, color: Colors.black),
    );
  }
}
  • FloatingGuestBookButton 클래스를 별도로 정의하여 객체지향적으로 분리
  • onPressed: 버튼 클릭 시 실행할 함수 (_goGuestbook을 전달받음)
  • backgroundColor: 노란색(0xFFF9E000)
  • child: 아이콘(Icons.comment)

tistory blog app

WebViewController 사용을 위한 설정 방법

Flutter에서 WebViewController를 이용한 webview_flutter 패키지를 사용할 때, Android와 iOS에서 정상적으로 작동하려면 몇 가지 권한 및 설정 파일을 수정해야 합니다.

1. Android 설정

1.1.  AndroidManifest.xml 설정

android/app/src/main/AndroidManifest.xml 에 필요한 권한 추가

 

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.myapp">

    <!-- 인터넷 접근 권한 -->
    <uses-permission android:name="android.permission.INTERNET"/>
    
    <!-- 네트워크 상태 확인 권한 (필요 시) -->
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>

    <application
        android:usesCleartextTraffic="true"
        android:networkSecurityConfig="@xml/network_security_config"
        android:requestLegacyExternalStorage="true">
        
        <!-- WebView 구성 -->
        <meta-data
            android:name="android.webkit.WebView.EnableSafeBrowsing"
            android:value="false"/>
        
        ...
    </application>

</manifest>
  1. 인터넷 사용 권한 (android.permission.INTERNET)
    • WebView가 인터넷을 통해 외부 웹페이지를 로드할 수 있도록 설정합니다.
  2. 네트워크 상태 확인 권한 (android.permission.ACCESS_NETWORK_STATE)
    • 네트워크 연결 상태를 확인할 수 있도록 설정 (필수는 아님).
  3. android:usesCleartextTraffic="true"
    • HTTP (비보안) 요청을 허용하려면 필요합니다.
      (https만 사용하면 필요 없음)
  4. android:requestLegacyExternalStorage="true"
    • 앱에서 로컬 파일 저장 및 로드 기능을 활용할 경우 필요.
  5. android.webkit.WebView.EnableSafeBrowsing
    • false로 설정하면 안전한 브라우징 필터링을 비활성화 (일부 웹페이지 차단 방지).

 

2. iOS 설정

2.1. WebView 사용 권한 설정

ios/Runner/Info.plist 네트워크 및 WebView 사용 권한 추가

<key>NSAppTransportSecurity</key>
<dict>
    <key>NSAllowsArbitraryLoads</key>
    <true/>
</dict>

<key>NSAllowsLocalNetworking</key>
<true/>

<key>NSBluetoothAlwaysUsageDescription</key>
<string>이 앱은 Bluetooth를 사용하여 WebView 기능을 제공합니다.</string>

<key>NSCameraUsageDescription</key>
<string>이 앱은 카메라 액세스를 요청합니다.</string>

<key>NSMicrophoneUsageDescription</key>
<string>이 앱은 마이크 액세스를 요청합니다.</string>
  1. NSAllowsArbitraryLoads
    • true로 설정하면 HTTP 요청이 허용됩니다.
    • HTTPS가 기본이지만, HTTP 페이지도 허용하려면 필요.
  2. NSAllowsLocalNetworking
    • 로컬 네트워크 통신을 허용합니다.
  3. NSBluetoothAlwaysUsageDescription
    • WebView가 Bluetooth 기능을 사용할 경우 필요.
  4. NSCameraUsageDescription / NSMicrophoneUsageDescription
    • 웹사이트에서 카메라 및 마이크 액세스를 요청할 경우 필요한 설정.
728x90