switch가 진화했다 — switch 표현식
PART 05에서 열거형을 배우며 switch 표현식을 잠깐 만났습니다. switch (this) { TrafficLight.red => '정지', ... }처럼 => 화살표로 값을 반환하는 형태였습니다. 이 챕터에서는 그 switch 표현식을 제대로 파헤칩니다.
PART 03에서 배운 switch 문은 분기마다 case와 break를 쓰고, 값을 반환하지 않았습니다. Dart 3.0의 switch 표현식은 값을 직접 반환하고, 패턴 매칭과 결합하며, 모든 경우를 빠짐없이 처리했는지 컴파일러가 확인해줍니다. 문(statement)이 표현식(expression)으로 진화한 것입니다.
기존 switch 문 복습
PART 03에서 배운 switch 문입니다. 각 케이스를 처리하되 값을 반환하지는 않습니다.
void main() {
String day = '월';
String type;
switch (day) {
case '월':
case '화':
case '수':
case '목':
case '금':
type = '평일';
case '토':
case '일':
type = '주말';
default:
type = '알 수 없음';
}
print(type); // 평일
}
여러 케이스를 묶을 수 있고(Dart 3부터 fall-through 없이 케이스 나열), default로 나머지를 처리합니다. 그런데 type 변수를 먼저 선언하고, switch 안에서 할당하는 방식이 번거롭습니다. switch 표현식은 이 불편함을 해결합니다.
switch 표현식 — 값을 반환하는 switch
switch 표현식은 switch가 값 자체를 반환합니다.
void main() {
String day = '월';
String type = switch (day) {
'월' || '화' || '수' || '목' || '금' => '평일',
'토' || '일' => '주말',
_ => '알 수 없음',
};
print(type); // 평일
}
변화한 점들을 살펴봅시다. case 키워드가 사라졌습니다. => 화살표로 반환값을 지정합니다. ||로 여러 패턴을 한 줄에 묶습니다. _(와일드카드)가 default 역할을 합니다. 그리고 switch 전체가 값이 되므로 변수에 바로 할당할 수 있습니다.
세미콜론은 switch 표현식 전체 끝에 한 번만 씁니다. 마지막 케이스 뒤에는 쉼표(,)가 옵니다.
가드 절 — when으로 조건 추가
패턴이 일치하더라도 추가 조건을 검사하고 싶을 때 when 절을 씁니다.
void main() {
int score = 87;
String grade = switch (score) {
int n when n >= 90 => 'A',
int n when n >= 80 => 'B',
int n when n >= 70 => 'C',
int n when n >= 60 => 'D',
_ => 'F',
};
print(grade); // B
}
int n when n >= 90을 읽으면 이렇습니다. 값이 int이고(int n 타입 패턴), 그 값이 90 이상일 때(when n >= 90) 매치됩니다. 패턴 검사가 먼저 통과해야 가드 절이 평가됩니다.
가드 절은 if-case 문에서도 쓸 수 있습니다.
void main() {
(String, int) person = ('김다트', 17);
if (person case (var name, var age) when age < 18) {
print('$name은 미성년자입니다.');
}
}
완전성 검사 — 컴파일러가 지키는 안전망
switch 표현식의 가장 강력한 특징입니다. 컴파일러가 모든 경우를 다뤘는지 확인하고, 빠진 경우가 있으면 오류를 냅니다.
enum Direction { north, south, east, west }
void main() {
Direction dir = Direction.north;
// 아래 코드는 컴파일 오류: south, east, west 케이스 없음
// String label = switch (dir) {
// Direction.north => '북쪽',
// };
// 모든 케이스를 처리해야 합니다
String label = switch (dir) {
Direction.north => '북쪽',
Direction.south => '남쪽',
Direction.east => '동쪽',
Direction.west => '서쪽',
};
print(label); // 북쪽
}
주석 처리된 switch는 south, east, west를 처리하지 않으므로 컴파일 오류가 납니다. 이를 완전성 검사(exhaustiveness checking)라고 합니다. _ 와일드카드로 나머지를 묶어도 됩니다.
완전성 검사 덕분에 나중에 열거형에 새 값을 추가하면, 처리하지 않은 switch 표현식들이 전부 컴파일 오류로 드러납니다. 실수를 코드를 실행하기 전에 잡을 수 있습니다.
enum + switch 표현식
열거형과 switch 표현식은 최고의 짝입니다.
enum Season { spring, summer, autumn, winter }
String describeWeather(Season season) => switch (season) {
Season.spring => '꽃이 피고 따뜻합니다.',
Season.summer => '덥고 습합니다.',
Season.autumn => '선선하고 단풍이 집니다.',
Season.winter => '춥고 눈이 옵니다.',
};
int avgTemp(Season season) => switch (season) {
Season.spring => 15,
Season.summer => 32,
Season.autumn => 18,
Season.winter => -2,
};
void main() {
for (var season in Season.values) {
print('${season.name}: ${describeWeather(season)} (평균 ${avgTemp(season)}°C)');
}
}
실행 결과입니다.
spring: 꽃이 피고 따뜻합니다. (평균 15°C)
summer: 덥고 습합니다. (평균 32°C)
autumn: 선선하고 단풍이 집니다. (평균 18°C)
winter: 춥고 눈이 옵니다. (평균 -2°C)
describeWeather와 avgTemp 모두 화살표 함수(=>)와 switch 표현식을 결합했습니다. 함수 한 줄로 완전한 열거형 처리가 이루어집니다.
패턴과 함께 — 타입별 분기
switch 표현식은 패턴 매칭과 결합할 때 더욱 강력해집니다. 타입에 따라 다른 처리를 할 수 있습니다.
String describe(Object value) => switch (value) {
int n when n < 0 => '음수: $n',
int n => '양의 정수: $n',
double d => '소수: $d',
String s => '문자열: "$s"',
bool b => '불리언: $b',
List<dynamic> l => '리스트 (${l.length}개 요소)',
_ => '기타: $value',
};
void main() {
var values = [-5, 42, 3.14, 'Dart', true, [1, 2, 3], null];
for (var v in values) {
print(describe(v));
}
}
실행 결과입니다.
음수: -5
양의 정수: 42
소수: 3.14
문자열: "Dart"
불리언: true
리스트 (3개 요소)
기타: null
케이스들은 위에서 아래로 순서대로 평가됩니다. int n when n < 0이 먼저 오고, int n이 나중에 오므로 음수는 첫 번째, 양수는 두 번째 케이스에 걸립니다.
sealed class + switch 표현식 미리보기
Ch 05에서 자세히 다루겠지만, sealed class와 switch 표현식의 조합을 미리 보겠습니다.
// sealed class: 같은 파일 안에서만 상속 가능
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);
}
double area(Shape shape) => switch (shape) {
Circle(radius: var r) => 3.14159 * r * r,
Rectangle(width: var w, height: var h) => w * h,
};
void main() {
var shapes = [Circle(5.0), Rectangle(4.0, 6.0)];
for (var shape in shapes) {
print('넓이: ${area(shape).toStringAsFixed(2)}');
}
}
실행 결과입니다.
넓이: 78.54
넓이: 24.00
Shape가 sealed이므로 Circle과 Rectangle 외에 다른 서브클래스가 없다는 것을 컴파일러가 압니다. 덕분에 area 함수의 switch에서 _(와일드카드) 없이도 완전성 검사가 통과합니다. sealed class의 자세한 동작 원리는 Ch 05에서 배웁니다.
전체 예제 — HTTP 상태 코드 처리기
String describeHttpStatus(int code) => switch (code) {
200 => '200 OK — 성공',
201 => '201 Created — 생성됨',
204 => '204 No Content — 내용 없음',
int n when n >= 200 && n < 300 => '2xx 성공 ($n)',
301 => '301 Moved Permanently — 영구 이동',
302 => '302 Found — 임시 이동',
int n when n >= 300 && n < 400 => '3xx 리다이렉션 ($n)',
400 => '400 Bad Request — 잘못된 요청',
401 => '401 Unauthorized — 인증 필요',
403 => '403 Forbidden — 접근 금지',
404 => '404 Not Found — 찾을 수 없음',
int n when n >= 400 && n < 500 => '4xx 클라이언트 오류 ($n)',
500 => '500 Internal Server Error — 서버 오류',
int n when n >= 500 && n < 600 => '5xx 서버 오류 ($n)',
_ => '알 수 없는 상태 코드: $code',
};
void main() {
var codes = [200, 201, 301, 400, 404, 500, 999];
for (var code in codes) {
print(describeHttpStatus(code));
}
}
실행 결과입니다.
200 OK — 성공
201 Created — 생성됨
301 Moved Permanently — 영구 이동
400 Bad Request — 잘못된 요청
404 Not Found — 찾을 수 없음
500 Internal Server Error — 서버 오류
알 수 없는 상태 코드: 999
구체적인 코드는 앞에, 범위 조건은 뒤에 배치했습니다. 케이스들이 위에서부터 순서대로 평가되기 때문에, 구체적인 것을 먼저 처리하면 원하는 동작을 정확하게 만들 수 있습니다.
다음 챕터에서는 구조 분해를 배웁니다. 패턴 매칭과 switch 표현식에서 (var x, var y)처럼 값을 꺼내는 것이 구조 분해입니다. 변수 선언, for-in 반복문, 심지어 스왑 연산까지 구조 분해가 활약하는 다양한 상황을 살펴봅니다.