Dart 함수형 프로그래밍
이 문서 주변 탐색
주제 태그, 링크, 시리즈 흐름을 중심으로 옆으로 이동할 수 있습니다.
시리즈 흐름
이 문서는 아직 읽기 시리즈에 연결되지 않았습니다.
관련 문서
같이 읽을 만한 관련 문서가 아직 없습니다.
이 문서를 참조하는 문서
이 문서를 참조하는 다른 문서가 아직 없습니다.
1. 함수형 프로그래밍 기초
함수형 프로그래밍이란?
데이터를 변형(mutate)하지 않고, 새로운 데이터를 만들어내는 프로그래밍 패러다임임
// ❌ 명령형 (Imperative) - 데이터 변형
void addSuffixImperative(List<String> list) {
for (int i = 0; i < list.length; i++) {
list[i] = list[i] + '##'; // 원본 변경!
}
}
// ✅ 함수형 (Functional) - 새 데이터 생성
List<String> addSuffixFunctional(List<String> list) {
return list.map((x) => x + '##').toList(); // 새 리스트 반환
}핵심 원칙
원칙 | 설명 |
|---|---|
불변성 | 원본 데이터 변경 안 함 |
순수 함수 | 같은 입력 → 항상 같은 출력 |
일급 함수 | 함수를 값처럼 전달, 반환 |
합성 | 작은 함수들 조합해서 복잡한 로직 구성 |
Dart는 OOP + 함수형 모두 지원하는 멀티패러다임 언어임
함수형 패턴은 코드를 예측 가능하고 테스트하기 쉽게 만듦
Flutter 상태 관리(Provider, Riverpod, Bloc)도 함수형 원칙 따름
2. 컬렉션 변환 (List ↔ Map ↔ Set)
List → Map (asMap)
void main() {
List<String> list = ['a', 'b', 'c', 'd'];
// asMap() - 인덱스가 키가 됨
Map<int, String> map = list.asMap();
print(map); // {0: a, 1: b, 2: c, 3: d}
print(map.keys); // (0, 1, 2, 3)
print(map.values); // (a, b, c, d)
// keys, values는 Iterable이라서 List로 변환 필요
print(map.keys.toList()); // [0, 1, 2, 3]
print(map.values.toList()); // [a, b, c, d]
}List → Set (중복 제거)
void main() {
List<String> list = ['1', '2', '3', '3', '2', '1'];
// 방법 1: toSet()
Set<String> set1 = list.toSet();
print(set1); // {1, 2, 3}
// 방법 2: Set.from()
Set<String> set2 = Set.from(list);
print(set2); // {1, 2, 3}
// Set → List
List<String> uniqueList = set1.toList();
print(uniqueList); // [1, 2, 3]
}Map 변환
void main() {
Map<String, String> map = {
'1': '11',
'2': '22',
'3': '33',
};
// map.map() - MapEntry를 반환해야 함
final result = map.map(
(key, value) => MapEntry('oh-$key', 'my-$value'),
);
print(result); // {oh-1: my-11, oh-2: my-22, oh-3: my-33}
// keys나 values만 변환
final keys = map.keys.map((x) => 'hello $x').toList();
print(keys); // [hello 1, hello 2, hello 3]
final values = map.values.map((x) => '$x!').toList();
print(values); // [11!, 22!, 33!]
}asMap()은 인덱스를 키로 사용하는 Map 만듦toSet()이랑Set.from()은 동일한 결과 냄Set도
map(),where()등 함수형 메서드 다 지원함Map의
map()은 MapEntry 반환해야 해서 문법이 다름
3. map - 변환하기
기본 사용법
void main() {
List<String> list = ['1', '2', '3'];
// map으로 각 요소 변환
final newList = list.map((x) {
return x + '##';
});
print(newList); // (1##, 2##, 3##) ← 괄호 주목!
// 화살표 함수로 간결하게
final newList2 = list.map((x) => '$x###');
print(newList2); // (1###, 2###, 3###)
// ⚠️ 둘은 다른 객체!
print(newList == newList2); // false
}
실전 예시: 문자열 파싱
void main() {
String number = '12345';
// 각 숫자를 이미지 파일명으로 변환
final parsed = number
.split('') // ['1', '2', '3', '4', '5']
.map((x) => '$x.jpg') // (1.jpg, 2.jpg, ...)
.toList(); // [1.jpg, 2.jpg, ...]
print(parsed); // [1.jpg, 2.jpg, 3.jpg, 4.jpg, 5.jpg]
}인덱스가 필요할 때
void main() {
List<String> fruits = ['사과', '바나나', '오렌지'];
// 방법 1: asMap + entries
final indexed1 = fruits.asMap().entries.map(
(entry) => '${entry.key}: ${entry.value}',
).toList();
print(indexed1); // [0: 사과, 1: 바나나, 2: 오렌지]
// 방법 2: extension 활용 (직접 만들거나 collection 패키지)
// final indexed2 = fruits.mapIndexed((i, x) => '$i: $x').toList();
}4. where - 필터링하기
기본 사용법
void main() {
List<int> numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
// 짝수만 필터링
final evens = numbers.where((x) => x % 2 == 0);
print(evens); // (2, 4, 6, 8, 10)
print(evens.toList()); // [2, 4, 6, 8, 10]
}복잡한 객체 필터링
void main() {
List<Map<String, String>> people = [
{'name': '1번', 'group': '그룹A'},
{'name': '2번', 'group': '그룹B'},
{'name': '3번', 'group': '그룹A'},
{'name': '4번', 'group': '그룹C'},
{'name': '5번', 'group': '그룹A'},
];
// 그룹A인 사람들만
final groupA = people.where((x) => x['group'] == '그룹A');
print(groupA.toList());
// [{name: 1번, group: 그룹A}, {name: 3번, group: 그룹A}, {name: 5번, group: 그룹A}]
// 이름에 '1' 포함된 사람
final hasOne = people.where((x) => x['name']!.contains('1'));
print(hasOne.toList()); // [{name: 1번, group: 그룹A}]
}관련 메서드들
void main() {
List<int> numbers = [1, 2, 3, 4, 5];
// firstWhere - 조건에 맞는 첫 번째 요소
final first = numbers.firstWhere((x) => x > 3);
print(first); // 4
// firstWhere with orElse - 없으면 기본값
final notFound = numbers.firstWhere(
(x) => x > 10,
orElse: () => -1,
);
print(notFound); // -1
// lastWhere - 조건에 맞는 마지막 요소
final last = numbers.lastWhere((x) => x < 4);
print(last); // 3
// any - 하나라도 만족하면 true
print(numbers.any((x) => x > 4)); // true
// every - 모두 만족하면 true
print(numbers.every((x) => x > 0)); // true
}where는 JavaScript의filter랑 동일함firstWhere는find,any는some,every는every랑 같음where도 Iterable 반환하니까 필요하면toList()호출해야 함
5. reduce와 fold - 접기
reduce - 리스트를 하나의 값으로
void main() {
List<int> numbers = [1, 2, 3, 4, 5];
// reduce: 첫 번째 요소부터 시작
final sum = numbers.reduce((prev, curr) => prev + curr);
print(sum); // 15
// 동작 과정:
// 1 + 2 = 3
// 3 + 3 = 6
// 6 + 4 = 10
// 10 + 5 = 15
// 최댓값 찾기
final max = numbers.reduce((a, b) => a > b ? a : b);
print(max); // 5
// 문자열 합치기
List<String> words = ['Hello', 'World', 'Dart'];
final sentence = words.reduce((a, b) => '$a $b');
print(sentence); // Hello World Dart
}⚠️ reduce의 제한
void main() {
List<int> numbers = [1, 2, 3, 4, 5];
// ❌ reduce는 반환 타입이 리스트 요소 타입과 같아야 함!
// final asString = numbers.reduce((a, b) => '$a$b'); // 에러!
// JavaScript에서는 가능하지만 Dart에서는 불가:
// [1,2,3].reduce((acc, x) => acc + '-' + x, '') // JS는 됨
}fold - 초기값과 다른 타입 반환
void main() {
List<int> numbers = [1, 2, 3, 4, 5];
// fold: 초기값을 지정할 수 있음
final sum = numbers.fold(0, (prev, curr) => prev + curr);
print(sum); // 15
// 다른 타입으로 변환 가능!
final asString = numbers.fold<String>(
'',
(prev, curr) => prev.isEmpty ? '$curr' : '$prev-$curr',
);
print(asString); // 1-2-3-4-5
// 초기값이 있어서 빈 리스트도 안전
List<int> empty = [];
// empty.reduce((a, b) => a + b); // ❌ 에러! (빈 리스트)
final safeSum = empty.fold(0, (a, b) => a + b); // ✅ 0
}fold 실전 예시
void main() {
List<Map<String, dynamic>> items = [
{'name': '사과', 'price': 1000, 'qty': 3},
{'name': '바나나', 'price': 500, 'qty': 5},
{'name': '오렌지', 'price': 800, 'qty': 2},
];
// 총 금액 계산
final totalPrice = items.fold<int>(
0,
(sum, item) => sum + (item['price'] as int) * (item['qty'] as int),
);
print('총 금액: $totalPrice원'); // 총 금액: 6100원
// 짝수/홀수 분류
List<int> numbers = [1, 2, 3, 4, 5, 6, 7, 8];
final grouped = numbers.fold<Map<String, List<int>>>(
{'even': [], 'odd': []},
(acc, n) {
if (n % 2 == 0) {
acc['even']!.add(n);
} else {
acc['odd']!.add(n);
}
return acc;
},
);
print(grouped); // {even: [2, 4, 6, 8], odd: [1, 3, 5, 7]}
}reduce: 리스트 요소랑 같은 타입만 반환 가능, 빈 리스트 불가fold: 초기값 지정 가능, 다른 타입 반환 가능, 빈 리스트 안전JavaScript의
reduce는 Dart의fold랑 더 비슷함 (초기값 지정 가능)복잡한 집계 로직에는 항상
fold쓰면 됨
6. Iterable vs List - ()와 []의 차이
왜 괄호()랑 대괄호[]가 다를까?
void main() {
List<String> list = ['1', '2', '3'];
// map은 Iterable을 반환 (게으른 평가)
final mapped = list.map((x) => '$x##');
print(mapped); // (1##, 2##, 3##) ← 괄호!
print(mapped.runtimeType); // MappedListIterable<String, String>
// toList()로 List로 변환
final asList = mapped.toList();
print(asList); // [1##, 2##, 3##] ← 대괄호!
print(asList.runtimeType); // List<String>
}Iterable의 특징 (게으른 평가)
void main() {
var numbers = [1, 2, 3, 4, 5];
// map은 즉시 실행 안 됨!
var mapped = numbers.map((x) {
print('변환 중: $x');
return x * 2;
});
print('--- map 호출 완료 ---');
// 아직 "변환 중" 출력 안 됨!
print('--- toList 호출 ---');
var result = mapped.toList();
// 이제야 "변환 중" 출력됨!
// 출력:
// --- map 호출 완료 ---
// --- toList 호출 ---
// 변환 중: 1
// 변환 중: 2
// 변환 중: 3
// 변환 중: 4
// 변환 중: 5
}언제 toList()를 호출해야 할까?
void main() {
var numbers = [1, 2, 3, 4, 5];
// ✅ toList() 필요한 경우
// 1. 인덱스 접근이 필요할 때
var list = numbers.map((x) => x * 2).toList();
print(list[0]); // 2
// 2. length를 효율적으로 알아야 할 때
print(list.length); // 5
// 3. 결과를 여러 번 사용할 때
for (var i = 0; i < 3; i++) {
for (var item in list) {
print(item);
}
}
// ⚠️ toList() 없이 Iterable 재사용하면 매번 재계산!
var iterable = numbers.map((x) {
print('계산!');
return x * 2;
});
// 두 번 순회하면 두 번 계산
iterable.forEach(print); // 계산! 5번
iterable.forEach(print); // 또 계산! 5번
// ✅ toList() 불필요한 경우
// 1. 체이닝 중간 단계
var result = numbers
.map((x) => x * 2) // toList 불필요
.where((x) => x > 5) // toList 불필요
.toList(); // 마지막에만!
// 2. 한 번만 순회할 때
numbers.map((x) => x * 2).forEach(print);
}()= Iterable (게으른 평가, lazy evaluation)[]= List (즉시 평가, eager evaluation)Iterable은 순회할 때마다 재계산됨
성능상 체이닝 중간에는 toList 불필요, 마지막에만 호출하면 됨
결과를 여러 번 사용하거나 인덱스 접근 필요하면
toList()해야 함
7. Spread 연산자 (...)
기본 사용법
void main() {
var list1 = [1, 2, 3];
var list2 = [4, 5, 6];
// Spread 연산자로 합치기
var combined = [...list1, ...list2];
print(combined); // [1, 2, 3, 4, 5, 6]
// 중간에 요소 추가
var withMiddle = [...list1, 100, ...list2];
print(withMiddle); // [1, 2, 3, 100, 4, 5, 6]
}짝수/홀수 분리 예시
void main() {
var numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
var even = numbers.where((x) => x % 2 == 0);
var odd = numbers.where((x) => x % 2 == 1);
// ❌ 중첩 리스트가 됨
print([even, odd]);
// [(2, 4, 6, 8, 10), (1, 3, 5, 7, 9)]
// ✅ Spread로 펼치기
print([...even, ...odd]);
// [2, 4, 6, 8, 10, 1, 3, 5, 7, 9]
// toList() 없이도 Spread 사용 가능!
// Spread가 Iterable을 자동으로 펼침
}Null-aware Spread (...?)
void main() {
List<int>? nullableList;
var list = [1, 2, 3];
// ❌ null이면 에러!
// var combined = [...nullableList, ...list];
// ✅ null-aware spread
var safeCombined = [...?nullableList, ...list];
print(safeCombined); // [1, 2, 3]
// null이 아니면 정상 동작
nullableList = [0];
var result = [...?nullableList, ...list];
print(result); // [0, 1, 2, 3]
}Set과 Map에도 사용 가능
void main() {
// Set spread
var set1 = {1, 2, 3};
var set2 = {3, 4, 5};
var combinedSet = {...set1, ...set2};
print(combinedSet); // {1, 2, 3, 4, 5} - 중복 제거됨
// Map spread
var map1 = {'a': 1, 'b': 2};
var map2 = {'c': 3, 'd': 4};
var combinedMap = {...map1, ...map2};
print(combinedMap); // {a: 1, b: 2, c: 3, d: 4}
// Map 덮어쓰기
var override = {...map1, 'a': 100}; // 'a' 값 변경
print(override); // {a: 100, b: 2}
}Collection if와 for
void main() {
bool showExtra = true;
// Collection if
var items = [
'item1',
'item2',
if (showExtra) 'extraItem',
];
print(items); // [item1, item2, extraItem]
// Collection for
var numbers = [1, 2, 3];
var doubled = [
for (var n in numbers) n * 2,
];
print(doubled); // [2, 4, 6]
// 조합
var complex = [
'header',
for (var i = 0; i < 3; i++)
if (i.isEven) 'even-$i',
'footer',
];
print(complex); // [header, even-0, even-2, footer]
}Spread(
...)는 새로운 컬렉션 생성함 (불변성 유지)...?는 null 안전한 spread임Collection if/for로 선언적으로 컬렉션 구성 가능
JavaScript의 spread랑 동일하게 동작함
8. 기타 유용한 메서드
take와 skip
void main() {
var numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
// take - 처음 N개
print(numbers.take(3).toList()); // [1, 2, 3]
// skip - 처음 N개 건너뛰기
print(numbers.skip(7).toList()); // [8, 9, 10]
// 조합: 페이지네이션
int page = 2;
int pageSize = 3;
var pageItems = numbers.skip((page - 1) * pageSize).take(pageSize);
print(pageItems.toList()); // [4, 5, 6]
}expand (flatMap)
void main() {
var nested = [[1, 2], [3, 4], [5, 6]];
// expand로 평탄화
var flat = nested.expand((x) => x);
print(flat.toList()); // [1, 2, 3, 4, 5, 6]
// 각 요소를 여러 개로 확장
var numbers = [1, 2, 3];
var repeated = numbers.expand((x) => [x, x, x]);
print(repeated.toList()); // [1, 1, 1, 2, 2, 2, 3, 3, 3]
}sort와 sorted
void main() {
var numbers = [3, 1, 4, 1, 5, 9, 2, 6];
// sort - 원본 변경 (비함수형!)
// numbers.sort();
// sorted 패턴 - 새 리스트 반환 (함수형)
var sorted = [...numbers]..sort();
print(sorted); // [1, 1, 2, 3, 4, 5, 6, 9]
print(numbers); // [3, 1, 4, 1, 5, 9, 2, 6] - 원본 유지
// 커스텀 정렬
var people = [
{'name': '철수', 'age': 25},
{'name': '영희', 'age': 22},
{'name': '민수', 'age': 30},
];
var byAge = [...people]..sort((a, b) =>
(a['age'] as int).compareTo(b['age'] as int)
);
}reversed
void main() {
var numbers = [1, 2, 3, 4, 5];
// reversed - Iterable 반환
print(numbers.reversed); // (5, 4, 3, 2, 1)
print(numbers.reversed.toList()); // [5, 4, 3, 2, 1]
// 원본은 변경 안 됨
print(numbers); // [1, 2, 3, 4, 5]
}9. 체이닝과 조합
메서드 체이닝
void main() {
var data = [
{'name': '사과', 'price': 1000, 'inStock': true},
{'name': '바나나', 'price': 500, 'inStock': true},
{'name': '오렌지', 'price': 800, 'inStock': false},
{'name': '포도', 'price': 2000, 'inStock': true},
{'name': '수박', 'price': 15000, 'inStock': true},
];
// 체이닝으로 복잡한 로직 표현
var result = data
.where((item) => item['inStock'] == true) // 재고 있는 것만
.where((item) => (item['price'] as int) < 2000) // 2000원 미만
.map((item) => item['name']) // 이름만 추출
.toList();
print(result); // [사과, 바나나]
}가독성 높이기
void main() {
var users = [/* ... */];
// ❌ 한 줄로 길게
var result = users.where((u) => u.isActive).map((u) => u.name).where((n) => n.startsWith('A')).toList();
// ✅ 줄바꿈으로 가독성 향상
var betterResult = users
.where((u) => u.isActive)
.map((u) => u.name)
.where((n) => n.startsWith('A'))
.toList();
// ✅ 중간 변수로 의미 부여
var activeUsers = users.where((u) => u.isActive);
var names = activeUsers.map((u) => u.name);
var filteredNames = names.where((n) => n.startsWith('A')).toList();
}10. 실전 패턴
패턴 1: 데이터 변환 파이프라인
class Product {
final String name;
final double price;
final String category;
Product(this.name, this.price, this.category);
}
void main() {
var products = [
Product('아이폰', 1200000, '전자기기'),
Product('갤럭시', 1000000, '전자기기'),
Product('에어팟', 200000, '전자기기'),
Product('책상', 300000, '가구'),
Product('의자', 150000, '가구'),
];
// 전자기기의 총 가격
var totalElectronics = products
.where((p) => p.category == '전자기기')
.map((p) => p.price)
.fold(0.0, (sum, price) => sum + price);
print('전자기기 총액: $totalElectronics원'); // 2400000원
// 카테고리별 그룹화
var byCategory = products.fold<Map<String, List<Product>>>(
{},
(map, product) {
map.putIfAbsent(product.category, () => []);
map[product.category]!.add(product);
return map;
},
);
print(byCategory.keys); // (전자기기, 가구)
}패턴 2: 중복 제거와 정렬
void main() {
var tags = ['dart', 'flutter', 'dart', 'mobile', 'flutter', 'web'];
// 중복 제거 + 정렬
var uniqueSorted = tags.toSet().toList()..sort();
print(uniqueSorted); // [dart, flutter, mobile, web]
// 또는 함수형으로
var result = [...{...tags}]..sort();
print(result); // [dart, flutter, mobile, web]
}패턴 3: 조건부 리스트 생성
void main() {
bool isAdmin = true;
bool isPremium = false;
var menuItems = [
'Home',
'Profile',
if (isAdmin) 'Admin Panel',
if (isPremium) 'Premium Features',
'Settings',
];
print(menuItems); // [Home, Profile, Admin Panel, Settings]
}패턴 4: 객체 복사와 업데이트 (불변 패턴)
class User {
final String name;
final int age;
final List<String> hobbies;
const User({
required this.name,
required this.age,
required this.hobbies,
});
// copyWith 패턴
User copyWith({
String? name,
int? age,
List<String>? hobbies,
}) {
return User(
name: name ?? this.name,
age: age ?? this.age,
hobbies: hobbies ?? this.hobbies,
);
}
}
void main() {
var user = User(name: '모리', age: 25, hobbies: ['독서', '코딩']);
// 원본 변경 없이 새 객체 생성
var olderUser = user.copyWith(age: 26);
var newHobbies = user.copyWith(hobbies: [...user.hobbies, '운동']);
print(user.age); // 25 (원본 유지)
print(olderUser.age); // 26
print(newHobbies.hobbies); // [독서, 코딩, 운동]
}본 글은 AI의 도움을 받아 작성되었습니다.
읽기 시리즈
현재 위치를 확인하고, 흐름을 따라 바로 다음 읽기로 이어갈 수 있습니다.