핵심 요약
순서와 중복 여부로 컬렉션을 선택하고, UI 상태에서는 같은 객체를 몰래 수정하기보다 변경 의도가 드러나는 갱신 방식을 사용하는 편이 안전합니다.
검수 범위
선언·복사·추가·삭제와 const 또는 불변 뷰의 차이를 현재 Dart 컬렉션 문서 기준으로 검수했습니다.
Flutter 개발에서 컬렉션(List/Map/Set)은 상태(state), UI 렌더링 데이터, API 응답 파싱 결과를 담는 핵심 자료구조입니다. 문제는 “담는 것”보다 어떻게 바꾸는지(수정), 그리고 어떻게 안 바뀌게(불변) 설계하는지에서 실수가 많이 난다는 점입니다. 이 글에서 선언/수정/불변 패턴을 한 번에 정리해드리겠습니다.
List / Map / Set 요약
-
List: 순서가 있는 값들의 묶음 (인덱스로 접근, 중복 허용)
-
Map: key → value 매핑 (키는 유일, 값은 중복 가능)
-
Set: 중복 없는 값들의 집합 (순서 개념은 약함, “유일성”이 핵심)
선언(생성) 기본 패턴
List 선언
// 타입 명시 List numbers = [1, 2, 3]; // 타입 추론 var names = ['a', 'b', 'c']; // List // 빈 리스트: 타입 지정 습관이 버그를 줄임 final List tags = [];
자주 하는 실수
var empty = []; // empty는 "List"으로 추론되기 쉬워서, 이후 타입 안정성이 약해질 수 있음
권장
final List emptyInts = [];
Map 선언
Map scores = {
'kim': 10,
'lee': 20,
};
var user = {
'name': 'hong',
'age': 30,
}; // Map 비슷하게 추론될 수 있음(값 타입이 섞이면)
빈 맵도 타입 지정 권장
final Map headers = {};
Set 선언 (중요: {}는 기본이 Map!)
Dart에서 {}는 빈 Set이 아니라 빈 Map입니다. Set은 이렇게 선언합니다:
final Set ids = {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) 사용을 검토하세요.