개요
Flutter/Dart 개발 과정에서 발생하는 실수들은 대부분 패턴이 있습니다. 이 글은 실제 1개월간의 Flutter 학습 과정에서 겪은 구체적인 실수들과 그 해결 방법을 체계적으로 정리한 실무 가이드입니다. 각 실수 사례는 중요도별로 분류하여 우선순위를 명확히 하였으며, 실제 코드 예시와 함께 올바른 해결 방법을 제시합니다.
목차
- 데이터 저장 및 처리 실수 방지
- UI 및 Widget 관련 실수 방지
- Navigation 및 생명주기 관리
- 네트워크 및 WebView 처리
- 비동기 처리 및 검색 최적화
- 수학 계산 및 타입 처리
- 에러 처리 및 사용자 피드백
- 실무 적용 베스트 프랙티스
데이터 저장 및 처리 실수 방지
SharedPreferences 올바른 사용법
중요도: HIGH - 반드시 기억해야 할 핵심
많은 개발자들이 SharedPreferences를 사용할 때 불필요하게 JSON 인코딩/디코딩을 수행하는 실수를 범합니다. 단순 String 데이터의 경우 직접 저장이 가능하며, 이는 성능과 코드 가독성 모두에 도움이 됩니다.
잘못된 구현 사례
// ❌ 불필요한 JSON 처리
Future<void> saveUserTheme(String theme) async {
final prefs = await SharedPreferences.getInstance();
var jsonEncode2 = jsonEncode(theme); // 단순 String에는 불필요
await prefs.setString('user_theme', jsonEncode2);
}
Future<String> getUserTheme() async {
final prefs = await SharedPreferences.getInstance();
final storedTheme = prefs.getString('user_theme');
var jsonDecode2 = jsonDecode(storedTheme ?? "light"); // 불필요
return jsonDecode2;
}
올바른 구현 방법
// ✅ 직접 저장 방식
Future<void> saveUserTheme(String theme) async {
final prefs = await SharedPreferences.getInstance();
if (theme.isNotEmpty) {
await prefs.setString('user_theme', theme); // 바로 저장
}
}
Future<String> getUserTheme() async {
final prefs = await SharedPreferences.getInstance();
return prefs.getString('user_theme') ?? 'light'; // 기본값 제공
}
핵심 포인트: 단순 String 데이터는 JSON 변환 없이 바로 저장/조회가 가능하며, 이는 성능상 이점을 제공합니다.
Map 데이터 구조 안전한 업데이트
중요도: HIGH - 실무에서 자주 사용
Map 데이터를 업데이트할 때 조건 검사의 대상을 잘못 설정하는 실수가 빈번히 발생합니다. 기존 값이 아닌 새로 들어오는 값의 null 여부를 확인해야 합니다.
잘못된 구현 사례
// ❌ 조건이 반대로 설정됨
void updateProfile(Map<String, dynamic> newData) {
newData.forEach((key, value) {
if (currentProfile[key] != null) { // 기존 값 확인 (잘못됨)
currentProfile[key] = value;
}
});
}
올바른 구현 방법
// ✅ 새로운 값의 null 여부 확인
void updateProfile(Map<String, dynamic> newData) {
newData.forEach((key, value) {
if (value != null) { // 새로운 값이 null이 아닐 때만 업데이트
currentProfile[key] = value;
}
});
}
핵심 포인트: 새로 들어오는 값의 null 여부를 확인하여 유효한 데이터만 업데이트해야 합니다.
JSON 파싱 중첩 구조 처리
중요도: HIGH - JSON API 연동 시 필수
중첩된 JSON 구조를 처리할 때 null 체크 없이 직접 접근하면 런타임 에러가 발생할 수 있습니다. 안전한 중첩 접근 패턴을 구현해야 합니다.
// ✅ 안전한 중첩 JSON 파싱
Map<String, dynamic> parseUserData(String jsonString) {
try {
var data = jsonDecode(jsonString);
var address = data["address"];
return {
'id': data["id"],
'name': data["name"],
'email': data["email"],
'city': address != null ? address["city"] : null, // 중첩 처리
};
} catch (e) {
print('JSON 파싱 오류: $e');
return {};
}
}
핵심 포인트: 중첩 객체는 null 체크 후 접근하며, try-catch 블록을 활용한 예외 처리가 필수입니다.
UI 및 Widget 관련 실수 방지
Size와 EdgeInsets 크기 계산
중요도: MEDIUM - 실무에서 자주 사용
UI 레이아웃에서 크기 계산 시 EdgeInsets의 속성을 정확히 이해하지 못해 발생하는 실수들이 있습니다.
// EdgeInsets 속성 이해
// padding.left + padding.right == padding.horizontal // 좌우 합
// padding.top + padding.bottom == padding.vertical // 위아래 합
// 크기 계산 함수
Size calculateChildSize(Size parentSize, EdgeInsets padding) {
double width = parentSize.width - padding.horizontal; // 좌우 패딩 제거
double height = parentSize.height - padding.vertical; // 위아래 패딩 제거
return Size(width, height);
}
핵심 포인트: horizontal은 좌우 패딩의 합, vertical은 위아래 패딩의 합을 의미합니다.
Padding vs Container 성능 최적화
중요도: MEDIUM - 성능 최적화 관련
단순한 여백 처리 시 Container 대신 Padding을 사용하면 성능상 이점을 얻을 수 있습니다.
// ✅ 성능 최적화를 고려한 위젯 선택
Widget createOptimalPadding(Widget child, double padding, {Color? backgroundColor}) {
if (backgroundColor == null) {
return Padding( // 단순 여백만 필요
padding: EdgeInsets.all(padding),
child: child,
);
} else {
return Container( // 배경색 등 추가 기능 필요
padding: EdgeInsets.all(padding),
color: backgroundColor,
child: child,
);
}
}
핵심 포인트: 단순 여백은 Padding, 복합 기능이 필요한 경우 Container를 사용하여 성능을 최적화할 수 있습니다.
Navigation 및 생명주기 관리
Navigator 안전장치 구현
중요도: HIGH - 앱 크래시 방지
Navigator 사용 시 뒤로 갈 페이지가 없을 때 발생하는 크래시를 방지하기 위한 안전장치 구현이 필요합니다.
// ✅ 안전한 Navigation 패턴
// 방법 1: 수동 확인 후 처리
bool safeGoBack(BuildContext context) {
if (Navigator.canPop(context)) { // 뒤로 갈 수 있는지 확인
Navigator.pop(context); // 안전하게 뒤로가기
return true;
}
return false;
}
// 방법 2: 자동 확인 후 처리
void smartGoBack(BuildContext context) {
Navigator.of(context).maybePop(); // 내부적으로 canPop 체크 후 처리
}
핵심 포인트: canPop()으로 확인 후 pop() 실행하거나, maybePop()으로 안전하게 처리할 수 있습니다.
StatefulWidget 생명주기와 리소스 관리
중요도: HIGH - 메모리 누수 방지
StatefulWidget의 생명주기를 올바르게 이해하지 못하면 메모리 누수나 앱 성능 저하가 발생할 수 있습니다.
잘못된 구현 사례
// ❌ 잘못된 리소스 관리
class TimerWidget extends StatefulWidget {
Timer? timer; // Widget 클래스에 선언 (잘못됨)
@override
void dispose() {
dispose(); // 직접 호출 (잘못됨)
}
}
올바른 구현 방법
// ✅ 올바른 생명주기 관리
class TimerWidget extends StatefulWidget {
@override
_TimerWidgetState createState() => _TimerWidgetState();
}
class _TimerWidgetState extends State<TimerWidget> {
Timer? timer; // State 클래스에 선언
int count = 0;
@override
void initState() {
super.initState(); // 먼저 호출
timer = Timer.periodic(Duration(seconds: 1), (t) {
setState(() => count++);
});
}
@override
void dispose() {
timer?.cancel(); // 리소스 정리
super.dispose(); // 마지막에 호출
}
@override
Widget build(BuildContext context) {
return Text('$count');
}
}
핵심 포인트: 리소스는 State 클래스에서 관리하며, dispose()는 프레임워크가 자동으로 호출합니다.
네트워크 및 WebView 처리
WebView 패키지 설정과 사용
중요도: MEDIUM - 웹 콘텐츠 연동 시 필수
WebView 사용 시 패키지 설정 과정에서 발생하는 실수들을 방지하기 위한 체계적인 접근이 필요합니다.
필수 설정 단계
# pubspec.yaml
dependencies:
flutter:
sdk: flutter
webview_flutter: ^4.4.2
# 터미널에서 실행
flutter pub get
// 파일 상단에 임포트
import 'package:webview_flutter/webview_flutter.dart';
// URL 패턴에 따른 분기 처리
void navigateToPage(BuildContext context, String url) {
if (url.contains("intranet.company.com")) {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => WebView(
initialUrl: url,
javascriptMode: JavascriptMode.unrestricted,
),
),
);
} else {
// 일반 페이지 처리
print("일반 URL로 이동: $url");
}
}
핵심 포인트: 패키지 추가 → pub get → 임포트 → 분기 처리 순서로 진행해야 합니다.
HTTP 메서드 선택 기준
중요도: LOW - 알아두면 좋은 개념
RESTful API 호출 시 적절한 HTTP 메서드를 선택하는 것이 중요합니다.
// ✅ 시나리오별 HTTP 메서드 선택
String selectHttpMethod(String scenario) {
switch (scenario) {
case "get_user": return "GET"; // 조회
case "create_order": return "POST"; // 생성
case "update_profile": return "PUT"; // 전체 수정
case "delete_post": return "DELETE"; // 삭제
default: return "GET";
}
}
핵심 포인트: 데이터 조회=GET, 생성=POST, 수정=PUT, 삭제=DELETE가 기본 원칙입니다.
비동기 처리 및 검색 최적화
where() 메서드 활용
중요도: MEDIUM - 데이터 필터링 시 필수
컬렉션에서 조건에 맞는 데이터를 찾을 때 where() 메서드를 효과적으로 활용할 수 있습니다.
// ✅ 효율적인 검색 구현
List<Map<String, dynamic>> filterProducts(String query) {
return products.where((product) {
return product["name"]
.toString()
.toLowerCase()
.contains(query.toLowerCase());
}).toList();
}
핵심 포인트: where()로 조건 검색, contains()로 부분 일치 검색이 가능합니다.
async/await 순차 처리 패턴
중요도: MEDIUM - 비동기 처리 최적화
비동기 작업 시 단계별 검증이 필요한 경우와 단순 처리가 필요한 경우를 구분하여 처리해야 합니다.
단순 버전 (동시 처리)
// ✅ 단순 동시 처리
Future<Map<String, dynamic>> initializeUser(String userId) async {
return {
'authenticated': await authenticateUser(userId),
'profile': await loadUserProfile(userId),
'settings': await loadUserSettings(userId),
};
}
실무 버전 (단계별 검증)
// ✅ 단계별 검증 처리
Future<Map<String, dynamic>> initializeUser(String userId) async {
// 1. 인증 먼저
final authenticated = await authenticateUser(userId);
// 2. 인증 실패 시 나머지 생략
if (!authenticated) {
return {'authenticated': false, 'profile': null, 'settings': null};
}
// 3. 인증 성공 시에만 나머지 진행
final profile = await loadUserProfile(userId);
final settings = await loadUserSettings(userId);
return {
'authenticated': authenticated,
'profile': profile,
'settings': settings,
};
}
핵심 포인트: 익숙해지면 한 번에 Map으로 리턴, 실무에서는 단계별 검증이 더 안전합니다.
수학 계산 및 타입 처리
pow() 함수 타입 변환
중요도: MEDIUM - 수학 계산 시 필수
pow() 함수의 반환 타입은 num이므로 double 변수에 직접 할당할 수 없습니다. 적절한 타입 변환이 필요합니다.
잘못된 구현 사례
// ❌ 타입 에러 발생
double future = inputPrice * pow(1 + interest, years);
// ERROR: A value of type 'num' can't be assigned to a variable of type 'double'
올바른 구현 방법
// ✅ 올바른 타입 변환
// 방법 1: 바로 정수 변환
int total = (inputPrice * pow(1 + interest, years)).round();
// 방법 2: 타입 명시 후 변환
double future = inputPrice * (pow(1 + interest, years) as double);
int total = future.round();
// 방법 3: toDouble() 사용
double future = inputPrice * pow(1 + interest, years).toDouble();
int total = future.round();
핵심 포인트: pow() 반환 타입은 num이므로 double 변수에 담으려면 명시적 변환이 필요합니다.
소수점 자리 표시
중요도: LOW - 표시 형식 처리
소수점 자릿수를 제한하여 표시할 때 사용하는 패턴입니다.
// ✅ 소수점 둘째 자리까지 반올림
double discountRate = ((originalPrice - salePrice) / originalPrice) * 100;
return double.parse(discountRate.toStringAsFixed(2));
// 또는 직접 반올림
return (discountRate * 100).round() / 100.0;
핵심 포인트: toStringAsFixed(자릿수) → 문자열 → double.parse() 순서로 처리합니다.
에러 처리 및 사용자 피드백
ScaffoldMessenger 활용
중요도: MEDIUM - 사용자 경험 개선
사용자에게 적절한 피드백을 제공하기 위한 메시지 표시 방법입니다.
// ✅ 성공/실패에 따른 메시지 표시
void showResultMessage(BuildContext context, bool isSuccess, String message) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
message, // 실제 메시지 사용 (하드코딩 아님)
style: TextStyle(
color: isSuccess ? Colors.green : Colors.red,
),
),
duration: isSuccess ? Duration(seconds: 1) : Duration(seconds: 2),
),
);
}
핵심 포인트: 메시지 내용을 제대로 전달하고, 성공/실패에 따른 시각적 구분이 필요합니다.
안전한 데이터 접근
중요도: HIGH - 런타임 에러 방지
Map 데이터에 접근할 때 키 존재 여부와 타입을 확인하는 안전한 접근 패턴입니다.
// ✅ 안전한 데이터 접근 패턴
String getStringValue(Map<String, dynamic> data, String key, String defaultValue) {
if (data.containsKey(key) && data[key] is String) {
return data[key] as String;
}
return defaultValue;
}
핵심 포인트: 키 존재 여부 확인 + 타입 확인 후 안전하게 접근해야 합니다.
실무 적용 베스트 프랙티스
복리 이자 계산 (금융 수학)
실제 금융 애플리케이션에서 사용되는 복리 이자 계산 구현 예시입니다.
import 'dart:math';
// ✅ 복리 이자 계산 함수
int calculateCompoundInterest(int principal, double rate, int years) {
// A = P(1 + r)^t 공식 적용
double result = principal * pow(1 + rate, years);
return result.round(); // 반올림하여 정수 반환
}
// 사용 예시
void main() {
int finalAmount = calculateCompoundInterest(100000, 0.03, 5);
print("5년 후 금액: $finalAmount원");
}
핵심 포인트: 복리 공식 적용 + pow() 타입 주의 + 반올림 처리가 필요합니다.
입력값 검증 패턴
사용자 입력값을 검증하는 체계적인 패턴입니다.
// ✅ 포괄적인 입력값 검증
Map<String, String?> validateForm(String email, String password) {
Map<String, String?> errors = {};
// 이메일 검증
if (email.isEmpty || !email.contains("@")) {
errors['email'] = "올바른 이메일을 입력하세요";
} else {
errors['email'] = null; // 에러 없음
}
// 비밀번호 검증
if (password.length < 8) {
errors['password'] = "비밀번호는 8자 이상이어야 합니다";
} else {
errors['password'] = null;
}
return errors;
}
핵심 포인트: 각 필드별 검증 후 에러 맵 반환 (null = 에러 없음)을 통해 체계적으로 관리할 수 있습니다.
결론 및 학습 방향
피해야 할 핵심 실수 패턴
- StatefulWidget에서 리소스를 Widget 클래스에 선언
- dispose() 직접 호출
- 단순 String에 JSON 인코딩/디코딩 사용
- pow() 결과를 double에 직접 할당
- Map 업데이트 시 기존 값 확인 대신 새 값 확인
지속적인 학습 방법
이 가이드의 내용들을 단순 암기가 아닌 실제 코드에 적용하며 체화하는 것이 중요합니다. 특히 실수했던 부분들은 반복 연습을 통해 완전히 자신의 것으로 만들어야 합니다.
질문과 답변
이 포스트가 도움이 되셨나요?
🔍 기술 토론 참여
댓글로 의견을 나누어주세요:
- 이런 실수들을 경험해보셨나요? 어떻게 해결하셨는지 공유해주세요
- 실제 프로덕션 환경에서 치명적이었던 실수 사례가 있다면 알려주세요
- 코드 리뷰에서 자주 지적받는 다른 실수 패턴들을 공유해주세요
💡 지식 공유
커뮤니티와 함께 성장해요:
- Flutter 버전 업데이트에 따른 새로운 실수 패턴들
- 팀 개발 환경에서의 실수 방지 방법
- 디버깅 효율성을 높이는 실무 팁들
🚀 다음 주제 제안
어떤 실수 방지 가이드를 다뤄드릴까요?
- Flutter 성능 최적화 실수 방지 - 메모리 누수와 렌더링 최적화
- Firebase 연동 실수 방지 - 인증, 데이터베이스, 스토리지 활용
- 앱 스토어 배포 실수 방지 - 빌드 설정과 배포 과정
- Flutter 테스트 작성 실수 방지 - 단위 테스트와 위젯 테스트
가장 궁금한 주제를 댓글로 알려주세요!
참고 자료
관련 포스트
태그: Flutter, Dart, 실무개발, 실수방지, SharedPreferences, Navigator, StatefulWidget, JSON파싱, 타입변환, 비동기처리, 성능최적화, 디버깅, 모바일앱개발, 크로스플랫폼