Dart 언어 오답노트 : 33. main 함수, compile, !

샤워실의 바보·2023년 10월 31일
0
post-thumbnail
void main() {
  final List<Map<String, String>> people = [
    {
      'name': '지수',
      'group': '블랙핑크',
    },
    {
      'name': '로제',
      'group': '블랙핑크',
    },
    {
      'name': 'RM',
      'group': 'BTS',
    },
    {
      'name': '뷔',
      'group': 'BTS',
    },
  ];

  print(people);

  final List<Person> parsedPeople = people.map((one) {
    return Person(
      name: one['name']!,
      group: one['group']!,
    );
  }).toList();

  print(parsedPeople);
}

class Person {
  final String name;
  final String group;

  Person({
    required this.name,
    required this.group,
  });

  
  String toString() {
    return 'Person(name: $name, group: $group)';
  }
}

1. 프로그램의 개괄

이 프로그램은 K-pop 그룹의 멤버들을 나타내는 people이라는 리스트를 처리하는 Dart 프로그램입니다. people 리스트는 Map 객체들의 리스트이며, 각 Map 객체는 멤버의 이름(name)과 그룹(group)을 문자열로 저장하고 있습니다.

final List<Map<String, String>> people = [
  {
    'name': '지수',
    'group': '블랙핑크',
  },
  // ... 다른 멤버들
];

프로그램의 주된 목적은 이 리스트를 사용하여 Person 객체의 리스트를 생성하고, 이 객체들의 정보를 출력하는 것입니다. Person 클래스는 이름과 그룹 정보를 저장하기 위해 두 개의 String 속성을 가지고 있습니다.

class Person {
  final String name;
  final String group;

  Person({required this.name, required this.group});
}

리스트 people에서 각 맵을 Person 객체로 변환하기 위해 map 함수를 사용합니다. map 함수는 리스트의 각 요소에 대해 주어진 함수를 실행하고, 결과를 새로운 Iterable로 반환합니다. 이때, toList() 함수를 사용하여 결과 IterableList로 변환합니다.

final List<Person> parsedPeople = people.map((one) {
  return Person(
    name: one['name']!,
    group: one['group']!,
  );
}).toList();

변환된 Person 객체 리스트인 parsedPeople에는 이제 Person 객체들이 저장되어 있습니다. 이 리스트를 반복하여 각 Person 객체의 이름과 그룹 정보를 출력합니다.

for (var person in parsedPeople) {
  print('이름: ${person.name}, 그룹: ${person.group}');
}

이 프로그램의 출력 결과는 다음과 같을 것입니다.

이름: 지수, 그룹: 블랙핑크
이름: 로제, 그룹: 블랙핑크
이름: RM, 그룹: BTS
이름: 뷔, 그룹: BTS

전체적으로 이 코드는 Dart의 map 함수, 리스트, 클래스, 그리고 출력을 사용하여 K-pop 그룹 멤버들의 정보를 처리하고 출력하는 간단한 예제입니다.

2. null assertion operator

Dart 언어에서 ! 연산자는 "null assertion" 연산자로, 해당 값이 null이 아니라는 것을 확실히 알려주는 역할을 합니다. 즉, 이 연산자를 사용하면 해당 값이 null이 아니라는 것을 시스템에게 알려주고, 만약 값이 null이라면 런타임 에러를 발생시킵니다.

위의 코드에서

final List<Person> parsedPeople = people.map((one) {
  return Person(
    name: one['name']!,
    group: one['group']!,
  );
}).toList();

people 리스트는 Map<String, String> 타입의 리스트입니다. Map에서 특정 키에 해당하는 값을 조회할 때, 그 키가 맵에 존재하지 않으면 Dart는 null을 반환합니다. 그러나 Person 클래스의 생성자에서는 namegroup이 non-nullable String 타입으로 선언되어 있기 때문에, null 값을 전달할 수 없습니다.

여기서 ! 연산자를 사용함으로써, 우리는 시스템에게 "이 값은 절대 null이 아니다"라고 알려주고 있습니다. 이것은 우리가 확실히 알고 있는 상황에서 안전하게 사용할 수 있지만, 만약 값이 실제로 null이라면 런타임 에러가 발생합니다.

예를 들어, 만약 people 리스트에 'name' 키나 'group' 키가 없는 맵이 있다면, 이 코드는 런타임 에러를 발생시킬 것입니다. 따라서 이 연산자를 사용할 때는 주의가 필요합니다.

! 연산자를 사용하는 대신, 좀 더 안전한 방법으로 null 확인을 할 수 있습니다. 예를 들어, null 병합 연산자 ??를 사용하여 기본값을 제공하거나, null을 허용하는 방법으로 코드를 수정할 수 있습니다.

전체적으로 이 코드는 Dart 언어의 기능을 사용하여 K-pop 그룹 멤버들의 정보를 Person 객체로 변환하고 출력하는 예제입니다. ! 연산자의 사용은 값이 null이 아니라고 확신할 때 사용할 수 있지만, 잘못 사용하면 런타임 에러를 유발할 수 있으므로 주의가 필요합니다.

3. main 함수와 compile

Dart 언어에서는 자바스크립트와는 다르게 클래스가 호이스팅되지 않습니다. 호이스팅이란 변수나 함수 선언을 코드의 최상단으로 끌어올리는 것을 의미합니다. 자바스크립트에서는 함수 선언이나 var 키워드로 선언된 변수가 호이스팅되지만, let, const 키워드로 선언된 변수나 클래스 선언은 호이스팅되지 않습니다.

Dart에서는 클래스, 함수, 변수 선언 등이 코드에 작성된 순서대로 처리됩니다. 따라서 클래스를 사용하기 전에 먼저 선언되어 있어야 합니다.

예를 들어, 다음 Dart 코드는 오류를 발생시킵니다.

void main() {
  var myCar = Car(); // 오류: Car 클래스가 아직 정의되지 않았음
}

class Car {
  String model = 'Tesla';
}

이 코드는 Car 클래스를 main 함수 안에서 사용하려고 시도하지만, Car 클래스는 그 이후에 선언되어 있기 때문에 오류를 발생시킵니다. Dart에서는 클래스를 사용하기 전에 선언해야 합니다.

따라서 Dart에서는 클래스 호이스팅에 의존할 수 없으며, 코드의 구조를 명확하게 관리해야 합니다.

Dart 언어에서 클래스의 위치는 클래스를 사용하기 전에 선언되어 있어야 합니다. 그러나 main 함수와 같은 실행 흐름을 가진 함수 내부에서 클래스의 인스턴스를 생성하는 경우에는 문제가 되지 않습니다. Dart는 전체 코드를 컴파일한 후에 실행하기 때문에, 코드의 실행 흐름이 클래스 선언보다 먼저 오더라도 문제가 되지 않습니다.

예를 들어, 위의 코드에서 main 함수가 호출될 때 Person 클래스가 이미 선언되어 있기 때문에 main 함수 내에서 Person 클래스의 인스턴스를 생성하는 것이 가능합니다.

이것은 함수와 변수의 호이스팅과는 다른 개념입니다. Dart에서 클래스는 호이스팅되지 않지만, 컴파일러는 코드 전체를 먼저 파악하고 실행하기 때문에 main 함수 안에서 Person 클래스를 사용할 수 있는 것입니다.

따라서 다음과 같은 경우는 문제가 없습니다:

void main() {
  final person = Person(name: 'Alice', group: 'Group A');
  print('이름: ${person.name}, 그룹: ${person.group}');
}

class Person {
  final String name;
  final String group;

  Person({required this.name, required this.group});
}

하지만 다음과 같이 함수 외부에서 클래스 인스턴스를 생성하려고 하면 에러가 발생합니다:

final person = Person(name: 'Alice', group: 'Group A'); // 에러

class Person {
  final String name;
  final String group;

  Person({required this.name, required this.group});
}

void main() {
  print('이름: ${person.name}, 그룹: ${person.group}');
}

이 경우 Person 클래스가 person 인스턴스를 생성하는 코드보다 뒤에 선언되어 있기 때문에 Dart 컴파일러는 에러를 발생시킵니다.

4. 총정리

이 Dart 프로그램은 몇 가지 주요 부분으로 구성되어 있습니다: main 함수, people 리스트, Person 클래스, 그리고 toString 메서드의 오버라이드입니다.

1) main 함수:

이것은 Dart 프로그램의 진입점입니다. 프로그램이 실행되면 main 함수가 가장 먼저 호출됩니다.

2) people 리스트:

이 리스트는 Map<String, String> 타입의 여러 개체를 포함하고 있습니다. 각 맵은 사람의 이름(name)과 그룹(group)을 나타냅니다.

final List<Map<String, String>> people = [
  {
    'name': '지수',
    'group': '블랙핑크',
  },
  // ...
];

3) 리스트의 변환:

main 함수 내에서 people 리스트는 map 함수를 사용하여 List<Person>으로 변환됩니다.

final List<Person> parsedPeople = people.map((one) {
  return Person(
    name: one['name']!,
    group: one['group']!,
  );
}).toList();

map 함수는 리스트의 각 원소에 주어진 함수를 적용하고, 결과를 새로운 이터러블로 반환합니다. 여기서는 각 맵을 Person 객체로 변환합니다. one['name']!one['group']!에서 ! 연산자는 해당 값이 null이 아님을 Dart에게 알리는 역할을 합니다. 이는 Dart의 null safety 기능의 일부로, 개발자가 null에 대해 명시적으로 처리하도록 강제합니다. 그러나 이 코드는 name이나 group이 실제로 null일 경우 런타임 에러를 일으킬 위험이 있습니다. 이를 방지하기 위해서는 값의 존재 여부를 확인하고 적절히 처리하는 것이 좋습니다.

4) Person 클래스:

이 클래스는 사람을 나타냅니다. 이름(name)과 그룹(group) 프로퍼티를 가지고 있습니다.

class Person {
  final String name;
  final String group;

  Person({
    required this.name,
    required this.group,
  });
}

required 키워드는 Dart의 null safety 기능의 일부로, 생성자 호출 시 해당 인수가 반드시 제공되어야 함을 의미합니다.

5) toString 메서드의 오버라이드:

Person 클래스 내에서 toString 메서드를 오버라이드하여 객체의 문자열 표현을 커스터마이즈합니다.


String toString() {
  return 'Person(name: $name, group: $group)';
}

이를 통해 print(parsedPeople)를 호출할 때 각 Person 객체가 보다 읽기 쉬운 형태로 출력됩니다.

6) 결과 출력:

프로그램이 실행되면, 먼저 people 리스트가 출력되고, 그 다음으로 변환된 parsedPeople 리스트가 출력됩니다.

이러한 모든 부분이 함께 작동하여 Dart 프로그램을 구성하고, 리스트를 다루고, 객체 지향 프로그래밍의 기능을 활용하며, null safety를 적용하는 방법을 보여줍니다.

5. toString 오버라이드

toString 메서드는 Dart에서 모든 객체의 기본 클래스인 Object 클래스에 정의되어 있습니다. 이 메서드는 객체를 문자열로 변환하여 반환하며, 기본적으로 객체의 타입과 해당 객체의 해시코드를 포함한 문자열을 반환합니다.

예를 들어, Object 클래스에서 toString 메서드의 기본 구현은 다음과 같이 될 수 있습니다:

String toString() => 'Instance of ${runtimeType}';

여기서 runtimeType은 객체의 런타임 타입을 반환하는 속성입니다.

toString 메서드를 오버라이드하는 것은 객체의 문자열 표현을 보다 읽기 쉽고, 유용한 정보를 포함하도록 커스터마이징하기 위해 흔히 사용됩니다. 예를 들어, 위에서 본 Person 클래스에서 toString 메서드를 오버라이드하면, Person 객체를 출력할 때 "Person(name: 지수, group: 블랙핑크)"와 같은 형식으로 보기 좋게 표시할 수 있습니다.

이런 커스터마이징 없이 객체를 출력하려고 하면, 기본적으로 제공되는 toString의 결과, 즉 "Instance of 'Person'"와 같은 형식으로 출력됩니다.

6. chaining

void main() {
  final List<Map<String, String>> people = [
    {
      'name': '지수',
      'group': '블랙핑크',
    },
    {
      'name': '로제',
      'group': '블랙핑크',
    },
    {
      'name': 'RM',
      'group': 'BTS',
    },
    {
      'name': '뷔',
      'group': 'BTS',
    },
  ];

  print(people);

  final List<Person> parsedPeople = people.map((one) {
    return Person(
      name: one['name']!,
      group: one['group']!,
    );
  }).toList();

  print(parsedPeople);

  for (Person person in parsedPeople) {
    print(person.name);
    print(person.group);
  }

  final bts = parsedPeople.where((one) => one.group == 'BTS').toList();
  print(bts);

  final result = parsedPeople
    .where((one) => one.group == 'BTS')
    .fold<int>(0, (prev, next) => prev + next.name.length);
  print(result);
}

class Person {
  final String name;
  final String group;

  Person({
    required this.name,
    required this.group,
  });

  
  String toString() {
    return 'Person(name: $name, group: $group)';
  }
}

체이닝(chainning)은 여러 메서드나 함수를 연속적으로 호출하는 프로그래밍 패턴을 말합니다. Dart에서는 이 패턴을 통해 코드를 간결하고 읽기 쉽게 만들 수 있습니다.

final result = parsedPeople
  .where((one) => one.group == 'BTS')
  .fold<int>(0, (prev, next) => prev + next.name.length);
print(result);

위 코드에서는 parsedPeople 리스트에 대해 두 개의 메서드를 연속적으로 호출하고 있습니다.

1. where 메서드

where 메서드는 리스트의 각 요소에 대해 주어진 조건 함수를 적용하고, 조건을 만족하는 요소들로 구성된 새로운 이터러블을 반환합니다.

.where((one) => one.group == 'BTS')

여기서는 Person 객체의 group 속성이 'BTS'와 같은지 확인합니다. 결과적으로 group이 'BTS'인 Person 객체만 포함된 새로운 이터러블이 생성됩니다.

2. fold 메서드

fold 메서드는 이터러블의 요소들을 결합하여 단일 값을 생성합니다. 이 메서드는 두 개의 인자를 받습니다: 초기값과 요소들을 결합하는 함수입니다.

.fold<int>(0, (prev, next) => prev + next.name.length)

여기서 초기값은 0이고, 결합 함수는 (prev, next) => prev + next.name.length입니다. 이 함수는 이전의 누적 값(prev)과 현재 요소(next)를 가져와, next의 이름 길이를 prev에 더합니다. 이 과정은 이터러블의 모든 요소에 대해 반복되며, 결과적으로 이름 길이의 총 합이 반환됩니다.

이 예에서는 'BTS' 그룹의 멤버 'RM'과 '뷔'의 이름 길이의 합을 계산합니다. 'RM'은 2글자, '뷔'는 1글자이므로 결과는 3입니다.

정리

이러한 체이닝 방식을 통해 간결하게 데이터를 필터링하고, 결과 값을 계산할 수 있습니다. 각 메서드는 중간 결과를 생성하지 않고 바로 다음 메서드로 결과를 넘기기 때문에 코드가 더 간결하고 성능적인 이점도 있습니다.

profile
공부하는 개발자

0개의 댓글