[Flutter] 스나이퍼팩토리 Flutter 주간평가 (7주차)

GONG·2023년 5월 10일
0
post-thumbnail

[github 링크]


github - 7주차 주간평가

문제 1.

로그인페이지에서 다음과 같이 [로그인정보 저장] 기능을 구현한다.


  • 체크박스를 누르고 로그인을 진행하면, 앱을 재시작해도 (새로고침하여도) 자동으로 로그인이 진행되도록 한다.
  • 이 때, 로그인시 얻은 token값을 저장해야 하며, 아래 API를 사용하여 유저정보를 가져올 수 있다.
    • POST http://52.79.115.43:8090/api/collections/users/auth-refresh
    • header에 Authorization값으로 “Bearer $token”을 보내면 다음의 결과를 얻을 수 있다.
      // 성공시 Response
      {
        "token": "JWT_TOKEN",
        "record": {
          "id": "RECORD_ID",
          "collectionId": "_pb_users_auth_",
          "collectionName": "users",
          "created": "2022-01-01 01:00:00Z",
          "updated": "2022-01-01 23:59:59Z",
          "username": "username123",
          "verified": false,
          "emailVisibility": true,
          "email": "test@example.com",
          "name": "test",
          "avatar": "filename.jpg"
        }
      }

📄 api_routes.dart

  • 수정된 코드
    class ApiRoutes {
      static const String baseUrl = 'http://52.79.115.43:8090/';
      static const String authWithPassword = 'api/collections/users/auth-with-password';
      static const String signUp = 'api/collections/users/records';
      static const String readSecret = 'api/collections/secrets/records?sort=-created';
      static const String uploadSecret = 'api/collections/secrets/records';
    	// 자동 로그인에 필요한 api 경로 추가
      static const String autoLogin = 'http://52.79.115.43:8090/api/collections/users/auth-refresh';
    }

📄 auth_controller.dart

  • 수정된 코드
    import 'package:dio/dio.dart';
    import 'package:get/get.dart';
    import 'package:shared_preferences/shared_preferences.dart';
    
    import '../model/user.dart';
    import '../util/app_routes.dart';
    import '../util/api_routes.dart';
    
    class AuthController extends GetxController {
      final Rxn<User> _user = Rxn();
      String? _token;
      Dio dio = Dio();
    
      User? get user => _user.value;
    
      login(String id, String pw, bool saveToken) async {
        dio.options.baseUrl = ApiRoutes.baseUrl;
    
        try {
          var res = await dio.post(
            ApiRoutes.authWithPassword,
            data: {'identity': id, 'password': pw}
          );
          if (res.statusCode == 200) {
            var user = User.fromMap(res.data['record']);
            _user(user);
    
    				// login 함수에 토큰 저장 코드 추가
    				// ~~~ 추가된 부분 ~~~
            if (saveToken) {
              SharedPreferences prefs = await SharedPreferences.getInstance();
              _token = res.data['token'];
              prefs.setString('token', _token!);
            }
    				// ~~~ 여기까지 ~~~
          }
        } on DioError catch(e) {
          Get.snackbar('로그인 실패!', '아이디와 비밀번호를 다시 확인해보게...');
          print(e.message);
        }
      }
    
      signUp(String email, String password, String passwordConfirm, String username) async {
        dio.options.baseUrl = ApiRoutes.baseUrl;
    
        try {
          await dio.post(
            ApiRoutes.signUp,
            data: {
              'email': email,
              'password': password,
              'passwordConfirm': passwordConfirm,
              'username': username,
            }
          );
          Get.snackbar('회원가입 성공~', '환영합니다');
          Get.back();
        } on DioError catch(e) {
          print(e.message);
          Get.snackbar('회원가입 실패', '환영할뻔');
        }
      }
    
      logout() {
        _user.value = null;
      }
    
    	// auth_controller에 autoLogin 함수 추가
    	// ~~~ 추가된 부분 ~~~
      autoLogin() async {
        SharedPreferences prefs = await SharedPreferences.getInstance();
        _token = prefs.getString('token');
        if (_token != null) {
          try {
            var res = await dio.post(
              ApiRoutes.autoLogin,
              options: Options(headers: {"Authorization": 'Bearer $_token'}),
            );
            if (res.statusCode == 200) {
              var user = User.fromMap(res.data['record']);
              _user(user);
              return;
            }
          } on DioError catch(e) {
            print('auto login : ${e.message}');
          }
        }
        Get.offNamed(AppRoutes.login);
      }
    	// ~~~ 여기까지 ~~~
    
      _handleAuthChanged(User? userData) {
        if (userData != null) {
          Get.offNamed(AppRoutes.main);
          return;
        }
        Get.offAllNamed(AppRoutes.login);
        return;
      }
    
      
      void onInit() {
        super.onInit();
        ever(_user, _handleAuthChanged);
      }
    }

📄 login_controller.dart

  • 체크박스에 활용할 saveToken 변수 만들고, auth controller의 login() 메서드의 매개변수로 값 넘겨주기
    import 'package:flutter/material.dart';
    import 'package:get/get.dart';
    import 'package:shared_preferences/shared_preferences.dart';
    
    import 'auth_controller.dart';
    
    class LoginController extends GetxController {
      var idController = TextEditingController();
      var pwController = TextEditingController();
      RxBool saveToken = false.obs;
    
      login() async {
        Get.find<AuthController>().login(idController.text, pwController.text, saveToken.value);
      }
    }

📄 login_page.dart

  • 로그인 페이지에 체크박스 만들기
    ...
    Row(
      children: [
        Obx(() => Checkbox(
          value: controller.saveToken.value,
          onChanged: (value) {
            controller.saveToken(value!);
          }
        )),
        Text('로그인 정보 저장'),
      ],
    ),
    ...

📄 splash_page.dart

  • 스플래시 화면 코드 수정
    import 'package:flutter/material.dart';
    import 'package:get/get.dart';
    
    import '../../controller/auth_controller.dart';
    
    class SplashPage extends StatelessWidget {
      const SplashPage({Key? key}) : super(key: key);
    
      
      Widget build(BuildContext context) {
    
        Future.delayed(const Duration(seconds: 3), () {
          Get.find<AuthController>().autoLogin();
        });
    
        return Scaffold(
          body: Container(
            decoration: BoxDecoration(
              image: DecorationImage(
                image: AssetImage('assets/images/secret_hamburger/Splash.png'),
                fit: BoxFit.fill
              )
            ),
          )
        );
      }
    }


문제 2.

비밀을 업로드할 때 작성자를 남길 수 있다.
[내이름 공개하기] 체크버튼이 활성화 된 상태라면 현재 사용자의 이름을 함께 남길 수 있도록 하시오.


  • 이 때 필요시 Upload API를 확인한다. (33일차 과제의 내용과 같음)

    📄 Upload 비밀을 업로드하는 기능

    • POST http://52.79.115.43:8090/api/collections/secrets/records
    • Request
      • secret (String)
      • author (String, (optional))
        • 해당 author는 User Record ID를 입력할 것
      • authorName (String, (optional))
        • 해당 authorName은 닉네임을 입력할 것
    • Success Response
      {
        "id": "RECORD_ID",
        "collectionId": "5647cebjvtwtcu1",
        "collectionName": "secrets",
        "created": "2022-01-01 01:00:00Z",
        "updated": "2022-01-01 23:59:59Z",
        "secret": "test",
        "author": "RELATION_RECORD_ID",
      	"authorName":"test"
      }

📄 upload_controller.dart

  • 체크박스에 활용할 showName 변수 만들고 showName이 true면 authorName에 user.name, false면 빈 문자열 넣기
    import 'package:dio/dio.dart';
    import 'package:flutter/material.dart';
    import 'package:get/get.dart';
    
    import '../util/api_routes.dart';
    import 'auth_controller.dart';
    
    class UploadController extends GetxController {
      TextEditingController secretController = TextEditingController();
      Dio dio = Dio();
    	// ~~~ 추가된 코드 ~~~
      RxBool showName = false.obs;
    	// ~~~ 추가된 코드 ~~~
    
      RxString hamText = '진짜\n나만 알고\n잇을게'.obs;
    
      var user = Get.find<AuthController>().user;
    
      upload() async {
        dio.options.baseUrl = ApiRoutes.baseUrl;
        if (secretController.text == '') return;
    
        try {
          var res = await dio.post(
            ApiRoutes.uploadSecret,
            data: {
              'secret': secretController.text,
              'author': user!.id,
    					// ~~~ 수정된 코드 ~~~
              'authorName': showName.value ? user!.username : '',
    					// ~~~ 수정된 코드 ~~~
            }
          );
          if (res.statusCode == 200) {
            Get.snackbar('비밀 등록 성공!', '야호');
            hamText('헤헤\n뻥이지롱');
            Future.delayed(Duration(seconds: 3), () {
              hamText('진짜\n나만 알고\n잇을게');
            });
          }
        } on DioError catch(e) {
          Get.snackbar('비밀 등록 실패', 'ㅠㅠ');
          print(e.message);
        }
      }
    }

📄 upload_page.dart

  • 체크박스 추가
    Row(
      children: [
        Obx(() => Checkbox(
          value: controller.showName.value,
          onChanged: (value) {
            controller.showName(value!);
          }
        )),
        Text('이름 공개하기'),
      ],
    ),


문제 3.

Singleton이라는 디자인 패턴에 대해 학습하고, 내용을 정리하며 Dart 코드를 제시하시오.


  • 이 때, Singleton의 코드는 검색을 통해 참고할 수 있도록 한다.
  • 어떠한 경우에 Singleton 디자인 패턴이 유용하게 쓰일 수 있을지 고민하고 정리하시오.

Singleton 패턴

  • 클래스의 인스턴스를 오직 하나만 생성하고 전역적으로 접근할 수 있도록 하는 패턴
  • 전역 변수의 사용을 최소화하고, 여러 곳에서 동시에 같은 인스턴스에 접근할 수 있는 단일 지점을 제공
  • 주로 리소스 관리, 로깅, 데이터베이스 연결 등의 상황에서 사용됨

https://velog.velcdn.com/images/gongd/post/1fa4b927-4184-4e8c-9af7-0271b0025e5a/image.png

특징


  1. 하나의 인스턴스싱글톤 클래스는 오직 하나의 인스턴스만을 생성→ 인스턴스는 클래스 내부에서 관리되며, 외부에서는 직접 생성할 수 없음
  2. 전역적인 접근싱글톤 인스턴스는 전역적으로 접근 가능함→ 다른 클래스나 모듈에서 인스턴스에 접근할 수 있으며 동시에 같은 인스턴스에 접근할 수 있음
  3. 지연된 초기화싱글톤 인스턴스는 필요한 시점에서 생성됨→ 처음 사용되기 전까지는 인스턴스가 생성되지 않음→ 자원의 낭비를 방지할 수 있음

예시 코드

class Singleton {
  static Singleton _instance;

  // private 생성자
  Singleton._();

  factory Singleton.getInstance() {
    if (_instance == null) {
      _instance = Singleton._();
    }
    return _instance;
  }

  // 싱글톤 인스턴스의 기능과 데이터
}
  • _instance가 null인 경우에만 인스턴스를 생성하고, 그렇지 않은 경우에는 이미 생성된 인스턴스를 반환 → 싱글톤 클래스의 인스턴스는 오직 하나만 생성되고 전역적으로 접근할 수 있음

문제 4.

기존의 비밀듣는고양이의 앱은 각 컨트롤러에서 네트워크 통신이 필요하면 서로 다른 Dio객체를 각각 생성해야 했었다. Singleton으로 단일 객체를 사용하기 위하여 Dio를 Singleton을 통해 사용할 수 있는 방법에 대한 코드를 OpenAI의 ChatGPT를 활용하여 생성하시오.


  • 이 때 클래스의 이름은 CustomDio로 정의한다.
  • 얻은 코드를 사용하여 비밀듣는고양이 앱에 사용되는 다음의 Controller에 각각 적용하고
    과정을 설명하시오.
    - AuthController의 Login, Signup기능
    - SecretController의 비밀읽기 기능
    - UploadController의 비밀업로드 기능
  • 이 때 Dio를 사용한 CustomDio를 만들어 Singleton으로 사용하면 어떠한 이점이 있는지 고민하고 정리하시오

ChatGPT:

챗선생님의 말씀…


아래의 코드는 CustomDio라는 Singleton 클래스를 생성하여 Dio 객체를 관리하는 예시입니다. 코드는 Flutter에서 사용할 수 있도록 작성되었습니다.

import 'package:dio/dio.dart';

class CustomDio {
  static CustomDio? _instance;
  late Dio _dio;

  factory CustomDio() {
    _instance ??= CustomDio._(); // 인스턴스가 없는 경우에만 생성
    return _instance!;
  }

  CustomDio._() {
    _dio = Dio(); // Dio 객체 초기화
    // 추가적인 설정을 할 수 있습니다.
  }

  Dio get dio => _dio;
}

위의 코드를 사용하면 CustomDio 클래스를 Singleton으로 사용할 수 있습니다. 다음은 사용 예시입니다:

void main() {
  CustomDio customDio = CustomDio(); // Singleton 객체 생성

  Dio dio1 = customDio.dio; // Singleton 객체에서 Dio 인스턴스 가져오기
  dio1.get('https://api.example.com/endpoint1');

  Dio dio2 = customDio.dio; // 이미 생성된 Singleton 객체에서 다시 Dio 인스턴스 가져오기
  dio2.post('https://api.example.com/endpoint2');
}

위의 코드에서 CustomDio 클래스의 dio getter 메서드를 통해 Singleton 객체 내부의 Dio 인스턴스를 얻을 수 있습니다. Singleton으로 생성된 CustomDio 객체를 통해 다양한 컨트롤러에서 동일한 Dio 인스턴스에 접근할 수 있습니다.

적용

util > custom_dio.dart

  • Singleton 클래스를 생성하여 Dio 객체를 관리
    import 'package:dio/dio.dart';
    
    import 'api_routes.dart';
    
    class CustomDio {
      static CustomDio? _instance;
      late Dio _dio;
    
      factory CustomDio() {
        _instance ??= CustomDio._(); // 인스턴스가 없는 경우에만 생성
        return _instance!;
      }
    
      CustomDio._() {
        _dio = Dio(); // Dio 객체 초기화
        // 추가적인 설정을 할 수 있습니다.
        dio.options.baseUrl = ApiRoutes.baseUrl;
      }
    
      Dio get dio => _dio!;
    }

📄 auth_controller.dart

  • CustomDio 클래스로 singleton 객체 생성
    import 'package:dio/dio.dart';
    import 'package:get/get.dart';
    import 'package:shared_preferences/shared_preferences.dart';
    
    import '../model/user.dart';
    import '../util/app_routes.dart';
    import '../util/api_routes.dart';
    import '../util/custom_dio.dart';
    
    class AuthController extends GetxController {
      final Rxn<User> _user = Rxn();
      String? _token;
      Dio dio = CustomDio().dio;
    
      User? get user => _user.value;
    
      login(String id, String pw, bool saveToken) async {
    
        try {
          var res = await dio.post(
            ApiRoutes.authWithPassword,
            data: {'identity': id, 'password': pw}
          );
          if (res.statusCode == 200) {
            var user = User.fromMap(res.data['record']);
            _user(user);
    
            if (saveToken) {
              SharedPreferences prefs = await SharedPreferences.getInstance();
              _token = res.data['token'];
              prefs.setString('token', _token!);
            }
          }
        } on DioError catch(e) {
          Get.snackbar('로그인 실패!', '아이디와 비밀번호를 다시 확인해보게...');
          print(e.message);
        }
      }
    
      signUp(String email, String password, String passwordConfirm, String username) async {
    
        try {
          await dio.post(
            ApiRoutes.signUp,
            data: {
              'email': email,
              'password': password,
              'passwordConfirm': passwordConfirm,
              'username': username,
            }
          );
          Get.snackbar('회원가입 성공~', '환영합니다');
          Get.back();
        } on DioError catch(e) {
          print(e.message);
          Get.snackbar('회원가입 실패', '환영할뻔');
        }
      }
    
      logout() {
        _user.value = null;
      }
    
      autoLogin() async {
        SharedPreferences prefs = await SharedPreferences.getInstance();
        _token = prefs.getString('token');
        if (_token != null) {
          try {
            var res = await dio.post(
              ApiRoutes.autoLogin,
              options: Options(headers: {"Authorization": 'Bearer $_token'}),
            );
            if (res.statusCode == 200) {
              var user = User.fromMap(res.data['record']);
              _user(user);
              return;
            }
          } on DioError catch(e) {
            print('auto login : ${e.message}');
          }
        }
        Get.offNamed(AppRoutes.login);
      }
    
      _handleAuthChanged(User? userData) {
        if (userData != null) {
          Get.offNamed(AppRoutes.main);
          return;
        }
        Get.offAllNamed(AppRoutes.login);
        return;
      }
    
      
      void onInit() {
        super.onInit();
        ever(_user, _handleAuthChanged);
      }
    }

📄 secret_controller.dart

  • CustomDio 클래스로 singleton 객체 생성
    import 'package:dio/dio.dart';
    import 'package:flutter/material.dart';
    import 'package:get/get.dart';
    
    import '../model/secret.dart';
    import '../util/api_routes.dart';
    import '../util/custom_dio.dart';
    
    class SecretController extends GetxController {
      RxList<Secret> secrets = RxList();
      Dio dio = CustomDio().dio;
    
      PageController pageController = PageController();
      RxBool isPageChanging = false.obs;
      int currentPageIndex = 0;
    
      readSecrets() async {
        try {
          var res = await dio.get(ApiRoutes.readSecret);
          if (res.statusCode == 200) {
            List<Map<String, dynamic>> data = List<Map<String, dynamic>>.from(res.data['items']);
            secrets(data.map((e) => Secret.fromMap(e)).toList().obs);
          }
        } on DioError catch(e) {
          print(e.message);
        }
      }
    
      void _handlePageChange() {
        if (pageController.page!.toInt() != currentPageIndex) {
          isPageChanging.value = true;
        } else {
          isPageChanging.value = false;
        }
      }
    
      
      void onInit() {
        super.onInit();
        readSecrets();
    
        pageController = PageController(initialPage: currentPageIndex);
        pageController.addListener(_handlePageChange);
      }
    
      
      void onClose() {
        super.onClose();
        pageController.dispose();
      }
    }

📄 upload_controller

  • CustomDio 클래스로 singleton 객체 생성
    import 'package:dio/dio.dart';
    import 'package:flutter/material.dart';
    import 'package:get/get.dart';
    
    import '../util/api_routes.dart';
    import '../util/custom_dio.dart';
    import 'auth_controller.dart';
    
    class UploadController extends GetxController {
      TextEditingController secretController = TextEditingController();
      Dio dio = CustomDio().dio;
      RxBool showName = false.obs;
    
      RxString hamText = '진짜\n나만 알고\n잇을게'.obs;
    
      var user = Get.find<AuthController>().user;
    
      upload() async {
        if (secretController.text == '') return;
    
        try {
          var res = await dio.post(
            ApiRoutes.uploadSecret,
            data: {
              'secret': secretController.text,
              'author': user!.id,
              'authorName': showName.value ? user!.username : '',
            }
          );
          if (res.statusCode == 200) {
            Get.snackbar('비밀 등록 성공!', '야호');
            hamText('헤헤\n뻥이지롱');
            Future.delayed(Duration(seconds: 3), () {
              hamText('진짜\n나만 알고\n잇을게');
            });
          }
        } on DioError catch(e) {
          Get.snackbar('비밀 등록 실패', 'ㅠㅠ');
          print(e.message);
        }
      }
    }

이점

Dio를 사용한 CustomDio를 만들어 Singleton으로 사용하면


  1. 리소스 공유
    모든 컨트롤러 또는 클래스에서 동일한 Dio 인스턴스를 공유할 수 있음
    → 여러 곳에서 동시에 Dio 인스턴스를 생성하거나 관리할 필요가 없으므로 리소스 사용이 줄어든다
  2. 일관성
    동일한 Dio 인스턴스를 사용하면 네트워크 요청에 대한 일관성을 유지할 수 있음
    → 인증 토큰이나 인터셉터와 같은 Dio 구성을 단일 인스턴스에 적용하면 앱 전체에서 일관된 설정을 사용할 수 있음
  3. 성능 개선
    Dio 인스턴스를 생성하는 데는 약간의 오버헤드가 발생할 수 있음
    → Singleton 패턴을 사용하면 앱 시작 시에 Dio 인스턴스를 미리 생성하고 필요할 때마다 공유하여 이러한 오버헤드를 줄일 수 있음
  4. 테스트 용이성
    Dio를 Singleton으로 사용하면 테스트를 더 쉽게 수행할 수 있음
    → 모든 테스트에서 동일한 Dio 인스턴스를 사용하여 일관된 결과를 얻을 수 있음
    → 테스트 중에 Dio 인스턴스를 모의(mock) 또는 대체(substitute)할 수도 있음

7주차 끝!

조금 늦었지만 ..^^ 7주차도 끝냈습니다~ (8주차 수요일에 작성중)
이론과정이 끝나가고 있네요... 시간이 왜케빨리가지....

이제 드디어 7주차 과제들을 모두 해치워서 파이어베이스로 넘어갈 수 있게 되었습니다 ㅎㅎ

재밋겟다!!!!!!!!!

profile
우와재밋다

0개의 댓글