변수 및 타입 선언
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`을 저장할 수도 있다.
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();
}
'TIL(Today I Learned)' 카테고리의 다른 글
[TIL-006] Dart로 콘솔 쇼핑몰 만들기-1 (0) | 2025.03.10 |
---|---|
[TIL-005] Flutter 위젯 생명 주기(Widget Life Cycle) (0) | 2025.03.07 |
[TIL-004] 내가 Dart, Flutter 중에 무엇을 사용하고 있는 것일까? (0) | 2025.03.06 |
[TIL-003] Dart 실전 문법 파헤치기 - 'const 생성자' (1) | 2025.03.06 |
[TIL-002] Flutter 위젯은 쌓기 놀이 (0) | 2025.03.05 |