Flutter - 로그인, 로그아웃 구현하기 (1/3) - MySQL DB 연동, 레이아웃 설계

반응형

 

포스팅에서 진행한 환경

  • OS : Windows 10
  • IDE : Visual Studio Code
  • Emulator : Pixel 5 API 27 (Android-x86 emulator), Windows (windows-x64)

 

이번 포스팅에서는 로그인과 로그아웃을 위해 아래와 같은 내용을 다룹니다.

  • 유저의 아이디와 비밀번호를 users 테이블을 통해 유저를 식별하기 위한 MySQL DB 연동
  • 회원가입(계정 생성), 로그인, 자동 로그인, 로그아웃을 위한 레이아웃 설계

 

다른 설명이 필요하면 아래의 링크를 참고해주세요.


포스팅에서 다루는 예시 프로젝트는 아래의 깃허브 링크에서 다운로드 받을 수 있습니다.

'6_add_login_with_shared_preferences_and_mysql' 폴더를 확인해주세요.


기본 프로젝트 설정

  • pubspec.yaml 파일에 아래의 코드 추가
dependencies:
  flutter:
    sdk: flutter
  sqflite: ^2.2.8 # SQL 쿼리를 사용하기 위함
  mysql_client: ^0.0.27 # MySQL DB 연동을 하기 위함
  shared_preferences: ^2.1.0 # 공유저장소 사용을 하기 위함(자동 로그인)
  crypto: ^3.0.3 # 정보 암호화를 위함 (사용자 비밀번호 암호화)
  cupertino_icons: ^1.0.2 # iOS 스타일의 위젯 사용을 위함

MySQL DB 연동

아래의 포스팅에서 MySQL DB 연동에 대해 더 자세한 내용을 확인하실 수 있습니다.


MySQL 테이블 설계

참고 : 테스트를 위한 테이블이므로 최대한 간소화하여 테이블을 생성하였습니다.


MySQL 접속 설정

  • 프로젝트 디렉토리 lib 폴더 내에 'config'라는 이름의 폴더 생성 (폴더명은 사용자에 따라 명명)

 

  • MySQL 접속 설정의 정보를 저장하기 위한 dart 파일 생성 (mySQLConnector.dart)

 

  • 코드 작성
    • host, port, userName, password, databaseName은 자신의 DB 정보 입력
  • 코드 간단 설명
    • 반환타입을 MySQLConnection을 갖는, 즉 MySQL에 접속중인 상태 정보를 이용하여 다른 클래스에서 DB에 접근하기 위함
    • 때문에 코드의 마지막에서 conn.close()를 사용하지 않았음
/* mySqlConnector.dart */
import 'package:mysql_client/mysql_client.dart';
import 'package:flutter_memo_app/config/dbInfo.dart';

// MySQL 접속
Future<MySQLConnection> dbConnector() async {
  print("Connecting to mysql server...");

  // MySQL 접속 설정
  final conn = await MySQLConnection.createConnection(
    host: DbInfo.hostName,
    port: DbInfo.portNumber,
    userName: DbInfo.userName,
    password: DbInfo.password,
    databaseName: DbInfo.dbName, // optional
  );

  await conn.connect();

  print("Connected");

  return conn;
}

접속 확인

  • main.dart의 메인 함수에 정의한 MySQL 접속 함수 호출
import 'package:flutter/material.dart';
// MySQL 연동을 위한 다트 파일 호출
import 'package:flutter_memo_app/config/mySqlConnector.dart';

void main() {
  dbConnector(); // mySQLConnector.dart의 dbConnector 함수 호출
}

 

  • MySQL DB 접속 성공


레이아웃 설계

  • 프로젝트 디렉토리 lib 폴더 내의 'loginPage' 라는 이름의 폴더 생성 (폴더명은 사용자에 따라 명명)

 

  • 각각의 dart 파일 생성
    • loginDB.dart : MySQL의 쿼리를 수행하기 위한 코드가 있는 파일
      • 회원가입(계정 생성)
      • 유저ID 중복확인
      • 로그인
    • loginMainPage.dart : 로그인을 하기 위한 페이지
      • 추가 기능 : 자동 로그인 여부, 회원가입(계정 생성) 페이지로 이동
    • memberRegisterPage.dart : 회원가입(계정 생성)을 하기 위한 페이지

MySQL 쿼리 수행 파일(loginDB.dart)

아래의 포스팅에서 암호화에 대해 더 자세한 내용을 확인하실 수 있습니다.


코드 보기

더보기
import 'package:flutter_memo_app/config/hashPassword.dart';
import 'package:flutter_memo_app/config/mySqlConnector.dart';
import 'package:mysql_client/mysql_client.dart';

// 계정 생성
Future<void> insertMember(String userName, String password) async {
  // MySQL 접속 설정
  final conn = await dbConnector();

  // 비밀번호 암호화
  final hash = hashPassword(password);

  // DB에 유저 정보 추가
  try {
    await conn.execute(
        "INSERT INTO users (userName, password) VALUES (:userName, :password)",
        {"userName": userName, "password": hash});
    print(hash);
  } catch (e) {
    print('Error : $e');
  } finally {
    await conn.close();
  }
}

// 로그인
Future<String?> login(String userName, String password) async {
  // MySQL 접속 설정
  final conn = await dbConnector();

  // 비밀번호 암호화
  final hash = hashPassword(password);

  // 쿼리 수행 결과 저장 변수
  IResultSet? result;

  // DB에 해당 유저의 아이디와 비밀번호를 확인하여 users 테이블에 있는지 확인
  try {
    result = await conn.execute(
        "SELECT id FROM users WHERE userName = :userName and password = :password",
        {"userName": userName, "password": hash});

    if (result.isNotEmpty) {
      for (final row in result.rows) {
        print(row.assoc());
        // 유저 정보가 존재하면 유저의 index 값 반환
        return row.colAt(0);
      }
    }
  } catch (e) {
    print('Error : $e');
  } finally {
    await conn.close();
  }
  // 예외처리용 에러코드 '-1' 반환
  return '-1';
}

// 유저ID 중복확인
Future<String?> confirmIdCheck(String userName) async {
  // MySQL 접속 설정
  final conn = await dbConnector();

  // 쿼리 수행 결과 저장 변수
  IResultSet? result;

  // ID 중복 확인
  try {
    // 아이디가 중복이면 1 값 반환, 중복이 아니면 0 값 반환
    result = await conn.execute(
        "SELECT IFNULL((SELECT userName FROM users WHERE userName=:userName), 0) as idCheck",
        {"userName": userName});

    if (result.isNotEmpty) {
      for (final row in result.rows) {
        return row.colAt(0);
      }
    }
  } catch (e) {
    print('Error : $e');
  } finally {
    await conn.close();
  }
  // 예외처리용 에러코드 '-1' 반환
  return '-1';
}

로그인 페이지(loginMainPage.dart)

  • 레이아웃 설계에 관련된 포스팅이므로 로그인과 자동 로그인 기능은 다음 포스팅에서 다룹니다.
  • 다음과 같이 레이아웃 설계

 

코드보기

더보기
// ignore_for_file: use_build_context_synchronously

import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_memo_app/header&footer.dart';
import 'package:flutter_memo_app/loginPage/loginDB.dart';
import 'package:flutter_memo_app/loginPage/memberRegisterPage.dart';

// 로그인 페이지
class LoginMainPage extends StatelessWidget {
  const LoginMainPage({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: LoginPage(),
    );
  }
}

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

  @override
  State<LoginPage> createState() => _LoginState();
}

class _LoginState extends State<LoginPage> {
  // 자동 로그인 여부
  bool switchValue = false;

  // 아이디와 비밀번호 정보
  final TextEditingController userIdController = TextEditingController();
  final TextEditingController passwordController = TextEditingController();

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        body: Center(
          child: Padding(
            padding: const EdgeInsets.all(8.0),
            child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: <Widget>[
                // ID 입력 텍스트필드
                Padding(
                  padding: const EdgeInsets.all(8.0),
                  child: SizedBox(
                    width: 300,
                    child: CupertinoTextField(
                      controller: userIdController,
                      placeholder: '아이디를 입력해주세요',
                      textAlign: TextAlign.center,
                    ),
                  ),
                ),
                // 비밀번호 입력 텍스트필드
                Padding(
                  padding: const EdgeInsets.all(8.0),
                  child: SizedBox(
                    width: 300,
                    child: CupertinoTextField(
                      controller: passwordController,
                      placeholder: '비밀번호를 입력해주세요',
                      textAlign: TextAlign.center,
                      obscureText: true,
                    ),
                  ),
                ),
                // 자동 로그인 확인 토글 스위치
                SizedBox(
                  width: 300,
                  child: Row(
                    mainAxisAlignment: MainAxisAlignment.center,
                    children: [
                      Text(
                        '자동로그인 ',
                        style: TextStyle(
                            color: CupertinoColors.activeBlue,
                            fontWeight: FontWeight.bold),
                      ),
                      CupertinoSwitch(
                        // 부울 값으로 스위치 토글 (value)
                        value: switchValue,
                        activeColor: CupertinoColors.activeBlue,
                        onChanged: (bool? value) {
                          // 스위치가 토글될 때 실행될 코드
                          setState(() {
                            switchValue = value ?? false;
                          });
                        },
                      ),
                      Text('    '),
                      // 계정 생성 페이지로 이동하는 버튼
                      OutlinedButton(
                        onPressed: () {
                          Navigator.push(
                            context,
                            MaterialPageRoute(
                              builder: (context) => MemberRegisterPage(),
                            ),
                          );
                        },
                        child: Text(
                          '계정생성',
                        ),
                      ),
                    ],
                  ),
                ),
                // 로그인 버튼
                Padding(
                  padding: const EdgeInsets.all(8.0),
                  child: SizedBox(
                    width: 250,
                    child: ElevatedButton(
                      onPressed: () {},
                      child: Text('로그인'),
                    ),
                  ),
                ),
              ],
            ),
          ),
        ),
      ),
    );
  }
}

계정 생성 페이지(memberRegisterPage.dart)

  • 레이아웃 설계에 관련된 포스팅이므로 계정 생성 기능은 다음 포스팅에서 다룹니다.
  • 다음과 같이 레이아웃 설계

 

코드 보기

더보기
// ignore_for_file: use_build_context_synchronously

import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_memo_app/loginPage/loginDB.dart';

class MemberRegisterPage extends StatefulWidget {
  const MemberRegisterPage({Key? key}) : super(key: key);

  @override
  State<MemberRegisterPage> createState() => _MemberRegisterState();
}

class _MemberRegisterState extends State<MemberRegisterPage> {
  // 유저의 아이디와 비밀번호의 정보 저장
  final TextEditingController userIdController = TextEditingController();
  final TextEditingController passwordController = TextEditingController();
  final TextEditingController passwordVerifyingController =
      TextEditingController();

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        body: Center(
          // 아이디 입력 텍스트필드
          child: Padding(
            padding: const EdgeInsets.all(8.0),
            child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: <Widget>[
                Padding(
                  padding: const EdgeInsets.all(8.0),
                  child: SizedBox(
                    width: 300,
                    child: CupertinoTextField(
                      controller: userIdController,
                      placeholder: '아이디를 입력해주세요',
                      textAlign: TextAlign.center,
                    ),
                  ),
                ),
                // 비밀번호 입력 텍스트필드
                Padding(
                  padding: const EdgeInsets.all(8.0),
                  child: SizedBox(
                    width: 300,
                    child: CupertinoTextField(
                      controller: passwordController,
                      placeholder: '비밀번호를 입력해주세요',
                      textAlign: TextAlign.center,
                      obscureText: true,
                    ),
                  ),
                ),
                // 비밀번호 재확인 텍스트필드
                Padding(
                  padding: const EdgeInsets.all(8.0),
                  child: SizedBox(
                    width: 300,
                    child: CupertinoTextField(
                      controller: passwordVerifyingController,
                      placeholder: '비밀번호를 다시 입력해주세요',
                      textAlign: TextAlign.center,
                      obscureText: true,
                    ),
                  ),
                ),
                // 로그인 페이지로 돌아가기
                Padding(
                  padding: const EdgeInsets.all(8.0),
                  child: Row(
                    mainAxisAlignment: MainAxisAlignment.center,
                    children: [
                      SizedBox(
                        width: 95,
                        child: OutlinedButton(
                          onPressed: () {
                            Navigator.of(context).pop();
                          },
                          child: Text('뒤로 가기'),
                        ),
                      ),
                      Text('   '),
                      // 계정 생성 버튼
                      SizedBox(
                        width: 195,
                        child: ElevatedButton(
                          onPressed: () {},
                          child: Text('계정 생성'),
                        ),
                      ),
                    ],
                  ),
                ),
              ],
            ),
          ),
        ),
      ),
    );
  }
}

 

반응형