토이프로젝트

[토이프로젝트]대출이자계산기 만들어보기 - 4편 : Flutter로 모바일 서비스 개발하기

  • -
반응형

본 포스팅부터 모바일 서비스 개발편이 시작됩니다. 진행을 위해 Flutter가 필수적으로 설치되어 있어야 하니 Flutter 설치 글을 먼저 확인해 주세요.

모바일 개발편부터는 MacOS를 사용합니다.

  • vscode
  • Flutter 3.16.0
  • Dart 3.2.0

vscode를 실행하고 프로젝트를 생성해 보겠습니다.

Command + Shift + P 를 눌러 Next Project를 클릭합니다.

좌측부터 웹 / android / ios

 

Application 선택 후 프로젝트를 생성할 경로를 지정합니다.

좌측부터 웹 / android / ios좌측부터 웹 / android / ios

 

프로젝트명을 입력합니다. 이때 프로젝트명은 카멜케이스가 아닌 스네이크 케이스를 사용해야 합니다.

좌측부터 웹 / android / ios

 

프로젝트가 생성되었습니다. Simulator를 실행하고 Command + Shift + D를 눌러 디버그 실행을 클릭합니다.

좌측부터 웹 / android / ios좌측부터 웹 / android / ios

 

Simulator는 Xcode 실행 후 Open Developer Tool 탭에서 실행할 수 있습니다.

좌측부터 웹 / android / ios

 

※ iOS 앱 배포를 위해 사전에 TestFlight 설정이 필요합니다.

이 글을 통해 TestFlight 설정 후 다음 내용을 진행해 주세요!

 

Flutter에서는 웹 뷰 라이브러리를 통해 모바일 어플리케이션에 웹 사이트를 뷰어로 추가해 서비스할 수 있습니다.

대표적으로 많이 사용되는 웹 뷰 라이브러리는 2가지가 있습니다.

  • webview_flutter : 경량화된 웹 뷰 기능을 제공하며, 자바스크립트 채널 이용이 가능하며 localhost 연결이 불가능합니다.(테스트시에 실제 도메인을 통해 연결해야 합니다). 
  • flutter_inappwebview : webview_flutter에 비해 기능이 많으며, 자바스크립트 채널 이용이 가능하고, localhost 연결이 가능합니다.

 

자바스크립트 채널은 자바스크립트를 통해 작성한 코드를 앱 - 웹 양방향으로 소통할 수 있게 도와주는 도구입니다.

간단한 웹 뷰 구현시에는 webview_flutter가 유용하기 때문에 우리는 webview_flutter를 이용해보겠습니다. 

여기서부터 Next.js와 Flutter 프로젝트의 코드가 섞여 있습니다. 명령어를 제외한 코드는 확장자로 코드를 구분하시면 됩니다.

  • .tsx : Next.js
  • .dart : Flutter

 

웹 뷰 설정

아래 명령어를 통해 라이브러리를 설치합니다.

flutter pub add webview_flutter

 

웹 뷰는 안드로이드 플랫폼 최소 sdk 버전을 19로 지정해주어야 합니다.

안드로이드 디렉토리 우클릭 후 안드로이드 스튜디오를 실행합니다.

좌측부터 웹 / android / ios

 

앱 수준의 build.gradle(Module: app) 파일 클릭 후 minSdkVersion을 19로 수정합니다. Sync Now를 클릭해 리빌딩합니다.

build.gradle(Module: app)

좌측부터 웹 / android / ios

 

 

환경 변수 추가

연결시킨 웹 사이트 주소를 환경 변수로 등록해보겠습니다.

아래 명령어를 입력해 flutter_dotenv를 설치합니다. flutter_dotenv 패키지는 .env 파일을 관리하고 사용할 수 있게 도와줍니다.

flutter pub add flutter_dotenv

 

프로젝트 수준에 .env 파일을 추가합니다.

좌측부터 웹 / android / ios

 

 

웹 사이트 주소를 등록하겠습니다.

.env

BASE_URL=https://calculator.codedream.co.kr

 

마지막으로 env 파일을 추가하기 위해서 pubspec.yaml 파일을 열어 assets 부분의 주석을 해제하고, .env를 등록합니다.,

좌측부터 웹 / android / ios

 

flutter는 lib/main.dart 파일로 시작됩니다. 

main.dart에서 환경 변수를 읽어올 수 있도록 dotenv.load를 추가합니다.

 

lib/main.dart

Future<void> main() async { WidgetsFlutterBinding.ensureInitialized(); // 구성 파일 로드 await dotenv.load(fileName: ".env"); runApp(const MyApp()); }

 

 

웹 뷰 위젯 추가

flutter는 lib 디렉토리를 루트 디렉토리로 사용합니다.

widget 디렉토리를 추가하고 webview.dart 파일을 추가합니다.

좌측부터 웹 / android / ios

 

 

lib/widget/webview.dart

import 'package:flutter/material.dart'; import 'package:flutter_dotenv/flutter_dotenv.dart'; import 'package:webview_flutter/webview_flutter.dart'; class MainWebView extends StatefulWidget { const MainWebView({ Key? key, }) : super(key: key); @override State<MainWebView> createState() => _MainWebViewState(); } class _MainWebViewState extends State<MainWebView> { late WebViewController _controller = WebViewController(); @override void initState() { _controller = WebViewController() ..setJavaScriptMode(JavaScriptMode.unrestricted) ..setNavigationDelegate( NavigationDelegate( onProgress: (int progress) {}, onPageStarted: (String url) {}, onPageFinished: (String url) async {}, onWebResourceError: (WebResourceError error) {}, onNavigationRequest: (NavigationRequest request) { return NavigationDecision.navigate; }, ), ) ..loadRequest(Uri.parse('${dotenv.env['BASE_URL']}')); super.initState(); } @override Widget build(BuildContext context) { return WebViewWidget(controller: _controller); } }

 

initState() 안에서 WebViewController() 함수를 통해 웹 뷰를 생성합니다.

initState() 함수는 위젯 초기화시 최초 1회 호출됩니다. WebViewContoller의 loadRequest() 값으로 웹 사이트 주소를 입력해 연결할 웹 사이트를 설정합니다.

생성한 웹 뷰 위젯을 main에서 실행해보겠습니다.

 

lib/main.dart

import 'package:app_my_calculator/widget/webview.dart'; import 'package:flutter/material.dart'; import 'package:flutter_dotenv/flutter_dotenv.dart'; Future<void> main() async { WidgetsFlutterBinding.ensureInitialized(); // 구성 파일 로드 await dotenv.load(fileName: ".env"); runApp(const MyApp()); } class MyApp extends StatelessWidget { const MyApp({super.key}); @override Widget build(BuildContext context) { return MaterialApp( title: 'Flutter Demo', home: MainWebView(), ); } }

 

프로젝트를 실행합니다.

지금까지 만든 웹 사이트가 앱 안에 조회됩니다.

좌측부터 웹 / android / ios

 

 

 

웹 뷰를 이용해 웹 사이트를 앱으로 띄우는 경우 개발이 간편하고 수정 사항을 앱에 바로 적용할 수 있다는 장점이 있습니다. 하지만 네이티브 앱에 비해 사용감이 매끄럽지 않은 단점이 존재합니다.

예를 들면 현재 프로젝트에서 값을 입력하지 않은 경우 계산하기를 클릭하게 되면 alert 알림창을 확인할 수 있는데요. 이걸 앱에서 보면 어떻게 될까요?

좌측부터 웹 / android / ios좌측부터 웹 / android / ios좌측부터 웹 / android / ios
좌측부터 웹 / android / ios

 

플랫폼에 따라 팝업이 다르게 출력됩니다. Android는 alert가 작동하지만 iOS는 작동하지 않습니다. webview_flutter에서 지원되지 않기 때문에 따로 코드를 추가해주어야 합니다. 또 앞서 만든 카카오톡 공유하기도 카카오톡 앱이 실행되는 매끄러운 연결이 불가능합니다. 이를 위해서 자바스크립트 채널을 이용해 웹 -> 앱으로  파라미터들을 전달해주고, 앱에서의 작업을 따로 해줄 것입니다.

결과적으로 자바스크립트 채널을 사용하는 것은 모바일 어플리케이션에 웹 콘텐츠가 잘 통합되도록 하기 위함입니다.

 

채널 추가

알림, 공유하기를 연결할 두가지의 채널을 추가합니다.

 

lib/widget/webview.dart

@override void initState() { _controller = WebViewController() ..setJavaScriptMode(JavaScriptMode.unrestricted) ..addJavaScriptChannel('shareChannel', onMessageReceived: (JavaScriptMessage javaScriptMessage) async { var data = jsonDecode(javaScriptMessage.message); print(data); }) ..addJavaScriptChannel('alertChannel', onMessageReceived: (JavaScriptMessage javaScriptMessage) { var data = jsonDecode(javaScriptMessage.message); print(data); }) ..setNavigationDelegate( NavigationDelegate( onProgress: (int progress) {}, onPageStarted: (String url) {}, onPageFinished: (String url) async {}, onWebResourceError: (WebResourceError error) {}, onNavigationRequest: (NavigationRequest request) { return NavigationDecision.navigate; }, ), ) ..loadRequest(Uri.parse('${dotenv.env['BASE_URL']}')); super.initState(); }

 

addJavaScriptChannel를 통해 shareChannel, alertChannel 두 채널을 생성(각 채널명은 웹과 동일하게 맞추면 됩니다)했고 JavaScriptMessage를 통해 파라미터를 전달받게 됩니다. jsonDecode를 추가해 map 형태로 파라미터를 전달받아 보겠습니다.

좌측부터 웹 / android / ios

 

 

웹으로 돌아가보겠습니다.

수정할 곳은 공유하기 컴포넌트와 계산기 컴포넌트입니다.

먼저 채널 함수를 사용하기 위해  layout 파일을 열어 Window 인터페이스에 채널명을 추가합니다.

 

app/layout.tsx

declare global { interface Window { Kakao: any; shareChannel: any; alertChannel: any; } }

 

 

app/components/share/share.tsx

"use client"; import { useEffect } from "react"; import Image from "next/image"; import styles from "./share.module.scss"; type Props = { paymentMethod: string; amount: string; period: string; interest: string; }; export default function MyShare({ paymentMethod, amount, period, interest, }: Props) { const onClickWebView = async () => { if (window.shareChannel) { window.shareChannel.postMessage( JSON.stringify({ paymentMethod: paymentMethod, amount: amount, period: period, interest: interest, }) ); } }; useEffect(() => { if (!window.shareChannel) { if (window.Kakao) { const { Kakao } = window; if (!Kakao.isInitialized()) { Kakao.init(`${process.env.NEXT_PUBLIC_KAKAO_KEY}`); } const redirectUrl = `${window.location.origin}/share?paymentMethod=${paymentMethod}&amount=${amount}&period=${period}&interest=${interest}`; let methodName = ""; switch (paymentMethod) { case "1": methodName = "원리금 균등 상환"; break; case "2": methodName = "원금 균등 상환"; break; case "3": methodName = "만기일시 상환"; break; } Kakao.Share.createDefaultButton({ container: "#kakao-link-btn", objectType: "feed", content: { title: "my-calculator", description: "계산 결과를 확인해 보세요!", imageUrl: `${window.location.origin}/favicon.png`, link: { mobileWebUrl: redirectUrl, webUrl: redirectUrl, }, }, buttons: [ { title: "계산 결과 확인하기", link: { webUrl: redirectUrl, mobileWebUrl: redirectUrl, }, }, ], }); } } }, []); return ( <div className={styles.share_image}> <Image id="kakao-link-btn" width={44} height={44} src={"/kakaotalk_sharing_btn_medium.png"} alt="카카오톡 공유 이미지" onClick={() => onClickWebView()} /> </div> ); }

 

useEffect에서 실행되는 카카오 공유하기 버튼의 초기화를 웹에서만 실행되도록 합니다. window.shareChannel이 true가 되면 접속 상태가 앱임을 의미합니다. 접속 상태가 앱인 경우에는 onClickWebView() 함수가 실행되며 postMessage 함수를 통해 추가한 파라미터들이 전달됩니다.

 

app/components/form/form.tsx

"use client"; import { FormProvider, useForm } from "react-hook-form"; import MyInput from "../input/input"; import MyButton from "../button/button"; import MyRadio from "../radio/radio"; import Modal from "../modal/modal"; import styles from "./form.module.scss"; import { useState } from "react"; import MyGrid, { GridType } from "../grid/grid"; const OPTIONS = [ { value: "1", name: "원리금 균등", }, { value: "2", name: "원금 균등", }, { value: "3", name: "만기 일시", }, ]; export default function Calcurator() { const [toggleModal, setToggleModal] = useState(false); const [params, setParams] = useState<GridType>(); const methods = useForm({ mode: "onChange", defaultValues: {}, }); const onSubmit = async (form: any) => { console.log(form); setParams({ paymentMethod: form.paymentMethod, amount: form.amount, period: form.period, interest: form.interest, }); setToggleModal(!toggleModal); }; const onInvalid = (errors: any) => { if (errors) { if (window.alertChannel) { let message = ""; if (errors && errors.amount) { message = errors.amount.message; } else if (errors && errors.period) { message = errors.period.message; } else if (errors && errors.interest) { message = errors.interest.message; } window.alertChannel.postMessage( JSON.stringify({ message: message, }) ); } else { if (errors && errors.amount) { alert(errors.amount.message); return; } else if (errors && errors.period) { alert(errors.period.message); return; } else if (errors && errors.interest) { alert(errors.interest.message); return; } } } }; return ( <FormProvider {...methods}> <MyRadio id={"paymentMethod"} options={OPTIONS} label={"상환방식"} /> <MyInput id={"amount"} label={"대출원금"} require={true} unit={"원"} /> <MyInput id={"period"} label={"대출기간"} require={true} unit={"년"} /> <MyInput id={"interest"} label={"연이자"} require={true} unit={"%"} /> <MyButton text={"계산하기"} onClick={methods.handleSubmit(onSubmit, onInvalid)} /> {toggleModal && ( <Modal setToggleModal={setToggleModal}> <article className={styles.popup}> <MyGrid params={params} /> </article> </Modal> )} </FormProvider> ); }

 

같은 방식으로 onInvalid 함수에 window.alertChannel이 있는 경우 알람 문구를 파라미터로 전달합니다.

다시 앱으로 돌아와 print에 파라미터가 조회되는지 확인해보겠습니다.

좌측부터 웹 / android / ios

 

웹 -> 앱으로 파라미터가 잘 전달되는 것을 확인할 수 있습니다. 다음 포스팅에서는 전달받은 파라미터를 통해 웹의 처리 방식들을 앱에서 구현해보도록 하겠습니다.

전체 코드는 GitHub에서 확인하실 수 있습니다.

 

참고문서
 

webview_flutter | Flutter package

A Flutter plugin that provides a WebView widget on Android and iOS.

pub.dev

 

Kakao Developers

카카오 API를 활용하여 다양한 어플리케이션을 개발해보세요. 카카오 로그인, 메시지 보내기, 친구 API, 인공지능 API 등을 제공합니다.

developers.kakao.com

 

반응형

포스팅 주소를 복사했습니다.

이 글이 도움이 되었다면 공감 부탁드립니다.