Programming-[CrossPlatform]/Flutter

Flutter 기본-10. Dart 학습: final/const, List.generate, Future/Async, FutureBuilder

컴퓨터 탐험가 찰리 2023. 1. 24. 09:22
728x90
반응형

Youtube 코딩셰프님의 강의를 요약 정리한 글이다. dart 언어나 이론 부분은 자바와 유사하여 대부분 제외하였고, flutter 기초 위주로 정리한다.

https://www.youtube.com/@codingchef

 

코딩셰프

향후 대세가 될 플러터를 단계별로 맛있게 학습하실 수 있습니다!

www.youtube.com

 


 

이번 글에서는 기반 지식을 dart로 학습해본다. 마지막에는 이를 적용하여 flutter로 비동기처리하는 방법에 대해 배운다.

 

1. final 과 const의 차이. 용례

 

final과 const는 둘 다 변하지 않는 값의 의미를 내포하므로 정확한 의미가 용례가 살짝 헷갈릴 수 있다.

 

const

const는 컴파일 후에도 할당이 필요없는 값에 사용한다. 선언과 동시에 초기화되어 고정된다. flutter 위젯들 앞에 const 키워드가 붙은 부분들은 Stateless 위젯처럼 정적인 페이지를 보여줄 때 사용한다는 의미가 된다.

 

final

final은 선언때 할당되는 것이 아니라 컴파일 이후에 1번만 할당된다는 의미이다. 아래 코드에서 DateTime.now() 값은 코드를 작성하는 시점이 아니라 맨 처음 해당 부분의 코드가 실제 실행될 때(런타임 때) 할당된다. 따라서 const를 적용하면 에러가 나고, final을 적용하면 실행 시의 시각을 할당하는 것이다.

 

void main() {
	// const time = DateTime.now(); // 에러
    final time = DateTime.now(); // 해당 코드 실행 시각이 time에 1회성 할당 후 변하지 않음
}

 

 

 

 

 

2. List.generate, shuffle, cascade notation

 

문법적으로 배운 부분만 기록한다. 실제 프로그램 코드는 코딩 셰프 github 페이지에 있다.

https://github.com/icodingchef/lottory_app

 

List.generate

많은 프로그래밍 언어들의 대표적인 학습 예제로 로또 프로그램 만들어보기가 있다. 일반 for문, for in 문, forEach Stream 등을 사용하여 로또 1~45까지의 숫자를 임의로 생성하고 리스트화 할 수도 있다. 하지만 여기서는 Dart의 List.generate 및 더 편리한 문법들을 학습한다.

 

var number = (List.generate(45, (i) => ++i)..shuffle()).sublist(0, 6);

 

generate의 첫 번째 인자는 생성할 List의 길이, 두 번째는 요소들을 생성할 함수이다. 

 

 

cascade notation

점 두개 .. 는 오타가 아니라 실제 사용하는 cascade notation 이라는 문법이다. 이 문법을 적용하면 멤버함수와 멤버변수에 간단하게 접근이 가능하다. ..으로 이어가다가 맨 마지막에 세미콜론을 두어 종료하면 된다.

Person p1 = Person();
p1..name = 'Jackson'
  ..setAge(20)
  ..greeting();

여기서는 generate 메서드로 만든 리스트에 shuffle 함수를 바로 적용하는 방식으로 사용되었다.

 

 

shuffle, sublist

shuffle과 sublist는 이름에서 알 수 있듯 각각 list의 특정 부분을 임의 순서대로 섞어주는 기능, List의 일부분만 포함하여 다른 리스트로 반환해주는 기능을 한다.

 

String interpolation

이전 글에서 살펴본 내용이다. $로 String 내 변수값을 편하게 표현할 수 있다.

코드 참조: https://www.educative.io/answers/what-is-string-interpolation-in-dart

void main() {
    
  // Assigning values to the variable
  String shot1 = "String";
  String shot2 = "interpolation";
  String shot3 = "in";
  String shot4 = "Dart programming";
    
  // Concatenate all values using 
  // string interpolation without space
  print('$shot1$shot2$shot3$shot4');
  }

 

 

후위 연산자, 전위 연산자

마지막으로 ++i는 전위 연산자라 부르고, i++은 후위 연산자라 부른다. 전위 연산자는 변수에 ++을 적용한 후 값을 리턴하고, 후위 연산자는 ++을 적용 전 값을 리턴한다.

 

void main() {
	int i = 1;
    int j = i++;
    int k = ++i;
    
    print(j) // return 1
    print(k) // return 2
}

 

정리

List.generate(...)을 통해 1부터(++i) 45까지 리스트를 생성하고 shuffle을 적용하여 순서를 섞는다. 그리고 그 중 0~5번째 인덱스에 해당하는 6개 숫자만 추출한다.

 

 

 

2. Future, async / await

 

비동기 처리 기본

dart에서 비동기 처리는 Future, async/await 키워드로 처리한다. dart는 싱글 쓰레드로 동작하기 때문에 동기적으로 코드를 한 줄씩 순차적으로 처리할 수만 있다. 그래서 비동기적인 처리가 필요하면 Future 객체를 생성하여 미래 시점으로 처리를 미뤄야한다.

 

Future는 미래의 어느 시점에 데이터를 받거나 처리, 또는 에러 반환을 하는 객체이다. 아래 코드의 main에서 startTask, fetchData, endTask를 순차적으로 실행한다. fetchData의 결과를 endTask의 인자로 넘겨서 최종적인 잔액을 표시하는 기능을 한다. fetchData()의 Future 객체는 3초 후에야 balance 변수에 "8,000"을 할당할 수 있다. 마치 다른 웹사이트에 정보를 요청하고 3초 후에서야 정보를 받아올 수 있는 구조를 만든 것이다. 이럴 때 미래에 가져올 데이터를 표현하기 위해서 Future 객체를 사용한다.

void main() {
  startTask();
  String balance = fetchData();
  endTask(balance);
}

void startTask(){
  print("요청 시작");
}

String fetchData() {
  String balance;
  Future.delayed(Duration(seconds: 3), (){
    balance = "8,000";
  });
  return balance;
}

void endTask(String balance) {
  print("잔액은 $balance원 입니다.");
  print("요청 끝");
}

 

위 코드의 실행 결과 balance 값이 올바르게 나타나지 않았다. fetchData 메서드가 3초가 걸리는데, 따로 비동기처리하는 구문이 없다보니 그냥 순차적으로 세 메서드를 실행하여 Future의 실제 데이터값이 반영되지 않은 것이다.

 

비동기 방식 적용

아래와 같이 수정하면 비동기 작업을 구현할 수 있다. 우선 fetchData 메서드의 실행부에 이 실행부는 비동기작업을 담당한다고 표시하는 async 키워드를 붙여준다. 그리고 Future의 데이터를 얻어올 때까지 기다렸다가 처리하라는 의미로 await를 Future 객체 앞 부분에 넣어준다. Future 객체를 반환하므로 fetchData 메서드의 반환타입도 Future<String>이 된다.

 

그리고 async/await를 실행하는 fetchData가 포함되므로 main 메서드의 실행부도 async 구문을 적용한다. 비동기 작업을 하는 fetchData 메서드 앞에 await를 붙여주어 비동기처리 메서드임을 표시해주면 Future 객체의 처리가 완료된 상태를 의미하므로 반환값 타입이 Future<String>이 아닌 String 타입이 된다.

 

만약 balance 값을 제대로 얻어오지 못했을 수 있으므로 보통 프로그래밍을 할 때 balance값을 에러 처리해주는 코드를 추가해준다(뒷 부분에서 다룰 예정).

void main() async {
  startTask();
  String balance = await fetchData();
  endTask(balance);
}

void startTask(){
  print("요청 시작");
}

Future<String> fetchData() async {
  String balance;
  await Future.delayed(Duration(seconds: 3), (){
    balance = "8,000";
  });
  return balance;
}

void endTask(String balance) {
  print("잔액은 $balance원 입니다.");
  print("요청 끝");
}

 

 

 

 

비동기 방식 원리

dart는 동기적(synchronous) 방식으로 처리해야할 부분들부터 처리하고, Future로 작성된 코드는 event loop(Queue)에 일단 등록한다. Future로 event loop에 등록된 코드는 그 다음 등록된 순서대로 순차적으로 실행된다.

 

async 키워드는 await 키워드를 만날 때까지는 동기적인 방식으로 코드를 처리한다. 아래 코드의 main 메서드에서, await 처리가 되어있으므로 일단 methodD는 실행을 대기한다. 같은 원리로 methodB() 내부에서도 'B start' 이후 methodC('B')의 실행결과를 기다린다.

(코드 : https://github.com/icodingchef/flutter_future_ex/blob/master/futurePractice.dart)

 

다만 methodC 내부에서는 'C start from $from'이 실행되고 바로 'C end from $from'이 출력된다. Future 객체는 event loop에 바로 담기되, 동기적으로 처리되는 'C end from $from'가 먼저 실행되고 나서야 event loop에서 꺼내와져서 실행되는 것이다.

void main() async {
  methodA();
  await methodB();
  await methodC('main');
  methodD();
}

methodA(){
  print('A');
}

methodB() async {
  print('B start');
  await methodC('B');
  print('B end');
}

methodC(String from) async {
  print('C start from $from');

  Future((){
    print('C running Future from $from');
  }).then((_){
    print('C end of Future from $from');
  });

  print('C end from $from');
}

methodD(){
  print('D');
}

 

 

 

 

 

비동기 방식 Flutter 적용

 

.then

then은 Future 객체가 처리된 후 실행할 함수를 지정할 수 있다. 아래에서 좀 더 자세히 알아본다.

 

예제 코드 : https://github.com/icodingchef/flutter_future_ex/blob/master/lib/main.dart

 

result 변수를 선언 및 할당하고, Button/onPressed에 futureTest() 메서드를 실행하게 해주었다. futureTest는 async/await로 처리되며 Future<void>로 반환값은 없으나 Future로 처리된다. 비동기적으로 적용된 await 구문 외, 아래에 동기 방식인 'Here comes first', 'Here is the last one'이 먼저 실행된다. 그리고 Future가 3초후에 실행되며 setState()구문을 통해 result 값이 업데이트되고 rebuild가 실행된다.

String result = 'no data found';

// ...

ElevatedButton(
                onPressed: () {
                  futureTest();
                },
                
// ...

Future<void> futureTest() async {
    await Future.delayed(Duration(seconds: 3)).then((value) {
      print('Here comes second');

      setState(() {
        this.result = 'The data is fetched';
        print(result);
        print('Here comes third');
      });
    });
    
    print('Here comes first');
    print('Here is the last one');
  }

 

 

FutureBuilder: snapshot, connectionState

 

Future로 받아온 정보를 렌더링하기 위해서 FutureBuilder를 사용한다. 아래 코드를 보면 future 인자에서 Future 객체를 반환하는 myFuture 메서드를 실행하도록 했다. builder 인자에서는 기존에 가져오던 context 인자 외에 snapshot이라는 인자가 추가되었다. snapshot은 Future의 특정 시점에서의 데이터를 저장하는 의미로 사용된다. 그리고 Future의 상태에 따라 connectionState가 3가지로 구분된다. ConnectionState.none, ConnectionState.waiting, ConnectionState.done으로 특정 시점에 Future 객체의 데이터 상태를 구분할 수 있는 것이다.

 

마지막으로 CircularProgressIndicator() 위젯은 로딩 중인 페이지에 표시되는 대기 중임을 의미하는 애니메이션을 표시해준다.

FutureBuilder(
                  future: myFuture(),
                  builder: (context, snapshot) {
                    if (snapshot.connectionState == ConnectionState.done) {
                      return Text(
                        snapshot.data,
                        style: TextStyle(
                          fontSize: 20.0,
                          color: Colors.blue,
                        ),
                      );
                    }
                    return CircularProgressIndicator();
                  }),
                  
//중략

Future<String> myFuture() async {
    await Future.delayed(Duration(seconds: 2));
    return 'another Future completed';
  }

728x90
반응형