📚 학습 기록/Dart & Flutter 기초

Flutter/Dart 실무 개발 실수 방지 가이드: 1개월 학습 과정의 핵심 문제 해결 패턴

zenjoydev 2025. 7. 17. 23:57

개요

Flutter/Dart 개발 과정에서 발생하는 실수들은 대부분 패턴이 있습니다. 이 글은 실제 1개월간의 Flutter 학습 과정에서 겪은 구체적인 실수들과 그 해결 방법을 체계적으로 정리한 실무 가이드입니다. 각 실수 사례는 중요도별로 분류하여 우선순위를 명확히 하였으며, 실제 코드 예시와 함께 올바른 해결 방법을 제시합니다.

목차

  1. 데이터 저장 및 처리 실수 방지
  2. UI 및 Widget 관련 실수 방지
  3. Navigation 및 생명주기 관리
  4. 네트워크 및 WebView 처리
  5. 비동기 처리 및 검색 최적화
  6. 수학 계산 및 타입 처리
  7. 에러 처리 및 사용자 피드백
  8. 실무 적용 베스트 프랙티스

데이터 저장 및 처리 실수 방지

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 = 에러 없음)을 통해 체계적으로 관리할 수 있습니다.

결론 및 학습 방향

피해야 할 핵심 실수 패턴

  1. StatefulWidget에서 리소스를 Widget 클래스에 선언
  2. dispose() 직접 호출
  3. 단순 StringJSON 인코딩/디코딩 사용
  4. pow() 결과를 double에 직접 할당
  5. Map 업데이트 시 기존 값 확인 대신 새 값 확인
  •  

지속적인 학습 방법

이 가이드의 내용들을 단순 암기가 아닌 실제 코드에 적용하며 체화하는 것이 중요합니다. 특히 실수했던 부분들은 반복 연습을 통해 완전히 자신의 것으로 만들어야 합니다.

질문과 답변

이 포스트가 도움이 되셨나요?

🔍 기술 토론 참여

댓글로 의견을 나누어주세요:

  • 이런 실수들을 경험해보셨나요? 어떻게 해결하셨는지 공유해주세요
  • 실제 프로덕션 환경에서 치명적이었던 실수 사례가 있다면 알려주세요
  • 코드 리뷰에서 자주 지적받는 다른 실수 패턴들을 공유해주세요

💡 지식 공유

커뮤니티와 함께 성장해요:

  • Flutter 버전 업데이트에 따른 새로운 실수 패턴들
  • 팀 개발 환경에서의 실수 방지 방법
  • 디버깅 효율성을 높이는 실무 팁들

🚀 다음 주제 제안

어떤 실수 방지 가이드를 다뤄드릴까요?

  1. Flutter 성능 최적화 실수 방지 - 메모리 누수와 렌더링 최적화
  2. Firebase 연동 실수 방지 - 인증, 데이터베이스, 스토리지 활용
  3. 앱 스토어 배포 실수 방지 - 빌드 설정과 배포 과정
  4. Flutter 테스트 작성 실수 방지 - 단위 테스트와 위젯 테스트

가장 궁금한 주제를 댓글로 알려주세요!


참고 자료

관련 포스트

태그: Flutter, Dart, 실무개발, 실수방지, SharedPreferences, Navigator, StatefulWidget, JSON파싱, 타입변환, 비동기처리, 성능최적화, 디버깅, 모바일앱개발, 크로스플랫폼