Ch 02. API 설계와 문서화
코드를 잘 짜는 것만큼 중요한 것이 문서화입니다. pub.dev에 올라간 패키지를 처음 보는 사람은 README와 API 문서를 보고 사용 여부를 결정합니다. 문서가 부실하면 아무리 훌륭한 코드도 외면받습니다.
Dart에는 dartdoc이라는 공식 문서 생성 도구가 있습니다. /// 주석을 코드에 달면 자동으로 HTML 문서를 만들어줍니다. pub.dev의 API 문서 탭도 dartdoc으로 생성됩니다.
dartdoc 주석 작성법
///로 시작하는 줄이 dartdoc 주석입니다. /* */ 스타일도 지원하지만 ///가 Dart 관례입니다.
// 수정: lib/src/validators.dart
import 'password_strength.dart';
/// 다양한 유효성 검사 정적 메서드를 제공하는 클래스입니다.
///
/// 모든 메서드는 정적(static)이므로 인스턴스를 생성하지 않고
/// `Validators.isEmail(value)` 형태로 바로 사용할 수 있습니다.
///
/// ## 예제
///
/// ```dart
/// import 'package:dart_validator/dart_validator.dart';
///
/// void main() {
/// print(Validators.isEmail('[email protected]')); // true
/// print(Validators.isPhone('010-1234-5678')); // true
/// }
/// ```
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,
);
/// 이메일 형식인지 검사합니다.
///
/// RFC 5322 표준을 간략화한 정규식을 사용합니다.
/// 로컬 파트와 도메인 파트가 `@`로 구분되어야 합니다.
///
/// ```dart
/// Validators.isEmail('[email protected]'); // true
/// Validators.isEmail('invalid'); // false
/// ```
static bool isEmail(String value) => _emailRegex.hasMatch(value);
/// 한국 전화번호 형식인지 검사합니다.
///
/// 다음 형식을 모두 허용합니다.
/// - `010-1234-5678`
/// - `01012345678`
/// - `010 1234 5678`
///
/// ```dart
/// Validators.isPhone('010-1234-5678'); // true
/// Validators.isPhone('01012345678'); // true
/// ```
static bool isPhone(String value) => _phoneKrRegex.hasMatch(value);
/// URL 형식인지 검사합니다.
///
/// `http://` 또는 `https://`로 시작하는 URL을 허용합니다.
///
/// ```dart
/// Validators.isUrl('https://dart.dev'); // true
/// Validators.isUrl('ftp://example'); // false
/// ```
static bool isUrl(String value) => _urlRegex.hasMatch(value);
/// 값이 비어 있지 않은지 검사합니다.
///
/// 공백만 있는 문자열도 빈 것으로 처리합니다.
///
/// ```dart
/// Validators.isNotEmpty('hello'); // true
/// Validators.isNotEmpty(' '); // false
/// ```
static bool isNotEmpty(String value) => value.trim().isNotEmpty;
/// 최소 길이 조건을 만족하는지 검사합니다.
///
/// [min] 이상의 길이여야 합니다.
static bool minLength(String value, int min) => value.length >= min;
/// 최대 길이 조건을 만족하는지 검사합니다.
///
/// [max] 이하의 길이여야 합니다.
static bool maxLength(String value, int max) => value.length <= max;
/// 비밀번호 강도를 반환합니다.
///
/// 대문자, 소문자, 숫자, 특수문자 포함 여부로 강도를 계산합니다.
///
/// | 조건 충족 수 | 강도 |
/// |-------------|------|
/// | 8자 미만 | weak |
/// | 1~3가지 | fair |
/// | 4가지 | strong |
///
/// ```dart
/// Validators.passwordStrength('weak'); // PasswordStrength.weak
/// Validators.passwordStrength('Password1!'); // PasswordStrength.strong
/// ```
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;
}
}
주석에서 [min]처럼 대괄호로 감싸면 dartdoc이 해당 파라미터나 클래스로 자동 링크를 생성합니다. 코드 블록은 ```dart로 감싸서 예제를 직접 보여줍니다.
ValidatorChain 클래스 구현
체인 방식은 여러 검사를 연결해서 한 번에 실행하는 패턴입니다. 빌더 패턴(Builder Pattern)의 응용입니다.
// 새 파일: lib/src/validator_chain.dart
import 'validators.dart';
import 'validation_result.dart';
import 'password_strength.dart';
/// 여러 유효성 검사를 체인으로 연결하는 클래스입니다.
///
/// 각 검사 메서드는 [ValidatorChain] 자신을 반환하므로
/// 메서드 체이닝으로 여러 검사를 연결할 수 있습니다.
///
/// ## 예제
///
/// ```dart
/// final result = ValidatorChain('[email protected]')
/// .isEmail()
/// .maxLength(100)
/// .validate();
///
/// if (!result.isValid) {
/// print(result.errors); // 오류 메시지 목록
/// }
/// ```
class ValidatorChain {
final String _value;
final List<String> _errors = [];
/// 검사할 [value]로 체인을 시작합니다.
ValidatorChain(this._value);
/// 이메일 형식인지 검사합니다.
ValidatorChain isEmail({String? message}) {
if (!Validators.isEmail(_value)) {
_errors.add(message ?? '올바른 이메일 형식이 아닙니다.');
}
return this;
}
/// 한국 전화번호 형식인지 검사합니다.
ValidatorChain isPhone({String? message}) {
if (!Validators.isPhone(_value)) {
_errors.add(message ?? '올바른 전화번호 형식이 아닙니다.');
}
return this;
}
/// URL 형식인지 검사합니다.
ValidatorChain isUrl({String? message}) {
if (!Validators.isUrl(_value)) {
_errors.add(message ?? '올바른 URL 형식이 아닙니다.');
}
return this;
}
/// 비어 있지 않은지 검사합니다.
ValidatorChain isNotEmpty({String? message}) {
if (!Validators.isNotEmpty(_value)) {
_errors.add(message ?? '값을 입력해주세요.');
}
return this;
}
/// 최소 길이 조건을 검사합니다.
ValidatorChain minLength(int min, {String? message}) {
if (!Validators.minLength(_value, min)) {
_errors.add(message ?? '최소 $min자 이상 입력해주세요.');
}
return this;
}
/// 최대 길이 조건을 검사합니다.
ValidatorChain maxLength(int max, {String? message}) {
if (!Validators.maxLength(_value, max)) {
_errors.add(message ?? '최대 $max자까지 입력 가능합니다.');
}
return this;
}
/// 비밀번호가 최소 [strength] 이상의 강도인지 검사합니다.
ValidatorChain hasPasswordStrength(
PasswordStrength strength, {
String? message,
}) {
final actual = Validators.passwordStrength(_value);
if (actual.index < strength.index) {
_errors.add(message ?? '비밀번호 강도가 부족합니다. (현재: ${actual.label})');
}
return this;
}
/// 지금까지의 검사 결과를 [ValidationResult]로 반환합니다.
ValidationResult validate() {
if (_errors.isEmpty) return ValidationResult.valid();
return ValidationResult.invalid(List.from(_errors));
}
}
README.md 작성
README는 pub.dev의 첫 화면에 표시됩니다. 사용자가 처음 보는 문서입니다.
// 새 파일: README.md
# dart_validator
[](https://pub.dev/packages/dart_validator)
[](https://pub.dev/packages/dart_validator)
[](https://opensource.org/licenses/MIT)
A comprehensive validation utility library for Dart.
Validates emails, phone numbers, URLs, passwords, and more.
## Features
- Email address validation
- Korean phone number validation
- URL validation
- Password strength checker
- Chainable validator for multiple rules
## Getting Started
Add `dart_validator` to your `pubspec.yaml`:
```yaml
dependencies:
dart_validator: ^0.1.0
Usage
Static methods
import 'package:dart_validator/dart_validator.dart';
Validators.isEmail('[email protected]'); // true
Validators.isPhone('010-1234-5678'); // true
Validators.isUrl('https://dart.dev'); // true
Validators.passwordStrength('Str0ng!'); // PasswordStrength.strong
Chainable validator
final result = ValidatorChain('[email protected]')
.isNotEmpty()
.isEmail()
.maxLength(100)
.validate();
if (result.isValid) {
print('Valid!');
} else {
print(result.errors);
}
License
MIT License. See LICENSE for details.
### CHANGELOG.md 작성
CHANGELOG는 버전별 변경 내역을 기록합니다. pub.dev에서 "Changelog" 탭으로 표시됩니다.
```markdown
// 새 파일: CHANGELOG.md
## 0.1.0
- Initial release.
- Added `Validators` class with email, phone, URL, password strength checks.
- Added `ValidatorChain` for chainable validation.
- Added `ValidationResult` for structured validation results.
- Added `PasswordStrength` enum.
문서 생성 확인
dartdoc 명령으로 문서를 로컬에서 미리 볼 수 있습니다.
dart pub global activate dartdocdartdoc
doc/api/ 폴더에 HTML 문서가 생성됩니다. index.html을 브라우저로 열면 pub.dev와 동일한 형태의 문서를 확인할 수 있습니다.
문서화 점수를 확인하려면 pana 도구를 사용합니다.
dart pub global activate panapana
pana는 pub.dev에서 패키지의 점수를 계산하는 도구입니다. 130점 만점에서 문서화, 테스트, 분석 등 여러 항목을 평가합니다. pana 출력에서 각 항목의 감점 이유를 확인하고 개선할 수 있습니다.
이번 챕터 정리
///주석으로 클래스, 메서드, 파라미터에 dartdoc 문서를 달았습니다.[파라미터명]문법으로 자동 링크를 만들고, 코드 블록으로 예제를 포함했습니다.ValidatorChain으로 체이닝 방식 API를 구현했습니다.- README.md와 CHANGELOG.md를 작성하여 pub.dev에 표시되는 문서를 완성했습니다.
다음 챕터에서는 100% 커버리지를 목표로 테스트를 작성하고, example/ 폴더에 실행 가능한 예제를 만들겠습니다.