본문 바로가기

Personal Posting/Flutter

Extension Method in Dart

 

이번 포스팅에서는 객체 지향 프로그래밍 언어인 Dart의 강력한 기능 중 하나인 익스텐션 메서드에 대해 정리해본다. Dart 2.7에 도입된 이 기능은 소스 코드에 접근할 수 없더라도 기존 라이브러리와 클래스에 새로운 기능을 추가할 수 있는 유연한 방법을 제공한다. 익스텐션 메서드를 사용하면 개발자는 String, List 등의 내장 타입이든 사용자 정의 클래스든 원래 클래스 정의를 변경하지 않고도 모든 타입에 새로운 메서드를 추가할 수 있다.

 

왜 익스텐션 메서드를 사용해야 할까?

익스텐션 메서드는 클래스를 상속하지 않고도 클래스의 기능을 확장할 수 있는 강력한 메커니즘을 제공한다. 관련 기능을 하나의 익스텐션 메서드로 그룹화할 수 있으므로, 코드를 더욱 간결하고 읽기 쉽게 작성할 수 있다.

 

익스텐션 메서드의 구문

extension ExtensionName on <Type> {
  // Your method 
}
  • ExtensionName: 익스텐션의 이름
  • Type: 확장하려는 클래스 또는 유형

예를 들어, 문자열을 정수로 파싱하는 parse가 있다.

int.parse('42')
'42'.parseInt()

 

이 코드를 parseInt로 활성화하려면 문자열 클래스의 확장을 포함하는 라이브러리를 가져오면 된다.

import 'string_apis.dart';

void main() {
  print('42'.parseInt()); // Use an extension method.
}

 

확장은 메서드뿐만 아니라 getter, setter, 연산자와 같은 다른 멤버도 정의할 수 있다. 또한 확장은 이름을 가질 수 있는데, 이는 API 충돌 발생 시 도움이 될 수 있다.

 

익스텐션 메서드의 Use Case

 

1. Utility Methods

extension ContainExtension on List<String> {
  bool containWithoutCase(String value) {
    return any((element) => element.toLowerCase() == value.toLowerCase());
  }
}

위의 방법에서는 이름이 ContainExtension인 익스텐션이 List<String> 타입에 생성되므로 목록 항목을 검사할 때마다 해당 목록의 항목에 매개변수로 제공된 문자열과 일치하는 "문자열"이 포함되어 있으면 true를 반환한다.

 

다음과 같이 코드에 이 기능을 추가할 수 있다.

bool checkValue = 
searchList.map((e) => e.label).toList().containWithoutCase(searchKey);

 

이렇게 하면 코드가 더 깔끔해지고, 단 한 줄의 코드로 필요한 곳마다 이 검사를 쉽게 적용할 수 있다.

 

2. Flexible Interfaces

익스텐션 메서드는 유연하고 유창한 인터페이스를 만드는 데 도움이 되며, 이는 더 읽기 쉽고 깔끔한 코드라는 이점을 제공한다.
클래스에 적용된 익스텐션 메서드의 예를 살펴보고, 이를 클래스 내부의 메서드에 적용해 보자.

class Coordinates {
  double x, y;
  Coordinates(this.x, this.y);
}

extension CoordinatesExtensions on Coordinates {
  Coordinates translate(double dx, double dy) {
    return Coordinates(x + dx, y + dy);
  }
}

void main() {
  Coordinates p = Coordinates(2, 3)
    .translate(1, 1);

  print('(${p.x}, ${p.y})'); // Output: (6.0, 8.0)
}

여기서는 "translate" 메서드를 Coordinates 클래스 객체와 함께 사용하여 얼마나 간단하게 적용할 수 있는지 볼 수 있다. 이를 통해 코드의 가독성이 더 높아질 수 있다.

 

3. Custom Widget Methods

사용자 정의 메서드로 위젯을 확장하여 필요에 따라 작업을 간소화할 수도 있다. 예를 들어, 아래 예에서는 컨테이너에 둥근 사각형 테두리를 설정하려는 경우 사용할 수 있는 확장 기능을 Container에 추가했다.

extension ContainerExtensions on Container {
  Container withRRBorder(double radius) {
    return Container(
      key: this.key,
      alignment: this.alignment,
      padding: this.padding,
      color: this.color,
      decoration: BoxDecoration(
          borderRadius: BorderRadius.circular(radius),
          color: Colors.black,
        ),
      foregroundDecoration: this.foregroundDecoration,
      width: this.width,
      height: this.height,
      constraints: this.constraints,
      margin: this.margin,
      transform: this.transform,
      child: this.child,
    );
  }
}

void main() {
  runApp(
    MaterialApp(
      home: Scaffold(
        body: Center(
          child: Container().withRRBorder(8.0),
        ),
      ),
    ),
  );
}

 

익스텐션 메서드의 Conflict Case

 

1. dynamic 타입에 대한 익스텐션 구현

익스텐션 메서드는 dynamic 유형의 변수에서 호출할 수 없다. 예를 들어, 다음 코드는 런타임 익셉션을 발생시킨다.

extension NumberParsing on String {
  int parseInt() {
    return int.parse(this);
  }
  // ···
}

dynamic d = '2';
print(d.parseInt()); // Runtime exception: NoSuchMethodError

 

아래와 같이 수정하면 에러가 발생하지 않는다.

var v = '2';
print(v.parseInt()); // Output: 2

 

이유:
익스텐션 메서드는 수신자의 정적 타입을 기준으로 결정되기 때문에 Dart의 타입 추론과 호환되며, 따라서 정적 함수를 호출하는 것만큼 빠르다. 위 수정은 변수 v가 ​​문자열 타입으로 추론되기 때문에 이 방식은 문제없이 작동한다.

 

2. API 충돌

서로 다른 Dart 파일에 동일한 익스텐션 메서드를 생성하면 API 충돌이 발생할 가능성이 있다.

 

솔루션1) hide

// Defines the String extension method parseInt().
import 'string_apis.dart';

// Also defines parseInt(), but hiding NumberParsing2
// hides that extension method.
import 'string_apis_2.dart' hide NumberParsing2;

// ···
// Uses the parseInt() defined in 'string_apis.dart'.
print('42'.parseInt());

 

이렇게 하면 NumberParsing2가 ‘string_apis_2.dart’에서 숨겨지고 결과적으로 print문은 위의 익스텐션 메서드를 실행하는 것이 된다.

 

솔루션2) 확장 방법을 명시적으로 언급

a. 익스텐션 메서드의 이름이 동일한 경우

// Both libraries define extensions on String that contain parseInt(),
// and the extensions have different names.
import 'string_apis.dart'; // Contains NumberParsing extension.
import 'string_apis_2.dart'; // Contains NumberParsing2 extension.

// ···
// print('42'.parseInt()); // Doesn't work.
print(NumberParsing('42').parseInt());
print(NumberParsing2('42').parseInt());

명시적으로 제공하여 사용할 익스텐션 메서드를 참조할 수 있으며, 이렇게 하면 확장이 래퍼 클래스인 것처럼 보이는 코드가 생성된다.

 

b. 익스텐션 메서드 이름이 다른 경우

// Both libraries define extensions named NumberParsing
// that contain the extension method parseInt(). One NumberParsing
// extension (in 'string_apis_3.dart') also defines parseNum().
import 'string_apis.dart';
import 'string_apis_3.dart' as rad;

// ···
// print('42'.parseInt()); // Doesn't work.

// Use the ParseNumbers extension from string_apis.dart.
print(NumberParsing('42').parseInt());

// Use the ParseNumbers extension from string_apis_3.dart.
print(rad.NumberParsing('42').parseInt());

// Only string_apis_3.dart has parseNum().
print('42'.parseNum());

이 경우 어떤 dart 파일에서 사용할지 익스텐션 메서드를 알려주는 접두사를 사용하여 가져올 수 있다.

 

결론

이 글을 따라가다 보면 플러터 익스텐션 메서드를 사용하는 방법과 이유에 대한 더 많은 통찰력을 얻을 수 있을 것입니다. 이 글에서는 플러터 확장 메서드를 구현하는 데 따른 장점과 다양한 시나리오, 그리고 충돌하는 상황에서의 사용법에 대해 다루었습니다.