Ch 01. 나만의 패키지 설계
지금까지 우리는 남이 만든 패키지를 가져다 썼습니다. pub.dev에서 http를 설치하고, dart_frog를 설치하고, mockito를 설치했습니다. 이제는 반대로 우리가 만든 코드를 세상에 내놓을 차례입니다.
이번 프로젝트에서 만들 것은 dart_validator라는 유효성 검사 유틸리티 패키지입니다. 이메일 형식, 전화번호, 비밀번호 강도, URL 유효성 등을 검사하는 기능을 제공합니다. 단순해 보이지만, 패키지를 제대로 설계하고 배포하는 전 과정을 경험하는 것이 목표입니다.
패키지 프로젝트 생성
Dart는 패키지 전용 템플릿을 제공합니다. -t package 옵션으로 패키지 구조를 바로 생성할 수 있습니다.
dart create -t package dart_validatorcd dart_validator
생성된 구조를 살펴봅니다.
dart_validator/
├── lib/
│ ├── dart_validator.dart ← 공개 API 진입점
│ └── src/
│ └── dart_validator_base.dart ← 내부 구현
├── test/
│ └── dart_validator_test.dart
├── example/
│ └── dart_validator_example.dart
├── pubspec.yaml
├── README.md
├── CHANGELOG.md
└── analysis_options.yaml
lib/ 아래의 두 파일이 핵심입니다. dart_validator.dart는 외부에 공개할 API를 정의하고, lib/src/ 안의 파일들은 내부 구현을 담습니다.
공개 API와 내부 구현의 분리
Dart 패키지에서 가장 중요한 원칙 중 하나는 공개 API와 내부 구현을 명확히 분리하는 것입니다.
lib/src/ 폴더에 있는 파일은 외부 패키지가 직접 import 'package:dart_validator/src/...'로 접근하더라도 pub.dev 정책상 권장되지 않습니다. 사용자는 오직 lib/dart_validator.dart에서 export된 것만 사용해야 합니다.
이렇게 설계하면 두 가지 장점이 생깁니다. 첫째, 내부 구현을 자유롭게 바꿀 수 있습니다. 둘째, 사용자는 깔끔한 API만 보게 됩니다.
패키지 API 설계
dart_validator가 제공할 기능을 먼저 설계합니다. 실제 코드를 작성하기 전에 사용자 관점에서 API를 그려봅니다.
// 사용자가 이렇게 사용할 수 있어야 합니다
import 'package:dart_validator/dart_validator.dart';
void main() {
// 이메일 검사
print(Validators.isEmail('[email protected]')); // true
print(Validators.isEmail('not-an-email')); // false
// 전화번호 검사 (한국 형식)
print(Validators.isPhone('010-1234-5678')); // true
print(Validators.isPhone('01012345678')); // true
// 비밀번호 강도
print(Validators.passwordStrength('weak')); // PasswordStrength.weak
print(Validators.passwordStrength('Str0ng!pw')); // PasswordStrength.strong
// URL 검사
print(Validators.isUrl('https://dart.dev')); // true
// 체인 방식으로 사용
final result = ValidatorChain('[email protected]')
.isEmail()
.maxLength(100)
.validate();
print(result.isValid); // true
}
이처럼 API를 먼저 그려보는 작업을 "API 우선 설계(API-first design)"라고 합니다. 코드를 작성하기 전에 사용자 경험을 먼저 생각하는 것입니다.
pubspec.yaml 구성
# 새 파일: pubspec.yamlname: dart_validatordescription: A comprehensive validation utility library for Dart. Validates emails, phone numbers, URLs, passwords, and more.version: 0.1.0homepage: https://github.com/yourusername/dart_validatorenvironment: sdk: '>=3.0.0 <4.0.0'dev_dependencies: lints: ^3.0.0 test: ^1.24.0
description은 pub.dev 검색 결과에 표시됩니다. 짧고 명확하게 작성합니다. homepage는 GitHub 저장소 주소를 넣으면 pub.dev에서 링크로 표시됩니다.
내부 구현 파일 구조 설계
하나의 파일에 모든 것을 넣지 않습니다. 기능별로 파일을 분리합니다.
lib/
├── dart_validator.dart ← export 모음
└── src/
├── validators.dart ← 정적 메서드 모음
├── validator_chain.dart ← 체인 방식 클래스
├── password_strength.dart ← 비밀번호 강도 enum
└── validation_result.dart ← 검사 결과 클래스
진입점 파일은 모든 내부 파일을 export합니다.
// 새 파일: lib/dart_validator.dart
library dart_validator;
export 'src/validators.dart';
export 'src/validator_chain.dart';
export 'src/password_strength.dart';
export 'src/validation_result.dart';
library dart_validator; 선언은 선택 사항이지만, dartdoc이 라이브러리 문서를 생성할 때 이 이름을 사용하므로 명시하는 것이 좋습니다.
ValidationResult 클래스 설계
유효성 검사 결과를 담는 클래스를 먼저 만듭니다. bool 하나로 반환하는 것보다 오류 메시지까지 담을 수 있어서 실용적입니다.
// 새 파일: lib/src/validation_result.dart
/// 유효성 검사 결과를 담는 불변 클래스입니다.
class ValidationResult {
/// 검사 통과 여부입니다.
final bool isValid;
/// 검사 실패 시 오류 메시지 목록입니다.
final List<String> errors;
const ValidationResult._({
required this.isValid,
required this.errors,
});
/// 검사를 통과한 결과를 만듭니다.
factory ValidationResult.valid() {
return const ValidationResult._(isValid: true, errors: []);
}
/// 검사를 실패한 결과를 만듭니다.
factory ValidationResult.invalid(List<String> errors) {
return ValidationResult._(isValid: false, errors: List.unmodifiable(errors));
}
@override
String toString() {
if (isValid) return 'ValidationResult(valid)';
return 'ValidationResult(invalid: ${errors.join(', ')})';
}
}
생성자를 private(._)으로 만들고 factory 생성자로만 인스턴스를 만들 수 있게 했습니다. 이렇게 하면 잘못된 상태(isValid: true이면서 errors가 있는 경우 등)를 원천 차단할 수 있습니다.
PasswordStrength enum 설계
// 새 파일: lib/src/password_strength.dart
/// 비밀번호 강도를 나타내는 열거형입니다.
enum PasswordStrength {
/// 8자 미만이거나 단순한 패턴의 비밀번호입니다.
weak,
/// 기본 조건을 충족하지만 개선이 필요한 비밀번호입니다.
fair,
/// 영문 대소문자, 숫자, 특수문자를 포함한 강력한 비밀번호입니다.
strong;
/// 강도를 사람이 읽기 좋은 한국어로 반환합니다.
String get label {
switch (this) {
case PasswordStrength.weak:
return '약함';
case PasswordStrength.fair:
return '보통';
case PasswordStrength.strong:
return '강함';
}
}
}
Validators 클래스 초안
// 새 파일: lib/src/validators.dart
/// 다양한 유효성 검사 정적 메서드를 제공하는 클래스입니다.
class Validators {
Validators._(); // 인스턴스 생성 방지
static final RegExp _emailRegex = RegExp(
r'^[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}$',
);
static final RegExp _phoneKrRegex = RegExp(
r'^0\d{1,2}[-\s]?\d{3,4}[-\s]?\d{4}$',
);
static final RegExp _urlRegex = RegExp(
r'^https?://[^\s/$.?#].[^\s]*$',
caseSensitive: false,
);
/// 이메일 형식인지 검사합니다.
static bool isEmail(String value) => _emailRegex.hasMatch(value);
/// 한국 전화번호 형식인지 검사합니다.
static bool isPhone(String value) => _phoneKrRegex.hasMatch(value);
/// URL 형식인지 검사합니다.
static bool isUrl(String value) => _urlRegex.hasMatch(value);
/// 값이 비어 있지 않은지 검사합니다.
static bool isNotEmpty(String value) => value.trim().isNotEmpty;
/// 최소 길이 조건을 만족하는지 검사합니다.
static bool minLength(String value, int min) => value.length >= min;
/// 최대 길이 조건을 만족하는지 검사합니다.
static bool maxLength(String value, int max) => value.length <= max;
/// 비밀번호 강도를 반환합니다.
static PasswordStrength passwordStrength(String value) {
if (value.length < 8) return PasswordStrength.weak;
final hasUpper = value.contains(RegExp(r'[A-Z]'));
final hasLower = value.contains(RegExp(r'[a-z]'));
final hasDigit = value.contains(RegExp(r'\d'));
final hasSpecial = value.contains(RegExp(r'[!@#$%^&*(),.?":{}|<>]'));
final score = [hasUpper, hasLower, hasDigit, hasSpecial]
.where((e) => e)
.length;
if (score >= 4) return PasswordStrength.strong;
if (score >= 2) return PasswordStrength.fair;
return PasswordStrength.weak;
}
}
설계 결과 정리
이번 챕터에서 설계한 내용을 정리합니다.
dart create -t package명령으로 패키지 프로젝트를 생성했습니다.lib/에 공개 API 파일,lib/src/에 내부 구현을 분리하는 원칙을 적용했습니다.- 사용자 관점에서 API를 먼저 설계하고, 그에 맞게 클래스를 분리했습니다.
ValidationResult,PasswordStrength,Validators세 핵심 타입의 초안을 완성했습니다.
다음 챕터에서는 dartdoc 주석으로 API 문서를 작성하고, README와 CHANGELOG를 구성합니다.