"주문 상태"를 코드로 표현해야 한다고 생각해봅시다. String으로 쓰면 'pending', 'processing', 'completed', 'cancelled' 중 어떤 값이든 들어올 수 있어서 오타가 발생해도 컴파일러가 잡아주지 않습니다. 숫자 상수 0, 1, 2, 3으로 쓰면 의미를 알 수 없습니다. 이때 열거형(enum)이 정답입니다. 가능한 값들을 타입으로 고정해서, 허용된 값 외에는 아예 들어올 수 없게 만드는 것입니다.
기본 enum
// 파일: main.dart
enum OrderStatus {
pending,
processing,
shipped,
delivered,
cancelled,
}
void printStatus(OrderStatus status) {
switch (status) {
case OrderStatus.pending:
print('주문 접수 대기 중');
case OrderStatus.processing:
print('주문 처리 중');
case OrderStatus.shipped:
print('배송 시작');
case OrderStatus.delivered:
print('배송 완료');
case OrderStatus.cancelled:
print('주문 취소');
}
}
void main() {
var myOrder = OrderStatus.processing;
printStatus(myOrder);
// 모든 케이스 출력
for (var status in OrderStatus.values) {
print('${status.index}: ${status.name}');
}
}
열거형 값은 EnumName.value 형태로 씁니다. .values는 모든 값의 리스트이고, .index는 선언 순서(0부터), .name은 이름 문자열입니다.
Dart 3의 switch는 모든 케이스를 처리하지 않으면 컴파일 경고가 나옵니다. 열거형과 switch를 함께 쓰면 새로운 케이스가 추가됐을 때 처리를 빠뜨리는 실수를 컴파일 타임에 잡을 수 있습니다.
향상된 enum (Dart 2.17+)
Dart 2.17부터 enum에 필드, 생성자, 메서드, 심지어 implements까지 추가할 수 있게 됐습니다.
// 파일: main.dart
enum Planet {
mercury(3.303e+23, 2.4397e6),
venus(4.869e+24, 6.0518e6),
earth(5.976e+24, 6.37814e6),
mars(6.421e+23, 3.3972e6),
jupiter(1.9e+27, 7.1492e7),
saturn(5.688e+26, 6.0268e7),
uranus(8.686e+25, 2.5559e7),
neptune(1.024e+26, 2.4746e7);
const Planet(this.mass, this.radius);
final double mass; // kg
final double radius; // m
static const double G = 6.67430e-11;
double get surfaceGravity => G * mass / (radius * radius);
double surfaceWeight(double otherMass) => otherMass * surfaceGravity;
}
void main() {
const earthWeight = 75.0; // kg
const mass = earthWeight / Planet.earth.surfaceGravity;
for (var p in Planet.values) {
final weight = p.surfaceWeight(mass);
print('${p.name.padRight(10)}: ${weight.toStringAsFixed(2)} kg');
}
}
출력 결과입니다.
mercury : 28.33 kg
venus : 67.86 kg
earth : 75.00 kg
mars : 28.46 kg
jupiter : 189.78 kg
saturn : 79.93 kg
uranus : 67.56 kg
neptune : 85.04 kg
enum Planet에 생성자(const Planet(...))와 필드(mass, radius), 메서드(surfaceGravity, surfaceWeight)를 모두 넣었습니다. 각 행성 값을 선언할 때 생성자 매개변수를 전달합니다.
enum과 switch 함께 쓰기
switch의 패턴 매칭과 enum이 만나면 강력해집니다.
// 파일: main.dart
enum TrafficLight {
red,
yellow,
green;
TrafficLight get next => switch (this) {
TrafficLight.red => TrafficLight.green,
TrafficLight.yellow => TrafficLight.red,
TrafficLight.green => TrafficLight.yellow,
};
Duration get duration => switch (this) {
TrafficLight.red => const Duration(seconds: 60),
TrafficLight.yellow => const Duration(seconds: 5),
TrafficLight.green => const Duration(seconds: 45),
};
String get instruction => switch (this) {
TrafficLight.red => '정지',
TrafficLight.yellow => '주의',
TrafficLight.green => '진행',
};
}
void main() {
var light = TrafficLight.red;
for (var i = 0; i < 6; i++) {
print('${light.name}: ${light.instruction} (${light.duration.inSeconds}초)');
light = light.next;
}
}
switch 표현식(switch (this) { ... })을 getter 안에서 사용했습니다. 새로운 값이 추가되면 switch에서 처리되지 않은 케이스가 있다고 컴파일 오류가 나오므로 안전합니다.
implements로 인터페이스 구현
향상된 enum은 implements도 지원합니다.
// 파일: main.dart
abstract class Describable {
String get description;
bool get isActive;
}
enum AppTheme implements Describable {
light(primaryColor: 0xFFFFFFFF, textColor: 0xFF000000),
dark(primaryColor: 0xFF121212, textColor: 0xFFFFFFFF),
highContrast(primaryColor: 0xFF000000, textColor: 0xFFFFFF00);
const AppTheme({required this.primaryColor, required this.textColor});
final int primaryColor;
final int textColor;
@override
String get description => switch (this) {
AppTheme.light => '밝은 테마 — 주간 사용에 적합',
AppTheme.dark => '어두운 테마 — 야간 사용에 적합',
AppTheme.highContrast => '고대비 테마 — 시각 접근성 향상',
};
@override
bool get isActive => this != AppTheme.highContrast; // 예시용
String get hexPrimary =>
'#${primaryColor.toRadixString(16).padLeft(8, '0').substring(2)}';
}
void main() {
for (var theme in AppTheme.values) {
print('${theme.name}: ${theme.description}');
print(' 기본색: ${theme.hexPrimary}, 활성: ${theme.isActive}');
}
}
실용적 활용 — 상태 관리
Flutter 앱에서 자주 쓰는 패턴입니다. 데이터 로딩 상태를 열거형으로 표현합니다.
// 파일: main.dart
enum LoadState {
idle,
loading,
success,
failure;
bool get isLoading => this == LoadState.loading;
bool get isSuccess => this == LoadState.success;
bool get hasError => this == LoadState.failure;
T when<T>({
required T Function() idle,
required T Function() loading,
required T Function() success,
required T Function() failure,
}) =>
switch (this) {
LoadState.idle => idle(),
LoadState.loading => loading(),
LoadState.success => success(),
LoadState.failure => failure(),
};
}
class DataFetcher {
LoadState state = LoadState.idle;
String? data;
String? errorMessage;
Future<void> fetch() async {
state = LoadState.loading;
render();
await Future.delayed(const Duration(seconds: 1));
// 성공 시뮬레이션
data = 'Dart 데이터 로드 완료!';
state = LoadState.success;
render();
}
void render() {
state.when(
idle: () => print('대기 중...'),
loading: () => print('불러오는 중...'),
success: () => print('완료: $data'),
failure: () => print('오류: $errorMessage'),
);
}
}
void main() async {
var fetcher = DataFetcher();
fetcher.render();
await fetcher.fetch();
}
when 메서드는 각 상태에 맞는 콜백을 받아서 실행합니다. 모든 케이스를 강제로 처리하므로 빠뜨림이 없습니다.
enum 유틸리티
// 파일: main.dart
enum Direction { north, south, east, west }
void main() {
// 이름으로 찾기
var dir = Direction.values.byName('north');
print(dir); // Direction.north
// 인덱스로 찾기
var second = Direction.values[1];
print(second); // Direction.south
// 비교
print(Direction.north == Direction.north); // true
print(Direction.north.index < Direction.east.index); // true
// Set으로 사용
var allowed = {Direction.north, Direction.east};
print(allowed.contains(Direction.south)); // false
}
정리
열거형은 "가능한 값의 집합"을 타입으로 고정합니다. 기본 enum으로 상수를 나열하고, 향상된 enum으로 필드와 메서드를 추가할 수 있습니다. switch와 조합하면 컴파일 타임에 케이스 누락을 검출할 수 있어서 안전한 코드를 작성하는 데 큰 도움이 됩니다.
PART 05가 끝났습니다. 클래스, 다양한 생성자, 상속, 추상 클래스, 인터페이스, 믹스인, 열거형까지 객체지향의 핵심을 모두 다뤘습니다. 다음 PART에서는 Null Safety 심화 과정으로, 타입 시스템을 더욱 안전하게 활용하는 방법을 배웁니다.