Dart는 Flutter에서 주로 사용하는 언어이고, Kotlin은 Android Native 개발에서 주로 사용하는 언어다.
둘 다 모바일 앱 개발에 많이 사용되지만 UI를 구성하는 방식과 문법에는 차이가 있다.
Dart
- Flutter 앱 개발에 사용
- Widget 중심 UI 구성
- Android, iOS, Web, Desktop 개발 가능
Kotlin
- Android Native 앱 개발에 사용
- View 또는 Compose 중심 UI 구성
- Android 개발에 최적화
이번 글에서는 Dart와 Kotlin을 다음 기준으로 비교한다.
1. 위젯과 뷰
2. 위젯 트리와 뷰 트리
3. 변수 선언
4. Null Safety
5. dynamic과 Any
6. late, lateinit, lazy
7. const, final, val
Flutter에서 UI를 구성하는 기본 단위는 Widget이다.
Android XML 기반 개발에서는 TextView, ImageView, Button 같은 View를 사용한다.
Flutter / Dart
- Text
- Image
- Icon
- ElevatedButton
- Column
- Row
Android / Kotlin
- TextView
- ImageView
- Button
- LinearLayout
- ConstraintLayout
즉, Flutter에서는 화면에 보이는 모든 요소를 위젯으로 표현한다.
Text('Hello Flutter')
Android XML에서는 다음처럼 View를 사용한다.
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hello Android" />
정리하면 다음과 같다.
Dart / Flutter
- UI 요소를 Widget으로 구성
Kotlin / Android XML
- UI 요소를 View로 구성
Flutter에서는 화면에 보이는 요소뿐만 아니라 정렬, 여백, 레이아웃도 모두 위젯이다.
Flutter에서 Widget인 것
- Text
- Image
- Button
- Padding
- Center
- Column
- Row
Flutter에서는 여러 위젯이 부모-자식 관계로 연결된다.
이 구조를 위젯 트리라고 한다.
MaterialApp
|
v
Scaffold
|
v
Column
|
|-- Text
|-- TextField
|-- ElevatedButton
예시는 다음과 같다.
MaterialApp(
home: Scaffold(
appBar: AppBar(
title: const Text('Login'),
),
body: Column(
children: const [
Text('아이디'),
Text('비밀번호'),
],
),
),
)
위 코드는 다음과 같은 위젯 트리 구조를 가진다.
MaterialApp
|
v
Scaffold
|
|-- AppBar
| |
| v
| Text
|
v
Column
|
|-- Text
|-- Text
위젯 트리의 장점은 다음과 같다.
위젯 트리 장점
- UI 구조를 코드로 쉽게 표현 가능
- 위젯 단위로 재사용 가능
- 상태가 있는 위젯과 없는 위젯을 구분 가능
- 변경된 부분 중심으로 UI 업데이트 가능
- Hot Reload로 빠르게 화면 확인 가능
Android XML 기반 개발에서는 여러 View가 부모-자식 관계로 연결된다.
이 구조를 뷰 트리라고 한다.
ConstraintLayout
|
|-- TextView
|-- EditText
|-- Button
예시는 다음과 같다.
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="아이디" />
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="로그인" />
</LinearLayout>
Kotlin 코드에서는 XML에 정의한 View를 가져와서 동작을 연결한다.
val loginButton = findViewById<Button>(R.id.loginButton)
loginButton.setOnClickListener {
println("로그인 버튼 클릭")
}
흐름은 다음과 같다.
XML에서 UI 정의
|
v
Activity 또는 Fragment에서 View 찾기
|
v
Kotlin 코드로 이벤트 연결
정리하면 다음과 같다.
Flutter
- Dart 코드에서 Widget으로 UI 구성
Android XML
- XML에서 View 구성
- Kotlin에서 View 동작 처리
Dart와 Kotlin의 UI 구조를 비교하면 다음과 같다.
Flutter Widget Tree
- UI를 Dart 코드로 선언
- Widget 중심
- 상태 변화에 따라 build 재실행
- Hot Reload에 강점
Android View Tree
- XML 또는 Kotlin 코드로 UI 구성
- View 중심
- Activity / Fragment에서 View 제어
- XML 기반에서는 UI와 로직이 나뉘는 경우가 많음
간단히 보면 다음과 같다.
Dart / Flutter
Widget을 조합해서 화면을 만든다.
Kotlin / Android XML
View를 XML에 배치하고 Kotlin에서 제어한다.
다만 Kotlin도 Jetpack Compose를 사용하면 Flutter처럼 선언형 UI 방식으로 작성할 수 있다.
@Composable
fun LoginScreen() {
Column {
Text(text = "아이디")
Button(onClick = {}) {
Text(text = "로그인")
}
}
}
즉, 비교 기준에 따라 다음처럼 볼 수 있다.
Flutter
- Widget Tree
Android XML
- View Tree
Android Compose
- Composable Tree
Dart와 Kotlin은 변수 선언 방식이 다르다.
Dart는 타입을 변수 이름 앞에 쓴다.
String name = 'hyunsoo';
int age = 24;
Kotlin은 타입을 변수 이름 뒤에 쓴다.
val name: String = "hyunsoo"
val age: Int = 24
정리하면 다음과 같다.
Dart
- 타입을 변수 이름 앞에 작성
- String name
Kotlin
- 타입을 변수 이름 뒤에 작성
- val name: String
Dart와 Kotlin 모두 타입 추론을 지원한다.
즉, 타입을 직접 쓰지 않아도 컴파일러가 타입을 추론할 수 있다.
var name = 'hyunsoo';
final age = 24;
Dart에서 var는 값을 변경할 수 있다.
var name = 'hyunsoo';
name = 'hyunsoo24'; // 가능
final은 한 번 값을 넣으면 변경할 수 없다.
final age = 24;
age = 25; // 에러
var name = "hyunsoo"
val age = 24
Kotlin에서 var는 값을 변경할 수 있다.
var name = "hyunsoo"
name = "hyunsoo24" // 가능
val은 한 번 값을 넣으면 변경할 수 없다.
val age = 24
age = 25 // 에러
차이를 정리하면 다음과 같다.
Dart
- var: 값 변경 가능
- final: 값 변경 불가능
Kotlin
- var: 값 변경 가능
- val: 값 변경 불가능
즉, Dart의 final은 Kotlin의 val과 비슷하게 이해할 수 있다.
Dart final
≈ Kotlin val
Dart와 Kotlin 모두 Null Safety를 지원한다.
기본적으로 null이 들어갈 수 없는 타입과 null이 들어갈 수 있는 타입을 구분한다.
String name = 'hyunsoo';
String? nickname = null;
val name: String = "hyunsoo"
val nickname: String? = null
둘 다 ?를 붙이면 null이 가능한 타입이 된다.
String
- null 불가능
String?
- null 가능
null 가능 타입을 사용할 때는 null 체크를 해야 한다.
String? name = null;
print(name?.length);
val name: String? = null
println(name?.length)
?.는 값이 null이 아닐 때만 접근한다.
name이 null이 아니다
-> length 접근
name이 null이다
-> null 반환
정리하면 다음과 같다.
Dart와 Kotlin 공통점
- ?를 붙이면 null 가능
- ?.로 안전하게 접근 가능
- null 관련 오류를 줄이기 위해 사용
Dart에는 dynamic이 있다.
dynamic은 어떤 타입이든 들어올 수 있는 변수다.
dynamic value;
value = 'hello';
value = 123;
value = true;
하지만 Kotlin에는 Dart의 dynamic과 완전히 같은 일반적인 개념은 없다.
대신 어떤 타입이든 담고 싶을 때는 Any를 사용할 수 있다.
var value: Any = "hello"
value = 123
value = true
Dart의 dynamic과 Kotlin의 Any는 비슷해 보이지만 차이가 있다.
Dart dynamic
- 컴파일 시 타입 검사를 많이 우회함
- 존재하지 않는 메서드를 호출해도 컴파일 단계에서 놓칠 수 있음
- 런타임 에러 가능성이 커짐
Kotlin Any
- 모든 타입의 최상위 타입
- 사용하려면 타입 체크나 캐스팅이 필요함
- Dart dynamic보다 타입 안정성이 높음
예를 들어 Dart에서는 다음 코드가 컴파일될 수 있지만 런타임에서 에러가 날 수 있다.
dynamic value = 123;
print(value.length); // 런타임 에러 가능
Kotlin에서는 Any 타입에서 바로 length를 사용할 수 없다.
val value: Any = 123
// value.length // 컴파일 에러
타입 확인 후 사용해야 한다.
val value: Any = "hello"
if (value is String) {
println(value.length)
}
정리하면 다음과 같다.
Dart dynamic
- 매우 유연하지만 위험할 수 있음
Kotlin Any
- 모든 타입을 담을 수 있지만 타입 확인이 필요함
Dart와 Kotlin 모두 변수를 나중에 초기화하는 방법이 있다.
하지만 문법과 사용 상황이 다르다.
Dart에서는 late를 사용한다.
late String name;
void main() {
name = 'hyunsoo';
print(name);
}
late는 선언 시점에는 값을 넣지 않지만, 사용하기 전에는 반드시 값을 넣겠다는 의미다.
Dart late
- 나중에 초기화
- null이 아닌 타입에도 사용 가능
- 초기화 전에 사용하면 에러 발생
Kotlin에서는 lateinit을 사용할 수 있다.
lateinit var name: String
fun main() {
name = "hyunsoo"
println(name)
}
lateinit은 다음 조건에서 사용할 수 있다.
Kotlin lateinit
- var에만 사용 가능
- null이 아닌 타입에 사용
- primitive 타입에는 사용 불가
- 클래스 프로퍼티에서 주로 사용
- 초기화 전에 사용하면 에러 발생
예를 들어 Android에서 View나 객체를 나중에 초기화할 때 사용할 수 있다.
private lateinit var adapter: UserAdapter
Kotlin의 lazy는 처음 사용할 때 값을 초기화한다.
val name: String by lazy {
"hyunsoo"
}
lazy는 값 생성 비용이 크거나, 실제로 사용될 때까지 초기화를 미루고 싶을 때 사용한다.
Kotlin lazy
- val에 사용
- 처음 접근할 때 초기화
- 한 번 초기화되면 같은 값 재사용
- 무거운 객체 생성 지연에 유용
정리하면 다음과 같다.
Dart
- late
Kotlin
- lateinit
- lazy
사용 기준은 다음과 같다.
Dart late
- 나중에 값을 넣을 변수
Kotlin lateinit
- 클래스 프로퍼티를 나중에 초기화할 때
- var만 가능
Kotlin lazy
- 처음 사용할 때 초기화하고 싶을 때
- val에 사용
Dart의 const는 컴파일 전에 이미 알고 있는 값을 의미한다.
const pi = 3.14;
Dart의 final은 실행 중에 값이 결정되어도 되지만, 한 번 정하면 변경할 수 없다.
final now = DateTime.now();
DateTime.now()는 실행 중에 알 수 있는 값이므로 const로 만들 수 없다.
const now = DateTime.now(); // 에러
Kotlin에서는 변경 불가능한 변수에 val을 사용한다.
val name = "hyunsoo"
그리고 컴파일 타임 상수에는 const val을 사용한다.
const val BASE_URL = "https://example.com"
const val은 보통 클래스 밖, object 안, companion object 안에서 사용한다.
object ApiConfig {
const val BASE_URL = "https://example.com"
}
정리하면 다음과 같다.
Dart final
- 한 번만 할당 가능
- 실행 중 결정되는 값 가능
Dart const
- 컴파일 전에 값이 정해져 있어야 함
Kotlin val
- 한 번만 할당 가능
- 실행 중 결정되는 값 가능
Kotlin const val
- 컴파일 전에 값이 정해져 있어야 함
비슷한 개념끼리 비교하면 다음과 같다.
Dart final
≈ Kotlin val
Dart const
≈ Kotlin const val
Dart와 Kotlin은 둘 다 모바일 앱 개발에 많이 사용되는 언어지만 사용하는 환경과 UI 구성 방식이 다르다.
Dart
- Flutter 개발에 사용
- Widget Tree 기반 UI
- Android, iOS, Web, Desktop 지원
Kotlin
- Android Native 개발에 사용
- XML 기반 View Tree 또는 Compose 기반 UI
- Android 개발에 최적화
문법 비교는 다음과 같다.
변수 선언 위치
Dart
- String name = 'hyunsoo';
Kotlin
- val name: String = "hyunsoo"
값 변경 가능 여부
Dart
- var: 변경 가능
- final: 변경 불가능
Kotlin
- var: 변경 가능
- val: 변경 불가능
Null Safety
Dart
- String?
- name?.length
Kotlin
- String?
- name?.length
유연한 타입
Dart
- dynamic
Kotlin
- Any
나중에 초기화
Dart
- late
Kotlin
- lateinit
- lazy
컴파일 타임 상수
Dart
- const
Kotlin
- const val
Dart와 Kotlin의 핵심 차이는 다음과 같다.
Dart
- Flutter에서 사용
- 모든 UI를 Widget으로 구성
- Widget Tree 기반
- var, final, const, late 사용
- dynamic 제공
Kotlin
- Android Native에서 사용
- XML View 또는 Compose로 UI 구성
- View Tree 또는 Composable Tree 기반
- var, val, const val, lateinit, lazy 사용
- Any 제공
간단히 말하면 다음과 같다.
Flutter를 공부한다
-> Dart 문법과 Widget Tree 이해가 중요
Android Native를 공부한다
-> Kotlin 문법과 View / Compose 구조 이해가 중요
Dart와 Kotlin은 문법이 비슷한 부분도 많지만,
UI를 구성하는 방식과 일부 키워드의 의미가 다르기 때문에 비교하면서 공부하면 더 이해하기 쉽다.