본문 바로가기
[Flutter]

[Flutter] Dart언어

by Hevton 2021. 8. 11.
반응형

 

Flutter 개발에는 다트 언어를 사용한다. 온라인 메뉴얼에서 자세한 공부 자료를 얻을 수 있다.

https://dart.dev/guides/language/language-tour

 

A tour of the Dart language

A tour of all the major Dart language features.

dart.dev

 

 

개발 도구를 추가로 설치하지 않고 다트패드 웹사이트를 활용하여 온라인에서 다트 코드를 실행할 수 있다.

https://dartpad.dev/

 

DartPad

 

dartpad.dev

 

 

 

기본 문법 

 

- 다트 문법 역시 main 함수가 엔트리 포인트다.

 

- 주석

// 한줄 주석

/* 여러줄 주석 */

/// 는 HTML 형식의 문서를 생성할 수 있는 주석

 

- 변수

// 문자열 묶음 기호로 작은따옴표와 큰따옴표 모두를 사용할 수 있다.
String name = '홍길동';
String hobby = "축지법";


int i = 10;
double d = 10.0;

// int와 double은 num 타입에 포함된다. 따라서 아래와 같이 할 수도 있다.
num a = 10;
num b = 20.0;

// 일부 언어에서, double 타입에 int 타입을 대입하는 자동 형변환을 지원하기도 하지만
// 다트에서는 지원하지 않는다. 따라서 아래는 오류를 발생시킨다.
int a = 10;
double b = a; // 에러


// 반면 num 타입에는 int와 double 타입 모두 대입할 수 있다.
int a = 10;
double b = 20.0;

num c = a; // ok
c = b; // ok

 

num과 int, double 포함관계

 

- 다트는 타입을 직접 명시하지 않고 var로 대체할 수 있는 타입 추론을 지원한다. 일반적으로 이 방법을 주로 사용한다

( 자바스크립트의 var과 비슷하지만 완전히 같지는 않다. 자바스크립트의 var는 Dart에서 dynamic과 같다https://zucca.tistory.com/106 )

var i = 10; // int
var d = 10.0; // double
var s = 'hello'; // 문자열
var s2 = "hello"; // 문자열
var b = true; // 불리언

 

- 상수 final, const

final String name = '홍길동';
name = '임꺽정'; // 에러

 

- assert() 함수는 계산 결과가 참인지 검사

assert(2 + 3 == 5); // 결과 : true
assert(5 - 2 == 3); // 결과 : true

 

- 자바와 마찬가지로 더하기 연산으로 문자열 결합 가능

String str = 'hello' + 'world';

 

- is 키워드를 통해 타입을 검사할 수 있다.

// is : 같은 타입이면 true
// is! : 다른 타입이면 true


int a = 10;

if(a is int) {
	print('정수');
}

String text = 'hello';
if(text is! int) {
	print('숫자가 아님');
}

 

- 형변환은 as 키워드를 사용한다. 전혀 관련없는 타입으로는 당연히 안된다.

// int와 double은 모두 num을 구현하는 타입이지만 서로는 관계가 없기 때문에 형변환 불가
var c = 30.5; // double
int d = c as int; // 에러

하지만 int와 double 모두 상위 개념인 num으로 형변환 가능
dynamic d = 30.5;
num n = d; // as num; 생략 가능

 

- 함수의 형태

int f(int x) {
	return x + 10;
}

void main() {
	var result = f(10);
}

 

- 변수 앞에 $ 기호를 붙여 문자열 내에 변수 삽입 가능. 또한 $기호 뒤에 {}로 둘러싸 수식을 포함한 각종 표현식 사용 가능.

String _name = '홍길동';
int _age = 20;

void main() {
  print('$_name은 $_age살입니다.');
  print('$_name은 ${_name.length} 글자입니다.');
  print('10년 후에는 ${_age + 10}살입니다.');
}

--------
OUTPUT

홍길동은 20살입니다.
홍길동은 3 글자입니다.
10년 후에는 30살입니다.

뭔가 왠만하면 그냥 다 ${변수} 로 사용하는게 안전할 것 같다.. 내생각엔!

 

 

- 클래스 밖에 작성하는 함수를 최상위 함수라고 한다. 최상위 함수는 어디에서나 호출할 수 있는 함수이며 main 함수처럼 가장 바깥에 작성되어 있는 함수다. =>  멤버함수 = 메서드 가 아닌 함수를 말하는 것.

// 최상위 함수
bool isEven(int number) {
  return number % 2 == 0;
}

void main() {
  print(isEven(10));
}

 

- 클래스 내부에 작성하는 함수를 메서드라고 부른다.

class MyClass {
  // 메서드
  bool isEven(int number) {
    return number % 2 == 0;
  }
}

void main() {
  var myClass = MyClass();
  print(myClass.isEven(10));
}

 

- 클래스 내부에 선언된 함수라도 static 키워드를 붙이면 정적 메서드가 되며 함수로 볼 수 있다.

( static 키워드가 붙은 함수는 최상위 함수처럼 사용할 수 있다)

class MyClass {
  // 정적 메서드, 함수
  static bool isEven(int number) {
    return number % 2 == 0;
  }
}

void main() {
  // 인스턴스 없이 가능
  print(MyClass.isEven(10));
}

 

- 클래스 예제

void main() {
  Person person = Person("obj", 39);
  person.greeting();
}

class Person {
  String name;
  int age = 0;
  Person(this.name, this.age);
  
  void greeting() {
    print('안녕하세요 저는 $name입니다.');
  }
}

// https://papabee.tistory.com/37

 

 

- 익명 함수

([인수명]) { [동작 또는 반환값] }
void main() {
  var x = (var number) {
    return number % 2 == 0;
  };
  
  print(x(10));
}

 

 

- 람다식

([인수명]) => [동작 또는 반환값]
void main() {
  
  var func = (var number) => number % 2 == 0;
  
  print(func(10));
  
}

dart 람다에 대한 내용 : 여기

 

 

- 선택 매개변수

함수 정의에서 {}로 감싼 매개변수는 선택적으로 사용할 수 있다.

호출할 때 매개변수명을 값 앞에 써주게 된다. 따라서 이런 매개변수를 'Named Parameter' 라고 부른다.

// 아래처럼 하면 플러터 2에서 오류가 나온다. 널 세이프티가 적용되지 않았기 때문.
// 책이 플러터2 대응이라 해놓으시고, 플러터2에 대한 단원만 넣고 전체적인 내용은 수정하지 않은 것 같다. 당했다.
void something({String name, int age}) {}

void main() {
  something(name: '홍길동', age: 10);
}

// 선택 매개변수의 특성상 선택적으로 사용할 수 있기 때문에
// 값들에 널이 들어갈 수 있음을 명시적으로 알려야 한다. (플러터 2는 널 세이프티이기 때문)
// 타입 뒤에 ?를 붙이면 null을 허용하는 타입이란 말이 된다.
void something({String? name, int? age}) {}

void main() {
  something(name: '홍길동', age: 10);
}

// 또는 초기값을 지정해줘서 null이 발생할 일이 없도록 하면 된다.
void something(String name, {int age = 10}) {}

void main() {
  something('홍길동', age: 10);
}

 

선택 매개변수 중에 필수로 받아야 하는 것이 있다면 앞에 required를 넣어준다.

required는 named parameter중 필수 파라미터를 나타낸다. (예전엔 어노테이션이었는데, 바뀜)

 

 + 선택 매개변수( {}로 싸여있는것 )의 경우엔 이름있이(age: 10) 적어줘야한다는 것은 알겠는데,

굳이 그냥 매개변수(필수)의 경우에도 이름있이 적어주면 어떻게 될까 궁금해서 해봤다.

class hello {
  void  hi(int a, {int? b}) {
    
  }
}
void main() {
  hello a = hello();
  
//   a.hi(a: 10, b: 5); // 컴파일 안됨.
  a.hi(10, b: 5); // 이렇게 해야 됨.
}

결론 : 선택 매개변수의 경우에만 이름있이 넣어줘야 하는게 규칙이다.

 

 

- 만약 필수 매개변수와 선택 매개변수를 함께 사용하려면, 앞쪽에 필수 매개변수를 먼저 둔다.

void something(String name, {int? age}) {}

void main() {
  something('홍길동', age: 10); // ok
  something('홍길동'); // ok
  something(age: 10); // 에러
  something(); // 에러
}

 

- 위에서 잠깐 봤듯, 선택 매개변수는 기본값을 지정할 수 있다.

void something(String name, {int age = 10}) {}

void main() {
  something('홍길동', age: 10);
}

난 null safty 하기 위해서 오히려 이 방법이 더 좋은 것 같다.

 

 

- if else 문

void main() {
  String text = 'hello';
  
  if(text is int) {
    print('정수');
  } else if (text is double) {
    print('실수');
  } else {
    print('정수도 실수도 아님');
  }
}

 

- 삼항 연산자

// isRainy 값에 따라
var todo = isRainy ? '빨래를 하지 않는다' : '빨래를 한다';

 

- 스위치 케이스 문

enum Status { Uninitialized, Authenticated, Authenticating, Unauthenticated }

void main() {
  
  var status = Status.Authenticated;
  
  switch(status) {
    case Status.Authenticated:
      print('인증됨');
      break;
      
    case Status.Authenticating:
      print('인증 처리 중');
      break;
      
    case Status.Unauthenticated:
      print('미인증');
      break;
      
    case Status.Uninitialized:
      print('초기화됨');
      break;
  }
  
}

 

- 포문 (반복문)

void main() {
  var items = ['짜장', '라면', '볶음밥'];

  for (var i = 0; i < items.length; i++) {
    print(items[i]);
  }
}

 

- 클래스 기본

class Person {
  // Flutter 2부터 Null Safty를 위해 ?를 붙여주어야 한다.
  String? name;
  // 또는 초기값 명시.
  int age = 0;
    
  Person() {
    name = "Tom";
    age = 0;
  }
  
  
  void addOneYear() {
    age++;
  }
}

void main() {
  var person = new Person();
  // var person = Person(); // new 키워드 생략가능. 일반적으로 생략
  
  
  person.addOneYear();
  
  print(person.name);
  print(person.age);
  print('${person.age}살');
}

 

 

- 접근 지정자

// 변수명 앞에 _ 기호를 붙이면 '외부' 에서 접근이 불가능하다.
// 여기서의 외부는 자바의 private와 다르다.
// 동일 파일 내에서는 접근이 가능하다는 차이가 있다.
class Person {
  String? name;
  int? _age;
}
void main() {
  var person = new Person();  
  person._age = 20;
}

다트에서 말하는 private는 동일 파일이 아닌 기준이어서
자바에서 말하는 private와는 다르다.

-----------------
Dart에서의 private : 해당 클래스가 정의되어 있지 않은 다른 파일에서 접근 불가.
하지만 정의되어 있는 파일 내에서는 여전히 접근 가능.

 

+ ? 와 ! 의 차이 : 널일수도있어요? 와 널아니라고! 의 차이

 

- 생성자

아무것도 추가하지 않을 시 기본 생성자는 디폴트로 있긴 하지만, 사용자 정의 생성자를 추가할 경우 기본 생성자를 사용할 수 없게 된다는 치명적인 단점이 있다. 선택 매개변수를 사용하여서 이를 막을 수도 있지만 좋은 방법은 아니다. (어쨌든 동일한 생성자가 호출되므로)

class Person {
  int? age = 0;
  
  Person({this.age});
}

void main() {
  var person = Person();
  var pserson = Person(age: 10);
}

 

- getter, setter 키워드

set get 키워드를 사용한 이러한 특수한 유형의 메서드를 사용할 수 있다. private인 _ 와 함께 사용할 때 좋다.

class Rectangle {
  num left, top, width, height;
  
  
  Rectangle(this.left, this.top, this.width, this.height);
  
  num get right => left + width;
  set right(num value) => left = value - width;
  num get bottom => top + height;
  set bottom(num value) => top = value - height;
}

void main() {
  var rec = Rectangle(0, 0, 20, 20);
  
  rec.bottom = 10;
  print(rec.right);
 
}

 

 

- 상속

컴퓨터 세계의 상속은 슈퍼클래스를 그대로 복사한 후 기능 추가나 변경이 첨가됩니다. -> 음.. 확실히 동의는 안하지만, 개념상ㅇㅇ..

class Hero {
  String name = '영웅';
  
  void run() {}
}

class SuperHero extends Hero { // Hero를 상속
  
  // 부모의 run() 메서드를 다시 정의(오버라이드)
  @override
  void run() {
    super.run(); // 부모의 run()을 실행
    this.fly(); // 추가로 fly()도 실행
  }
  
  void fly() {}

}

void main() {
  var hero = SuperHero();
  hero.run();
  hero.fly();
  print(hero.name); // 영웅
}

 

 

- 추상 클래스

추상 클래스는 추상 메서드를 포함하는 클래스. 추상 메서드 : 선언만 되고 정의가 없는 메서드

추상 클래스는 그대로 인스턴스화할 수 없으며 다른 클래스에서 implement 하여 기능을 완성하는 상속 재료로 사용됩니다.

abstract class Monster {
  void attack();
}

class Goblin implements Monster {
  
  @override
  void attack() {
    print('고블린 어택');
  }
}

class Bat implements Monster {
  @override
  void attack() {
    print('할퀴기');
  }
}

 

- 믹스인

with 키워드를 사용하면, 상속하지 않고 다른 클래스의 기능을 가져오거나 오버라이드할 수 있다.

class Hero {
  String name = '영웅';
  
  void run() {}
}

abstract class Monster {
  void attack();
}

class Goblin implements Monster {
  
  @override
  void attack() {
    print('고블린 어택');
  }
}

class DarkGoblin extends Goblin with Hero {
  
}

 

- 열거 타입

열거 타입은 switch와 찰떡궁합이다.

enum Status { login, logout }

void main() {
  var authStatus = Status.logout;
  
  switch(authStatus) {
    case Status.login:
      print('로그인');
      break;
      
    case Status.logout:
      print('로그아웃');
      break;
  }
}

 

 

- 컬렉션

다트는 List, Map, Set 등의 Collection을 제공한다.

List : 같은 타입의 자료를 여러 개 담을 수 있고, 특정 인덱스로 접근할 수 있다.

Map : 키와 값의 쌍으로 저장할 수 있고, 키를 통해 값을 얻을 수 있다.

Set : 중복이 허용되지 않고, 찾는 값이 있는지 없는지 판단하고자 할 때 사용한다.

 

+ 다트에는 배열이 없다.

 

List

void main() {
  List<String> items = ['짜장', '라면', '볶음밥'];
  // var items = ['짜장', '라면', '볶음밥'];
  
  items[0] = '떡볶이';
  
  for(var i = 0; i < items.length; i++) {
    print(items[i]);
  }
}

기본선언

List<type> lst = [];

 

 

스프레드 연산자 '...'

... 연산자는 컬렉션을 펼쳐주는 연산자로, spread 연산자라고 한다. 다른 컬렉션 안에 컬렉션을 삽입할 때 사용한다.

void main() {

  var items = ['짜장', '라면', '볶음밥'];
  
  var items2 = ['떡볶이', ...items, '순대'];
  
  
  for(var i = 0; i < items2.length; i++) {
    print(items2[i]);
  }
}

List를 Set에 담게 되면 자동으로 중복 제거의 효과도 얻을 수 있다.

void main() {
  final items = [1, 2, 2, 3, 3, 4, 5];
  
  final myNumbers = {...items, 6, 7};
}

 

Map

순서가 없고 탐색이 빠른 컬렉션. 키와 값의 쌍으로 이루어져 있다.

void main() {
//   Map<String, String> cityMap = {
//     '한국': '부산',
//     '일본': '도쿄',
//     '중국': '북경'
//     };
  var cityMap = {
    '한국': '부산',
    '일본': '도쿄',
    '중국': '북경'
    };
  
  cityMap['한국'] = '서울';
}

 

Set

집합을 표현하는 컬렉션. 중복을 허용하지 않음.

void main() {
//   Set<String> citySet = {'서울', '수원', '오산', '부산'};
  var citySet = {'서울', '수원', '오산', '부산'};
  
  citySet.add('안양'); // 추가
  citySet.remove('수원'); // 삭제
  
  citySet.contains('서울'); // true
}

 

 

 

- 일급 객체

다트에서는 함수를 값으로 취급할 수 있으므로, 다른 변수에 함수를 대입할 수 있다.

void greeting(String text) {
  print(text);
}

void main() {
  var f = greeting;
  f('hello');
}

 

 

- 다른 함수의 인수로 함수 자체를 전달하거나 반환받을 수도 있다.

void something(Function(int i) f) {
  f(10);
}

void main() {
  int value = 10;
  // 익명함수 활용
  something((value) {
    print(value);
  });
}

 

 

- 다트에서 함수로 표현할 수 있는 것들(람다식, 익명함수, 메서드)은 모두 값으로 취급할 수 있다.

void something(Function(int i) f) {
  f(10);
}

void myPrintFunction(int i) {
  print('내가 만든 함수에서 출력한 $i');
}

void main() {
  // 동일한 결과
  something(myPrintFunction);
  something((i) => myPrintFunction(i));
  something((i) => print(i));
  something(print);
}

 

 

- forEach 함수

내부적으로 반복을 수행한다. forEach() 함수는 (E element) {} 형태의 함수를 인수로 받는다. (E는 모든 타입이 가능하다는 뜻)

void main() {
  final items = [1, 2, 3, 4, 5];
  
  
  items.forEach(print); //1, 2, 3, 4, 5
  
  // 익명함수로 표현하면
  items.forEach((e) {
    print(e);
  });
  
  // 람다식으로 표현하면
  items.forEach((e) => print(e));
}

(e) => print 형태의 함수에서 e는 items의 각 요소가 내부적으로 반복하면서 하나씩 들어올 인수이다.

 

 

- where 함수

조건 필터링에 사용하는 함수. 짝수만 출력하고 싶을 때의 예시는 아래와 같다.

 void main() {
   
   final items = [ 1, 2, 3, 4, 5 ];
   
   // for문으로 구현 예시
   for(var i = 0; i < items.length; i++) {
     if(items[i] % 2 == 0) {
       print(items[i]);
     }
   }
   
   // where 함수로 구현 예시
   items.where((e) => e % 2 == 0).forEach(print);
 }

 

- toList

결과를 리스트로 저장

void main() {
  var items = [1, 2, 3, 4, 5];
  final result = [];
  
  items.forEach((e) {
    if(e % 2 == 0) {
      result.add(e);
    }
  });
  
  // 위 코드를 toList로 한줄로 구현 가능
  final results = items.where((e) => e % 2 == 0).toList();
  
}

참고로, final 키워드를 쓰면 타입 생략 가능.

 

- toSet

리스트에 중복된 데이터가 있을 경우 중복을 제거한 리스트를 얻고 싶을 수 있다.

void main() {
  final items = [1, 2, 2, 3, 3, 4, 5];
  
  var result = [];
  
  for(var i = 0; i < items.length; i++) {
    if(items[i] % 2 == 0) {
      result.add(items[i]);
    }
  }
  
  print(result); // 2, 2, 4
  
  //위 코드와 동일한 코드는
  final result2 = items.where((e) => e % 2 == 0).toList(); // 2, 2, 4
  
  
  // 결과에 중복을 피하기 위해서 Set에 먼저 담으면 된다.
  final result3 = items..where((e) => e % 2 == 0).toSet().toList(); // 2, 4
  
}

 

+  var을 사용하여 빈 Set이나 빈 Map을 작성할 때는 문법을 조심해야 한다. 값 없이 그냥 {} 만 작성하면 Set이 아닌 Map으로 인식한다.

var mySet = <String>{}; // Set<String> mySet
var myMap = {};  // Map<dynamic, dynamic>

 

 

- any()

any() 함수는 리스트에 특정 조건을 충족하는 요소가 있는지 없는지 검사할 때 사용한다.

다음은 리스트에 짝수가 하나라도 있는지 검사하여 결과를 출력하는 코드이다.

void main() {
  final items = [1, 2, 2, 3, 3, 4, 5];
  
  print(items.any((e) => e % 2 == 0)); // true;
}

 

 

- 계단식 표기법 '.. 연산자'

계단식 표기법(cascade notation)을 사용하면, 동일 객체에서 일련의 작업을 수행할 수 있다.

컬렉션의 add() 메서드는 void를 반환하고, remove() 메서드는 bool을 반환하지만 .. 연산자를 사용하면 연쇄 작업이 가능하다.

void main() {
  final items1 = [1, 2, 3, 4, 5];
  final items2 = [1, 2, 3, 4, 5];

  // 기존 방식
  items1.add(6);
  items1.remove(2);
  
  print(items1);
  
  
  // 새로운 방식
  print(items2..add(6)..remove(2));
}

 

- 컬렉션 if

다트에서는 컬렉션 내부에 if문이나 for문을 사용할 수 있다. (이 때 {} 블록은 사용할 수 없다)

void main() {
  bool promoActive = true;
  
  print([1, 2, 3, 4, 5, if (promoActive) 6]);
}

 

- 컬렉션 for

컬렉션 내부에 for문을 사용할 수도 있다.

void main() {
  var listOfInts = [1, 2, 3];
  var listOfStrings = [ '#0', for (var i in listOfInts) '#$i'];
}

 

 

- 다트에서는 모든 것이 객체이다 (파이썬처럼). 심지어 int double bool 같은 타입도 모두 클래스이다. 이러한 타입들은 모두 null을 가질 수 있다. 따라서 다트에서 모든 타입은 null일 수 있다.

 

 

서적 : 오준석의 플러터 생존코딩

 

아쉬운점 : 플러터 2 대응이라는데,대응이라고 하기에는 플러터 2 단원에 대한 설명 단원만 넣고

전체적인 책의 내용에서는 플러터 2에 맞게 수정되지 않아서, 돌아가지 않는 예제들이 꽤 있다...

 

 

 

 

반응형

'[Flutter]' 카테고리의 다른 글

[Flutter] 앱 구조  (0) 2021.08.14
[Flutter] 프로젝트 구조  (0) 2021.08.13
[Flutter] 개발 환경 진단  (0) 2021.08.08
[Flutter] Flutter의 Hot Reload 기능  (0) 2021.08.08
[Flutter] 첫 Flutter Project 생성해보기  (0) 2021.08.08