Flutter 튜토리얼 67편: 패키지 개발하기
요약
핵심 요지
- 문제 정의: 프로젝트 간에 코드를 재사용하거나 커뮤니티와 공유하려면 패키지로 만들어야 한다.
- 핵심 주장: Flutter는 패키지 생성부터 배포까지 체계적인 도구를 제공한다.
- 주요 근거:
flutter create --template명령어로 패키지 골격을 자동 생성하고,pub publish로 pub.dev에 배포할 수 있다. - 실무 기준: 순수 Dart 패키지, 플러그인 패키지, FFI 패키지 중 목적에 맞는 유형을 선택해야 한다.
- 한계: 플러그인 개발은 각 플랫폼의 네이티브 언어(Kotlin, Swift 등)에 대한 이해가 필요하다.
문서가 설명하는 범위
- 패키지 유형과 구조 이해하기
- Dart 패키지 생성 및 구현
- 플러그인 패키지 개발 기초
- 연합 플러그인(Federated Plugin) 아키텍처
- 패키지 문서화와 pub.dev 배포
읽는 시간: 20분 | 난이도: 고급
참고 자료
- Developing packages & plugins - 공식 패키지 개발 가이드
- Swift Package Manager for app developers - Swift PM 앱 개발자 가이드
- Swift Package Manager for plugin authors - Swift PM 플러그인 작성자 가이드
- Writing a good plugin - 좋은 플러그인 작성법
문제 상황
프로젝트를 진행하다 보면 여러 앱에서 동일한 코드를 사용해야 할 때가 있습니다. 또는 유용한 기능을 커뮤니티와 공유하고 싶을 수도 있습니다.
// 여러 프로젝트에서 같은 유틸리티 함수를 복사-붙여넣기?String formatCurrency(double amount) { return '\$${amount.toStringAsFixed(2)}';}
// 동일한 위젯을 매번 다시 작성?class CustomButton extends StatelessWidget { // ...중복 코드}문제는 다음과 같습니다.
- 코드를 복사하면 버그 수정이나 업데이트를 모든 프로젝트에 적용해야 한다.
- 유용한 코드를 공유하려면 표준화된 형식이 필요하다.
- 네이티브 기능에 접근하려면 플랫폼별 코드가 필요하다.
해결 방법
Flutter는 세 가지 유형의 패키지를 지원합니다. 목적에 맞는 패키지 유형을 선택하고, 체계적인 구조로 개발하면 됩니다.
챕터 1: 패키지 유형 이해하기
Why
NOTE패키지를 만들기 전에 어떤 유형이 적합한지 결정해야 합니다. 잘못된 유형을 선택하면 나중에 구조를 변경하기 어렵습니다.
세 가지 유형은 다음과 같습니다.
- Dart 패키지: 순수 Dart 코드만 포함
- 플러그인 패키지: 네이티브 코드 포함 (Platform Channel 사용)
- FFI 패키지: 네이티브 코드 포함 (dart
사용)
What
NOTEDart 패키지
순수 Dart 코드로만 구성된 패키지입니다.
my_package/├── lib/│ └── my_package.dart├── test/├── pubspec.yaml└── README.md
- 예:
provider,http,intl- Flutter에 의존할 수도 있음 (예:
fluro)- 모든 플랫폼에서 동일하게 작동
플러그인 패키지
플랫폼별 네이티브 코드를 포함합니다.
my_plugin/├── lib/│ └── my_plugin.dart├── android/│ └── src/main/kotlin/├── ios/│ └── Classes/├── pubspec.yaml└── README.md
- 예:
camera,geolocator,battery_plus- Platform Channel1로 Dart와 네이티브 코드 통신
- 각 플랫폼별로 구현 필요
FFI 패키지
dart:ffi2를 사용하여 C/C++ 코드를 직접 호출합니다.my_ffi_package/├── lib/│ └── my_ffi_package.dart├── src/│ └── my_native_code.c├── pubspec.yaml└── README.md
- Flutter 3.38부터 권장되는 네이티브 통합 방식
- 플랫폼별 빌드 파일 불필요
- 고성능 네이티브 코드 호출에 적합
How
TIP패키지 유형 선택 가이드:
요구사항 적합한 유형 순수 Dart 로직 (상태 관리, 유틸리티) Dart 패키지 기기 API 접근 (카메라, GPS) 플러그인 패키지 기존 C/C++ 라이브러리 사용 FFI 패키지 모든 플랫폼 동일 작동 Dart 패키지 플랫폼별 다른 동작 플러그인 패키지 결정 플로우차트:
네이티브 코드가 필요한가?├── 아니오 → Dart 패키지└── 예 → C/C++ 라이브러리인가?├── 예 → FFI 패키지└── 아니오 → 플러그인 패키지
Watch out
WARNING플러그인 개발 시 고려사항:
각 플랫폼의 네이티브 언어에 대한 이해가 필요합니다.
- Android: Kotlin 또는 Java
- iOS/macOS: Swift 또는 Objective-C
- Windows: C++
- Linux: C
- Web: JavaScript/TypeScript
모든 플랫폼을 지원할 필요는 없습니다.
지원하지 않는 플랫폼에서 사용 시 에러 처리를 해야 합니다.
챕터 2: Dart 패키지 만들기
Why
NOTE순수 Dart 패키지는 가장 간단한 형태의 패키지입니다. 네이티브 코드 없이 Dart만으로 재사용 가능한 기능을 제공합니다.
유틸리티 함수, 상태 관리 솔루션, 데이터 모델 등을 패키지로 만들 수 있습니다.
What
NOTE
flutter create --template=package명령어로 패키지를 생성합니다.Terminal window flutter create --template=package hello생성되는 파일 구조:
파일/폴더 설명 lib/hello.dart패키지의 메인 코드 test/hello_test.dart단위 테스트 pubspec.yaml패키지 메타데이터 README.md패키지 설명 문서 CHANGELOG.md버전 변경 기록 LICENSE라이선스 파일
How
TIP1단계: 패키지 생성
Terminal window flutter create --template=package my_utilscd my_utils2단계: pubspec.yaml 설정
name: my_utilsdescription: A collection of useful utility functions.version: 1.0.0homepage: https://github.com/username/my_utilsenvironment:sdk: ^3.0.0flutter: ">=3.0.0"dependencies:flutter:sdk: flutterdev_dependencies:flutter_test:sdk: flutterflutter_lints: ^5.0.03단계: 코드 구현
lib/my_utils.dart library my_utils;export 'src/string_utils.dart';export 'src/date_utils.dart';lib/src/string_utils.dart /// 문자열의 첫 글자를 대문자로 변환합니다.String capitalize(String input) {if (input.isEmpty) return input;return input[0].toUpperCase() + input.substring(1);}/// 문자열을 카멜케이스로 변환합니다.String toCamelCase(String input) {final words = input.split(RegExp(r'[\s_-]+'));return words.first.toLowerCase() +words.skip(1).map((w) => capitalize(w)).join();}4단계: 테스트 작성
test/string_utils_test.dart import 'package:flutter_test/flutter_test.dart';import 'package:my_utils/my_utils.dart';void main() {group('capitalize', () {test('빈 문자열은 그대로 반환', () {expect(capitalize(''), '');});test('첫 글자를 대문자로 변환', () {expect(capitalize('hello'), 'Hello');});});group('toCamelCase', () {test('snake_case를 camelCase로 변환', () {expect(toCamelCase('hello_world'), 'helloWorld');});test('kebab-case를 camelCase로 변환', () {expect(toCamelCase('hello-world'), 'helloWorld');});});}Terminal window # 테스트 실행flutter test
Watch out
WARNING패키지 구조 모범 사례:
lib/my_utils.dart // ❌ 나쁜 예: 모든 코드를 한 파일에String capitalize(String s) { ... }String toCamelCase(String s) { ... }// ... 수백 줄의 코드// ✅ 좋은 예: 논리적으로 분리// lib/my_utils.dart (진입점)library my_utils;export 'src/string_utils.dart';export 'src/date_utils.dart';// lib/src/string_utils.dart// lib/src/date_utils.dartsrc 폴더 규칙:
lib/아래의 파일은 외부에서 직접 import 가능lib/src/아래의 파일은 내부 구현용- 메인 라이브러리 파일에서
export로 공개 API 정의
챕터 3: 플러그인 패키지 만들기
Why
NOTE기기의 카메라, GPS, 센서 등에 접근하려면 플러그인이 필요합니다. Dart 코드만으로는 이러한 네이티브 기능에 접근할 수 없습니다.
플러그인은 Dart API와 플랫폼별 네이티브 구현을 연결합니다.
What
NOTE
flutter create --template=plugin명령어로 플러그인을 생성합니다.Terminal window flutter create --template=plugin --platforms=android,ios my_plugin주요 옵션:
옵션 설명 --platforms지원할 플랫폼 (android, ios, web, macos, windows, linux) --org조직 식별자 (예: com.example) -aAndroid 언어 (kotlin, java) -iiOS 언어 (swift, objc) Terminal window # 예: Swift와 Kotlin을 사용하는 모든 플랫폼 플러그인flutter create --template=plugin \--platforms=android,ios,web,macos,windows,linux \--org com.mycompany \-a kotlin \-i swift \my_plugin
How
TIP플러그인 구조 이해하기:
my_plugin/├── lib/│ ├── my_plugin.dart # Dart API│ ├── my_plugin_method_channel.dart│ └── my_plugin_platform_interface.dart├── android/│ └── src/main/kotlin/.../MyPluginPlugin.kt├── ios/│ └── Classes/MyPluginPlugin.swift├── example/ # 예제 앱└── pubspec.yamlDart 측 코드 (lib/my_plugin.dart):
import 'my_plugin_platform_interface.dart';class MyPlugin {Future<String?> getPlatformVersion() {return MyPluginPlatform.instance.getPlatformVersion();}}플랫폼 인터페이스 (lib/my_plugin_platform_interface.dart):
import 'package:plugin_platform_interface/plugin_platform_interface.dart';import 'my_plugin_method_channel.dart';abstract class MyPluginPlatform extends PlatformInterface {MyPluginPlatform() : super(token: _token);static final Object _token = Object();static MyPluginPlatform _instance = MethodChannelMyPlugin();static MyPluginPlatform get instance => _instance;static set instance(MyPluginPlatform instance) {PlatformInterface.verifyToken(instance, _token);_instance = instance;}Future<String?> getPlatformVersion() {throw UnimplementedError('getPlatformVersion() has not been implemented.');}}Android 구현 (Kotlin):
package com.example.my_pluginimport io.flutter.embedding.engine.plugins.FlutterPluginimport io.flutter.plugin.common.MethodCallimport io.flutter.plugin.common.MethodChannelimport io.flutter.plugin.common.MethodChannel.MethodCallHandlerimport io.flutter.plugin.common.MethodChannel.Resultclass MyPluginPlugin: FlutterPlugin, MethodCallHandler {private lateinit var channel : MethodChanneloverride fun onAttachedToEngine(binding: FlutterPlugin.FlutterPluginBinding) {channel = MethodChannel(binding.binaryMessenger, "my_plugin")channel.setMethodCallHandler(this)}override fun onMethodCall(call: MethodCall, result: Result) {if (call.method == "getPlatformVersion") {result.success("Android ${android.os.Build.VERSION.RELEASE}")} else {result.notImplemented()}}override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {channel.setMethodCallHandler(null)}}iOS 구현 (Swift):
import Flutterimport UIKitpublic class MyPluginPlugin: NSObject, FlutterPlugin {public static func register(with registrar: FlutterPluginRegistrar) {let channel = FlutterMethodChannel(name: "my_plugin",binaryMessenger: registrar.messenger())let instance = MyPluginPlugin()registrar.addMethodCallDelegate(instance, channel: channel)}public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {switch call.method {case "getPlatformVersion":result("iOS " + UIDevice.current.systemVersion)default:result(FlutterMethodNotImplemented)}}}
Watch out
WARNING플러그인 개발 주의사항:
- 예제 앱(
example/)에서 반드시 테스트하세요.- 각 플랫폼에서 실제 기기 테스트가 필요합니다.
- 에뮬레이터/시뮬레이터에서 일부 기능이 작동하지 않을 수 있습니다.
Terminal window # 예제 앱 실행cd exampleflutter runpubspec.yaml 플랫폼 선언:
flutter:plugin:platforms:android:package: com.example.my_pluginpluginClass: MyPluginPluginios:pluginClass: MyPluginPlugin
챕터 4: 연합 플러그인 아키텍처
Why
NOTE플러그인이 여러 플랫폼을 지원하면 코드베이스가 커집니다. 한 플랫폼의 변경이 다른 플랫폼에 영향을 줄 수 있습니다.
연합 플러그인(Federated Plugin)3 아키텍처는 이 문제를 해결합니다. 각 플랫폼 구현을 별도의 패키지로 분리합니다.
What
NOTE연합 플러그인 구조:
url_launcher/ # 앱 개발자가 사용하는 패키지url_launcher_platform_interface/ # 플랫폼 인터페이스url_launcher_android/ # Android 구현url_launcher_ios/ # iOS 구현url_launcher_web/ # Web 구현url_launcher_macos/ # macOS 구현url_launcher_windows/ # Windows 구현url_launcher_linux/ # Linux 구현장점:
장점 설명 독립적 개발 각 플랫폼 전문가가 별도로 개발 가능 독립적 버전 관리 플랫폼별로 다른 릴리스 주기 선택적 의존성 필요한 플랫폼만 포함 가능 커뮤니티 기여 새 플랫폼 구현을 쉽게 추가
How
TIP연합 플러그인 의존성 구조:
# url_launcher/pubspec.yaml (메인 패키지)dependencies:url_launcher_platform_interface: ^2.0.0url_launcher_android: ^6.0.0url_launcher_ios: ^6.0.0url_launcher_web: ^2.0.0# ...# url_launcher_android/pubspec.yaml (Android 구현)dependencies:flutter:sdk: flutterurl_launcher_platform_interface: ^2.0.0flutter:plugin:implements: url_launcherplatforms:android:package: io.flutter.plugins.urllauncherpluginClass: UrlLauncherPlugin플랫폼 인터페이스 패키지:
url_launcher_platform_interface/lib/url_launcher_platform_interface.dart abstract class UrlLauncherPlatform extends PlatformInterface {static UrlLauncherPlatform _instance = /* default implementation */;static UrlLauncherPlatform get instance => _instance;static set instance(UrlLauncherPlatform instance) {PlatformInterface.verifyToken(instance, _token);_instance = instance;}Future<bool> canLaunch(String url);Future<bool> launch(String url, {required LaunchOptions options});}플랫폼 구현 패키지:
url_launcher_android/lib/url_launcher_android.dart class UrlLauncherAndroid extends UrlLauncherPlatform {static void registerWith() {UrlLauncherPlatform.instance = UrlLauncherAndroid();}@overrideFuture<bool> canLaunch(String url) async {// Android 전용 구현}@overrideFuture<bool> launch(String url, {required LaunchOptions options}) async {// Android 전용 구현}}
Watch out
WARNING연합 플러그인 개발 시 주의:
- 인터페이스 패키지의 변경은 모든 구현에 영향을 줍니다.
- 인터페이스 변경 시 메이저 버전을 올려야 합니다.
- 기존 플러그인을 연합 구조로 마이그레이션하려면 주의가 필요합니다.
# 구현 패키지의 pubspec.yamlflutter:plugin:implements: url_launcher # 어떤 플러그인을 구현하는지 명시platforms:android:package: com.example.url_launcher_androidpluginClass: UrlLauncherAndroidPlugin
챕터 5: 패키지 문서화와 배포
Why
NOTE좋은 패키지는 좋은 문서가 있어야 합니다. 문서가 없으면 다른 개발자가 사용하기 어렵고, pub.dev 점수도 낮아집니다.
pub.dev에 배포하면 전 세계 개발자가 패키지를 사용할 수 있습니다.
What
NOTE문서화 요소:
파일 용도 README.md패키지 소개, 설치 방법, 기본 사용법 CHANGELOG.md버전별 변경 사항 LICENSE라이선스 정보 example/예제 코드 API 문서 주석 코드 내 ///주석API 문서 주석 (dartdoc):
/// 문자열의 첫 글자를 대문자로 변환합니다.////// 빈 문자열이 입력되면 빈 문자열을 반환합니다.////// 예제:/// ```dart/// final result = capitalize('hello');/// print(result); // Hello/// ```////// [input]이 null이면 [ArgumentError]를 던집니다.String capitalize(String input) {if (input.isEmpty) return input;return input[0].toUpperCase() + input.substring(1);}
How
TIPREADME.md 작성 가이드:
# my_utilsA collection of useful utility functions for Flutter.## Features- String manipulation utilities- Date formatting helpers- Currency formatters## Getting startedAdd this to your `pubspec.yaml`:```yamldependencies:my_utils: ^1.0.0Usage
import 'package:my_utils/my_utils.dart';void main() {final result = capitalize('hello');print(result); // Hello}Additional information
**CHANGELOG.md 작성**:```markdown## 1.1.0- Added `toCamelCase` function- Fixed edge case in `capitalize` for empty strings## 1.0.0- Initial release- Added `capitalize` function- Added `formatDate` functionpub.dev에 배포하기:
Terminal window # 1. 배포 전 검증dart pub publish --dry-run# 2. 문제가 없으면 배포dart pub publish# 3. 첫 배포 시 pub.dev 계정 인증 필요
Watch out
WARNING배포 전 체크리스트:
□ pubspec.yaml의 version 업데이트□ CHANGELOG.md 작성□ README.md 최신 정보 반영□ 모든 테스트 통과□ dart pub publish --dry-run 성공□ 라이선스 파일 확인□ 민감한 정보 제거 (API 키 등)배포 후 주의사항:
- 한번 배포된 버전은 삭제할 수 없습니다.
- 버전 번호는 Semantic Versioning을 따르세요.
- Breaking change가 있으면 메이저 버전을 올려야 합니다.
# Semantic Versioning# MAJOR.MINOR.PATCH# 1.0.0 → 2.0.0: Breaking changes# 1.0.0 → 1.1.0: 새 기능 추가 (호환성 유지)# 1.0.0 → 1.0.1: 버그 수정
챕터 6: 기존 프로젝트에 플랫폼 추가하기
Why
NOTE이미 만든 플러그인에 새로운 플랫폼 지원을 추가해야 할 때가 있습니다. 예를 들어, Android/iOS만 지원하던 플러그인에 Web 지원을 추가하는 경우입니다.
What
NOTE
flutter create명령어의--template=plugin --platforms옵션으로 기존 프로젝트에 새 플랫폼을 추가할 수 있습니다.Terminal window # 기존 플러그인 프로젝트 폴더에서flutter create --template=plugin --platforms=web .이 명령어는:
- 새 플랫폼 폴더 생성 (예:
web/)pubspec.yaml에 플랫폼 선언 추가- 기본 구현 코드 생성
How
TIPWeb 플랫폼 추가 예제:
Terminal window # 프로젝트 루트에서 실행cd my_pluginflutter create --template=plugin --platforms=web .pubspec.yaml 자동 업데이트:
flutter:plugin:platforms:android:package: com.example.my_pluginpluginClass: MyPluginPluginios:pluginClass: MyPluginPluginweb: # 새로 추가됨pluginClass: MyPluginWebfileName: my_plugin_web.dartWeb 구현 코드:
lib/my_plugin_web.dart import 'dart:html' as html;import 'package:flutter_web_plugins/flutter_web_plugins.dart';import 'my_plugin_platform_interface.dart';class MyPluginWeb extends MyPluginPlatform {static void registerWith(Registrar registrar) {MyPluginPlatform.instance = MyPluginWeb();}@overrideFuture<String?> getPlatformVersion() async {return html.window.navigator.userAgent;}}
Watch out
WARNING플랫폼 추가 시 주의사항:
- 새 플랫폼의 구현이 기존 API와 호환되어야 합니다.
- 플랫폼별로 지원하지 않는 기능이 있을 수 있습니다.
// 플랫폼별 기능 지원 처리Future<bool> takePhoto() async {if (kIsWeb) {// Web에서는 카메라 접근이 제한적throw UnsupportedError('Web에서는 takePhoto를 지원하지 않습니다.');}return _nativeTakePhoto();}Swift Package Manager 지원 (iOS/macOS):
Flutter 3.38부터 Swift Package Manager를 지원합니다. 기존 CocoaPods 기반 플러그인도 SPM을 추가로 지원할 수 있습니다.
자세한 내용은 공식 문서를 참고하세요.
한계
이 문서는 패키지 개발의 기초를 다룹니다. 다음 주제는 별도로 학습해야 합니다.
- 복잡한 플러그인 개발: EventChannel, BasicMessageChannel 사용
- FFI 고급 기능: 복잡한 데이터 구조 전달, 메모리 관리
- 네이티브 UI 임베딩: PlatformView 사용
- CI/CD 구성: GitHub Actions로 자동 배포 설정
Footnotes
공유
이 글이 도움이 되었다면 다른 사람과 공유해주세요!