[Flutter] 다트 언어 마스터하기 – 다트 입문 하기 ❷

이 글은 [Must Have] 코드팩토리의 플러터 프로그래밍 2판에서 발췌했습니다.
골든래빗 출판사
코드팩토리(최지호) 지음

플러터(Flutter)는 다트(Dart) 언어를 사용합니다. 다트를 알아야 플러터로 앱 개발이 가능하므로 먼저 다트 문법을 알아봅시다.

대부분의 플러터 입문자는 별도의 책으로 다트를 공부하지 않고 플러터 서적에서 1개 장 분량으로 얕게 배웁니다. 플러터로 앱을 원활히 개발하려면 다트를 탄탄하게 아는 것이 중요합니다. 그래서 이 책은 또 다른 자료를 찾아보지 않아도 될 정도로 깊이 있게 다트를 다룹니다.

1장에서 다트 입문하기, 2장에서 객체지향 프로그래밍, 3장에서 비동기 프로그래밍, 4장은 다트 3.0 신규 문법을 학습합니다.

 

다트 입문하기 ❷

다트로 코딩하는 데 필요한 기초 지식인 변수와 상수, 컬렉션, 연산자, 제어문, 함수를 알아봅니다.

 

4. 컬렉션

컬렉션은 여러 값을 하나의 변수에 저장할 수 있는 타입입니다. 여러 값을 순서대로 저장하거나(List), 특정 키값을 기반으로 빠르게 값을 검색해야 하거나(Map), 중복된 데이터를 제거할 때 사용됩니다(Set). 컬렉션 타입은 서로의 타입으로 자유롭게 형변환이 가능하다는 매우 큰 장점이 있습니다. 이 장점을 이용해서 각 타입의 특징을 프로그래밍에 적극적으로 활용할 수 있습니다.

 

4.1 List 타입

리스트(List) 타입은 여러 값을 순서대로 나열한 변수에 저장할 때 사용됩니다. 리스트의 구성 단위를 원소라고 합니다. 리스트명[인덱스] 형식으로 특정 원소에 접근할 수 있습니다. 인덱스는 원소의 순번이라고 생각하면 됩니다. 제일 첫 원소는 0으로 지정합니다. 따라서 마지막 원소는 ‘리스트 길이 – 1’로 지정해야 합니다.

 

void main() {
  // 리스트에 넣을 타입을 <> 사이에 명시할 수 있습니다.
  List<String> blackPinkList = ['리사', '지수', '제니', '로제'];

  print(blackPinkList);
  print(blackPinkList[0]);  // 첫 원소 지정
  print(blackPinkList[3]);  // 마지막 원소 지정

  print(blackPinkList.length);  // ❶ 길이 반환

  blackPinkList[3] = '코드팩토리';  // 3번 인덱스값 변경
  print(blackPinkList);
}

[리사, 지수, 제니, 로제]
리사
로제
4
[리사, 지수, 제니, 코드팩토리]

 

리스트 길이는 ❶ length를 가져와 확인할 수 있습니다.

List 타입에는 다트 언어에서 기본으로 제공하는 함수가 많습니다. 그중 가장 많이 사용하는 add( ), where( ), map( ), reduce( ) 함수를 알아보겠습니다.

 

add( ) 함수

add( ) 함수는 List에 값을 추가할 때 사용되며 추가하고 싶은 값을 매개변수에 입력하면 됩니다.

 

void main() {
  List<String> blackPinkList = ['리사', '지수', '제니', '로제'];

  blackPinkList.add('코드팩토리');  // 리스트의 끝에 추가

  print(blackPinkList);
}

 

▼ 실행 결과

 

where( ) 함수

where( ) 함수는 List에 있는 값들을 순서대로 순회looping하면서 특정 조건에 맞는 값만 필터링하는 데 사용합니다. 매개변수에 함수를 입력해야 하며, 입력된 함수는 기존 값을 하나씩 매개변수로 입력받습니다. 각 값별로 true를 반환하면 값을 유지하고, false를 반환하면 값을 버립니다. 순회가 끝나면 유지된 값들을 기반으로 이터러블이 반환됩니다.

 

💡 [NOTE] 이터러블(Iterable)

이터러블은 추상 클래스(2.7 ‘추상’)로 List나 다음으로 배울 Set 등의 컬렉션 타입들이 상속(2.3 ‘상속’)받는 클래스입니다. 쉽게 풀어 설명하면 List와 Set 같은 컬렉션이 공통으로 사용하는 기능을 정의해둔 클래스입니다. where( )나 map( ) 등 순서가 있는 값을 반환할 때 사용합니다. 아직 클래스를 배우지 않아서 이 정의를 이해하기 어려울 수 있습니다. 클래스는 2장에서 다룹니다.

 

void main() {
  List<String> blackPinkList = ['리사', '지수', '제니', '로제'];

  final newList = blackPinkList.where(
        (name) => name == '리사' || name == '지수',  // ‘리사’ 또는 ‘지수’만 유지
  );

  print(newList);
  print(newList.toList()); // Iterable을 List로 다시 변환할 때  .toList() 사용
}

(리사, 지수)
[리사, 지수]

 

map( ) 함수

map( ) 함수는 List에 있는 값들을 순서대로 순회하면서 값을 변경할 수 있습니다. 매개변수에 함수를 입력해야 하며 입력된 함수는 기존 값을 하나씩 매개변수로 입력받습니다. 반환하는 값이 현잿값을 대체하며 순회가 끝나면 Iterable이 반환됩니다.

 

void main() {
  List<String> blackPinkList = ['리사', '지수', '제니', '로제'];

  final newBlackPink = blackPinkList.map(
        (name) => '블랙핑크 $name',  // 리스트의 모든 값 앞에 ‘블랙핑크’를 추가
  );
  print(newBlackPink);

  // Iterable을 List로 다시 변환하고 싶을 때 .toList() 사용
  print(newBlackPink.toList());
}

(블랙핑크 리사, 블랙핑크 지수, 블랙핑크 제니, 블랙핑크 로제)
[블랙핑크 리사, 블랙핑크 지수, 블랙핑크 제니, 블랙핑크 로제]

 

reduce( ) 함수

reduce( ) 함수 역시 List에 있는 값들을 순회하면서 매개변수에 입력된 함수를 실행합니다. 다만 reduce( ) 함수는 순회할 때마다 값을 쌓아가는 특징이 있습니다. 지금까지 배운 함수들은 모두 Iterable을 반환했지만 reduce( ) 함수는 List 멤버의 타입과 같은 타입을 반환합니다.

 

void main() {
  List<String> blackPinkList = ['리사', '지수', '제니', '로제'];

  final allMembers = blackPinkList.reduce((value, element) => value + ', ' + element);  // ➊ 리스트를 순회하며 값들을 더합니다.

  print(allMembers);
}

리사, 지수, 제니, 로제

 

❶ 기존 함수들과 다르게 reduce( ) 함수는 매개변수로 함수(1.7.2 ‘익명 함수와 람다 함수’ 참조)를 입력받고 해당 함수는 매개변수 2개를 입력받습니다. 순회가 처음 시작될 때 첫 번째 매개변수(value)는 리스트의 첫 번째 값 즉, ‘리사’를 받게 되고 두 번째 매개변수(element)는 ‘지수’를 받게 됩니다. 첫 번째 순회 이후로는 첫 번째 매개변수에 기존 순회에서 반환한 값이 첫 번째 매개변수에 입력되고 리스트에서의 다음 값이(제니) 두 번째 매개변수에 입력됩니다. 그래서 reduce( ) 함수는 리스트 내부의 값들을 점차 더해가는 기능으로 사용됩니다.

 

fold( ) 함수

fold( ) 함수는 reduce( ) 함수와 실행되는 논리는 똑같습니다. reduce( ) 함수는 함수가 실행되는 리스트 요소들의 타입이 같아야 하지만, fold( ) 함수는 어떠한 타입이든 반환할 수 있습니다.

 

void main() {
  List<String> blackPinkList = ['리사', '지수', '제니', '로제'];

  // ➊ reduce() 함수와 마찬가지로 각 요소를 순회하며 실행됩니다.
  final allMembers =
  blackPinkList.fold<int>(0, (value, element) => value + element.length);

  print(allMembers);
}

 

❶ 미리 배운 fold( ) 함수는 reduce( ) 함수의 특수한 형태라고 생각하면 됩니다. reduce( ) 함수는 리스트를 구성하는 값들의 타입과 반환되는 리스트를 구성할 값들의 타입이 완전히 같아야 합니다. 하지만 fold( ) 함수는 그런 제약이 없습니다. 그래서 첫 번째 매개변수에 시작할 값을 지정하고, 두 번째 매개변수에는 reduce( ) 함수와 똑같이 작동하는 함수를 입력합니다. 다만 첫 번째 순회 때 리스트의 첫 번째 값이 아닌 fold( ) 함수의 첫 번째 매개변수에 입력된 값이 초깃값으로 사용됩니다. 두 번째 매개변수인 (value, element) => value + element.length는 람다식으로 최초 순회 때 value에는 초깃값(여기서는 0)이 입력되고 이후에는 기존 순회의 반환값이 입력됩니다. element는 reduce( ) 함수와 마찬가지로 리스트의 다음 값이 입력됩니다. 람다 함수는 1.7.2 ‘익명 함수와 람다 함수’에서 추가로 다룹니다.

 

4.2 Map 타입

맵(Map) 타입은 키(Key)와 값(Value)의 짝을 저장합니다. 순서대로 값을 저장하는 데 중점을 두는 리스트와 달리 맵은 키를 이용해서 원하는 값을 빠르게 찾는 데 중점을 둡니다. Map<키 타입, 값 타입> 맵 이름 형식으로 생성합니다.

 

void main() {
  Map<String, String> dictionary = {
    'Harry Potter': '해리 포터',        // 키 : 값
    'Ron Weasley': '론 위즐리',
    'Hermione Granger': '헤르미온느 그레인저',
  };
  print(dictionary['Harry Potter']);
  print(dictionary['Hermione Granger']);
}

 

키와 값 반환받기

모든 Map 타입은 키와 값을 모두 반환받을 수 있습니다. 값을 반환받고 싶은 Map 타입의 변수에 key와 value 게터를 실행하면 됩니다. 게터와 세터는 2.2.4 ‘게터 / 세터’에서 알아봅니다.

 

void main() {
  Map<String, String> dictionary = {
    'Harry Potter': '해리 포터',
    'Ron Weasley': '론 위즐리',
    'Hermione Granger': '헤르미온느 그레인저',
  };

  print(dictionary.keys);
  // Iterable이 반환되기 때문에 .toList()를 실행해서 List를 반환받을 수도 있음
  print(dictionary.values);
}

(Harry Potter, Ron Weasley, Hermione Granger)
(해리 포터, 론 위즐리, 헤르미온느 그레인저)

 

4.3 Set 타입

맵이 키와 값의 조합이라면 셋(Set)은 중복 없는 값들의 집합입니다. Set<타입> 세트이름 형식으로 생성합니다. 중복을 방지하므로 유일한 값들만 존재하는 걸 보장합니다.

 

void main() {
  Set<String> blackPink = {'로제', '지수', '리사', '제니', '제니'}; // ➊ 제니 중복

  print(blackPink);
  print(blackPink.contains('로제')); // ➋ 값이 있는지 확인하기
  print(blackPink.toList());         // ➌ 리스트로 변환하기

  List<String> blackPink2 = ['로제', '지수', '지수'];
  print(Set.from(blackPink2));  // ➍ List 타입을 Set 타입으로 변환
}

 

Set는 절대로 중복값을 허용하지 않기 때문에 각 값의 유일unique함을 보장받을 수 있습니다. ❶ 에서 ‘제니’가 두 번 입력됐지만, 출력은 한 번뿐입니다. ❷ 추가적으로 contains( ) 함수로 값이 있는지 없는지 확인할 수 있습니다. 원한다면 Set 타입을 ❸ List 타입으로 변환하거나 ❹ List를 Set 타입으로 변환할 수 있습니다.

컬렉션 타입은 타입 그 자체로도 다채로운 자료 형태를 표현할 수 있습니다. 하지만 컬렉션 타입의 진정한 장점은 서로의 타입으로 형변환을 하며 나타납니다. 예를 들면 Set 타입에 .toList( ) 함수를 실행하면 기존 존재하던 데이터를 유지한 채로 Set 타입을 List 타입으로 변환할 수 있습니다. 그리고 Map 타입의 키와 값을 따로 리스트로 받아보고 싶다면 .keys.toList( )와 .values.toList( ) 함수를 사용할 수 있습니다. 마지막으로 Set.from( )을 사용하면 어떤 리스트든 Set 타입으로 변환할 수 있습니다. 물론 Set 타입의 특성대로 중복값은 제거됩니다.

4.4 enum

enum은 한 변수의 값을 몇 가지 옵션으로 제한하는 기능입니다. 선택지가 제한적일 때 사용합니다. String으로 완전 대체할 수 있지만 enum은 기본적으로 자동 완성이 지원되고 정확히 어떤 선택지가 존재하는지 정의해둘 수 있기 때문에 유용합니다.

 

enum Status {
  approved,
  pending,
  rejected,
}

void main() {
  Status status = Status.approved;
  print(status);  // Status.approved
}

 

▼ 실행 결과

 

5. 연산자

연산자로는 수치 연산자, null값 입력 관련 연산자, 값 비교 연산자, 타입 비교 연산자, 논리 연산자가 있습니다.

 

5.1 기본 수치 연산자

다트 언어에서는 일반적으로 다른 언어에서도 사용하는 기본 산수 기능을 제공해줍니다.

 

void main() {
  double number = 2;

  print(number + 2); // 4 출력
  print(number - 2); // 0 출력
  print(number * 2); // 4 출력
  print(number / 2); // 1 출력. 나눈 몫
  print(number % 3); // 2 출력. 나눈 나머지

  // 단항 연산도 됩니다.
  number++; // 3
  number--; // 2
  number += 2; // 4
  number -= 2; // 2
  number *= 2; // 4
  number /= 2; // 1
}

 

▼ 실행 결과

 

다트패드로 실습하면 소수점이 없는 정수로 출력됩니다.

 

5.2 null 관련 연산자

null은 아무 값도 없음을 뜻합니다. 0과는 다릅니다(0은 0이라는 값을 가지는 겁니다). 다트 언어에서는 변수 타입이 null값을 가지는지 여부를 직접 지정해줘야 합니다. 타입 키워드를 그대로 사용하면 기본적으로 null값이 저장될 수 없습니다. 타입 뒤에 ‘?’를 추가해줘야 null값이 저장될 수 있습니다.

void main() {
  // 타입 뒤에 ?를 명시해서 null값을 가질 수 있습니다.
  double? number1 = 1;

  // 타입 뒤에 ?를 명시하지 않아 에러가 납니다.
  // double number2 = null;
}

 

타입 뒤에 ?를 추가해주면 null값이 저장될 수 있습니다. null을 가질 수 있는 변수에 새로운 값을 추가할 때 ??를 사용하면 기존에 null인 때만 값이 저장되도록 할 수도 있습니다.

 

void main() {
  double? number;   // 자동으로 null값 지정
  print(number);

  number ??= 3;   // ??를 사용하면 기존 값이 null일 때만 저장됩니다.
  print(number);

  number ??= 4; // null이 아니기 때문에 기존 3이 그대로 유지됩니다.
  print(number);
}

 

▼ 실행 결과

 

역시나 다트패드로 실습하면 소수점이 없는 정수로 출력됩니다.

 

5.3 값 비교 연산자

정수 크기를 비교하는 연산자를 확인해봅시다.

 

void main() {
  int number1 = 1;
  int number2 = 2;

  print(number1 > number2); // false
  print(number1 < number2); // true
  print(number1 >= number2); // false
  print(number1 <= number2); // true
  print(number1 == number2); // false
  print(number1 != number2); // true
}

 

5.4 타입 비교 연산자

is 키워드를 사용하면 변수의 타입을 비교할 수 있습니다.

 

void main() {
  int number1 = 1;

  print(number1 is int);     // true
  print(number1 is String);  // false
  print(number1 is! int);    // false. !는 반대를 의미합니다(int 타입이 아닌 경우 true).
  print(number1 is! String); // true
}

 

5.5 논리 연산자

and와 or을 의미하는 연산자도 사용해봅시다.

void main() {
  bool result = 12 > 10 && 1 > 0; // 12가 10보다 크고 1이 0보다 클 때
  print(result); // true

  bool result2 = 12 > 10 && 0 > 1; // 12가 10보다 크고 0이 1보다 클 때
  print(result2); // false

  bool result3 = 12 > 10 || 1 > 0; // 12가 10보다 크거나 1이 0보다 클 때
  print(result3); // true

  bool result4 = 12 > 10 || 0 > 1; // 12가 10보다 크거나 0이 1보다 클 때
  print(result4); // true

  bool result5 = 12 < 10 || 0 > 1; // 12가 10보다 작거나 0이 1보다 클 때
  print(result5); // false
}

 

다음편에서 계속 됩니다.

 

다트 입문하기

 

객체지향 프로그래밍

 

비동기 프로그래밍

 

다트 3.0 신규 문법

최지호(코드팩토리) 
임페리얼 칼리지 런던을 졸업하고 계리 컨설팅 회사 밀리만(Milliman) 한국 지사에서 소프트웨어 엔지니어로 일했습니다. 현재 주식회사 코드팩토리를 창업하여 개발을 하면서 초보자뿐만 아니라 현직 개발자에게도 유용한 개발 강의를 제작합니다. 밀리의서재 플러터 전환 차세대 프로젝트를 리드했습니다.

Leave a Reply

©2020 GoldenRabbit. All rights reserved.
상호명 : 골든래빗 주식회사
(04051) 서울특별시 마포구 양화로 186, 5층 512호, 514호 (동교동, LC타워)
TEL : 0505-398-0505 / FAX : 0505-537-0505
대표이사 : 최현우
사업자등록번호 : 475-87-01581
통신판매업신고 : 2023-서울마포-2391호
master@goldenrabbit.co.kr
개인정보처리방침
배송/반품/환불/교환 안내