Dart 버전3

이 문서 주변 탐색

주제 태그, 링크, 시리즈 흐름을 중심으로 옆으로 이동할 수 있습니다.

시리즈 흐름

이 문서는 아직 읽기 시리즈에 연결되지 않았습니다.

관련 문서

같이 읽을 만한 관련 문서가 아직 없습니다.

이 문서를 참조하는 문서

이 문서를 참조하는 다른 문서가 아직 없습니다.

1. Dart 3 개요

Dart 3가 중요한 이유

변경점

영향

Null Safety 필수

기존 코드 마이그레이션 필요

Records

클래스 없이 여러 값 반환 가능

Patterns

switch문 완전히 새로워짐

Sealed Classes

상태 관리 패턴 개선

버전 확인

dart --version
# Dart SDK version: 3.x.x

pubspec.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);  // true

4. 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

다른 파일에서 확장

abstract

sealed

❌ (같은 파일만)

final

6. Class Modifiers

새로운 클래스 수정자들

수정자

extends

implements

mixin

다른 파일

(없음)

base

interface

final

sealed

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. 에러 수정

자주 발생하는 문제

문제

해결

The body might complete normally

switch에서 모든 케이스 처리

A value of type 'Null' can't be assigned

null safety 적용

Missing case clause

sealed class switch에서 모든 케이스 추가

Dart 3 기능 요약표

기능

키워드/문법

용도

Records

(값1, 값2)

여러 값 묶어서 반환

패턴 매칭

switch, case, when

값 검사 및 분해

Sealed Classes

sealed class

완전한 타입 계층

Class Modifiers

base, interface, final

클래스 확장 제어

Switch Expression

switch (x) { ... => ... }

값 반환하는 switch

구조 분해

var [a, b] = list

값 추출

Guard

when 조건

패턴에 조건 추가

if-case

if (x case 패턴)

조건부 패턴 매칭

🤖

본 글은 AI의 도움을 받아 작성되었습니다.

읽기 시리즈

현재 위치를 확인하고, 흐름을 따라 바로 다음 읽기로 이어갈 수 있습니다.

Reading Series

Dart
0편

현재 읽는 문서

Dart 버전3

Dart 언어에 대해서 알아보기

이전 문서

시리즈의 시작 문서입니다.

다음 문서

시리즈의 마지막 문서입니다.