Flutter 튜토리얼 67편: 패키지 개발하기

요약#

핵심 요지#

  • 문제 정의: 프로젝트 간에 코드를 재사용하거나 커뮤니티와 공유하려면 패키지로 만들어야 한다.
  • 핵심 주장: Flutter는 패키지 생성부터 배포까지 체계적인 도구를 제공한다.
  • 주요 근거: flutter create --template 명령어로 패키지 골격을 자동 생성하고, pub publish로 pub.dev에 배포할 수 있다.
  • 실무 기준: 순수 Dart 패키지, 플러그인 패키지, FFI 패키지 중 목적에 맞는 유형을 선택해야 한다.
  • 한계: 플러그인 개발은 각 플랫폼의 네이티브 언어(Kotlin, Swift 등)에 대한 이해가 필요하다.

문서가 설명하는 범위#

  • 패키지 유형과 구조 이해하기
  • Dart 패키지 생성 및 구현
  • 플러그인 패키지 개발 기초
  • 연합 플러그인(Federated Plugin) 아키텍처
  • 패키지 문서화와 pub.dev 배포

읽는 시간: 20분 | 난이도: 고급


참고 자료#


문제 상황#

프로젝트를 진행하다 보면 여러 앱에서 동일한 코드를 사용해야 할 때가 있습니다. 또는 유용한 기능을 커뮤니티와 공유하고 싶을 수도 있습니다.

// 여러 프로젝트에서 같은 유틸리티 함수를 복사-붙여넣기?
String formatCurrency(double amount) {
return '\$${amount.toStringAsFixed(2)}';
}
// 동일한 위젯을 매번 다시 작성?
class CustomButton extends StatelessWidget {
// ...중복 코드
}

문제는 다음과 같습니다.

  • 코드를 복사하면 버그 수정이나 업데이트를 모든 프로젝트에 적용해야 한다.
  • 유용한 코드를 공유하려면 표준화된 형식이 필요하다.
  • 네이티브 기능에 접근하려면 플랫폼별 코드가 필요하다.

해결 방법#

Flutter는 세 가지 유형의 패키지를 지원합니다. 목적에 맞는 패키지 유형을 선택하고, 체계적인 구조로 개발하면 됩니다.

챕터 1: 패키지 유형 이해하기#

Why#

NOTE

패키지를 만들기 전에 어떤 유형이 적합한지 결정해야 합니다. 잘못된 유형을 선택하면 나중에 구조를 변경하기 어렵습니다.

세 가지 유형은 다음과 같습니다.

  • Dart 패키지: 순수 Dart 코드만 포함
  • 플러그인 패키지: 네이티브 코드 포함 (Platform Channel 사용)
  • FFI 패키지: 네이티브 코드 포함 (dart 사용)

What#

NOTE

Dart 패키지

순수 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#

TIP

1단계: 패키지 생성

Terminal window
flutter create --template=package my_utils
cd my_utils

2단계: pubspec.yaml 설정

name: my_utils
description: A collection of useful utility functions.
version: 1.0.0
homepage: https://github.com/username/my_utils
environment:
sdk: ^3.0.0
flutter: ">=3.0.0"
dependencies:
flutter:
sdk: flutter
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^5.0.0

3단계: 코드 구현

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.dart

src 폴더 규칙:

  • 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.yaml

Dart 측 코드 (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_plugin
import io.flutter.embedding.engine.plugins.FlutterPlugin
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
import io.flutter.plugin.common.MethodChannel.MethodCallHandler
import io.flutter.plugin.common.MethodChannel.Result
class MyPluginPlugin: FlutterPlugin, MethodCallHandler {
private lateinit var channel : MethodChannel
override 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 Flutter
import UIKit
public 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 example
flutter run

pubspec.yaml 플랫폼 선언:

flutter:
plugin:
platforms:
android:
package: com.example.my_plugin
pluginClass: MyPluginPlugin
ios:
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.0
url_launcher_android: ^6.0.0
url_launcher_ios: ^6.0.0
url_launcher_web: ^2.0.0
# ...
# url_launcher_android/pubspec.yaml (Android 구현)
dependencies:
flutter:
sdk: flutter
url_launcher_platform_interface: ^2.0.0
flutter:
plugin:
implements: url_launcher
platforms:
android:
package: io.flutter.plugins.urllauncher
pluginClass: 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();
}
@override
Future<bool> canLaunch(String url) async {
// Android 전용 구현
}
@override
Future<bool> launch(String url, {required LaunchOptions options}) async {
// Android 전용 구현
}
}

Watch out#

WARNING

연합 플러그인 개발 시 주의:

  • 인터페이스 패키지의 변경은 모든 구현에 영향을 줍니다.
  • 인터페이스 변경 시 메이저 버전을 올려야 합니다.
  • 기존 플러그인을 연합 구조로 마이그레이션하려면 주의가 필요합니다.
# 구현 패키지의 pubspec.yaml
flutter:
plugin:
implements: url_launcher # 어떤 플러그인을 구현하는지 명시
platforms:
android:
package: com.example.url_launcher_android
pluginClass: 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#

TIP

README.md 작성 가이드:

# my_utils
A collection of useful utility functions for Flutter.
## Features
- String manipulation utilities
- Date formatting helpers
- Currency formatters
## Getting started
Add this to your `pubspec.yaml`:
```yaml
dependencies:
my_utils: ^1.0.0

Usage#

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` function

pub.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#

TIP

Web 플랫폼 추가 예제:

Terminal window
# 프로젝트 루트에서 실행
cd my_plugin
flutter create --template=plugin --platforms=web .

pubspec.yaml 자동 업데이트:

flutter:
plugin:
platforms:
android:
package: com.example.my_plugin
pluginClass: MyPluginPlugin
ios:
pluginClass: MyPluginPlugin
web: # 새로 추가됨
pluginClass: MyPluginWeb
fileName: my_plugin_web.dart

Web 구현 코드:

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();
}
@override
Future<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#

  1. Platform Channel(플랫폼 채널): Flutter(Dart)와 네이티브 코드(Kotlin, Swift 등) 간에 메시지를 주고받는 통신 메커니즘이다.

  2. dart(다트 FFI): Foreign Function Interface의 약자로, Dart에서 C/C++ 등의 네이티브 코드를 직접 호출할 수 있게 해주는 라이브러리이다.

  3. Federated Plugin(연합 플러그인): 플러그인의 인터페이스와 각 플랫폼 구현을 별도의 패키지로 분리한 아키텍처로, 독립적인 개발과 유지보수가 가능하다.

공유

이 글이 도움이 되었다면 다른 사람과 공유해주세요!

Flutter 튜토리얼 67편: 패키지 개발하기
https://moodturnpost.net/posts/flutter/flutter-packages-developing/
작성자
Moodturn
게시일
2026-01-08
Moodturn

목차