enum과 extension으로 앱 확장하기

enum과 extension으로 앱 확장하기

enhanced enum과 extension 같은 고급 기능을 살펴보면서 Dart 실력을 키워 봐요. 애플리케이션의 출력 형식과 색을 개선해 더 사용하기 좋게 만들어 볼게요.

출처: 원문

본문

이 챕터에서는 커맨드라인 애플리케이션의 사용자 경험을 개선해 주는 Dart 고급 기능을 살펴볼 거예요. 콘솔 색을 관리하는 데 enhanced enum을, 기존 타입에 새 기능을 추가하는 데 extension을 어떻게 쓰는지 배우면서, 애플리케이션을 더 인터랙티브하고 시각적으로 매력적으로 만들어 볼 거예요.

이번 장에서 할 일

  • 필드와 메서드를 가진 enhanced enum 사용하기
  • extension으로 기존 타입 확장하기
  • 알록달록한 콘솔 출력 추가하기

사전 준비

이 챕터를 시작하기 전에 다음을 확인하세요:

  • 6장을 마쳤고 dartpedia 프로젝트로 작업할 수 있는 Dart 개발 환경이 갖춰져 있을 것.
  • 변수, 함수, 제어 흐름 같은 기본 프로그래밍 개념에 익숙할 것.
  • Dart의 패키지와 라이브러리 개념을 이해하고 있을 것.
  • 클래스와 enum 같은 객체지향 프로그래밍 개념을 기본적으로 이해하고 있을 것.

Tasks

여러분의 Dartpedia CLI 애플리케이션에 색을 더하고 텍스트 형식을 개선해 사용자 경험을 향상시킬 거예요.

Task 1: 콘솔 색 enum 개선하기

먼저 콘솔 출력에 색을 추가해 볼게요. ConsoleColor enum에 RGB 값을 포함시키고 텍스트에 색을 적용하는 메서드도 넣을 거예요.

  • command_runner/lib/src/console.dart 파일을 만드세요.

  • 다음 코드를 추가해 ConsoleColor enum을 정의하세요:

command_runner/lib/src/console.dart

import 'dart:io';

const String ansiEscapeLiteral = '\x1B';

/// Splits strings on `\n` characters, then writes each line to the
/// console. [duration] defines how many milliseconds there will be
/// between each line print.
Future<void> write(String text, {int duration = 50}) async {
  final List<String> lines = text.split('\n');
  for (final String l in lines) {
    await _delayedPrint('$l \n', duration: duration);
  }
}

/// Prints line-by-line
Future<void> _delayedPrint(String text, {int duration = 0}) async {
  return Future<void>.delayed(
    Duration(milliseconds: duration),
    () => stdout.write(text),
  );
}

/// RGB formatted colors that are used to style input
///
/// All colors from Dart's brand styleguide
///
/// As a demo, only includes colors this program cares about.
/// If you want to use more colors, add them here.
enum ConsoleColor {
  /// Sky blue - #b8eafe
  lightBlue(184, 234, 254),

  /// Accent colors from Dart's brand guidelines
  /// Warm red - #F25D50
  red(242, 93, 80),

  /// Light yellow - #F9F8C4
  yellow(249, 248, 196),

  /// Light grey, good for text, #F8F9FA
  grey(240, 240, 240),

  ///
  white(255, 255, 255);

  const ConsoleColor(this.r, this.g, this.b);

  final int r;
  final int g;
  final int b;
}

이 enum은 각각의 RGB 값과 함께 콘솔 색 집합을 정의해요. 각 색은 ConsoleColor enum의 상수 인스턴스예요.

  • 텍스트에 색을 적용하는 메서드를 ConsoleColor enum에 추가하세요:

command_runner/lib/src/console.dart

enum ConsoleColor {
  // ... (existing enum values)

  const ConsoleColor(this.r, this.g, this.b);

  final int r;
  final int g;
  final int b;

  /// Change text color for all future output (until reset)
  /// ```dart
  /// print('hello'); // prints in terminal default color
  /// print(ConsoleColor.red.enableForeground);
  /// print('hello'); // prints in red color
  /// ```
  String get enableForeground => '$ansiEscapeLiteral[38;2;$r;$g;${b}m';

  /// Change text color for all future output (until reset)
  /// ```dart
  /// print('hello'); // prints in terminal default color
  /// print(ConsoleColor.red.enableBackground);
  /// print('hello'); // prints with red background color
  /// ```
  String get enableBackground => '$ansiEscapeLiteral[48;2;$r;$g;${b}m';

  /// Reset text and background color to terminal defaults
  static String get reset => '$ansiEscapeLiteral[0m';

  /// Sets text color for the input
  String applyForeground(String text) {
    return '$ansiEscapeLiteral[38;2;$r;$g;${b}m$text$reset';
  }

  /// Sets background color and then resets the color change
  String applyBackground(String text) {
    return '$ansiEscapeLiteral[48;2;$r;$g;${b}m$text$ansiEscapeLiteral[0m';
  }
}

이 메서드들은 ANSI 이스케이프 코드를 사용해 텍스트에 전경색과 배경색을 적용해요. applyForegroundapplyBackground 메서드는 ANSI 이스케이프 코드가 적용된 문자열을 반환해요.

Task 2: String extension 만들기

다음으로 String 클래스에 콘솔 색을 적용하고 텍스트를 형식화하는 유틸리티 메서드를 추가하는 extension을 만들어 볼게요.

  • command_runner/lib/src/console.dart 파일에 다음 코드를 추가하세요:

command_runner/lib/src/console.dart

// Add this code to the bottom of the file
extension TextRenderUtils on String {
  String get errorText => ConsoleColor.red.applyForeground(this);
  String get instructionText => ConsoleColor.yellow.applyForeground(this);
  String get titleText => ConsoleColor.lightBlue.applyForeground(this);

  List<String> splitLinesByLength(int length) {
    final List<String> words = split(' ');
    final List<String> output = <String>[];
    final StringBuffer strBuffer = StringBuffer();
    for (int i = 0; i < words.length; i++) {
      final String word = words[i];
      if (strBuffer.length + word.length <= length) {
        strBuffer.write(word.trim());
        if (strBuffer.length + 1 <= length) {
          strBuffer.write(' ');
        }
      }
      // If the next word surpasses length, start the next line
      if (i + 1 < words.length &&
          words[i + 1].length + strBuffer.length + 1 > length) {
        output.add(strBuffer.toString().trim());
        strBuffer.clear();
      }
    }

    // Add left overs
    output.add(strBuffer.toString().trim());
    return output;
  }
}

이 코드는 String 클래스에 TextRenderUtils라는 extension을 정의해요. 콘솔 색을 적용하는 세 개의 getter 메서드(errorText, instructionText, titleText)를 추가하고, 문자열을 지정한 길이의 줄로 나누는 splitLinesByLength 메서드도 추가해요.

Task 3: command_runner 패키지 갱신하기

command_runner 패키지가 console.dart를 export하도록 갱신해요.

  • command_runner/lib/command_runner.dart를 열고 다음 줄을 추가하세요:

command_runner/lib/command_runner.dart

library;

export 'src/arguments.dart';
export 'src/command_runner_base.dart';
export 'src/exceptions.dart';
export 'src/help_command.dart';
export 'src/console.dart'; // Add this line

// TODO: Export any libraries intended for clients of this package.

Task 4: 알록달록한 echo 명령 구현하기

마지막으로 출력을 테스트할 예시 명령을 구현해 볼게요. 여러분의 패키지를 사용할 개발자를 위해 패키지의 예시 용법을 구현해 두는 건 좋은 관행이에요. 이 예시는 콘솔 출력을 다채롭게 만드는 명령을 만들어요.

  • example/command_runner_example.dart 파일을 여세요.

  • 파일 내용을 다음 코드로 바꾸세요:

command_runner/example/command_runner_example.dart

import 'dart:async';

import 'package:command_runner/command_runner.dart';

class PrettyEcho extends Command {
  PrettyEcho() {
    addFlag(
      'blue-only',
      abbr: 'b',
      help: 'When true, the echoed text will all be blue.',
    );
  }

  @override
  String get name => 'echo';

  @override
  bool get requiresArgument => true;

  @override
  String get description => 'Print input, but colorful.';

  @override
  String? get help =>
      'echos a String provided as an argument with ANSI coloring,';

  @override
  String? get valueHelp => 'STRING';

  @override
  FutureOr<String> run(ArgResults arg) {
    if (arg.commandArg == null) {
      throw ArgumentException(
        'This argument requires one positional argument',
        name,
      );
    }

    List<String> prettyWords = [];
    var words = arg.commandArg!.split(' ');
    for (var i = 0; i < words.length; i++) {
      var word = words[i];
      switch (i % 3) {
        case 0:
          prettyWords.add(word.titleText);
        case 1:
          prettyWords.add(word.instructionText);
        case 2:
          prettyWords.add(word.errorText);
      }
    }

    return prettyWords.join(' ');
  }
}

void main(List<String> arguments) {
  final runner = CommandRunner()..addCommand(PrettyEcho());

  runner.run(arguments);
}

이 코드는 Command 클래스를 상속하는 PrettyEcho 명령을 정의해요. 인자로 문자열을 받아 문자열 안의 각 단어 위치에 따라 단어마다 다른 색을 적용해요. run 메서드는 TextRenderUtils extension의 titleText, instructionText, errorText getter 메서드를 사용해 색을 적용해요.

  • /dartpedia/command_runner로 이동해 다음 명령을 실행하세요:
dart run example/command_runner_example.dart echo "hello world goodbye"

콘솔에 다음 텍스트가 출력되는 걸 볼 수 있을 거예요. 첫 단어는 옅은 파란색, 두 번째는 노란색, 세 번째는 빨간색으로 나타나요.

hello world goodbye

Review

무엇을 해냈나요

이번 레슨에서 만들고 배운 내용을 정리해 볼게요.

  • 필드와 메서드를 가진 enhanced enum 사용하기

    r, g, b 필드와 applyForeground() 같은 메서드를 가진 ConsoleColor를 enhanced enum으로 만들었어요. Enhanced enum은 생성자, 프로퍼티, 메서드를 가질 수 있어서 enum과 일반 클래스의 장점을 결합해요.

  • extension으로 기존 타입 확장하기

    String 클래스에 TextRenderUtils extension을 만들어 모든 문자열에 errorText, titleText 같은 getter를 추가했어요. Extension은 타입을 수정하거나 서브클래스를 만들지 않고도 어떤 타입에든 기능을 추가하게 해 주는데, 특히 서브클래스를 만들 수 없는 문자열 같은 경우에 유용해요.

  • 알록달록한 콘솔 출력 추가하기

    CLI의 가독성과 사용자 경험을 개선하려고 ANSI 이스케이프 코드로 터미널 출력에 색을 입혔어요. 특히 ConsoleColor를 사용해서 오류는 빨간색, 제목은 파란색, 안내문은 노란색으로 표시하도록 CLI를 갱신했어요.

Quiz

이해를 확인해 봐요

Dart에서 enhanced enum이란 무엇인가요?

  • 메서드와 프로퍼티를 가질 수 있는 enum — 맞아요! Enhanced enum은 enum 타입에 필드, 생성자, 메서드를 추가할 수 있게 해 주어 단순 열거형보다 훨씬 강력해요.
  • 문자열 값만 가질 수 있는 enum — 아니에요. Enhanced enum은 특정 타입에 한정되지 않아요 "enhanced"라는 말은 기본 열거형 너머의 추가 기능을 뜻해요.
  • Dart 컴파일러가 자동 생성하는 enum — 아니에요. enum은 여러분이 직접 정의하는 것이지 자동 생성되는 게 아니에요. "Enhanced"는 enum 정의 안에서 할 수 있는 일을 가리켜요.
  • Flutter 애플리케이션에서만 쓸 수 있는 enum — 아니에요. Enhanced enum은 Flutter뿐 아니라 모든 Dart 애플리케이션에서 쓸 수 있는 핵심 Dart 기능이에요.

Dart extension이란 무엇인가요?

  • 기존 클래스에 새 메서드를 추가하는 방법 — 맞아요! Extension은 기존 타입을 수정하거나 서브클래스를 만들지 않고도 새 기능을 추가하게 해 줘요.
  • 기존 클래스에서 새 클래스를 만드는 방법 — 아니에요. 기존 클래스에서 새 클래스를 만드는 건 상속(extends)이나 컴포지션으로 해요. Extension은 새 타입을 만들지 않아요.
  • Dart에서 새 연산자를 정의하는 방법 — 아니에요. Extension이 연산자를 정의할 수는 있지만 그것이 주되거나 유일한 목적은 아니에요. 연산자보다 훨씬 일반적이에요.
  • Dart에서 새 변수를 만드는 방법 — 아니에요. 변수는 varfinal 같은 선언으로 만들어요. Extension은 다른 목적을 가져요.

StringcapitalizeWords() 메서드를 추가하고 싶은데 String 클래스를 수정할 수 없어요. 가장 좋은 방법은 무엇인가요?

  • 메서드를 추가하는 String extension을 만든다 — 맞아요! Extension은 기존 타입에 메서드를 추가하게 해 줘요. 그러면 'hello world'.capitalizeWords()를 마치 capitalizeWordsString에 내장된 것처럼 호출할 수 있어요.
  • String을 감싸고 메서드를 추가하는 MyString 클래스를 만든다 — 아니에요. 래퍼 클래스도 동작하지만 모든 곳에서 타입 변환을 해야 해요. 감싸지 않아도 되는 더 깔끔한 방법이 있어요.
  • 새 메서드를 가진 String의 서브클래스를 만든다 — 아니에요. String은 서브클래스를 만들 수 없는 핵심 Dart 타입이에요. 여기에 메서드를 추가하려면 다른 방법이 필요해요.
  • 대신 전역 함수 capitalizeWords(String s)를 만든다 — 아니에요. 전역 함수도 동작하지만 자연스럽게 읽히지 않고 발견하기 어려운 경우가 많아요. capitalizeWords(text)보다 text.capitalizeWords()가 더 직관적이에요. Dart는 후자를 달성하는 방법을 제공해요.

다음 레슨

다음 레슨에서는 HelpCommand를 다듬고, CommandRunner 클래스를 완성하고, onOutput 인자를 추가하고, 완전한 예시를 제공하면서 command_runner 패키지를 더 개선하는 법을 배울 거예요.

더 알아보기

  • Enhanced enums — 필드와 메서드를 가진 강력한 enum에 대해 자세히 알아보세요.
  • Extensions — 기존 타입에 새 기능을 추가하는 확장 기능을 살펴보세요.