본문 바로가기
Flutter for Beginners

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

by Andrew's Akashic Records 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