들어가기 전
이전 글에서 크로스 플랫폼 앱 개발의 선두주자인 '플러터'와 그것을 지원하는 프로그래밍 언어 '다트'에 대해 알아보았습니다. 플러터의 역사와 특징, 그리고 다트 언어의 특성 및 그 중요성을 알아보았습니다.
이번에는 아래의 주요 주제들을 다루어 보겠습니다.
- 플러터 프로젝트 구조 분석하기: 플러터 프로젝트를 시작하면 어떤 폴더와 파일들이 생성되는지, 그리고 그 구조와 각 파일의 역할을 자세히 알아봅니다.
- 다트 언어의 기본 기능 알아보기: 다트 언어에서 제공하는 import, as, 그리고 외부에서 사용할 수 없게 제한하는 '_'로 시작하는 identifier, show 등의 기본적인 기능들을 학습합니다.
- 데이터 타입과 널 안정성: 다트의 다양한 데이터 타입과, 최근 강화된 널 안정성에 대해 깊게 탐구합니다.
플러터 프로젝트 구조 분석하기
플러터는 굉장히 강력한 크로스 플랫폼 앱 개발 프레임워크입니다. 이 프레임워크의 높은 유연성과 확장성은 플러터 프로젝트의 구조에서부터 드러납니다. 이번에는 그 구조와 주요 파일에 대해 자세히 알아봅시다.
프로젝트 폴더 & 파일 구성 알아보기
- android: 안드로이드 앱을 위한 설정과 코드들이 들어 있는 폴더입니다.
- ios: ios 앱을 위한 설정과 코드들이 담겨 있습니다.
- lib: 여기에 모든 다트 파일이 들어갑니다. 특히, 이 폴더 내의 main.dart 파일은 앱의 시작점입니다.
- test: 당신의 앱을 테스트하기 위한 다트 파일들을 담고 있습니다.
- .gitignore: 이 파일은 git에 업로드하지 않을 파일이나 폴더를 지정합니다.
- pubspec.yaml: 앱의 메타데이터와 의존성을 정의하는 중요한 파일입니다.
main.dart 파일 분석하기
- import 구문: 다른 다트 파일이나 라이브러리를 이 파일에서 사용하기 위해 불러옵니다.
- main() 함수: 이 함수는 앱이 시작될 때 가장 먼저 호출되는 함수입니다. 이 함수에서 runApp() 함수를 호출하여 화면에 위젯을 출력합니다.
main()이 없으면 오류 발생
- MyApp 위젯 이해하기
- MyApp 위젯은 플러터 앱의 기본 구조를 정의하는 핵심 위젯 중 하나입니다.
- MyApp 클래스 특징:
- StatelessWidget 또는 StatefulWidget 중 하나를 상속받아서 생성됩니다. 대부분의 경우 초기 설정 위젯으로 StatelessWidget을 상속받습니다.
- build() 함수 내에서 화면의 구성을 정의합니다.
- MaterialApp 위젯을 통해 앱에 머티리얼 디자인 테마와 설정을 적용할 수 있습니다.
- 여기서 'State'는 변수를 의미하지만, 플러터에서는 화면의 상태를 자동으로 업데이트하는 특별한 변수로 사용됩니다.
- MyHomePage 위젯 깊게 알아보기
- MyHomePage는 사용자에게 보여질 화면의 주요 위젯입니다.
- MyHomePage 클래스 특징:
- StatefulWidget을 상속받아 화면의 동적 변화를 관리합니다.
- StatefulWidget이 실행될 때 createState() 함수가 호출되어 상태를 초기화합니다.
- StatefulWidget을 상속받아 화면의 동적 변화를 관리합니다.
- MyHomePage 클래스 특징:
- MyHomePage는 사용자에게 보여질 화면의 주요 위젯입니다.
- _MyHomePageState 클래스의 역할
- _MyHomePageState는 MyHomePage의 상태를 관리하고 화면에 반영하는 역할을 합니다.
- _MyHomePageState 클래스 특징:
- 해당 클래스의 build() 함수가 호출될 때 화면에 표시될 위젯 구성이 결정됩니다.
- MyHomePage는 주로 생성자를 통해 데이터를 전달받아 화면에 표시합니다.
- _MyHomePageState는 상태(State)를 관리하며, 해당 상태의 변화가 화면에 자동으로 반영됩니다
pub.dev 사이트에서 외부 패키지 사용하기
pub.dev는 플러터와 다트를 위한 패키지 저장소입니다. 여기에서 다양한 외부 패키지를 찾을 수 있으며, 패키지의 LINKS, PUB POINTS, 및 POPULARITY 수치를 통해 해당 패키지의 인기도와 안정성을 확인할 수 있습니다. 또한 패키지의 지원 플랫폼 정보도 확인하는 것이 중요합니다.
1. 메인 환경 파일에서 패키지 사용하기
- pubspec.yaml 파일에 원하는 패키지의 이름과 버전을 등록합니다.
- dependencies 섹션에는 앱이 빌드 및 실행될 때 필요한 패키지를 명시합니다.
- 개발 중에만 사용되는 패키지는 dev_dependencies에 등록합니다. 이러한 패키지는 앱의 최종 빌드에는 포함되지 않습니다.
- 패키지의 버전은 특정 버전 또는 범위를 지정하여 사용할 수 있습니다. 예를 들면, ^1.2.3은 1.2.3 버전 이상, 그리고 2.0.0 미만의 버전을 의미합니다.
- 패키지 관리 명령어:
- Pub get: 지정된 패키지를 다운로드합니다.
- Pub upgrade: 패키지를 최신 버전으로 업데이트합니다.
- Pub outdated: 오래된 패키지 종속성을 확인합니다.
- Flutter doctor: 플러터의 개발 환경을 점검합니다.
2. 터미널을 이용한 패키지 관리
- Flutter 프로젝트 생성: flutter create app1
- 앱 실행: flutter run
- 패키지 추가: flutter pub add provider
- 기타 패키지 관리 명령어:
- 패키지 다운로드: flutter pub get
- 오래된 패키지 확인: flutter pub outdated
- 패키지 업그레이드: flutter pub upgrade
다트 언어의 기본 기능 알아보기
다트는 유연하고 풍부한 기능을 가진 프로그래밍 언어입니다. 아래의 기본 기능들을 이해하고 활용하면 더 효과적인 다트 프로그래밍이 가능해집니다.
라이브러리 불러오기 – import
다트에서 라이브러리를 사용하려면 import 키워드를 사용해야 합니다.
- dart:core 라이브러리: 모든 다트 애플리케이션에 기본적으로 포함되어 있으므로 별도로 선언하지 않아도 사용할 수 있습니다. 기본 데이터 타입이나 기본 연산 등의 기능을 제공합니다.
- 상대 경로로 불러오기: 같은 프로젝트 내의 다른 파일을 불러올 때 사용합니다. 예: import 'path/to/my_library.dart';
- package 접두사로 불러오기: 특정 패키지의 라이브러리를 불러올 때 사용합니다. 예: import 'package:flutter/material.dart';
- dart 접두사로 불러오기: 다트에서 기본으로 제공하는 라이브러리를 불러올 때 사용합니다. 예를 들면, dart:math 라이브러리는 수학 관련 함수와 클래스를 제공합니다.
외부에서 사용할 수 없게 제한하기
다트에서는 접근 제한자를 따로 제공하지 않습니다. 대신, 식별자 앞에 밑줄(_)을 붙여 private하게 만들 수 있습니다.
- public: 기본 상태로서, 다른 파일에서도 접근 가능합니다.
- private: 식별자 앞에 _를 붙이면 해당 요소는 해당 파일 내에서만 접근 가능합니다.
식별자에 별칭 정의하기 – as
다트에서는 라이브러리의 식별자나 클래스에 별칭을 붙일 수 있습니다. 이는 as 예약어를 사용하여 수행됩니다.
예: import 'package:my_package/my_library.dart' as myLib;
특정 요소 불러오기 - show
show 키워드를 사용하면 특정 라이브러리에서 원하는 요소만을 불러올 수 있습니다.
예: import 'package:my_package/my_library.dart' show MyFunction, MyClass;
특정 요소 제외하기 - hide
hide 키워드를 사용하면 라이브러리에서 특정 요소를 제외하고 불러올 수 있습니다.
예: import 'package:my_package/my_library.dart' hide UnwantedClass;
데이터 타입과 널 안정성
1. 데이터 타입
다트에서는 모든 변수가 객체입니다. 이러한 객체 지향적 특성 덕분에 각 변수는 특정 클래스의 인스턴스로 취급됩니다.
int? no = 10;
main(){
bool? data = no?.isEven;
no = null;
Object? obj = no;
}
위 코드에서 ?는 변수가 null 값을 가질 수 있음을 나타내는 옵셔널 마커입니다. 다트 2.12 버전부터는 널 안정성이 도입되었기 때문에, 변수에 null 값을 허용하려면 변수 타입 뒤에 ?를 붙여야 합니다. 만약 해당 변수가 null일 경우에만 특정 속성이나 메소드를 접근하려면 ?. 연산자를 사용합니다. 이 경우, 변수가 null이 아닐 때만 해당 속성이나 메소드에 접근하게 됩니다.
다트의 타입 클래스
다트는 다양한 기본 데이터 타입을 제공합니다:
- bool: 불리언 값으로, true 또는 false 값을 가질 수 있습니다.
- double: 실수 값을 표현합니다.
- int: 정수 값을 표현합니다.
- num: 숫자를 표현하는 클래스로, double과 int의 상위 클래스입니다.
- String: 문자열을 표현합니다.
dart:typed_data 라이브러리에는 ByteData 타입도 포함되어 있어 바이트 데이터를 다룰 수 있습니다.
dartpad.dev에서 다음 코드를 실행하면:
int? no = 10;
main() {
bool? data = no?.isEven;
no = null;
Object? obj = no;
print('$data, $no, $obj');
}
변수 no는 처음에 10이라는 값을 가지지만, main 함수 내에서 null로 변경됩니다. data 변수는 no가 짝수인지 여부를 가리키는 불리언 값이며, 이후 no의 값과 obj 변수의 값을 출력합니다.
문자열 표현하기
다트에서 문자열은 String 클래스로 표현됩니다. 문자열을 생성할 때는 작은따옴표('hello'), 큰따옴표("hello"), 또는 삼중 따옴표('''hello''' 또는 """hello""")를 사용할 수 있습니다.
두 문자열의 내용이 동일한지 비교할 때는 == 연산자를 사용합니다. 다트에서 == 연산자는 문자열의 내용이 동일한지를 검사합니다.
문자열 내에 변수나 표현식의 값을 포함하려면 ${표현식} 형태의 문자열 템플릿을 사용할 수 있습니다.
예시:
main() {
String str1 = 'hello1';
String str2 = 'hello'+'1';
print(str1==str2); // true
}
위 예시에서 두 문자열 str1과 str2는 같은 값을 가지므로 print 함수는 true를 출력합니다.
형 변환하기
다트에서 모든 변수는 객체이므로 자동으로 형 변환이 이루어지지 않습니다. 명시적으로 형 변환을 하려면, 해당 타입의 생성자나 변환 메소드를 사용해야 합니다. 예를 들면, 문자열을 정수로 변환하려면 int.parse(문자열)을 사용하고, 정수를 문자열로 변환하려면 정수.toString()을 사용합니다.
2. 상수 변수 - const, final
다트에서 상수 변수는 변경 불가능한 값을 저장하는 변수를 의미합니다. 다트에는 두 가지 종류의 상수 변수가 있습니다: const와 final.
컴파일 타임 상수 변수 - const
const piValue = 3.141592;
const 예약어로 선언된 변수는 컴파일 타임에 그 값이 결정되어야 합니다. 이는 const 변수가 프로그램 실행 중에는 그 값을 변경할 수 없다는 것을 의미합니다. 또한, 클래스 내에서 const 변수를 선언할 때는 반드시 static 키워드와 함께 사용해야 합니다.
주의: const 변수의 값은 컴파일 타임에 반드시 결정되어야 하기 때문에, 실행 시간에 값이 결정되는 표현식을 사용할 수 없습니다.
런 타임 상수 변수 - final
final currentTime = DateTime.now();
final 예약어로 선언된 변수는 처음 값을 할당 받은 후에는 그 값을 변경할 수 없습니다. 그러나 그 값은 실행 시간에 결정될 수 있습니다.
상수 변수와 문자열 템플릿
const name = 'John';
const greeting = 'Hello, $name'; // 템플릿 내부의 값은 컴파일 타임 상수
const로 선언된 문자열 상수 변수에 값을 할당할 때 문자열 템플릿을 사용하려면, 템플릿 내의 값도 반드시 컴파일 타임 상수여야 합니다. 그렇지 않으면 오류가 발생합니다.
3. var와 dynamic 타입
다트에서는 변수의 타입을 명시적으로 선언하지 않아도 됩니다. 그러나 이 경우 변수에 어떤 종류의 데이터를 할당할 것인지를 컴파일러가 유추해야 합니다.
타입 유추 - var
var number = 10; // int 타입으로 추측
var 예약어를 사용하여 변수를 선언하면, 그 변수의 초기 값에 따라 컴파일러가 타입을 자동으로 추측합니다. 초기값이 할당되면 해당 변수의 타입이 고정됩니다.
모든 타입 지원 - dynamic
dynamic anything = 10;
anything = 'hello'; // 문자열로 변경 가능
dynamic 타입은 모든 종류의 데이터를 할당할 수 있는 변수를 선언할 때 사용합니다. 이는 dynamic 변수가 어떤 타입의 데이터도 수용할 수 있음을 의미합니다. 이것은 다른 언어의 타입 없음(Type-less) 특성과 유사합니다.
4. 컬렉션 타입 - List, Set, Map
리스트 타입
- List는 데이터를 여러 개 저장하고 인덱스값으로 데이터를 이용하는 컬렉션 타입입니다. 리스트를 선언하면서 초기화할 때는 대괄호([])를 이용합니다.
var numbers = [1, 2, 3, 4, 5];
→ 실행결과: [1, 2, 3, 4, 5]
- List에 타입을 지정하지 않으면 모든 타입을 요소로 저장할 수 있습니다. 특정한 타입의 데이터만 저장하는 리스트를 선언할 때는 데이터 타입을 제네릭으로 명시합니다.
List<int> integers = [1, 2, 3];
→ Generic이란? 제네릭은 다양한 데이터 타입에 동작하는 클래스나 함수의 타입을 명시할 때 사용합니다.
- 데이터를 추가하거나 제거하려면 add()나 removeAt() 함수를 이용합니다.
numbers.add(6);
numbers.removeAt(0);
- filled(), generate()는 List 클래스에 선언된 생성자입니다. 데이터를 몇 개 저장할지 크기를 지정할 수 있습니다.
var filledList = List.filled(5, 'value');
- 처음에 지정한 크기보다 많은 데이터를 저장할 수 있도록 허용하려면 filled() 생성자에 growable 매개변수를 true로 지정합니다.
var growableList = List.filled(5, 'value', growable: true);
- 리스트를 초기화할 때 특정한 로직으로 구성된 데이터를 지정합니다.
var logicList = List.generate(5, (index) => index * 2);
→ 실행결과: [0, 2, 4, 6, 8]
집합 타입
- Set은 List와 마찬가지로 여러 건의 데이터를 저장하는 컬렉션 타입입니다. 리스트와 차이가 있다면 중복 데이터를 허용하지 않습니다.
var uniqueNumbers = {1, 2, 3, 4, 5, 5};
→ 실행결과: {1, 2, 3, 4, 5}
맵 타입
- Map은 여러 건의 데이터를 키와 값 형태로 저장하는 타입입니다.
var fruits = {
'apple': 'red',
'banana': 'yellow',
'grape': 'purple'
};
→ 실행결과: {apple: red, banana: yellow, grape: purple}
5. 널 포인트 예외 관리하기
널 안전성이란?
다트 언어는 최근 널 안전성(null safety)를 도입하여 프로그래밍의 안정성을 높였습니다. 널 안전성(null safety)란 코드를 작성하는 시점에서 널 포인트 예외(NPE: null point exception)의 가능성을 컴파일러가 미리 점검하는 것을 의미합니다. NPE 발생 가능성을 미리 점검하면, 프로그램 실행 전에 널에 안전한 코드를 작성할 수 있습니다.
→ 널 안전성은 자주 사용되므로 매우 중요합니다. 초기에는 어렵게 느껴질 수 있지만 Quick Fix 기능을 이용하면 쉽게 코드를 수정할 수 있습니다. 그렇지만 그 원리를 이해하는 것은 중요합니다.
널 허용과 널 불허
다트에서는 변수를 선언할 때 기본적으로 널 불허로 선언됩니다. 널 허용 변수로 선언하려면 타입 뒤에 물음표 ?를 추가합니다.
int nonNullVar; // 널 불허 변수
int? nullVar; // 널 허용 변수
int nonNullInt = 5; // 널 불허
int? nullInt = null; // 널 허용
→ 대부분의 프로그래밍 언어에서는 이러한 구분이 없었기 때문에, 다트를 처음 접하는 사용자들에게는 조금 생소하게 느껴질 수 있습니다.
널 불허 변수의 초기화
널 불허 변수는 반드시 초기화되어야 합니다. 초기화되지 않은 변수를 사용하면 컴파일 오류가 발생합니다.
int a1;
a1 = 5; // 값을 대입하고 사용
print(a1);
int a2;
print(a2); // 오류! 초기화되지 않은 변수를 사용
var 타입의 널 안전성
var로 선언한 변수는 널 허용 여부를 대입하는 값에 따라 컴파일러가 자동으로 결정합니다. 그렇기 때문에 var? 형태의 선언은 허용되지 않습니다.
var varNotNull = 10; // 널 불허
var varNull = null; // 널 허용
var? errorVar = null; // 오류! var 타입에는 ?를 붙일 수 없습니다.
dynamic 타입의 널 안전성
dynamic 타입은 모든 타입의 데이터를 저장할 수 있으므로 널 허용입니다.
dynamic dynamicNotNull = 10;
dynamic dynamicNull = null;
dynamic? unnecessary = null; // 불필요한 ? 사용, 경고가 발생할 수 있습니다.
dynamic dynamicData = "Hello";
dynamic? a3 = null; // 경고! ?를 사용하는 것은 의미가 없습니다.
널 안전성과 형 변환
Nullable 타입은 NonNull 타입의 상위 타입으로 간주됩니다. 따라서 다트에서는 명시적 형 변환 연산자 as를 사용하여 타입 변환을 수행합니다.
int num = 10;
String str = num as String; // 오류! int와 String 사이의 형 변환이 허용되지 않습니다.
Object o = "String";
String s = o as String; // 명시적 형 변환
초기화를 미루는 late 연산자
late 연산자를 사용하면 변수의 초기화를 미룰 수 있습니다. 하지만 반드시 나중에 해당 변수를 초기화해야 합니다.
late int delayedInit;
delayedInit = 5; // 나중에 값 할당
print(delayedInit); // 출력: 5
→ late 연산자는 변수를 선언할 때 초기화하지 않아도 되지만, 해당 변수를 사용하기 전에 반드시 초기화해야 합니다. 초기화하지 않고 변수를 사용하면 런타임 오류가 발생합니다.
널 안전성은 다트의 중요한 특징 중 하나로, 코드의 안정성과 신뢰성을 높이는 데 큰 역할을 합니다. 이 기능을 제대로 활용하면 런타임 중에 발생할 수 있는 많은 오류들을 사전에 예방할 수 있습니다.
6. Null 안전성 연산자
Null 확인 - ! 연산자
! 연산자를 사용하면, 변수의 값이 null일 경우 런타임 오류를 발생시킵니다.
예시: 함수에 ! 연산자 사용
int? testFunction(arg){
if(arg == 15){
return 5;
} else{
return null;
}
}
main(){
int x = testFunction(15)!;
print('x: $x'); // 출력: x: 5
int y = testFunction(25)!; // testFunction() 함수가 null을 반환하므로 런타임 오류
print('y: $y');
}
예시: Null 확인
int? a2 = 25;
main(){
a2!;
a2 = null;
a2!; // 런타임 오류 발생
}
두번째 예시에서, testFunction(25)은 null을 반환하기 때문에 int y는 null을 저장할 수 없습니다.
문제 해결:
String? message = "world";
main() {
message.isEmpty;
}
위 코드에서는 에러가 발생합니다.
해결방법
message 변수는 null이 될 수 있으므로, null 조건 연산자 ?. 또는 null값 보증 연산자 !를 사용해야 합니다.
예:
message?.isEmpty;
또는
message!.isEmpty;
멤버 접근 - ?. 연산자와 ?[] 연산자
null이 될 수 있는 객체나 리스트의 멤버에 접근할 때 ?. 또는 ?[] 연산자를 사용합니다. 이 연산자들은 변수나 리스트가 null일 경우 오류를 발생시키지 않고 안전하게 접근할 수 있게 해줍니다.
main() {
List<int>? numbers = [15, 25, 35];
print('numbers[1]: ${numbers[1]}');
numbers = null;
print('numbers[1]: ${numbers?[1]}'); // ?를 삭제하면 오류 발생
}
왼쪽의 이미지를 참고하면:
- no1이 null인지 먼저 확인합니다.
- 만약 no1이 null이면, result1의 값은 null이 됩니다.
- no1이 null이 아니라면, isEven을 확인합니다.
- isEven의 결과에 따라 result1의 값을 결정합니다.
이렇게 null safety 연산자를 사용하면, 변수나 리스트가 null일 경우 프로그램이 중단되는 대신 안전하게 처리할 수 있습니다.
대체 값 지정 - ?? 연산자
null이 될 수 있는 변수가 null일 때 다른 값을 지정하려면 ?? 연산자를 사용합니다.
예:
String? info = null;
String response = info ?? "hello";
print(response); // 출력: hello
info가 null일 경우 "hello"로 대체됩니다.
값 할당 - ??= 연산자
null이 될 수 있는 변수에 null이 아닌 값을 할당하려면 ??= 연산자를 사용합니다.
예:
int? data4;
data4 ??= 15;
print(data4); // 출력: 15
data4가 null이면 15를 할당합니다.
결론
Dart 언어에서 const와 Sound Null Safe는 코드의 효율성과 안정성을 높이는 중요한 도구로 여겨집니다. 특히 Sound Null Safe는 개발 과정 중 발생할 수 있는 다양한 오류를 사전에 감지하고 수정하는 데 큰 도움을 줍니다. 이와 마찬가지로, const는 실행 속도를 최적화하는 데 중점을 둔 도구입니다.
그러나 이러한 도구들은 초기의 코드 작성 과정을 복잡하게 만들 수 있습니다. 특히, 여러 오류와 경고 메시지를 해석하고 적절한 수정을 하는 과정은 개발자에게 큰 부담으로 작용할 수 있습니다. 이러한 상황에서는 오류 메시지를 꼼꼼히 읽어보는 것이 중요하며, 이를 통해 필요한 수정 사항을 정확히 파악하는 데 도움을 받을 수 있습니다.
VSCode와 같은 통합 개발 환경(IDE)의 도구, 특히 Quick Fix 기능은 이러한 복잡한 과정을 조금 더 간소화하는 데 큰 도움을 줍니다. 하지만 이러한 자동화된 도구를 효과적으로 활용하기 위해서는 Dart의 핵심 개념에 대한 충분한 이해가 필요합니다. 이를 통해, 개발자는 다양한 문제 상황에 빠르고 정확하게 대응할 수 있게 됩니다.