Flutter 개발을 위한 Dart 언어 간단 정리
2023-06-05 05:06
최근 효과적인 멀티플랫폼 앱 개발을 위한 많이 사용하는 Flutter의 기초 언어인 Dart를 javaScript와 비교하면서 간단히 정리해봅니다.
개요
최근 iOS와 Android 앱을 동시에 개발 가능한 Flutter가 많은 관심을 받고 있다. Flutter는 독자적인 방식을 통해서 기존 하이브리드 플랫폼에 비해 월등히 높은 수준의 앱을 선보일 수 있다. 이런 Flutter 플랫폼의 기반 언어는 Google에서 개발한 Dart를 사용하고 있다. 한 때 잠시 유행했던 javaScript의 랩핑 언어 정도로 오해 받은 적도 있지만, Dart 언어는 네이티브와 웹 모두를 지원하는 상당히 범용적인 언어다. 오늘은 Flutter를 시작하기 위해 기본적으로 학습해야 하는 Dart 언어에 대해서 알아보자. 본문에서 사용한 많은 예제는 Dart 공식 문서에서 발췌하였기 때문에 더 자세한 내용이 궁금하다면 Dart 공식 문서를 찾아보는 것을 추천한다.
1. 다른 언어와의 유사점
처음 Dart를 접하는 경우, 최근 많은 언어에서 찾아볼 수 있는 함수형 언어의 특성을 많이 찾아볼 수 있다. 특히 최신 javaScript에 익숙한 분이라면 이미 javaScript에서 지원하고 있는 문법이나 패턴이 금방 눈에 보일 것이다. var
를 활용한 느슨한 타입 시스템이라든지, 함수를 매개변수로 제공할 수 있다든지, ...
을 활용한 스프레딩(Spreading), 구조분해(Destruction) 같은 것이 대표적이다.
그러나, Dart는 기본적으로 컴파일 언어이며, Java 언어와 유사한 강한 타입 시스템을 가지고 있다. var
이외에도 객체 타입을 지원하며, 클래스, 인터페이스, 열거형(Enum) 등을 활용할 수 있도록 했다. 이런 컴파일 언어의 특성은 const
를 사용한 변수 정의 등에서 인터프리터 언어와의 차이점을 도드라지게 살펴볼 수 있다.
Dart는 네이티브/웹 모두를 지원하기 때문에 javaScript와 유사한 모습도 많이 가지고 있고, 반면에 Java에서와 같이 객체 지향적인 특성도 많이 찾아볼 수 있다. 이처럼 Dart 언어는 최신의 개발 언어 트렌드를 반영하여 개발자가 자연스럽고 편리하게 개발할 수 있도록 하는 것에 집중하고 있다.
2. 변수
Dart에서는 강한 타입 시스템을 지원하고 있지만, 유연한 개발을 위해서 var
라는 느슨한 타입도 허용한다. 이보다 더 열려있는 dynamic
타입까지 지원하여 개발자가 상황에 따라 타입 사용을 유연하게 할 수 있도록 지원한다. 이 둘은 Dart 언어의 특징 중 하나인 타입 추론을 통해서 동적으로 타입을 할당한다.
// var는 추론 후 다른 타입을 담을 수 없다.
var name = 'This is a string type';
name = 111; // Error
// dynamic은 추론 후에도 다른 타입을 담을 수 있다.
dynamic nickName = 'This is also a string type';
nickName = 111; // OK!
이외에도 final
, const
과 같은 식별자를 제공하여, 변수의 값 할당에 대한 제약을 조절할 수 있게 해준다.
// final은 변수 재할당을 금지한다. 하지만, 객체 자체의 수정은 가능하다.
final names = ['name1', 'name2'];
name = ['newName1']; // Error
name.add('name3'); // OK!
// const는 변수 재할당 및 객체 수정까지 금지한다.
const nickName = ['nickName1', 'nickName2'];
nickName = ['newNickName1']; // Error
nickName.add('nickName3'); // Error
3. 타입과 타입 추론
Dart는 안전한 코드 개발을 위해서 타입 체킹을 한다. 그러나, var
나 final
등을 사용하는 코드에서 본 것처럼 굳이 타입 어노테이션을 사용하지 않아도 내부의 타입 추론을 통해서 해당 변수의 타입을 자동으로 확인한다. String, int 등 Dart 언어에서 기본적으로 제공하는 타입이 있으며, 이외에 직접 작성한 클래스를 타입으로 사용할 수 있다.
아래의 코드 예제에서 보듯이 타입 어노테이션이 없이 final
로 선언된 변수에 값이 할당되면서 타입 추론이 일어나고, 이 변수를 int 배열을 인자로 받는 printInts
에 사용하게 되면 컴파일 에러가 발생된다.
// 매개변수 인자로 int 배열을 받는 함수
void printInts(List<int> a) => print(a);
void main() {
// list 변수는 값으로 number와 string을 받기 때문에 List<dynamic>으로 추론된다.
final list = [];
list.add(1);
list.add('2');
// List<int>에 List<dynamic>을 사용하려고 시도하면서 에러가 발생한다.
printInts(list);
}
이외에도 컴파일 시점에는 검사할 수 없는, 타입 캐스팅으로 인한 에러도 런타임 시점에 체크하고 있다.
void main() {
// Animal 타입으로 선언하여 하위 Dog 타입을 받아줄 수 있다.
List<Animal> animals = [Dog()];
// Cat 타입은 Animal 타입의 구현체로, List<Cat>으로 캐스팅하는 경우 Dog 타입을 할당할 수 없기 때문에 에러가 발생한다.
List<Cat> cats = animals as List<Cat>;
}
4. 다채로운 지원 문법
Dart에서는 프로그래밍을 더 효율적으로 할 수 있는 다양한 지원 문법을 제공한다.
타입 가드와 타입 캐스팅을 위한 is와 as
is
와 as
문법의 경우, typeScript와 같은 스크립트 언어에서 찾아볼 수 있는 문법이다. 다만, typeScript가 런타임에서 이를 지원할 수 없는 것과 달리 Dart는 런타임에서도 해당 기능을 사용할 수 있다.
void main() {
Animal animal = Cat();
// is를 사용하여 animal의 서브 타입이 Cat인지 확인한다.
if (animal is Cat) {
// Cat 객체만 보유한 함수를 사용하기 위해서 as를 사용해 Cat으로 캐스팅한다.
Cat cat = animal as Cat;
cat.meow();
print('This is a cat!');
} else {
print('This is not a cat!');
}
}
Null 안정성을 위한 ?, !
Dart는 기본적으로 모든 변수는 non-nullable이다. null을 허용하고 싶다면 타입 뒤에 ?
을 붙여주어야 한다. 이 문법은 null-checking에도 사용되어서, 하위 필드를 참조할 때 null인 경우 실행하지 않도록 해준다.
!
를 통해서 null-assertion을 지원한다. 변수 뒤에 선언해서 해당 값이 null이 아니라고 지정할 수 있다. 다만, null-checking과 달리 실제 값에 null이 들어오는 경우 예외가 발생하게 된다.
void main() {
// confirm이라는 id를 가진 요소가 존재하는지를 확인한다.
var button = querySelector('#confirm');
button?.text = 'Confirm';
List<String?> nullableList = ['not null', null, 'not null too'];
String str = nullableList.second!; // null이 아님라고 표시
// str이 null이 아니라고 했지만 실제는 null이므로 런타임 예외 발생
print('str is $str.');
}
Chaining과 Spreading
Dart는 Cascade notation이라고 하는 문법을 통해 객체에 대한 변수 할당이나 함수를 손쉽게 할 수 있다. Java 등에서 흔히 사용되는 builder 패턴과 유사하지만, 기존 문법을 그대로 인라인으로 사용할 수 있다는 점에서 훨씬 직관적이다.
또한, spreading을 지원하여, 배열이나 객체를 쉽게 shallow copy 할 수 있다. 이외에도 동일한 문법으로 rest parameter로 사용할 수 있기 때문에 배열 등에서 특정 위치에만 있는 값만 참조하고 싶을 때 편리하게 사용할 수 있다. spreading 시 null-checking을 지원하므로, 대상 배열이나 객체가 null 경우도 방어할 수 있다.
void main() {
// 케스케이딩은 객체를 반환하는 경우에만 사용할 수 있다.
querySelector('#confirm') // 객체 찾기.
?..text = 'Confirm' // 객체의 멤버 사용.
..classes.add('important')
..onClick.listen((e) => window.alert('Confirmed!'))
..scrollIntoView();
// spreaing으로 배열을 확장할 수 있다.
List<String> list1 = ['second', 'third', 'fourth'];
List<String> list2 = ['first', ...list1];
// null-checking이 가능하다. Null인 경우 무시한다.
List? listNull = null;
List<String> list3 = ['first', ...?listNull];
// 배열의 나머지 인자를 표시한다.
// first == 'first', last == 'fourth'
var [first, ..., last] = list2;
}
5. 클래스 시스템
Dart의 클래스 시스템은 스크립트 언어와 컴파일 언어의 중간적인 모습을 보이고 있다. 그동안 typeScript 등에서 제공해주지 못하던 클래스 및 인터페이스를 온전하게 구현하고 있다. 반면, 접근 제어자를 사용하지 않고, mixin
이나 named
생성자를 지원하는 등 확장된 클래스 시스템을 제공하고 있다. 클래스에 대해서는 짧게 정리하기 어려울 정도로 많은 기능이 있기 때문에 공식 문서의 클래스 설명을 읽어보길 추천한다.
클래스 관련해서 Dart가 지원하고 있는 또다른 강력한 기능 중 하나는 enum 클래스다. Dart의 enum은 Java의 enum과 거의 비슷한 수준으로 사용할 수 있다. enum 객체는 내부적으로 별도의 필드를 가질 수 있고, getter를 따로 정의할 수도 있다.
enum Vehicle implements Comparable<Vehicle> {
// 내부 필드를 가지는 enum 값
car(tires: 4, passengers: 5, carbonPerKilometer: 400),
bus(tires: 6, passengers: 50, carbonPerKilometer: 800),
bicycle(tires: 2, passengers: 1, carbonPerKilometer: 0);
// enum 생성자
const Vehicle({
required this.tires,
required this.passengers,
required this.carbonPerKilometer,
});
// 내부 필드
final int tires;
final int passengers;
final int carbonPerKilometer;
// 내부 필드를 활용하는 getter
int get carbonFootprint => (carbonPerKilometer / passengers).round();
bool get isTwoWheeled => this == Vehicle.bicycle;
// enum도 인터페이스 구현이 가능하다.
@override
int compareTo(Vehicle other) => carbonFootprint - other.carbonFootprint;
}
6. 비동기 제어
Dart에서는 비동기(Asynchronous)를 지원하기 위해서 Future
와 Stream
객체를 지원한다. Future는 promise 패턴에 대한 Dart의 구현체라고 할 수 있고, Stream은 연속된 Future의 집합체를 의미한다고 볼 수 있다.
// await를 사용하는 코드는 async로 표시된 함수 내에 있어야 한다.
Future<void> checkVersion() async {
// try, catch를 통해 예외를 처리할 수 있다.
try {
version = await lookUpVersion();
} catch (e) {
// 버전을 조회할 수 없을 경우 ...
}
// version 변수를 사용...
}
void main() async {
checkVersion(); // await를 사용하지 않는 경우, 동시성을 보장하지 않는다.
print('In main: version is ${await lookUpVersion()}');
// await를 생성하는 배열을 순회하는 경우, await for로 순서를 보장한다. (Stream)
await for (final request in requestServer) {
handleRequest(request);
}
}
정리
오늘 살펴본 Dart 언어는 기존 typeScript나 Java와 같은 언어에서 경험해본 기능을 언어 목적에 맞춰 다양한 방식으로 지원하고 있다. 스크립트 언어의 특징도 살펴볼 수 있고, 클래스 기반의 언어의 특성도 상당히 많다. 이런 점이 처음 접할 때는 어려움으로 느껴지기도 한다. Dart 언어는 javaScript로 변환되는 웹 플랫폼과, Flutter로 대변되는 네이티브 앱 플랫폼을 지원하기 위한 언어이기 때문에, 각종 언어가 혼재된 듯한 모습은 오히려 의도적인 것이라 생각해볼 수 있다. Flutter를 사용하게 되면 오늘 살펴본 언어의 특성과 기능을 대부분 활용하게 되니 꼼꼼히 공부해보는 것이 좋을 것 같다.