Flutter 개발에서 컬렉션(List/Map/Set)은 상태(state), UI 렌더링 데이터, API 응답 파싱 결과를 담는 핵심 자료구조입니다. 문제는 “담는 것”보다 어떻게 바꾸는지(수정), 그리고 어떻게 안 바뀌게(불변) 설계하는지에서 실수가 많이 난다는 점입니다. 이 글에서 선언/수정/불변 패턴을 한 번에 정리해드리겠습니다.
List / Map / Set 요약
-
List: 순서가 있는 값들의 묶음 (인덱스로 접근, 중복 허용)
-
Map: key → value 매핑 (키는 유일, 값은 중복 가능)
-
Set: 중복 없는 값들의 집합 (순서 개념은 약함, “유일성”이 핵심)
선언(생성) 기본 패턴
List 선언
// 타입 명시 Listnumbers = [1, 2, 3]; // 타입 추론 var names = ['a', 'b', 'c']; // List // 빈 리스트: 타입 지정 습관이 버그를 줄임 final List tags = [];
자주 하는 실수
var empty = []; // empty는 "List"으로 추론되기 쉬워서, 이후 타입 안정성이 약해질 수 있음
권장
final ListemptyInts = [];
Map 선언
Mapscores = { 'kim': 10, 'lee': 20, }; var user = { 'name': 'hong', 'age': 30, }; // Map 비슷하게 추론될 수 있음(값 타입이 섞이면)
빈 맵도 타입 지정 권장
final Mapheaders = {};
Set 선언 (중요: {}는 기본이 Map!)
Dart에서 {}는 빈 Set이 아니라 빈 Map입니다. Set은 이렇게 선언합니다:
final Setids = {1, 2, 3}; // 빈 Set은 타입을 명시해야 Set으로 인식 final Set uniqueNames = {};
실수 예
var s = {};
// 이건 Set이 아니라 Map로 추론됨
수정(변경) 기본 API
List 수정: add / insert / remove / removeAt / clear
final list =['a', 'b']; list.add('c'); // ['a','b','c'] list.insert(1, 'x'); // ['a','x','b','c'] list.remove('b'); // 값 기준 삭제(첫 번째 1개) list.removeAt(0); // 인덱스 기준 삭제 list.clear(); // 모두 삭제
추가로 자주 쓰는 패턴
final numbers = [1, 2, 3, 4]; numbers.removeWhere((n) => n.isEven); // 홀수만 남김 -> [1,3]
Map 수정: []= / putIfAbsent / remove / update
final map ={'a': 1}; map['b'] = 2; // 추가/수정 map.putIfAbsent('c', () => 3); // 없을 때만 생성 map.update('a', (v) => v + 10); // 키가 있을 때 업데이트 map.remove('b'); // 삭제
키 존재 여부 체크
if (map.containsKey('a')) {
// ...
}
Set 수정
final set ={1, 2, 3}; set.add(3); // 변화 없음(이미 존재) set.add(4); // {1,2,3,4} set.remove(2); // {1,3,4} final a = {1, 2, 3}; final b = {3, 4, 5}; final u = a.union(b); // {1,2,3,4,5} final i = a.intersection(b); // {3} final d = a.difference(b); // {1,2}
불변(Immutable) 패턴이 중요한 이유
Flutter에서는 특히 다음 상황에서 “기존 컬렉션을 직접 수정”하면 문제가 생기기 쉽습니다.
-
상태관리에서 “같은 리스트를 계속 수정” → 변경 감지가 꼬이거나, 예상치 못한 UI 갱신/미갱신 발생
-
여러 곳에서 같은 리스트 참조 공유 → 한 곳에서 바꿨는데 다른 곳 데이터도 같이 바뀌는 “사이드 이펙트”
그래서 실무에서는 “원본은 그대로 두고, 새 컬렉션을 만들어 교체”하는 방식(불변 업데이트)을 많이 씁니다.
불변 업데이트(복사해서 수정) 실전 패턴
List 불변 업데이트: 스프레드(...)로 새 리스트 생성
final oldList = [1, 2, 3]; // 추가 final newList = [...oldList, 4]; // 중간 삽입(예: index 1에 99) final inserted = [ ...oldList.take(1), 99, ...oldList.skip(1), ]; // 삭제(조건) final removed = oldList.where((x) => x != 2).toList();
Map 불변 업데이트: 스프레드로 새 맵 생성
final oldMap = {'a': 1, 'b': 2};
// 키 추가/수정
final newMap = {...oldMap, 'b': 999, 'c': 3};
// 키 삭제(복사 후 제거)
final removed = Map.from(oldMap)..remove('a');
Set 불변 업데이트
final oldSet = {1, 2, 3};
// 추가
final newSet = {...oldSet, 4};
// 삭제
final removed = oldSet.where((x) => x != 2).toSet();
unmodifiable vs const 컬렉션 차이 (핵심 정리)
여기가 가장 헷갈리는 구간입니다.
-
const컬렉션: 컴파일 타임 상수로 만들어지는 “진짜 불변”에 가까움(수정 시도 자체가 런타임 전에 막히거나, 런타임에서 예외). -
unmodifiable컬렉션: “수정 메서드만 막는 래퍼/스냅샷” 성격. 얕은(shallow) 불변이며, 원본 참조가 살아있으면 원본이 변할 수도 있음.
const 컬렉션
const list = [1, 2, 3];
// list.add(4); // 불가
const map = {'a': 1};
// map['b'] = 2; // 불가
const set = {1, 2, 3};
// set.add(4); // 불가
특징
-
요소들도 const로 구성되어야 합니다(중첩도 포함).
-
“앱 실행 중 바뀌면 안 되는 설정값/기본값”에 적합.
-
Flutter에서 위젯 트리 최적화에도 도움이 되는 경우가 많습니다(특히 const 위젯과 함께).
unmodifiable 컬렉션
final source = [1, 2, 3]; final view = List.unmodifiable(source); // view.add(4); // 불가(예외)
중요 포인트(자주 터지는 버그)
final source = [1, 2, 3]; final view = List.unmodifiable(source); source.add(999); print(view); // [1, 2, 3, 999] 처럼 보일 수 있음(“원본이 바뀌면 뷰도 바뀜”)
즉, unmodifiable은 “이 참조를 통해서는 수정 못 하게” 막는 성격이지, 원본까지 봉인하는 “완전 불변”이 아닙니다.
const vs unmodifiable 사용 시기
-
const-
앱 전체에서 고정되는 값(상수 데이터, 라우트 이름 목록, 고정 메뉴, 고정 매핑 등)
-
“절대 바뀌면 안 된다”를 컴파일 타임부터 강하게 보장하고 싶을 때
-
-
unmodifiable-
외부로 컬렉션을 노출할 때(캡슐화) “읽기 전용 뷰”를 주고 싶을 때
-
예: 클래스 내부 리스트는 수정 가능하지만, getter로는 수정 못 하게 공개
-
class UserStore {
final List _users = [];
List get users => List.unmodifiable(_users);
void addUser(String name) {
_users.add(name);
}
}
final / const / unmodifiable 혼동 포인트 정리
final은 참조 변경 불가
final list = [1, 2, 3]; list.add(4); // 가능 (내용 변경) // list = [9, 9, 9]; // 불가 (참조 변경)
var은 재할당 가능
var list = [1, 2]; list.add(3); // 가능 list = [9, 9]; // 가능
const 내용 변경 자체가 불가
const list = [1, 2]; // list.add(3); // 불가
unmodifiable 수정 메서드만 막는 읽기 전용 성격
final src = [1, 2]; final ro = List.unmodifiable(src); // ro.add(3); // 불가
실무 팁
-
Flutter 상태 업데이트는 가능하면 이런 형태를 권장합니다.
-
List:
state = [...state, newItem] -
Map:
state = {...state, key: value}
-
-
외부로 컬렉션을 노출할 때는
unmodifiablegetter로 보호하세요. -
중첩 컬렉션(예:
List<Map<...>>)은 unmodifiable이 얕기 때문에 내부까지 안전하게 하려면 구조 자체를 불변으로 두거나, 별도 불변 모델(예: freezed) 사용을 검토하세요.