먹고 기도하고 코딩하라

생존을 위한 Dart 문법 톺아보기 본문

앱/Flutter

생존을 위한 Dart 문법 톺아보기

사과먹는사람 2021. 10. 2. 19:20
728x90
728x90

 

동아리 이번 기수에는 사실 프로젝트나 알고리즘 스터디를 하고 싶었다. 여름방학 동안 어느 정도 단련(?)된 나는 이제 중급자 대상 알고리즘 스터디를 하고 싶어서 간을 보고 있었는데... 아쉽게도 중급자용 스터디가 없었다. 그리고 전반기가 거의 그렇듯 스터디 위주이고 프로젝트도 들어갈 만한 데가 없었다.

그래서 이번 기수 전반기도 스터디를 하기로 했다. 대신 이번에는 웹 말고 앱 스터디를 하기로 했다. 웹 프론트는 이제 스스로 어느 정도 꾸밀 수 있을 정도로 공부를 하기도 했고, 실제로 구현하고 싶은 것을 할 수 있을 정도는 됐기 때문에 앱을 잘 해보고 싶어서 선택했다. 아는 분이 여신 플러터 앱 스터디에 들어가서 공부 중이다.

지금 swift를 다시 공부하고 iOS 앱을 만드는 중이라 언어를 또 새로 배운다는 게 약간 부담이 되기도 했다. 하지만 결국에는 크로스 플랫폼 앱을 만들 줄 아는 게 도움이 될 거라는 생각에 스터디를 시작했다. 그렇게 접한 플러터는 아직은 그리 어렵지 않다. Dart라는 언어는 기존에 있는 swift, java, javascript 등 여러 언어를 섞어놓은 것 같은 그런 익숙한 느낌이 든다. 게다가 위젯 모음 라이브러리들이 잘 되어 있어서 갖다쓰기도 쉽다. 아직까진 모든 게 좋다. 구글이 언제 죽일지 모른다는 점만 빼면 Dart는 꽤 좋은 크로스 플랫폼 앱 개발 언어가 아닌가 하는 생각이 든다. 앵귤러의 뒤를 밟는다면...

 

현재 <Do it! 플러터 앱 프로그래밍>이라는 책을 보면서 공부 중이다. 난 책을 보면서 예제 코드를 따라 치고 공부하기보다는 일단 검색하면서 만들고 마개조하면서 배우는 걸 더 선호하지만 그룹 스터디에는 역시 책이 있어야 하는 것 같다. 스터디 준비에 공들이며 프로젝트 마개조를 하면 얻어가는 것도 많다.

 

 

다트 언어의 주요 특징

1. 다트는 항상 main() 함수에서 시작한다. 이 함수는 아무것도 반환하지 않는다. (티스토리 코드에 Dart를 지원하지 않아서 Swift로 해놓고 코드를 적는다. 하이라이팅이 잘 안 되는 점을 양해 바란다.) 사실 void는 안 써도 된다.

모든 것이 main 함수에서 시작되므로 main 함수를 생략할 수 없다.

 

void main() {
  var a = 'a';
  print(a);	//a
}

2. 모든 변수가 Object 클래스를 상속받는 객체이다. 이를 확인하려면, a is class 식으로 확인해보면 된다.

void main() {
  var a = 'a';
  print(a is Object);	//true
}

 

3. 자료형에 엄격하다. 파이썬처럼 자료형을 자유롭게 바꿀 수 있는 언어가 아니다. 한 번 변수의 타입이 정해지면, 이 변수에는 다른 타입의 값을 넣을 수 없다. 유일하게 dynamic 타입만 그것을 허용한다. var 타입조차도 값이 처음으로 할당되었을 때의 타입으로 타입이 고정된다.

3-1. var 타입을 쓰는 이유는 일단 자료형이 뭐가 들어올지는 모르겠지만 받아본 다음에 결정하겠다는 의미로 정하는 것이다. var 타입의 변수는 최초로 값이 할당된 후 타입 추론으로 자료형이 정해진다.

 

4. 제네릭 타입을 통해 개발할 수 있는데, 나중에 List<int>, List<String> 이런 식으로 써먹기 좋다. 

 

5. 자바 같은 public, protected, private 같은 접근자 키워드는 없다. 그래서 내부에서만 접근해야 하는 변수나 속성, 메소드에는 언더스코어(_)를 이름 앞에 붙여주는 것으로 대신한다. 

 

6. 다트 2.0부터 Null safety를 지원한다. Swift에 있는 ?, ! 등 변수의 Null과 관련된 문자들을 사용할 수 있어 더욱 안전한 프로그래밍을 할 수 있다.

Null safety를 사용해서 어떤 변수나 nullable한지 아닌지 나타낼 수 있다. 어떤 변수가 null이 될 수 있다면 ?를 붙여서 선언하면 된다.

int? age;
age = age ?? 0;
print(age);	//0

근데 특이한 건 var 타입에는 ?을 붙일 수가 없다. 타입이 정해져 있지 않아서 그런가? dynamic 타입에는 가능한데 말이다. 관련해서 검색해보니 var 타입은 Null safety를 할 수 없다고 한다. 

사실 Null Safety가 빛을 발하는 건 클래스와 함께 썼을 때인 것 같다. 간단한 클래스 예시를 보자.

class Person {
  String? _name;
  int? _age;
  
  Person(String? name, int? age) {
    this._name = name!;
    this._age = age!;
  }
  
  void printName() {
    print('My name is ${_name!}');
  }
}

void main() {
  Person john = Person('John', 25);
  john.printName();
  Person casey = Person();
  casey.printName();
}

이렇게 하면 john의 이름을 출력했을 때 'My name is John' 하면서 잘 나온다. null이 아니라는 것이 확실한 상황에서는 저렇게 느낌표 !를 붙여 출력할 수 있다. 단점은 null일 때 프로그램이 뻗어버린다는 점이다.

 

7. 내 예상과 달리 typescript처럼 동작하지는 않는 모양이다. casey를 선언해주는 데에서 2개의 인자가 필요한데 0개의 인자를 전했다고 오류가 나는 걸 보니...

검색해 보니 다트 클래스는 함수 오버로딩을 허용하지 않는다고 한다. 그럼 생성자를 여러 개 써야 할 때는 어떻게 해야 하나 보니, 이름 없는 생성자는 클래스당 단 1개만 가질 수 있지만, 클래스 생성자에 이름을 주면 생성자를 더 만들어 사용할 수 있다고 한다. 클래스 내에서 클래스이름.함수명(매개변수)를 쓰면 생성자가 된다고 하니 위의 코드는 다음과 같이 바꾸는 게 좋겠다.

class Person {
  String? _name;
  int? _age;
  
  Person(String? name, int? age) {
    this._name = name!;
    this._age = age!;
  }
  
  Person.defaultConstructor() {}
  
  void printName() {
    print('My name is ${_name!}');
  }
}

void main() {
  Person john = Person('John', 25);
  john.printName();	//My name is John
  Person casey = Person.defaultConstructor();
  casey.printName();
  //Uncaught TypeError: Cannot read properties of null (reading 'toString')
  //Error: TypeError: Cannot read properties of null (reading 'toString')
}

casey는 보다시피 defaultConstructor를 통해 만들었기 때문에 이름이 없다. 그런데도 _name이 null이 아니라는 것을 강하게 주장하는 printName을 마주치자 에러를 뱉으며 프로그램이 죽어버린다. 

여튼 클래스 생성자 오버로딩은 이렇게 할 수 있는 걸로.

 

8. 삼항 연산자를 사용할 수 있는데, 일반적인 expression ? true : false; 외에도 주어진 expression이 null인지 아닌지에 따라 다른 값을 반환하는 연산자를 사용할 수도 있다. 다음 코드에서 첫 번째 줄은 일반적인 삼항 연산자이며, 두 번째 줄은 age가 null이면 0을 대입하고 null이 아니면 원래 값을 넣는다.

var visibility = isPublic ? 'public' : 'private'; // isPublic이 true일 경우 public, false일 경우 private
var age;
age = age ?? 0;	//age가 null일 때 age에는 0이 대입되고, null이 아니라면 age 원래 값이 대입된다

삼항연산자와 조건문 등을 쓸 때 주의할 점은, 다트는 타입을 엄격하게 검사하는 언어라서 다른 언어에서 일명 truthy한 값인 비어있지 않은 문자열, 리스트, 0이 아닌 수 등은 true가 아니다. 오직 어떤 변수가 bool 타입일 때만 조건 연산의 대상이 될 수 있다.

즉, 다음과 같은 연산이 불가능하다는 것이다.

if (age) {
    print('yes');
} else {
    print('no');
}
// Uncaught Error: TypeError: 0: type 'JSInt' is not a subtype of type 'bool'

 

9. 별 건 아니지만 문장 뒤에 세미콜론을 붙여야 한다. 파이썬과 자바스크립트를 오래 쓰다가 세미콜론이 필수인 언어로 돌아오니 살짝 어색하다. ㅎㅎ 세미콜론을 깜빡하면 컴파일 에러가 일어나니 주의하도록 하자.

 

10. 문자열 내에서 어떤 변수의 값을 그대로 쓰고 싶다면 앞에 달러($) 표시를 붙여주면 된다.

void printAge(int age) {
  print("I'm $age years old.");
}

이것을 string interpolation(한글로 직역하면 문자열 보간 정도 되겠다)이라고 한다. 상세한 예시는 이 문서에 잘 나와 있다. 문서에 따르면, $ 표시 뒤에 오는 것이 변수명이라면 { }을 생략하고 그냥 써도 되지만, 표현식을 써야 할 때는 반드시 ${ } 안에 식을 쓰라고 한다. 나이에 따라 성인인지 청소년인지 출력하는 함수를 만들어보자. 

void printAge(int age) {
  print("I'm an ${age > 18 ? 'adult' : 'adolescence'}");
}

이 때 'adult'와 'adolescence'는 문자열 그 자체이고 이에 해당하는 변수가 없으므로 꼭 문자열 표시인 따옴표나 큰따옴표로 감싸줘야 함을 의식하자.

 

11. 참, 함수의 헤더는 반환형 이름(타입 변수명, 타입 변수명, ...)이다. 

 

12. 다른 타입은 다 소문자로 시작하는데 문자열 String만 대문자로 시작한다. 개별 문자 character를 나타내는 건 없고 그냥 문자 들어가면 String인 모양이다.

 

13. 정수형은 int, 실수형은 double, 정수형과 실수형 모두 담을 수 있는 수는 num 자료형이다.

 

14. 다트로도 비동기 프로그래밍이 가능한데, async-await 키워드를 써서 가능하다. ES2018 이후 느낌이 물씬 난다. ㅋㅋ 그래서 스터디 때 비동기 프로그래밍 이해하는 게 쉬웠다.

함수에 async 키워드가 붙어 있으면 이는 비동기 함수이고, 이 async가 붙은 함수 내에서는 반드시 다른 함수를 await로 호출하는 부분이 있다. (이 부분이 없을 수도 있지만 그렇다면 굳이 async를 써줄 필요가 없다)

다음은 교재에 나와 있는 예제 코드이다.

void main() {
  getVersionName().then((value) => {
    print(value)
  });
  print('end process');
}

Future<String> getVersionName() async {
  var versionName = await lookUpVersionName();
  return versionName;
}

String lookUpVersionName() {
  return 'Android Q';
}

Android Q가 나오고 end process가 나와야 할 것 같지만 결과는 반대다. end process가 더 먼저 나온다. 왜 이런 일이 일어나는 걸까?

 

순서를 되짚어보자. 제일 먼저, main 함수의 getVersionName()이 호출된다. 값이 돌아오지 않았으므로 아직 then 절은 실행되지 않는다. 그런데 getVersionName은 async 키워드가 붙은 비동기 함수이다. 즉, main 함수의 다른 일을 먼저 처리해도 된다는 의미이다. 그래서 print('end process')가 2번째로 실행되면서, 동시에 getVersionName에서는 lookUpVersionName을 받아 돌려준다. lookUpVersionName은 즉시 'Android Q'를 돌려주는데, 이렇게 받은 것을 main으로 또 토스한다. 그럼 then절이 그때서야 실행된다.

참고로 await 키워드는 처리가 끝나고 값을 반환할 때까지 이후 코드 처리를 멈추도록 한다. 그렇기 때문에 lookUpVersionName에서 결과가 돌아오기 전에는 return versionName; 문장을 실행하지 않는다.

 

Future<String>은 뭘까? versionName이 Future<String> 타입이라는 걸까? 그렇지는 않다. versionName을 찍어보면 String 타입이다. await가 붙은 함수를 비동기적으로 처리한 후, 결과를 Future 클래스에 저장해둔다는 의미라고 한다. Future는 비동기 작업의 결과를 나타낸다고 한다. 설명을 들어보니 자바스크립트의 Promise와 비슷한 것 같다. 어쨌든 getVersionName에서 돌려주는 versionName의 값이 Future 클래스에 저장되며, String 타입이기 때문에 Future<String>으로 제네릭 타입 선언을 해준 것 같다. 그 값을 main에서는 then절 안에 value라는 이름으로 받는 것이다.

 

15. 지금은 아주 간단하게 만들었지만 실제로 비동기 처리를 해야될 때는 서버에서 어떤 데이터를 가져오거나 할 때이다. 그런데 에러가 일어나는 상황도 있을 거 아닌가? 그럴 때 then절만 있고 에러에 대응하는 코드가 없으면 안 된다. 에러에 대응하려면 .catchError를 붙여줘야 한다.

void main() {
  getVersionName()
    .then((value) => {
      print(value)
    })
    .catchError((e) => {
      print(e)
    });
  print('end process');
}

Future<String> getVersionName() async {
  var versionName = await lookUpVersionName();
  return versionName;
}

String lookUpVersionName() {
  throw 'Catch me if you can';
  return 'Android Q';
}

위 코드에서는 return으로 Android Q를 주기 전에 일부러 에러를 만들어 던졌다. throw로 커스텀 에러를 일으켰으므로 then이 아니라 catchError 절로 넘어가 에러 메시지를 출력하는 코드가 되는 것이다.

이 경우에도 end process가 먼저 나오는데, 이게 의도한 상황이 아니라면 main 함수에도 async를 붙이고, getVersionName 앞에 await 키워드를 붙이면 된다.

void main() async {
  await getVersionName()
    .then((value) => {
      print(value)
    })
    .catchError((e) => {
      print(e)
    });
  print('end process');
}

Future<String> getVersionName() async {
  var versionName = await lookUpVersionName();
  return versionName;
}

String lookUpVersionName() {
  throw 'Catch me if you can';
  return 'Android Q';
}

 

16. 다트는 싱글 스레드 프로그래밍 언어이다. 

 

17. 조건문, 반복문은 java 문법과 비슷하게 쓰면 되는 것 같다. switch도 있으니 자유롭게 쓰면 된다. do-while 루프가 있다! 

// 기본적인 for loop
for (int i = 0; i < 10; i++) {
    print(i);
}

// for-in-loop
for (int i in [1, 2, 3, 4, 5]) {
    print(i);
}

// while loop
int i = 10;
while (i > 0) {
    print(i--);
}

// do-while
do {
    print(i--);
} while (i > 0);
// if-else if-else
int score = 90;
if (score > 90) {
  print('A');
} else if (score > 80) {
  print('B');
} else {
  print('C');
}

// switch
String grade = 'B';
switch (grade) {
  case 'A': {
    print('Splendid');
    break;
  }
  case 'B': {
    print('Excellent');
    break;
  }
  case 'C': {
    print('Good');
    break;
  }
  case 'D': {
    print('need to study harder');
    break;
  }
  default: {
    print('?');
    break;
  }
}

문서에 보니 case에 묶인 break문을 중괄호 밖으로 뺐던데, 2가지 모두 실험해본 결과 빼나 안 빼나 결과는 똑같았다.

 

18. 한 줄 주석은 //, 여러 줄 주석은 /* */ 사이에 감싸주면 된다.

 

19. async* 라고 쓰인 함수가 있다. 이것은 다른 프로그래밍 언어에서 제너레이터로 설명되는 것과 비슷하게 yield를 통해 지속적으로 값을 반환하는 함수인데, 이런 함수들의 특징은 Stream 타입을 반환하는 것이다.

import 'dart:async';

Stream<int> countStream(int to) async* {
  for (int i = 1; i <= to; i++) {
    print('countStream : $i');
    yield i;
  }
}

void main() async {
  var stream = countStream(10);
  stream.listen((x) => print(x));	// countStream과 print(x)가 번갈아 실행된다
}

책은 스트림에 대한 설명이 빈약해서.. 문서랑 이 블로그가 도움이 많이 됐다.

 

Flutter - 스트림. 다트에서 비동기 프로그래밍

Flutter - 스트림. 다트에서 비동기 프로그래밍 목차 스트림이란? 스트림 간단한 예제 스트림 다양하게 처리하기 스트림 내부 구조 서브스크립션 브로드 캐스트 스트림 컨트롤러 1. Stream이란? 스트

software-creator.tistory.com

 

 

19화 다트 stream

플러터를 위한 다트 프로그래밍 | 다트의 비동기 프로그래밍 (3/3) 이전 글에서 살펴본 future는 하나의 데이터(결괏값)를 then()에서 전달받았다. 반면에 stream은 연속된 데이터를 listen()을 통해서 비

brunch.co.kr

책에 나오는 예시는 위의 코드와 조금 다른데, 책 코드가 이해가 안 돼서 이해하려고 제너레이터까지 다시 보게 됐다. 책에는 나와있지 않지만, stream.listen(함수)로 stream에 이벤트가 발생(데이터 유입)하면 실행할 함수를 지정할 수 있다. 

 

 

음 얼추 정리는 된 것 같은데, 중요한 리스트와 맵 객체를 안 다뤘다. 클래스도 좀 더 다뤄보면 좋을 것 같으니 이건 다른 포스팅으로 빼겠다. 이미 길이가 너무 길어졌다.

 

 

 

References

 

728x90
반응형
Comments