1. 고급 Dart가 필요한 이유
Flutter 앱을 어느 정도 만들 수 있게 되면 다음 문제가 생깁니다.
- 코드가 길어진다.
- 화면과 데이터 처리가 뒤섞인다.
- API 응답 구조가 복잡해진다.
- 상태값 관리가 어려워진다.
- 비동기 이벤트가 많아진다.
- 성능 문제가 발생한다.
- 오류 처리가 흩어진다.
이때 필요한 것이 Dart 고급 문법입니다.
고급 Dart는 단순히 어려운 문법을 쓰기 위한 것이 아닙니다.
실무에서 안정적이고 유지보수 가능한 Flutter 앱을 만들기 위한 언어적 도구입니다.
2. Generic
Generic은 타입을 나중에 지정할 수 있게 하는 문법입니다.
class Box<T> {
final T value;
Box(this.value);
}
void main() {
final intBox = Box<int>(100);
final stringBox = Box<String>('Dart');
print(intBox.value);
print(stringBox.value);
}
T는 타입을 의미합니다.
Generic을 사용하면 하나의 클래스를 여러 타입에 재사용할 수 있습니다.
실무에서는 API 응답 구조를 만들 때 유용합니다.
class ApiResponse<T> {
final bool success;
final String message;
final T? data;
ApiResponse({
required this.success,
required this.message,
this.data,
});
}
사용 예시는 다음과 같습니다.
final response = ApiResponse<String>(
success: true,
message: '성공',
data: '사용자 데이터',
);
Generic은 코드 재사용성과 타입 안정성을 동시에 높여줍니다.
3. Record
Record는 여러 값을 간단하게 묶어서 반환할 수 있는 문법입니다.
(String, int) getUser() {
return ('Kim', 30);
}
void main() {
final user = getUser();
print(user.$1);
print(user.$2);
}
이름을 붙인 Record도 사용할 수 있습니다.
({String name, int age}) getUser() {
return (name: 'Kim', age: 30);
}
void main() {
final user = getUser();
print(user.name);
print(user.age);
}
간단한 결과를 반환하기 위해 굳이 class를 만들 필요가 없을 때 Record를 사용할 수 있습니다.
예를 들어 로그인 검증 결과를 다음처럼 반환할 수 있습니다.
({bool success, String message}) validateLogin(String id, String password) {
if (id.isEmpty) {
return (success: false, message: '아이디를 입력하세요.');
}
if (password.isEmpty) {
return (success: false, message: '비밀번호를 입력하세요.');
}
return (success: true, message: '검증 성공');
}
4. Pattern Matching
Pattern Matching은 값의 구조를 기준으로 조건을 처리하는 문법입니다.
void checkValue(Object value) {
switch (value) {
case int n:
print('정수: $n');
case String s:
print('문자열: $s');
case bool b:
print('참거짓: $b');
default:
print('알 수 없음');
}
}
Record와 함께 사용하면 더 강력합니다.
void handleResult(({bool success, String message}) result) {
switch (result) {
case (success: true, message: var msg):
print('성공: $msg');
case (success: false, message: var msg):
print('실패: $msg');
}
}
Pattern Matching은 복잡한 조건문을 더 명확하게 정리할 수 있게 해줍니다.
5. sealed class
sealed class는 제한된 타입 계층을 만들 때 사용합니다.
상태값을 더 안전하게 표현할 수 있습니다.
sealed class LoginState {}
class LoginInitial extends LoginState {}
class LoginLoading extends LoginState {}
class LoginSuccess extends LoginState {
final String userName;
LoginSuccess(this.userName);
}
class LoginFailure extends LoginState {
final String message;
LoginFailure(this.message);
}
사용 예시는 다음과 같습니다.
void handleLoginState(LoginState state) {
switch (state) {
case LoginInitial():
print('초기 상태');
case LoginLoading():
print('로그인 중');
case LoginSuccess(userName: var name):
print('로그인 성공: $name');
case LoginFailure(message: var msg):
print('로그인 실패: $msg');
}
}
Flutter 상태 관리에서 loading, success, failure 같은 상태를 명확하게 표현할 때 유용합니다.
6. Stream
Future가 한 번의 비동기 결과를 처리한다면, Stream은 여러 번 발생하는 비동기 데이터를 처리합니다.
예를 들면 다음과 같은 경우에 Stream을 사용할 수 있습니다.
- 실시간 채팅
- 센서 데이터 수신
- 위치 정보 변경
- 소켓 데이터 수신
- 파일 업로드 진행률
- 타이머
- 알림 이벤트
기본 예시는 다음과 같습니다.
Stream<int> countStream() async* {
for (int i = 1; i <= 5; i++) {
await Future.delayed(Duration(seconds: 1));
yield i;
}
}
void main() async {
await for (final value in countStream()) {
print(value);
}
}
async*는 여러 값을 비동기로 내보낼 때 사용합니다.
yield는 Stream에 값을 전달합니다.
Flutter에서는 StreamBuilder와 함께 실시간 화면 갱신에 사용할 수 있습니다.
7. Isolate
Dart는 기본적으로 하나의 isolate에서 코드를 실행합니다.
무거운 작업을 메인 isolate에서 실행하면 앱 화면이 멈출 수 있습니다.
이미지 처리, 대용량 JSON 파싱, 복잡한 계산, AI 전처리 같은 작업은 별도 isolate로 분리하는 것이 좋습니다.
간단한 예시는 다음과 같습니다.
import 'dart:isolate';
void heavyTask(SendPort sendPort) {
int sum = 0;
for (int i = 0; i < 100000000; i++) {
sum += i;
}
sendPort.send(sum);
}
void main() async {
final receivePort = ReceivePort();
await Isolate.spawn(heavyTask, receivePort.sendPort);
receivePort.listen((message) {
print('결과: $message');
receivePort.close();
});
}
Flutter 앱에서는 사용자가 느끼는 속도가 중요합니다.
무거운 작업 때문에 화면이 버벅이면 앱 품질이 낮아 보입니다.
8. Callable Class
Dart에서는 class를 함수처럼 호출할 수 있습니다.
class Validator {
bool call(String value) {
return value.isNotEmpty;
}
}
void main() {
final validator = Validator();
print(validator('test'));
}
call() 메서드를 정의하면 객체를 함수처럼 사용할 수 있습니다.
검증기, 변환기, 조건 판단 로직 등에 활용할 수 있습니다.
9. cascade operator
Cascade operator ..는 같은 객체에 여러 작업을 연속으로 적용할 때 사용합니다.
class User {
String name = '';
int age = 0;
void show() {
print('$name / $age');
}
}
void main() {
final user = User()
..name = 'Kim'
..age = 30
..show();
}
객체를 생성한 뒤 여러 속성을 설정할 때 코드가 깔끔해집니다.
10. collection if와 collection for
List 안에서 조건문과 반복문을 사용할 수 있습니다.
bool isAdmin = true;
final menus = [
'홈',
'조회',
if (isAdmin) '관리자',
];
반복문도 가능합니다.
final numbers = [1, 2, 3];
final items = [
for (final n in numbers) '번호 $n',
];
print(items);
Flutter 위젯 리스트를 만들 때 자주 사용합니다.
final menuWidgets = [
for (final menu in menus)
Text(menu),
];
11. 예외 처리 구조화
실무 앱에서는 오류 처리를 대충 하면 안 됩니다.
API 오류, 네트워크 오류, 권한 오류, 데이터 오류를 구분해야 합니다.
class AppException implements Exception {
final String message;
AppException(this.message);
@override
String toString() => message;
}
사용 예시는 다음과 같습니다.
Future<void> loadData() async {
try {
throw AppException('서버 연결에 실패했습니다.');
} on AppException catch (e) {
print('앱 오류: ${e.message}');
} catch (e) {
print('알 수 없는 오류: $e');
}
}
오류를 구조화하면 사용자에게 더 정확한 메시지를 보여줄 수 있습니다.
12. 코드 분리 기준
Dart 고급 문법을 잘 쓰는 것만큼 중요한 것이 코드 분리입니다.
Flutter 프로젝트에서는 보통 다음처럼 역할을 나눕니다.
lib/
├─ main.dart
├─ models/
├─ services/
├─ repositories/
├─ screens/
├─ widgets/
├─ utils/
└─ constants/
각 폴더의 역할은 다음과 같습니다.
- models: 데이터 구조
- services: API, DB, 외부 연동
- repositories: 데이터 접근 추상화
- screens: 화면 단위
- widgets: 재사용 UI
- utils: 공통 함수
- constants: 상수값
작은 앱은 단순하게 시작해도 되지만, 앱이 커질수록 구조화가 중요합니다.
13. 실무 Dart 작성 원칙
Dart를 실무에서 사용할 때는 다음 원칙을 지키는 것이 좋습니다.
첫째, 가능한 명확한 타입을 사용합니다.
final String name = 'Kim';
둘째, 변하지 않는 값은 final 또는 const를 사용합니다.
final now = DateTime.now();
const appTitle = 'MES Lite';
셋째, null 가능성은 명확하게 표현합니다.
String? selectedUserId;
넷째, API 데이터는 Model class로 변환합니다.
final user = User.fromJson(json);
다섯째, 복잡한 상태는 enum이나 sealed class로 표현합니다.
enum LoadingStatus {
idle,
loading,
success,
failure,
}
여섯째, 비동기 오류는 반드시 처리합니다.
try {
await service.load();
} catch (e) {
print(e);
}
14. Flutter 개발자가 Dart를 깊게 알아야 하는 이유
Flutter는 위젯 중심의 프레임워크입니다.
하지만 앱의 품질은 결국 Dart 코드의 품질에 따라 달라집니다.
Dart를 제대로 알면 다음이 가능해집니다.
- 앱 구조를 깔끔하게 설계할 수 있다.
- API 연동 코드를 안정적으로 만들 수 있다.
- 비동기 오류를 줄일 수 있다.
- 상태 관리를 명확하게 할 수 있다.
- 성능 문제를 줄일 수 있다.
- 유지보수하기 쉬운 코드를 만들 수 있다.
Flutter를 배우는 사람은 많지만, Dart를 제대로 이해하고 사용하는 사람은 상대적으로 적습니다.
Dart 언어를 제대로 이해하면 Flutter 개발 실력이 확실히 올라갑니다.
15. 고급 단계에서 꼭 기억할 것
Dart 고급 문법의 핵심은 실무 품질입니다.
- Generic은 재사용 가능한 타입 구조를 만든다.
- Record는 간단한 복수 결과 반환에 유용하다.
- Pattern Matching은 복잡한 조건 처리를 명확하게 만든다.
- sealed class는 상태를 안전하게 표현한다.
- Stream은 연속적인 비동기 데이터를 처리한다.
- Isolate는 무거운 작업을 분리해 앱 멈춤을 줄인다.
- collection if와 for는 Flutter 위젯 리스트 작성에 유용하다.
- 예외 처리는 구조화해야 한다.
- 프로젝트 폴더 구조는 앱 규모가 커질수록 중요하다.
마무리
Dart는 단순히 Flutter를 사용하기 위한 부속 언어가 아닙니다.
Dart를 제대로 이해하면 Flutter 앱의 구조, 안정성, 성능, 유지보수성이 모두 좋아집니다.
기초 단계에서는 문법을 익히고, 중급 단계에서는 객체지향과 비동기를 이해하고, 고급 단계에서는 실무 품질을 높이는 방향으로 학습하는 것이 좋습니다.
Flutter 앱 개발을 오래 할 계획이라면 Dart 언어 자체를 반드시 깊게 익혀야 합니다.
'IT > Flutter' 카테고리의 다른 글
| 2회. Flutter 실무에 필요한 Dart 중급 문법 (0) | 2026.06.27 |
|---|---|
| 1회. Flutter를 제대로 배우기 위한 Dart 기초 문법 (0) | 2026.06.27 |
| Flutter 앱 개발 기본 절차 (0) | 2026.06.26 |