플러터(Flutter)는 다트(Dart) 언어를 사용합니다. 다트를 알아야 플러터로 앱 개발이 가능하므로 먼저 다트 문법을 알아봅시다.
대부분의 플러터 입문자는 별도의 책으로 다트를 공부하지 않고 플러터 서적에서 1개 장 분량으로 얕게 배웁니다. 플러터로 앱을 원활히 개발하려면 다트를 탄탄하게 아는 것이 중요합니다. 그래서 이 책은 또 다른 자료를 찾아보지 않아도 될 정도로 깊이 있게 다트를 다룹니다.
1장에서 다트 입문하기, 2장에서 객체지향 프로그래밍, 3장에서 비동기 프로그래밍, 4장은 다트 3.0 신규 문법을 학습합니다.
다트 입문하기 ❸
다트로 코딩하는 데 필요한 기초 지식인 변수와 상수, 컬렉션, 연산자, 제어문, 함수를 알아봅니다.
6. 제어문
제어문으로는 if문, switch문, for문, while문을 제공합니다.
6.1 if문
if문은 원하는 조건을 기준으로 다른 코드를 실행하고 싶을 때 사용됩니다. if문, else if문, else문의 순서대로 괄호 안에 작성한 조건이 true이면 해당 조건의 코드 블록이 실행이 됩니다.
void main() {
int number = 2;
if (number % 3 == 0) {
print('3의 배수입니다.');
} else if (number % 3 == 1) {
print('나머지가 1입니다.');
} else {
// 조건에 맞지 않기 때문에 다음 코드 실행
print('맞는 조건이 없습니다.');
}
}
▼ 실행 결과
6.2 switch문
입력된 상수값에 따라 알맞은 case 블록을 수행합니다. break 키워드를 사용하면 switch문 밖으로 나갈 수 있습니다. case 끝에 break 키워드를 사용하는 걸 잊지 마세요(빼먹으면 컴파일 중에 에러가 납니다). enum과 함께 사용하면 유용합니다.
enum Status {
approved,
pending,
rejected,
}
void main() {
Status status = Status.approved;
switch (status) {
case Status.approved:
// approved 값이기 때문에 다음 코드가 실행됩니다.
print('승인 상태입니다.');
break;
case Status.pending:
print('대기 상태입니다.');
break;
case Status.rejected:
print('거절 상태입니다.');
break;
default:
print('알 수 없는 상태입니다.');
}
// Enum의 values값은 Enum의 모든 수를 리스트로 반환합니다.
print(Status.values);
}
▼ 실행 결과
6.2 for문
for문은 작업을 여러 번 반복해서 실행할 때 사용합니다.
void main() {
// 값 선언; 조건 설정; loop 마다 실행할 기능
for (int i = 0; i < 3; i++) {
print(i);
}
}
▼ 실행 결과
다트 언어에서는 for…in 패턴의 for문도 제공해줍니다. 일반적으로 List의 모든 값을 순회하고 싶을 때 사용됩니다. 다음 예제 코드를 통해 알아봅시다.
void main() {
int total = 0;
List<int> numberList = [3, 6, 9];
for (int number in numberList) {
print(number);
}
}
▼ 실행 결과
6.4 while문과 do…while문
while문과 do…while문은 for문과 마찬가지로 반복적인 작업을 실행할 때 사용됩니다. 미리 알아본 for문은 횟수 기반으로 함수를 반복적으로 실행합니다. 예를 들어 특정 리스트 길이나 지정한 숫자 이하의 횟수만 반복하도록 코드를 작성합니다. while문은 조건을 기반으로 반복문을 실행합니다. 조건이 true이면 계속 실행하고 false이면 멈추게 됩니다.
while문 먼저 알아보겠습니다.
void main(){
int total = 0;
while(total < 10){ // total 값이 10보다 작으면 계속 실행
total += 1;
}
print(total);
}
▼ 실행 결과
do…while은 특수한 형태의 while문입니다. while문은 조건을 먼저 확인한 후 true가 반환되면 반복문을 실행하지만 do…while은 반복문을 실행한 후 조건을 확인합니다.
void main(){
int total = 0;
do {
total += 1;
} while(total < 10);
print(total);
}
▼ 실행 결과
7. 함수와 람다
일반적인 함수 관련 특징을 알아보고 나서, 이름이 없는 익명 함수와 람다 함수를 알아봅니다. 마지막으로 typedef와 함수가 어떻게 다른지도 살펴보겠습니다.
7.1 함수의 일반적인 특징
함수를 사용하면 한 번만 작성하고 여러 곳에서 재활용할 수 있습니다. 반환할 값이 없을 때는 void 키워드를 사용합니다.
int addTwoNumbers(int a, int b) {
return a + b;
}
void main() {
print(addTwoNumbers(1, 2));
}
▼ 실행 결과
다트 함수에서 매개변수를 지정하는 방법으로 순서가 고정된 매개변수(포지셔널 파라미터, Positional Parameter, 위치 매개변수라고도 합니다)와 이름이 있는 매개변수(네임드 파라미터, Named Parameter, 명명된 매개변수라고도 합니다)가 있습니다. 포지셔널 파라미터는 입력된 순서대로 매개변수에 값이 지정됩니다. 예를 들어 위 코드에서 int a가 int b보다 먼저 선언됐기 때문에 함수를 실행할 때도 1, 2 순서대로 a와 b에 입력됩니다. 두 번째 이름을 덧붙이는 매개변수는 순서와 관계없이 지정하고 싶은 매개변수의 이름을 이용해 값을 입력할 수 있습니다. 키와 값 형태로 매개변수를 입력하면 되므로 입력 순서는 중요하지 않습니다.
네임드 파라미터를 지정하려면 중괄호 { }와 required 키워드를 사용해야 합니다. addTwoNumbers( ) 함수를 네임드 파라미터 방식으로 변환하겠습니다.
int addTwoNumbers({
required int a,
required int b,
}) {
return a + b;
}
void main() {
print(addTwoNumbers(a: 1, b: 2));
}
▼ 실행 결과
여기서 required 키워드는 매개변수가 null값이 불가능한 타입이면 기본값을 지정해주거나 필수로 입력해야 한다는 의미입니다.
기본값을 갖는 포지셔널 파라미터를 지정하겠습니다. 기호를 사용하면 됩니다.
int addTwoNumbers(int a, [int b = 2]) {
return a + b;
}
void main() {
print(addTwoNumbers(1));
}
▼ 실행 결과
입력값이 하나뿐이라서 두 번째 매개변수에 기본값 2를 적용해 계산한 결과를 반환했습니다.
이번에는 네임드 파라미터에 기본값을 적용하겠습니다. required 키워드를 생략해주고 등호 다음에 원하는 기본값을 입력해주면 됩니다.
int addTwoNumbers({
required int a,
int b = 2,
}) {
return a + b;
}
void main() {
print(addTwoNumbers(a: 1));
}
▼ 실행 결과
포지셔널 파라미터와 네임드 파라미터를 섞어서 사용할 수도 있습니다. 섞어 쓸 때는 포지셔널 파라미터가 네임드 파라미터보다 반드시 먼저 위치해야 합니다.
int addTwoNumbers(
int a, {
required int b,
int c = 4,
}) {
return a + b + c;
}
void main() {
print(addTwoNumbers(1, b: 3, c: 7));
}
▼ 실행 결과
7.2 익명 함수와 람다 함수
익명 함수(Anonymous Function)와 람다 함수는 둘 다 함수 이름이 없습니다. 이 둘은 함수 이름이 없고 일회성으로 사용된다는 공통점이 있습니다. 통상적으로 많은 언어에서 익명 함수와 람다 함수를 구분하지만 다트에서는 구분하지 않습니다. 여기서 설명은 편의를 고려해 ‘기본적인 익명 함수’와 ‘람다식(Lambda Expression)을 사용하는 익명 함수’로 나누어 설명합니다.
▼ 익명 함수와 람다 함수 표현 방식
익명 함수에서 { }를 빼고 => 기호를 추가한 것이 람다 함수입니다. 매개변수는 아예 없거나 하나 이상이어도 됩니다. 익명 함수와 달리 코드 블록을 묶는 { }가 없는 람다는 함수 로직을 수행하는 스테이트먼트가 딱 하나이어야 합니다(한 줄이 아닙니다. 명령 단위가 하나여야 합니다). 람다 함수는 이름을 정하고 미리 선언할 필요가 없어서 글로벌 스코프(Global Scope)로 다룰 필요가 없습니다. 더 나아가 하나의 스테이트먼트만 다루기 때문에 적절히 사용하면 간결하게 코드를 작성할 수 있으며 (실행하는 위치에 로직 코드가 있기 때문에) 가독성이 높습니다. 그렇기 때문에 콜백 함수나 리스트의 map( ), reduce( ), fold( ) 함수 등에서 일회성이 높은 로직을 작성할 때 주로 사용니다.
reduce( ) 함수를 이용해서 리스트의 모든 값을 더하는 익명 함수와 람다 함수를 작성해보겠습니다.
익명 함수
void main() {
List<int> numbers = [1,2,3,4,5];
// 일반 함수로 모든 값 더하기
final allMembers = numbers.reduce((value, element) {
return value + element;
});
print(allMembers);
}
람다 함수
void main() {
List<int> numbers = [1,2,3,4,5];
// 람다 함수로 모든 값 더하기
final allMembers = numbers.reduce((value, element) => value + element);
print(allMembers);
}
출력값은 둘 다 15입니다.
7.3 typedef와 함수
typedef 키워드는 함수의 시그니처를 정의하는 값으로 보면 됩니다. 여기서 시그니처는 반환값 타입, 매개변수 개수와 타입 등을 말합니다. 즉 함수 선언부를 정의하는 키워드입니다. 함수가 무슨 동작을 하는지에 대한 정의는 없습니다.
typedef Operation = void Function(int x, int y);
함수를 선언하기는 했지만 무얼하는지 동작이 없습니다. 그럼 이 함수를 어떻게 사용할까요? 시그니처에 맞춘 함수를 만들어서 사용하면 됩니다.
typedef Operation = void Function(int x, int y);
void add(int x, int y) {
print('결괏값 : ${x + y}');
}
void subtract(int x, int y) {
print('결괏값 : ${x - y}');
}
void main() {
// typedef는 일반적인 변수의 type처럼 사용 가능
Operation oper = add;
oper(1, 2);
// subtract() 함수도 Operation에 해당되는
// 시그니처이므로 oper 변수에 저장 가능
oper = subtract;
oper(1, 2);
}
▼ 실행 결과
다트에서 함수는 일급 객체(First-Class Citizen)이므로 함수를 값처럼 사용할 수 있습니다(퍼스트 클래스 시티즌, 일급 시민이라고도 합니다). 그래서 플러터에서는 typedef으로 선언한 함수를 다음과 같이 매개변수로 넣어 사용합니다.
typedef Operation = void Function(int x, int y);
void add(int x, int y) {
print('결괏값 : ${x + y}');
}
void calculate(int x, int y, Operation oper) {
oper(x, y);
}
void main() {
calculate(1, 2, add);
}
▼ 실행 결과
calculate( ) 함수의 3번째 매개변수로 add( ) 함수를 입력했습니다.
8. try…catch
try…catch문의 목적은 특정 코드의 실행을 시도(try)해보고 문제가 있다면 에러를 잡으라(catch)는 뜻입니다. try…catch문은 try와 catch 사이의 괄호에 에러가 없을 때 실행할 로직을 작성하고 catch가 감싸는 괄호에 에러가 났을 때 실행할 로직을 작성하면 됩니다. 만약에 try 로직에서 에러가 나면 이후의 로직은 실행되지 않고 바로 catch 로직으로 넘어갑니다.
void main() {
try{
// 에러가 없을 때 실행할 로직
final String name = '코드팩토리';
print(name); // ➊ 에러가 없으니 출력됨
}catch(e){ // catch는 첫 번째 매개변수에 에러 정보를 전달해줍니다.
// 에러가 있을 때 실행할 로직
print(e);
}
}
▼ 실행 결과
try 로직에서 에러가 나지 않았으니 catch문이 실행되지 않고 ‘코드팩토리’가 출력됩니다.
다트 언어에서는 throw 키워드를 사용해 에러를 발생시킬 수 있습니다. 위와 같은 예제에서 throw 키워드를 사용해서 에러를 발생시켜보겠습니다.
void main() {
try{
final String name = '코드팩토리';
// ➊ throw 키워드로 고의적으로 에러를 발생시킵니다.
throw Exception('이름이 잘못됐습니다!');
print(name);
}catch(e){
// ➋ try에서 에러가 발생했으니 catch 로직이 실행됩니다.
print(e);
}
}
▼ 실행 결과
❶ throw 키워드를 사용해서 에러를 발생시키니 try에 있는 로직 실행이 중지되고 catch 로직이 실행됐습니다. 그래서 name 변숫값은 출력되지 않고 ❷ 발생한 에러 메시지가 출력되었습니다.
다트 입문 마무리하기
- JIT(Just in Time)은 변경된 코드만 컴파일하는 방식입니다. 핫 리로드 기능은 변경된 내용을 UI에 뿌려줍니다. 컴파일 시간을 단축시켜주므로 개발할 때 유용합니다. 반면 AOT(Ahead of Time) 컴파일은 시스템에 최적화해 컴파일하는 방식으로 런타임 성능을 개선하고, 저장 공간을 절약하고, 설치와 업데이트 시간을 단축시켜줍니다. 배포할 때 적합한 방식입니다.
- 다트 언어가 자동으로 타입을 유추하는 변수를 선언할 때는 var 키워드를 사용합니다.
- 다트의 기본 타입에는 String(문자열), int(정수), double(실수), bool(불리언, true/false) 이 있습니다.
- dynamic 키워드는 어떤 타입이든 저장할 수 있는 변수를 선언할 때 사용합니다.
- 다트 언어의 대표적인 컬렉션 타입은 List, Map, Set입니다.
- if문과 switch문을 사용해서 조건문을 실행할 수 있습니다.
- for문, while문 그리고 do…while문을 사용해서 반복문을 실행할 수 있습니다.
- 함수는 반환값, 매개변수, 실행문으로 이루어져 있습니다.
- 익명 함수와 람다 함수 모두 함수 이름이 없으며 일회성으로 쓸 때 사용합니다.
- typedef는 함수의 시그니처인 함수의 선언부만 정의할 수 있습니다.
여기까지 1장 다트 입문하기였습니다. 다트 언어 마스터하기는 2장 객체지향 프로그래밍으로 이어집니다.
다트 입문하기
- 1편 보기: https://bit.ly/47NAtjC
- 2편 보기: https://bit.ly/3Sfl8SX
- 3편 보기: https://bitly.ws/3biHw
객체지향 프로그래밍
- 1편 보기: https://bitly.ws/3cmYi
- 2편 보기: https://bitly.ws/3ctA9
비동기 프로그래밍
- 더 보기: https://bit.ly/49bgMDJ
다트 3.0 신규 문법
- 더 보기: https://bit.ly/3unqMuB
최지호(코드팩토리)
임페리얼 칼리지 런던을 졸업하고 계리 컨설팅 회사 밀리만(Milliman) 한국 지사에서 소프트웨어 엔지니어로 일했습니다. 현재 주식회사 코드팩토리를 창업하여 개발을 하면서 초보자뿐만 아니라 현직 개발자에게도 유용한 개발 강의를 제작합니다. 밀리의서재 플러터 전환 차세대 프로젝트를 리드했습니다.