iBetter Books
수정

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

[![pub package](https://img.shields.io/pub/v/dart_validator.svg)](https://pub.dev/packages/dart_validator)
[![Dart SDK Version](https://badgen.net/pub/sdk-version/dart_validator)](https://pub.dev/packages/dart_validator)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](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/ 폴더에 실행 가능한 예제를 만들겠습니다.