기초

변수와 타입

var, final, const, 명시 타입을 구분하고 언제 무엇을 쓰는지 정리합니다.

개념 먼저 보기

변수는 값을 담는 이름표입니다

변수는 숫자, 글자, 객체, 위젯, 상태값 같은 데이터를 코드 안에서 다시 사용할 수 있게 이름을 붙인 것입니다. Dart는 정적 타입 언어라서 변수마다 어떤 종류의 값이 들어갈 수 있는지 타입으로 표현합니다.

Flutter 코드에서 변수는 단순 계산값뿐 아니라 화면에 표시할 제목, 위젯 생성자에 전달되는 값, State 안에서 바뀌는 상태, 앱 전체에서 공유하는 상수까지 여러 위치에 등장합니다. 그래서 먼저 '어디에 선언되는 변수인지'와 '값이 바뀌는 변수인지'를 나누어 보면 이해가 쉬워집니다.

종류별로 하나씩

Dart와 Flutter에서 자주 보는 변수 형태

명시 타입 변수

String, int, double, bool처럼 타입을 직접 적는 방식입니다. 읽는 사람이 변수에 어떤 값이 들어가는지 바로 알 수 있습니다.

언제 쓰나: 모델 필드, 함수 반환값, 공개 API처럼 타입이 문서 역할을 해야 할 때 좋습니다.

Flutter에서: 위젯의 final 필드나 데이터 모델 클래스에서 가장 자주 봅니다.

// String 타입: 글자만 담을 수 있습니다.
String title = 'Flutter Wiki';

// int 타입: 정수만 담을 수 있습니다.
int completedCount = 3;

// bool 타입: true 또는 false만 담습니다.
bool isPublished = false;

var

초기값을 보고 Dart가 타입을 추론하게 하는 방식입니다. var라고 해서 아무 타입이나 들어가는 것은 아닙니다. 처음 String으로 추론되면 계속 String 변수입니다.

언제 쓰나: 지역 변수처럼 타입이 오른쪽 값만 봐도 너무 명확할 때 씁니다.

Flutter에서: build 메서드 안에서 임시 목록이나 계산값을 만들 때 자주 씁니다.

// 오른쪽 값이 문자열이므로 title은 String으로 추론됩니다.
var title = 'Dart 변수';

// 이 줄은 오류입니다. 이미 String으로 추론된 변수에 int를 넣을 수 없습니다.
// title = 10;

// 오른쪽 값이 리스트이므로 topics는 List<String>에 가깝게 추론됩니다.
var topics = ['변수', '함수', '클래스'];

final

한 번 값을 넣으면 다시 다른 값으로 바꿀 수 없는 변수입니다. 런타임에 계산된 값도 final에 담을 수 있습니다.

언제 쓰나: 값을 한 번 정한 뒤 바뀌면 안 되는 경우 기본 선택지로 삼으면 좋습니다.

Flutter에서: StatelessWidget의 생성자 값, 모델 객체의 필드, build 안의 계산 결과에 자주 사용합니다.

class LessonTitle extends StatelessWidget {
  const LessonTitle({super.key, required this.title});

  // 위젯이 받은 제목은 위젯 내부에서 바뀌지 않아야 하므로 final을 씁니다.
  final String title;

  @override
  Widget build(BuildContext context) {
    // Theme.of(context)는 런타임에 알 수 있지만 한 번 정한 뒤 바꾸지 않으므로 final이 적합합니다.
    final textTheme = Theme.of(context).textTheme;

    return Text(title, style: textTheme.titleLarge);
  }
}

const

컴파일 시점에 이미 값이 확정되는 상수입니다. final보다 더 강한 고정값이라고 생각하면 됩니다.

언제 쓰나: 숫자 상수, 고정 문자열, const 생성자가 있는 불변 객체를 만들 때 사용합니다.

Flutter에서: 변하지 않는 위젯 앞에 const를 붙이면 불필요한 객체 생성을 줄이고 코드 의도를 분명하게 합니다.

// 앱 전체에서 반복해서 쓰는 고정 여백입니다.
const double pagePadding = 16;

// Text 위젯도 const 생성자를 지원하므로 고정 문구라면 const를 붙일 수 있습니다.
const emptyMessage = Text('아직 문서가 없습니다.');

// 런타임 값은 const가 될 수 없습니다.
// const now = DateTime.now(); // 오류

late

선언 시점에는 값을 넣지 않지만, 사용하기 전에는 반드시 초기화하겠다고 약속하는 변수입니다.

언제 쓰나: initState에서 초기화해야 하는 컨트롤러처럼 생성 시점과 초기화 시점이 다른 경우에 씁니다.

Flutter에서: AnimationController, TabController, TextEditingController 같은 객체를 State에서 다룰 때 종종 등장합니다.

class _SearchPageState extends State<SearchPage> {
  // initState에서 초기화할 것이므로 late를 사용합니다.
  late final TextEditingController controller;

  @override
  void initState() {
    super.initState();
    controller = TextEditingController();
  }

  @override
  void dispose() {
    // 직접 만든 컨트롤러는 화면이 사라질 때 정리합니다.
    controller.dispose();
    super.dispose();
  }
}

nullable 변수

타입 뒤에 ?를 붙이면 null이 들어갈 수 있는 변수가 됩니다. 값이 없을 수 있다는 사실을 타입에 표시하는 방식입니다.

언제 쓰나: 선택 입력, 아직 로드되지 않은 데이터, 실패할 수 있는 검색 결과처럼 값이 없을 수 있을 때 사용합니다.

Flutter에서: API 응답 모델, 선택된 항목, 비동기 로딩 전 데이터에서 자주 쓰입니다.

String? selectedTopic;

// null이면 기본 문구를 사용합니다.
final label = selectedTopic ?? '선택된 주제가 없습니다.';

// null 검사를 통과한 뒤에는 안전하게 사용할 수 있습니다.
if (selectedTopic != null) {
  print(selectedTopic.length);
}

dynamic과 Object

dynamic은 타입 검사를 미루는 변수이고, Object는 모든 값의 최상위 타입에 가깝습니다. 둘 다 남용하면 코드가 불안정해질 수 있습니다.

언제 쓰나: 외부 JSON처럼 타입을 아직 모르는 값을 잠깐 받을 때만 제한적으로 사용합니다.

Flutter에서: Map<String, dynamic> 형태의 JSON을 모델 클래스로 변환하는 중간 단계에서 자주 봅니다.

// 서버에서 온 원본 JSON은 dynamic 값을 포함할 수 있습니다.
final Map<String, dynamic> json = {
  'title': '변수와 타입',
  'minutes': 20,
};

// 가능한 한 빨리 명확한 타입으로 변환하는 것이 좋습니다.
final title = json['title'] as String;
final minutes = json['minutes'] as int;

State 안의 상태 변수

StatefulWidget의 State 클래스 안에 선언되어 화면의 현재 상태를 기억하는 변수입니다.

언제 쓰나: 버튼 클릭 횟수, 입력값, 선택 탭처럼 사용자 행동에 따라 바뀌는 값을 저장할 때 사용합니다.

Flutter에서: setState와 함께 바뀌며, 값이 바뀌면 해당 화면이 다시 그려집니다.

class _CounterPageState extends State<CounterPage> {
  // 화면이 기억해야 하는 값입니다.
  int count = 0;

  void increase() {
    // 상태 변수 값을 바꾼 뒤 setState로 화면 갱신을 요청합니다.
    setState(() {
      count += 1;
    });
  }
}

부연 설명

타입 선언

  • Dart는 정적 타입 언어지만 var로 타입 추론을 사용할 수 있습니다.
  • 값이 변하지 않는다면 final을 우선 고려합니다.
  • 컴파일 타임에 확정되는 상수는 const로 선언합니다.

부연 설명

실무 기준

  • 공개 API나 모델 필드는 명시 타입을 쓰면 읽기 쉽습니다.
  • 지역 변수는 타입이 명확하면 var를 써도 괜찮습니다.
  • Flutter 위젯에서 변하지 않는 생성자는 const를 붙이는 습관이 좋습니다.

깊게 이해하기

변수 선언은 단순히 값을 담는 문법이 아니라 코드의 의도를 드러내는 장치입니다. 값이 변하지 않는다는 사실을 final이나 const로 표현하면, 나중에 코드를 읽는 사람이 상태 변화 가능성을 훨씬 좁게 추적할 수 있습니다.

Flutter에서는 build 메서드가 자주 다시 실행될 수 있으므로 변하지 않는 값과 위젯을 const로 표현하는 습관이 성능과 가독성 모두에 도움이 됩니다.

상세 예제

아래 예제는 명시 타입, var, final, const가 각각 어떤 의도로 쓰이는지 한 화면 모델로 보여줍니다.

// 강의 진행률을 표현하는 모델 클래스입니다.
class LessonProgress {
  // const 생성자는 같은 값으로 만들어지는 불변 객체에 적합합니다.
  const LessonProgress({
    required this.title,
    required this.completedCount,
    required this.totalCount,
  });

  // title은 글자만 담아야 하므로 String으로 명시합니다.
  final String title;

  // 완료 개수와 전체 개수는 정수이므로 int를 사용합니다.
  final int completedCount;
  final int totalCount;

  // get은 값을 저장하지 않고, 필요할 때 계산해서 돌려주는 읽기 전용 속성입니다.
  double get ratio => completedCount / totalCount;
}

void main() {
  // 컴파일 타임에 확정되는 고정값은 const로 선언합니다.
  const maxVisibleItems = 5;

  // 객체 생성 결과는 한 번 정해지면 바꾸지 않을 것이므로 final을 사용합니다.
  final progress = LessonProgress(
    title: 'Dart 변수와 타입',
    completedCount: 3,
    totalCount: 8,
  );

  // 오른쪽 값을 보고 Dart가 String으로 추론하므로 var를 사용할 수 있습니다.
  var message = '${progress.title}: ${progress.ratio * 100}%';

  // 변수에 담긴 값을 실제로 사용합니다.
  print(message);
  print(maxVisibleItems);
}

실무에서 주의할 점

  • 변할 수 없는 값은 기본적으로 final을 먼저 고려하세요.
  • const는 컴파일 타임 상수와 const 생성자가 있는 객체에 사용할 수 있습니다.
  • 공개 클래스의 필드는 명시 타입을 쓰는 편이 협업에서 더 읽기 쉽습니다.

실습 체크리스트

var와 final 차이 설명const 사용 위치 찾기명시 타입이 필요한 상황 구분