
이번 포스팅에서는 객체 지향 프로그래밍 언어인 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 파일에서 사용할지 익스텐션 메서드를 알려주는 접두사를 사용하여 가져올 수 있다.
결론
이 글을 따라가다 보면 플러터 익스텐션 메서드를 사용하는 방법과 이유에 대한 더 많은 통찰력을 얻을 수 있을 것입니다. 이 글에서는 플러터 확장 메서드를 구현하는 데 따른 장점과 다양한 시나리오, 그리고 충돌하는 상황에서의 사용법에 대해 다루었습니다.
'Personal Posting > Flutter' 카테고리의 다른 글
| Dart 3.3의 Extension Types: 쉽게 이해하는 개념과 활용법 (0) | 2025.05.26 |
|---|---|
| Flutter에서의 Permission Handler 사용 (1) | 2025.05.21 |
| Flutter Data ENCRYPT & DECRYPT (플러터 데이터 암호화/복호화) (0) | 2025.05.21 |
| Dart의 Event Loop 정리 (0) | 2025.04.03 |
| Flutter TextFormFields 사용 시 체크사항 (0) | 2025.04.03 |