Flutter를 시작하면 가장 먼저 마주치는 것이 Dart 문법이고, 그중에서도 “변수 선언”과 “타입”을 제대로 이해해두면 이후 위젯, 상태관리, 비동기까지 훨씬 수월해집니다. 이 글에서는 Dart에서 변수를 선언하는 방법, 타입 시스템의 핵심, 그리고 실무에서 자주 겪는 실수 포인트까지 한 번에 정리합니다.
변수
변수(variable)는 값을 담아두는 이름표입니다. 프로그램은 실행 중에 숫자, 문자열, 참/거짓 같은 값을 계속 다루는데, 그 값을 재사용하기 위해 변수에 저장합니다.
Dart에서는 변수를 선언할 때 “타입을 명시”할 수도 있고, “자동으로 추론”하게 할 수도 있습니다. 또한 “값이 바뀔 수 있는가(가변/불변)”에 따라 var / final / const를 구분해서 씁니다.
var, final, const 차이
var: 일반 변수(가변)
var는 값을 바꿀 수 있는 변수입니다. 처음 값을 대입하면 Dart가 타입을 추론합니다.
-
초기값을 넣으면 타입이 고정됩니다.
-
이후 다른 타입 값을 넣으려 하면 오류가 납니다.
예: 처음에 정수로 시작하면 계속 정수로 유지됩니다.
final: 한 번만 대입(런타임 상수 느낌)
final은 “한 번만 값이 결정되는 변수”입니다.
-
실행 중에 값이 정해질 수 있습니다. (예: API 결과, 현재시간 등)
-
단, 한 번 값이 들어가면 변경 불가입니다.
const: 컴파일 타임 상수(진짜 상수)
const는 “컴파일 시점에 값이 확정되는 상수”입니다.
-
빌드(컴파일) 단계에서 이미 값이 확정되어 있어야 합니다.
-
완전히 고정된 리터럴/상수 표현식만 가능.
실무 감각으로는 이렇게 정리하면 편합니다.
-
값이 바뀔 가능성이 있으면
var -
값은 안 바뀌는데 실행 중에 결정될 수 있으면
final -
앱 코드상 완전히 고정된 값이면
const
Flutter(Dart) 코드 작성 기본: var / final / const로 변수 선언
타입(Type) 시스템의 핵심: 정적 타입 + 타입 추론
Dart는 정적 타입 언어입니다. 즉, 변수의 타입이 명확해야 하고, 컴파일 단계에서 타입 오류를 잡아줍니다. 다만 개발 편의를 위해 타입을 생략하면 Dart가 타입을 추론해줍니다.
-
타입을 직접 쓰는 방식: 코드 명확성 ↑
-
타입을 생략하고 추론: 코드 간결성 ↑
Flutter 실무에서는 “위젯 트리/콜백 타입”이 복잡해질수록 명시 타입이 디버깅에 유리해지는 경우가 많습니다.
기본 타입(Primitive/코어 타입)
Dart에서 가장 많이 쓰는 대표 타입은 아래와 같습니다.
int (정수)
정수 값에 사용합니다. 예: 0, 1, -10
double (실수)
소수점이 있는 수에 사용합니다. 예: 3.14, 0.5
num (숫자 상위 타입)
int와 double을 모두 포함하는 타입입니다. 숫자 처리를 통합하고 싶을 때 사용합니다.
String (문자열)
텍스트를 저장합니다. Dart는 문자열 보간(interpolation)이 강력합니다.
-
${변수}또는$변수형태로 문자열 안에 값을 삽입할 수 있습니다.
bool (불리언)
true / false 두 값만 가집니다. 조건문, 토글 상태 등에서 사용합니다.
dynamic, Object, 그리고 타입 안정성
Dart에는 “타입을 유연하게” 쓰는 방법이 몇 가지 있습니다. 하지만 Flutter 프로젝트에서는 무분별하게 쓰면 런타임 에러가 늘어나는 원인이 됩니다.
Object
Dart의 거의 모든 값이 Object로 표현될 수 있습니다.
-
다만
Object로 받으면 “그 값이 무엇인지” 확실치 않기 때문에 사용 시 캐스팅/검사가 필요합니다.
dynamic
dynamic은 타입 체크를 느슨하게 만들어, 컴파일 단계에서 잡아야 할 오류가 런타임으로 넘어갈 수 있습니다.
-
빠르게 테스트할 때는 편하지만, 운영 코드에서는 최소화하는 편이 좋습니다.
정리하면:
-
가능한 한 정적 타입 유지
-
정말 필요할 때만
Object또는dynamic사용 -
외부 데이터(JSON 등)를 다룰 때는 파싱/모델링으로 안정성 확보
Null Safety(널 안정성): 타입 뒤의 ? 의미
Dart(2.12+)는 Null Safety가 기본입니다. 즉, 기본적으로 변수는 null을 가질 수 없습니다.
-
String name;은 null 불가 -
String? name;은 null 가능
?는 “Nullable(널 가능)”을 의미합니다. Flutter에서 특히 API 응답, 선택 입력값, 조건부 위젯 등에서 매우 자주 쓰입니다.
널 가능 타입을 쓸 때는 다음 케이스를 신경 써야 합니다.
-
값이 null일 수 있는지
-
null일 때 대체값을 줄지
-
null이 아니라고 확신할 근거가 있는지
컬렉션 타입: List, Map, Set
UI/데이터 처리에서 가장 많이 쓰는 구조입니다.
List<T>
순서가 있는 배열입니다.
-
예:
List<String>은 문자열 목록
Map<K, V>
키-값 구조입니다.
-
예:
Map<String, int>는 문자열 키로 정수 값을 저장
Set<T>
중복을 허용하지 않는 집합입니다.
-
태그 목록, 선택 항목 중복 제거 등에 사용
Dart는 제네릭(Generic)으로 내부 타입을 명확히 지정할 수 있어서, 컬렉션에서도 타입 안정성을 확보할 수 있습니다.
late 키워드: “나중에 초기화” 선언
late는 “지금은 값이 없지만, 사용하기 전에 반드시 넣겠다”는 약속입니다.
-
생성자 이후에 초기화되거나
-
특정 흐름에서만 초기화되는 값에 사용됩니다.
주의할 점은, 실제로 값을 넣기 전에 접근하면 런타임 에러가 납니다. 즉 late는 편하지만, 책임도 함께 커집니다.
타입 변환(캐스팅/파싱) 기본 감각
앱 개발에서는 문자열로 들어온 값을 숫자로 바꾸거나(JSON, 입력값), 숫자를 문자열로 바꾸는 일이 흔합니다.
-
문자열 → 숫자: 파싱(parse)
-
숫자 → 문자열: 변환(toString)
또한 as 캐스팅은 타입이 확실할 때만 사용해야 하며, 불확실하면 타입 체크(is)가 안전합니다.
실무에서 가장 추천하는 변수/타입 습관
Flutter/Dart로 앱을 만들 때 실수를 줄이는 습관은 아래 3가지입니다.
-
가능하면
final을 기본값으로 두고, 정말 필요할 때만var로 바꾸기 -
dynamic은 최소화하고, JSON은 모델 클래스로 파싱해서 타입을 고정하기 -
Nullable(
?)는 “정말 null일 수 있는가”를 기준으로 설계하고, null 처리 흐름을 코드에 명확히 드러내기
결론
Dart의 변수와 타입은 Flutter 개발 전반의 안정성과 생산성을 좌우하는 핵심 기초입니다. var, final, const로 “값 변경 가능성”을 명확히 구분하고, int/double/String/bool 같은 기본 타입과 List/Map/Set 같은 컬렉션 타입을 제네릭으로 정확히 선언하면 타입 오류를 초기에 차단할 수 있습니다. 또한 Null Safety에서 ?의 의미를 분명히 이해하고, dynamic 사용을 최소화하며, 외부 데이터(JSON)는 모델링과 파싱으로 타입을 고정하는 습관을 들이면 런타임 오류를 크게 줄일 수 있습니다. 결국 “가능하면 타입을 명확히, 값은 불변으로, null은 설계로 관리”하는 방향이 Dart 초보에서 실무 단계로 넘어가는 가장 빠른 길입니다.
FAQ
var, final, const는 언제 각각 쓰는 게 가장 좋나요?
값이 바뀔 가능성이 있으면 var를 쓰고, 앱 실행 중 한 번만 정해지고 이후 바뀌지 않으면 final을 쓰는 것이 안전합니다. 코드 상에서 완전히 고정된 상수(컴파일 시점에 확정되는 값)라면 const를 사용합니다. 실무에서는 기본을 final로 두고, 필요할 때만 var로 바꾸는 습관이 오류를 줄이는 데 효과적입니다.
var로 선언하면 타입이 계속 바뀔 수 있나요?
아닙니다. var는 “타입을 생략한 선언”일 뿐이며, 최초 대입된 값으로 타입이 추론되어 고정됩니다. 예를 들어 처음에 정수를 넣었다면 이후 문자열을 넣을 수 없습니다. 타입을 유연하게 바꾸고 싶다면 dynamic이 필요하지만, 그만큼 런타임 오류 위험이 커집니다.
dynamic과 Object는 뭐가 다르고 어떤 걸 써야 하나요?
Object는 “무슨 타입인지 모른다”는 의미로 받을 수 있지만, 사용 시에는 타입 체크나 캐스팅이 필요합니다. dynamic은 컴파일러의 타입 검사를 느슨하게 만들어, 잘못된 접근이 컴파일 단계에서 잡히지 않고 런타임으로 넘어갈 수 있습니다. 불가피하게 범용 타입이 필요하다면 먼저 Object를 고려하고, dynamic은 정말 필요한 경우에만 제한적으로 사용하는 것이 좋습니다.
Null Safety에서 String과 String?의 차이는 정확히 뭔가요?
String은 null을 가질 수 없는 타입이고, String?는 null을 가질 수 있는 타입입니다. Dart는 기본적으로 null 불가가 기본값이기 때문에, null이 들어올 가능성이 있는 값(선택 입력값, API 응답의 선택 필드 등)에만 ?를 붙이는 것이 안전합니다.
! 연산자와 late는 언제 써야 하고 위험한 점은 뭔가요?
!는 “절대 null이 아니다”라고 강제로 단언하는 것이고, late는 “지금은 값이 없지만 사용 전에는 반드시 초기화하겠다”는 약속입니다. 둘 다 잘못 쓰면 런타임 에러로 이어질 수 있습니다. !는 논리적으로 null이 될 수 없다는 근거가 확실할 때만 쓰고, late는 초기화 흐름이 명확하고 테스트 가능한 경우에만 사용해야 합니다.
List, Map, Set은 언제 쓰는 게 맞나요?
List는 순서가 중요한 목록(아이템 리스트, 화면 렌더링 데이터)에 적합합니다. Map은 키로 빠르게 값을 찾는 구조(JSON 파싱, 설정값, id 기반 조회)에 적합합니다. Set은 중복이 없어야 하는 집합(태그 중복 제거, 선택 항목 관리)에 적합합니다. 컬렉션은 제네릭으로 내부 타입을 명확히 지정하는 것이 유지보수에 유리합니다.
num 타입은 언제 유용하고 int/double만 써도 되나요?
num은 int와 double의 상위 타입이라, 숫자 타입이 혼재할 수 있는 계산 로직에서 유용합니다. 하지만 UI나 데이터 모델에서는 가능한 한 int 또는 double로 명확히 고정하는 것이 좋습니다. 예외적으로 계산 결과가 정수/실수가 섞일 수 있거나 외부 입력이 다양하게 들어올 때 num을 고려합니다.
타입 변환은 어떤 방식이 안전한가요?
문자열을 숫자로 바꿀 때는 파싱을 사용하고, 실패 가능성을 고려해 예외 처리나 안전한 변환 패턴을 적용하는 것이 좋습니다. 반대로 숫자를 문자열로 바꿀 때는 toString() 계열을 사용합니다. 외부 데이터(JSON)는 먼저 Map<String, dynamic> 형태로 받은 뒤 모델 클래스로 변환하면서 타입을 확정하는 방식이 가장 안정적입니다.
JSON을 받을 때 dynamic을 많이 쓰게 되는데, 어떻게 줄일 수 있나요?
핵심은 “모델링”입니다. API 응답을 그대로 dynamic으로 굴리지 말고, 필요한 필드를 기준으로 모델 클래스를 만들고, fromJson 같은 변환 로직에서 타입을 확정하세요. 이렇게 하면 UI나 비즈니스 로직은 강한 타입 위에서 동작하게 되어, 런타임 오류가 급격히 줄어듭니다.
초보자가 가장 흔히 하는 타입 관련 실수는 무엇인가요?
대표적으로 nullable 설계 없이 !로만 해결하려는 습관, dynamic을 남발해 컴파일 단계에서 잡을 수 있는 오류를 런타임으로 넘기는 습관, 컬렉션 제네릭을 생략해 타입 혼란을 만드는 경우가 많습니다. 해결책은 final 중심으로 변수 선언을 정리하고, nullable은 설계로 관리하며, 컬렉션과 모델에서 타입을 명확히 고정하는 것입니다.