반응형

 

1. Firebase App Check

- 앱의 보안성을 강조하기 위한 서비스 입니다. 앱의 무단 접근, 자동화 봇 등에 의한 보안 위협으로부터 앱을 보호하면, 신뢰할 수 있는 사용자만이 앱에 액세스 할 수 있도록 보장합니다.

- Firebase App Check는 클라이언트-서버 인증 방식을 사용하여 동작합니다. 앱 클라이언트는 인증된 앱으로 등록되어 있는지 확인하기 위해 Firebase App Check SDK를 사용하여 앱 서버에 요청을 보내고, 서버는 이 요청에 대해 Firebase App Check API를 사용하여 요청을 검증합니다. 이를 통해 클라이어트가 신뢰할 수 있는 Firebase 프로젝트에 속하는 앱인지 확인하고, 무단 액세스를 차단할 수 있습니다.

 

2. Flutter에 적용하기

  • Firebase Console에서 Firebase 프로젝트를 생성하고, 해당 프로젝트에 Firebase App Check를 활성화합니다.
  • Flutter 프로젝트의 pubspec.yaml 파일에  Firebase App Check Flutter 패키지를 추가합니다. 
dependencies:
  firebase_app_check: ^0.7.0
  • Flutter 프로젝트의 main.dart 파일에 Firebase App Check를 구성합니다. 
import 'package:firebase_app_check/firebase_app_check.dart';
import 'package:flutter/material.dart';

void main() async {
  // Firebase 초기화
  WidgetsFlutterBinding.ensureInitialized();
  await Firebase.initializeApp();

  // Firebase App Check 활성화
  FirebaseAppCheck.instance.activate(webRecaptchaSiteKey: 'YOUR_RECAPTCHA_SITE_KEY');

  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Firebase App Check Demo',
      home: Scaffold(
        appBar: AppBar(
          title: Text('Flutter Firebase App Check Demo'),
        ),
        body: Center(
          child: Text('Hello World!'),
        ),
      ),
    );
  }
}
  • Firebase App Check를 사용하여 네트워크 요청을 검증하기 위해 package:http 패키지를 사용하여 네트워크 요청을 보낼 때, FirebaseAppCheckToken을 추가합니다.
import 'dart:convert';
import 'package:http/http.dart' as http;
import 'package:firebase_app_check/firebase_app_check.dart';

final _httpClient = http.Client();

Future<String> fetchData() async {
  // Firebase App Check 토큰 가져오기
  final FirebaseAppCheckToken appCheckToken = await FirebaseAppCheck.instance.getToken();

  // 네트워크 요청 보내기
  final response = await _httpClient.get(
    Uri.parse('https://example.com/data.json'),
    headers: {
      // Firebase App Check 토큰 헤더 추가
      'X-Firebase-AppCheck': 'Bearer ${appCheckToken.token}',
    },
  );

  // 데이터 반환
  return utf8.decode(response.bodyBytes);
}

 

728x90
반응형
반응형

flutter에서 클래스 내부에 선언된 변수와 메소드를 의미하며 클래스 멤버는 다음과 같이 분류됩니다.

  1. 인스턴스 변수 (Instance Variables) : 객체의 인스턴스에 속한 변수로, 객체를 생성할 때 초기화되며, 객체의 속성이나 상태를 나타냅니다.
  2. 정적 변수 (Static Variables) : 클래스에 속한 변수로, 클래스를 로드할 때 초기화되며, 클래스와 관련된 정보를 나타냅니다.
  3. 인스턴스 메소드 (Instance Methods) : 객체의 인스턴스에 속한 메소드로, 객체를 통해 호출됩니다. 객체의 속성이나 상태를 조작하거나, 객체와 관련된 행동을 나타냅니다.
  4. 정적 메소드 (Static Methods) : 클래스에 속한 메소드로, 클래스를 통해 호출됩니다. 클래스와 관련된 행동을 나타냅니다.

다음은 간단한 Flutter  클래스의 예시입니다.

class Person {
  String name;
  int age;

  Person(this.name, this.age);

  void sayHello() {
    print("Hello, my name is $name and I am $age years old.");
  }

  static String hello() {
    return "Hello from Person class!";
  }
}

위의 코드에서 name과 age는 인스턴스 변수

say Hello()는 인스턴스 메소드

hello()는 정적 메소드 입니다.

728x90
반응형
반응형

인스타그램과 같은 UI를 만들기 위해서 ListView.builder를 사용하여 상하 스크롤을 기본적으로 배치하고 각 페이지를 PageView.builder로  좌우 스크롤이 가능하게 구현을 했다.

실제로는 Firebase의 데이터를 불러서 동적으로 구현하지만 예제 소스는 텍스트 값을 리스트에 넣어서 정리했다.

class _HomePageState extends State<HomePage> {
  final ScrollController _scrollController = ScrollController();
  final PageController _pageController = PageController(initialPage: 1);

  List verticalList = ["1", "2"];
  List horizontalList = ["1", "2", "3"];

  @override
  Widget build(BuildContext context) {
    var width = MediaQuery.of(context).size.width;

    return Scaffold(
      appBar: AppBar(
        title: const Text('Photo Manager'),
      ),
      body: SafeArea(
        child: ListView.builder(
          controller: _scrollController,
          scrollDirection: Axis.vertical,
          shrinkWrap: true,
          itemCount: verticalList.length,
          itemBuilder: (context, index) =>
              SizedBox(
                  width: width,
                  height: width * 1.35,
                  child: PageView.builder(
                    controller: _pageController,
                    itemCount: horizontalList.length,
                    itemBuilder: (context, index) =>
                        Text(horizontalList[index])
                ),
              ))
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {},
        child: const Icon(Icons.add),
      ),
    );
  }
}

해당 소스를 실행해보면 화면 상하 2개의 위젯과 각 위젯 안에 3개씩의 위젯으로 구성된 결과가 보여지게 된다.

대충 이런 느낌....

상하좌우 스크롤을 해보면 문제없이 잘 동작한다. 화면상으로만 콘솔을 보지 않는다면 말이다.

콘솔을 확인해보면 아래와 같은 메시지를 볼수가 있다.

위의 메세지는 좌우 스크롤을 끝까지 한상태에서 한번더 스크롤을 해보면 발생한다.

'package:flutter/src/widgets/overscroll_indicator.dart': Failed assertion: line 243 pos 14: 'notification.metrics.axis == widget.axis': is not true.

overscroll_indicator.dart 에서 발생한다고 하니 스크롤이 범위를 벗어났다고 하는거 같긴한데 지금까지 해본결과 좌우던 상하던 오버스크롤이 되도 문제가 없었는데 느닷없는 오류에 많이 당황했다.

 

해당 메시지로 구글링을 해봐도 문제를 해결하기 위한 답변을 찾지 못하고 NotificationListener라는 힌트만 얻게 되었다.

그리고 NotificationListener에 대해서 찾아보기 시작한 결과

위와 같은 중첩구조에서  scroll event가 발생할 경우 상위의 위젯(위에선 ListView.builder)에서는 단일 위젯과 같기에 문제 없으나 내부에 있는 위젯(위에선 PageView.builder)에 발생한 event를 상위 위젯에서도 잡는다는 것이다.

page_view.dart나 list_view.dart 소스를 열어보면 안에 NotificationLIstener를 return 하는것을 찾아볼수 있다.

 

그럼 상위위젯에서 listening 하지 못하게 하면 해결되는거 아닌가? 란 생각과 함께 찾아본결과

해당 이벤트를 발생시키는 위젯을 NotificationLIstener위젯으로 감싸서 직접 컨트롤 할 수 있다는걸 알게 되었다.

class _HomePageState extends State<HomePage> {
  final ScrollController _scrollController = ScrollController();
  final PageController _pageController = PageController(initialPage: 1);

  List verticalList = ["1", "2"];
  List horizontalList = ["1", "2", "3"];

  @override
  Widget build(BuildContext context) {
    var width = MediaQuery.of(context).size.width;

    return Scaffold(
      appBar: AppBar(
        title: const Text('Photo Manager'),
      ),
      body: SafeArea(
        child: ListView.builder(
          controller: _scrollController,
          scrollDirection: Axis.vertical,
          shrinkWrap: true,
          itemCount: verticalList.length,
          itemBuilder: (context, index) =>
              SizedBox(
                  width: width,
                  height: width * 1.35,
                  child: NotificationListener<ScrollNotification> (
                    onNotification: (notification) {
                      return true;
                    },
                    child: PageView.builder(
                        controller: _pageController,
                        itemCount: horizontalList.length,
                        itemBuilder: (context, index) =>
                            Text(horizontalList[index])
                    ),
                  ))
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {},
        child: const Icon(Icons.add),
      ),
    );
  }
}

위처럼 변경하고 실행해보면 더이상 에러가 발생하지 않는다.

NotificationListener로 감싸서  onNotification을 무조건 true로 던졌다. true를 쓴 이유는 notification_listener.dart에 있는 설명에 나와있다.

Return true to cancel the notification bubbling. Return false to allow the notification to continue to be dispatched to further ancestors.

번역해보면

"알림 버블링을 취소하려면 true를 반환합니다. 알림이 추가 조상에게 계속 전달되도록 하려면 false를 반환합니다." 라고 한다.

지금은 상위 위젯에 연계해서 이벤트 처리할 일이 없기에 무조건 true로 처리하지만 내부 위젯과 연계해서 상위 위젯에 어떠한 이벤트를 주고자 한다면 

onNotification: (notification) {
.
.
}

onNotification에 인자로 받은  notification은 실제로 scroll_notification.dart 객체이기 때문에 소스를 열어보면 얻을 수 있는 정보들이 존재함을 확인할 수 있다.

NotificationListener와 관련된 부분은 아직 이해가 부족하고 추측성 결론이 많아 후에 조금 더 공부해 보기로 하고 마무리 한다.

728x90
반응형
반응형

AI 분야가 굉장히 빠르게 발전하고 관련 기술들이 조금씩 공개되고 있는데 그중에 가장 핫한 녀석이 ChatGPT이지 않을까 생각됩니다.

현재 무료로 사용해 볼 수 있기에 시험삼아 사용해 보았습니다.

 

한글은 오답률이 높다고 하여 못하는 영어로 질문을 던져봤는데요

문법과 상관없이 단어의 의미를 해석하고 연관성을 찾아 답변을 주는것에 놀랐습니다.

질문을 디테일하게 할수록 좋은 답변이 나온다고 하는데 본인의 영어 한계때문에 질문을 디테일하게 하지 못하는게 아쉽네요.

올해는 영어공부를 열심히 해야하겠네요.

 

직업이 개발자이다 보니 개발과 관련해서도  질문을 해봤습니다. 단 한글로요

flutter 를 시작하기 위한 제안보다 놀라운건 사이트 링크인데요 해당 단계와 관련된 정확한 페이지 였습니다. 구글링 보다 조금 더 편한 접근방법이 될 수 있겠다는 생각을 하게 되네요.

 

그럼 소스도 만들어 주는지 확인해봤습니다.

ChatGPT가 만들어준 소스입니다.

adobe XD나 제플린에 그린 그림을 export하는 소스와는 비교되지 않는 깔끔한 소스가 나오네요. (내 일자리를 잃는건가란 생각이..) 

import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        body: Container(
          child: GridView.count(
            crossAxisCount: 3,
            children: List.generate(
              9,
              (index) {
                return Container(
                  child: Image.network(
                    "https://picsum.photos/id/$index/200/200",
                    fit: BoxFit.cover,
                  ),
                );
              },
            ),
          ),
        ),
      ),
    );
  }
}

 실제로 실행해본 결과입니다. 놀랍게도 잘 동작합니다. 물론 원하는 결과물은 아니지만 짧은 요청글 하나로 이런 소스 코드를 내려준다는게 대단하다는 생각을 했습니다.

물론 현 단계에서는 문제도 있습니다.

ChatGPT는 2021년까지 학습을 진행한 탓에 그 이후 업데이트 된 내용들이 반영되지 않습니다.

예를 들어 상하 좌우 스크롤이 가능한 앱 소스를 만들어 달라고 했을때

위와 같은 내용이 포함되어 있는데 현재는 Axis는 vertical과 horizontal만 지원하고 있기때문에 오류가 발생합니다. 

그리고 상하 좌우기능을 동시에 만족하는 소스를 만들어주지 못합니다.(혹은 저의 영어능력이 문제일지도) 

그 이유는 학습데이터에 해당 사항이 없었던것이겠죠?

간혹 ai때문에 개발자들이 일자리를 잃는게 아니냐 하는 말도 나오는데 먼미래는 모르겠지만 아직은 대체할 수 있는 정도는 아니지 않을까 조심스럽게 예측해봅니다.

728x90
반응형
반응형
  • 일반 쿼리와 같이 특정 조건에 맞는 데이터를 찾고자할때 where를 사용한다. 
  • 예를 들어 현재 시간과 같거나 이전에 등록된 상품을 조회한다고 한다면 아래와 같이 쓰게 된다.
var snapshot = await FirebaseFirestore.instance.collection('product')
        .where('regTime', isLessThanOrEqualTo: DateTime.now().toString())
        .get();
  • 조회한 데이터를 sort시킨다고 하면 이 또한 일반 쿼리에서 사용하는 orderBy를 사용하면 된다.
  • 예를 들어 등록된 시간별로 sort시켜보자.
// 내림 차순
var snapshot = await FirebaseFirestore.instance.collection('product')
        .orderBy('regTime', descending: true)
        .get();
        
// 오름 차순
var snapshot = await FirebaseFirestore.instance.collection('product')
        .orderBy('regTime')
        .get();
  • 만약 sort 조건으로 두가지 이상을 사용한다고 하면 orderBy 뒤에 추가로 .orderBy(${field})를 추가해주면 된다.
  • 단 firebase는 단일 필드에 대한 인덱스는 자동으로 제공하지만 두가지 이상에 대해서는 직접 색인(index)를 추가해주어야 한다.
  • 그럼 where와 orderBy를 같이 사용하려고 한다면 어떻게 될까? 
var snapshot = await FirebaseFirestore.instance.collection('product')
        .where('regTime', isLessThanOrEqualTo: DateTime.now().toString())
        .orderBy('productName', descending: true).orderBy('price')
        .limit(5)
        .get();
  • 위의 코드와 같이 where조건과 orderBy를 진행했으나 원하는 결과가 아닌 오류가 발생하였다.
Failed assertion: line 487 pos 13: 'conditionField == orders[0][0]': 
The initial orderBy() field "[[FieldPath([productName]), true]][0][0]" 
has to be the same as the where() field parameter "FieldPath([regTime])" 
when an inequality operator is invoked.
  • where절에 사용한 parameter와 뒤에 이어지는 orderBy가 같아야 한다는 의미 메시지를 던져주게 된다.
  • 결과적으로 where와 orderBy를 같이 사용할때는 where절에 사용한 필드에 대한 orderBy가 바로 뒤에 따라오고 그 다음에 원하는 orderBy를 사용해야만 한다.
var snapshot = await FirebaseFirestore.instance.collection('product')
        .where('regTime', isLessThanOrEqualTo: DateTime.now().toString())
        .orderBy('regTime', descending: true)
        .orderBy('productName', descending: true).orderBy('price')
        .limit(5)
        .get();
  • 그리고 저렇게 된다면  where절에 사용한 필드를 orderBy에 사용했기때문에 firebase의 색인에 위의 코드에서 orderBy에 사용한 세가지 필드의 sort조건에 맞는 색인을 만들어 두어야 한다.
728x90
반응형
반응형
  • GetX 를 사용하여 페이지 전환
    • Get.toNamed('호출 페이지')를 사용하면 호출 페이지로 화면이 전환되어 짐
    • ex) Get.toNamed('/event')
    • Navigator.of(context).pushNamed('호출 페이지)')를 사용해도 동일한 결과를 얻을 수 있음
  • toNamed사용시 arguments 전달
    • Navigator를 사용하는 경우 다소 복잡한 절차가 필요함
    • 일단 빠르게 만들기 위해 GetX의 기능을 사용하기로 함
    • Get.toNamed('/event', arguments: {'choice': 'codeA'})와 같은 형태로 호출하면 됨
  • /event페이지에서 arguments사용
    • Navigator를 사용할 경우와 다르게 arguments를 받기 위한 생성자나 변수가 필요하지 않음
    • 해당 페이지내에서 arguments를 사용하고자 하는 위치에서 Get.arguments['choice'] 사용하면 'codeA'를 리턴해줌

샘플 소스

//event_main page
eventEnter(int index) async {
    final result = await Get.toNamed('event_enter', arguments: {'choiceCode' : index == 0 ? 'a' : 'b' });
    if (result == true) {
      setState(() {
        .....;
      });
    }
  }
  
  
  // event_enter page
  class EventEnterView extends StatelessWidget {
  	const EventEnterView({Key? key}) : super(key: key);

  	@override
  	Widget build(BuildContext context) {
    	return Container(
        	.
            .
            .
            child: ElevatedButton(
              onPressed: () async {
                Map<String, dynamic> data = {
                  'choice': Get.arguments['choiceCode'],
					.
                    .
                    .
                };
              }
            .
            .
            .
        );
    }

 

 

 

728x90
반응형

+ Recent posts