[Flutter] 플러터로 쇼핑몰 앱 만들기 [1]

     

 

플러터로 쇼핑몰 앱 만들기

 

구현 기능 

- 회원가입 / 로그인

- 상품 조회

- 장바구니

 

개발환경

- 윈도우 10, IntelliJ

 

github:

전체 - tkdlek11112/shopingmall_flutter: simple shopingmall by flutter (github.com)

이번 포스팅 완료 브랜치 -  tkdlek11112/shopingmall_flutter at 플러터쇼핑몰만들기_1 (github.com)

 

 

프로젝트 만들기

인텔리제이에서 플러터 프로젝트를 생성합니다.
쇼핑몰 플러터

 

프로젝트를 만들면 바로 git에 올리기 위해서 리드미 파일을 만듭니다 ㅋㅋ 

시작은 리드미

리드미 파일만 만들었을 뿐인데 많이 한 느낌.. 시작이 반이랄까 ^^

 

 

메인화면 만들기 - 하단 바

일단 무언가 시작을 하려면 어떤 화면을 만들어야 할지 생각하고 만들어야겠지요? 제일 먼저 아래 화면을 그릴 예정입니다.

메인화면

메인화면입니다. 쇼핑몰 앱을 만들자고 하긴 했는데 앱으로 실행하려면 안드로이드 기기가 있어야 되니까 웹으로 실행해 봤습니다 ㅋㅋ. 지금 개발환경이 윈도우라서 안드만 실행 가능한데 안드로 해볼까요...?

 

안드로이드 에뮬레이터로 실행

오 생각보다 금방 켜지네요. 아무튼 메인화면은 위 스크린샷처럼 하단 바를 가진 화면입니다. 4개의 탭이 있는데 아직 탭만 있고 화면은 없습니다 ㅋㅋ 이 것처럼 만들기 위해 main.dart와 screen_index.dart를 만듭니다.

 

//main.dart
import 'package:flutter/material.dart';
import 'screens/screen_index.dart';

void main (){
  runApp(MyApp());
}

class MyApp extends StatelessWidget {

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Shopping mall',
      routes: {
        '/index': (context) => IndexScreen(),
      },
      initialRoute: '/index',
    );
  }
}

 

main.dart 파일입니다. 기본적으로 생성되는 소스들 지우고 간단하게 만들었습니다. main.dart만 만들었다면 IndexScreen()이 없다고 에러가 나는데 screen_index.dart에서 만들어지기 때문에 괜찮습니다.

 

build에 보면 routes라는 항목이 있는데, 이것은 말 그대로 화면을 routing 해주는 기능입니다. 앱에도 여러 개의 화면이 있으니 각각 화면을 uri로 구분하고, 화면을 이동할 때 uri를 호출해서 이동할 수 있습니다. Navigator를 이용해서 이동할 수도 있지만 화면이 여러 개일 경우 routes를 사용하면 훨씬 더 간단하게 화면 간에 이동을 할 수 있습니다. 

 

여기서는 먼저 index화면을 만들고 initialRoute를 index화면으로 설정했습니다. initialRoute는 말 그대로 앱을 처음 실행했을 때 나오는 화면입니다.

 

//screens/screen_index.dart
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';

class IndexScreen extends StatefulWidget {

  @override
  _IndexScreenState createState() {
    return _IndexScreenState();
  }
}

class _IndexScreenState extends State<IndexScreen> {

  int _currentIndex = 0;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(),
      bottomNavigationBar: BottomNavigationBar(
        type: BottomNavigationBarType.fixed,
        iconSize: 44,
        selectedItemColor: Colors.blue,
        unselectedItemColor: Colors.grey,
        selectedLabelStyle: TextStyle(fontSize: 12),
        currentIndex: _currentIndex,
        onTap: (index) {
          setState(() {
            _currentIndex = index;
          });
        },
        items: [
          BottomNavigationBarItem(icon: Icon(Icons.home), label: 'home'),
          BottomNavigationBarItem(icon: Icon(Icons.search), label: 'search'),
          BottomNavigationBarItem(icon: Icon(Icons.shopping_cart), label: 'cart'),
          BottomNavigationBarItem(icon: Icon(Icons.person), label: 'profile'),
        ],
      ),
    );
  }
}

 

기본이 되는 화면 중에 하나인 screen_index.dart입니다. 화면들은 screen이라는 폴더를 만들어서 관리하겠습니다. bottomNavigationBar를 사용하면 아주 쉽게 하단 바를 만들 수 있습니다. 이것저것 설정한 다음 items에다가 하단바에 넣을 아이콘들을 정의하면 됩니다. 

 

여기서 State를 사용해서 위젯을 만들었는데, 사용자가 어떤 하단 바를 누른 상태인지 알기 위해 _currentIndex라는 값을 상태로 가지고 있습니다. 위 코드까지 만들고 나서 실행을 하게되면 처음 보여드린것처럼 하단바가 있는 화면이 실행됩니다. 아직 하단바의 메뉴들을 눌렀을 때 화면 변화를 주지 않았기 때문에 아무일도 일어나지 않습니다. 이제 하단바를 눌렀을 때 나오는 화면들을 각각 만들어봅시다.

 

 

하단바를 누르면 화면 전환하기 - 탭 만들기

하단 바에 버튼들을 각각 탭 화면으로 만들어보도록 하겠습니다. 4개의 탭이 있기 때문에 파일도 4개를 만들어야겠죠? 화면은 screens 폴더에 모아서 관리하는데, tab들은 tabs폴더를 따로 만들겠습니다.

 

폴더구조

탭 화면들의 이름은 각각 아이콘 이름으로 만들었습니다. cart, home, profile, search 4개를 만들고 화면을 구분할 수 있게 간단히 텍스트만 띄워보겠습니다.

 

// tabs/tab_cart.dart
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';

class TabCart extends StatelessWidget{

  @override
  Widget build(BuildContext context) {
    return const Center(
      child: Text("Cart Tab"),
    );
  }
}

 

위 코드처럼 TabXXX로 이름만 바꿔서 4개를 만듭니다. build에 있는 위젯에 Text 위젯도 적절히 메시지를 바꿔줍니다. 이제 4개의 탭이 생겼으니 이 탭들을 screen_index.dart에 등록해야 합니다. 

 

우리가 하단 바를 사용할 때 중앙에 있는 화면은 body입니다. 따라서 하단 탭이 변경될 때마다 body에 나오는 화면을 바꿔주면 우리가 기대한 대로 작동하게 됩니다. screen_index.dart에 아래와 같이 Widget리스트를 추가하고 body를 넣습니다.

 // screens/index_screen.dart
 
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:shopingmall_flutter/tabs/tab_home.dart';
import 'package:shopingmall_flutter/tabs/tab_profile.dart';
import 'package:shopingmall_flutter/tabs/tab_search.dart';

import '../tabs/tab_cart.dart';

class IndexScreen extends StatefulWidget {

  @override
  _IndexScreenState createState() {
    return _IndexScreenState();
  }
}

class _IndexScreenState extends State<IndexScreen> {

  int _currentIndex = 0;

  final List<Widget> tabs = [
    TabHome(),
    TabSearch(),
    TabCart(),
    TabProfile(),
  ];

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(),
      bottomNavigationBar: BottomNavigationBar(
        type: BottomNavigationBarType.fixed,
        iconSize: 44,
        selectedItemColor: Colors.blue,
        unselectedItemColor: Colors.grey,
        selectedLabelStyle: TextStyle(fontSize: 12),
        currentIndex: _currentIndex,
        onTap: (index) {
          setState(() {
            _currentIndex = index;
          });
        },
        items: [
          BottomNavigationBarItem(icon: Icon(Icons.home), label: 'home'),
          BottomNavigationBarItem(icon: Icon(Icons.search), label: 'search'),
          BottomNavigationBarItem(icon: Icon(Icons.shopping_cart), label: 'cart'),
          BottomNavigationBarItem(icon: Icon(Icons.person), label: 'profile'),
        ],
      ),
      body: tabs[_currentIndex],
    );
  }
}

추가된 코드는 _currentIndex 뒤에 tabs라는 리스트를 추가했고, Scaffold 위젯 안에 body를 tabs[_currentIndex]로 변경했습니다. 이제 탭을 클릭하면 onTap이 동작하면서 setState를 통해 _currentIndex값을 변경하게 되고, 변경될 때마다 body가 알맞은 화면으로 변경되는 것을 알 수 있습니다.

 

탭을 누르면 변경

 

로그인 만들기 - splash 화면 넣기, 로그인 여부 체크

모든 서비스의 시작은 로그인 아니겠습니까~? ㅋㅋ 앱을 처음 실행했을 때 로그인 화면을 만들어보도록 하겠습니다. 보통 한번 로그인 하면 자동 로그인이 되는 구조이기 때문에, 스플래쉬화면을 실행시키고 로그인여부를 체크해서 로그인화면을 보여주던지, 넘어가던지 하는 구조로 만들어보겠습니다. (*스플래쉬 화면은 앱 실행할 때 잠깐 애니메이션 나오는 화면을 말합니다.)

 

시작하기 전에 우리는 클라이언트를 만들고 있습니다. 나중에 따로 백엔드를 만들 예정이긴 한데, 클라이언트만 개발하고 싶은 분들도 있기 때문에 여기서는 firebase를 통해 로그인과 회원가입을 만들도록 하겠습니다. 

 

로그인이 되었는지 안되었는지를 확인하기 위해서 SharedPreferences 패키지를 사용해서 로그인 여부를 전역에서 체크할 수 있도록 만들어보겠습니다.

 

플러터에서 패키지를 설치하는 방법은 pubsepc.yaml에 패키지를 추가하고 flutter pub get을 하면 됩니다.

 

현재 최신 버전이 2.0.15네요. 최신 버전을 확인하는 방법은 https://pub.dev/ 여기 접속하셔서 패키지 이름을 입력하면 됩니다.

 

// screens/screen_splash.dart

import 'dart:async';

import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';

class SplashScreen extends StatefulWidget {

  @override
  _SplashScreenState createState() {
    return _SplashScreenState();
  }
}

class _SplashScreenState extends State<SplashScreen>{

  Future<bool> checkLogin() async {
    SharedPreferences prefs = await SharedPreferences.getInstance();
    bool isLogin = prefs.getBool('isLogin') ?? false;
    return isLogin;
  }
  
  void moveScreen() async {
    await checkLogin().then((isLogin){
      if(isLogin){
        Navigator.of(context).pushReplacementNamed('/index');
      } else {
        Navigator.of(context).pushReplacementNamed('/login');
      }
    });
  }


  @override
  void initState() {
    super.initState();
    Timer(Duration(milliseconds: 2000), (){
      moveScreen();
    });
  }

  @override
  Widget build(BuildContext context) {
    return const Scaffold(
      appBar: null,
      body: Center(
        child: Text("Splash Screen"),
      )
    );
  }
}

 

SharedPreference는 싱글톤으로 key:value의 데이터를 저장할 수 있는 패키지입니다. 로그인을 했을 경우 isLogin을 true로 해놓고, 아닐 경우 false로 저장할 예정입니다. 따라서 isLogin값을 가져와서 true라면 이미 로그인이 되어있는 상황이기 때문에 /index 화면으로 넘어가면 되고, false라면 로그인을 해야 하기 때문에 /login으로 보내면 됩니다.  

 

SharedPreference에서 값을 꺼내오는 건 CheckLogin()에서 실행되는데, 여기 잘 보시면 Future - await를 사용했습니다. 값을 바로 가져오지는 못하기 때문에 async(비동기)로 가져와야 합니다. 

 

ScreenSplash는 실행되면 initState가 실행되고 거기서 2초의 딜레이를 갖고 moveScreen()을 실행하고, 여기서 isLogin의 여부로 /index로 갈지 /login으로 갈지 결정됩니다. /index화면은 이미 만들었기 때문에 /login화면도 한번 만들어볼까요?

 

// screens/screen_login.dart

import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';

class LoginScreen extends StatelessWidget {

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(),
      body: const Center(
        child: Text('Login'),
      ),
    );
  }
}

 

화면을 전체를 다 그리지는 않고 아까처럼 텍스트만 표기해서 화면이 바꼈다 정도만 인식하도록 만들었습니다. 이제 splash와 login화면은 route에 등록해주고, 시작 화면은 splash로 바꿔줍니다.

 

// main.dart

import 'package:flutter/material.dart';
import 'screens/screen_splash.dart';
import 'screens/screen_index.dart';
import 'screens/screen_login.dart';

void main (){
  runApp(MyApp());
}

class MyApp extends StatelessWidget {

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Shopping mall',
      routes: {
        '/index': (context) => IndexScreen(),
        '/login': (context) => LoginScreen(),
        '/splash': (context) => SplashScreen(),
      },
      initialRoute: '/splash',
    );
  }
}

 

실행해볼까요?

 

splash화면이 잠깐 보이고 login으로 넘어간다.

우리가 의도한 대로 splash화면이 2초 정도 보이고 login화면으로 넘어갑니다. 만약 login 되었다면 여기서 바로 index로 넘어가겠죠? 그럼 이제 본격적으로 로그인/회원가입을 만들어보도록 하겠습니다.

 

 

로그인 만들기 - 로그인/회원가입 화면 만들기

 

앞에서 얘기한 것처럼 저희는 파이어 베이스의 기능을 이용하여 로그인을 만들 예정입니다. 관련 패키지들을 설치해줘야 하는데요, 아래와 같이 패키지를 추가합니다.

 

  shared_preferences: ^2.0.15
  firebase_core: ^1.19.2
  cloud_firestore: ^3.3.0
  firebase_auth: ^3.4.2
  provider: ^6.0.3

 

패키지를 추가하고 pub get을 하는 것을 잊지 맙시당 ㅎㅎ

 

먼저 로그인 화면을 그려보겠습니다. 로그인에는 이메일 주소와 비밀번호를 넣는 간단한 구성으로 만들겠습니다.

 

// screens/screen_login.dart
import 'package:flutter/material.dart';

class LoginScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(),
      body: Column(
        children: [
          EmailInput(),
          PasswordInput(),
          LoginButton(),
          Padding(
            padding: EdgeInsets.all(10),
            child: Divider(
              thickness: 1,
            ),
          ),
          RegisterButton(),
        ],
      ),
    );
  }
}

class EmailInput extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Container(
      padding: EdgeInsets.all(10),
      child: TextField(
        onChanged: (email) {},
        keyboardType: TextInputType.emailAddress,
        decoration: InputDecoration(
          labelText: 'email',
          helperText: '',
        ),
      ),
    );
  }
}

class PasswordInput extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Container(
      padding: EdgeInsets.all(10),
      child: TextField(
        onChanged: (password) {},
        obscureText: true,
        decoration: InputDecoration(
          labelText: 'password',
          helperText: '',
        ),
      ),
    );
  }
}

class LoginButton extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Container(
      width: MediaQuery.of(context).size.width * 0.7,
      height: MediaQuery.of(context).size.height * 0.05,
      child: ElevatedButton(
        style: ElevatedButton.styleFrom(
          shape: RoundedRectangleBorder(
            borderRadius: BorderRadius.circular(30.0),
          ),
        ),
        onPressed: () {},
        child: Text('Login'),
      ),
    );
  }
}

class RegisterButton extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return TextButton(
        onPressed: () {
          Navigator.of(context).pushNamed('/register');
        },
        child: Text(
          'Regist by email',
        ));
  }
}

 

 

 

플러터의 장점은 모든 것이 위젯이기 때문에, 입력하는 위젯들을 각각 class로 구현해서 만들어줍니다. 이메일을 입력하는 위젯, 비밀번호를 입력하는 위젯, 로그인 버튼, 회원가입 버튼 각각 만들어주었습니다. 아직 로그인 버튼을 눌렀을 때 아무것도 하지 않게 만들었지만, 회원가입을 누르면 /register로 라우팅 됩니다. 먼저 register 화면을 간단하게 만들어줍니다.

 

// screens/screen_register.dart

import 'package:flutter/material.dart';

class RegisterScreen extends StatelessWidget {

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(),
      body: const Center(
        child: Text('Register'),
      ),
    );
  }
}

 

main.dart에 있는 route에 /register 화면을 추가해주시면 됩니다. 이제 앱을 실행하면 splash -> login -> register 화면으로 이동이 가능해졌습니다. register화면에도 회원가입을 위한 UI를 만들어봅시다. 로그인 화면에서 살짝만 수정하면 됩니다.

 

// screens/screen_register.dart

import 'package:flutter/material.dart';

class RegisterScreen extends StatelessWidget {

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(),
      body: Column(
        children: [
          EmailInput(),
          PasswordInput(),
          PasswordConfirmInput(),
          RegistButton()
        ],
      ),
    );
  }
}

class EmailInput extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Container(
      padding: EdgeInsets.all(5),
      child: TextField(
        onChanged: (email) {},
        keyboardType: TextInputType.emailAddress,
        decoration: InputDecoration(
          labelText: 'email',
          helperText: '',
        ),
      ),
    );
  }
}

class PasswordInput extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Container(
      padding: EdgeInsets.all(5),
      child: TextField(
        onChanged: (password) {},
        obscureText: true,
        decoration: InputDecoration(
          labelText: 'password',
          helperText: '',
        ),
      ),
    );
  }
}

class PasswordConfirmInput extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Container(
      padding: EdgeInsets.all(5),
      child: TextField(
        onChanged: (password) {},
        obscureText: true,
        decoration: InputDecoration(
          labelText: 'password confirm',
          helperText: '',
        ),
      ),
    );
  }
}

class RegistButton extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Container(
      width: MediaQuery.of(context).size.width * 0.7,
      height: MediaQuery.of(context).size.height * 0.05,
      child: ElevatedButton(
        style: ElevatedButton.styleFrom(
          shape: RoundedRectangleBorder(
            borderRadius: BorderRadius.circular(30.0),
          ),
        ),
        onPressed: () {
          ScaffoldMessenger.of(context)..hideCurrentSnackBar()..showSnackBar(SnackBar(content: Text('Regist Success!!')));
          Navigator.pop(context);
        },
        child: Text('Regist'),
      ),
    );
  }
}

 

Login화면과 다른 점은 PasswordConfirm이 추가되었고 버튼이 Regist버튼으로 변경된 점입니다. 또한 버튼을 클릭했을 때 아직 로직적으로 동작은 하지 않지만, 회원가입이 성공했다는 토스트 메시지를 띄워주며 이전 화면인 Login화면으로 이동하게 됩니다. 여기서 .. 문법이 사용됬는데, ..은 함수의 반환 객체를 뜻하는 다트 문법입니다. 만약 ..을 쓰지 않으면 아래와 같이 써야 합니다.

 

var scaffoldMessenger = ScaffoldMessenger.of(context)
scaffoldMessenger.hideCurrentSnackBar()
scaffoldMessenger.showSnackBar(SnackBar(content: Text('Regist Success!!')));

 

Navigator.pop(context)를 하게 되면 바로 전에 화면으로 돌아가기 때문에 login화면으로 돌아가게 됩니다. 무조건 바로 전 화면으로 돌아가는 것은 아니고 화면이 스택 구조로 쌓이게 되면 바로 전에 쌓인 화면으로 가게 됩니다. 우리가 login화면에서 regist화면으로 넘어올 때 push로 넘어왔기 때문에 pop을 하게 되면 이전 화면인 login으로 넘어가게 됩니다. 

 

회원가입화면

 

 

 

마무리

이번 포스팅에서는 플러터 프로젝트를 만들고 로그인과 회원가입 UI를 만들어봤습니다. 로직을 추가하기 위해 파이어 베이스를 붙여야 하는데, 한꺼번에 하게 되면 글이 너무 길어져서 한번 자르고 가도록 하겠습니다 ㅎㅎ. 다음 시간에 이번에 만든 화면을 가지고 파이어 베이스에 회원가입/로그인을 붙여서 로그인이 되도록 만들도록 하겠습니다. 

 

이번 시간에 사용한 것 중 따로 한번 더 공부하면 좋은 것

- route를 사용한 화면 이동a

- SharedPreference 패키지

 

 

 

 

 

반응형

댓글

Designed by JB FACTORY