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가 없어 필드 값을 수정할 수 없습니다.
- 이름 지정 필드:
.필드명으로 직접 접근 - 위치 필드:
$숫자로 가져옴, 숫자는 이름 지정 필드를 건너뛰고 위치 필드 순서만 카운트
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가 같다고 인정받으려면 두 조건을 모두 만족해야 합니다.
- 두 Record의 구조 형태가 완전히 일치
- 대응하는 위치/이름 필드의 값이 모두 동일
이름 지정 필드의 작성 순서는 동등 비교에 영향을 주지 않지만, 필드명 자체가 다르면 타입 자체가 달라 무조건 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를 사용하지 않으면 아래 불편함이 발생합니다.
- 새 클래스 생성: 생성자, 필드 정의로 코드가 길어짐
- List 반환: 타입 안전성 상실, 모두 dynamic 타입으로 변환
- 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: 일반 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)
- 방법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. 요약
- 버전 요구사항: Dart 3.0 이상
- 3대 핵심 속성: 익명, 불변, 집합; 길이 고정, 이종 타입 저장, 강타입 제약
- 필드 두 종류: 위치 필드
$1/$2, 이름 지정 필드.name - 타입 판단 규칙: 구조 형태로 타입 결정, 이름 필드명이 형태에 포함됨; 위치 필드 별칭은 주석에 불과
- 동등 비교: 구조 형태 동일하고 모든 필드 값 일치하면 true
- 핵심 장점: 함수 다중 반환값, 가벼운 데이터 컨테이너, 완전한 타입 안전성
- 확장 방안: typedef로 타입 별칭 간소화, 추후 Class/Extension Type으로 무중단 교체 가능