[TIL-001] Dart 기본 문법은 JS와 비슷했다

변수 및 타입 선언

var 키워드

`var` 키워드를 사용하면 타입을 직접 명시하지 않아도 Dart에서 자동으로 타입을 추론한다.
한 번 타입이 지정되면 같은 타입의 값만 재할당 가능하다.
하지만 초기화 없이 선언하면 `dynamic` 타입이 되어 어떤 값이든 재할당 가능하다.

void main() {
  // int 타입으로 추론
  var a = 10;
  a = '123'; // error
  
  // String 타입으로 추론
  var b = '123';
  b = true; // error
  
  // bool 타입으로 추론
  var c = true;
  c = 10; // error
  
  // dynamic 타입으로 추론
  var d;
  d = 10;
  d = '123';
  d = true;
}

 

타입 종류

타입 설명
`int` 정수형 타입 (예: 10, -5)
`double` 실수형 타입 (예: 3.14, -0.99)
`String` 문자열 타입 (예: "hello", 'Dart')
`bool` 불리언 타입 (true 또는 false)
`List` 리스트 (배열과 비슷)
`Map<K, V>` 키-값 쌍을 가지는 객체
`Set` 중복 없는 컬렉션
`dynamic` 모든 타입을 저장할 수 있는 타입
void main() {
  // int 타입으로 지정
  int a = 10;
  
  // String 타입으로 지정
  String b = '123';
  
  // bool 타입으로 지정
  bool c = true;
   
  // dynamic 타입으로 지정
  dynamic d = 10;
  d = '123';
  d = true;
  
  // double 타입으로 지정
  double e = 10.2;
  
  // List 타입으로 지정
  List<int> f = [1,2,3];
  List<String> g = ['1', '2', '3'];
  
  // Map 타입으로 지정
  Map<String, Object> h = {
    'name': 'mark',
    'age': 20,
    'married': false
  };
  
   // Set 타입으로 지정
  Set<int> uniqueNumbers = {1, 2, 3, 1, 2}; // {1, 2, 3}으로 저장됨
}

 

📝 정리

  • `var`는 자동 타입 추론, 하지만 초기화 없이 선언하면 `dynamic`이 됨.
  • `int`, `double`, `String`, `bool`, `List`, `Map`, `Set` 등의 타입을 명시적으로 선언할 수 있음.
  • `dynamic`은 모든 타입을 저장할 수 있지만, 타입 안정성이 떨어짐.

final 키워드

한 번 값이 할당되면 변경할 수 없는 변수를 선언할 때 사용한다.

void main() {
  var name = 'mark';
  name = 'kelly';
  
  print(name); // 'kelly'
  
  final name2 = 'mark';
  name2 = 'kelly'; // error
  
  final String name3 = 'mark';
  name3 = 'kelly'; // error
}

late 키워드

변수를 선언만 먼저 하고, 나중에 값을 할당할 수 있도록 해주는 키워드이다.

즉, 초기화 시점을 조절할 수 있어서 매우 유용하다

값을 나중에 할당하더라도 `null`은 허용되지 않는다.

주로 `final`과 함께 사용하면 초기화 시점을 늦출 수 있어 유용하다.

void main() {
  // late 키워드를 사용하여 변수를 선언만 해둠 (초기값 없음)
  late String name;

  // 나중에 값을 할당할 수 있음
  name = 'mark';

  print(name); // 'mark'
}

언제 사용할까?

  • 클래스에서 멤버변수를 나중에 초기화할 때
class Person {
  late String name;

  Person(String name) {
   this.name = name;
  }
}

void main() {
  var p1 = Person('lee');
}
  • 비동기 작업에서 값을 나중에 할당할 때
late String data;

Future<void> fetchData() async {
  await Future.delayed(Duration(seconds: 2));
  data = '데이터 불러옴';
  print(data);
}

? 연산자

`?` 연산자는 변수가 `null`을 가질 수 있도록 허용하는 연산자이다.

즉, 변수를 선언한 후 나중에 값을 할당할 수도 있고, `null`을 저장할 수도 있다.

void main() {
  // ? 연산자를 이용해 나중에 null을 할당해도 됨
  int? age;
  age = null;
  print(age);
  
  // late 키워드를 이용해 나중에 null을 할당하면 error
  late int score;
  score = null; // error
}

String Interpolation

변수를 문자열 안에서 직접 사용할 수 있도록 해주는 기능이다.

사용법 설명
`$변수명` 변수의 값을 문자열에 포함
`${표현식}` 표현식(연산, 함수 호출 등)을 문자열에 포함
void main() {
  var name = 'mark';
  var age = 10;

  // 변수 직접 사용
  var message = 'Hello, My name is $name, I\'m ${age + 2} years old.';
  print(message);

  // 리스트 값 사용
  var favoriteThings = ['🍕', '🍇'];
  var message2 = 'My favorite food is ${favoriteThings[0]} and my favorite fruit is ${favoriteThings[1]}.';
  print(message2);
}

Collection if

`collection if`는 리스트(List) 내부에서 조건문을 사용하여 값을 추가하는 기능이다.
일반적인 `if` 문을 사용하지 않고 리스트 선언 시점에서 조건을 적용할 수 있어 코드가 더 간결해진다.

void main() {
  // List에 값을 추가하는 기본적인 방법
  bool active = true;
  List<int> nums = [1,2,3];
  
  if(active) {
    nums.add(4);
  }
  
  print(nums); // [1,2,3,4]
  
  // collection if를 사용하여 추가하는 방법
  bool active2 = true;
  List<int> nums2 = [
    1,
    2,
    3,
    if(active2) 4, // collection if
  ];
  
  print(nums2); // [1,2,3,4]
}

Collection for

`collection for`는 리스트(List) 내부에서 반복문을 사용하여 값을 추가하는 기능이다.

기존 리스트의 데이터를 변형하여 새로운 리스트를 만들 때 사용한다.

void main() {
  var arr = ['a', 'b', 'c'];
  
  // for문 사용
  var arr2 = [
    for(var i = 0; i < 3; i++) '${arr[i]}' 
  ];
  
  print('arr2 - $arr2'); // ['a', 'b', 'c']
  
  // for-in문 사용
  var arr3 = [
    for(var item in arr) '$item'
  ];
  
  print('arr3 - $arr3'); // ['a', 'b', 'c']
}

Map

`Map`은 key: value 형태로 이루어진 객체이다

`Object` 타입을 사용하면 숫자, 문자열, 불리언 등 다양한 값을 저장할 수 있다.

void main() {
  var obj = {
    'name': 'lee',
    'xp': 19.99,
    'superpower': false
  };
  
  // Map<key 타입, value 타입> 변수 = 초기화
  Map<String, Object> obj2 = {
    'name': 'lee2',
    'xp': 30.33,
    'superpower': true
  };
}

Set

`Set`은 배열처럼 반복 가능한 객체이지만, 중복된 값을 허용하지 않는 특징이 있다.

void main() {
  var num1 = {1, 2, 3};
  Set<int> num2 = {1, 2, 3};
}

함수

/*
반환타입 함수이름(매개변수 및 타입, ...) {
  return 반환값;
}
*/
int add(int a, int b) {
  int sum = a + b;
  return sum;
}

void main() { // void는 반환값이 없을 때 사용하는 타입
  int result = add(3, 5);
  print('결과: $result');  // 결과: 8
}

 

Positional Parameters

`Positional Parameters`는 함수를 호출할 때, 전달하는 값의 순서가 중요한 매개변수이다.
즉, 잘못된 순서로 값을 전달하면 의도한 결과와 다르게 동작할 수 있다.

void sayHello(String name, String gender) {
  print('안녕하세요 제 이름은 $name이고, $gender입니다.');
}

void main() {
  // 올바른 순서로 호출
  sayHello('lee', '남자'); // '안녕하세요 제 이름은 lee이고, 남자입니다.'
  
  // 잘못된 순서로 호출
  sayHello('남자', 'lee'); // '안녕하세요 제 이름은 남자이고, lee입니다.'
}

Positional Parameters의 단점

  • 매개변수가 많아질수록 가독성이 떨어진다.
  • 순서를 기억해야 하기 때문에 실수할 가능성이 높다.
  • 잘못된 순서로 전달해도 오류가 발생하지 않아서, 디버깅이 어려울 수 있다.

 

Named Parameters

`Named Parameters`는 함수를 호출할 때 매개변수의 이름을 직접 지정하여 전달하는 방식이다.

`{}`(중괄호)로 감싸면 된다.
매개변수의 순서에 영향을 받지 않으며, 코드의 가독성을 높일 수 있다.

 

`Named Parameters`를 사용하면 매개변수가 null을 허용하거나 확실하게 null이 아니여야 한다는 점이다.

  • `required` 키워드를 사용하여 반드시 전달하게 함
  • 기본값을 설정하여 생략 가능하게 함
  • `?` 연산자로 설정하여 null을 허용하게 함
/*
  name은 required 키워드,
  country는 기본 값 할당하여 non-null(null이 아니다)이라는 것을 나타냄
  gender는 옵셔널 연산자를 사용하여 nullable(null일수도 있다)이라는 것을 나타냄
*/

void sayHello({required String name, String country = '한국', String? gender}) {
  print('안녕하세요 제 이름은 $name이고, $country에 살고 있는 $gender입니다.');
}

void main() {
  // 순서 상관없음
  sayHello(name: 'lee', gender: '남자');
  sayHello(gender: '남자', name: 'lee');
}

 

클래스

멤버변수

class Person {
  // 멤버 변수는 선언과 동시에 초기화 할 수 있다.
  String name = 'lee';
  String gender = 'man';
  
  @override
  String toString() {
    return 'Person{name: $name, gender: $gender}';
  }
}

void main() {
  var p1 = Person();
  print(p1); // 'Person{name: lee, gender: man}'
}

 

생성자(Constructor)

클래스를 호출하여 객체를 만들 때 자동으로 실행되는 특별한 함수생성자라고 한다.
객체를 만들면서 멤버 변수의 값을 초기화(할당) 할 수 있다.

 

클래스 내부에서 생성자를 바로 호출할 수 있는데 호출할 때 this를 사용하면 멤버변수 선언 시 late 키워드를 생략할 수 있다. 그리고 생성자에서 멤버변수의 값을 할당(초기화) 하지 않아도 된다.

class Person {
  // 생성자로 값을 할당할 예정이므로 late 키워드를 사용
  late String name;
  late String gender;

  // 생성자는 class의 이름과 동일해야한다.
  // 위 3, 4줄에 name과 gender의 타입을 명시했으니
  // 생성자에서는 생략이 가능하다.
  Person(String name, String gender) {
    this.name = name;
    this.gender = gender;
  }

  @override
  String toString() {
    return 'Person{name: $name, gender: $gender}';
  }
}

void main() {
  var p1 = Person('lee', 'man');
  print(p1); // 'Person{name: lee, gender: man}'
}

Positional Constructor Parameters

class Person {
  // this를 사용하면 late키워드는 생략 가능하다.
  String name;
  String gender;

  // 생성자는 class의 이름과 동일해야한다.
  // this를 사용하면 생성자에서 멤버변수의 값을 할당(초기화) 하지 않아도 된다.
  // 위 3, 4줄에 name과 gender의 타입을 명시했으니
  // 생성자에서는 생략이 가능하다.
  Person(this.name, this.gender);

  @override
  String toString() {
    return 'Person{name: $name, gender: $gender}';
  }
}

void main() {
  var p1 = Person('lee', 'man');
  print(p1); // 'Person{name: lee, gender: man}'
}

Named Constructor Parameters

class Person {
  // this를 사용하면 late키워드는 생략 가능하다.
  String name;
  String gender;

  // 생성자는 class의 이름과 동일해야한다.
  // this를 사용하면 생성자에서 멤버변수의 값을 할당(초기화) 하지 않아도 된다.
  // 위 3, 4줄에 name과 gender의 타입을 명시했으니
  // 생성자에서는 생략이 가능하다.
  Person({
    required this.name, 
    required this.gender,
  });

  @override
  String toString() {
    return 'Person{name: $name, gender: $gender}';
  }
}

void main() {
  // named parameters로 할당
  var p1 = Person(
  	name: 'lee', 
    gender: 'man',
  );
  print(p1); // 'Person{name: lee, gender: man}'
}

Named Constructor

생성자(Constructor) 는 클래스의 인스턴스를 생성할 때 호출되는 특수한 메서드이다.
기본적으로 생성자는 클래스의 이름과 동일해야 하지만, Named Constructor(이름이 있는 생성자)를 사용하면 여러 개의 생성자를 정의할 수 있다.

 

  `:` (콜론) 을 기준으로 좌측은 `Named Constructor`를 호출할 때 어떤 멤버 변수를 어떻게 입력 받을지(positional 또는 named parameters)  정의하는 부분이고, 우측은 멤버 변수를 초기화하는 부분이다.

class Person {
  String name;
  String gender;
  String country;

  // 기본 생성자 (클래스 이름과 동일해야 함)
  Person({
    required this.name,
    required this.gender,
    required this.country,
  });

  // Named Constructor - 한국인을 위한 생성자
  Person.createKorean({
    required String name,
    required String gender,
  })  : this.name = name,
        this.gender = gender,
        this.country = 'Korea'; // country 기본값 설정

  // Named Constructor - 미국인을 위한 생성자
  Person.createAmerican(String name, String gender)
      : this.name = name,
        this.gender = gender,
        this.country = 'America'; // country 기본값 설정

  @override
  String toString() {
    return 'Person{name: $name, gender: $gender, country: $country}';
  }
}

void main() {
  // Named Parameter 방식으로 객체 생성
  var korean = Person.createKorean(
    name: 'Lee',
    gender: 'Man',
  );

  // Positional Parameter 방식으로 객체 생성
  var american = Person.createAmerican('Mark', 'Man');

  print(korean); // Person{name: Lee, gender: Man, country: Korea}
  print(american); // Person{name: Mark, gender: Man, country: America}
}

 

코드 설명

Named Constructor - `createKorean`

Person.createKorean({
  required String name,
  required String gender,
})  : this.name = name,
      this.gender = gender,
      this.country = 'Korea';
  • `name`과 `gender`는 Named Parameter(이름이 있는 매개변수)로 입력받고,
  • `country`는 기본값으로 'Korea' 를 설정한다.

 

Named Constructor - `createAmerican `

Person.createAmerican(String name, String gender)
    : this.name = name,
      this.gender = gender,
      this.country = 'America';
  • `name`과 `gender`는 Positional Parameter(위치 기반 매개변수)로 입력받고,
  • `country`는 기본값으로 'America' 를 설정한다.

 

메서드

클래스 내부에서 this 를 사용하지 않아도 멤버 변수에 접근할 수 있다.
그러나 지역 변수와 멤버 변수의 이름이 동일할 경우, 멤버 변수를 명확하게 구분하기 위해 `this.멤버변수` 형식으로 접근해야 한다.

class Person {
  String name = 'lee';
  String gender = 'man';
  
  void sayHello() {
    var gender = 'woman'; // 메서드 내부의 지역 변수
    
    // this를 사용하지 않아도 멤버 변수에 접근이 가능
    print('Hello, my name is $name.');
    
    // 지역변수와 멤버변수가 같을 경우, this를 사용해서 멤버 변수에 접근
    print('I\'m not a $gender, I\'m a ${this.gender}.');
  }
}

void main() {
  var p1 = Person();
  p1.sayHello();
}

 

추상 클래스(abstract class)

추상 클래스 (Abstract Class) 는 클래스에서 사용할 메서드의 시그니처(반환 타입, 매개변수 개수와 타입) 만 정의하는 클래스이다.
즉, 구체적인 구현 없이 메서드만 선언 하며, 이를 상속받은 클래스는 반드시 해당 메서드를 구현해야 한다.

// 추상 클래스 정의
abstract class Human {
  void walk(); // 추상 메서드 (구현 없이 선언만 함)
}

class Korean extends Human { 
  String name = 'lee';
  String gender = 'man';
  
  // Human을 상속 받기 때문에 walk 메서드가 꼭 있어야 한다.
  // 대신 로직은 자유. 다른 클래스와 달라도 된다.
  void walk() {
    print('한국 사람이 걷는다.');
  }
}

class American extends Human { 
  String name = 'mark';
  String gender = 'man';
  
  // Human을 상속 받기 때문에 walk 메서드가 꼭 있어야 한다.
  // 대신 로직은 자유. 다른 클래스와 달라도 된다.
  void walk() {
    print('미국 사람이 걷는다.');
  }
}

void main() {
  var k = Korean();
  var a = American();
  
  k.walk();
  a.walk();
}

 

상속(확장)

상속(Inheritance)기존 클래스를 확장하여 새로운 클래스를 만드는 개념 이다.
부모 클래스의 속성과 메서드를 자식 클래스에서 그대로 사용하거나, 재정의(override)할 수 있다.

 

✅ 핵심 개념

  • 부모 클래스(Super Class) - 공통된 속성과 메서드를 정의
  • 자식 클래스(Sub Class) - 부모 클래스를 상속받고, 추가적인 기능을 구현
  • 부모 클래스의 생성자 호출 - `: super(부모클래스의 생성자 매개변수)`
  • 멤버 변수 초기화 - `this.변수명 = 값` 또는 `this.변수명` 사용
// 팀을 나타내는 enum
enum Team {
  red,
  blue,
  green,
}

// 부모 클래스 (Super Class)
class Human {
  final String name;
  
  Human(this.name);
  
  void sayHello() {
    print('Hi my name is $name');
  }
}

// Player1 클래스 - 명시적으로 멤버 변수 초기화
class Player1 extends Human {
  Team team;
  
  // 멤버 변수에서 this를 사용하지 않으면 :(콜론) 우측에서 명시적으로 초기화해야 한다.
  // 부모 클래스의 생성자 호출은 항상 초기화 이후 마지막에 호출해야 한다.
  Player1(String name, Team team) : this.team = team, super(name);
  
  // 생성자에서는 멤버변수의 타입을 생략 가능하다.
  // Player1(name, team) : this.team = team, super(name);
}

// Player2 클래스 - this를 사용한 자동 멤버 변수 초기화
class Player2 extends Human {
  Team team;
  
  // this를 사용하면 자동으로 멤버 변수가 초기화됨
  // 그래서 부모 클래스의 생성자만 호출해도 된다.
  // name은 부모 클래스의 멤버 변수이므로 자식 클래스에서 this로 
  // 접근할 수 없음
  Player2(String name, this.team) : super(name);
}

class Player3 extends Human {
  Team team;
  
  // 네임드 파라미터도 가능하다.
  Player3({required String name, required this.team}) : super(name);
}

void main() {
  var p1 = Player1('lee', Team.red);
  var p2 = Player2('kim', Team.blue);
  var p3 = Player3(name: 'choi', team: Team.green);
  
  p1.sayHello();
  p2.sayHello();
  p3.sayHello();
}

 

오버라이딩

부모 클래스에서 상속받은 메서드를 자식 클래스에서 재정의(대체)하는 것이다.
Dart에서는 @override 키워드를 사용하여 부모 클래스의 메서드를 재정의할 수 있다.

 

✅ 핵심 개념

  • 부모 클래스에서 정의한 메서드를 자식 클래스에서 변경할 수 있다.
  • @override 키워드를 사용하여 명시적으로 메서드를 재정의한다.
  • 부모 클래스의 메서드를 유지하면서 확장하려면 `super.메서드명()` 을 호출한다.
// 팀을 나타내는 enum
enum Team {
  red,
  blue,
  green,
}

// 부모 클래스 (Super Class)
class Human {
  final String name;
  
  Human(this.name);
  
  void sayHello() {
    print('Hi my name is $name');
  }
}

// 자식 클래스(Sub Class) - Player1
class Player1 extends Human {
  Team team;
  
  // 멤버 변수를 직접 초기화한 후 부모 클래스의 생성자를 호출
  Player1(String name, Team team) : this.team = team, super(name);
  
  // @override 키워드를 이용해 메서드를 대체할 수 있다.
  @override
  void sayHello(){
    // 부모 클래스의 메서드를 호출
    super.sayHello();
    
    print('and I\'m a man');
  }
}
void main() {
  var p1 = Player1('lee', Team.red);
  
  p1.sayHello();
}

 

Mixins

클래스의 프로퍼티나 메서드를 상속 없이 공유할 때 사용 한다.
즉, "클래스의 기능을 다른 클래스에 추가하는 방법" 이다.


✅ 핵심 개념

  • `with` 키워드를 사용하여 mixin을 추가할 수 있다.
  • 여러 개의 mixin을 동시에 사용할 수 있다.
  • Mixin은 생성자가 없기 때문에 직접 인스턴스를 생성할 수 없다.
  • 상속과 달리 필요한 기능만 선택적으로 추가할 수 있다.

 

상속은 자식 클래스가 부모 클래스의 모든 상위 클래스의 속성과 메서드를 상속 받기 때문에 모든 것을 사용할 수 있지만, mixins은 단순히 공유이기 때문에 공유한 클래스의 속성과 메서드만 사용할 수 있다.

mixin Gender {
  String gender = 'man';
}

mixin Say {
  String hello() {
    return 'hello';
  }
}
class Person with Gender, Say {
  // height 속성과 hello 메서드를 공유했기 때문에
  // Person 클래스 내부에서도 사용 가능하다.
  void introduce() {
  print(hello() + ', I\'m Lee. I\'m a $gender');
  }
}
void main() {
  var p1 = Person();
  
  print(p1.hello()); // Person의 인스턴스에서도 hello 메서드를 사용할 수 있다.
  print(p1.gender);  // gender는 p1의 프로퍼티가 됐다.
  p1.introduce();
}