Dart는 구글이 개발한 클라이언트 최적화 프로그래밍 언어로, 모바일, 데스크톱, 웹 등 모든 플랫폼에서 아름답고 빠른 사용자 인터페이스를 구축하기 위해 설계되었습니다. 특히 Flutter 프레임워크의 핵심 언어로 채택되면서, Dart는 현대 애플리케이션 개발의 중심으로 빠르게 자리 잡았습니다. 이 언어의 진정한 힘은 단순히 문법을 아는 것을 넘어, 그 이면에 있는 설계 철학과 핵심 원리를 이해할 때 비로소 발휘됩니다.
이 글은 Dart의 기본 문법을 나열하는 데 그치지 않습니다. 변수와 타입 시스템의 깊은 이해부터 시작하여, 객체 지향 프로그래밍의 정수, 그리고 반응형 UI의 근간이 되는 비동기 처리 모델에 이르기까지, Dart를 구성하는 핵심 원리들을 체계적으로 탐구합니다. 각 개념이 왜 그렇게 설계되었는지, 그리고 실제 견고하고 유지보수 가능한 애플리케이션을 구축하는 데 어떻게 활용될 수 있는지에 대한 깊이 있는 통찰을 제공하는 것을 목표로 합니다.
1. Dart 코드의 첫걸음: 변수와 자료형
모든 프로그래밍의 시작은 데이터를 저장하고 다루는 것입니다. Dart는 강력한 타입 시스템과 유연한 변수 선언 방식을 통해 개발자가 안정적이면서도 효율적으로 코드를 작성할 수 있도록 지원합니다.
1.1. 변수 선언의 세 가지 키워드: `var`, `final`, `const`
Dart에서 변수를 선언하는 키워드는 세 가지로, 각각의 역할과 사용 시점이 명확히 구분됩니다. 이를 올바르게 이해하는 것은 코드의 의도를 명확히 하고 성능을 최적화하는 첫걸음입니다.
-
var
: 타입 추론을 통한 유연한 선언
var
는 변수를 선언할 때 초기화되는 값을 바탕으로 컴파일러가 타입을 자동으로 추론하게 합니다. 이는 코드를 간결하게 만들어주지만, 한번 타입이 추론되면 다른 타입의 값을 할당할 수 없는 타입 안정성(Type Safety)을 보장합니다.
물론, 코드의 가독성을 위해 타입을 명시적으로 선언하는 것도 좋은 방법입니다. 특히 함수의 반환 값이나 매개변수처럼 경계가 명확한 곳에서는 타입을 명시하는 것이 권장됩니다.void main() { // name 변수는 'String' 타입으로 추론됩니다. var name = 'Dart'; // age 변수는 'int' 타입으로 추론됩니다. var age = 10; // 에러: 한번 추론된 타입은 변경할 수 없습니다. // name = 123; }
String getUserName() { String name = 'Flutter'; return name; }
-
final
: 변경 불가능한 런타임 상수
final
로 선언된 변수는 단 한 번만 값을 할당할 수 있습니다. 할당된 이후에는 값을 변경할 수 없어, 불변성(Immutability)을 보장하는 데 핵심적인 역할을 합니다.final
변수의 값은 런타임에 결정될 수 있습니다. 예를 들어, API 호출 결과나 클래스 생성 시점에 전달받는 값을 저장하는 데 적합합니다.class User { final String name; // 생성 시점에 값이 결정됨 final DateTime creationTime = DateTime.now(); // 객체 생성 시점에 값이 결정됨 User(this.name); void printInfo() { // 에러: final 변수는 재할당할 수 없습니다. // name = 'New Name'; print('User: $name, Created at: $creationTime'); } } void main() { final user = User('Alice'); user.printInfo(); }
-
const
: 예측 가능한 컴파일 타임 상수
const
는final
보다 더 강력한 불변성을 가집니다.const
로 선언된 변수는 컴파일 시점에 그 값이 반드시 결정되어야 합니다. 이는 컴파일러가 코드를 최적화하고 성능을 향상시키는 데 중요한 단서가 됩니다. Flutter에서 위젯을const
로 선언하면, 해당 위젯은 변경되지 않았을 때 다시 빌드되지 않아 성능상 큰 이점을 가집니다.void main() { const int maxUsers = 100; // 컴파일 시점에 100으로 확정 const String appName = 'My Awesome App'; // const 생성자를 사용한 객체 생성 // a와 b는 동일한 값을 가지므로, 동일한 메모리 주소를 참조합니다. const point1 = Point(1, 2); const point2 = Point(1, 2); print(identical(point1, point2)); // 출력: true // final과 const의 차이 final currentTime = DateTime.now(); // 런타임에 결정되므로 OK // const compileTimeError = DateTime.now(); // 컴파일 시점에 값을 알 수 없으므로 에러 } class Point { final int x; final int y; const Point(this.x, this.y); }
1.2. Dart의 타입 시스템과 Sound Null Safety
Dart 2.12부터 도입된 Sound Null Safety는 Dart 언어의 가장 중요한 특징 중 하나입니다. 이는 코드에서 널 포인터 예외(Null Pointer Exception)를 원천적으로 방지하여 애플리케이션의 안정성을 극대화합니다.
-
기본 자료형 심층 분석
num
: 숫자를 나타내는 추상 클래스로,int
(정수)와double
(실수)이 이를 상속받습니다.String
: 문자열을 나타냅니다. 작은따옴표('
)나 큰따옴표("
)로 감싸며,${expression}
형태의 문자열 보간(String Interpolation)을 지원합니다. 여러 줄 문자열은 삼중 따옴표('''
또는"""
)를 사용하고, 이스케이프 문자를 무시하는 Raw String은 문자열 앞에r
을 붙여 만듭니다.bool
:true
와false
두 가지 값을 갖는 불리언 타입입니다.dynamic
: 모든 타입의 값을 할당할 수 있는 특수 타입입니다. 타입 검사를 런타임으로 미루기 때문에, 꼭 필요한 경우(예: JSON 파싱)에만 제한적으로 사용해야 합니다. 남용할 경우 Null Safety의 이점을 잃고 예측 불가능한 런타임 에러를 유발할 수 있습니다.
-
핵심 개념: Sound Null Safety
기본적으로 모든 변수는 null이 될 수 없습니다(Non-nullable). 변수가 null 값을 가질 수 있음을 명시하려면 타입 뒤에 물음표(?
)를 붙여야 합니다.
Nullable 변수를 안전하게 사용하기 위해 Dart는 여러 연산자와 키워드를 제공합니다.void main() { int a = 5; // a = null; // 에러: Non-nullable 타입에는 null을 할당할 수 없습니다. String? nullableName = 'Bob'; nullableName = null; // OK: Nullable 타입이므로 null 할당 가능 // Nullable 변수 사용 시 주의점 // print(nullableName.length); // 에러: nullableName이 null일 수 있으므로 직접 접근 불가 }
?
(Nullable): 변수가 null 값을 가질 수 있음을 선언합니다. (예:String? name
)!
(Null Assertion): 개발자가 해당 변수가 절대 null이 아님을 컴파일러에게 보증할 때 사용합니다. 만약 런타임에 해당 변수가 null이라면 예외가 발생하므로, 값이 null이 아님이 100% 확실할 때만 사용해야 합니다.late
(지연 초기화): Non-nullable 변수를 선언 시점에 초기화할 수 없을 때 사용합니다.late
키워드는 "이 변수는 나중에 반드시 초기화될 것이니, 지금은 걱정하지 마라"고 컴파일러에게 알리는 역할을 합니다. 만약 초기화되기 전에 접근하면 런타임 에러가 발생합니다.
class Profile { late final String nickname; void initialize(String name) { nickname = name; // 나중에 초기화 } void printNickname() { print(nickname); // 초기화된 후에 접근해야 함 } } void main() { String? maybeName; // ... 어떤 로직 ... if (DateTime.now().hour > 12) { maybeName = 'Alice'; } // 방법 1: Null 체크 if (maybeName != null) { print(maybeName.length); } // 방법 2: Null-aware access (?.) - 아래 연산자 섹션에서 자세히 설명 print(maybeName?.length); // 방법 3: Null assertion (!) - null이 아님을 확신할 때 // print(maybeName!.length); // 만약 maybeName이 null이면 런타임 에러 발생! var profile = Profile(); // profile.printNickname(); // 에러: nickname이 아직 초기화되지 않음 profile.initialize('Dart Master'); profile.printNickname(); // 출력: Dart Master }
2. 코드의 표현력을 높이는 연산자
연산자는 데이터를 조작하고 결합하여 새로운 값을 만들어내는 도구입니다. Dart는 다른 언어에서 흔히 볼 수 있는 연산자 외에도 코드의 가독성과 안정성을 크게 향상시키는 독특하고 강력한 연산자들을 제공합니다.
2.1. Dart 특화 연산자 심층 탐구
-
타입 검사 연산자:
is
,is!
객체의 타입을 런타임에 확인하는 데 사용됩니다.if
문과 함께 사용하면, 해당 블록 내에서 컴파일러가 객체의 타입을 자동으로 캐스팅(Smart Casting)해줍니다.void printValue(Object value) { if (value is String) { // 이 블록 안에서 value는 String 타입으로 취급됩니다. print('The string has ${value.length} characters.'); } else if (value is int) { // 이 블록 안에서 value는 int 타입으로 취급됩니다. print('The number is ${value.isEven ? 'even' : 'odd'}.'); } } void main() { printValue('Hello Dart'); printValue(42); }
-
Null 관련 연산자:
?.
,??
,??=
Null Safety와 함께 사용될 때 진가를 발휘하는 연산자들입니다.?.
(Null-aware access): 객체가 null이 아니면 메서드나 속성에 접근하고, null이면 `null`을 반환합니다.if (obj != null) { obj.method(); }
와 같은 코드를 간결하게 줄여줍니다.??
(If-null operator): 왼쪽 피연산자가 null이 아니면 그 값을 반환하고, null이면 오른쪽 피연산자를 반환합니다. 기본값을 제공할 때 매우 유용합니다.??=
(Null-aware assignment): 변수가 null일 경우에만 값을 할당합니다. 이미 값이 있는 경우에는 아무 동작도 하지 않습니다.
void main() { String? name; // name이 null이므로, length에 접근하지 않고 null을 반환합니다. print(name?.length); // 출력: null String displayName = name ?? 'Guest'; // name이 null이므로 'Guest'가 할당됩니다. print(displayName); // 출력: Guest name = 'Alice'; displayName = name ?? 'Guest'; // name이 null이 아니므로 'Alice'가 할당됩니다. print(displayName); // 출력: Alice int? age; age ??= 20; // age가 null이므로 20이 할당됩니다. print(age); // 출력: 20 age ??= 30; // age가 null이 아니므로 아무 일도 일어나지 않습니다. print(age); // 출력: 20 }
-
Cascade 표기법 (
..
): 객체에 대한 연속적인 작업
객체에 대한 일련의 작업을 수행할 때 매우 유용한 문법입니다. 객체 자신을 반환하므로, 여러 메서드 호출이나 속성 설정을 연쇄적으로 수행할 수 있습니다.class Paint { String? color; double? strokeWidth; void draw() { print('Drawing with color $color and stroke width $strokeWidth'); } } void main() { // 일반적인 방식 var paint1 = Paint(); paint1.color = 'red'; paint1.strokeWidth = 5.0; paint1.draw(); // Cascade 표기법을 사용한 방식 var paint2 = Paint() ..color = 'blue' ..strokeWidth = 10.0 ..draw(); }
3. 로직의 구성 요소: 함수와 제어문
함수는 코드 재사용성의 기본 단위이며, 제어문은 프로그램의 실행 흐름을 결정합니다. Dart는 현대적인 함수 기능과 강력한 제어문을 통해 논리적이고 구조적인 코드 작성을 지원합니다.
3.1. 함수 정의의 모든 것
-
매개변수(Parameters): Dart 함수는 유연한 매개변수 정의 방식을 제공합니다.
- 위치 기반 매개변수(Positional Parameters): 순서에 따라 값이 전달되는 일반적인 방식입니다.
- 이름 기반 매개변수(Named Parameters): 중괄호
{}
로 묶어 정의하며, 호출 시 매개변수 이름을 명시해야 합니다. 코드의 가독성을 크게 향상시키며,required
키워드를 통해 필수 매개변수로 지정할 수 있습니다. - 기본값(Default Values): 이름 기반 매개변수나 위치 기반 옵셔널 매개변수(
[]
)에 기본값을 설정할 수 있습니다.
// 위치 기반과 이름 기반 매개변수를 함께 사용 void createUser(String name, {required int age, String country = 'Unknown'}) { print('User: $name, Age: $age, Country: $country'); } void main() { createUser('Bob', age: 30); // country는 기본값 'Unknown' 사용 createUser('Charlie', age: 25, country: 'USA'); }
-
Arrow Function (
=>
): 함수 본문이 단일 표현식으로 구성될 때 코드를 간결하게 만들어주는 축약 문법입니다. `=> expression;`은 `{ return expression; }`과 동일합니다. -
익명 함수 (람다)와 일급 객체: Dart에서 함수는 변수에 할당되거나, 다른 함수의 인자로 전달되거나, 함수의 반환 값이 될 수 있는 일급 객체(First-class Citizen)입니다. 이는 함수형 프로그래밍 패러다임을 지원하는 기반이 됩니다.
void main() { var numbers = [1, 2, 3, 4, 5]; // List의 forEach 메서드에 익명 함수를 전달 numbers.forEach((number) { print('Number: $number'); }); // 변수에 함수 할당 var loudify = (String msg) => '!!! ${msg.toUpperCase()} !!!'; print(loudify('hello')); // 출력: !!! HELLO !!! }
3.2. 프로그램 흐름 제어
Dart는 if-else
, for
, while
등 표준적인 제어문을 모두 지원합니다. 특히 주목할 만한 것은 Dart 3.0부터 도입된 강력한 패턴 매칭 기능이 적용된 switch
문입니다.
강력해진 switch
문과 패턴 매칭
기존의 switch
문은 정수나 문자열 같은 상수 값만 비교할 수 있었지만, 이제는 복잡한 데이터 구조를 분해하고 조건을 검사하는 패턴 매칭을 지원합니다.
// HTTP 상태 코드를 처리하는 예제
String handleResponse(int statusCode) {
return switch (statusCode) {
200 => 'Success',
401 => 'Unauthorized',
404 => 'Not Found',
500 => 'Internal Server Error',
// case 키워드와 when 절을 사용한 조건부 매칭
int code when code >= 400 && code < 500 => 'Client Error: $code',
_ => 'Unknown Error', // _ 는 와일드카드 패턴
};
}
void main() {
print(handleResponse(200)); // 출력: Success
print(handleResponse(403)); // 출력: Client Error: 403
}
4. 데이터의 조직화: 컬렉션 프레임워크
여러 개의 데이터를 효율적으로 관리하기 위해 Dart는 List, Set, Map과 같은 강력한 컬렉션 자료구조를 제공합니다. 또한, 컬렉션을 선언적으로 생성하고 조작할 수 있는 현대적인 기능들도 지원합니다.
List
: 순서가 있는 데이터의 집합입니다. 인덱스를 통해 요소에 접근할 수 있으며,map
,where
,reduce
등 다양한 고차 함수를 제공하여 데이터를 함수형 스타일로 처리할 수 있습니다.Set
: 중복된 요소를 허용하지 않는, 순서가 없는 데이터의 집합입니다. 데이터의 유일성을 보장해야 할 때 유용합니다.Map
: 키(Key)와 값(Value)의 쌍으로 이루어진 데이터 구조입니다. 각 키는 고유해야 하며, 키를 통해 값에 빠르게 접근할 수 있습니다.
컬렉션 고급 기법
Dart는 컬렉션을 더 간결하고 직관적으로 다룰 수 있는 문법을 제공합니다.
- Spread Operator (
...
,...?
): 다른 컬렉션의 모든 요소를 현재 컬렉션 안으로 펼쳐 넣을 때 사용합니다....?
는 null-aware 버전으로, 대상 컬렉션이 null이면 아무 작업도 수행하지 않습니다. - Collection
if
&for
: 컬렉션 리터럴 내에서 조건문이나 반복문을 사용하여 동적으로 요소를 추가할 수 있습니다. 이는 특히 Flutter에서 조건에 따라 다른 위젯을 포함시켜야 할 때 매우 유용합니다.
void main() {
var list1 = [1, 2, 3];
var list2 = [4, 5, 6];
var combinedList = [...list1, ...list2];
print(combinedList); // 출력: [1, 2, 3, 4, 5, 6]
bool includeAd = true;
var uiElements = [
'Header',
'Content',
if (includeAd) 'Advertisement', // Collection if
'Footer',
];
print(uiElements); // 출력: [Header, Content, Advertisement, Footer]
var categories = ['Tech', 'Life', 'Sports'];
var menuItems = [
'Home',
for (var category in categories) 'Category: $category', // Collection for
'About',
];
print(menuItems); // 출력: [Home, Category: Tech, Category: Life, Category: Sports, About]
}
5. 객체 지향 프로그래밍의 정수
Dart는 클래스 기반의 완벽한 객체 지향 언어입니다. 캡슐화, 상속, 다형성을 지원하며, 코드 재사용성과 유연성을 높이기 위한 Mixin이라는 독특한 개념도 제공합니다.
5.1. 생성자 심층 탐구
생성자는 객체가 생성될 때 호출되는 특별한 메서드입니다. Dart는 다양한 시나리오에 대응할 수 있도록 여러 종류의 생성자를 제공합니다.
- Named Constructor: 클래스 이름 뒤에
.이름
을 붙여 정의합니다. 객체가 생성되는 목적이나 방식을 명확하게 표현할 수 있습니다. 예를 들어,Vector.zero()
,Map.fromEntries()
등이 있습니다. - Factory Constructor:
factory
키워드를 사용하며, 항상 새로운 인스턴스를 생성하지 않을 수 있습니다. 싱글톤 패턴이나 캐시된 인스턴스를 반환하는 등 복잡한 객체 생성 로직을 캡슐화할 때 사용됩니다. - Constant Constructor:
const
키워드를 사용하며, 클래스의 모든 필드가final
이어야 합니다. 컴파일 타임 상수로 객체를 생성할 수 있게 해줍니다.
class Logger {
final String name;
static final Map<String, Logger> _cache = <String, Logger>{};
// private named constructor
Logger._internal(this.name);
// factory constructor for singleton pattern
factory Logger(String name) {
if (_cache.containsKey(name)) {
return _cache[name]!;
} else {
final logger = Logger._internal(name);
_cache[name] = logger;
return logger;
}
}
void log(String msg) {
print('$name: $msg');
}
}
void main() {
var logger1 = Logger('UI');
var logger2 = Logger('Network');
var logger3 = Logger('UI');
logger1.log('Button clicked');
logger2.log('Request sent');
// logger1과 logger3는 동일한 인스턴스입니다.
print(identical(logger1, logger3)); // 출력: true
}
5.2. 상속과 추상화
- 상속 (
extends
): 자식 클래스가 부모 클래스의 속성과 메서드를 물려받는 기능입니다. Dart는 단일 상속만 지원합니다. - 추상 클래스 (
abstract class
): 인스턴스를 직접 생성할 수 없으며, 다른 클래스가 상속받아야 할 공통적인 인터페이스나 구현의 일부를 정의하는 데 사용됩니다. - 인터페이스 (
implements
): Dart에서는 모든 클래스가 암묵적으로 인터페이스를 정의합니다. 따라서implements
키워드를 사용하면 특정 클래스의 인터페이스(메서드 시그니처)를 구현하도록 강제할 수 있습니다. 이는 상속 관계가 아니더라도 특정 기능을 갖도록 강제하는 데 유용하며, 다중 상속의 효과를 낼 수 있습니다.
abstract class Vehicle {
void move(); // 추상 메서드 (구현 없음)
void honk() {
print('Beep beep!'); // 구체적인 구현
}
}
class Car extends Vehicle {
@override
void move() {
print('The car is driving on the road.');
}
}
// Musician 클래스는 상속 관계가 아니지만, Honkable 인터페이스를 구현합니다.
abstract class Honkable {
void honk();
}
class Musician implements Honkable {
@override
void honk() {
print('The musician plays the trumpet!');
}
}
5.3. 코드 재사용의 혁신: `mixin`
Mixin은 상속 계층 구조에 얽매이지 않고 여러 클래스에 걸쳐 코드(메서드와 속성)를 재사용하는 방법입니다. with
키워드를 사용하여 클래스에 Mixin의 기능을 "혼합"할 수 있습니다. 이는 단일 상속의 한계를 극복하고 유연하게 기능을 조합할 수 있게 해주는 강력한 도구입니다.
mixin Flyer {
void fly() {
print("I'm flying!");
}
}
mixin Swimmer {
void swim() {
print("I'm swimming!");
}
}
class Bird with Flyer {}
class Duck extends Bird with Swimmer { // Bird를 상속받고 Swimmer 기능을 추가
void quack() {
print("Quack!");
}
}
void main() {
var duck = Duck();
duck.fly(); // from Flyer mixin
duck.swim(); // from Swimmer mixin
duck.quack(); // from Duck class
}
6. 반응형 애플리케이션의 핵심: 비동기 프로그래밍
현대 애플리케이션은 네트워크 요청, 파일 I/O, 데이터베이스 접근 등 시간이 걸리는 작업을 처리해야 합니다. 이러한 작업을 동기적으로 처리하면 UI가 멈추는(freezing) 현상이 발생합니다. Dart는 `Future`와 `Stream`, 그리고 `async`/`await` 문법을 통해 비동기 작업을 효율적이고 직관적으로 처리할 수 있는 환경을 제공합니다.
-
Future
: 미래의 어느 시점에 완료될 하나의 결과를 나타내는 객체입니다. 작업이 성공하면 값을 반환하고, 실패하면 에러를 반환합니다..then()
콜백을 통해 결과를 처리할 수 있습니다. -
async
와 `await`: 비동기 코드를 동기 코드처럼 보이게 만드는 문법적 설탕(Syntactic Sugar)입니다.async
로 표시된 함수 내에서 `await` 키워드를 사용하면 `Future`가 완료될 때까지 기다렸다가 그 결과를 반환받을 수 있습니다. 이는 콜백 지옥(Callback Hell)을 피하고 코드의 가독성을 크게 향상시킵니다.// 가상의 데이터 fetch 함수 Future<String> fetchUserData() { return Future.delayed(Duration(seconds: 2), () => 'John Doe'); } // then()을 사용한 전통적인 방식 void printUserDataOld() { print('Fetching user data...'); fetchUserData().then((name) { print('User: $name'); }).catchError((error) { print('Error: $error'); }); print('This line executes before data is fetched.'); } // async/await을 사용한 현대적인 방식 Future<void> printUserDataNew() async { print('Fetching user data...'); try { String name = await fetchUserData(); // 2초간 기다림 print('User: $name'); } catch (error) { print('Error: $error'); } print('This line executes AFTER data is fetched.'); } void main() async { await printUserDataNew(); }
-
Stream
: 시간이 지남에 따라 연속적으로 발생하는 비동기 이벤트(데이터)의 흐름을 나타냅니다. 파일 읽기, 웹소켓 통신, 사용자 입력 이벤트 등 여러 개의 값을 비동기적으로 전달받을 때 사용됩니다.listen()
메서드를 통해 스트림을 구독하고, 데이터가 도착할 때마다 콜백 함수를 실행할 수 있습니다.
7. 견고한 코드의 반석: 예외 처리
예상치 못한 오류는 언제나 발생할 수 있습니다. 잘 만들어진 애플리케이션은 이러한 오류를 우아하게 처리하여 비정상적으로 종료되지 않고, 사용자에게 유용한 피드백을 제공합니다. Dart는 try-catch-finally
블록과 사용자 정의 예외를 통해 강력한 에러 처리 메커니즘을 제공합니다.
Exception
vs.Error
:Exception
은 개발자가 예상하고 처리할 수 있는 예외 상황(예: 네트워크 연결 실패)을 의미합니다. 반면,Error
는 개발자의 논리적 실수로 인해 발생하는 프로그램의 비정상적인 상태(예: null 변수 접근)를 의미하며, 일반적으로는 잡아서(catch) 처리하지 않는 것이 원칙입니다.try-catch
: 예외가 발생할 가능성이 있는 코드를try
블록으로 감싸고,catch
블록에서 발생한 예외를 처리합니다.on
키워드를 사용하면 특정 타입의 예외만 선택적으로 처리할 수 있습니다.finally
: 예외 발생 여부와 관계없이 항상 실행되어야 하는 코드를finally
블록에 작성합니다. 파일 핸들이나 네트워크 소켓과 같은 자원을 안전하게 해제하는 데 주로 사용됩니다.throw
: 의도적으로 예외를 발생시킬 때 사용합니다. 애플리케이션의 특정 도메인에 맞는 사용자 정의 예외 클래스를 만들어throw
하면, 코드의 의도를 더 명확하게 전달할 수 있습니다.
class InsufficientFundsException implements Exception {
final String message;
InsufficientFundsException(this.message);
@override
String toString() => 'InsufficientFundsException: $message';
}
void withdraw(double amount, double balance) {
if (amount > balance) {
throw InsufficientFundsException('Cannot withdraw $amount. Balance is only $balance.');
}
print('Withdrawal successful.');
}
void main() {
try {
withdraw(100, 50);
} on InsufficientFundsException catch (e) {
print(e); // 사용자 정의 예외 처리
} catch (e) {
print('An unknown error occurred: $e'); // 그 외 예외 처리
} finally {
print('Transaction finished.');
}
}
8. Dart 생태계 활용: 패키지 관리
Dart의 강력함은 언어 자체뿐만 아니라, Pub.dev를 통해 공유되는 방대한 오픈소스 패키지 생태계에서도 나옵니다. Pub은 Dart의 공식 패키지 매니저로, 의존성 관리와 프로젝트 빌드를 자동화합니다.
pubspec.yaml
: 프로젝트의 메타데이터와 의존성을 정의하는 파일입니다.dependencies
섹션에는 애플리케이션 실행에 필요한 패키지를,dev_dependencies
섹션에는 테스트나 빌드 과정 등 개발 중에만 필요한 패키지를 명시합니다.- 시맨틱 버저닝(Semantic Versioning): Pub은 시맨틱 버저닝을 따릅니다. 캐럿(
^
) 기호는 호환성이 보장되는 범위 내에서 최신 마이너 버전까지 자동으로 업데이트하도록 허용합니다. (예:^1.2.3
은1.2.3
이상2.0.0
미만 버전을 의미) - 주요 명령어:
dart pub get
(의존성 설치),dart pub upgrade
(의존성 업데이트),dart pub outdated
(업데이트 가능한 패키지 확인) 등의 명령어를 통해 패키지를 관리합니다.
결론: 지속적인 학습을 향하여
지금까지 Dart의 핵심적인 원리들을 깊이 있게 살펴보았습니다. 타입 시스템과 Null Safety가 어떻게 코드의 안정성을 보장하는지, 강력한 객체 지향 기능과 Mixin이 어떻게 유연하고 재사용 가능한 코드를 가능하게 하는지, 그리고 비동기 모델이 어떻게 반응성이 뛰어난 애플리케이션의 기반이 되는지를 이해했습니다.
Dart는 끊임없이 발전하는 언어입니다. 여기서 다룬 개념들을 탄탄한 기초로 삼아 Flutter를 이용한 UI 개발, 서버 사이드 Dart(Aqueduct, Angel), 또는 Dart의 새로운 기능들을 꾸준히 탐구해 나간다면, 어떤 플랫폼에서든 뛰어난 애플리케이션을 만들어내는 개발자로 성장할 수 있을 것입니다. 코드는 생각의 표현이며, Dart는 그 생각을 명확하고, 안전하며, 효율적으로 표현할 수 있는 훌륭한 도구입니다.
0 개의 댓글:
Post a Comment