본문 바로가기

flutter

Dart 기초 마스터하기 #2 함수와 클래스로 코드 구조화하기

반응형

Flutter 개발을 위한 Dart 언어 기초 시리즈 두 번째 시간입니다. 함수 작성부터 객체 지향 프로그래밍까지, 실무에서 꼭 알아야 할 내용들을 다룹니다.

 

이 글에서 배우는 것

  • 함수 선언과 호출의 모든 것
  • void vs 반환값이 있는 함수
  • 클래스와 객체의 기본 개념
  • 생성자의 다양한 형태
  • 상속과 메서드 오버라이드
  • Dart만의 독특한 문법 특징

 

함수(Function): 코드를 재사용 가능하게 만들기

반환값이 없는 함수 (void)

void greetUser(String name, int age) {
  print('안녕하세요! $name님, $age살이시군요.');
  // return 문이 없음 (void이므로)
}

// 사용법
greetUser('홍길동', 25);  // 안녕하세요! 홍길동님, 25살이시군요.

void는 "아무것도 반환하지 않는다"는 의미입니다. 주로 출력, 데이터 수정 등의 작업에 사용합니다.

 

반환값이 있는 함수

int addNumbers(int a, int b) {
  return a + b;  // 계산 결과를 반환
}

// 사용법
int sum = addNumbers(10, 20);
print('10 + 20 = $sum');  // 10 + 20 = 30

 

실용적인 계산 함수 예제

// BMI = 체중(kg) / 키(m)²
double calculateBMI(double weight, double height) {
  double heightInMeters = height / 100;  // cm를 m로 변환
  return weight / (heightInMeters * heightInMeters);
}

// 사용법
double result = calculateBMI(70, 175);
print('BMI: ${result.toStringAsFixed(2)}');  // BMI: 22.86

 

클래스(Class): 객체의 설계도 만들기

클래스는 데이터(속성) + 기능(메서드)를 하나로 묶는 객체 지향 프로그래밍의 핵심입니다.

 

기본 클래스 구조

class Person {
  // 속성(필드) - 클래스가 가지는 데이터
  String name;  // 이름
  int age;      // 나이

  // 생성자 - 객체를 만들 때 호출되는 특별한 함수
  Person(this.name, this.age);

  // 메서드 - 클래스가 가지는 행동/기능
  void introduce() {
    print('안녕하세요! 저는 $name이고, $age살입니다.');
  }

  void haveBirthday() {
    age++;  // 나이 1 증가
    print('생일축하합니다! 이제 $age살이 되었습니다.');
  }
}

 

객체 생성과 사용

// 객체 생성 (인스턴스 만들기)
Person person1 = Person('김철수', 30);

// 메서드 호출
person1.introduce();     // 안녕하세요! 저는 김철수이고, 30살입니다.
person1.haveBirthday();  // 생일축하합니다! 이제 31살이 되었습니다.
person1.introduce();     // 안녕하세요! 저는 김철수이고, 31살입니다.

 

Dart의 독특한 생성자 문법

// ✅ Dart 스타일: 간결한 생성자
Person(this.name, this.age);

// 다른 언어 스타일이라면:
Person(String name, int age) {
  this.name = name;
  this.age = age;
}

this.name은 "매개변수를 바로 속성에 할당하라"는 Dart만의 축약 문법입니다.

 

상속(Inheritance): 기존 클래스 확장하기

상속 클래스 정의

class Student extends Person {  // Person의 모든 기능을 물려받음
  String major;   // 전공 (Student만의 고유 속성)
  String? grade;  // 성적 (null일 수 있음)

  // 기본 생성자
  Student(String name, int age, this.major) : super(name, age);
  //                                         ↑ 부모 생성자 호출

  // Named 생성자 - 성적까지 포함해서 생성
  Student.withGrade(String name, int age, this.major, this.grade)
      : super(name, age);
}

 

핵심: :super()의 역할

다른 언어와 다른 Dart의 독특한 문법입니다.

// Java/C# 방식이라면:
Student(String name, int age, String major) {
  super(name, age);  // 본문 안에서 호출
  this.major = major;
}

// Dart 방식: 본문 실행 전에 초기화
Student(String name, int age, this.major) : super(name, age);
//                                         ↑ 초기화 리스트

 

실행 순서

  1. Student 생성자 매개변수 받음
  2. : super(name, age) - 부모 클래스 먼저 초기화
  3. this.major - Student의 고유 속성 초기화
  4. 생성자 본문 실행 (있다면)

 

메서드 오버라이드

class Student extends Person {
  String major;

  Student(String name, int age, this.major) : super(name, age);

  // @override: 부모 클래스의 메서드를 재정의한다는 표시
  @override
  void introduce() {
    super.introduce();  // 부모의 introduce() 먼저 호출
    print('전공은 $major입니다.');  // 추가 정보 출력
  }

  // Student만의 고유 메서드
  void study() {
    print('$name 학생이 $major를 공부하고 있습니다.');
  }
}

 

사용 예제

// 기본 생성자 사용
Student student1 = Student('이영희', 20, '컴퓨터공학');
student1.introduce();  // 부모 + 자식 메서드 모두 실행
student1.study();      // Student만의 메서드

// Named 생성자 사용
Student student2 = Student.withGrade('박민수', 22, '경영학', 'A+');
student2.introduce();
student2.showGrade();  // 성적 표시

 

Named 생성자: 다양한 생성 방법 제공

class Student extends Person {
  String major;
  String? grade;

  // 1. 기본 생성자
  Student(String name, int age, this.major) : super(name, age);

  // 2. Named 생성자 - 성적 포함
  Student.withGrade(String name, int age, this.major, this.grade)
      : super(name, age);

  // 3. Named 생성자 - 편입생용
  Student.transfer(String name, int age, this.major, int transferYear)
      : super(name, age) {
    print('$transferYear년에 편입한 $name 학생입니다.');
  }
}

// 사용법
Student freshMan = Student('김신입', 19, '컴공');
Student withGrade = Student.withGrade('이우등', 21, '경영', 'A+');
Student transfer = Student.transfer('박편입', 23, '물리', 2023);

 

null 체크와 안전한 코드

class Student extends Person {
  String major;
  String? grade;  // nullable

  Student.withGrade(String name, int age, this.major, this.grade)
      : super(name, age);

  void showGrade() {
    if (grade != null) {  // null 체크
      print('현재 성적: $grade');
    } else {
      print('아직 성적이 없습니다.');
    }
  }

  // 삼항 연산자 활용
  String getGradeStatus() {
    return grade != null ? '성적: $grade' : '성적 미입력';
  }
}

 

실무에서 자주 사용하는 패턴들

1. Factory 생성자 패턴

class ApiResponse {
  final bool success;
  final String message;
  final dynamic data;

  ApiResponse(this.success, this.message, this.data);

  // Factory 생성자 - 성공/실패 응답 생성
  factory ApiResponse.success(dynamic data) {
    return ApiResponse(true, 'Success', data);
  }

  factory ApiResponse.error(String message) {
    return ApiResponse(false, message, null);
  }
}

// 사용법
ApiResponse successResponse = ApiResponse.success({'users': []});
ApiResponse errorResponse = ApiResponse.error('서버 오류 발생');

 

2. Getter와 Setter

class Rectangle {
  double _width;   // private 속성 (언더스코어로 시작)
  double _height;

  Rectangle(this._width, this._height);

  // Getter - 속성값 가져오기
  double get area => _width * _height;
  double get perimeter => 2 * (_width + _height);

  // Setter - 속성값 설정하기
  set width(double value) {
    if (value > 0) _width = value;
  }

  set height(double value) {
    if (value > 0) _height = value;
  }
}

// 사용법
Rectangle rect = Rectangle(10, 5);
print('넓이: ${rect.area}');        // 50
print('둘레: ${rect.perimeter}');   // 30

rect.width = 15;  // setter 호출
print('새 넓이: ${rect.area}');      // 75

 

클래스 설계 베스트 프랙티스

1. 단일 책임 원칙

// ✅ Good: 각 클래스가 하나의 역할만
class User {
  String name;
  String email;

  User(this.name, this.email);

  bool isValidEmail() => email.contains('@');
}

class UserService {
  List<User> users = [];

  void addUser(User user) => users.add(user);
  User? findUser(String email) => users.firstWhere((u) => u.email == email);
}

// ❌ Bad: 너무 많은 책임
class UserEverything {
  // 사용자 데이터 + 네트워킹 + 파일 저장 + 이메일 발송...
}

 

2. 생성자 활용

class BankAccount {
  final String accountNumber;  // 변경 불가능한 계좌번호
  double _balance;             // private 잔액

  // 기본 생성자
  BankAccount(this.accountNumber, this._balance);

  // Named 생성자 - 신규 계좌
  BankAccount.newAccount(this.accountNumber) : _balance = 0;

  // Named 생성자 - VIP 계좌
  BankAccount.vip(this.accountNumber) : _balance = 1000000;

  double get balance => _balance;

  void deposit(double amount) {
    if (amount > 0) _balance += amount;
  }
}

 

함수 vs 메서드 차이점

구분 함수 메서드
위치 클래스 외부 클래스 내부
호출 방식 함수명() 객체.메서드명()
데이터 접근 매개변수만 클래스 속성 접근 가능
예시 calculateBMI() person.introduce()
반응형