[Dart] 문법 정리

CHOI·2022년 6월 6일
10

[Dart]

목록 보기
1/2
post-thumbnail

📌 이 글의 내용 및 이미지는 모두 "코딩파파"의 유튜브 영상과 "플러터를 위한 다트 언어" 브런치 북의 내용을 정리한 내용입니다.

코딩파파

1. 변수 이름 지정(Naming Variable)

int numberOne = 1;
// numberOne: 변수

2. Const vs Final

const, final 둘 다 데이터 변경을 원하지 않는다.
저장되는 시점이 다르다.
const 에 지정되는게 final 에 지정되는 것보다 리소스와 메소리를 조금이라도 적게 사용하는 장점이 있다.

  1. Compile: const 타입은 Compile에서 값이 지정된다.
  2. Install
  3. Running: final 타입은 Running에서 값이 지정된다.

3. 숫자 데이터 유형(Numeric Data Type)

int
double
num: 숫자 모두 사용 가능.
var: 처음 지정한 데이터 타입으로 생성

var number = 1;
number = 'apple'; 
// 처음에 int 데이터 타입으로 지정을 했기때문에 string 데이터 타입으로 변경 불가능.
number = number +3;
number += 3;
number  =  number * 2;
number *= 2;

5. Boolean Data Type

assert(trueOrFalse);
// assert의 값이 false이면 오류가 발생한다.

6. List Data Type

final List<int> numbers = [1,2,3];
// 리스트 안에 있는 값들을 변경 할 수 있다.
const List<int> numbers = [1,2,3];
// 리스트 안에 있는 값들도 변경 할 수 없다.

7. Set Data Type

Set<num> numSet = {1,2,3};
// print: {1,2,3}
Set<num> numSet = {1,2,3,3};
// print: {1,2,3}

set에서는 중복된 값이 있어서는 안된다.

8. Map data type

Map<int, String> mapD = {0:'zero', 1:'one'};
print(mapD[0]) // print: 0
print(mapD[100]) // print: null
Map<String,String> mapT = {'0':'zero', '1':'one'};
print(mapT[0]) // print: null
print(mapT['0']) // print: null
mapT['newKey'] = 'newValue';
print(mapT) // print: {'0':'zero', '1':'one', 'newKey':'newValue'}

9. Function

Method도 Object이다. 일반적으로 Object를 class만으로 생각할 수 있는데 Dart에서는 모든 데이터 타입과 function은 Object이다.

11. Parameter(argument)

Parameter: 매개변수. argumanet: 전달 인자.

final int a = newNumb(5);
// 5가 agument 이다.
int newNumb(int number){
  return 123+number;
}
// number가 parameter이다.
int newNumb({int number=0}) => 123+number
// 함수안에 기본값을 지정해줄수 있다.

13. Switch

switch(number){
  case 0:
    print('0');
  break;

  case 1:
  case 2:
    print('1or2');
  break;
}
// 2가지 케이스에 동일한 액션이 있을 경우에는 붙여서 사용할 수 있다.

14. Class, Object, Instance

15. Basic constructor

Class - 설계도(공장)
Object - 자동차 instance - 자동차
(Object와 instance는 비슷하다)

void main(){
  Car myCar = Car('pink', 4);
  print(myCar.color); // print: pink
}
	
class Car{
  String? color;
  int? wheels;
	
  // constructor(생성자): 주문서 작성 양식
  // Basic constructor
  // 작성하지 않는다고 하면 자동으로 생성해준다.
  Car(String clr, int wh){
    color = clr;
    wheels = wh;
  }
  // Car(this.color, this.wheels); 와 동일하다.

  int speed(int accel){
    return accel * 2;
  }
}

16. Named constructor

void main(){
  Car basicCar = Car.basic();
  Car fullOptionCar = Car.fullOption();
}

class Car{
  String? color;
  int? wheels;

  // Named constructor
  Car.basic(){
    color = 'red';
    wheels = 4;
  }
  Car.fullOption(){
    color = 'rainbow';
    wheels = 16;
  }
}

17. Forward constructor

class Car{
  String? color;
  int? wheels;

  Car(this.color, this.wheels); // 1번.

  // #1
  Car.basic() : this('red', 4);
  // this()는 해당 Object를 생성하는 기본 생성자를 나타낸다.
  // this()는 1번을 실행을 시킨다.
  // Car.basic(String clr) : this(clr,4);

  // #2
  Car.basic(String clr, {int wheels = 4}) : this(clr,wheels);
  Car.fullOption() : this.basic('rainbow', wheels: 16);
}

18. Initializer

final value가 있을 경우, 생성자 body에서 지정해줄 수 없다. initializer에서 지정해줘야한다.

class Car{
  final String? color;
  int? wheels;

   Car(String clr, this.wheels) : color = clr {
     ...
   }
}

정리

상수: 변하지 않는 값.
변수: 변하는 값.
Parameter(매개변수): 함수의 정의 부분에 나열되는 변수. -> variable.
argumanet(전달 인자): 함수를 호출 할때 전달되는 실제 값. -> value.


플러터를 위한 다트 언어

4. 다트 주석, 타입, 변수, 상수

  • 타입
    num: int와 double의 supertype
    int: 정수
    double: 실수
    String: 문자열
    bool: true 또는 false를 가지는 Boolean type
    var: 타입 미지정 및 타입 변경 불가
    dynamic: 타입 미지정 및 타입 변경 가능
    List: 다트의 array는 list로 대체
    Set: 순서가 없고 중복 없는 collection
    Map: key, value 형태를 가지는 collection

  • 상수
    상수와 변수의 가장 큰 차이는 값의 변경 유뮤이다. 변수는 값을 바꿀 수 있지만 상수는 값을 바꿀 수 없다. 상수를 지정하는 방법은 final과 const를 이용하면
    된다.

5. 다트 함수 (Dart Function)

다트는 완전한 객체 지향 언어이다. 따라서 다트에서는 모든 것이 객체이기 때문에 함수도 객체이다. 함수가 객체이기 때문에 갖는 특징이 있다. 먼저 변수가 함수를 참조할 수 있다.
그리고 함수의 인자로 함수를 전달할 수 있다.

1) 변수가 함수 참조 가능

String name = getName();

String getName(){
  return 'abc';
}

2) 다른 함수의 인자로 함수 전달 가능

int add(int a, int b){
  return a + b;
}

int sub(int a, int b){
  return a - b;
}

int multi(int a, int b){
  return a + b;
}

main(){
 int a = 10;
 int b = 5;
 print(${multi( add(a,b), sub(a,b) )});
}

3) 이름 있는 선택 매개변수 (named optional parameter)

함수 호출 시 매개변수에 인자 값을 넘겨줄 때 매개변수명을 이용하여 인자 값을 넘겨줄 수 있다. 매개변수명으로 인자 값을 넘겨줄 매개변수는 {}로 감싸줘야 한다.

String getAddress(String city, {String district, String zipCode = '111222'}){
  return '$city시 $district구 $zipCode';
}

main(){
  print('${getAddress('서울', district: '강남')}');
}

4) 위치적 선택 매개변수 (positional optional parameter)

위치적 선택 매개변수는 미리 초깃값을 지정해놓고 함수 호출 시 해당 매개변수에 인자 값을 넘겨주지 않으면 초깃값을 사용하는 것이다. 선언 방법은 선택 매개변수 지정을 {} 대신에 []로 하는 것이 차이점이다.

String getAddress(String city, [String district = '강남구', String zipCode = '111222']){
return '$city시 $district구 $zipCode';
}

main(){
print('${getAddress('서울', '마포구')}');
}

5) 익명 함수 (anonymous function) 및 람다 (lambda expression)

다트 공식문서에는 익명 함수를 람다 또는 클로저(Closure)라고도 부른다고 설명해놨다.

// 익명 함수.
var multi = (_a, _b){
  return _a + _b;
}

// 람다.
sub(_a, _b) => _a - _b;

6. 다트 연산자 (Dart Operator)

1) 산수 연산자

+, -, *, /, ~/, %, ++, --
~/: 정수를 결괏값으로 가진다.
%: 나머지를 구한다.

2) 할당 연산자

=, +=, -=, *=, /=, ~/=

3) 관계 연산자 (비교 연산자)

==, !=, >, <, >=, <=

4) 비트 & 시프트 연산자

일반적인 모바일 앱 개발을 할 때는 자주 사용할 일이 없다. 특정 로직을 수행할 때 더 빠른 속도를 낼 수 있으며 로우 레벨에서 레지스트리를 다룰 때 유용하다.
&, |, ^, ~, <<, >>

5) 타입 검사 연산자

as: 형 변환
is: 객체가 특정 타입이면 true
is!: 객체가 특정 타입이면 false

6) 조건 표현식 (삼항 연산자)

조건 ? 표현식1 : 표현식2;

7) 캐스케이드 표기법

캐스케이드 표기법(..)은 한 객체로 해당 객체의 속성이나 멤버 함수를 연속으로 호출할 때 유용하다. 매번 객체를 표기하고 호출하는 불필요한 과정을 줄여주기 때문이다.

7. 다트 조건문과 반복문 (Control Flow)

1) 조건문

if, if~else, switch, assert
assert: 조건식이 거짓이면 에러가 발생한다. 또한 debug mode에서만 동작한다.

2) 반복문

for, while, do~while

8. 다트 클래스 (Dart Class)

다트는 모든 것이 객체인 완전 객체 지향 언어이다. 모든 객체는 클래스의 인스턴스이다. 그리고 모든 클래스는 Object 클래스의 자식이다.

1) 객체, 멤버, 인스턴스

클래스는 멤버를 가진다. 멤버는 멤버 함수(메서드)와 멤버 변수(인스턴스 변수)로 구성된다. 클래스를 사용하려면 객체를 생성해야 한다. 객체를 생성한다는 것은 클래스가 메모리에
올라간다는 의미이고 이것은 인스턴스화라고 부른다. 이렇게 메모리에 클래스가 할당되어 인스턴스가 된 것을 객체라고 한다.

클래스 외부에서 하나의 기능을 하는 함수는 Function이고 클래스 내부에 있는 멤버 함수는 Method라고 한다. 또한 멤버 변수는 객체가 생성되면 인스턴스 변수라고 한다.

2) 클래스 기본

class Person{
  // 멤버 변수.
  String name;
  
  // 멤버 함수.
  String getName(){
    return name;
  }
}

9. 다트 생성자 (Constructor)

클래스에는 생성자가 따른다. 생성자는 이름처럼 클래스가 인스턴스화 될 때, 즉 객체가 생성될 때 호출된다.

1) 기본 생성자 (Default Constructor)

클래스를 구현할 때 생성자를 선언하지 않으면(생략하면) 기본 생성자가 자동으로 제공된다. 기본 생성자는 클래스명과 동일하면서 인자가 없다. 또한 기본 생성자는 부모 클래스의 인수가 없는 생성자(= 기본 생성자)를 호출한다.

class Person{
  Person(){}
}

2) 이름 있는 생성자 (Named Constructor)

생성자에 이름을 부여한 형태이다. 한 클래스 내에 많은 생성자를 생성하거나 생성자를 명확히 하기 위해서 사용할 수 있다.

class Person{
  Person.init(){}
}

이름 없는 생성자는 단 하나만 가질 수 있다. 또한 이름 있는 생성자를 선언하면 기본 생성자는 생략할 수 없다.

3) 초기화 리스트 (Initializer List)

초기화 리스트를 사용하면 생성자의 구현부가 실행되기 전에 인스턴스 변수를 초기화할 수 있다. 초기화 리스트는 생성자 옆에 : 으로 선언할 수 있다.

class Person{
  String name;
  
  // 생성자 : 초기화 리스트
  Person() : name = 'Kim' {
    print('This is Person($name) Constructor');
  }
}

main(){
  Person person = Person();
}

// print: This is Person(Kim) Constructor

4) 리다이렉팅 생성자 (Redirecting constructor)

초기화 리스트를 약간 응용하면 단순히 리다이렉팅을 위한 생성자를 만들 수 있다. 이러한 생성자는 본체가 비어있고 메인 생성자에게 위임(delegate)하는 역할을 한다.

class Person{
  String name;
  int age;
  
  Person(this.name, this.age){
    print('This is Person($name, $age) Constructor');
  }
  
  Person.initName(String name) : this(name, 20);
}

main(){
  Person person = Person.initName('Kim');
  // print: This is Person(Kim, 20) Constructor
}

5) 상수 생성자 (Constant Constructor)

상수 생성자는 말 그대로 생성자를 상수처럼 만들어 준다. 이 말은 해당 클래스가 상수처럼 변하지 않는 객체를 생성한다는 것이다. 상수 생성자를 만들기 위해서는 인스턴스 변수가 모두
final 이어야 한다. 또한 생성자는 const 키워드가 붙어야 한다.

class Person{
  final String name;
  final int age;
  
  const Person(this.name, this.age);
}

6) 팩토리 생성자 (Factory Constructor)

팩토리 생성자는 팩토리 패턴을 사용하기 편리한다. 팩토리 패턴을 사용하면 해당 클래스의 인스턴스를 매번 생성하지 않아도 된다. 보통 자식 클래스의 인스턴스를 리턴 받는다.

class Person{
  Person.init();
  
  factory Person(String type){
    switch (type){
      case 'Student' :
        return Student();
    }
  }
  
  String getType(){
    return 'Person';
  }
}

class Student extends Person{
  Student() : super.init();
  
  
  String getType(){
    return 'Student';
  }
}

main(){
  Person student = Person('Student');
  print(${student.getType()});
  // print: 'Student';
}

11. 다트 상속 (Dart Inheritance)

상속은 객체지향 프로그래밍의 꽃이라고 할 수 있다. 객체지향 프로그래밍에서 상속은 클래스의 멤버를 물려주는 것을 의미한다. 이때 물려주는 쪽은 부모 클래스 혹은 Super
Class 라고 하고 상속 받는 쪽을 자식 클래스 혹은 Sub Class 라고 한다.

class 부모 클래스명 {
  멤버 변수;
  멤버 함수(){}
}

class 자식 클래스명 extends 부모 클래스명 {
  
  멤버 함수(){}
}

자식 클래스는 extends 키워드를 통해서 상속받고자 하는 부모 클래스를 지정한다. @override 어노테이션은 부모 클래스의 메서드를 재정의하고 싶을 때 사용한다. 재정의한다는 의미는 기존 메서드에서 구현한 내용 대신 다른 동작을 하는 코드를 구현하는 것이다.

12. 다트 접근 지정자 (Access Modifier)

객체지향 언어의 4대 특징 추상화, 캡슐화, 상속, 다형성

1) 캡슐화

간단하게 캡슐화를 설명하면 어떤 객체가 어떤 목적을 수행하기 위한 데이터(멤버 변수)와 기능(메서드)을 적절하게 모으는 것이다. 예를 들어 이름과 니이만 알고 먹고 자고 코딩만
하면 되는 개발자 객체를 캡슐화한다고 해보자.

class Developer{
  String name;
  int age;
  
  eat() => print('eat');
  sleep() => print('sleep');
  coding() => print('coding'); 
}

이런 식으로 하나의 기능을 수행하는 객체를 만드는 것이 캡슐화이다.

2) 추상화

추상화는 어떤 객체의 공통된 데이터와 메서드를 묶어서 이름(클래스명)을 부여하는 것이다. 이렇게 말하면 무슨 말인가 싶을 텐데 단순히 말하면 클래스를 만드는 일이다.

생각해보자. 사람 클래스를 만들 것이다. 사람 객체에는 남자도 있고 여자도 있고 학생도 있고 선생님도 있고 개발자도 있다. 이런 다양한 하위 객체들을 아우르는 사람을 의미하는
Person 클래스를 만들기 위해서 고민해야 하는 것은 무엇인가? 일단 모든 사람들이 공통적으로 갖는 데이터와 행동을 떠올려야 한다.

class Person{
  String name;
  int age;
  
  eat(){}
  sleep(){}
}

이런 공통 속성을 추출해 나가는 과정을 추상화라고 한다. 추상화를 잘한다면 앞선 Developer 클래스를 만들 때 Person 클래스를 상속받는 구조를 통해 중복된 부분을 많이
없앨 수 있다. 코드 재사용률을 높일 수 있다는 것이다.

3) 접근 지정자

접근 지정자는 캡슐화의 짝꿍인 정보은닉과 관련있다. 접근 지정자는 클래스의 멤버 변수 또는 메서드 앞에 선언되고 종류에 따라 행당 멤버들에게 접근할 수 있는 범위가 달라진다.

자바의 경우에는 네 가지 접근 지정자가 존재한다. 그중에서 private 라는 접근 지정자는 동일 클래스 내에서만 접근이 가능하다. 그리고 public은 접근 범위에 제한 없이
모든 곳에서 접근 가능하다.

다트의 접근 지정자는 딱 두 종류다. 바로 앞서 말한 private와 public이다. 그러나 주의할 점이 있다. 접근 범위가 자바와 다르다. 다트에서 private 멤버의 접근
범위는 동일 클래스가 아니라 라이브러리이다.

또한 접근 지정자의 키워드도 다르다. 다트는 기본적으로 아무런 키워드가 없는 경우 public이다. private로 선언하기 위해서는 변수나 메서드 앞에 _ 을 붙어야 한다.

class Person{
  String name;
  int _age;
  
  eat() => print('eat');
  _sleep() => print('sleep');
}

13. 다트 Getter & Setter

여기 저기서 참조하고 있으면 어디서 잘못된 값으로 변경했는지 모두 확인해야 되기 때문에 좋지 않다. 멤버 변수뿐만 아니라 메서드도 마찬가지 이다.

이런 일을 방지하고자 클래스의 내부 정보를 공개하지 않도록 하는 것이 정보 은닉 방법 중 한 가지이다. 이러한 정보 은닉은 바로 캡슐화를 통해서 이뤄진다. 특정 멤버 변수에 접근할
수 있는 특별한 기능을 하는 메서드를 만들면 된다.

멤버 변수를 private으로 선언하고 해당 변수에 접근할 수 있는 메서드는 public으로 선언하면 멤버 변수에 직접적으로 접근하는 것을 막을 수 있다. 그런 특별한 메서드가
바로 Getter와 Setter이다.

Getter는 멤버 변수의 값을 가져오는 역할을 하고, Setter는 값을 쓰는 역할을 한다. 다트에서는 getter와 setter 메서드는 각각 get과 set이라는 키워드로
사용한다.

class Person{
  String name;
  
  String get name => _name;
  set name(String name) => _name = name;
}

main(){
    Person person = Person();
    person.name = 'KIM';
    print(person.name); 
    // print: KIM
}

14. 다트 추상 클래스 (Abstract Class)

추상 클래스는 추상 메서드를 가질 수 있는 클래스이다. 필수적으로 포함해야 하는 것은 아니다. 일반 클래스에는 추상 메서드를 가지고 싶어도 선언할 수 없다. 그럼 추상 메서드는
뭘까? 미완성된 메서드라고 생각하면 된다. 다시 말하면 메서드 선언은 되어 있지만 바디가 정의되지 않은 형태이다.

추상 클래스는 기존 클래스 앞에 추상이라는 뜻을 가진 abstract 키워드를 붙여서 표현한다. 추상 클래스와 추상 메서드의 간단한 예는 다음과 같다.

abstract class Person{
    eat();
}

추상 클래스는 미완성 클래스이기 때문에 객체를 생성할 수 없다. 하지만 참조형 변수의 타입으로는 사용이 가능하다. 추상 클래스를 사용하기 위해서는 일반 클래스에서
implements 키워드로 임플리먼트 한 후에 받드시 추상 메서드를 오버라이딩 해야 한다.

abstract class Person{
    eat();
}

class Developer implements Person{
    
    eat(){
        print('Developer eat a meal');
    }
}

main(){
    Person person = Developer();
    person.eat();
}

Developer 클래스는 추상 클래스인 Person을 임플리먼트 했다. 이때 반드시 추상 클래스 Person의 추상 메서드인 eat()를 오버라이딩 해야한다. 꼭 다른 기능으로
사용하기 위한 재정의가 아니더라도 반드시 Developer 클래스 내에 선언되어 있어야 한다.

main() 함수를 보면 person 객체의 타입으로 추상 클래스인 Person을 사용했다. 다만 실제 생성되는 객체는 Developer이다. 이것이 참조형 변수 person의
타입으로 추상 클래스가 사용 가능한 예이다. 다만 다음과 같이 추상 클래스로 객체를 생성하려고 하면 오류가 발생한다.

Person person = Person(); // error

다트에서 추상 클래스에 반드시 추상 메서드만 존재해야 하는 것은 아니다. 앞서 말했듯이 추상 메서드가 존재할 수도 있을 뿐다. 따라서 일반 메서드를 정의할 수도 있고 일반 메서드만
존재할 수도 있따. 일반 메서드도 반드시 임플리먼트 하는 클래스에서 재정의 되어야 한다. 그리고 추상 메서드든 일반 메서드든 임플리먼트 하는 클래스에서 @override 어노테이션
생략이 가능하다.

abstract class Person{
    eat();
    sleep(){
        print('Person must sleep');
    }
}

class Developer implements Person{
    
    eat(){
        print('Developer eat a meal');
    }
    sleep(){
        print('Developer must sleep');
    }
}

main(){
    Person person = Developer();
    person.eat(); // print: Developer eat a meal
    person.sleep(); // print: Developer must sleep
}

위 예제를 보면 추상 클래스 Person에 일반 메서드인 sleep()이 추가되어 있다. 또한 Developer 클래스에서 재정의할 때 @override 어노테이션을 생략한 것을
볼 수 있다. 이때 예제 처럼 일반 메서드인 sleep()도 반드시 Developer 클래스에서 재정의 해야 한다는 점은 다시 한번 강조한다. 안하면 에러가 발생한다.

extends 키워드를 사용해서 상속받은 일반 클래스의 경우에는 단 하나의 클래스만 상속이 가능하다. 하지만 추상 클래스는 일반 클래스와 다르게 여러개의 임플리먼트 할 수 있다.

요약 추상 클래스와 추상 메서드는 abstract 키워드를 사용한다. 추상 클래스는 참조형 변수의 타입으로 사용할 수 있다. 추상 클래스를 임플리먼트 할 때 반드시 메서드를 오버라이딩 해야 한다. 추상 클래스에 추상 메서드만 존재하는 것은 아니다. 메서드 오버라이딩 시 @override 어노테이션은 생략 가능하다.

extends
부모에서 선언/정의를 모두하며, 자식은 오버라이딩 할 필요 없이 부모의 메소드/변수를 그대로 사용할 수 있다.
"부모의 특징을 연장해서 사용한다."라고 기억하면 될 듯!

implements (interface 구현)
부모 객체는 선언만 하며, 정의는 반드시 자식이 오버라이딩해서 사용한다.
"부모의 특징을 도구로 사용해 새로운 특징을 만들어 사용한다."라고 기억하면 될 듯!

정리

  • 일반 클래스와 abstract 클래스를 상속할 땐 extends를 사용하고, interface를 상속할 땐 implements를 사용한다.
    즉, class가 interface를 상속받는다면 implements를 사용하고,
    interface가 class를 상속 받는다면 extends를 사용한다.
  • class가 class를 상속받을 땐 extends를 사용하고, interface가 interface를 상속 받을 땐 extends를 사용한다.
  • extends는 클래스 한 개만 상속 받을 수 있으며, 자식 클래스는 부모 클래스의 기능을 사용할 수 있다.
  • implements는 여러 개의 interfaces를 상속 받을 수 있으며, 자식 클래스는 부모의 기능을 다시 정의해서 사용해야한다.

15. 다트 컬렉션 (Dart Collection)

컬렉션은 다수의 데이터를 처리할 수 있는 자료 구조이다. 하나의 데이터가 아닌 데이터의 집합이기 때문에 반복 가능하기도 하다. 반복 가능핟가는 의미를 단순하게 생각하면 반복문
내에서 순회할 수도 있다는 것이다.

List: 데이터 순서가 있고 중복 허용
Set: 데이터 순서가 없고 중복 허용하지 않음
Map: Key와 Value로 구성되면 키는 중복되지 않고 값은 중복 가능

3) Map

map에서 키에 대한 값의 맵핑을 새로운 값으로 변경하려면 update()라는 메서드를 이용하면 된다.

main(){
    Map<int, String> testMap = {
        1: 'Red',
        2: 'Oranage',
        3: 'Yellow',
    }
    testMap[4] = 'Green';
    
    testMap.update(1, (value) => 'NewRed', ifAbsent: () => 'NewColor');
    testMap.update(5, (value) => 'NewBlue', ifAbsent: () => 'NewColor');
    
    print(testMap[1]); // print: NewRed
    print(testMap[5]); // print: NewColor
    print(testMap); // { 1: 'NewRed', 2: 'Oranage', 3: 'Yellow', 4: 'Green', 5: 'NewColor' }
}

Line5는 키 1의 값을 'NewRed' 바꾸는 것이다. ifAbsent는 변경하고자 하는 키가 없을 때 해당 키와 값을 추가하도록 설정하는 것이다. Line6을 보면 키 5는
존재하지 않는다. 따라서 키 5가 추가되면서 값은 ifAbsent에서 지정한 'NewColor'가 된다.

16. 다트 제네릭 (Dart Generic)

제네릭은 타입 매개변수를 통해 다양한 타입에 대한 유연한 대처를 가능하게 한다. 컬렉션에서 이미 제네릭을 사용했다. List, Set, Map 모두 <>를 사용했는데 그 부분에
타입 매개변수 (Type Parameter)를 지정한다. 이렇게 <>에 타입 매개변수를 선언하는 것을 매개변수화 타입 (Parameterized Type)을 정의한다고 한다.

1) 타입 매개변수

매개변수는 클래스 생성 시 생성자에서 사용하거나 함수 호출 시 인자 값을 전달하기 위해서 사용한다. 타입 매개변수는 말 그대로 전다하는 것이 인자 값이 아니라 타입이라고 생각하면
된다.

abstract class List<E> implements EfficientLengthIterable<E>{
    ...
    void add(E value);
    ...
}

다트의 List 클래스는 위와 같이 선언되어 있다. 가 존재하기 때문에 타입 매개변수를 사용할 수 있다는 것을 알 수 있다.
(자바에서는 의 E는 형싱 타입 매개변수 (Formal Type Parameter)하고 한다.)
따라서 List 객체 생성 시 아래와 같이 사용할 수 있는 것이다.

List<String> colors = <String>[];
colors.add('Red');

타입 매개변수를 int로 지정하면 int 타입 값만 넣을 수 있는 List가 생성된다. 이런 식으로 매개변수에 값을 넘겨주듯이 타입을 넘겨줄 수 있는 것이 제네릭의 핵심이다.

이렇게 제네릭을 사용해서 얻는 이점이 뭘까? 바로 코드를 중복으로 선언할 필요가 없는 것이다.

다시 List로 돌아와서 생각해보자. String을 다루는 List와 int를 다루는 List를 각각 생성한다고 가정해보자. 그러면 백번 양보해서 오버로딩을 사용해도 add()
메서드만 2개가 필요하다. 다른 타입에 다른 메서드들까지 고려하면 그 수가 아주 많아진다. 그러나 제네릭을 사용하면 단 하나의 코드로 다양한 타입에 대한 커버가 가능한 것이다.

2) 매개변수화 타입을 제한하기

제네릭을 사용할 때 매개변수화 타입을 제한할 수도 있다. 타입 매개변수에 extends를 사용해서 특정 클래스를 지정하면 된다. 그러면 해당 특정 클래스와 그 클래스의 자식
클래스가 실제 타입 매개변수가 될 수 있는 것이다. (다향성을 생각하면 된다.)

class Person{
    eat(){
        print('Person eat a food');
    }
}

class Student extends Person{
    eat(){
        print('Student eat a hamberget');
    }
}

class Manager<T exntends Person>{
    eat(){
        print('Manager eat a sandwich');
    }
}

class Dog{
    eat(){
        print('Dog eat a dog food');
    }
}

void main(){
    var manager1 = Manager<Person>();
    manager1.eat();
    var manager2 = Manager<Student>();
    manager2.eat();
    var manager2 = Manager();
    manager2.eat();
    
    // print: Manager eat a sandwich
    // print: Manager eat a sandwich
    // print: Manager eat a sandwich
}

Line13의 Manager 클래스는 타입 매개변수로 을 선언했다. 그러면 Person 클래스와 그 자식 클래스가 실제 타입 매개변수가 될 수
있따. Line30과 같이 실제 타입 매개변수 없이 그냥 Manager 클래스 생성도 가능하다.

Line7의 Student 클래스는 Person의 자식 클래스이다. 따라서 Line28과 같이 Manager 클래스의 실제 타입 매개변수가 될 수 있다.

Line19의 Dog 클래스는 그냥 Dog 클래스이다. Person 클래스와 아무런 관계가 없다. 따라서 Dog 클래스는 Manager 클래스의 실제 타입 매개변수가 될 수 없다.
따라서 Line32와 같이 사용하려고 하면 에러가 발생한다.

3) 제네릭 메서드

제네릭은 클래스뿐만 아니라 메서드에도 사용할 수 있다. 메서드의 리턴 타입, 매개변수를 제네릭으로 지정할 수 잇다.

class Person{
    T getName<T>(T param){
        return param;
    }
}

void main(){
    var person = Person();
    print(person.getName<String>('Kim'));
    // print: Kim
}

Line2: getName() 메서드의 리턴 타입과 매개변수가 제네릭 타입으로 지정되었다. Line9: 실제 타입 매개변수를 String으로 지정하고 인자로 'Kim'을 넘겨주고
다시 'Kim'을 리턴 받아서 출력하고 있다.

17. 다트 비동기 프로그래밍 (Dart Asynchronous Programming) - isolate, future, stream

일반적인 프로그래밍은 순차적으로 작업을 처리한다. 즉 하나의 작업을 요청한 후 그 작업이 끝나면 다음 작업으로 넘어간다. 이런 경우 처리시간이 긴 작업 (특히 UI와 관련된 상황)
을 만나면 사용자는 프로그램이 멈춘 것 처럼 느낄 수 있다.

이러한 문제는 비동기 프로그래밍으로 해결할 수 있다. 비동기 프로그래밍은 요청한 작업의 결과를 기다리지 않고 바로 다음 작업으로 넘어감으로써 프로그램의 실행을 멈추지 않는다.
요청한 작업의 처리는 별도의 방식에 맡긴다.

비동기는 동시성(Councurrency)이나 병력(Parallel)은 비교군이 될 수 없는 다른 개념이다. 또한 비동기를 정확히 이해하기 위해서는 블록킹(blocking) /
논블록킹(non-blocking)에 대해서도 알아야한다.

다트는 future, stream을 통해서 자체적으로 비동기 프로그래밍을 지원한다. 그 전에 isolate 라는 다트의 독특한 구조부터 알아야한다.

1) isolate

isolate라는 단어는 격리하다는 의미이다. 다트의 isolate도 그 의미와 연관이 깊다. isolate는 다트의 모든 코드가 실행되는 공간이다. 싱글 스레드를 가지고 있고
이벤트 루프를 통해 작업을 처리한다. 기본 isolate인 main isolate는 런타임에 생성된다.

isolate가 비록 싱글 스레드이지만 다트가 자체적인 비동기 프로그래밍을 지원하기 때문에 비동기 작업도 이벤트 루프에 의해서 적절히 처리된다. 또한 main isolate에서
무거운 작업으로 인해 반응성이 떨어진다면 추가로 isolate를 생성할 수 있다. 그러면 스레드가 2개가 되는 것이다. 다만 기존의 언어에서 사용하는 스레드와 차이점이 있다.

1.1) isolate 구조 및 기존 스레드와 차이점

자바 등의 다른 언어에서 사용하는 스레드는 다음과 같이 스레드가 서로 메모리를 공유하는 구조이다.

자바 스레드 구조

하지만 isolate의 스레드는 자체적으로 메모리를 가지고 있다. 따라서 새로운 isolate를 생성하면 해당 isolate에 별도로 고유한 메모리를 가진 스레드가 하나 더 생기는
것이다. 즉 메모리 공유가 되지 않는다.

다트 isolate 구조

따라서 두 isolate가 함께 작업하려면 message를 주고 받아야만 가능하다. 이것이 불편하다고 생각할 수도 있다. 하지만 멀티스레드 사용 시 늘 주의해야 하는 공유자원에
대한 컨트롤에 신경 쓰지 않아도 된다.

이벤트 루프는 이벤트 큐에 쌓여있는 작업들을 오래된 순으로 하나씩 가져와서 처리하도록 하는 역할을 한다.

1.2) 새로운 isolate 생성하기

새로운 isolate는 spawn을 통해서 만들 수 있따. 다음 예제를 보자.

import 'dart:isolate';

void main(){
    // ReceivePort mainReceivePort = ReceivePort();
    Isolate.spawn(isolateTest, 1);
    Isolate.spawn(isolateTest, 2);
    Isolate.spawn(isolateTest, 3);
}

isolateTest(var m){
    print('isolate no.$m);
}

// print: isolate no.2
// print: isolate no.1

main() 함수에서 isolate를 3개 spawn 하였다. 따라서 3개의 isolate가 만들어진다.

1.3) isolate 간 message 주고받기

앞선 예제에서 Line 4에 주석 처리된 부분이 있다. ReceivePort는 isolate 간에 message를 주고받을 수 있는 역할을 한다. ReceivePort는
sendPort라는 getter를 통해서 SendPort를 리턴 받는다. 따라서 message를 보내고(send) 받기(receive)가 가능하다. message를 보낼 때는
SendPort의 send를 이용하고 수신할 때는 ReceivePort의 listen을 이용한다.

다음 예제는 main isolate가 5개의 isolate와 message를 주고받는 예제이다.

import 'dart:isolate';

void main(){
    int counter = 0;
    
    ReceivePort mainReceivePort = ReceivePort();
    
    mainReceiverPort.listen((){
        if(fooSendPort is SendPort){
            fooSendPort.send(counter++);
        }else{
            print(fooSendPort);
        }
    });
    
    for(var i=0; i<5; i++){
        Isolate.spawn(foo, mainReceivePort.sendPort);
    }
}

foo(SendPort mainSendPort){
    ReceivePort fooReceivePort = ReceivePort();
    mainSendPort.send(fooReceivePort);
    
    fooReceivePort.listen(($msg){
        mainSendPort.send('received: $msg');
    });
}

// print: received: 0
// print: received: 1
// print: received: 2
// print: received: 3
// print: received: 4
  • Line6: main isolate 에서 사용할 ReceivePort인 mainReceiverPort를 생성한다.
  • Line 8~14 : mainReceiverPort에서 message를 수신하는 listen를 선언한다. 만약 수신한 message가 SendPort 타입이면 해당
    SendPort로 count 변수를 message로 하여 send 한다. 수신한 message가 SendPort 타입이 아니면 message를 출력한다.
  • Line 17 : 5개의 isolate를 생성한다.
  • Line 22 : 새로 생성된 isolate의 RecivePort인 fooReceivePort를 생성한다.
  • Line 23 : isolate 생성 시 전달받은 main isolate의 SendPort를 이용하여 main isolate에 새로 생성된 isolate의 SendPort를
    전달한다.
  • Line25~27 : main isolate에서 받은 message(count 변숫값)을 'received: &msg' 형태의 String으로 만들어 다시 main
    isolate로 보낸다. 이 String이 Line 12의 print()를 통해서 출력되는 것이다.

isolate 예제 구조

2) future

다트는 future를 지원한다. future는 이미 다른 언어에서도 종종 사용되고 있는 키워드다. future는 어떤 작업 결괏값을 나중에 받기로 약속하는 것이다. 즉 요청한
작업의 결과를 기다리지 않고 바로 다음 작업으로 넘어간다. 그 후 작업이 완료되면 결과를 받는 방식으로 비동기 처리를 하는 것이다.

작업이 완료될 때까지 기다렸다가 결괏값을 받고 다음 작업으로 넘어갈 수도 있다. 이 경우는 sync, await를 사용하면 가능하다.

future는 크게 두 가지 상태를 가지고 세부적으로는 세 가지 상태를 가진다.

1.Uncompleted(미완료): future 객체를 만들어서 작업을 요청한 상태
2.Completed(완료): 요청한 작업이 완료된 상태
2-1.data: 정상적으로 작업을 수행하여 결괏값을 리턴하며 완료
2-2.error: 작업 처리 중 문제 발생 시 에러와 함께 완료

future는 상태별로 다른 작업과 마찬가지로 event loop에 의해서 순차적으로 처리된다.
처음 future를 생성하여 작업을 시작하면 Uncompleted future가 event queue에 들어간다.
해당 작업이 완료되기 전까지는 다른 작업들이 event queue에 들어가고 event loop에 의해서 꺼내져 처리된다.
그러다가 future가 작업을 끝내면 Completed future가 event queue에 들어가고 event loop에 의해 선택되면
Completed future가 가진 결괏값이나 에러에 대한 처리를 하는 것이다.

다음 그림은 future가 event queue에 어떤 식으로 쌓이는지 보여준다.

위 그림을 바탕으로 예제를 만들어 볼 것이다. 일단 future의 기본 형태를 잠시 살펴보자. 조금이라도 쉽게 이해할 수 있도록 가장 날 것의 형태부터 먼저 본다.

Future<T> 변수명 = Future((){
    // do something
}
return T;
);

변수명.then((결괏값){
    // do something
}, onError:(에러){
    // do something
});

future 객체를 만들 때 타입은 Future와 같이 제네릭을 사용한다. 만약 타입을 Future로 선언 했다면 future에서 작업 후 리턴될 결괏값의
타입이 String 타입이라는 의미다. future를 만들어서 작업이 시작된 이 상태가 Uncompleted future이다. 따라서 현재 상태를 event queue에 넣고 다음
작업으로 넘어간다. 이런 식으로 future내의 do something이 모두 처리되기 전에 다음 작업을 진행할 수 있다.

future의 작업이 완료되면 then()이 호출된다. 이따는 Completed future인 상태이다.
then()의 첫 번째 매개변수는 결괏값을 인자로 가지는 익명 함수이고 두 번째 onError는 에러 처리를 위한 함수이다.

가장 날 것의 모습을 한 코드로 구현하면 다음과 같다.

main(){
    print('start');
    
    Future<String> myFuture = Future((){
        for (int i =0; i<10000000; i++){}
        return 'I got lots of data! There are 10000000.';
    });
    
    myFuture.then((data){
        print('data');
    }, onError: (e){
        print(e);
    });
    
    print('do something');
}

// print: start
// print: do something
// print: I got lots of data! There are 10000000.

이제 위 코드를 좀 더 보기 좋게 정리해보자. for 문이 테이터를 얻는 과정이라고 가정하여 해당 부분을 별도 함수로 분리하고 then()의 익명 함수도 람다를 사용할 것이다.
에러 처리를 하는 onError도 builder pattern으로 변경할 것이다. 이때는 catchError()라는 함수를 사용한다.

Future<String> getData(){
    return Future((){
        for (int i =0; i<10000000; i++){}
        return 'I got lots of data! There are 10000000.';
    });
}

main(){
    print('start');
    
    var myFuture = getData();
    myFuture.then((data) => print(data))
        .catchError((e) => print(e);
    
    print('do something');
}

// print: start
// print: do something
// print: I got lots of data! There are 10000000.

onError와 catchError는 에러를 처리한다는 관점에서 역할은 동일하지만 차이점이 있다.
onError는 future에서 발생한 에러만 처리할 수 있다. 대신 catchError는 then()의 첫 번째 인자인 익명 함수 내부에서 발생한 에러까지 처리할 수 있다.

3) async, await

async와 await는 한 쌍으로 사용한다. await가 비동기(async) 함수 내에서만 사용할 수 있는 키워드이기 때문이다.
비동기 함수를 만드는 방법은 함수명 뒤에 async 키워드를 붙이는 것이다.
기본 형태는 다음과 같다.

함수명() async {
    await 작업함수();
}

비동기 함수 내에서 await가 붙은 작업은 해당 작업이 끝날 때까지 다음 작업으로 넘어가지 않고 기다린다. 어떤 경우에 async, await를 사용하는지 예제를 보자.

Future<String> getData(){
    return Future((){
        for (int i =0; i<10000000; i++){}
        return 'I got lots of data! There are 10000000.';
    });
}

main() async {
    print('start');
    
    var myFuture = await getData();
    myFuture.then((data) => print(data))
        .catchError((e) => print(e);
    
    print('do something');
}

// print: start
// print: I got lots of data! There are 10000000.
// print: do something

main() 함수를 비동기 함수로 만들고 getData()의 작업이 끝날 때까지 다음 작업으로 진행하지 않도록 await를 사용했다.
그러면 getData() 내부의 future가 작업을 마치고 결괏값을 제공하기 때문에 출력 결과가 이전과 다르게 결괏값을 보여준다.
또한 실제 실행 시 Line2의 start가 출력이 되고 Line4에서 getData() 내부 for문이 처리되는 동안 프로그램이 멈춘 것처럼
있다가 완료되는 순간 Line5가 실행되고 곧이어 Line7이 실행되는 것을 확인할 수 있다.

4) stream

stream은 연속된 데이터를 listen()을 통해서 비동기적으로 처리할 수 있다.
예를 들면 실시간으로 데이터를 처리할 때 future는 이미지 파일 하나를 다운로드하여 보여줄 때 적합하다면
stream은 동영상 (연속된 이미지)을 보여주는 데 사용할 수 있다.
stream은 이름처럼 흔히 말하는 스트리밍 (streaming) 서비스의 동작 방식과 다를 바 없는 것이다.

4.1) stream 동작 방식

이해를 돋기 위해 다음과 같은 가상의 동영상 스트리밍 환경이 있다고 가정하자.

실시간 스트리밍을 위한 동영상 파일은 여러 장의 이미지 파일로 구성됨
해당 파일은 서버에 존재하며 서버는 한번에 이미지 파일 한장씩 전송
클라이언트 (future or stream)에서는 한 번에 이미지 파일 한 장씩 수신
각 이미지 파일은 최소 2초 이내에 가져와야 원활한 재생이 가능

위 조건을 기반으로 future와 stream의 데이터 처리 과정을 그림으로 나타내면 다음과 같다.

future는 서버에 데이터를 요청하면 image01 하나만 수신하고 해당 파일만을 결괏값으로 가져오고 끝난다.
타입이 Future<image.> 라고 생각하면 된다. 하지만 stream은 image01을 수신하면 listen()에서 처리한 후 끝나는 게 아니다.
이어서 image02를 수신하면 바로 listen()에서 처리할 수 있도록 전달한다. 이런 식으로 동영상의 끝까지 처리가 가능하다.
listen()에서는 수신한 이미지를 연속으로 화면에 보여주는 처리를 하면된다.

다시 정리하면 좀 더 간략하게 표현하면 다음과 같다.
future는 서버에 데이터를 요청한 후 수신한 결괏값에 대해서 then()으로 전달한다.

stream은 구독자 패턴(또는 관찰자 패턴, Observer pattern)이다. 구독자(listen)가 관찰 대상(stream)을 구독하여
관찰 대상에 변화가 발생하면 구독자에게 그 변화를 알려준다.

서버에서 데이터를 받아오는 동작 중에 image01이 수신되면 (=stream 변화 발생) 곧바로 listen()(=구독자)에게 전달하고 대기한다.
그러다 또 어느 타이밍에 image02가 수신되면 listen에게 변화를 알려주는 것을 반복하는 것이다.

'future에서 for문으로 List에 image들을 전부 넣어서 결괏값으로 넘겨주면 되잖아!' 라고 할 수 있지만
여기서는 실시간 스트리밍이 목적이다. List로 저장하여 넘겨주는 것은 실시간이 아니라 동영상 파일을 다운로드 한 후 재생하는 것과 같다.
즉 다음 그림과 같이 처리된다.

4.2) stream 예제 코드
4.2.1) stream을 만드는 다양한 방법

stream의 동작 방식에 대한 감이 잡혔다면 실제로 stream을 생성하는 다양한 방법에 대해서 살펴보자.

main(){
    print('start');
    var stream = Stream.value(100).listen((dynamic x) => print('getData: $x'));
    print('do something');
}

// print: start
// print: do something
// print: getData: 100

Line3: 하나의 데이터에 대한 이벤트를 발생하는 stream을 생성한다. 100이란 정수형 데이터를 넘겨주면 곧바로 listen()에서 출력하는 단순한 동작이다.

특정 주기로 반복적으로 이벤트를 발생하는 stream을 생성할 수도 있다.

main(){
    print('start');
    var stream = Stream.periodic(Duration(seconds:1), (x) => x).take(5);
    stream.listen(print);
    print('do something');
}

// print: start
// print: do something
// print: 0
// print: 1
// print: 2
// print: 3
// print: 4

Line3: stream을 생성한다. Stream.periodic()은 특정 주기로 반복적으로 이벤트를 발생하는 stream을 만드는 것이다. 첫 번째 인자는 Duration()
객체이고, 두 번째 인자는 이벤트에서 발생한 값을 계산하는 함수이다. Duration은 1초 설정하여 1초 간격으로 설정했고 계산 함수는 디폴트인 카운트 함수를 사용하도록 했다.
0부터 시작해여 1초에 1씩 증가한다. take()는 몇 회까지 반복할지 정해주는 역할을 한다.

Line4: listen()은 stream의 변화를 관찰하여 변화가 있을 때, 즉 새로운 데이터 입력 시 해당 데이터를 출력해준다. var stream =
Stream.periodic(Duration(seconds: 1), (x) => x).take(5).listen(print); 이렇게 쭉 이어서 listen을 사용할 수도 있다.

Line8-14: 실행 결과이다. stream에 1초에 한 번씩 데이터가 들어오면 listen()에서 그때마다 출력을 한것이다.

periodic 외에도 fromIterable을 통해서 List와 같은 형태의 데이터를 다룰 수 있다. 또한 future를 다루려면 fromFuture를 사용하면 된다. 간단한
예시는 다음과 같다.

main(){
    print('start');
    var stream = Stream.periodic(Duration(seconds: 1), (x) => x+1)
        .take(5)
        .listen((x) => print('periodic: $x'));
        
    Stream.fromIterable(['one', 2.5, 'three', 4, 5])
        .listen((dynamic x) => print('fromIterable: $x'));
        
    Stream.fromFuture(getData())).listen((x) => print('fromFuture: $x'));
            
    print('do something');
}

Future<String> getData() async{
    return Future.delayed(Duration(seconds: 3), () => 'after 3 seconds');
}

// print: start
// print: do something
// print: fromIterable: one
// print: fromIterable: 2.5
// print: fromIterable: three
// print: fromIterable: 4
// print: fromIterable: 5
// print: periodic: 1
// print: periodic: 2
// print: periodic: 3
// print: fromFuture: after 3 seconds 
// print: periodic: 4
// print: periodic: 5

Line3-6: periodic의 listen을 조금 수정했다. builder 형식으로 바꾸고 print 부분도 변경되었다.

Line8-9: List 타입의 데이터에서 값을 받아서 처리하고 있다. 각 요소를 순차적으로 가져와서 출력한다.

Line11-12: future를 처리하는 stream이다. getData()를 보면 3초 후에 'after 3 seconds'라는 문자열을 결괏값으로 가진다. 따라서
periodic의 결과와 함께 보면 3초에 출력되는 것을 확인할 수 있다.

4.2.2) StreamController 사용하기

비동기 함수에 의해서 전달되는 형태가 아니라 stream에 이벤트를 직접 지정해주고 싶다면 StreamController를 사용하자. StreamController으로
stream을 만들고 이벤트를 채워 넣으면 된다.

import 'dart:async';

main(){
    print('start');
    
    StreamController streamCtr1 = StreamController();
    streamCtr1.stream.listen((x) => print(x));
    
    streamCtr1.add(100);
    streamCtr1.add('test');
    streamCtr1.add(200);
    streamCtr1.add(300);
    streamCtr1.close();
    
    print('do something');
}

// print: start
// print: do something
// print: 100
// print: test
// print: 200
// print: 300

Line6: StreamController를 생성한다. StreamController은 멤버로 stream을 포함하고 있다.

Line7: StreamController으로 만든 stream에 대한 구독을 위한 listen을 등록한다.

Line9-12: add()를 통해 이벤트를 추가한다. 각 이벤트가 발생하면 listen에서 출력으로 처리한다.

Line13: stream을 닫는다.

기본적으로 하나의 stream에 대한 구독자(listen)는 1개만 등록할 수 있다. 만약 2개 이상 등록하고 싶으면 어떻게 해야 할까?
broadcast를 사용하면 된다. stream: 제 방송 듣고 싶은 사람은 모두 들으세요. 이런 의미이다.

import 'dart:async';

main(){
    print('start');
    
    var stream = Stream.periodic(Duration(seconds: 1), (x) => x+1).take(3);
    stream.listen(print);
    // stream.listen(print); // error
    
    StreamController streamCtr1 = StreamController().broadcast();
    streamCtr1.stream.listen((x) => print('listen 1 = $x'));
    streamCtr1.stream.listen((x) => print('listen 2 = $x'));
    
    streamCtr1.add(100);
    streamCtr1.add(200);
    streamCtr1.add(300);
    streamCtr1.close();
    
    print('do something');
}

// print: start
// print: do something
// print: listen 1 = 100
// print: listen 2 = 100
// print: listen 1 = 200
// print: listen 2 = 200
// print: listen 1 = 300
// print: listen 2 = 300

Line8: broadcast가 아닌 stream에 listen을 2개 등록하면 에러가 발생한다.

Line10: StreamController를 만들 때 broadcast로 생성하였다.

Line11-12: 하나의 stream에 listen()을 2개 등록하였다. broadcast라 가능하다.

4.2.3) async*, yield 사용하기

제너레이터(Generator) 함수는 반복 가능한 함수이다. 보통 함수는 return을 맞이하면 종료된다.
하지만 제네리엍 함수는 return 대신에 yield를 사용한다. 제너레이터 함수를 만드는 방법은 비동기 함수와 비슷하게 함수명 뒤에
async 라는 키워드를 붙인다. 이러한 제너레이터 함수의 리턴 타입은 Stream이다.
다시 말하면 Stream 함수를 만들기 위해서 async
를 사용하는 것이다.

import 'dart:async';

main(){
    print('start');
    
    var stream = getData();
    stream.listen((x) => print(x));
    
    print('do something');
}

Stream<int> getData() async*{
    for (int i=0; i < 5; i++){
        yield i;
    }
}

// print: start
// print: do something
// print: 0
// print: 1
// print: 2
// print: 3
// print: 4

Line6: 함수를 통해서 stream을 생성하였다.

Line12-16: 제너레이터 함수인 getData()를 구현했다. 함수의 타입이 Stream인 것에 주목할 필요가 있다.
따라서 반복적으로 생성되는 데이터를 stream으로 전달하여 listen에서 처리가 가능하다.

초반에 예시로 언급했던 가상의 동영상 스트리밍 환경에 대한 동작을 처리하는 가상의 코드는 대충 다음과 같은 것이다.

import 'dart:async';

main(){
    print('start');
    
    var stream = requestData();
    stream.listen((String x) => print(x));
    
    print('do something');
}

Stream<String> requestData() async*{
    for (int i = 1; i < 5; i++){
        await Future.delayed(Duration(seconds: 1));
        yield 'image0$1';
    }
}

// print: start
// print: do something
// print: image01
// print: image02
// print: image03
// print: image04

Line6: 서버에 데이터를 요청하는 Stream 함수를 통해서 stream을 만든다.

Line 12-17: 서버에 데이터를 요청하면 4개의 이미지가 약 1초 간격으로 하나씩 전달된는 상황을 표현한 예시 코드이다.

실제 서버가 없는 상황이라 서버에 요청할 수는 없다. 따라서 실제라면 서버에 데이터를 요청하면 4개의 이미지가 약 1초 간격으로 하나씩 전달되는 어떤 Restful API가 있어야
하고 거기서 전달받은 결과를 yield로 stream에 넣어줘야 할 것이다.

profile
Mobile App Developer

2개의 댓글

comment-user-thumbnail
2022년 6월 17일

감사합니다 :)

1개의 답글