이 글은 노마드 코더 - Dart 시작하기를 참고하여 작성하였습니다.
작성자 : 조미서
개발 환경 : Mac OS, Android Studio
-> dart는 두 개의 컴파일러를 가지고 있다.(Dart Web, Dart Native)
- Dart web - dart로 쓴 코드를 javascript로 변환해 주는 컴파일러
- Dart Native - dart로 쓴 코드를 여러 CPU의 아키텍처에 맞게 변환 (ex.ARM32,ARM64,x86_64) 결론적으로, IOS,Android,Windows,Linux,Mac으로 모두 컴파일이 가능하다.
- 또한 더 작은 전력의 아키텍처로도 변환 가능하여 => flutter를 사물인터넷을 만드는데 사용함(ex. 자동차)
Dart의 컴파일 방식
Dart는 또한 Null safety를 가진다. (이는 프로그램을 더 안전하게 만들어준다)
flutter는 왜 dart를 택했을까?
dartpad.dev를 이용 & 또는 dart와 flutter를 이미 설치했을 경우 -> 새 프로젝트 만들어서 진행
void main(){
print('hello world');
}
main 함수는 모든 Dart 프로그램의 Entry point이다! (반드시 main 함수를 작성해야 한다!)
-> main 함수에서 작성한 코드가 호출되기 때문
만일
void hello(){
print('hello world');
}
이런 코드의 경우 run 버튼이 사라지고, 실제로도 코드를 강제로 실행시키게 되면, 오류가 발생하게 된다.
Dart_LoadScriptFromKernel: The binary program does not contain 'main'.
위와 같은 오류 문구가 출력된다.
class나 type 같은 건 main 외부에 만들지만 실제로 뭔가를 하는 코드는 반드시 main 내부에 넣어줘야 한다!
또한 dart는 세미콜론(;)을 무조건 붙여야 한다! (javascript나 typescript 같은 다른 프로그래밍 언어에서는 auto formatter가 자동으로 세미콜론을 달아주지만 dart는 그런 기능이 없다 -> 왜냐하면 dart에서는 일부러 세미콜론을 안 쓰는 경우가 있기 때문이다. [dart의 기능 중 하나 (cascade operator)]
변수를 만드는 방법은 크게 두 가지가 있다.
변수 사용 형식
var(키워드) name(변수 이름) = '다트'(저장하고 싶은 데이터);
이처럼 dart는 변수의 타입을 구체화할 필요가 없다. -> dart 컴파일러가 데이터 타입을 구별함
변수는 업데이트가 가능하다. (하지만 이때 변수는 본래 변수의 타입과 일치해야 한다. == 변수를 수정할 때는 같은 타입으로 해줘야 한다!)
변수 사용 형식
String(타입) name(변수 이름) = '다트'(저장하고 싶은 데이터);
두 방식을 언제 사용하는 게 적합할까?
dynamic - 여러 가지 타입을 가질 수 있는 변수에 쓰는 키워드
void main(){
var name;
}
위의 코드처럼 변수에 아무것도 지정해 주지 않았다면 데이터는 dynamic 타입으로 지정된다.(원하는 뭐든 될 수 있다.)
그러하여
void main(){
var name; //var 키워드 자리를 dynamic으로 바꿔써주어도 괜찮다
name = 'dart';
name = 12;
name = true;
}
위와 같이 작성하여도 전혀 문제가 되지 않는다.
왜 dynamic이 필요할까?
dynamic 변수로 뭔가 작업을 할 때 - 타입을 확인!
void main(){
dynamic name;
name. //dart가 변수의 타입을 몰라 메소드가 많지 않음을 확인할 수 있음
}
void main(){
dynamic name;
if(name is String){ //조건문 내부에서는 String이라는 타입으로 지정됨
name. //String 관련 다양한 메소드가 나타남 (이 블럭 안에서는 타입을 인지함)
}
//그 밖에서 name은 dynamic이기 때문에 타입을 또다시 체크해 줘야 한다
}
dynamic은 이상적으로는 쓰는 걸 피하는 게 좋다. (dynamic은 정말로 필요할 때만 사용하자!)
Null safety - 개발자가 null 값을 참조할 수 없도록 함 (null 값을 참조하게 되면 런타임 에러가 발생하게 됨)
//Without null safety:
bool isEmpty(String string) => string.length == 0;
main(){
isEmpty(null);
}
위 코드는 null safety가 없으므로 런타임 에러(NoSuchMethodError)가 난다. -> String을 보내야 하는 곳에 null을 보냈으므로
null을 보내주어 string(변수)의 length라는 속성에 접근하는데 실패하여 에러가 생긴다.
이와 같은 상황이 발생하지 않기 위해 null 값을 삭제하는 건 정답이 아님 (null은 아주 유용하다 -> 부재, 아무것도 있지 않음을 뜻하여 언어에서 이러한 상태 또한 가질 수 있어야 한다)
문제는 이러한 값을 참조할 때 생겨난다 -> 이를 방지하기 위해 null safety가 생김
dart에서는 어떠한 변수가 null이 될 수 있음을 정확히 표시해야 한다.
void main(){
String choms = 'choms';
choms = null; // 오류
}
위의 코드에서 dart는 오직 string이어야 하기 때문에 불가능한 코드이다.
dart가 null도, string도 될 수 있다고 하려면
void main(){
String? choms = 'choms'; // 물음표(?)추가
choms = null;
}
위의 코드와 같이 단순히 물음표(?)만 추가하면 된다! (dart는 choms가 String 일 수도, null 일 수도 있다는 것을 알게 된다)
void main(){
String? choms = 'choms';
choms = null;
choms.length; // 오류!
}
위의 코드를 추가할 때 오류가 발생하는 이유는 이제 choms가 null도 될 수 있고 string도 될 수 있기 때문에 둘 중에 choms가 null이 아님(string임)을 확인해야 한다
이를 확인하기 위하여
void main(){
String? choms = 'choms';
choms = null;
if(choms != null){
choms.isNotEmpty; // 이 부분에서 컴파일러는 choms가 확실히 null이 아님을 알 수 있다
}
}
dart에서 null safety는 어떤 변수, 혹은 데이터가 null이 될 수 있음을 명시하는 것을 말한다.
기본적으로 모든 변수는 non-nullable이다. (null이 될 수 없음)
어떤 변수를 nullable로 만들고 싶다면 물음표(?)를 사용하여 뒤에 나오는 변수가 null이 될 수도 있다고 지정할 수 있음 -> 그러하여 변수를 사용하기 전 null 일지 아니면 다른 타입일지 확인을 해야 한다.
void main(){
String? choms = 'choms';
choms = null;
choms?.NotEmpty; = // 이 부분에서 컴파일러는 choms가 확실히 null이 아님을 알 수 있다
}
전 코드와 비교해서 저렇게 한 줄로 요약도 가능하다.
choms(변수)?(null이 아니라면).isNotEmpty(속성)
final - 한 번 정의된 변수를 수정할 수 없게 만들기 위한 키워드
void main(){
final name = 'dart';
name = 'choms'; // 오류
}
var 키워드에서는 가능하지만 final 키워드에서는 불가능하다. (이는 javascript나 typescript의 const와 똑같은 기능을 함)
+ 더 구체적으로 해주고 싶다면 final 키워드와 변수 이름 사이에 타입(String, int, float...)을 넣어준다. (컴파일러가 알아서 타입을 추론하니까 필수는 아니다)
late - final이나 var 앞에 붙여 초기 데이터 없이 변수를 선언할 수 있게 해주는 수식어
void main(){
late final String name;
// do something, go to api
name = 'dart';
}
위 코드와 같이 데이터가 없는 변수를 선언하고 API 요청으로 데이터를 받은 다음 그 데이터를 나중에 변수에 넣는 경우가 있을 때 사용 가능하다
void main(){
late final String name;
// do something, go to api
print(name); // 오류
}
위의 오류는 name이 late 변수이므로 값이 할당되어야 하는데 할당되지 않은 채로 print 하라고 되어 있기에 값을 넣기 전까지는 접근할 수 없다.
print(name); 전에 name = 'dart';라는 문장이 있어야 오류가 발생하지 않는다!
* flutter로 data fetching을 할 때 유용하다
API에서 얻어온 값을 써야 할 때
한 번만 할당할 수 있는 변수(final)를 먼저 만들고, 데이터를 나중에 넣어준다.(late)
const - compile-time constant 컴파일 할 때 알고 있는 값에 사용하는 키워드
* javascript나 typescript의 const는 dart의 final과 비슷함
dart의 const도 final과 유사하지만 가장 큰 차이점은 const는 compile-time에 알고 있는 값이어야 한다는 것이다.
void main(){
const API = '121121'; // 값이 절대 바뀌지 않고 컴파일 될 때 그 값을 알고 있음
}
만약 API 요청을 한다고 할 때(사용자가 앱을 실행할 때 이뤄지는 것)
void main(){
const API = fetchApi(); // 이때 컴파일러는 API 변수의 값을 모르므로 const가 아닌 final 또는 var을 사용해야 한다
}
위 두 개의 코드를 비교했을 때 결론적으로 const는 컴파일 할 때 알고 있는 값(앱스토어에 앱을 올리기 전에 알고 있는 값)에 사용을 하고, 어떤 값인지 모르고 그 값이 API로부터 온다거나 사용자가 화면에서 입력해야 하는 값이라면 final이나 var가 되어야 한다.
const 사용 예
void main(){
const max_allowed_price = 120; // 미리 지정한 알고 있는 값
}
const 변수들은 컴파일 때 평가된다 (앱에 담긴 코드를 앱스토어에 보내기 전에)
void main(){
int i = 12; // 타입, 변수명, 데이터
var name = 'dart'; // var, 변수명, 데이터
i = 1212121121; // 타입을 지키면 수정 가능 int -> int
name = 'lalalala' // 타입을 지키면 수정 가능 String -> String
}
타입을 사용하는 방식보다는 var을 사용하는 방식을 권장
또한 메소드나 작은 함수 안에서 지역 변수를 선언할 때도 var을 사용하는 방식 권장
(타입을 사용하는 방식은 class의 property를 작성할 때 사용 권장)
void main(){
final name = 'dart'; // final - 값을 재할당하지 못하는 변수를 만든다
name = '21211211' // 오류
}
final은 값을 재할당 하지 못한다
void main(){
dynamic name;
name = '12121';
name = 12;
name = true;
}
dynamic은 어떤 데이터가 들어올지 모른다. (다양한 타입이 될 수 있다) *그래서 dynamic 안의 무언가를 사용하기 전에 확인을 해줘야 한다.
if(name is String){
name. // String 속성 접근 가능
}
void main(){
const api_key = '2121211211221';
api_key = '121212'; // 오류 - 수정불가
}
final과 const의 차이 - final의 값은 런타임 중(사용자가 앱을 실행하면서)에 만들어질 수 있고 const의 값은 컴파일 하기 전에 알고 있어야 한다.
기본적으로 dart의 모든 변수는 nullable이 아니다.
그런데 가끔씩 null을 사용하여 무언가의 부재를 나타내고 그것 자체가 유효한 상태를 나타내고자 할 때 어떤 변수가 null이 될 수도 있다는 것을 알릴 때 바로 변수명 앞 타입명 뒤에 물음표(?)를 붙여 이 변수가 null 일 수도 있다는 것을 알린다.
void main(){
String? name = 'dart';
name = null;
if(name != null){ // null 인지 String 인지 모르기에 null이 아니라고 확인하도록 해야 함
name.isEmpty;
}
}
if(name != null)~은 한 줄로 name?.isEmpty로 줄일 수 있다. (dart는 name이 null이면 isEmpty를 호출하지 않을 것이다)
late는 final, var, String 같은 것들 앞에 써 줄 수 있는 수식어임
dart에게 아직 어떤 데이터가 올지 모른다고 전달해 주는 역할
* 변수가 나중에 정의하기로 되어있다고 하면서 데이터를 넣기 전에는 사용하면 안 된다고 알려줌 (변수를 사용하기 전 데이터를 넣었는지 확인해 주는 것이 중요하다!!)
flutter로 API에서 데이터를 가져오는 일을 할 때 합리적이다. (정의해 주고 싶은 무언가가 있는데 데이터가 아직 없는 상태일 때)
list 생성
void main(){
var numbers = [1,2,3,4]; //List<int>
List<int> numbers = [1,2,3,4]; //List<int>
numbers.add('lalala'); // var에서는 가능 List<int>는 정수만 추가 가능
}
위 두 가지 방법은 똑같이 동작한다 하지만, string을 추가한다고 할 때 var 키워드에서는 가능하지만 List<int>에서는 불가능하다.
list 옵션
+ List를 만들면 끝에 쉼표로 마무리를 해라 - 저절로 여러 줄로 포매팅 된다 (훨씬 보기 편함)
collection if && collection for
void main(){
var giveMeFive = true;
var numbers = [
1,
2,
3,
4,
if(giveMeFive) 5, // giveMeFive가 true일 경우 리스트에 5를 추가해라
];
}
void main(){
var giveMeFive = true;
var numbers = [
1,
2,
3,
4,
];
if (giveMeFive){ // giveMeFive가 true일 경우 리스트에 5를 추가해라
numbers.add(5);
}
}
위의 두 코드는 똑같다. 보다시피 전자의 코드가 훨씬 간결한 것을 알 수 있다.
String interpolation - text에 변수를 추가하는 방법
void main(){
var name = 'dart';
var greeting = 'Hello everyone, my name is $name, nice to meet you!';
print(greeting);
}
규칙 - 작은따옴표(''), 큰따옴표("") 사용 둘 다 상관 없음 달러 기호($) 뒤에는 반드시 변수 사용
위와 같은 문법은 변수가 이미 존재할 때 사용하는 방식
계산을 실행할 때의 문법을 보면
void main(){
var name= = 'dart';
var age = 10;
var greeting = 'Hello everyone, my name is $name, and I\'m ${age+2}'; // 작은따옴표를 사용했으므로 I'm에서 escape 기호(\)를 사용해 줘야 한다
print(greeting);
}
달러 기호($)를 적고 중괄호를 적고 계산할 내용을 중간에 작성하면 된다. dart는 계산을 한 뒤에 값을 text로 치환해 줄 것이다.
void main(){
var oldFriends = ['nico', 'lynn'];
var newFriends = [
'lewis',
'ralph',
'darren',
for(var friend in oldFriends) "😘 $friend",
];
print(newFriends);
}
출력:
[lewis, ralph, darren, 😘 nico, 😘 lynn]
활용을 어떻게 하는지 보기!!
Map은 JavaScript나 TypeScript의 object, python의 dictionary같은 역할
void main(){
var player = { // 자료형 Map<String, Object>
'name' : 'dart',
'xp': 19.99,
'superpower': false,
};
}
Map은 key와 value로 이루어진 구조인데, 위 코드에서 key는 전부 String이고 value는 Object이다.
object는 기본적으로 어떤 자료형이든지 될 수 있다. (TypeScript에서의 any 같은 역할)
Map의 활용 또 다른 예
void main(){
Map<int, bool> player = { // 이와 같이 key와 value의 자료형을 정해 놓을 수도 있다
1: true,
2: false,
3: true,
};
}
또한 key로 integer List, value로는 bool을 가진다고 하는 Map이 있을 때
Map<List<int>, bool> = ~
[1,2,3,5]: true,
~
위와 같이 표현할 수 있다.
Map과 List를 동시에 활용한 예
void main(){
List<Map<String, Object>> players = [
{'name':'dart','xp':199999,99},
{'name':'nico','xp':199879,99},
];
}
set 생성
void main(){
var numbers = {1,2,3,4}; // Set<int> 자료형 var과 바꾸어 써도 됨
}
Set과 List의 차이점 - Set은 모든 요소가 유니크하다 (Set의 요소들은 순서를 가진다!!)
요소가 항상 하나씩만 있어야 하면 Set을 사용
유니크할 필요가 없다면 List를 사용하면 된다
Dart에서 List는 Python의 List와 같고 Dart에서 Set은 Python의 Tuple과 같다.
함수의 예시
void sayHello(String name){ // void는 이 함수가 아무것도 return하지 않음을 의미
print("Hello $name nice to meet you!");
}
void sayHello1(String name){
return "Hello $name nice to meet you!"; // void가 return 한다고 설정해뒀으므로 오류가 남
}
String sayHello2(String name){
return "Hello $name nice to meet you!"; // String이 문자열을 return 하므로 가능
}
void main(){
print(sayHello('dart'));
}
출력:
Hello dart nice to meet you!
또한 함수를 좀 더 간단히 표현할 수 있는 방법이 있는데
String sayHello(String name){
return "Hello $name nice to meet you!"; // String이 문자열을 return하므로 가능
}
String sayHello(String name) => "Hello $name nice to meet you!";
//fat arrow syntax를 사용하면 곧바로 return 하는 것과 같은 의미를 가진다
fat arrow syntax의 좋은 예
num plus(num a, num b) => a + b;
String sayHello(String name, int age, String country){
return "Hello $name, you are $age, and you come from $ country";
} // name, age, country 3개의 매개변수를 return한다.
void main(){
print(sayHello('dart', 19, 'Korea')); // 요소들의 순서를 바로 이해할 수 없다 -> named parameters사용
}
위 코드함수 호출에서 매개변수의 순서를 바로 이해할 수 있도록 named parameter를 사용하게 되면
String sayHello({String name, int age, String country}){
return "Hello $name, you are $age, and you come from $ country";
} // 또한 named parameter를 사용하기 위해 매개변수에 중괄호를 먼저 쳐줘야 하는 것을 주의!!
void main(){
print(sayHello(
age: 19,
country: 'Korea',
name: 'dart'
)); // 순서에 상관없이 argument의 이름들만 적어주면 된다, 중요한 건 argument의 자료형만 맞춰서 사용하면 됨
}
만약 매개변수 값을 지정하지 않는다면??
첫 번째 방법은 named argument에 defuault value를 정하는 것이다.
String sayHello({String name = 'anon', int age = 99, String country = 'wakanda',}) // default value 설정
{
return "Hello $name, you are $age, and you come from $ country";
}
void main(){
print(sayHello( )); // 사용자가 함수에 아무것도 전달하지 않아도 null safety에 걸리지 않는다
}
만약 default value를 지정하고 싶지 않고, 유저에게 실제 data를 받아야 한다면 어떻게 해야 될까? - required modifier를 이용한다
String sayHello({
required String name,
requried int age,
requried String country,})
{
return "Hello $name, you are $age, and you come from $ country";
}
void main(){
sayHello(
age: 14,
country: 'japan',
name = 'choms', // required modifier를 이용하면 무조건 parameter에 data가 들어올 때 실행된다
);
}
String sayHello(String name, String country, int age){
return "Hello $name, you are $age, and you come from $country";
} // positional parameter는 사용할 때, 각각의 위치를 기억해야 한다는 점을 주의
void main(){
sayHello("dart", "Korea", 19);
}
String sayHello({
String name,
String country,
int age,
}){
return "Hello $name, you are $age, and you come from $country";
} // parameter 리스트 전체를 중괄호로 감싼다
void main(){
sayHello(
country = "Korea",
name = "dart",
age = 19, // parameter 순서가 상관없음
);
}
만약 3개의 parameter의 값을 지정하지 않고 호출되는 경우 -> required modifier를 추가하여 sayHello 함수가 반드시 name, country, age와 함께 호출되어야 한다는 것을 알게 한다 / default value를 설정한다
String sayHello({
required String name, // required modifier
required String country,
required int age,
}){
return "Hello $name, you are $age, and you come from $country";
}
void main(){
sayHello(
country = "Korea",
name = "dart",
age = 19,
);
} // null safety로부터 미리 막아주는 역할을 함
String sayHello({
String name = 'choms', // default value 설정
String country = 'Brazil',
int age = 55,
}){
return "Hello $name, you are $age, and you come from $country";
}
void main(){
sayHello();
}
* required modifier은 사용자가 로그인할 때 이메일, 비밀번호 값을 default value로 줄 수 없으므로 이와 같은 경우에 사용된다.
optional positional parameter 함수 예
String sayHello(
String name,
int age,
[String? country = 'cuba'], // [] 대괄호로 감싸주고 country 값이 지정되어 있을 수도 없을 수도(?) 마지막으로 default value 부여
) => 'Hello $name, you are $age years old from $country';
void main(){
var results = sayHello('nico', 12);
print(results);
}
위 코드를 실행하면
Hello nico, you are 12 years old from cuba
그러하여 우리가 선택적으로 원하는 argument를 지정해 주거나 지정해 주지 않아도 함수를 호출해 줄 수 있게 된다.
QQ operator 사용 예
String capitalizeName(String? name) => name.toUpperCase();
//name 값이 null일 수도 아닐 수도 있다 이렇게 하면 toUpperCase가 null에서 호출이 안되므로 에러가 생긴다
// 위의 경우를 수정하는 첫 번째 방법
String capitalizeName(String? name){
if (name != null){
return name.toUpperCase();
}
return 'ANON'
}
// 위의 경우를 수정하는 두 번째 방법
String capitalizeName(String? name) => name != null ? name.toUpperCase() : 'ANON';
// 두 번째 방법을 좀 더 짧게 줄일 때 ??(QQ operator를 사용할 수 있다)
String capitalizeName(String? name) =>
name?.toUpperCase() ?? 'ANON';
void main(){
capitalizeName('dart');
capitalizeName(null);
}
위 코드에서 ?? (QQ operator)는 좌향과 우향을 비교하여 만약 좌향이 null이면 우항을 return한다 좌항이 null이 아니면 그대로 좌항을 return함
QQ equals 예시
void main(){
String? name; // name은 null일 수도 아닐 수도 있다
name ??= 'dart'; // name이 null이라면 값을 할당하라
name ??= 'another'; // name이 null이라면 값을 할당하라
print(name);
}
위 코드를 실행하면 dart
가 출력되는데, Warning이 동반된다. 그 이유는 이 줄에 있는 코드 뒤부터는 name이 null이 될 일이 없으므로 그러하여 name ??= 'another'이 부분은 절대로 실행될 일이 없다 그러나 중간에 dart name = null;
을 넣으면 맨 마지막 값인 another
이 출력된다.
typedef - alias를 만드는 방법 (자료형이 헷갈릴 때 도움을 주는)
alias는 별칭을 등록하는 명령어라고 생각하면 된다.
typedef ListOfInts = List<int>;
// List<int> 부분을 모두 ListOfInts로 바꿀어 쓸 수 있다
ListOfInts reverseListOfNumbers(ListOfInts list_a){ // ListOfInts는 자신이 별칭을 지정한 List<int>자료형임
var reversed = list_a.reversed; // list를 reverse하게 되면 list와 조금 다른 iterable이 되서
return reversed.toList(); // 다시 list로 변환해줘야 한다
}
void main(){
print(reverseListOfNumbers([1, 2, 3]));
}
출력:
[3, 2, 1<]
typedef는 원하는 만큼 생성이 가능하다.
이제 Map의 typedef를 만들어 보자
typedef Map_ss = Map<String, String>;
// 만약 구조화된 data의 형태를 지정하고 싶으면 class를 만들어야 함
String sayHi(Map_ss userInfo){
return "Hi ${userInfo['name']}";
}
void main(){
print(sayHi({'name' : 'dart'})); // Hi dart 출력
print(sayHi({'djklsj' : 'dart'})); // Hi null 출력
}
이와 같이 typedef는 자료형에 별칭을 붙여 코드의 가독성과 효율성을 높인다.