들어가기 전
지난 게시글에서는 플러터 프로젝트의 구조와 함께 프로젝트 폴더 및 파일의 구성 방식을 살펴보았습니다. 특히 main.dart 파일을 중점적으로 분석하면서 Dart 언어의 중요한 특징들을 배웠습니다. 외부 패키지를 불러와 사용하는 방법도 pub.dev 사이트를 통해 알아보았습니다.
또한, 다트 언어의 핵심 기능 중, 라이브러리 불러오기와 관련된 다양한 연산자와 명령어(import, as, show, hide)에 대해 배웠습니다. 이를 통해 코드의 가독성과 효율성을 높이는 방법을 알게 되었습니다. 데이터 타입, 상수 변수, 컬렉션 타입 및 널 안정성과 같은 Dart의 핵심 요소들도 자세히 탐구하였습니다.
이제 이러한 기본적인 개념들을 바탕으로 Dart에서의 함수와 제어문의 활용 방법을 알아볼 차례입니다. 이번 세 번째 글에서는 Dart의 함수와 제어문을 중심으로, 어떻게 더 효율적이고 깔끔한 코드를 작성할 수 있는지에 대해 알아보겠습니다.
함수와 제어문
함수는 프로그래밍의 기본 구성 요소 중 하나입니다. 함수를 통해 코드의 반복을 줄이고, 구조를 더욱 체계적으로 만들 수 있습니다. Dart 언어에서도 함수는 중요한 역할을 합니다. 이번 글에서는 Dart 언어에서의 함수 선언 방법부터 다양한 함수의 활용 방법까지 살펴보겠습니다.
또한, 제어문을 통해 코드의 실행 흐름을 원하는 대로 제어할 수 있습니다. Dart에서 제공하는 다양한 제어문의 사용법과 그 특징에 대해서도 함께 알아보게 됩니다.
이 글을 통해 Dart에서의 함수와 제어문을 어떻게 활용하는지, 그리고 이를 통해 어떻게 더 효율적인 코드를 작성하는지에 대한 방법을 함께 배워보도록 합시다.
함수 선언과 호출하기
함수의 선언 위치
- Dart에서는 함수의 선언 위치에 따라 여러 유용한 기능을 제공합니다.
- 최상위 레벨: 전역적으로 사용할 수 있는 함수를 선언합니다.
- 클래스 내부: 클래스의 메서드로서 작동하는 함수를 선언합니다.
- 다른 함수 내부: 특정 함수 내에서만 사용되는 '내부 함수'를 선언합니다.
// 함수 선언 위치
void functionA(){
}
void functionB(){
void innerFunction(){
}
innerFunction();
}
class SampleClass{
void classMethod(){
}
}
매개변수의 타입
- 함수의 매개변수는 Dart에서의 타입 안정성을 위해 타입을 지정할 수 있습니다. 하지만 필요에 따라 타입을 생략하거나 동적으로 할당할 수도 있습니다.
- var: 매개변수 타입을 명시하지 않고 동적으로 할당합니다. 이 경우, 매개변수는 dynamic 타입이 됩니다.
// var 타입 매개변수
void functionB(var a){
a = 25;
a = 'globe';
a = false;
a = null;
}
main(){
functionB(); // 매개변수에 값을 전달하지 않아서 오류
functionB(15);
functionB('planet');
}
- 타입 생략: 매개변수의 타입을 생략하면 var로 선언한 것과 동일한 효과를 가집니다.
// 매개변수 이름 생략 -> dynamic 타입
void functionC(a){ // <- a : dynamic 타입
a = 25;
a = 'world';
a = false;
a = null;
}
main(){
functionC(); // 오류
functionC(15);
}
함수의 반환 타입
- Dart에서 함수는 특정 타입의 값을 반환할 수 있습니다. 반환할 값이 없을 경우 void 타입을 사용하며, 반환 타입을 생략하면 dynamic 타입이 됩니다.
// 반환 타입이 dynamin인 함수에서 return 문 생략
dynamic functionD(){ // dynamic 타입
return 15;
}
functionE(){ // dynamic 타입
return 15;
}
functionF(){ // dynamic 타입
}
main(){
print('functionD result : ${functionD()}');
print('functionE result : ${functionE()}');
print('functionF result : ${functionF()}');
}
→ 실행결과
functionD result : 15
functionE result : 15
functionF result: null // dynamic 타입 함수에 return문이 없으면 null 반환
// 반환 타입
void functionG(){
}
int functionH(){
return 12;
}
화살표 함수
- Dart에서는 함수의 본문이 한 줄인 경우, 간결한 화살표(=>) 표기법을 사용하여 함수를 정의할 수 있습니다. 이는 코드의 가독성을 향상시킵니다.
// 화살표 함수 사용
void printPlanetA(){
print('hello universe');
}
void printPlanetB() => print('hello universe');
main(){
printPlanetA();
printPlanetB();
}
→ 실행결과
hello universe
hello universe
명명된 매개변수
Dart 언어에서는 함수의 매개변수를 좀 더 유연하게 활용할 수 있도록 '명명된 매개변수'라는 개념을 제공합니다. 이를 통해 함수 호출 시 어떤 매개변수에 값을 전달할 것인지 지정할 수 있습니다.
명명된 매개변수란?
명명된 매개변수는 그 이름에서 알 수 있듯이, 특정 이름을 가진 매개변수입니다. 이를 사용하면 함수를 호출할 때 해당 매개변수의 이름과 값을 함께 전달하여 원하는 매개변수에 데이터를 전달할 수 있습니다.
// 명명된 매개변수
void showMessage({ String? msg }){
print('Msg: $msg');
showMessage(msg: 'world'); // msg : 이름, world : 값
}
명명된 매개변수의 선언 규칙
명명된 매개변수를 선언하고 사용할 때는 몇 가지 규칙을 따라야 합니다.
- 명명된 매개변수는 중괄호 { }로 묶어서 선언합니다.
- 여러 매개변수를 중괄호로 묶어 명명된 매개변수로 한 번에 명명할 수 있습니다.
- 한 함수에서 명명된 매개변수는 한 번만 선언 가능하며, 이는 함수의 매개변수 선언 부분에서 마지막에 위치해야 합니다.
- 명명된 매개변수는 기본값을 가질 수 있습니다.
// 명명된 매개변수 선언
void showDetail1({String? txt, bool? flg}, int num){} // 오류
void showDetail2(int num, {String? txt, bool? flg}, {int? num2}){} // 오류
void showDetail3(int num, {String? txt, bool? flg}){} // 성공
명명된 매개변수 호출 규칙
- 명명된 매개변수에 데이터를 전달하지 않을 수 있습니다.
- 명명된 매개변수에 데이터를 전달하려면 반드시 이름을 명시해야 합니다.
- 명명된 매개변수에 데이터를 전달할 때 선언된 순서와 맞추지 않아도 됩니다.
// 명명된 매개 변수 선언
void showDetail(int num, {String? txt, bool? flg}){} // 성공
// 명명된 매개변수 호출 예
showDetail(); // 오류
showDetail(10); // 성공
showDetail(10, 'hello', true); // 오류
showDetail(10, txt: 'hello',flg: true); // 성공
showDetail(10, flg: true, txt: 'hello'); // 성공
showDetail(txt: 'hello', 10, flg: true); // 성공
기본 인자 설정하기
명명된 매개변수에 기본값을 지정하면, 해당 매개변수에 값이 전달되지 않았을 때 그 기본값이 사용됩니다.
// 기본 인자 설정
String greet({String word = 'hello'}){
return word;
}
main(){
print('greet result : ${greet()}'); // greet() result : hello
print('greet(world) result : ${greet(word: "world")}'); // greet(world) result : world
}
필수 매개변수 선언하기 - required
때로는 명명된 매개변수가 반드시 값을 전달받아야 할 필요가 있습니다. 이럴 때는 required 키워드를 사용하여 해당 매개변수가 반드시 값을 전달받아야 함을 지정할 수 있습니다.
// 명명된 필수 매개변수 선언
showReq({required int n}){
print('showReq().. n: $n');
}
main(){
showReq(); // 오류
showReq(n: 10); // 성공
}
이러한 방식으로 Dart에서의 명명된 매개변수를 유용하게 활용할 수 있습니다. 이를 통해 코드의 유연성과 가독성을 높일 수 있습니다.
옵셔널 위치 매개변수
Dart에서는 함수의 매개변수를 유연하게 관리하기 위한 다양한 방법 중 하나로 '옵셔널 위치 매개변수'를 제공합니다. 이를 활용하면 함수의 매개변수를 생략하거나 순서대로 전달하는 것이 가능합니다.
옵셔널 위치 매개변수란?
옵셔널 위치 매개변수는 이름에서 알 수 있듯이, 순서대로 위치를 기반으로 선택적으로 전달될 수 있는 매개변수입니다. 이는 대괄호 '[]'를 활용하여 선언됩니다. 옵셔널 위치 매개변수로 선언된 함수는 데이터 전달은 자유지만 순서는 맞춰서 호출해야 합니다.
// 옵셔널 위치 매개변수
void display([String name = 'hello', int age = 10]){
print('name: $name, age: $age');
display('world', 15); // world, 15 : 값
}
옵셔널 위치 매개변수의 규칙
- 옵셔널 위치 매개변수는 대괄호 []로 묶어서 선언합니다.
- 함수 내에서는 오직 마지막 매개변수에만 옵셔널 위치 매개변수를 사용할 수 있습니다.
- 옵셔널 위치 매개변수에는 기본값을 지정할 수 있습니다.
- 함수를 호출할 때는 매개변수 이름을 생략하고 순서대로 전달합니다.
// 옵셔널 위치 매개변수 선언
void displayInfo(int id, [String desc = 'hello', bool flag = false]){}
// 옵셔널 위치 매개변수 호출 예시
displayInfo(); // 오류
displayInfo(10); // 성공
displayInfo(10, desc: 'world', flag: true) // 오류
displayInfo(10, 'world', true); // 성공
displayInfo(10, true, 'world'); // 오류
displayInfo(10, 'world'); // 성공
displayInfo(10, true); // 오류
사용 빈도
실제 프로그래밍 환경에서 옵셔널 위치 매개변수는 그렇게 자주 사용되지 않습니다. 대부분의 경우, 함수 호출 시에 매개변수의 명확한 이름을 지정하여 값을 전달하는 것이 더 가독성 있고 효율적이기 때문입니다. 그럼에도 불구하고, 특정한 경우나 필요에 따라 옵셔널 위치 매개변수를 활용할 수 있습니다.
이렇게 Dart의 옵셔널 위치 매개변수는 함수의 유연성을 높여줄 수 있으나, 실제 사용 시에는 주의가 필요합니다. 매개변수의 순서나 기본값 설정 등을 잘 고려하여 활용하는 것이 중요합니다.
함수 타입 인수
Dart에서는 함수도 객체로 취급됩니다. 함수를 다른 변수에 대입하거나, 다른 함수의 인수로 전달하는 것이 가능합니다. 이렇게 함수를 대입할 수 있는 객체를 "함수 타입"이라고 부릅니다.
함수 타입의 선언 및 사용
함수 타입은 Function으로 선언할 수 있습니다.
// 함수 타입의 선언
void foo(){}
Function fVar = foo;
함수를 인수로 받거나 반환하는 예제는 다음과 같습니다.
// 함수를 활용한 예
int add(int n){
return n + 10;
}
int mul(int n){
return n * 10;
}
Function choose(Function fn){
print('Function: ${fn(20)}');
return mul;
}
void main(){
var resFn = choose(add);
print('Result: ${resFn(20)}');
}
이를 실행하면 다음과 같은 결과를 얻습니다:
Function: 30
Result: 200
함수 타입의 제한
특정한 형태의 함수만을 인수로 받고 싶을 때, 함수 타입에 제한을 줄 수 있습니다.
// 함수 타입 제한
void specFn(int g(int a)){
g(30);
}
void main(){
specFn((int a){
return a+20;
});
}
익명 함수 (Anonymous Functions)
Dart에서는 이름이 없는 함수를 "익명 함수"라고 합니다. 이는 일반적으로 람다 함수 또는 함수 리터럴(Function Literal)로도 알려져 있습니다.
익명 함수의 사용 예제는 다음과 같습니다:
// 익명함수 사용 예
var anonFn1 = (arg){
return 10;
};
Function anonFn2 = (arg){
return 10;
};
익명 함수는 특정 작업을 빠르게 수행하거나, 한 번만 사용될 함수에 주로 사용됩니다. Dart에서는 함수를 값처럼 취급하여 다양한 방법으로 활용할 수 있어, 코드의 유연성과 재사용성을 높일 수 있습니다.
게터와 세터 함수
Dart에서는 데이터의 접근과 수정을 효과적으로 관리하기 위해 게터(getter)와 세터(setter)라는 특별한 유형의 함수를 제공합니다.
게터(Getter)
게터는 특정 데이터나 속성의 값을 반환하는 데 사용됩니다. Dart에서는 get 예약어를 사용하여 게터를 정의합니다. 게터는 속성처럼 사용될 수 있지만, 실제로는 함수와 같은 방식으로 작동합니다.
세터(Setter)
세터는 특정 데이터나 속성의 값을 설정하거나 변경하는 데 사용됩니다. Dart에서는 set 예약어로 세터를 정의하며, 세터도 속성처럼 사용되지만 함수의 특징을 가지고 있습니다.
다음은 게터와 세터의 선언 및 사용 예제입니다:
// 게터와 세터의 선언
String _name = 'Hello';
String get name {
return _name.toUpperCase();
}
set name(value){
_name = value;
}
// 게터와 세터의 사용
void main(){
name = "World"; // 세터 호출
print('name: $name'); // 게터 호출
}
이 코드를 실행하면, 다음과 같은 결과가 출력됩니다:
name: WORLD
만약 get 예약어로 게터만을 선언하게 된다면, 해당 속성은 읽기 전용이 되어 값을 변경할 수 없게 됩니다. 이는 final 변수와 비슷한 특징을 가지게 됩니다.
// 게터만의 선언 예제
String _name = 'Hello';
String get name {
return _name.toUpperCase();
}
void main(){
// name = "World"; // 이 부분은 오류를 발생시킵니다.
print(name);
}
게터와 세터를 사용하면 데이터의 은닉 및 캡슐화를 효과적으로 수행할 수 있으며, 데이터의 유효성 검사나 다른 부수적인 작업을 쉽게 처리할 수 있습니다.
기타 연산자 알아보기
Dart는 다양한 연산자를 제공하여 프로그래머가 코드를 효율적으로 작성할 수 있도록 돕습니다. 이 중에서도, 특별한 기능을 가진 연산자들이 있어 이를 '기타 연산자'로 분류하곤 합니다.
나누기 연산자 - ~/
Dart에서는 나누기를 표현하는 두 가지 연산자, /와 ~/를 제공합니다. 특히 ~/ 연산자는 나눈 결과를 정수로 반환합니다.
// 나누기 연산자 예제
main(){
int num = 8;
print('num / 5 = ${num / 5}'); // 소수점 포함 결과 반환
print('num ~/ 5 = ${num ~/ 5}'); // 정수 결과 반환
}
→ 실행결과
num / 5 = 1.6
num ~/ 5 = 1
타입 확인과 변환 – is, as
Dart에서는 is 연산자로 변수의 타입을 확인하며, as 연산자를 사용해 변수의 타입을 변환할 수 있습니다.
// 타입 확인과 변환 예제
class Person{
void display(){
print("Person function...");
}
}
main(){
Object instance = Person();
// instance.display(); // 오류
if(instance is Person){ // 타입 확인 후 자동 형 변환
instance.display();
}
(instance as Person).display(); // 명시적 타입 변환
}
반복 접근 연산자 - …, ?..
Dart에서 ..는 객체의 여러 속성에 연속적으로 접근할 때 사용하는 캐스케이드 연산자입니다. 이를 사용하면 코드를 보다 간결하게 작성할 수 있습니다.
// Person 클래스 정의
class Person{
String? name;
int? age;
display(){
print('name: $name, age: $age');
}
}
// 객체 생성 및 멤버 접근 예제
var person = Person();
person.name = 'park';
person.age = 24;
person.display();
// 캐스케이드 연산자를 사용한 예제
Person()
..name = 'lee'
..age = 25
..display();
예외 던지기와 예외 처리
프로그래밍에서 코드의 실행 흐름을 제어하는 것은 중요합니다. Dart 언어는 다양한 제어문을 통해 실행 흐름을 사용자의 의도대로 관리할 수 있게 도와줍니다. 여기서는 몇 가지 주요 제어문과 그 활용 방법에 대해 알아봅니다.
for 반복문에서 in 연산자
Dart에서는 for 반복문을 사용하여 데이터의 시퀀스나 컬렉션을 순회하며 특정 작업을 반복적으로 수행할 수 있습니다.
// for 문 사용 예제
main(){
var numbers = [10, 20, 30];
for(var i= 0; i < numbers.length; i++){
print(numbers[i]);
}
}
→ 실행결과:
10
20
30
for 문에는 범위 연산자인 in을 사용하여 컬렉션 타입의 데이터 개수만큼 반복 실행할 수도 있습니다. 이 방법은 코드의 가독성을 높이며, 개발자가 명시적으로 인덱스를 관리할 필요가 없습니다.
// in 연산자를 활용한 간단한 for 문
main(){
var numbers = [10, 20, 30];
for(var num in numbers){
print(num);
}
}
switch~case 선택문
Dart에서 switch~case 문을 사용하면 변수의 값에 따라 여러 조건 중 하나를 선택하여 실행할 수 있습니다. Dart에서는 switch 문의 조건으로 정수나 문자열 타입을 사용할 수 있습니다.
다만, 각 case문 끝에는 break, continue, return, 또는 throw와 같은 제어문을 포함해야 합니다. 이는 코드의 논리적 오류를 방지하기 위함입니다.
// 잘못된 switch~case 사용
action(arg){
switch(arg){
case 'A': // 오류 발생 가능
print('A');
case 'B':
print('B');
}
}
// switch~case 문 올바른 사용 예
action(arg){
switch(arg){
case 'A':
print('A');
break;
case 'B':
print('B');
}
}
예외 던지기와 예외 처리
코드 실행 중에 예기치 않은 오류나 예외 상황이 발생할 수 있습니다. Dart에서는 이러한 예외를 핸들링하기 위한 다양한 방법들을 제공합니다.
- 예외 던지기: 예외를 발생시키려면 throw 문을 사용합니다. Dart에서는 Exception 클래스 이외에도 다양한 종류의 객체를 예외로 던질 수 있습니다.
// 예외 던지기
raiseError(){
throw Exception('an error occurred');
}
// 문자열 던지기
raiseStringError(){
throw 'an error occurred';
}
// 사용자 정의 객체 던지기
class CustomError{}
raiseCustomError(){
throw CustomError();
}
- 예외 처리: try~catch, try~on~finally를 사용하여 예외를 처리합니다. try 블록에는 예외가 발생할 가능성이 있는 코드를 넣습니다. 예외 발생 시, catch나 on 절에서 해당 예외를 처리하게 됩니다. finally 블록에는 예외 발생 여부와 관계없이 항상 실행될 코드를 넣습니다.
// try~on~finally 예외 처리
generateError(){
throw FormatException('custom exception');
}
main(List<String> arguments){
try{
print('step 1...');
generateError();
print('step 2...');
} on FormatException{
print('step 3...');
} on Exception{
print('step 4...');
} finally{
print('step 5...');
}
print('step 6...');
}
→ 실행결과
step1....
step3....
step5....
step6....
// 예외 객체 가져오기
generateError(){
throw FormatException('custom exception');
}
main(List<String>arguments){
try{
print('step1....');
generateError();
print('step2....');
} on FormatException catch(e){
print('step3....$e');
} on Exception{
print('step4....');
} finally{
print('step5....');
}
print('step6....');
}
// try~catch 예외 처리
try{
generateError();
} catch(e){
print('catch....$e');
}
→ 실행결과:
step1....
step3....FormatException: custom exception
step5....
step6....
catch....FormatException: custom exception
이와 같이 Dart에서는 실행 흐름을 제어하고 예외 상황에 효과적으로 대응하기 위한 다양한 제어문과 예외 처리 방법을 제공합니다.
결론
본 게시글에서는 Dart 프로그래밍의 핵심 요소 중 하나인 함수와 제어문에 대해 깊게 탐구해 보았습니다.
- 함수 선언과 호출하기: 기본적인 함수의 작성 및 사용 방법을 알아보았으며, 이를 통해 코드의 재사용성과 모듈성을 향상시킬 수 있음을 알게 되었습니다.
- 명명된 매개변수, 옵셔널 위치 매개변수, 함수 타입 인수: Dart에서 제공하는 다양한 함수 매개변수의 특징을 통해 유연하고 다양한 함수 정의가 가능하다는 것을 이해하였습니다.
- 게터와 세터 함수: 객체 지향 프로그래밍의 중요한 개념 중 하나로, 데이터의 캡슐화와 정보 은닉에 대한 중요성을 배웠습니다.
- 기타 연산자 알아보기: Dart에서 제공하는 다양한 연산자를 통해 코드의 간결성과 효율성을 높일 수 있음을 확인하였습니다.
- 예외 던지기와 예외 처리: 안정적인 프로그램을 위한 예외 처리 방법에 대해 배웠으며, 이를 통해 예기치 않은 오류 상황에 대비할 수 있게 되었습니다.
이러한 내용들을 종합해 보면, Dart 프로그래밍에서의 코드 구성과 실행 흐름 제어에 대한 깊은 이해를 얻을 수 있게 되었습니다. 이번에 얻은 지식을 바탕으로 Dart 코드 작성 시 효과적이고 안정적인 코드 구조를 설계하는 데 큰 도움이 될 것입니다.