Dart Record (레코드 튜플)

1. Record란 무엇인가?

Record는익명, 불변, 복합 집합 타입으로 여러 가지 다른 타입의 데이터를 하나의 객체로 묶어줍니다. List / Set / Map과의 핵심 차이점은 다음과 같습니다.

  • 길이 고정: 생성하면 필드 개수를 추가하거나 삭제할 수 없음, 이종 타입 저장:
  • 동일한 Record 안에 int, String, bool 등 완전히 다른 타입을 넣을 수 있음
  • 강타입 제약: 각 필드 타입이 컴파일러에서 인식되어 타입 안전성이 유지됨
  • 읽기 전용 수정 불가: 생성 후 필드 값을 재할당할 수 없음

Record는 완전히 유효한 변수로 변수 저장, 함수 매개변수/다중 반환값, 중첩, List/Map/Set 삽입 등 모든 일반 연산을 지원합니다.

2. Record 기본 문법

1. Record 리터럴

괄호로 감싸고 쉼표로 구분하며 위치 필드이름 지정 필드 두 종류로 나뉩니다. 위치 필드를 맨 앞에 적고 이름 지정 필드는 뒤에 키: 형식으로 작성합니다.

// 위치 필드 + 이름 지정 필드 혼합 예시
var info = ('Jim', age: 20, flag: true, 'Student');Code language: JavaScript (javascript)

2. Record 사용법

1 위치 필드

// 선언: 두 개의 위치 필드, 순서대로 String, int
(String, int) user;

// 구조에 맞춰 값 할당
user = ('李四', 22);
Code language: JavaScript (javascript)

2 이름 지정 필드

타입 선언 시 {}로 감쌉니다

// a(int), b(bool) 두 이름 지정 필드 선언
({int a, bool b}) data;

data = (a: 100, b: false);Code language: JavaScript (javascript)

3 혼합 사용

위치 필드와 이름 지정 필드를 함께 타입으로 선언

// String 위치 필드 1개, score(int)、pass(bool) 이름 필드 2개
(String, {int score, bool pass}) exam;
exam = ('기말고사', score: 90, pass: true);
Code language: JavaScript (javascript)

3. 자주 하는 실수

1: 이름 지정 필드의 이름은 타입 구조에 포함되며 이름이 다르면 완전히 다른 타입
// 타입: ({int a, int b})
({int a, int b}) pointAB = (a: 1, b: 2);
// 타입: ({int x, int y})
({int x, int y}) pointXY = (x: 3, y: 4);

// 컴파일 오류! 타입 불일치로 서로 할당 불가
// pointAB = pointXY;
Code language: JavaScript (javascript)

이름 지정 필드의 경우 둘 다 int 두 개라도 필드명이 ab, xy로 달라 서로 할당할 수 없습니다. 위치 필드는 아래 내용 참고.

2: 위치 필드 타입 주석에 적은 별칭은 주석일 뿐 타입 매칭에 영향 없음

괄호 안 위치 필드에 붙인 이름은 개발자용 문서 주석일 뿐 컴파일러가 무시하며 구조만 같으면 서로 할당 가능합니다.

(int a, int b) p1 = (10, 20);
(int x, int y) p2 = (30, 40);

// 정상 작동, 내부적으로 두 개의 int 위치 필드로 타입 완전 일치
p1 = p2;Code language: JavaScript (javascript)

함수 매개변수와 유사한 논리: 매개변수 이름은 함수 서명을 바꾸지 않습니다.

4. Record 내부 필드 읽기

Record는 불변 객체이므로 getter만 존재하고 setter가 없어 필드 값을 수정할 수 없습니다.

  1. 이름 지정 필드.필드명으로 직접 접근
  2. 위치 필드$숫자로 가져옴, 숫자는 이름 지정 필드를 건너뛰고 위치 필드 순서만 카운트
var record = ('first', a: 2, b: true, 'last');

print(record.$1); // 첫 번째 위치 필드:first
print(record.a);  // 이름 필드 a:2
print(record.b);  // 이름 필드 b:true
print(record.$2); // 두 번째 위치 필드:last
Code language: PHP (php)

5. Record 구조 타입 판단 규칙

Record는 별도 class 정의 없이 구조 형태로 동일 타입 여부를 판단합니다.

형태 = 필드 총 개수 + 각 필드 타입 + 이름 지정 필드의 명칭

구조 형태가 완전히 같다면 다른 파일, 다른 라이브러리에서 생성되어도 같은 타입으로 인식합니다.

모든 필드의 정적 타입을 컴파일러가 인지하므로 타입 정보가 유실되지 않습니다.

// 1번 위치 num 타입, 2번 위치 Object 타입
(num, Object) pair = (42, '텍스트 문자열');

var firstVal = pair.$1; // 정적 타입 num, 실행 시 int
var secondVal = pair.$2;// 정적 타입 Object, 실행 시 StringCode language: JavaScript (javascript)

6. Record 동등 비교 ==

두 Record가 같다고 인정받으려면 두 조건을 모두 만족해야 합니다.

  1. 두 Record의 구조 형태가 완전히 일치
  2. 대응하는 위치/이름 필드의 값이 모두 동일

이름 지정 필드의 작성 순서는 동등 비교에 영향을 주지 않지만, 필드명 자체가 다르면 타입 자체가 달라 무조건 false입니다.

// 위치 필드, 형태 같고 값 일치 → true
(int x, int y, int z) point = (1, 2, 3);
(int r, int g, int b) color = (1, 2, 3);
print(point == color); // true

// 이름 필드명이 달라 타입 불일치 → false
({int x, int y, int z}) p = (x:1, y:2, z:3);
({int r, int g, int b}) c = (r:1, g:2, b:3);
print(p == c); // falseCode language: PHP (php)

Record는 자동으로 == 연산자와 hashCode가 재정의되므로 직접 구현할 필요 없습니다.

7. 함수 다중 반환값

Dart 함수는 단 하나의 값만 반환할 수 있으므로 Record로 여러 이종 타입 데이터를 묶고 패턴 분해로 한 번에 개별 변수로 분리할 수 있습니다. List/Map을 대체하기 최적(타입 안전성 유지).

1 위치 필드 분해

// 반환: 이름(String), 나이(int)
(String name, int age) getUserInfo(Map<String, dynamic> json) {
  return (json['name'] as String, json['age'] as int);
}

void main() {
  final userJson = {'name': 'Dash', 'age': 10, 'color': 'blue'};
  // 한 줄로 분해, 두 변수 할당
  var (userName, userAge) = getUserInfo(userJson);
  print(userName); // Dash
  print(userAge);  // 10
}
Code language: JavaScript (javascript)

2 이름 지정 필드 분해

:필드명 구문 사용

({String name, int age}) getUserInfo(Map<String, dynamic> json) {
  return (name: json['name'] as String, age: json['age'] as int);
}

void main() {
  final userJson = {'name': 'Dash', 'age': 10};
  // 이름 기반 분해 문법
  final (:name, :age) = getUserInfo(userJson);
  print(name);
}
Code language: JavaScript (javascript)

다른 방식의 단점 비교

C++, C#, Java 등 다른 언어에서 Record를 사용하지 않으면 아래 불편함이 발생합니다.

  1. 새 클래스 생성: 생성자, 필드 정의로 코드가 길어짐
  2. List 반환: 타입 안전성 상실, 모두 dynamic 타입으로 변환
  3. Map 반환: 키 이름 오타 위험, 정적 타입 검사 불가

8. 가벼운 구조화 데이터 저장

메서드 없이 데이터만 저장할 때 새 클래스를 만들지 않고 Record를 직접 사용할 수 있어 class 선언의 불필요한 코드를 줄이고 대량 리스트 데이터에 적합합니다.

예시 코드

화면 버튼 설정 리스트 (Flutter 예시, Flutter 학습 후 이해 가능)

import 'package:flutter/material.dart';

void main() {
  final buttonList = [
    (
      label: "파일 업로드",
      icon: const Icon(Icons.upload_file),
      onPressed: () => print("업로드 버튼 클릭"),
    ),
    (
      label: "상세 보기",
      icon: const Icon(Icons.info),
      onPressed: () => print("상세 버튼 클릭"),
    )
  ];
}
Code language: JavaScript (javascript)

9. typedef로 Record 타입 별칭 지정

긴 타입 선언 간소화

Record 타입 표기가 길어지므로 typedef로 타입 별칭을 정의하면 재사용하기 쉽고 추후 필드 구조 일괄 수정에 유리합니다.

기본 사용법
// 버튼 설정 Record 별칭 정의, onPressed는 null 허용
typedef ButtonConfig = ({
  String label,
  Icon icon,
  void Function()? onPressed
});

// 별칭으로 리스트 직접 선언
List<ButtonConfig> buttons = [
  (label: "제출", icon: const Icon(Icons.check), onPressed: () {}),
  (label: "취소", icon: const Icon(Icons.close), onPressed: null),
];
Code language: PHP (php)
확장 활용법

추후 데이터에 메서드, 로직을 추가해야 할 경우 반복 렌더링 비즈니스 코드는 수정하지 않고 타입 정의만 교체하면 됩니다.

  1. 방법1: 일반 Class로 교체
class ButtonConfig {
  final String label;
  final Icon icon;
  final void Function()? onPressed;
  ButtonConfig({required this.label, required this.icon, this.onPressed});
  // 커스텀 메서드 추가
  bool get hasClickEvent => onPressed != null;
}
Code language: PHP (php)
  1. 방법2: Extension Type으로 기존 Record 래핑
extension type ButtonConfig._(({String label, Icon icon, void Function()? onPressed}) _) {
  String get label => _.label;
  Icon get icon => _.icon;
  void Function()? get onPressed => _.onPressed;

  ButtonConfig({required String label, required Icon icon, void Function()? onPressed})
      : this._((label: label, icon: icon, onPressed: onPressed));

  bool get hasClickEvent => _.onPressed != null;
}
Code language: PHP (php)

위 두 가지 교체 방식 모두 화면에서 버튼 순회 렌더링하는 업무 코드는 전혀 수정할 필요 없습니다.

10. 요약

  1. 버전 요구사항: Dart 3.0 이상
  2. 3대 핵심 속성: 익명, 불변, 집합; 길이 고정, 이종 타입 저장, 강타입 제약
  3. 필드 두 종류: 위치 필드 $1/$2, 이름 지정 필드 .name
  4. 타입 판단 규칙: 구조 형태로 타입 결정, 이름 필드명이 형태에 포함됨; 위치 필드 별칭은 주석에 불과
  5. 동등 비교: 구조 형태 동일하고 모든 필드 값 일치하면 true
  6. 핵심 장점: 함수 다중 반환값, 가벼운 데이터 컨테이너, 완전한 타입 안전성
  7. 확장 방안: typedef로 타입 별칭 간소화, 추후 Class/Extension Type으로 무중단 교체 가능

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다