iBetter Books
수정

여러 값을 하나로 — 레코드

함수를 작성하다 보면 두 개, 세 개의 값을 동시에 돌려주고 싶을 때가 생깁니다. 예를 들어 HTTP 응답을 파싱한 뒤 상태 코드와 본문을 함께 반환하거나, 좌표 (x, y)를 한 번에 돌려주고 싶은 경우입니다.

Dart 3.0 이전에는 이런 상황을 Map이나 별도 클래스로 해결했습니다. Map은 타입 정보가 사라지고, 클래스는 값 몇 개를 위해 너무 많은 코드가 필요했습니다. Dart 3.0이 레코드(Record)를 도입한 이유가 바로 여기에 있습니다.

레코드란 무엇인가

레코드는 여러 값을 하나로 묶는 익명 불변 집합 타입입니다. 클래스처럼 이름을 선언할 필요가 없고, 리스트처럼 순서에만 의존하지도 않습니다. 값의 타입과 구조가 레코드의 "형태"를 결정합니다.

레코드의 특징을 세 가지로 정리할 수 있습니다. 첫째, 불변입니다. 레코드를 만든 뒤에는 필드 값을 바꿀 수 없습니다. 둘째, 익명입니다. 별도 클래스를 선언하지 않아도 됩니다. 셋째, 값 기반 동등성을 가집니다. 같은 필드 값이면 같은 레코드로 취급합니다.

위치 필드 — 순서대로 묶기

레코드의 가장 간단한 형태입니다. 괄호 안에 값을 나열하면 됩니다.

void main() {
  (int, String) person = (1, '김다트');

  print(person.$1); // 1
  print(person.$2); // 김다트
}

(int, String)이 레코드의 타입입니다. $1, $2처럼 달러 기호와 번호로 각 필드에 접근합니다. 번호는 1부터 시작합니다.

타입을 명시하지 않고 var로도 선언할 수 있습니다.

void main() {
  var point = (3.0, 4.0);

  double x = point.$1;
  double y = point.$2;

  print('좌표: ($x, $y)'); // 좌표: (3.0, 4.0)
}

이름 필드 — 의미를 붙이기

위치 필드는 1,1, 2 같은 접근자를 써야 해서 가독성이 떨어집니다. 이름을 붙이면 훨씬 명확해집니다.

void main() {
  ({String name, int age}) student = (name: '이다트', age: 21);

  print(student.name); // 이다트
  print(student.age);  // 21
}

중괄호 {} 안에 타입 이름 형식으로 선언합니다. 접근할 때는 .name, .age처럼 점 표기법을 씁니다. 클래스와 비슷하게 느껴지지만, 별도의 클래스 선언이 전혀 없습니다.

이름 필드는 순서에 상관없이 접근할 수 있습니다. 필드 이름이 타입의 일부이므로 ({String name, int age})({int age, String name})은 같은 타입입니다.

혼합 필드 — 위치와 이름을 함께

위치 필드와 이름 필드를 섞어서 쓸 수도 있습니다.

void main() {
  (int, {String label}) item = (42, label: '정답');

  print(item.$1);     // 42
  print(item.label);  // 정답
}

위치 필드가 앞에, 이름 필드가 뒤에 옵니다. 실무에서는 위치 필드만 쓰거나 이름 필드만 쓰는 편이 코드를 읽기 더 쉽습니다.

함수에서 여러 값 반환하기

레코드의 가장 빛나는 활용처입니다. 함수 반환 타입에 레코드를 쓰면 여러 값을 우아하게 돌려줄 수 있습니다.

(double, double) divide(int a, int b) {
  double quotient = a / b;
  double remainder = a % b.toDouble();
  return (quotient, remainder);
}

void main() {
  var result = divide(17, 5);
  print('몫: ${result.$1}');     // 몫: 3.4
  print('나머지: ${result.$2}'); // 나머지: 2.0
}

이름 필드를 쓰면 반환값의 의미가 더 명확해집니다.

({int statusCode, String body}) fetchData(String url) {
  // 실제로는 네트워크 요청을 하겠지만, 예제에서는 하드코딩합니다
  return (statusCode: 200, body: '{"message": "ok"}');
}

void main() {
  var response = fetchData('https://api.example.com/data');

  if (response.statusCode == 200) {
    print('성공: ${response.body}');
  } else {
    print('실패: ${response.statusCode}');
  }
}

반환값에 이름이 붙어 있어서 .statusCode, .body처럼 읽기 쉽게 사용할 수 있습니다.

레코드의 동등성 비교

레코드는 값 기반 동등성을 지원합니다. 같은 타입의 레코드이고 모든 필드가 같은 값이면 ==true를 반환합니다.

void main() {
  var a = (1, 'hello');
  var b = (1, 'hello');
  var c = (2, 'hello');

  print(a == b); // true  — 같은 값
  print(a == c); // false — $1이 다름
}

이는 클래스와 대조적입니다. 일반 클래스는 ==를 오버라이드하지 않으면 참조 동등성을 비교하기 때문에, 같은 내용이라도 다른 인스턴스이면 false가 나옵니다. 레코드는 별도 설정 없이 값 비교를 해줍니다.

이름 필드가 있는 레코드도 마찬가지입니다.

void main() {
  var p1 = (name: '홍길동', age: 30);
  var p2 = (name: '홍길동', age: 30);

  print(p1 == p2); // true
}

레코드 vs 클래스

레코드가 좋다고 해서 클래스를 버릴 필요는 없습니다. 각각 잘 맞는 상황이 다릅니다.

// 레코드: 임시로 몇 가지 값을 묶을 때
(String, int) getNameAndScore() {
  return ('김학생', 95);
}

// 클래스: 행동(메서드)이 필요하거나 복잡한 로직이 있을 때
class Student {
  final String name;
  final int score;

  Student(this.name, this.score);

  String get grade => score >= 90 ? 'A' : 'B';

  void printReport() {
    print('$name: $score점 ($grade학점)');
  }
}

간단한 기준으로 정리하면 이렇습니다. 함수에서 여러 값을 돌려주거나 임시로 데이터를 묶어야 할 때는 레코드를 씁니다. 메서드가 필요하거나 나중에 상속·믹스인을 사용해야 한다면 클래스를 씁니다.

전체 예제 — 성적 처리 시스템

배운 내용을 하나로 묶어 보겠습니다.

({String name, int score, String grade}) evaluateStudent(
  String name,
  int score,
) {
  String grade;
  if (score >= 90) {
    grade = 'A';
  } else if (score >= 80) {
    grade = 'B';
  } else if (score >= 70) {
    grade = 'C';
  } else {
    grade = 'D';
  }
  return (name: name, score: score, grade: grade);
}

void main() {
  var students = [
    ('김다트', 92),
    ('이플러터', 78),
    ('박클래스', 85),
  ];

  for (var (name, score) in students) {
    var result = evaluateStudent(name, score);
    print('${result.name}: ${result.score}점 → ${result.grade}학점');
  }
}

실행하면 다음 결과가 나옵니다.

김다트: 92점 → A학점
이플러터: 78점 → C학점
박클래스: 85점 → B학점

for (var (name, score) in students) 부분은 레코드 구조 분해입니다. Ch 04에서 자세히 다루겠습니다. 지금은 레코드 안의 값을 꺼내 쓰는 편리한 문법이라고 이해하면 됩니다.

다음 챕터에서는 패턴 매칭을 배웁니다. 레코드와 패턴 매칭이 만나면 값의 구조를 검사하고 분해하는 강력한 방법이 생깁니다. 레코드를 잘 이해해 두면 다음 챕터가 훨씬 수월하게 느껴질 것입니다.