분해하고 추출하기 — 구조 분해
Ch 01의 전체 예제에서 이런 코드가 잠깐 등장했습니다.
for (var (name, score) in students) {
// ...
}
(name, score)처럼 괄호 안에 변수 이름을 나열하는 것이 구조 분해(Destructuring)입니다. 복합적인 값에서 원하는 부분을 꺼내어 변수에 담는 기술입니다.
구조 분해는 새로운 개념이 아닙니다. 앞서 배운 패턴 매칭이 변수 선언과 반복문에도 적용된 것입니다. 패턴이 "값의 구조를 검사하고 분해한다"고 했는데, 구조 분해는 그 분해 기능을 전면에 내세운 활용 방법입니다.
변수 선언에서의 구조 분해
레코드를 반환하는 함수가 있다고 합시다. 이전에는 레코드를 변수에 담고 .name, .age로 접근했습니다. 구조 분해를 쓰면 한 번에 꺼낼 수 있습니다.
(String, int) getUser() {
return ('김다트', 25);
}
void main() {
// 구조 분해 없이
var user = getUser();
print(user.$1); // 김다트
print(user.$2); // 25
// 구조 분해 사용
var (name, age) = getUser();
print(name); // 김다트
print(age); // 25
}
var (name, age) = getUser()가 구조 분해 선언입니다. 우변의 레코드를 분해하여 첫 번째 요소를 name에, 두 번째 요소를 age에 담습니다.
타입을 명시할 수도 있습니다.
void main() {
var (String name, int age) = ('이플러터', 22);
print('$name, $age세'); // 이플러터, 22세
}
레코드 구조 분해
이름 필드가 있는 레코드도 구조 분해할 수 있습니다.
({String city, double lat, double lng}) getLocation() {
return (city: '서울', lat: 37.5665, lng: 126.9780);
}
void main() {
var (:city, :lat, :lng) = getLocation();
print('도시: $city');
print('위도: $lat');
print('경도: $lng');
}
:city는 이름 필드 city를 같은 이름의 변수 city에 담습니다. 이름 필드를 구조 분해할 때는 :필드이름 형식을 씁니다.
이름이 다른 변수에 담고 싶다면 이렇게 씁니다.
void main() {
var (city: cityName, lat: latitude, lng: longitude) =
(city: '부산', lat: 35.1796, lng: 129.0756);
print('$cityName ($latitude, $longitude)');
}
city: cityName은 "이름 필드 city의 값을 변수 cityName에 담아라"는 뜻입니다.
리스트 구조 분해
리스트도 구조 분해할 수 있습니다.
void main() {
var numbers = [10, 20, 30, 40, 50];
var [first, second, ...rest] = numbers;
print(first); // 10
print(second); // 20
print(rest); // [30, 40, 50]
}
[first, second, ...rest]가 리스트 구조 분해입니다. ...rest는 나머지 요소를 모두 rest 리스트에 담습니다.
나머지를 무시하고 싶다면 ...만 쓰면 됩니다.
void main() {
var scores = [95, 82, 71, 68, 55];
var [top, _, ...] = scores; // 두 번째는 무시, 나머지도 무시
print('최고점: $top'); // 최고점: 95
var [..., last] = scores; // 마지막만 꺼내기
print('최저점: $last'); // 최저점: 55
}
리스트 구조 분해는 리스트 길이가 맞지 않으면 실행 중 오류가 납니다. 길이가 정해지지 않은 리스트에 쓸 때는 ...를 활용하거나 if-case로 안전하게 처리합니다.
맵 구조 분해
맵도 구조 분해가 됩니다.
void main() {
var json = {
'name': '박패턴',
'score': 95,
'grade': 'A',
};
var {'name': name, 'score': score} = json;
print('$name: $score점'); // 박패턴: 95점
}
'name': name은 키 'name'의 값을 변수 name에 담습니다. 맵에 해당 키가 없으면 오류가 납니다.
for-in에서의 구조 분해
반복문에서 구조 분해는 특히 빛납니다. 복합적인 값의 컬렉션을 순회할 때 매우 편리합니다.
void main() {
var students = [
('김다트', 92),
('이플러터', 78),
('박클래스', 85),
];
for (var (name, score) in students) {
print('$name: $score점');
}
}
실행 결과입니다.
김다트: 92점
이플러터: 78점
박클래스: 85점
Map의 entries를 순회할 때도 유용합니다.
void main() {
var inventory = {
'사과': 30,
'바나나': 15,
'딸기': 22,
};
for (var MapEntry(:key, :value) in inventory.entries) {
print('$key: $value개');
}
}
실행 결과입니다.
사과: 30개
바나나: 15개
딸기: 22개
MapEntry(:key, :value)는 MapEntry 객체에서 key 필드와 value 필드를 꺼냅니다. :key는 필드명과 변수명이 같을 때의 단축 표기입니다.
스왑 패턴
구조 분해의 재미있는 활용입니다. 두 변수의 값을 교환할 때 임시 변수가 필요 없습니다.
void main() {
var a = 10;
var b = 20;
// 기존 방식: 임시 변수 필요
var temp = a;
a = b;
b = temp;
print('a=$a, b=$b'); // a=20, b=10
// 구조 분해 스왑
(a, b) = (b, a);
print('a=$a, b=$b'); // a=10, b=20
}
(a, b) = (b, a)는 오른쪽의 레코드 (b, a)를 만들고, 그것을 왼쪽 (a, b)로 구조 분해합니다. 임시 변수 없이 두 변수의 값을 교환합니다.
문자열이나 다른 타입도 마찬가지입니다.
void main() {
var x = '안녕';
var y = '세상';
(x, y) = (y, x);
print('$x, $y'); // 세상, 안녕
}
중첩 구조 분해
복잡한 중첩 구조도 한 번에 분해할 수 있습니다.
void main() {
// 레코드 안에 레코드
var student = ('김다트', (score: 92, grade: 'A'));
var (name, (score: s, grade: g)) = student;
print('$name: $s점 ($g)'); // 김다트: 92점 (A)
}
전체 예제 — 성적 분석기
({String name, int score, String grade}) evaluate(String name, int score) {
var 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',
};
return (name: name, score: score, grade: grade);
}
void main() {
var rawData = [
('김다트', 95),
('이플러터', 78),
('박클래스', 62),
('최레코드', 88),
('정패턴', 55),
];
// 구조 분해로 평가
var results = [
for (var (name, score) in rawData) evaluate(name, score),
];
// 결과 출력 — 구조 분해로 접근
for (var (:name, :score, :grade) in results) {
var status = score >= 60 ? '합격' : '불합격';
print('$name: $score점 ($grade) — $status');
}
// 합격자만 추출
var passed = results.where((r) => r.score >= 60).toList();
// 최고점과 최저점 — 리스트 구조 분해
var sorted = [...passed]..sort((a, b) => b.score.compareTo(a.score));
var [top, ..., bottom] = sorted;
print('\n최고점: ${top.name} (${top.score}점)');
print('최저 합격: ${bottom.name} (${bottom.score}점)');
}
실행 결과입니다.
김다트: 95점 (A) — 합격
이플러터: 78점 (C) — 합격
박클래스: 62점 (D) — 합격
최레코드: 88점 (B) — 합격
정패턴: 55점 (F) — 불합격
최고점: 김다트 (95점)
최저 합격: 박클래스 (62점)
레코드, 구조 분해, switch 표현식이 함께 어우러진 코드입니다. 각각 따로 배울 때보다 함께 사용할 때 훨씬 강력해집니다.
다음 챕터에서는 sealed class와 class modifier를 배웁니다. "어떤 클래스를 상속할 수 있고 없는지"를 제어하는 도구들입니다. 특히 sealed class와 switch 표현식이 만나면 API 응답 상태(로딩, 성공, 오류)를 타입 안전하게 설계할 수 있습니다.