Flutter 학습 로드맵 Phase 1 - 첫 번째 글
iOS 개발자가 Dart를 처음 접할 때, Swift와 비교하며 빠르게 익히는 핵심 문법 정리
들어가며
iOS 개발자로서 Flutter에 입문하면 가장 먼저 마주하는 것이 Dart 언어다.
다행히 Dart와 Swift는 현대적 언어 설계를 공유하고 있어서, Swift 경험이 있다면 Dart를 빠르게 익힐 수 있다.
이 글에서는 Swift 문법과 1:1로 비교하며 Dart의 핵심을 정리한다.
1. 변수 선언
Swift
var name = "원식" // 변경 가능
let age = 25 // 변경 불가 (런타임 상수)
Dart
var name = '원식'; // 변경 가능 (타입 추론)
final age = 25; // 변경 불가 (런타임 상수, Swift의 let과 동일)
const pi = 3.14; // 컴파일타임 상수 (Swift에는 없는 개념)
핵심 차이점
| 구분 | Swift | Dart |
|---|---|---|
| 변경 가능 | var |
var |
| 런타임 상수 | let |
final |
| 컴파일타임 상수 | 없음 (static let으로 유사 구현) |
const |
포인트: Dart의 const는 컴파일 시점에 값이 결정되어야 한다. final은 런타임에 한 번만 할당되면 된다.
예를 들어 final now = DateTime.now();는 가능하지만, const now = DateTime.now();는 불가능하다.
2. 기본 타입
Swift
let count: Int = 10
let price: Double = 9.99
let name: String = "Flutter"
let isActive: Bool = true
Dart
int count = 10;
double price = 9.99;
String name = 'Flutter';
bool isActive = true;
핵심 차이점
- Swift는
Int,Double등 대문자로 시작 → Dart도int,double은 소문자지만 클래스다 - Dart에서 모든 변수는 객체의 참조다.
int도 객체다 - Dart의
num타입은int와double의 상위 타입이다 (Swift에는 없음) - 문자열은 작은따옴표(
')와 큰따옴표(") 모두 사용 가능 (Swift는 큰따옴표만)
3. Null Safety
Swift와 Dart 모두 Null Safety를 지원한다. 문법도 매우 유사하다.
Swift
var name: String? = nil // Optional
let length = name?.count // Optional Chaining
let safeName = name ?? "기본값" // Nil Coalescing
let forcedName = name! // Force Unwrap
Dart
String? name = null; // Nullable
int? length = name?.length; // Null-aware access
String safeName = name ?? '기본값'; // Null-aware operator
String forcedName = name!; // Null assertion
핵심 차이점
- 문법이 거의 동일하다 (
?,??,!) - Swift의
if let,guard let바인딩 → Dart에는 직접적인 대응이 없다. Dart 3.0의 패턴 매칭으로 일부 케이스는 유사하게 처리 가능하지만, 완전한 대체는 아니다 - Dart는
late키워드로 나중에 초기화할 non-nullable 변수를 선언할 수 있다
late String description; // 나중에 반드시 초기화해야 함
// 초기화 전에 접근하면 런타임 에러 발생 (Swift의 IUO와 다른 개념)
// late는 "아직 값이 없지만, 사용 전에 반드시 넣겠다"는 의미
4. 컬렉션 타입
Swift
// Array
var fruits: [String] = ["사과", "바나나", "딸기"]
// Dictionary
var scores: [String: Int] = ["수학": 90, "영어": 85]
// Set
var uniqueNumbers: Set<Int> = [1, 2, 3]
Dart
// List (Swift의 Array)
List<String> fruits = ['사과', '바나나', '딸기'];
// 타입 추론 버전
var fruits2 = <String>['사과', '바나나', '딸기'];
// Map (Swift의 Dictionary)
Map<String, int> scores = {'수학': 90, '영어': 85};
// Set
Set<int> uniqueNumbers = {1, 2, 3};
핵심 차이점
| 구분 | Swift | Dart |
|---|---|---|
| 배열 | [String] 또는 Array<String> |
List<String> |
| 딕셔너리 | [String: Int] |
Map<String, int> |
| 집합 | Set<Int> |
Set<int> |
| 스프레드 연산자 | 없음 | ... 지원 |
// Dart의 스프레드 연산자 - 컬렉션 합치기가 간편하다
var list1 = [1, 2, 3];
var list2 = [0, ...list1, 4]; // [0, 1, 2, 3, 4]
// 조건부 컬렉션 요소 - UI 빌드 시 매우 유용
var nav = [
'Home',
'Settings',
if (isAdmin) 'Admin',
];
5. 함수
Swift
func greet(name: String, greeting: String = "안녕") -> String {
return "\(greeting), \(name)!"
}
// 호출
greet(name: "원식")
greet(name: "원식", greeting: "하이")
Dart
String greet(String name, {String greeting = '안녕'}) {
return '$greeting, $name!';
}
// 호출
greet('원식');
greet('원식', greeting: '하이');
핵심 차이점
- Swift는 기본적으로 argument label 사용 → Dart는 positional parameter가 기본
- Dart의 named parameter는
{}로 감싸서 선언, 호출 시 이름 필요 - Dart의 named parameter는 기본적으로 optional →
required키워드로 필수화
// required named parameter
void createUser({required String name, required int age}) {
print('$name, $age');
}
// positional optional parameter - []로 감싼다
void sayHello(String name, [String? title]) {
print('Hello ${title ?? ""} $name');
}
화살표 함수
// Swift - 클로저
let add = { (a: Int, b: Int) -> Int in a + b }
// Dart - 화살표 함수 (한 줄 표현식)
int add(int a, int b) => a + b;
6. 클래스
Swift
class Person {
let name: String
var age: Int
init(name: String, age: Int) {
self.name = name
self.age = age
}
func introduce() -> String {
return "저는 \(name)이고, \(age)살입니다."
}
}
Dart
class Person {
final String name;
int age;
Person(this.name, this.age); // 축약 생성자
// Named constructor
Person.anonymous() : name = '익명', age = 0;
String introduce() {
return '저는 $name이고, $age살입니다.';
}
}
핵심 차이점
- Dart는
this.name축약 생성자를 지원 → 보일러플레이트가 적다 - Dart는 Named Constructor를 지원 (
Person.anonymous()) - Swift의
self→ Dart에서도this(보통 생략) - Dart에는 Swift의
struct처럼 사용자가 정의하는 값 타입이 없다
주의: Swift 개발자가 가장 주의할 점은 Dart에 사용자 정의 값 타입(struct)이 없다는 것이다.
클래스는 모두 참조 타입이다. 단, Dart 3.0부터 도입된 Record는 값 의미론(value semantics)을 가진다.
Record는 Swift의 Tuple과 유사하며, 간단한 값 묶음을 표현할 때 사용한다.
void main() {
// Dart Record - Swift의 Tuple과 유사 (값 타입)
(String, int) person = ('원식', 25);
print(person.$1); // '원식'
print(person.$2); // 25
// Named field record
({String name, int age}) namedPerson = (name: '원식', age: 25);
print(namedPerson.name); // '원식'
}
7. 상속과 프로토콜
Swift
protocol Describable {
func describe() -> String
}
class Animal {
let name: String
init(name: String) { self.name = name }
}
class Dog: Animal, Describable {
func describe() -> String {
return "\(name)는 강아지입니다."
}
}
Dart
// abstract class = Swift의 protocol과 유사
abstract class Describable {
String describe();
}
class Animal {
final String name;
Animal(this.name);
}
class Dog extends Animal implements Describable {
Dog(super.name);
@override
String describe() {
return '$name는 강아지입니다.';
}
}
핵심 차이점
| 구분 | Swift | Dart |
|---|---|---|
| 프로토콜/인터페이스 | protocol |
abstract class 또는 abstract interface class |
| 상속 | : |
extends |
| 프로토콜 채택 | : (상속과 동일 문법) |
implements |
| 다중 상속 | 불가 | 불가 (하지만 mixin 지원) |
Mixin - Dart의 강력한 기능
mixin Swimming {
void swim() => print('수영 중!');
}
mixin Flying {
void fly() => print('비행 중!');
}
class Duck extends Animal with Swimming, Flying {
Duck(super.name);
}
void main() {
var duck = Duck('오리');
duck.swim(); // 수영 중!
duck.fly(); // 비행 중!
}
Swift에서는 protocol extension으로 기본 구현을 제공하지만, Dart의 mixin은 상태(변수)까지 포함할 수 있어 더 강력하다.
8. 비동기 프로그래밍
이 부분은 Swift 개발자에게 가장 친숙할 것이다. Swift Concurrency와 매우 유사하다.
Swift
func fetchUser() async throws -> User {
let data = try await URLSession.shared.data(from: url)
return try JSONDecoder().decode(User.self, from: data.0)
}
// 호출
Task {
let user = try await fetchUser()
}
Dart
Future<User> fetchUser() async {
final response = await http.get(Uri.parse(url));
return User.fromJson(jsonDecode(response.body));
}
// 호출
void main() async {
final user = await fetchUser();
}
핵심 차이점
| 구분 | Swift | Dart |
|---|---|---|
| 반환 타입 | async 키워드 사용 |
Future<T> 반환 |
| 에러 처리 | throws + try |
try-catch (throws 선언 불필요) |
| 비동기 호출 | await |
await |
| 스트림 | AsyncSequence |
Stream |
// Dart Stream - Swift의 AsyncSequence와 유사
Stream<int> countStream(int max) async* {
for (int i = 0; i < max; i++) {
await Future.delayed(Duration(seconds: 1));
yield i;
}
}
// 사용 (async 함수 내에서)
Future<void> main() async {
await for (var count in countStream(5)) {
print(count); // 0, 1, 2, 3, 4 (1초 간격)
}
}
9. 에러 처리
Swift
enum NetworkError: Error {
case notFound
case serverError(code: Int)
}
func fetchData() throws {
throw NetworkError.serverError(code: 500)
}
do {
try fetchData()
} catch NetworkError.notFound {
print("Not Found")
} catch NetworkError.serverError(let code) {
print("Server Error: \(code)")
} catch {
print("Unknown: \(error)")
}
Dart
class NetworkException implements Exception {
final String message;
final int? code;
NetworkException(this.message, {this.code});
}
void fetchData() {
throw NetworkException('Server Error', code: 500);
}
void main() {
try {
fetchData();
} on NetworkException catch (e) {
print('Network: ${e.message}, code: ${e.code}');
} on FormatException {
print('Format Error');
} catch (e) {
print('Unknown: $e');
}
}
핵심 차이점
- Swift는
throws선언 필수 → Dart는 아무 함수나 throw 가능 (선언 불필요) - Swift의
do-catch→ Dart의try-catch - Dart는
on 타입 catch (e)문법으로 특정 예외 타입을 잡는다 - Dart에서는 어떤 객체든 throw 가능 (문자열도 가능하지만 비권장)
10. 패턴 매칭 (Dart 3.0+)
Dart 3.0에서 추가된 패턴 매칭은 Swift의 switch-case와 유사하다.
Swift
enum Status {
case success(data: String)
case error(code: Int)
}
let result: Status = .success(data: "OK")
switch result {
case .success(let data):
print("성공: \(data)")
case .error(let code) where code >= 500:
print("서버 에러: \(code)")
case .error(let code):
print("에러: \(code)")
}
Dart
sealed class Status {}
class Success extends Status {
final String data;
Success(this.data);
}
class Error extends Status {
final int code;
Error(this.code);
}
void main() {
var result = Success('OK');
switch (result) {
case Success(data: var d):
print('성공: $d');
case Error(code: var c) when c >= 500:
print('서버 에러: $c');
case Error(code: var c):
print('에러: $c');
}
}
핵심 차이점
- Swift의
enumassociated value → Dart의sealed class+ 하위 클래스 - Swift의
where→ Dart의when - Dart 3.0의
sealed class는 exhaustive switching을 보장 (Swift enum처럼)
한눈에 보는 비교표
| 개념 | Swift | Dart |
|---|---|---|
| 변경 가능 변수 | var |
var |
| 런타임 상수 | let |
final |
| 컴파일타임 상수 | - | const |
| 문자열 보간 | "\(변수)" |
'$변수' 또는 '${표현식}' |
| 옵셔널 | Type? |
Type? |
| 배열 | [Type] |
List<Type> |
| 딕셔너리 | [K: V] |
Map<K, V> |
| 프로토콜 | protocol |
abstract interface class |
| 상속 | class A: B |
class A extends B |
| 구현 | class A: Protocol |
class A implements Interface |
| 비동기 반환 | () async -> T |
Future<T> |
| 스트림 | AsyncSequence |
Stream |
| 값 타입 | struct |
사용자 정의 불가 (Record로 제한적 대체) |
| 타입 별칭 | typealias |
typedef |
| 접근 제어 | private, internal, public |
_접두사 = private (라이브러리 단위) |
| 세미콜론 | 불필요 | 필수 |
마무리
Swift 개발자가 Dart를 배울 때 가장 크게 느끼는 차이점은 세 가지다:
- 세미콜론이 필수다 - 처음에는 자주 빼먹지만 IDE가 잡아준다
- 사용자 정의 값 타입이 없다 - 클래스는 모두 참조 타입이다. 불변 객체를 만들려면
final필드를 사용하고, 컴파일타임 상수가 필요하면const생성자를 추가한다. Record는 값 타입이지만 사용자 정의 메서드를 가질 수 없다 - 접근 제어가 언더스코어 기반이다 -
_로 시작하면 라이브러리 private, 그 외에는 모두 public
하지만 Null Safety, 비동기, 패턴 매칭 등 핵심 개념은 Swift와 매우 유사하다.
Swift에서 쌓은 개념적 이해를 그대로 가져오되, 문법만 Dart 방식으로 바꿔서 쓴다는 감각으로 접근하면 빠르게 적응할 수 있다.
참고 자료
'Flutter' 카테고리의 다른 글
| [ Flutter ] 프로젝트 구조 해부 (0) | 2026.04.23 |
|---|