flutter create를 통해 프로젝트를 생성하면 아래 사진과 같은 구조로 파일구조가 생성되게 된다. 

파일구조를 살펴보자 

 

제일먼저 lib 폴더는 프로그램에서 사용되는 dart파일을 넣어두는 폴더이다.(사진에서도 main.dart가 포함되어있다)
실제 여기서 여러 폴더구조를 만들고 파일을 생성하여 코딩을 하는 폴더이다 

 

다음 android, ios, linux, macos, web폴더인데 각각 디바이스에서 사용할 수 있는 센서등을 사용할때
native언어로 코드를 작성해서 사용하면 된다고 한다 

 

test폴더는 TDD를 위해 lib폴더의 테스트를 생성하기 위한 폴더이다 

 

다음으로 pubspec.yaml은 프로젝트 구성에 대해 선언이 있는곳인데, SDK버전이나, 사용할 라이브러리등을 선언한다
사용방법은 pub.dev에서 선언되있는 dependency를 긁어서 복붙하거나 flutter pub add명령어를 사용해서 라이브러리를 추가하는 방식이다 

 

그 외 파일들은 딱히 프로젝트 사용에 있어 건드릴 필요가 없어 소개는 여기까지 하기로 한다. 

비동기 프로그래밍이란?

 

일단 동기(Syncronous)는 무엇이고 비동기(Asyncronous)는 무엇인가?

  -> 동기는 blocking되는 코드 -> 이전 명령이 안끝나면 다음명령이 실행 되지 않음

  -> 비동기는 blocking되지 않는 코드 -> 이전 명령이 안끝나도 다음명이 실행 됨 

 

왜 동기적인 작업이 문제가 될까? 예를들어

  -> 파일을 불러올때

  -> 데이터베이스에 데이터를 쓸때

  -> 네트워크로부터 데이터를 불러올때

등등 여러 케이스에서 한 작업이 끝날때까지 뒤의 모든 작업이 기다리게 된다. 

이럴 경우 시간적으로 손해가 크기때문에 동기화된 작업은 추천 되지 않는다 

 

그렇다면 비동기는? 작업이 완료될때까지 기다리지 않고 다음 작업을 바로 실행함(병렬적)

그렇다면 결과값은? 작업이 마무리되면 그때 데이터를 받음 

 

dart에서는 dart.async 라이브러리를통해 Future, Stream기능, 비동기 프로그래밍을 지원하게 된다 

 

Future을 알아보자

Future을 사용할 때 반환 되는 타입은 Future타입이다. Future타입안에 데이터타입이 명시 된다. 

Future<int> number = Future.value(1);
Future<String> name = Future.value('스트링');
Future<bool> isTrue = Future.value(true);

 

언제 쓰일까?

  -> 하나의 작업에 대해 이벤트가 한번 발생하는 단일 비동기 작업에 사용된다. 

void main(){
  int seconds = 2;
  print('start');
  Future.delayed(Duration(seconds: seconds), () {
    print("waited $seconds");
  });
  print('done');
}

// 출력되는 결과
/*
  start
  done
  waited 2
*/

 

비동기 작업을 동기화 처럼 사용하고자 할때(순서를 지켜서 실행해야할때) async, await키워드를 사용해서 코드를작성한다 

void main() async{
  int seconds = 2;
  print('start');
  await Future.delayed(Duration(seconds: seconds), () {
    print("waited $seconds");
  });
  print('done');
}

// 출력되는 결과
/*
  start
  waited 2
  done
*/

 

하지만 Future은 단 한번만 결과값을 받게 된다. 그래서 Stream을 사용한다

 

Stream에 대해 알아보자

제일 큰 특성은 시간에 따라 연속적인 데이터 흐름을 제공한다

연속적으로 데이터를 받을 수 있기 때문에 프로그램이 임의로 Stream을 종료하지 않는다

사용자가 Stream을 시작하고 끝을 내야한다.

 

예제로 알아보기전에 yield와 listen() method를 알아야 한다 

 

yield는 값을 방출하도록 하는 키워드 -> return 과 비슷하다고 생각하면 된다

listen()은 yield로 방출되는 값을 얻기위해 사용되는 메서드다

 

예제로 알아보자 

참고로 Stream을 적용한 함수에는 async* 키워드를 달아주어야한다 async가 아니다 

아래 예제는 입력받은 숫자부터 0까지 1초를 기다리면서 출력한는 예제이다 

Stream<int> emitNUmbers(int first) async* {
  for(var i = first; i >= 0; i--){
    yield i;
    
    await Future.delayed(Duration(seconds:1));
  }
}

void main() {
  emitNumbers(10).listen((number) {
    print(number);
  }
}

/*
  10
  9
  8
  7
  6
  5
  4
  3
  2
  1
  0
  이 차례대로 출려 됨 
*/

 

참고로 고차함수로 체이닝하여 사용할 수도 있다고 한다. 

체이닝을 사용하면 특정 값만 걸러내서 받을 수도 있을거같다. 

예외(Exception)에 대해서 알아보자

 

정의는 프로그램 실행동안 발생할 수 있는 예외적인 상황에서 발생하는 Exception 객체

 

종류에는 2가지로 나누어질 수 있다

  -> Dart가 사전정의한 예외. 자주 발생하는 예외로는 아래와 같이 있다

    -> DeferredLoadException - 라이브러리 사용시점에 라이브러리가 로드 되지 않았을떄

    -> FormatException - 타입이 서로 안맞을때(int를 String에 넣는다거나..)

    -> IOException - 입출력 관련 동작중 발생하는 예외(FIleDecriptor, socket, protocol등등..)

    -> OSError - 운영체제 레벨에서 발생하는 예외, 운영체제 관련해서 에러코드를 가지고 있다 

    -> TimeoutException - 비동기 결과 돌릴때 시간이 넘 오래걸리거나 할때 발생하는 예외 

  -> 사용자가 정의한 예외

    -> 위 예외에 없을때 사용자가 정의한 모든 예외 

    -> Exception클래스를 상속받아서 아래와 같이 예외 클래스를 만들면 됨. 

// 예외 클래스 생성
class AgeException implements Exception {
  final String message;
  
  AgeException(this.message);
  
  void printAge(int age){
    if (age < 0){
      throw AgeException(null);
    }
  }
  @override
  String toString() => messsage ?? 'AgeException 발생';
}

// 실제 예외 발생 코드
void main(){
  try {
    printAge(-8);
  } catch (e){
    print(e);
  }
}

 

구문을 좀더 자세히 살펴보면

  -> try - 시도하고자 하는 코드 

  -> catch - 발생한 예외를 잡는 코드 

  -> on - catch와 같이 쓰이며 try에서 발생하는 특정타입의 예외를 다루기 위해 사용

on IOException catch(e){
  ...
}

  -> finally - 예외 발생과 상관없이 무조건 마지막에 실행되는 구문

finally{
  ...
}

4개로 나누어진다 

 

마지막으로 throw가 있는데 특정 exception을 발생시키는 구문이다

throw [Exception이름];

 

위와 같이 예외를 발생 시킬수 있다 

클래스간 상속에 대해서 간단하게 배워보자

 

정의는?

  기존 클래스의 기능을 확장하여 새로운 클래스를 만드는것

  하나의 클래스가 다른 클래스의 속성과 메서드를 물려받는것 

  물려주는 클래스는(Super, Parent)라고 부르고 물려받는 클래스는(Sub, Child)라고 부른다

 

어떻게?

  extends 키워드를 사용해서 상속 받는다. 아래 예재가 있다 

class Person{
  void eat(){
    print("냠냠");
  }
}

class Student extends Person {
  void Stury(){
    print('열공');
  }
}

 

특징?

  super키워드를 통해 자식클래스가 부모 클래스의 속성과 메서드를 사용할 수 있다

  자식 클래스는 상속받은 속성과 메서드를 재정의(Overriding)하거나 기능을 확장 할 수 있다(이때 super를 사용한다)

 

특이사항으로는 클래스를 만들때 final 키워드를 붙이면 자식클래스가 해당 클래스를 상속할 수 없다 

클래스는 개념이 좀 많다

알아보자

 

클래스란? 

  -> 객채의 구조와 동작을 정의하는 틀

객체는?

  -> 클래스에서 정의한 구조를 바탕으로 생성된 실제 엔티티 혹은 데이터

 

클래스를 생성하려면 다음과 같이 정의하면 된다

아래 코드는 Person 클래스를 정의하고 있다 

속성으로 name, int를 가지며 생성자를 통해 초기화 하고, method로 introduce를 정의하였다 

// class [클래스이름] { ... }

class Person {
  String name;
  int age;
  
  Person(this.name, this.age);
  
  void introduce() {
    print("안녕, 나이는 $age, 이름은 $name");
  }
}

 

아래코드는 실제로 Person클래스를 사용해서 Person객체를 생성하는 코드이다 

void main(){
  Person p1 = Person('john',24);
  p1.introduce(); // 안녕, 나이는 24, 이름은 john 출력
}

 

클래스는 다음과 같은 구성요소들로 이루어져 있다. 이 같은 구성요소를 클래스의 맴버라고 한다

  -> 속성

    -> 인스턴스 변수(instance variable)

      => 객체에 속해있는 global scope 변수

    -> 지역변수(local variable)

      => method 코드블럭 안에 scoped된 변수

    -> 정적변수(static variable)

      => 클래스 변수라고도 한다 - 객체에 종속되지 않고 클래스 자체에 속하는 변수

      => 같은 클래스를 가지는 객체는 이 변수를 공유한다. 

      => static키워드로 선언한다 

      => Class이름으로 접근한다( Person.[static변수이름] 이런식으로)

      => 따라서 객체를 통해 접근할 수 없다

      => 값을 변경할수 있는데 *매우매우* 조심해서 변경하자. 

 

-> method

    -> 객체의 동작응 정의한다(속성을 변경하거나, 정보를 프린트하거나 등)

    -> 함수와 method차이? 함수는 클래스에 의존하지 않는다 

 

    -> 종류에는 2가지 인스턴스 메서드, 정적 메서드로 나누어진다

    -> 인스턴스 메서드(Instance Method)

      => 객체에 속해있는 method

      => this를 통해 접근 가능함

      => 클래스의 모든 곳에서 접근할 수 있다(scoped)

    -> 정적 메서드(static Method 혹은 class method)

      => method 앞에 'static' 키워드를 붙여야 한다

      => 역시 변수와 마찬가지로 클래스 이름으로 접근한다.

      => *중요* 내부에서 인스턴스 변수 사용 금지 

 

  -> 생성자(constructor)

    -> 기본생성자(default constructor)

      => 자동으로 생성되는 생성자이다 

      => 인스턴스 변수가 모두 초기화 되어있는 상태여야 한다.     

    -> 매개변수 생성자(parameterized constructor)

      => [클래스](this.인스턴스변수);

      => [클래스]([타입] [매개변수]) : this.인스턴스변수;

      => [클래스]([타입] [매개변수]) { this..인스턴스변수; }

      => 위 3가지로 매개변수 생성자를 쓸 수 있다. 

    -> 네임드 생성자(Named constructor)

       => 클래스 method와 같은 형식으로 호출하는 생성자. 예제는 아래와 같다(처음보는 형식이라 적어둠)

Class Car {
  String name;
  List<String> models;
  
  Car.fromList(List values)
    : this.name = values[0],
      this.models = values[1];
}

 

    -> this는 현재 객체를 가르키는 키워드이다. 

      -> this 를 사용해서 맴버 변수와 method에 접근하고 사용할수 있다

함수형 프로그래밍에 대해서 배워보자

 

첫번째 정의는 함수의 연속으로 프로그램을 구성하는 것이라고 생각하면 된다고 한다 

Method Chaining이라고도 한다고 한다 

  - ' . ' 을사용해서 여러개의 함수를 하나로 연결하는 방식

 아래 코드와 같이 여러 method 를 연속적으로 사용하는 방식

var result = number.abs().toString();

 

두번째 정의는 가변적인 데이터의 사용을 최소화하여 프로그램을 구성하는 방식이라고 하는데 

예제로 살펴보자 

아래와 같이 매개변수에만 의존하는 함수의 형태를 Pure FUnction(순수 함수)라고 한다 

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

또다른 예제로는 아래와 같은 함수도 있다

잘 보면 매개변수를 의존하여 값을 계산하기 때문이다(스코프 안에서만 변경한다)

int getTotal(List<int> numbers) {
  int result = 0;
  for (var number in numbers){
   result += numbers;
  }
  return result;
}

 

함수형 프로그래밍에서 많이 사용하는 함수에는 2가지가 있다

  - 형변환 함수(Type Casting Function)

    -> 특정 데이터를 다른 타입의 데이터로 변경하는 함수(toString(), int.parse('')같은 함수)

  - 고차 함수(Higher-order function)

    -> map(), where(), reduce(), fold(), any(), every(), takeWhile()등  여러가지 종류가 있으며 중요한 몇가지 함수를 알아보자

   

      -> map()은  Collection타입의 데이터 각요소에 특정 함수를 적용한 새로운 collection을 반환하는 함수이다(shallow copy)

      -> 예제는 아래 코드와 같다. List의 모든 요소에 특정 작업을 해서 새로운 List를 반환하고 있다. 

//map(([매개변수]) { return [동작] });
List<String> fruitList = ['a', 'b', 'c'];
var delicious =. ruitList.map((fruit) {
  var word = '맛있는';
  word += fruit;
  reutrn word;
});
print(delicious); // 맛있는 a, 맛있는 b, 맛있는 c 출력

//map(([매개변수]) => [동작]); 
List<String> fruitList = ['a', 'b', 'c'];
var delicious = fruitList.map((fruit) => '맛있는 + $fruit');
print(delicious); // 맛있는 a, 맛있는 b, 맛있는 c 출력

     

      -> where()은 Collection 요소중 참인 조건에 부합되는 요소만 필터링한 컬렉션을 반환한다 

      -> 예제는 아래와 같다. Return안에 bool값을 판별할 수 있는 식이 존재해야 한다

      -> 자매품으로 firstWhere(), lastWhere()등이 있다만 얘네들은 참인 요소가 없을때는 에러를 뱉는다(stateError)

// where(([매개변수]) { return [조건식]; }
List<int> numbers = [1,2,3,4,5,6,7];
var result = numbers.where((number) {
  return number > 5;
});
print(result); // 6,7 출력

 

      -> reduce()는 collection에 있는 요소들을 하나의 값으로 결합하는 함수

      -> 예제는 아래와 같다, 리스트를 순회하면서 앞에 값을 뒤에 값에 계속더하면서 진행된다

      -> 특징은 collection의 데이터타입 과 같은 타입으로만 반환할 수 있고 데이터가 비어있을경우 에러가 발생한다(stateError)

// reduce(([매개변수1], [매개변수2]) { return [동작] };
List<int> numbers = [1,2,3,4,5];
var result = numbers.reduce((a, b) {
  return a + b
});
print(result); // 15 출력

 

      -> fold()는 reduce에서 초기값이 추가된다. reduce()와는 다르게 초기값이 초기값으로 세팅된다. 

      -> 특이사항으로는 다른 타입으로도 반환이 가능하고 요소가 없어도 에러가 발생 안한다고 한다. 

// fold(초기값, ([매개변수1], [매개변수2]) { return [동작] });
// 앞에 초기값이 추가된다

List<int> numbers = [1,2,3,4,5];
var result = numbers.fold(0, (a, b) {
  return a + b
});
print(result); // 15 출력

 

 

      -> any()는 주어진 collection중 하나라도 조건을 만족시 true를 반환한다(where과 비슷하지만 bool을 반환한다)

      -> every()는 collection에서 모든 요소가 맞아야 true가 반환된다 

      -> takeWhile()은 collection에서 true인 요소가 나오는 동안에는 요소를 반환하고 false로 나오는 요소부터는 무시하는 기능이다

      -> skipWhile()은 collection에서 true인 요소가 나오는 동안에는 요소를 스킵하고 false로 나오는 순간부터의 요소를 모두 반환한다

 

Method Chaining은 위 고차함수를 잘 조합해서 쓰는게 핵심이다 

map() + where(),

where() + reduce(),

where() + map() + fold() 등등

Chaining을 통해 collection에서 필요한 여러 작업을 짧은 코드수로 끝낼 수 있다

Function(함수) 

  - 입력을 받아 특정작업을 수행하고 반환값을 return하는 코드 블록

  - 구성요소로는 (반환타입, 함수이름, 매개변수, 실행할 코드, 반환 값)이 있음

  - 아래 코드에서 각각

      반환타입은 int,

      함수 이름은 hi,

      매개변수는 int a, int b,

      실행할 코드는 return a + b,

      반환값은 a + b 이다

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

 

  - 아래와 같이 위 코드를 한줄로 표현 가능하다(한줄로 표현할 수 있을때만)

int hi(int a, int b) => a + b;

 

Generic(제네릭)

  - 클래스, 함수에서 데이터타입을 일반화 하여 다양한 타입을 지원할수 있게 하능 기능

[타입파라미터] [함수이름]<타입파라미터>([매개변수]) { ... }

T getFirstElement<T>(List<T> list) { 
  return list[0];
}

 
 - 특정타입에 의존하지 않고 여러 타입의 데이터에 대해 동일한 코드를 적용할 수 있어서 재사용성 높은 코드를 짤수 있다. 

 

 

Collection(컬렉션)

  - 여러 값을 그룹으로 묶어서 효율적으로 관리하는 데이터 셋

  - 종류는 List Set Map이 있다

    - List -> 순서가 있는 데이터 셋, 선언할 때 꺽쇠(<>) 사이에 데이터 형을 넣어줘야한다(int의 LIst는 -> List<int>)

      - [ ] 기호를 사용한다

      - List안의 요소가 변동 될 경우 var로 선언해 줄 수 있고, 없는 경우 final, const로 선언 가능하다, 단 요소를 변경할 수 없다 

      - 타입추론으로 LIst선언가능 (var sample = [1,2,3] 이런식으로도 선언 가능하다는 말이다 )

    - Set -> 중복되지 않은 값들이 묶인 데이터 셋!(중복을 허용하지 않는다)

      - { } 기호를 사용한다

      - 참고로 빈 Set을 만들고 싶을때는 var names = <String>{}; 와 같은 형식으로 만든다

        - var numbers = {}. 로 선언하면 map을 만드는 것임 

      - 순서가 없다, Index가 없다는말임

    - Map -> Key, Value가 하나로 1:1매칭되는 값을 가지는 데이터 셋

      - 선언할때는 Map<String, String> sample = { 'k1': 'v1', 'k2':'v2 } 이런식으로 선언한다

      - 특이사항으로는 키는 중복 불가하지만 값은 중복으로 넣을 수 있다

      - [변수이름][[키 이름]] 을 통해 값을 검색하거나 수정할 수 있다

      - keys, values를 통해 각 키 값을 모두 반환할 수 있다

 

    - List와 Set의 차이? 순서, Index, 중복여부 

 

Enumerations(열거형)

  - 여러 '상수' 값을 묶은 데이터 셋

  - 예제 -> enum Color { red, green, blue } 

  - 특징으로는 

      - 열거형 이름을 통해 열거형 값에 접근 가능 ( var Color = Color.blue }

      - switch문을 통해 각 case로 분기할 수 있음

      - index를 통해 열거형에 포함된 값이 열거형의 몇번째 있는지 알수 있음 (Color.red.index -> 0 )

  - values를 통해 포함되어있는 값을 알 수 있음 ( var colors = Color.values 로 할경우 List<Color> 로 추론됨)

  - name을통해 포함되어있는 값을 알 수 있음 ( Color.red.name -> red 값을 가짐)

  - 언제 사용함? 연관된 상수들의 집합을 정의할때 사용한다고 -> 같은 개념의 여러 값을 지정할때 사용함

 

+ Recent posts