본문 바로가기
코딩생초보를 위한 플러터 빠르게 한바퀴

4.Json

by 타이싸란 2023. 8. 9.

4회차 세부 과정 목차

 

지난번 강의 숙제풀이

목차로돌아가기

 

더보기

저번 시간의 숙제 내용입니다.

 

<구조>

프로젝트의 구조입니다.

main.dart (1개)

component :  ProductGridViewWidget.dart , ProductListViewWidget.dart (2개)

pages :  MainPage.dart , ProductDetailPage.dart (2개)

vo :  Product.dart (1개)

 

이렇게 6개의 파일로 이루어진 구조입니다.

라이브러리는 GetX 하나만 사용했습니다.

 

<MainPage>

MainPage.dart 파일의 내용입니다.

mode 는 그리드뷰를 보여줄건지 리스트뷰를 보여줄건지에 대한 변수입니다.

0이면 그리드뷰를 보여주고 , 1이면 리스트 뷰를 보여주게 하려고 이렇게 만들었습니다.

(2개의 모드만 있다면 bool 타입의 변수를 사용해도됩니다.)

Product 타입으로 구성되어있는 productList 리스트 변수를 만들었습니다.

그리고 몇개의 Product를 만들어 주었습니다.

 

build 부분을 보면 Row위젯으로 리스트뷰,그리드뷰를 선택할수있게 버튼을 만들었습니다.

그리고 if(mode==0) 이런 방식을 사용하여 그리드뷰 또는 리스트뷰를 보여주게 만들었습니다.

GridView.builder 에서 ProductGridViewWidget을 retrun 하게 했으며 이 위젯의 파라메트로 index에 맞는 Product 를 넣어줬습니다. 리스트뷰는 ProductListViewWidget을 return 하게 했습니다.

 

아래는 복붙해서 사용할수있는 코드입니다. 적절히 import 부분만 수정해서 사용하심되고, github 를 사용할줄 안다면 클론받으세요.

import 'package:flutter/material.dart';

import '../component/ProductGridViewWIdget.dart';
import '../component/ProductListViewWidget.dart';
import '../vo/Product.dart';

class MainPage extends StatefulWidget {
  const MainPage({Key? key}) : super(key: key);
  @override
  State<MainPage> createState() => _MainPageState();
}
class _MainPageState extends State<MainPage> {
  int mode = 0;
  List<Product> productList = [
    Product(name: "포도", price: 3000, imagePath: "https://www.100ssd.co.kr/news/photo/202208/89896_70031_1951.jpg"),
    Product(name: "사과", price: 5000, imagePath: "https://i.namu.wiki/i/QHZlaOvDdhvtLDYrA6IRvUZdddgwY9q5d0rMBywEIh7dbcNTCzTmE2CDM05JA9GRuXWqp5LsxE_T8BvGNOJhVA.webp"),
    Product(name: "두부", price: 6000, imagePath: "https://img-cf.kurly.com/shop/data/goodsview/20220314/gv30000288794_1.jpg"),
    Product(name: "어묵", price: 7000, imagePath: "https://upload.wikimedia.org/wikipedia/commons/thumb/c/c3/Street_eomuk.jpg/1200px-Street_eomuk.jpg"),
    Product(name: "고무장갑", price: 8000, imagePath: "https://i.namu.wiki/i/Jy215uAViB3zyPM0uVkNkfFhP23gKnqTLtGN-F1iwWHottqISCSQrGkBJo9LIFcrMboEBDFoNNZgF6pGwX3EPA.webp"),
    Product(name: "비닐팩", price: 9000, imagePath: "https://m.tntpack.co.kr/web/product/big/201604/887_shop1_382294.jpg"),
    Product(name: "쌍스바", price: 500, imagePath: "https://img1.daumcdn.net/thumb/R1280x0.fpng/?fname=http://t1.daumcdn.net/brunch/service/user/4Slv/image/ebG80keywECvkxmNPYoVrbxVaAw.png"),
    Product(name: "메두나", price: 500, imagePath: "https://www.bebeyam.com/wp-content/uploads/2020/07/binggrae-melona-8-768x1024.jpg"),
  ];
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("싱싱마을"),
        backgroundColor: Colors.green,
        centerTitle: true,
      ),
      body: Container(
        padding: EdgeInsets.symmetric(horizontal: 10, vertical: 10),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Row(
              children: [
                GestureDetector(
                  onTap: () {
                    setState(() {
                      mode = 0;
                    });
                  },
                  child: Container(
                    padding: EdgeInsets.symmetric(horizontal: 10, vertical: 5),
                    decoration: BoxDecoration(
                        color: mode == 0 ? Colors.blue : Colors.grey,
                        borderRadius: BorderRadius.circular(10)),
                    child: Text("그리드 뷰"),
                  ),
                ),
                SizedBox(
                  width: 20,
                ),
                GestureDetector(
                  onTap: () {
                    setState(() {
                      mode = 1;
                    });
                  },
                  child: Container(
                    padding: EdgeInsets.symmetric(horizontal: 10, vertical: 5),
                    decoration: BoxDecoration(
                        color: mode == 1 ? Colors.blue : Colors.grey,
                        borderRadius: BorderRadius.circular(10)),
                    child: Text("리스트 뷰"),
                  ),
                ),
              ],
            ),
            SizedBox(
              height: 10,
            ),
            if(mode==0)
            Expanded(
                child: GridView.builder(
                    gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
                      crossAxisCount: 2, // 2개의 열로 구성된 그리드
                      mainAxisSpacing: 20,
                      crossAxisSpacing: 10, // 가로 세로 비율을 1:2로 설정
                    ),
                    itemCount: productList.length,
                    itemBuilder: (context, index) {
                      return ProductGridViewWidget(item: productList[index]);
                    })),
            if(mode==1)
              Expanded(
                  child: ListView.builder(

                      itemCount: productList.length,
                      itemBuilder: (context, index) {
                        return ProductListViewWidget(item: productList[index]);
                      }))
          ],
        ),
      ),
    );
  }
}

 

 

<ProductGridViewWIdget>

ProductGridViewWidget.dart 파일의 내용입니다.

Product 타입의 변수를 받았고 , 변수명을 item 으로 해줬습니다.

그리고 build 부분에서 사용할때 widget.item.imagePath 형태로 사용했습니다.

그리고 Widget을 누르면 ProductDetailPage 로 넘어가는데 파라메터로 item 을 통채로 넘겼습니다.

넘어온걸 그대로 또 다른 페이지에서 사용할수있도록 넘기는 겁니다.

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

import '../pages/ProductDetailPage.dart';
import '../vo/Product.dart';

class ProductGridViewWidget extends StatefulWidget {
  final Product item;
  const ProductGridViewWidget({Key? key , required this.item}) : super(key: key);

  @override
  State<ProductGridViewWidget> createState() => _ProductGridViewWidgetState();
}

class _ProductGridViewWidgetState extends State<ProductGridViewWidget> {
  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: (){
        Get.to(()=>ProductDetailPage(item:widget.item));
      },
      child: Container(
        color: Colors.orange,
        child: Stack(
          alignment: Alignment.bottomCenter,
          children: [

            AspectRatio(
              aspectRatio: 1,
                child: Image.network(widget.item.imagePath??"",fit: BoxFit.cover,)
            ),
            Positioned(
              bottom: 10,
              right: 0,
              left: 0,
              child: Padding(
                padding: EdgeInsets.symmetric(horizontal: 10),
                child: Row(
                  mainAxisAlignment: MainAxisAlignment.spaceBetween,
                  children: [
                    Container(
                      padding: EdgeInsets.symmetric(horizontal: 10,vertical: 3),
                      decoration: BoxDecoration(
                        color: Colors.blue,
                        borderRadius: BorderRadius.all(Radius.circular(10)),
                      ),
                      child: Center(child: Text("${widget.item.price} 원")),
                    ),
                    Container(
                      padding: EdgeInsets.symmetric(horizontal: 10,vertical: 3),
                      decoration: BoxDecoration(
                        color: Colors.red,
                        borderRadius: BorderRadius.all(Radius.circular(10)),
                      ),
                      child: Center(child: Text("${widget.item.name}")),
                    ),
                  ],
                ),
              ),
            ),

          ],
        ),
      ),
    );
  }
}

 

<ProductLIstVIewWidget>

내용이 별다른게 없어서 코드블럭만 추가합니다.

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

import '../pages/ProductDetailPage.dart';
import '../vo/Product.dart';

class ProductListViewWidget extends StatefulWidget {
  final Product item;

  const ProductListViewWidget({Key? key, required this.item}) : super(key: key);

  @override
  State<ProductListViewWidget> createState() => _ProductListViewWidgetState();
}

class _ProductListViewWidgetState extends State<ProductListViewWidget> {
  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: (){
        Get.to(()=>ProductDetailPage(item:widget.item));
      },
      child: Container(
        color: Colors.orange,
        margin: EdgeInsets.symmetric(vertical: 5),
        padding: EdgeInsets.symmetric(vertical: 5, horizontal: 10),
        child: Row(
          children: [
            Container(
                decoration: BoxDecoration(
                    borderRadius: BorderRadius.all(Radius.circular(10))),
                height: 100,
                width: 100,
                child: ClipRRect(
                    borderRadius: BorderRadius.all(Radius.circular(10)),
                    child: Image.network(
                      widget.item.imagePath ?? "",
                      fit: BoxFit.cover,
                    ))),
            SizedBox(width: 10,),
            Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                Text(widget.item.name??""),
                Text("${widget.item.price} 원")
              ],
            )
          ],
        ),
      ),
    );
  }
}

 

 <ProductDetailPage>

import 'package:flutter/material.dart';

import '../vo/Product.dart';

class ProductDetailPage extends StatefulWidget {
  final Product item;

  const ProductDetailPage({Key? key, required this.item}) : super(key: key);

  @override
  State<ProductDetailPage> createState() => _ProductDetailPAgeState();
}

class _ProductDetailPAgeState extends State<ProductDetailPage> {

  bool isLike = false;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.item.name ?? ""),
      ),
      body: Column(
        children: [
          SizedBox(
            height: 30,
          ),
          Stack(
            children: [
              AspectRatio(
                aspectRatio: 2 / 1,
                child: Image.network(
                  widget.item.imagePath ?? "",
                  fit: BoxFit.cover,
                ),
              ),
              Positioned(left: 10, bottom: 10, child: GestureDetector(
                  onTap: (){
                    setState(() {
                      isLike = !isLike;
                    });
                  },
                  child: Icon(isLike?Icons.favorite:Icons.favorite_border,color: Colors.pink,)))
            ],
          ),
          SizedBox(
            height: 10,
          ),
          Text(widget.item.name ?? ""),
          SizedBox(
            height: 10,
          ),
          Text("${widget.item.price} 원")
        ],
      ),
    );
  }
}

 

 


 

이번숙제에서는 비합리적인 부분을 2군대 찾을수있습니다.

1. Product 인스턴스를 만들때 수기로 몇개 만들어준것. 만약 상품이 300개면 300개를 손으로 쳐야하나?

 

2.MainPage -> ProductGridViewWidget or ProductListViewWIdget -> ProductDetailPage

이렇게 2 단계를 거쳐서 Product를 넘겨줬는데 , 계속이렇게 넘겨주는것이 맞나? 만약 10단계 넘게 넘겨주거나 , 연관관계가 없는쪽으로 넘겨줘야 할때는 어떻게 해야하나?

 

입니다.

1번은 빠르게 확인하기위해서 하드코딩으로 한건데, 실제로는 백엔드에서 데이터를 Json형태로 받은뒤 파싱을통해 객체를 만들어 줘야합니다.

백엔드에서 100개를 넘겨주면 로직을 통해 객체가 생성되고 생성된 객체가 하나하나 담겨서 list 를 만들어 줘야하죠.

이번시간엔 백엔드 통신은 안하더라도 Json 을 이용하여 해볼겁니다. 사실이것도 하드코딩이긴 한데 추후에 통신방식으로 하기위한 빌드업입니다.

 

2번은 글로벌변수 또는 상태관리라는걸 사용해야합니다.

각 페이지와 관계없는 데이터를 관리하는 파일을 만들어 놓고, 각페이지에서 끌어다 쓰는 방식이죠. 그런데 글로벌변수를 사용했을때는 변수가 변화되었을때의 작업들을 일일이 해줘야하는데 상태관리 라이브러리를 사용하면 편하게 할수있으니 그냥 상태관리는 라이브러리를 이용해서 해보겠습니다.

1.Json

목차로돌아가기

 

(오늘하는 부분은 직관적으로 잘 안와닫을수있습니다. 그런데 이부분은 계속해보면서 그냥 체득이 되고, 그냥 당연한듯이 받아들일때까지 계속해봐야 하는 부분입니다. 잘 이해가 안되도 일단 따라해보세요. 그리고 앞으로 데이터 받는 부분이 많을것이고 자연스럽게 체득이 되실테니 너무걱정하지 마세요.

그리고 다른 플랫폼으로 api 통신을 해보신 분들이라면 그냥 편하게 빠르게 보심됩니다.)

 

Json 이란 백엔드 서버와 데이터를 주고 받을때 사용하는 포멧입니다.

Json은 문자열로 되어있는데, 일반 막부가네 문자열이 아니고 특정한 형태를 가지고 있는 문자열입니다.

특정 형태의 문자열이기 때문에 백엔드로 부터 Json 문자열이 오면 그걸 객체로 변환 할 수 있는것이죠.

Json에 관한 상세한 내용은 여기를 확인해주세요 => 여기

 

대충 이런식으로 생겼습니다.

"키" : "값" 이런 형태로 되어있어서 저런 문자열을 받으면

우선 json.decode 를 통해서 map 형태로 바꿔줍니다.

 

( Map이란 {키 : 값, 키 : 값 .....} 형태로 이루어진 덩어리 입니다. 일반적인 변수가 예를들어 String name = "철수" 이렇게 한가지의 내용만 가진다면

Map<String,String> testMap = {"name":"철수","age":"25세"} 이렇게 n개의 내용을 가질수 있습니다.

String , String 이렇게한건 키의데이터 타입이 String, 값의 데이터 타입도 String 이라는걸 의미합니다.)

 

Map<String,dynamic> productFromJson = json.decode(Json문자열)

 

이렇게 바꿔주게 되면 그러면 productFromJson 이란 이름의 변수는 까보면

{
"name":"포도",
"price":3000,
"imagePath":"https://www.100ssd.co.kr/news/photo/202208/89896_70031_1951.jpg"
}

 

이렇게 생겼을겁니다. Json 문자열과 똑같이생겼죠.

 

그다음 map 을 class 객체로 만드는 거죠.

맵에서 꺼내쓸때는 맵변수명["키이름"] 이렇게 하면 키에 맞는 값을 불러올수있습니다.

 

객체를 만들때 대게 길었었는데 조금 줄어든 것이 보일겁니다. 300 개를 만들어야 되는 상황이면

Json 문자열 데이터가 List 형태 ( [ {}, {}, {} ....] ) 이렇게 올테니 그걸 파싱하면 300개의 map타입의 덩어리가 생길테니 for문으로 300번 객체로 바꿔주면 끝나는 겁니다.

(근데 이렇게 객체를 만드는 것도 좀 찝찝합니다. 그래서 좀있다 실습에서 Vo 클래스 에서 함수를 추가해 만들어줄겁니다.)

 

그래서 정리하자면

백엔드에 Json 데이터 요청 -> 백엔드에서 Json 데이터줌 -> Json을 Map 으로 바꿈 -> Map 을 객체로 바꿈

이렇게 됩니다.

 

그냥 글로만 보면 잘모르겠으니 실습을 하면서 해보겠습니다.

 

해볼것은 

1. 한개 데이터만 받기

2. 모델로 받기

 

입니다. 결국은 모든것을 모델로 받아서 하게 되겠지만 차근차근 해본다는 의미로 1번도 하는겁니다.

 

 

 

2. Json 1개 데이터 받기

목차로돌아가기

 

먼저 한개 데이터만 받기위해 유저데이터를 받아오는걸 예시로 사용해보겠습니다.

assets 폴더를 만든뒤 json 폴더를 만들고 그안에 user_json.json 이라는 파일을 만들어주세요.

그리고 assets 사용하니까 당연하게 pupspec 에 assets 사용등록을 하셔야겠죠?

 

그다음 vo 에 User Vo Class를 하나 만들어주세요.

기본적으로 하던 멤버변수와 기본 생성자를 하나 생성해주세요.

그다음 아래에 노란 네모칸 부분을 적어주세요.

이건 User class 에 있는 메소드 같은겁니다.

기본상속자 + .(닷) + 원하는 메소드명

이렇게 해주세요. 빨간 밑줄은 사실 아무렇게나 네이밍을 해도되지만, json 데이터를 기반으로 인스턴스를 만드는거라 보통 불문율처럼 fromJson 이라고 합니다. 그러니까 그냥 fromJson 이라고 해주세요.

이렇게 만들어진 User.fromJson() 메소드는 매개변수로 Map 형태의 변수를 받습니다.

mapData 라고 되있는 부분도 아무이름이나 넣어도되지만 불문율로 json 이라고 이름 붙입니다. 근데 json 이라고 하면 json 문자열이 오는건가? 라고 코딩입문자분들은 해깔릴수도있으니 그냥 제가 mapData라고 해뒀는데, 다음번부터는 json 이라고 써놓을겁니다.

 

MainPage.dart 에서 Scaffold 의 AppBar 에서 actions 옵션 부분에 노란색으로 표시해둔것처럼 마이페이지로 가는 버튼을 하나 만들었습니다.

참고로 앱바 왼쪽은 leading , 오른쪽은 action 입니다. 옵션설명을 보면 leading은 위젯을 , action은 배열위젯을 받습니다.

 

MyPage를 만들어 줍니다.

User? myInfo 를 선언해줍니다.

그뒤 initState 에 getUserInfo() 메소드를 호출합니다.

initState 에 적어둔것은 이 위젯이 호출될때 제일 처음 발동합니다. 라이프사이클 배울때했습니다.

 

getUserInfo() 메소드 부분을 보니 async 와 await 라는 처음보는게 있습니다.

코딩에서 아주 중요한 부분을 차지하는 비동기메소드를 다루는 방법인데, 아주중요하지만 나중에 비동기 부분에서 설명하는걸로 하고 일단 넘어가주세요. 간단히 말하자면 비동기는 코드가 읽는것과 동시에 결과를 바로주지 않는겁니다.

 

내용을 보면 먼저 jsonString 을 불러옵니다.

rootBundle.loadString(json 파일 경로) 으로 Json 파일을 읽어서 문자열로 반환합니다.

반한된걸 jsonString 이라는 변수에 저장했습니다.

 

그다음 json.decode 를 통해서 mapData 를 만들어줍니다.

그리고 아까 vo class 에서 만들어준 함수를 이용합니다. User.json(mapData) 요렇게 넣어주면

하나의 user 가 생성됩니다.

그리고 setState 를 통해  myInfo 라고 선언해둔곳에 박아넣습니다.

 

그뒤 myinfo.멤버변수 이런식으로 데이터를 꺼내서 사용하면됩니다.

 

참고로 json.decode 를 사용하려면 import 'dart:convert'; 부분을 해줘야하고

rootBundle.loadString 이부분쓸려면 저기 services 라는 부분 import 해줘야합니다.

 

이렇게 1개의 User 데이터를 json 으로 받아서 map 으로 변환시킨다음 객체를 만들어줬습니다.

근데 사실 json 데이터는 이렇게 하나하나 받는 경우는 없고 모델이라는 한덩어리로 받으니, 다음은 모델로 받는걸 해보겠습니다.

 

3.Json 모델로 받기

목차로돌아가기

 

product_json.json 파일을 만들어주세요.

이번 json 파일에는 total_count 라는게 있고 , data 라는게 있네요.

그리고 data 안에 내용물이 3개 들어가있습니다.

 

Product vo class 에 fromJson 메소드를 생성해주세요.

 

ProductModel 을 만들어주세요.

그리고 fromJson 메소드를 만들어주세요.

totalCount 는 하던데로 그대로 하면되지만

data 부분은 내용물이 Product로 구성된 리스트입니다.

data = <Product>[] 이렇게 처음에 data 에 빈배열을 넣어둔뒤에

json['data'] 를 뽑아와서 하나하나 forEach로 반복문들 돌며 Product.fromJson 에 넣어서 Product인스턴스를 만들어줍니다. 만들어진 인스턴스 각각을 data.add 로 data 배열을 완성해줍니다.

 

MainPage 로 와서 기존에 productList 부분은 주석처리를 해주던지, 삭제해주던지 해주세요.

 

productModel 부분을 선언한뒤에

initState 에서 getProductList 메소드를 실행시킵니다.

 

아래 그리드뷰와 리스트 뷰 부분을 수정해줍니다.

productModel.totalCout 로 카운트 도 불러와보세요.

proudctModel이 null일때와 productModel.data 가 null일때의 대비를 해준뒤에

itemCount 부분에 productModel.data.length 로 적어주심 됩니다.

 

이렇게 모델형태로 불러와봤습니다.

 

대부분은 이렇게 모델로 불러와서 처리를합니다.

1개의 유저 데이터를 백엔드에 요청하여 json 으로 받아도 이런식으로 되어있습니다.

(물론 백엔드에서 어떤식으로 줄것인가는 회사의 팀내부에서 정하기 나름입니다.)

 

이번실습에서는 모델객체안에 또 다른객체가 있어서 2번 생성자.fromJson 을 거쳤습니다.

ProductModel 이 있고 그안에 Product 객체가 있었죠.

근데 Product 안에 또 객체가 있을수있겠죠? 

예시에서는 아래처럼 되어있었는데

class Product{

String? name;

int? price;

String? imagePath;

}

 

Product 안에 SellerInfo 라는게 멤버로 있을수있고, Option 리스트 도 있을수있습니다.

 

class Product{

String? name;

int? price;

String? imagePate;

SellerInfo sellerInfo;

List<Option> optionList

}

 

SellerInfo 를 까보면 또 안에 또 다른 클래스나 리스트도 가질수도 있을겁니다.

그렇게 3단 4단 5단 6단 쭉쭉 나갈수있습니다.

실무에서는 이렇게 2단으로 끝나는게 아니라 몇단으로 이루어질수있다는점을 알고, 추후에 좀더 복잡한걸 해보도록 합시다.

 

이렇게 Json 으로 객체 만들기를 한번 해봤으니 , 이제 나중에 배울 실제로 백엔드 서버에서 데이터 받아서

처리하는것도 할 수있을겁니다.

 

4.as 별칭

목차로돌아가기

 

ProductModel 에서 Product 를 사용할수있는것은 Product.dart 파일을 import 를 통해 Product class 가 정의되어있는 부분을 불러왔기 때문입니다.

이렇게 import 를 하면 해당 다트파일에있는 요소들을 불러와 사용할수있습니다.

 

근데 문제점은 Product 라는게 일반적인 이름이다보니 다른 파일에서도 같은 이름의 클래스를 들고있다면 충돌이 일어날수있습니다.

 

가령 ProductModel 에서 Sample 이라는 클래스를 Sample.dart 파일에서 불러와 사용한다고 해봅시다.

이경우 Sample 이라는건 불러오는데 문제가 없지만, Product 라는게 Sample.dart 파일에도 있기때문에

 

ProductModel 에서 사용되는 Product 가

Product.dart 파일에 정의되어있는 Product 인지

Sample.dart 파일에 정의되어있는 Product 인지 모르기 때문에 에러가납니다.

그렇기 때문에 어디서 불어온건지 명확하게 할필요가 있는데 이때 사용하는것이 별칭입니다.

 

이렇게 as 를 사용해서 Product.dart 파일에서 불러온 모든내용을 ProductOfSaran 이라고 별칭을 붙였습니다.

그리고 사용할때는 별칭.Product 이렇게 사용하는것이죠.

그러면 ProductModel 에서 사용하는 Product는 Product.dart 파일에서 불러온거란것을 확실히 알수있기때문에 에러가 사라지게 됩니다.

 

여러명이서 팀단위로 작업을 하거나 라이브러리를 받는경우 User 나 Product 같이 일반적인 클래스는 중복이 일어날 가능성이 높겠죠

이때 특별한 네이밍으로 클래스를 만든다거나 별칭을 사용하면 됩니다.

나중에 http 라이브러리를 사용할때도 as 로 별칭을 만들어서 사용할것입니다.

 

(as 는 별칭뿐만아니라 부모에서 상속받은 클래스를 다운그래이드 할때도 사용하는데, 그부분은 나중에 다트언어에 대해 근본적으로 알고싶을때 직접찾아보는것이 더 효율적일것으로 보니까, 지금은 그냥 별칭쓸때 as 쓰는구나 라고 넘어가시면 됩니다.)

5.NullSafety

목차로돌아가기

 

지금껏 우리가 코딩하면서 ?와 ! 같은걸 많이 봤을겁니다.

가령 이런거죠.

String? name;

 

이부분은 name 이라는 변수가 null 이 가능한 변수라는 뜻입니다.

다트 2.0 이상 부터는 변수는 null 이 가능한지 아닌지 알려줘야합니다.

만약 null 이 불가능한 변수라면 초기화를 해줘야합니다.

String name=""; 이렇게 빈값이라도 초기값으로 줘야하죠.

 

근데 Null 을 허용하지 않는 변수이지만 초기값을 나중에 주고싶을때는 late를 씁니다.

 

late String name;

이런식입니다. 이건 name 은 null이 들어갈순 없지만 name 이라는 변수를 어딘가에서 사용하기전에 반드시 초기화를 해주겠다 라는 의미입니다.

우선 선언해두고 initState에서 초기화 해주면 되는것이죠.

근데 만약 late로 선언해두고 초기화를 안해주면 

공사하기전에 돈준다고 약속하고 설계하고,인원뽑고 이것저것 다했는데 막상 공사하기전에 돈을 안주면 안되는것처럼 반드시 초기화해준다고 했으면 해줘야합니다. 

 

예시를 위해 ProductDetailPage에 작성좀 해보겠습니다.

null 이 가능한 welcomeText 라면 변수를 만들어 줬습니다.

아래에 텍스트 위젯에 사용하려고하는데 에러가 납니다.

그이유는 Text위젯에는 반드시 문자열이 들어가야하는데 welcomeText가 null 일수도있기때문에 에러가 납니다.

이 상태로 빌드해서 앱을 돌리면 반드시 에러가 나기때문에 빌드전에 개발자에게 알려주는거죠. 그래서 NullSafety 입니다.

 

그래서 이것을 해결하기위해서 아래처럼합니다.

 

welcomeText가 null 이라면 "" 이 문자열을 대신 넣겠다 이죠.

그걸 코드로 하면 welcomeText??"" 이렇게 쓰는겁니다.

두개의 경우 모두 welcomeText가 null 이라면 Text("") 이런식으로 되는것과 쌤쌤이 됩니다. 이렇게 null 에대한 대비책을 만들어두면 에러가 사라지게 되고, 런타임중에도 이것 때문에 에러가 발생하지는 않게 됩니다.

 

근데 만약 같은상황에서 Text(welcomeText!) 이렇게 ! 를 붙여주면 어떻게 될까요?

이뜻은 welcomeText가 절대로 null 이 들어갈일이 없을거다. 반드시 값이 있다 라는걸 개발자가 보장함을 의미합니다.

 

근데 이 코드상에는 에러가 날겁니다. 왜냐하면 welcomeText! 이렇게 welcomeText가 null이 아님을 개발자가 보장했는데 , welcomeText가 사용되기전까지 값을 가지는 꺼리가 없기때문이죠.

즉 개발자가 뻥친거죠. 그렇기 때문에 업보에 대한 벌을 받는겁니다.

 

initState에서 welcomeText = "헬로우";

이렇게 준다면 아래 텍스트 위젯에서 welcomeText 를 사용할때는 반드시 값이 있을것이니까 개발자가 ! 를 사용해서 null 이 아님을 보장할 수 있을겁니다.

 

late 예시도 한번 보겠습니다.

 

만약 welcomeText 가 null 을 허용하지 않는 변수라면 어떻게될까요?

아래 Text(welcomeText) 부분은 에러가 안발생합니다. 왜냐면 welcomeText는 null 이 절대 될수없는 변수이기 때문이죠.

 

null을 허용하지 않는 변수는 초기값을 줘야한다고 했는데 welcomeText의 초기값을 안줘서 현재 에러가 나오고있습니다.

이때 그냥 "" 이렇게 초기값을 주면되지만, 어떠한 초기값도 주기 애매한 경우 late 를 사용합니다.

그리고 welcomeText가 사용되기전에 initState에서 welcomeText의 초기값을 주고있기때문에 정상적으로 작동됩니다.

 

이렇게 간단하게 nullSafety 에 관해 알아봤습니다. 사실 더 깊고 많은 부분이있지만, 이정도만 알고있다면 나머지는 부딪쳐가며 익히면 될것같습니다.

 

그래서 정리하자면

1. 변수는 선언을할때 null을 허용하는지 아닌지 명확하게 해야한다.

2. null을 허용하는 변수라면 변수를 사용할때 null일경우의 대비책을 만들어야한다.

3. null 이 아님을 개발자가 보장하려면 ! 를 사용한다. 다만 뻥이라면 에러난다.

4. null을 허용하지 않는 함수라면 초기값을 줘야한다. 

5. 초기값을 처음에 선언할때 주기 애매한경우 late를 쓴다.

6. 대신 변수가 처음사용되기전에 반드시 초기화가 진행되어야된다.

 

이정도입니다.

 

6.비동기

목차로돌아가기

 

비동기 부분은 flutter 뿐만아니라 모든 코딩에서 아주 중요한 부분입니다.

 

동기(synchronous)와 비동기(asynchronous)가 있는데 , 비동기는 동기가 아니라는 뜻입니다. 그럼 동기가 뭐냐?

컴퓨터는 코드를 위에서 아래로 , 왼쪽에서 오른쪽으로 읽습니다.

이렇게 정해진 순서대로 읽고 순차적으로 실행됩니다.

 

위에 예시는 동기 형식으로 작성된 코드입니다.

print("1"); 메소드가 실행되고 완료된후에 print("2") 가 실행되는것이죠.

순차적이기 때문에 사람이 보기에도 아주 직관적입니다.

그리고 String a ="ddd" ;로 정의했기때문에 아래에서 a 변수를 바로 사용할수가 있습니다.

 

컴퓨터의 속도가 아주 빠르기때문에 aa() 라는 메소드가 실행후 완료되기까지 찰나의 순간이 걸릴것입니다.

 

그런데 만약 print("2"); 부분이 시간이 10초 걸리는 함수라면 어떨까요? 

코드가 아래 print("3") 부분은 print("2")라는 함수와 상관이 없음에도 불구하고 print("2")부분이 완료된후 실행되기때문에 10초나 기다려야 할겁니다.

 

그래서 비동기라는 것이 있습니다.

 

비동기는 실행이 되는 순서는 동기와 같지만 , 완료가 되지않아도 다음코드가 실행됩니다.

위의예시에서 7줄을 썼는데, 만약 1줄당 1초시간이 걸린다고 한다면, 저함수가 동기일때는 총 7초가 걸릴것입니다.

근데 위의 7줄이 모두 비동기라면 , 거의 동시에 찰나의 순간에서 실행되서 1초 조금 넘은 언저리에서 모두 완료가 될 것입니다.

이렇게 코드를 병렬적으로 처리하게 되면서 시간을 줄일수가 있습니다.

 

그런데 문제점은 String a = "ddd" 부분이 비동기라고 가정한다면

저함수가 실행되고 결과값이 도출되기 전에, 즉 a변수에 값이 담기기전에

아래에서 a를 사용하고 있기때문에 에러가 발생하는 문제가 있습니다.

그리고 7개를 거의 동시에 실행했지만 어떤것이 먼저 끝날지는 아무도 모르게 됩니다.

이것이 비동기의 특징입니다. 

 

그래서 비동기를 사용하긴 하는데 비동기할거는 하고 ,

뒤에서 값을 사용해야하는 비동기 함수가 있는 경우, 이 비동기 함수가 끝난뒤에 다음것이 실행되게하는 방법이 있습니다.

 

그래서 일단 말로해서 좀 잘 안와닫을수 있기때문에 실습을 통해 알아보겠습니다.

 

flutter 에서는 비동기 함수를 쓸때 Futrue 라는것을 씁니다.

getUserName 이라는 비동기 함수를 하나 만들어 줬습니다.

그리고 printUserName 이라는 함수를 만들어 준다음 , 이 함수를 실행시켜줄수있는 버튼을 하나 만들었습니다.

그리고 실행시켜보면 Instance of 'Futrue<String>' 이라는 것이 나오게 됩니다.

print 에 김사랑 이라는 것이 나올것으로 예상했지만 이상한것이 나와버렸습니다.

 

이것은

Futrue<String> a = getUserName();

이라는 부분이 완료되기전에 아래 print(a); 가 실행됐기 때문입니다.

 

flutter에서는 비동기함수가 완료되기전 , 비동기 함수의 실행과동시에 일단 먼저 Futrue 타입의 어떤 미래의 객체를 대신전달해 줍니다.

그다음 완료되면 a에 값이 들어가는거죠.

그래서 콘솔에 저렇게 나오게 된겁니다.

 

그래서 김사랑이라는 글자가 나오기 위해서는 비동기함수가 완료된뒤에 아래가 실행되도록 할필요가 있습니다.

 

getUserName() 뒤에 . then 을 사용하게되면 getUserName의 비동기함수가 완료된 이후의 함수를 작성할 수 있습니다.

value 라고 적힌부분은 아무렇게나 네이밍해도되며, 의미하는 바는 getUserName 을 통해 들어온 완료된 값이 저 value 라는 변수에 할당되어 들어오는겁니다.

 

또는 async , await 문법을 사용할수있습니다. 둘이 항상 쌍으로 해야합니다.

이렇게 되면 await 가 붙어있는 부분이 끝난뒤에 그아래부분이 실행되게 됩니다.

 

만약 비동기함수가 여러개일때는 어떻게 될까요?

 

비동기 함수 3개가 있습니다.

getUserName() , getUserName2(), getUserName3() 

 

이렇게 있고 printUserName() 이 실행되면 순서대로 getUserName() 이 완료되면 getUserName2() 가 실행되고

최종적으로 print(ddd) 가 실행되게 됩니다.

 

그런데 문제점은 getUserName2() 와 getUserName3() 부분에 3초를 지연하는 함수를 만들어줬는데요.

 

print(ddd); 가 실행되려면 위에 함수가 모두 완료되야되기떄문에 6초이상 기다려야됩니다.

그런데 print(ddd) 부분은 getUserName() 함수랑만 상관있고, 아래는 상관이 없도록 되어있습니다. 상관없는것때문에 지연되면 불합리하죠.

그래서 코드를 아래처럼 바꿉니다.

 

 

이렇게 하면 getUserName2 와 3가 끝나든말든 print(ddd) 가 실행되어 우리가 원하는 결과를 시간손실없이 얻게 되는겁니다.

  

이렇게 비동기 부분을 알아봤습니다.

부족하기도하고 , 해깔리기도 하고 할겁니다.

하지만 개념을꽉잡고 코딩하는것도 중요하지만, 대충 어떤건줄알고 부딪쳐보며 해쳐나가는방법도 좋은방법같습니다.

일단 이정도만 알고 빠르게 한바퀴 돌도록합시다.

 

 

 

 

 

 

이번 차수는 숙제가 없습니다.

다음차수에는 상태관리 라는 flutter 및 컴포넌트형태의 코딩에서 아주중요한 부분을 합니다. 또 그다음엔 네트워크통신 부분을 합니다. 아주 중요하죠.

이번차수까지 했던내용을 한번씩 복습하시고, 다음부분을 시작해주세요.

'코딩생초보를 위한 플러터 빠르게 한바퀴' 카테고리의 다른 글

6.network  (0) 2023.08.22
5.state management  (0) 2023.08.11
3.component  (0) 2023.08.03
2.first my project  (1) 2023.07.29
1. create project  (0) 2023.07.29