dev/프론트엔드

코드팩토리 flutter : 0장+1장

wosrn 2024. 9. 15. 20:55

이번에 나가게 된 대회에서 플러터를 사용하게 되어서, 연휴동안 공부를 진행할 예정이다

 

https://product.kyobobook.co.kr/detail/S000200473539

 

코드팩토리의 플러터 프로그래밍 | 최지호(코드팩토리) - 교보문고

코드팩토리의 플러터 프로그래밍 | 인프런 NO. 1 플러터 강사와 함께 왕초보 실력을 현업 수준으로 끌어올리기저자는 왕초보 실력을 현업 수준으로 끌어올리기를 목표로 이 책을 썼습니다. 배운

product.kyobobook.co.kr

https://goldenrabbit.co.kr/2024/06/05/flutter-%ED%94%8C%EB%9F%AC%ED%84%B0-%EC%84%A4%EC%B9%98-%EC%9C%88%EB%8F%84%EC%9A%B0-%EB%A7%A5-%EA%B0%9C%EB%B0%9C%ED%99%98%EA%B2%BD-%EA%B5%AC%EC%B6%95%ED%95%98%EA%B8%B0-%EC%84%A4%EC%B9%98-%EB%AC%B8/

 

[Flutter] 플러터 설치 | 윈도우/맥 개발환경 구축하기, 설치 문제 해결하기 - 골든래빗

윈도우, macOS에 플러터(Flutter) 개발 환경을 설정하는 방법입니다. 운영체제 별 개발 환경 구축 방법 및 안드로이드 스튜디오 설치, 설치 중 발생하는 문제 해결하기까지 플러터를 시작하기 위한

goldenrabbit.co.kr

플러터와 안드로이드 설치+설치상 에러 해결 후 본격적으로 공부~

 

0. 다트 소개

-  컴파일 방식 : JIT는 다트 가상머신에서 제공하는 기능으로, 변경된 코드만 컴파일하는 방식 : 변경된 사항을 처음부터 다시 컴파일 할 필요 없이 즉시 화면에 반영하는 핫리로딩 기능, 실시간 메트릭스 확인기능, 디버깅 기능 제공  - 컴파일 시간을 단축시켜주므로 개발 시 유용 (개발시엔 리소스 효율보다 빠르게 개발가능한 효율이 중요)

    AOT 컴파일은 시스템에 최적화하여 컴파일하는 방식으로, 런타임 성능을 개선하고, 저장공간을 절약하고, 설치와 업데이트 시간 단축 - 배포 시 적합한 방식 (리소스 효율)

1. 기초문법

1) 메인함수

다트는 프로그램 시작점인 엔트리함수 기호로 main()을 사용한다. 

 

void는 아무 값도 반환하지 않는다는 뜻 

() 안에 매개변수 지정가능

 

2) var를 사용한 변수선언

 

다트는 변수에 값이 들어가면 자동으로 타입을 추론하는 기능을 제공함 -> 실제 코드가 컴파일 될 때 추론된 타입으로 var가 치환된다

 

3) dynamic을 사용한 변수 선언 : var는 변수의 값을 이용하여 변수의 타입을 유추 -> 한 번 유추하면 추론된 타입이 고정되므로, 고정된 타입과 다른 타입의 값을 해당 변수에 저장하려고 하면 에러 => 반면 dynamic을 사용하면 변수 타입이 고정되지 않아서, 다른 타입의 값 저장 가능

 

4) final / const : 변수 값 선언 후 변경 불가

final - 런타임 / const - 빌드타임 상수

DateTime.now() 함수는, 해당 함수가 실행되는 순간의 날짜/시간을 제공 -> 즉 실행을 해봐야 값을 알 수 있다(런타임) -> final 키워드를 사용하면 에러가 나지 않지만(추후 값 변경불가), const를 사용하면 에러가 난다! const로 지정한 변수는 빌드타임에 값을 알 수 있어야 하기 때문(DateTime.now()는 런타임이 되어야 알 수 있으니 에러)

 

즉 코드를 실행하지 않은 상태에서 값이 확정 가능하다면 const, 실행될 때 확정이 가능하다면 final을 쓰면 된다

 

5) 변수 타입 : var를 사용하면 자동으로 변수타입 유추가 가능하지만, 직접적으로 명시해주면 코드가 더욱 직관적이고 유지보수 쉬워짐

String : 문자열, int : 정수, double : 실수, bool - 불리언

 

6) 컬렉션 : 여러 값을 하나의 변수에 저장 가능 -> 여러 값 순서대로 저장(list), 특정 키값 기반으로 빠르게 값 검색(map) , 중복 데이터 제거할 때 사용(set) 

컬렉션의 장점 : 서로의 타입으로 자유롭게 형변환 가능

<List>

- List 타입 : 여러 값을 순서대로 한 변수에 저장할 때 사용, 리스트의 구성 단위를 원소라고 함

리스트명[인덱스] 형식으로 특정 원소에 접근 가능

리스트 길이는 length로 확인가능

리스트 타입에는 다트에서 기본제공하는 함수 많음

- add() : 맨 끝에 값 추가, where() : 순회하면서 조건에 맞는 값만 필터링, 매개변수에 함수를 입력/입력된 함수는 기존 값을 하나씩 매개변수로 입력받음 -> true가 반환되면 값을 유지하고 false가 반환되면 값 버림 -> 유지된 값을 기반으로 이터러블 반환

- map() : 순서대로 순회하며 값 변경, 매개변수에 함수 들어감, 매개변수의 함수가 반환하는 값이 리스트 원소의 현재값을 대체 -> 순회 끝나면 이터러블 반환

- reduce() : 이 함수 역시 list에 있는 값을 순회하면서 매개변수에 입력된 함수 실행, 다만 reduce() 함수는 순회할 때 마다 값을 쌓아감. 위의 함수들과 달리, 이터러블이 아닌 List 멤버의 타입과 같은 타입 반환 (리스트 내부의 값을 점차 더해가는 기능으로 사용)

- fold() : reduce()함수와 같은 논리로 실행되지만, reduce는 리스트를 구성하는 값들의 타입과 반환되는 리스트를 구성할 값들의 타입이 같아야하지만, fold는 어떤 타입이든 반환가능

* 이터러블 ; 추상클래스로, List나 Set 등의 컬렉션 타입들이 상속받는 클래스 (리스트나 셋 과 같은 컬렉션이 공통으로 사용하는 기능 정의해둔 클래스) / 이터러블은 where() 이나 map() 등 순서가 있는 값을 반환할 때 사용 / 리스트는 [ ] 로 출력되는 반면 이터러블은 ( ) 로 반환됨

 

where함수

 

fold 함수

fold()는 reduce()와 달리, 첫 순회 때 리스트의 첫번째 값이 아닌 fold()함수의 첫 매개변수에 있는 값이 초기값으로 사용

두번째 매개변수인

(value,element) => value + element.length

는 람다식이므로 value에는 최초 순회 때는 초기값(여기선 0)이 입력되고 이후엔 기존 순회의 반환값 입력됨. element는 reduce와 마찬가지로 리스트의 다음값이 입력됨

0

0+1 = 1

1+1 = 2

2+1 = 3

.. 해서 결국 7이 반환됨

 

<Map>

- 키와 값의 짝을 저장 -> 키를 이용하여 원하는 값을 빠르게 찾을 수 있다

 

<Set> 

- map이 키와 값의 조합이라면, set은 중복 없는 값들의 집합 (중복을 방지하므로, 유일한 값들만 존재하는걸 보장함)

Set<String> fruits = { '사과', '레몬', '수박', '포도', '복숭아','복숭아' };

위와 같이 입력해도 출력하면 복숭아가 한번만 출려됨

contains()함수로 특정 값이 있는지 없는지 확인 가능

toList() 를 통해 셋을 리스트로 변환가능

Set.form(리스트명) 을 이용하면 리스트를 셋으로 변환가능

 

*컬렉션의 진정한 장점은 셋을 리스트로, 맵을 리스트로, 리스트를 셋으로 변경하는 등 서로의 타입으로 형변환을 하며 나타남

 

7) enum : 한 변수의 값을 몇가지 옵션으로 제한하는 기능

enum Status {
approved,
pending,
rejected,
}

void main() {
Status status = Status.approved;
print(status) //Status.approved 출력
}

 

 

8) 연산자

- 기본 수치 연산자 : + - * / % 등

- null 관련 연산자 : null은 아무 값도 없음을 뜻함 (0과 다르다) / 다트언어에선 변수타입이 null값을 가지는지 여부를 직접 지정해줘야함 -> 타입키워드 그대로 사용하면 해당 변수는 null값 저장 불가 / 타입 뒤에 ? 를 추가해줘야 가능 / null을 가질 수 있는 변수에 새로운 값을 대입할 때 ?? 를 사용하면 기존 값이 null인 때만 값이 저장되도록 할 수 있음 (기존 값이 null 아니라면 새 값을 대입해도 기존 값 유지)

- 값 비교 연산자

- 타입 비교 연산자

- 논리 연산자 : &&(and), ||(or)

 

9) 제어문

- if

- switch -> case 끝에 break 키워드 사용! / enum과 함께 쓰면 유용

- for / for(int num in numList) 형식의 for문도 제공(일반적으로 리스트의 모든 값 순회하고 싶을 때 사용)

- while과 do while

 

10) 일반적인 함수의 특징

- 함수는 한번만 작성하면 여러곳에서 재활용 가능

- 반환값 없으면 void 키워드 사용

- 다트 함수에서 매개변수를 지정하는 방법 : 순서가 고정된 매개변수(포지셔널 파라미터), 이름이 있는 매개변수(네임드 파라미터) -> 포지셔널 : 입력된 순서대로 매개변수에 값 지정됨 , 네임드 : 입력순서와 관계없이, 지정하고 싶은 매개변수의 이름을 이용하여 값 입력 가능, 입력 순서 중요하지 않음(키와 값 형태로 매개변수 입력하면 되므로)

- 네임드 파라미터 지정하려면 : 중괄호 { } 와 required 키워드 사용해야

- required 키워드 ; 매개변수가 null이 불가능한 타입이라면 기본값을 지정하거나 필수로 입력해야한다는 의미

- 포지셔널 파라미터에 기본값 지정하려면 [ ] 사용

- 네임드 파라미터에 기본값 지정하려면 ; required 키워드 생략하고 등호 다음에 원하는 기본값 입력

- 포지셔널과 네임드 섞어서도 사용 가능, 이때는 포지셔널이 반드시 먼저 와야함

 

11) 익명 함수와 람다 함수

- 둘 다 함수 이름이 없고, 일회성으로 사용된다는 공통점

- 다트에선 이 둘을 구분하지 않음

- 익명함수 : (매개변수) { 함수바디 } 

- 람다함수 : (매개변수) => 단 하나의 스테이트먼트

- 익명함수에서 {}를 빼고 => 기호를 추가한게 람다함수임

- 익명함수와 달리 람다함수는 { } 가 없기에, 함수 로직을 수행하는 스테이트먼트가 단 하나여야(한 줄을 의미하는게 아니라, 명령 단위가 하나여야 - 단 하나의 연산 혹은 단 하나의 값 반환)

 

12) typedef와 함수

- 함수의 시그니처를 정의하는 값(시그니처란, 반환값 타입 혹은 매개변수 개수와 타입 등을 의미)

- 즉 함수 선언부를 정의하는 키워드이고, 함수가 무슨 동작 하는지에 대한 정의는 없음

typedef Opration = void Function(int x, int y);

void add(int x,int y) {
print('결괏값 : ${x+y}');

void main() {
Operation oper = add; //typedef는 변수의 type처럼 사용가능
oper(1,2);
}

 

다트에서 함수는 일급객체 이므로, 함수를 값처럼 사용 가능

따라서 플러터에선 typedef로 선언한 함수를 다음와 같이 매개변수로 넣어서 사용

typedef Operation = void Function(int x,int y);
void add(int x,int y) {
print('결괏값 : ${x+y}');
}

void cal(int x,int y,Operation oper) {
oper(x,y);
} //typedef로 선언한 함수를 매개변수로 넣어 함수 본문에서 사용함

 

11) try...catch

- 특정 코드의 실행을 시도해보고, 문제가 있으면 에러를 잡으라는 의미

- try 와 catch 사이의 괄호에 에러가 없을 때 실행할 로직을 작성, catch가 감싸는 괄호에 에러가 났을 때 실행할 로직 작성

- try 로직에서 에러 발생 시 이후로직은 실행 x - catch로직으로 넘어감

- throw 키워드 사용하여 에러를 고의로 발생시킬 수 있음

 

2. 다트 객체지향

- 클래스를 인스턴스화하면 (객체를 만들면) 인스턴스가 생성됨

- 클래스는 설계도, 인스턴스는 설계도를 바탕으로 만든 실제 물체

 

1) 클래스

- 클래스 안에, 해당 클래스에 종속되는 변수와 함수 지정가능

- this 키워드는 클래스에 종속되어있는 값 지칭 시 사용 (스코프 안에 같은 속성 이름 하나만 존재한다면 생략가능)

- 인스턴스 생성 : 클래스명 인스턴스명 = 클래스명();

- 인스턴스의 함수 실행시엔 . 사용

 

2) 생성자

- 클래스와 같은 이름

- 생성자의 매개변수에 해당하는 변수의 값을, 외부에서(인스턴스를 생성할 때) 입력 가능

- 생성자에서 입력받을 변수는 일반적으로 final로 선언 (인스턴스화 후 변수 값 변경하는 실수 막기위해)

- 생성자는 기본적으로 클래스명(매개변수) : this.변수명 = 매개변수 ; 의 형식이지만, 클래스명(this.변수명) 으로도 가능

- 네임드 생성자 : 네임드 파라미터와 유사한 개념 - 일반적으로 클래스를 생성하는 여러가지 방법을 명시하고 싶을 때 사용

class Idol {
final String name;
final int membersCount;

//생성자
Idol(String name, membersCount)
: this.name = name,
  this.membersCount = membersCount;

Idol(this.name, this.membersCount) //위와 동일
  
//네임드 생성자 
//클래스명.네임드생성자명 형식
Idol.fromMap(Map<String, dynamic> map)
 : this.name = map['name'],
 : this.membersCount = map['membersCount'];
}

//사용시
Idol bts = Idol.fromMap({'name' : 'bts', 'membersCount' : 7});

 

3) private 변수

- 일반적으로 private변수는 클래스 내부에서만 사용하는 변수를 칭하지만, 다트 언어에서는 같은 파일에서만 접근 가능한 변수를 의미함

- 일반적으로 클래스를 선언하는 파일과 사용하는 파일이 다른데, private으로 선언하면 다른 파일에선 접근불가

- 변수명을 _ 기호로 시작해서 선언

 

4) getter / setter 

- getter : 값 가져올 때, setter : 값 세팅 시 사용

- getter setter를 사용하면 어떤 값이 노출될 지, 어떤 형태로 노출될 지, 어떤 변수를 변경 가능하게 할 지 유연하게 정할 수 있음

- 최근엔 객체지향 프로그래밍을 할 때 변수의 값을 불변성 특성으로 사용하기 때문에 세터는 거의 쓰지 않지만 게터는 종종 사용

- getter 는 get 키워드를, setter는 set 키워드를 사용하여 명시

String get name {
return this.name;
}

set name(String name){
this.name = name;
}

//외부 파일에서 getter setter 사용
blackPink.name = '블핑'; //setter
print(blackPink.name) //getter

 

- private를 외부 파일에서 직접 접근할 수는 없지만, getter를 선언해두면 getter를 통해 외부에서도 간접적으로 접근가능

- getter setter는 모두 변수처럼 사용할 수 있고, 사용할 때 메소드명 뒤에 () 붙이지 않음

 

5) 상속

- extends 키워드 이용하여 상속

- class 자식클래스명 extends 부모클래스명

- 자시클래스는 부모크래스의 함수와 변수 상속받음

- super 키워드 자주 사용 (상속한 부모클래스를 지칭)

- 부모 클래스에 기본 생성자가 있다면 자식클래스에서도 부모클래스의 생성자 실행해줘야함

- 상속받지 않은 변수나 함수도 자식클래스에서  추가로 생성 가능

class BoyGroup extends Idol {

BoyGroup(String name,int membersCount) : super(name,membersCount);

}

 

 

6) 오버라이드

- 부모 클래스 또는 인터페이스에 정의된 메서드를 재정의할 때 사용됨

- 부모클래스에 이미 존재하는 메소드명을 또 선언하면 자동으로 override로 인식되지만, 협업 및 유지보수를 위해선 직접 명시하는게 좋음

class GirlGroup extends Idol {


//생성자
GirlGroup (
super.name,
super.membersCount
);

@override
void sayNmae() {
print( " " );
}

}

 

 

7) 인터페이스

- 상속은 공통된 기능을 상속받는 개념이라면, 인터페이스는 공통으로 필요한 기능을 미리 정의만 해두는 역할

- 인터페이스는 implements 키워드로 사용함

- 상속은 단 하나의 클래스만 가능하지만, 인터페이스는 적용개수에 제한 없음

- 상속받을때는 부모 클래스의 모든 기능이 우선 부모단에서 완성되어 상속되므로 재정의를 꼭 할 필요는 없지만, 인터페이스의 경우는 모든 기능을 다시 정의해줘야함 (추상메소드 아니라도 모두 재정의해야)

- 따라서 인터페이스는 개별 클래스에서 다시 정의할 필요가 있는 기능들을 정의하는 용도

- interface라는 키워드를 쓰진 않지만, 어떤 클래스든 implements 키워드를 통해 인터페이스로 사용 가능 

 

8) 믹스인

- 특정 클래스에 원하는 기능들만 골라넣을 수 있는 기능

- 특정 클래스를 지정해서 속성들을 정의할 수 있음

- 지정한 클래스를 상속하는 클래스에서도 사용 가능

- 인터페이스 처럼, 하나의 클래스에 여러개의 믹스인 적용가능

- mixin 선언 시 on 없이 선언하면, 모든 클래스에서 해당 기능 갖다쓸 수 있음

mixin IdolSingMixin on Idol{
 void sing() {
 	print("idol의 추가기능 sing");
   }
}

class BoyGroup extends Idol with IdolSingMixin {
BoyGroup(super.name, super.membersCount);

}

 

9) 추상

- 상속이나 인터페이스로 사용되는 데 필요한 기능을 정의 만 하고, 인스턴스화는 할 수 없도록 하는 기능

- 특정 클래스를 인터페이스로만 사용하고 이를 인스턴스화할 일이 없다면, 추상클래스로 선언해서 인터페이스화를 방지하고 메소드 정의를 자식 클래스에 위임 가능

- 추상클래스는 추상메소드 선언 가능 : 함수의 반환타입, 이름, 매개변수만 정의하고 바디선언을 자식이 필수로 하도록 강제

- 추상클래스에선 변수 선언, 생성자 선언, 추상메소드 선언까지만 함 -> 구현은 이를 implements한 자식클래스에서

** 인터페이스는 별도 키워드 없이 선언하지만, 만약 인터페이스에서 추상메소드를 선언하고싶다면 abstract 키워드는 붙여줘야함

 

* extends 와 implements 비교

1. extends (상속)

  • 부모 클래스의 모든 메서드와 속성(필드)을 그대로 상속받습니다.
  • 자식 클래스는 부모 클래스의 기능을 **재정의(override)**하거나 확장할 수 있습니다.
  • 부모 클래스의 구현된 메서드는 재정의하지 않아도 자동으로 자식 클래스에서 사용 가능합니다.
  • Dart에서는 단일 상속만 가능하므로 한 번에 하나의 클래스만 상속받을 수 있습니다

2. implements (구현)

  • implements는 인터페이스처럼 사용됩니다. 이를 사용하는 클래스는 해당 클래스의 메서드 시그니처만 가져오고, 그 구현모두 직접 해야 합니다.
  • 부모 클래스의 구현된 메서드의 바디는 상속되지 않으며, 반드시 재정의해야 합니다.
  • Dart에서는 여러 클래스를 구현할 수 있으므로 다중 인터페이스를 사용할 수 있습니다.

 

10) 제네릭

- 클래스나 함수의 정의를 선언 시가 아니라 인스턴스화 혹은 실행시로 미룬다

- 특정 변수의 타입을 하나로 제한하고 싶지 않을 때 사용

- 예를들어 정수를 받는 함수, 문자열을 받는 함수를 각각 따로 안만들고 set()하나로 여러 자료형을 추후 입력받을 수 있게 만들 수 있다

- Map,List,Set 등에 사용된 < > 사이의 값이 제네릭 문자임 : List클래스는 제네릭이므로, 인스턴스화 하기 전엔 어떤 타입으로 List가 생성될 지 알지 못한다

class Cache<T> {
final T data;

//생성자, 네임드 매개변수 사용
Cache({
requied this.data,
});
}

//Cache의 T 타입을 List<int>로 입력하여 인스턴스화
void main() {
final cache = Cache<List<int>>(
data : [1,2,3],
);

- 주로 변수명은 T, 리스트 내부 요소들은 E, 키는 K, 값은 V를 약자로 사용

 

11) static (정적 변수)

- 지금까지 작성한 변수와 메소드들은 '클래스의 인스턴스'에 귀속되었지만, static 키워드를 사용하면 클래스 자체에 귀속됨

- 인스턴스들끼리 공유해야하는 정보가 있을 때 사용

 

12) Cascade 연산자

- 인스턴스에서 해당 인스턴스의 속성 혹은 멤버 함수를 연속하여 사용하는 기능

- .. 기호를 사용

- 여러 메서드 호출 또는 속성 접근을 체이닝하여, 객체를 여러 번 참조하지 않고도 일련의 작업을 수행가능

class Person {
  String name;
  int age;

  Person(this.name, this.age);

  void celebrateBirthday() {
    age++;
    print('Happy Birthday, $name! You are now $age years old.');
  }

  void printDetails() {
    print('Name: $name, Age: $age');
  }
}

void main() {
  var person = Person('Alice', 30);

  // Cascade 연산자를 사용하지 않은 경우
  person.celebrateBirthday();
  person.printDetails();

  // Cascade 연산자를 사용한 경우
  person
    ..celebrateBirthday()
    ..printDetails();
}

 

 

 

3. 다트 비동기 프로그래밍

 

1) 동기 vs 비동기 프로그래밍

- 위의 코드들은 모두 동기방식 : 함수를 실행하면 다음 코드가 실행되기 전에 해당 함수의 결괏값이 먼저 반환됨

- 반면 비동기는 요청 결과를 기다리지 않고 다음 코드 실행, 응답 순서가 요청 순서와 다를 수 있음 -> 컴퓨터 자원 낭비하지 않고 더 효율적으로 사용 가능

 

2) Future 

- Future 클래스는 '미래'라는 단어의 의미대로, 미래에 받아올 값을 뜻함

- List처럼 제네릭으로 어떤 미래의 값을 받아올 지 정할 수 있음

Future<String> name; //미래에 받을 string값

 

- 비동기 프로그래밍은 서버요청과 같이 오래 걸리는 작업을 기다린 후에 값을 받아와야 하기 때문에, 미래에 받아올 값을 표현하는 future클래스가 필요함

void main {
 addNumbers(1,1);
}

void addNumbers(int num1,int num2){
print('$num1 + $num2 계산 시작!');

Future.delayed(Duration(seconds : 3), () {
print('$num1 + $num2 = ${num1 + num2}');
});

print('코드 실행 끝');
}

 

- 위의 코드를 실행하면, 계산 시작 -> 코드 실행 끝 -> 1+1=2 순으로 출력된다

- Future.delayed() 함수는 일정 시간 이후 콜백함수를 실행하는 비동기 함수 -> 대기 시간동안 아무것도 안하는게 아니라 다음 작업을 실행함

** 콜백 함수는 다른 함수의 인자로 전달되어 특정 작업(위 예제에선 3초 대기)이 끝난 후 실행되는 함수 

 

다트의 Future 클래스는 비동기 작업을 처리할 때 사용되며, Flutter 앱 개발에서도 비동기적인 작업이 필요할 때 자주 사용됩니다. Future를 활용하면 앱이 멈추거나 중단되지 않고 동시에 여러 작업을 처리할 수 있습니다.

예) api 통신

앱이 서버와 통신하거나 API에서 데이터를 가져올 때는 시간이 걸릴 수 있습니다. 이때 Future를 사용하여 비동기적으로 데이터를 요청하고, 응답이 올 때까지 다른 작업을 계속 수행할 수 있습니다.

Future<void> fetchData() async {
  var response = await http.get('https://example.com/api/data');
  print(response.body); // 데이터를 받아온 후 출력
}

 

- **Future.wait()함수는 하나의 future로 구성된 리스트를 매개변수로 입력받음 / 여러 비동기 작업을 병렬로 실행한 후, 모든 작업이 완료될 때까지 기다리는 기능을 제공. 즉, 여러 개의 Future 객체를 동시에 실행하고, 그 중 하나라도 완료될 때마다 결과를 받는 것이 아니라, 모든 Future가 완료된 후에 결과를 얻는다. 만약 Future.wait에 전달된 Future 중 하나라도 오류가 발생하면, Future.wait는 즉시 오류를 반환하고 나머지 Future가 어떻게 되든 상관없이 중단된다.

Future.wait의 사용 목적

  • 병렬로 실행 가능한 비동기 작업이 있을 때, 모든 작업을 한꺼번에 실행하고, 모두 완료될 때까지 기다리기 위해 사용합니다.
  • 각 작업의 실행 순서가 상관없고 독립적이라면, Future.wait로 여러 작업을 병렬 처리함으로써 성능을 최적화할 수 있습니다.
  • 병렬로 실행 가능한 작업들을 처리할 때 매우 유용하며, 비동기 작업이 완료된 후 결과를 리스트 형태로 반환합니다.
  • 기본 문법
Future.wait([
  future1, 
  future2, 
  future3
]);

Future.wait는 Future 객체들의 리스트를 인자로 받습니다.
리스트에 있는 모든 Future가 완료될 때까지 기다린 후, 완료된 결과를 리스트 형태로 반환합니다.

Future<void> fetchData() async {
  Future f1 = Future.delayed(Duration(seconds: 2), () => print('작업 1 완료'));
  Future f2 = Future.delayed(Duration(seconds: 1), () => print('작업 2 완료'));
  Future f3 = Future.delayed(Duration(seconds: 3), () => print('작업 3 완료'));

  // 세 작업이 모두 끝날 때까지 기다림
  await Future.wait([f1, f2, f3]);

  print('모든 작업 완료');
}

void main() {
  fetchData();
}
//
세 개의 비동기 작업이 병렬로 시작됩니다.
작업 2가 가장 먼저 완료되고, 그다음 작업 1, 마지막으로 작업 3이 완료됩니다.
모든 작업이 완료된 후에 Future.wait가 끝나고 "모든 작업 완료"가 출력됩니다.
Future.wait의 특징
병렬 실행: 리스트에 있는 모든 Future들이 병렬로 실행됩니다. 각 Future가 동시에 실행되며, 완료된 순서에 상관없이 모든 작업이 끝날 때까지 기다립니다.

모든 Future가 완료될 때까지 대기: 모든 Future가 완료되기 전까지는 Future.wait 자체가 완료되지 않습니다.

결과 반환: 모든 Future가 성공적으로 완료되면, 결과 리스트를 반환합니다. 만약 반환값이 있는 작업이라면, 해당 작업들의 결과 값이 리스트 형태로 반환됩니다.
Future<int> task1() async {
  await Future.delayed(Duration(seconds: 2));
  return 1;
}

Future<int> task2() async {
  await Future.delayed(Duration(seconds: 1));
  return 2;
}

Future<int> task3() async {
  await Future.delayed(Duration(seconds: 3));
  return 3;
}

Future<void> main() async {
  List<int> results = await Future.wait([task1(), task2(), task3()]);
  print(results);  // [1, 2, 3]
}

 

- 위의 예제를 통해 볼 수 있듯, Future.wait는 응답값들을 요청을 보낸 순서대로 저장해두었다가 반환한다! (작업이 완료된 순서와 상관없이 요청을 보낸 순서대로 결과를 반환)

 
 

 

3) async와 await

- 비동기 프로그래밍을 하면서도 가독성을 유지하는 방법

- async : async는 함수가 비동기임을 의미. 즉, 이 함수가 호출되면 즉시 실행되는 것이 아니라 Future 객체를 반환하고, 그 안에서 비동기 작업을 처리할 수 있음.

- await는 비동기 작업이 완료될 때까지 기다림을 나타냄. await를 사용하면 특정 작업이 끝날 때까지 코드가 멈추고, 작업이 끝난 후 다음 코드가 실행됨 (비동기 함수를 논리적 순서대로 실행가능)

void main {
 addNumbers(1,1);
}

//async는 함수 매개변수 정의와 바디 사이에 입력
Future<void> addNumbers(int num1, int num2) async {
print('$num1 + $num2 계산 시작!');

//await는 대기하고 싶은 비동기함수 앞에 입력
await Future.delayed(Duration(seconds : 3), () {
print('$num1 + $num2 = ${num1 + num2}');
});

print('코드 실행 끝');
}

 

- 위처럼 코드를 입력하면, 2)에서의 코드와 달리 계산시작->1+1=2->코드 실행 끝 순으로 출력된다

- 즉 함수를 async로 지정해주고 나서, 대기하고 싶은 비동기함수를 실행할 때 await를 사용하면 코드를 작성한 순서대로 실행됨

- 이렇게 되면 이것은 비동기가 아니라 동기가 아니냐고 생각할 수 있지만, async와 await는 비동기 프로그래밍 특성을 그대로 유지하며, 코드가 작성된 순서대로 프로그램을 실행함

void main() {
 addNumbers(1,1);
 addNumbers(2,2);
 }
 
 //위의 코드는,
 1+1 계산 시작
 2+2 계산 시작
 1+1 = 2
 1+1 코드 실행 끝
 2+2 = 4
 2+2 코드 실행 끝
 의 순으로 출력됨

 

- addNumbers() 함수가 비동기 함수 이기 때문에, addNumbers(1,1)이 끝나기 전에(3초 대기중에) addNumbers(2,2)가 시작된 것을 볼 수 있음

- 그 후 3초 대기가 끝나자 1+1=2를 실행하고 코드 실행 끝을 출력함

- 만약  addNumbers(1,1)과 addNumbers(2,2)가 순차적으로 실행되길 원한다면, 

void main() async {
 await addNumbers(1,1);
 await addNumbers(2,2);
 }

 

 

- async와 await를 사용하면, 비동기이긴 하지만 해당 함수 내부에선 await가 있는 줄 다음줄을 await가 있는 줄의 작업을 끝내기까지 실행하지 않음으로서, 비동기이긴 하나 순서를 지키는 원리. 즉 비동기적인 작업을 처리하면서도 코드가 마치 동기적으로 실행되는 것처럼 보이게 만드는 것

1. await 가 있는 줄의 작업 기다림 (future.delayed 실행 후에 2초 후 실행이라는 문구 출력)

Future<void> example() async {
  print("작업 시작");

  // 비동기 작업이 끝날 때까지 기다림
  await Future.delayed(Duration(seconds: 2));

  print("2초 후 실행");
}

 

2. 비동기이지만 내부적 순서를 지킴

Future<void> fetchData() async {
  print("데이터 로드 시작");

  await Future.delayed(Duration(seconds: 2));  // 2초 대기

  print("데이터 로드 완료");
}

 

위 코드에서는:

  1. 비동기 작업이 시작되고 (await를 사용해 2초를 기다림)
  2. 그 다음 줄인 **"데이터 로드 완료"**는 비동기 작업이 끝난 후에 실행된다.

즉, 코드의 실행 순서가 보장되는 동시에, 비동기 작업이 수행되는 것

 

3. 비동기 함수의 비동기성 유지

하지만 함수 자체는 비동기로 작동한다!  즉, 이 fetchData() 함수를 호출한 외부 코드는 await에 의해 중단되지 않습니다. 예를 들어:

Future<void> main() async {
  print("시작");

  fetchData();  // 여기서 기다리지 않고 다음 줄 실행

  print("끝");
}

 

이 경우, fetchData() 함수는 2초 대기 중이어도, main() 함수의 "끝"이 즉시 출력된다. 물론 await를 사용해서 fetchData()의 완료를 기다리게 할 수는 있음

 

요약

  • **await**는 비동기 작업이 끝날 때까지 해당 줄에서 대기하고, 그 이후 코드를 실행
  • 비동기 작업이 진행되더라도 코드의 실행 순서가 유지
  • 함수 자체는 비동기적으로 작동하므로, 외부에서 보면 함수가 완료될 때까지 다른 작업을 수행할 수 있음

따라서 **async와 await**는 비동기 작업의 효율성코드의 가독성을 동시에 유지할 수 있는 매우 유용한 도구

 

 

4) 결괏값 반환하기

- async 와 await 키워드를 사용한 함수에서도 결괏값을 받아낼 수 있고, 이때 앞서 배운 future 클래스를 활용함

void main async{
 final result = await addNumbers(1,1);
}

//async는 함수 매개변수 정의와 바디 사이에 입력
Future<int> addNumbers(int num1, int num2) async {
print('$num1 + $num2 계산 시작!');

//await는 대기하고 싶은 비동기함수 앞에 입력
await Future.delayed(Duration(seconds : 3), () {
print('$num1 + $num2 = ${num1 + num2}');
});

print('코드 실행 끝');

return num1+num2;
}

 

 

5) Stream

- 스트림(Stream)은 Dart에서 연속적으로 발생하는 비동기 이벤트를 처리하는데 사용되는 데이터 구조

- 스트림은 데이터의 흐름을 표현하고, 여러 데이터 항목을 비동기적으로 처리할 수 있도록 해줌

- 스트림을 사용하면 시간이 지남에 따라 발생하는 여러 이벤트를 처리하거나, 지속적으로 데이터를 받아 처리하는 애플리케이션을 만들 수 있다

- Future는 반환값을 딱 한 번 받아내는 비동기 프로그래밍에 적합 

- 지속적으로 값을 반환받을 때는 Stream을 사용함

- Stream은 한번 listen하면, Stream에 주입되는 모든 값들을 지속적으로 받아옴

 

<기본 사용법>

//stream 사용하려면 dart:async 패키지를 불러와야 한다
import 'dart:async' ;

void main() {

final controller = StreamController(); //streamController 선언
final stream = controller.stream; //stream 가져오기

//stream에 listen함수를 실행하면 값이 주입될 때마다 콜백함수 실행 가능
final streamListener1 = stream.listen((val) {
print(val);
});

//stream에 값 주입 -> 이때마다 print 실행됨
controller.sink.add(1);
controller.sink.add(2);
controller.sink.add(3);
controller.sink.add(4);

 

 

<브로드캐스트 스트림> : 하나의 스트림을 생성하고 여러 번 listen()함수를 실행하고 싶을 때 사용

//stream 사용하려면 dart:async 패키지를 불러오야 한다
import 'dart:async' ;

void main() {

final controller = StreamController(); //streamController 선언
final stream = controller.stream.asBroadcastStream(); //여러번 리슨 가능한 브로드캐스트 스트림 객체 생성

//첫 리슨함수
final streamListener1 = stream.listen((val) {
print(val);
});

//두번째 리슨함수
final streamListener1 = stream.listen((val) {
print('listen2')
print(val);
});

//stream에 값 주입 -> 이때마다 listen()하는 모든 콜백함수에 값 주입됨
controller.sink.add(1);
controller.sink.add(2);
controller.sink.add(3);
controller.sink.add(4);

 

 

6) 함수로 stream 반환하기

- streamController 를 사용하지 않고도 직접 Stream을 반환하는 함수를 작성할 수도 있음

- future를 반환하는 함수는 async로 함수를 선언하고 return 키워드를 이용했었음, stream을 반환하는 함수는 async*로 함수를 선언하고 yield 키워드로 값을 반환하면 된다

  1. main 함수가 실행되면서 playStream 함수가 호출됨
  2. playStream 함수에서 cal(1)을 호출하여 스트림을 반환받음
  3. cal(1) 함수는 1초 간격으로 "i = 0", "i = 1", "i = 2", "i = 3", "i = 4"를 스트림에 전송
  4. playStream에서 listen 메서드를 사용하여 스트림을 구독하고, 각 데이터 이벤트가 발생할 때마다 print(val)으로 출력
import 'dart:async' ;

Stream<String> cal(int number) async* {
for(int i=0;i<5;i++) {
yield 'i = $i'; //스트림에 값 전달
await Future.delayed(Duration(seconds :1)); //각 루프마다 1초씩 대기하여, 스트림에 값이 1초 간격으로 전달되도록
}
}

//이 함수는 cal(1)을 호출하여 스트림을 구독(listen)함
listen 함수는 스트림에서 값이 전달될 때마다 실행될 콜백 함수를 등록합니다. 여기서는 전달된 값인 val을 출력하는 코드
즉, cal(1)이 스트림을 생성하고, playStream()이 해당 스트림을 구독하여 값을 실시간으로 출력하는 역할
void playStream() {

cal(1).listen((val) {
print(val);
});
}



void main() {
playStream();
}