Dart 버전3
이 문서 주변 탐색
주제 태그, 링크, 시리즈 흐름을 중심으로 옆으로 이동할 수 있습니다.
시리즈 흐름
이 문서는 아직 읽기 시리즈에 연결되지 않았습니다.
관련 문서
같이 읽을 만한 관련 문서가 아직 없습니다.
이 문서를 참조하는 문서
이 문서를 참조하는 다른 문서가 아직 없습니다.
1. Dart 3 개요
Dart 3가 중요한 이유
변경점 | 영향 |
|---|---|
Null Safety 필수 | 기존 코드 마이그레이션 필요 |
Records | 클래스 없이 여러 값 반환 가능 |
Patterns | switch문 완전히 새로워짐 |
Sealed Classes | 상태 관리 패턴 개선 |
버전 확인
dart --version
# Dart SDK version: 3.x.xpubspec.yaml 설정
environment:
sdk: '>=3.0.0 <4.0.0'2. 100% Null Safety 필수화
Dart 2 vs Dart 3
// Dart 2: null safety 선택 가능 (// @dart=2.9로 비활성화 가능했음)
// Dart 3: 무조건 null safety 적용됨!기본 규칙
// null 불가능 (기본)
String name = 'Hello';
// name = null; // ❌ 컴파일 에러!
// null 허용 (? 붙이기)
String? nickname = null; // ✅ OK
// null 체크 필수
void greet(String? name) {
// print(name.length); // ❌ 에러! null일 수 있음
// 방법 1: null 체크
if (name != null) {
print(name.length); // ✅ OK
}
// 방법 2: ?. (null이면 null 반환)
print(name?.length); // ✅ OK
// 방법 3: ?? (null이면 기본값)
print(name ?? '손님'); // ✅ OK
// 방법 4: ! (null 아님 확신할 때만)
// print(name!.length); // ⚠️ null이면 런타임 에러!
}3. Records (레코드)
핵심 개념
여러 값을 하나로 묶어서 반환할 수 있는 새로운 타입
이전 방식 vs Dart 3
// 이전: 클래스를 만들어야 했음
class UserResult {
final String name;
final int age;
final bool isActive;
UserResult(this.name, this.age, this.isActive);
}
UserResult getUser() {
return UserResult('홍길동', 25, true);
}
// Dart 3: Record로 간단하게!
(String, int, bool) getUser() {
return ('홍길동', 25, true);
}기본 사용법
// Record 생성
var person = ('홍길동', 25);
// 인덱스로 접근 ($1, $2, ...)
print(person.$1); // 홍길동
print(person.$2); // 25이름 있는 Record
// 이름 붙이기
({String name, int age}) getUser() {
return (name: '홍길동', age: 25);
}
void main() {
var user = getUser();
print(user.name); // 홍길동
print(user.age); // 25
}혼합 사용
// 위치 기반 + 이름 기반 혼합
(String, int, {bool isActive}) getUser() {
return ('홍길동', 25, isActive: true);
}
void main() {
var user = getUser();
print(user.$1); // 홍길동
print(user.$2); // 25
print(user.isActive); // true
}구조 분해로 받기
(String, int) getUser() {
return ('홍길동', 25);
}
void main() {
// 구조 분해
var (name, age) = getUser();
print('$name, $age세'); // 홍길동, 25세
// 타입 명시
var (String n, int a) = getUser();
}Record 비교
// Record는 값으로 비교됨 (구조와 값이 같으면 동일)
var r1 = (1, 2);
var r2 = (1, 2);
print(r1 == r2); // true
var r3 = (name: '철수', age: 20);
var r4 = (name: '철수', age: 20);
print(r3 == r4); // true4. Patterns (패턴 매칭)
핵심 개념
값의 구조와 내용을 검사하고 분해하는 강력한 기능
switch 표현식
// 이전: switch 문 (값 반환 불가)
String getGradeOld(int score) {
String grade;
switch (score ~/ 10) {
case 10:
case 9:
grade = 'A';
break;
case 8:
grade = 'B';
break;
default:
grade = 'C';
}
return grade;
}
// Dart 3: switch 표현식 (값 반환 가능!)
String getGrade(int score) {
return switch (score ~/ 10) {
10 || 9 => 'A', // || 로 여러 케이스
8 => 'B',
7 => 'C',
_ => 'F', // _ 는 default
};
}타입 패턴
String describe(Object obj) {
return switch (obj) {
int() => '정수임',
double() => '실수임',
String() => '문자열임',
List() => '리스트임',
_ => '뭔지 모르겠음',
};
}
void main() {
print(describe(42)); // 정수임
print(describe('hello')); // 문자열임
print(describe([1, 2])); // 리스트임
}값 추출 패턴
String describe(Object obj) {
return switch (obj) {
int n => '정수: $n',
String s => '문자열: $s',
[int a, int b] => '정수 2개: $a, $b',
{'name': String name} => '이름: $name',
_ => '알 수 없음',
};
}
void main() {
print(describe(42)); // 정수: 42
print(describe('hello')); // 문자열: hello
print(describe([1, 2])); // 정수 2개: 1, 2
print(describe({'name': '홍길동'})); // 이름: 홍길동
}Guard (when 절)
String checkNumber(int n) {
return switch (n) {
int x when x < 0 => '음수',
int x when x == 0 => '영',
int x when x > 0 && x < 10 => '한 자리 양수',
int x when x >= 10 && x < 100 => '두 자리',
_ => '세 자리 이상',
};
}
void main() {
print(checkNumber(-5)); // 음수
print(checkNumber(0)); // 영
print(checkNumber(7)); // 한 자리 양수
print(checkNumber(42)); // 두 자리
print(checkNumber(100)); // 세 자리 이상
}if-case 문
void processJson(Map<String, dynamic> json) {
// 패턴이 매칭되면 실행
if (json case {'name': String name, 'age': int age}) {
print('이름: $name, 나이: $age');
} else {
print('유효하지 않은 데이터');
}
}
void main() {
processJson({'name': '홍길동', 'age': 25}); // 이름: 홍길동, 나이: 25
processJson({'name': '철수'}); // 유효하지 않은 데이터
}리스트 패턴
String analyzeList(List<int> list) {
return switch (list) {
[] => '빈 리스트',
[var single] => '요소 1개: $single',
[var first, var second] => '요소 2개: $first, $second',
[var first, ...var rest] => '첫 요소: $first, 나머지: $rest',
};
}
void main() {
print(analyzeList([])); // 빈 리스트
print(analyzeList([1])); // 요소 1개: 1
print(analyzeList([1, 2])); // 요소 2개: 1, 2
print(analyzeList([1, 2, 3, 4])); // 첫 요소: 1, 나머지: [2, 3, 4]
}객체 패턴
class Point {
final int x;
final int y;
Point(this.x, this.y);
}
String describePoint(Point p) {
return switch (p) {
Point(x: 0, y: 0) => '원점',
Point(x: 0, y: var y) => 'Y축 위: y=$y',
Point(x: var x, y: 0) => 'X축 위: x=$x',
Point(x: var x, y: var y) when x == y => '대각선 위: $x',
Point(x: var x, y: var y) => '일반 점: ($x, $y)',
};
}
void main() {
print(describePoint(Point(0, 0))); // 원점
print(describePoint(Point(0, 5))); // Y축 위: y=5
print(describePoint(Point(3, 0))); // X축 위: x=3
print(describePoint(Point(4, 4))); // 대각선 위: 4
print(describePoint(Point(2, 5))); // 일반 점: (2, 5)
}5. Sealed Classes
핵심 개념
같은 파일 내에서만 상속 가능
컴파일러가 모든 하위 타입을 알고 있음
switch에서 모든 케이스 처리 강제
기본 사용법
// sealed = 봉인된 클래스
sealed class Shape {}
class Circle extends Shape {
final double radius;
Circle(this.radius);
}
class Rectangle extends Shape {
final double width;
final double height;
Rectangle(this.width, this.height);
}
class Triangle extends Shape {
final double base;
final double height;
Triangle(this.base, this.height);
}완전한 switch 처리
double getArea(Shape shape) {
// 모든 케이스 처리 안 하면 컴파일 에러!
return switch (shape) {
Circle(radius: var r) => 3.14159 * r * r,
Rectangle(width: var w, height: var h) => w * h,
Triangle(base: var b, height: var h) => 0.5 * b * h,
// 새로운 Shape 추가하면 여기도 수정 필요 (컴파일러가 알려줌!)
};
}
void main() {
print(getArea(Circle(5))); // 78.53975
print(getArea(Rectangle(4, 5))); // 20.0
print(getArea(Triangle(6, 4))); // 12.0
}상태 관리 패턴
sealed class LoadingState<T> {}
class Initial<T> extends LoadingState<T> {}
class Loading<T> extends LoadingState<T> {}
class Success<T> extends LoadingState<T> {
final T data;
Success(this.data);
}
class Error<T> extends LoadingState<T> {
final String message;
Error(this.message);
}
// UI 빌드
String buildUI(LoadingState<String> state) {
return switch (state) {
Initial() => '시작 전',
Loading() => '로딩 중...',
Success(data: var d) => '성공: $d',
Error(message: var m) => '에러: $m',
};
}
void main() {
print(buildUI(Initial())); // 시작 전
print(buildUI(Loading())); // 로딩 중...
print(buildUI(Success('데이터!'))); // 성공: 데이터!
print(buildUI(Error('네트워크 오류'))); // 에러: 네트워크 오류
}sealed vs abstract vs final
키워드 | 상속 | implements | 다른 파일에서 확장 |
|---|---|---|---|
| ✅ | ✅ | ✅ |
| ✅ | ✅ | ❌ (같은 파일만) |
| ❌ | ❌ | ❌ |
6. Class Modifiers
새로운 클래스 수정자들
수정자 | extends | implements | mixin | 다른 파일 |
|---|---|---|---|---|
(없음) | ✅ | ✅ | ❌ | ✅ |
| ✅ | ❌ | ❌ | ✅ |
| ❌ | ✅ | ❌ | ✅ |
| ❌ | ❌ | ❌ | ❌ |
| ✅ | ✅ | ❌ | ❌ |
| ❌ | ❌ | ✅ | ✅ |
base class
// 상속만 허용, implements 금지
base class Animal {
void breathe() => print('숨쉬기');
}
// ✅ OK: 상속
class Dog extends Animal {
void bark() => print('멍멍');
}
// ❌ 에러: implements 불가
// class Cat implements Animal {}interface class
// implements만 허용, 상속 금지
interface class Drawable {
void draw() => print('그리기');
}
// ❌ 에러: 상속 불가
// class Circle extends Drawable {}
// ✅ OK: implements
class Circle implements Drawable {
@override
void draw() => print('원 그리기');
}final class
// 상속, implements 둘 다 금지
final class Config {
final String apiKey;
Config(this.apiKey);
}
// ❌ 에러: 상속 불가
// class MyConfig extends Config {}
// ❌ 에러: implements 불가
// class MyConfig implements Config {}mixin class
// 클래스이면서 mixin으로도 사용 가능
mixin class Swimmer {
void swim() => print('수영');
}
// 상속으로 사용
class Fish extends Swimmer {}
// mixin으로 사용
class Duck extends Animal with Swimmer {}7. Switch Expression
기본 문법
// 화살표(=>) 사용, 값 반환
var result = switch (value) {
패턴1 => 결과1,
패턴2 => 결과2,
_ => 기본값,
};실용 예시들
// HTTP 상태 코드
String getStatus(int code) => switch (code) {
200 => 'OK',
201 => 'Created',
400 => 'Bad Request',
401 => 'Unauthorized',
404 => 'Not Found',
500 => 'Server Error',
_ => 'Unknown',
};
// 요일 이름
String getDayName(int day) => switch (day) {
1 => '월요일',
2 => '화요일',
3 => '수요일',
4 => '목요일',
5 => '금요일',
6 || 7 => '주말', // 여러 값 매칭
_ => '잘못된 요일',
};
// 등급 계산
String getGrade(int score) => switch (score) {
>= 90 => 'A',
>= 80 => 'B',
>= 70 => 'C',
>= 60 => 'D',
_ => 'F',
};8. 구조 분해 (Destructuring)
리스트 구조 분해
void main() {
// 기본
var [a, b, c] = [1, 2, 3];
print('$a, $b, $c'); // 1, 2, 3
// 일부만 추출
var [first, _, last] = [1, 2, 3]; // _는 무시
print('$first, $last'); // 1, 3
// 나머지 요소 (...)
var [head, ...tail] = [1, 2, 3, 4, 5];
print(head); // 1
print(tail); // [2, 3, 4, 5]
// 중간 무시
var [x, ..., z] = [1, 2, 3, 4, 5];
print('$x, $z'); // 1, 5
}Map 구조 분해
void main() {
var user = {'name': '홍길동', 'age': 25, 'city': '서울'};
// 필요한 값만 추출
var {'name': name, 'age': age} = user;
print('$name, $age'); // 홍길동, 25
}Record 구조 분해
void main() {
// 위치 기반
var (name, age) = ('홍길동', 25);
print('$name, $age'); // 홍길동, 25
// 이름 기반
var (name: n, age: a) = (name: '철수', age: 30);
print('$n, $a'); // 철수, 30
}객체 구조 분해
class Point {
final int x;
final int y;
Point(this.x, this.y);
}
void main() {
var point = Point(10, 20);
// 객체에서 값 추출
var Point(:x, :y) = point;
print('$x, $y'); // 10, 20
}for 문에서 구조 분해
void main() {
var users = [
('홍길동', 25),
('철수', 30),
('영희', 28),
];
for (var (name, age) in users) {
print('$name: $age세');
}
// 홍길동: 25세
// 철수: 30세
// 영희: 28세
}9. 실전 활용 예시
API 응답 처리
// API 결과를 sealed class로 정의
sealed class ApiResult<T> {}
class ApiSuccess<T> extends ApiResult<T> {
final T data;
ApiSuccess(this.data);
}
class ApiError<T> extends ApiResult<T> {
final int code;
final String message;
ApiError(this.code, this.message);
}
// API 호출 함수
Future<ApiResult<Map<String, dynamic>>> fetchUser(int id) async {
try {
// 실제로는 HTTP 요청
await Future.delayed(Duration(seconds: 1));
if (id == 1) {
return ApiSuccess({'name': '홍길동', 'age': 25});
} else {
return ApiError(404, '사용자를 찾을 수 없음');
}
} catch (e) {
return ApiError(500, '서버 오류: $e');
}
}
// 사용
void main() async {
var result = await fetchUser(1);
var message = switch (result) {
ApiSuccess(data: var d) => '성공: ${d['name']}',
ApiError(code: 404, message: var m) => '찾을 수 없음: $m',
ApiError(code: var c, message: var m) => '에러($c): $m',
};
print(message);
}JSON 파싱
void parseUserJson(Map<String, dynamic> json) {
// if-case로 안전하게 파싱
if (json case {
'name': String name,
'age': int age,
'email': String email,
}) {
print('사용자: $name ($age세)');
print('이메일: $email');
} else {
print('유효하지 않은 JSON');
}
}
void main() {
parseUserJson({
'name': '홍길동',
'age': 25,
'email': 'hong@example.com',
});
}상태 머신
sealed class AuthState {}
class Unauthenticated extends AuthState {}
class Authenticating extends AuthState {}
class Authenticated extends AuthState {
final String userId;
final String token;
Authenticated(this.userId, this.token);
}
class AuthError extends AuthState {
final String error;
AuthError(this.error);
}
// 상태에 따른 UI
String getAuthUI(AuthState state) => switch (state) {
Unauthenticated() => '로그인 버튼 표시',
Authenticating() => '로딩 스피너 표시',
Authenticated(userId: var id) => '환영합니다, $id님!',
AuthError(error: var e) => '에러: $e',
};계산기
sealed class Expression {}
class Number extends Expression {
final double value;
Number(this.value);
}
class Add extends Expression {
final Expression left;
final Expression right;
Add(this.left, this.right);
}
class Multiply extends Expression {
final Expression left;
final Expression right;
Multiply(this.left, this.right);
}
double evaluate(Expression expr) => switch (expr) {
Number(value: var v) => v,
Add(left: var l, right: var r) => evaluate(l) + evaluate(r),
Multiply(left: var l, right: var r) => evaluate(l) * evaluate(r),
};
void main() {
// (2 + 3) * 4 = 20
var expr = Multiply(
Add(Number(2), Number(3)),
Number(4),
);
print(evaluate(expr)); // 20.0
}10. 마이그레이션 가이드
Dart 2 → Dart 3 체크리스트
sdk: '>=3.0.0 <4.0.0'설정모든 null safety 이슈 해결
더 이상 사용 안 되는 문법 제거
새 기능 활용해 코드 개선 (선택)
단계별 마이그레이션
# 1. 현재 버전 확인
dart --version
# 2. pubspec.yaml 업데이트
# sdk: '>=3.0.0 <4.0.0'
# 3. 의존성 업데이트
dart pub upgrade
# 4. 분석 실행
dart analyze
# 5. 에러 수정자주 발생하는 문제
문제 | 해결 |
|---|---|
| switch에서 모든 케이스 처리 |
| null safety 적용 |
| sealed class switch에서 모든 케이스 추가 |
Dart 3 기능 요약표
기능 | 키워드/문법 | 용도 |
|---|---|---|
Records |
| 여러 값 묶어서 반환 |
패턴 매칭 |
| 값 검사 및 분해 |
Sealed Classes |
| 완전한 타입 계층 |
Class Modifiers |
| 클래스 확장 제어 |
Switch Expression |
| 값 반환하는 switch |
구조 분해 |
| 값 추출 |
Guard |
| 패턴에 조건 추가 |
if-case |
| 조건부 패턴 매칭 |
본 글은 AI의 도움을 받아 작성되었습니다.
읽기 시리즈
현재 위치를 확인하고, 흐름을 따라 바로 다음 읽기로 이어갈 수 있습니다.