값의 구조를 읽다 — 패턴 매칭
코드를 쓰다 보면 "이 값이 특정 모양인지 확인하고, 맞으면 안에서 값을 꺼내고 싶다"는 상황이 자주 생깁니다. 예를 들어 리스트의 첫 번째 요소와 마지막 요소를 동시에 꺼내거나, Map에서 특정 키가 있는지 확인하면서 그 값을 변수에 담고 싶을 때입니다.
Dart 3.0의 패턴 매칭(Pattern Matching)은 이 모든 것을 우아하게 처리합니다. 패턴은 값의 구조를 검사하고, 검사가 통과하면 값을 분해하여 변수에 넣어줍니다. 검사와 분해를 동시에 하는 것이 패턴의 핵심입니다.
패턴이란 무엇인가
패턴은 값이 특정 조건을 만족하는지 검사합니다. 그리고 조건이 맞으면 값의 일부를 꺼내어 새 변수에 바인딩합니다. 이 두 가지 동작 — 검사(match)와 바인딩(bind) — 이 패턴의 전부입니다.
다음과 같이 이해하면 좋습니다. if (score >= 60)은 score라는 값이 60 이상인지 검사합니다. 그런데 패턴은 값의 구조 자체를 검사합니다. 예를 들어 "이 값이 두 요소짜리 리스트이고, 첫 번째가 String이면"처럼요.
상수 패턴 — 정확한 값과 비교
가장 단순한 패턴입니다. 값이 특정 상수와 일치하는지 확인합니다.
void main() {
int code = 200;
// if-case 문으로 패턴 사용
if (code case 200) {
print('성공');
}
}
if (값 case 패턴) 구조가 if-case 문입니다. 값이 패턴과 일치하면 블록을 실행합니다. 상수 패턴에서는 ==으로 비교하는 것과 같습니다.
변수 패턴 — 값을 변수에 담기
변수 패턴은 값을 새 변수에 바인딩합니다.
void main() {
Object data = 'Dart 3.0';
if (data case var text) {
print(text.runtimeType); // String
print(text); // Dart 3.0
}
}
var text가 변수 패턴입니다. 어떤 값이든 text에 담습니다. 변수 패턴 단독으로는 항상 매치되기 때문에, 보통 타입 패턴과 함께 씁니다.
타입 패턴 — 타입 검사와 바인딩 한 번에
타입 패턴은 값의 타입을 확인하면서 동시에 변수에 담습니다. 기존의 is 연산자 + 별도 변수 선언을 한 줄로 줄여줍니다.
void main() {
Object value = 42;
// 기존 방식
if (value is int) {
int n = value;
print(n * 2);
}
// 타입 패턴 방식
if (value case int n) {
print(n * 2); // 84
}
}
int n이 타입 패턴입니다. 값이 int이면 매치가 성공하고, n에 값이 담깁니다. 한 줄로 타입 확인과 변수 선언을 동시에 처리합니다.
와일드카드 패턴 — 신경 쓰지 않는 값
_(언더스코어)는 와일드카드 패턴입니다. "이 자리의 값은 신경 쓰지 않겠다"는 뜻입니다.
void main() {
var record = ('김다트', 21, '서울');
if (record case (var name, _, var city)) {
print('$name은 $city에 살고 있습니다.');
}
}
두 번째 필드(21, 나이)는 _로 무시하고, 이름과 도시만 꺼냅니다. 값은 매치하되 변수에 담지 않을 때 사용합니다.
패턴과 Null Safety — null 체크 패턴
패턴은 Null Safety와 자연스럽게 연계됩니다. nullable 값의 null 여부를 패턴으로 확인할 수 있습니다.
void main() {
String? name = '이플러터';
// 기존 방식
if (name != null) {
print(name.length);
}
// null 체크 패턴 (!)
if (name case var n?) {
print(n.length); // 4 — n은 String 타입
}
}
var n?의 ?가 null 체크 패턴입니다. 값이 null이 아닐 때만 매치하고, null이 아닌 값을 n에 담습니다. 이 블록 안에서 n은 non-nullable String입니다.
리스트 패턴 — 리스트의 구조 검사
리스트 패턴은 리스트의 요소를 위치별로 검사하고 바인딩합니다.
void main() {
List<String> fruits = ['사과', '바나나', '체리'];
if (fruits case [var first, _, var last]) {
print('첫 번째: $first'); // 첫 번째: 사과
print('마지막: $last'); // 마지막: 체리
}
}
[var first, _, var last]가 리스트 패턴입니다. 리스트에 정확히 3개의 요소가 있어야 매치됩니다. 첫 번째는 first에, 두 번째는 무시하고, 세 번째는 last에 담습니다.
요소 개수가 다르면 매치가 실패합니다.
void main() {
List<String> one = ['사과'];
List<String> three = ['사과', '바나나', '체리'];
if (one case [var first, _, var last]) {
print('매치됨'); // 요소가 1개라 실행되지 않습니다
}
if (three case [var first, _, var last]) {
print('매치됨: $first, $last'); // 매치됨: 사과, 체리
}
}
스프레드(...)를 사용하면 나머지 요소를 모두 변수에 담거나 무시할 수 있습니다.
void main() {
List<int> numbers = [1, 2, 3, 4, 5];
if (numbers case [var first, ...var rest]) {
print('첫 번째: $first'); // 첫 번째: 1
print('나머지: $rest'); // 나머지: [2, 3, 4, 5]
}
}
맵 패턴 — 맵의 키-값 검사
맵 패턴은 맵에서 특정 키가 존재하는지 확인하고, 그 값을 바인딩합니다.
void main() {
Map<String, dynamic> json = {
'name': '박레코드',
'age': 22,
'city': '부산',
};
if (json case {'name': var name, 'age': var age}) {
print('이름: $name, 나이: $age'); // 이름: 박레코드, 나이: 22
}
}
{'name': var name, 'age': var age}가 맵 패턴입니다. 맵에 'name'과 'age' 키가 있어야 매치되고, 각 값이 name과 age에 담깁니다. 맵에 다른 키가 더 있어도 괜찮습니다. 패턴에 명시한 키만 확인합니다.
패턴에 명시한 키가 없으면 매치가 실패합니다.
void main() {
Map<String, dynamic> data = {'city': '서울'};
if (data case {'name': var name}) {
print(name); // 'name' 키가 없으므로 실행되지 않습니다
} else {
print('name 키가 없습니다.'); // 이 쪽이 실행됩니다
}
}
전체 예제 — JSON 파싱
실제 개발에서 패턴 매칭이 빛나는 장면을 보여드리겠습니다.
void processResponse(Map<String, dynamic> response) {
if (response case {'status': 'success', 'data': var data}) {
print('성공 응답: $data');
return;
}
if (response case {'status': 'error', 'message': var msg}) {
print('오류 발생: $msg');
return;
}
print('알 수 없는 응답 형식');
}
void main() {
processResponse({'status': 'success', 'data': '사용자 목록 10명'});
processResponse({'status': 'error', 'message': '인증 실패'});
processResponse({'code': 500});
}
실행하면 다음과 같이 출력됩니다.
성공 응답: 사용자 목록 10명
오류 발생: 인증 실패
알 수 없는 응답 형식
패턴 매칭 덕분에 "이 맵이 성공 형태인가", "이 맵이 오류 형태인가"를 깔끔하게 나눌 수 있습니다. 기존처럼 containsKey나 null 체크를 중첩할 필요가 없습니다.
다음 챕터에서는 switch 표현식을 배웁니다. switch에 패턴 매칭이 결합되면 여러 경우를 더 체계적으로 처리할 수 있습니다. 특히 모든 경우를 빠짐없이 다뤘는지 컴파일러가 확인해주는 완전성 검사 기능이 매우 유용합니다.