CLI 앱 다듬기

CLI 앱 다듬기

HelpCommand를 개선해 더 자세한 정보를 제공하고, 유연한 출력 처리를 위한 onOutput 인자를 추가해 봐요.

출처: 원문

본문

이 챕터에서는 command_runner 패키지에 마무리 손질을 가할 거예요. HelpCommand를 다듬어 더 자세한 사용 정보를 제공하고, 더 유연한 출력 처리를 위한 onOutput 인자를 추가할 거예요. 이렇게 하면 CommandRunner 패키지가 완성되어 더 복잡한 시나리오에서 사용할 준비가 돼요.

이번 장에서 할 일

  • 문자열을 반복적으로 만드는 방법 배우기
  • help 사용 정보 개선하기
  • 커맨드 러너를 더 유연하게 만들기

사전 준비

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

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

Tasks

command_runner 패키지를 더 견고하고 사용자 친화적으로 만들도록 다듬을 거예요.

Task 1: HelpCommand 출력 개선하기

옵션과 그 설명을 포함해 더 자세한 사용 정보를 제공하도록 HelpCommand를 개선해요. 이렇게 하면 사용자가 여러분의 CLI 애플리케이션을 어떻게 쓰는지 더 쉽게 이해할 수 있어요.

  • command_runner/lib/src/help_command.dart 파일을 여세요.

  • 파일 맨 위에 console.dartexceptions.dart import를 추가하세요. 색 extension을 사용하고 ArgumentException을 던지기 위해 이것들이 필요해요.

import 'dart:async';

import 'package:command_runner/command_runner.dart';

import 'console.dart';
import 'exceptions.dart';
  • 기존 run 메서드를 다음으로 바꾸세요. 이 새 버전은 StringBuffer를 사용해 help 문자열을 효율적으로 만들고, 상세(verbose) 출력을 처리하는 로직을 포함해요.
@override
FutureOr<String> run(ArgResults args) async {
  final buffer = StringBuffer();
  buffer.writeln(runner.usage.titleText);

  if (args.flag('verbose')) {
    for (var cmd in runner.commands) {
      buffer.write(_renderCommandVerbose(cmd));
    }

    return buffer.toString();
  }

  if (args.hasOption('command')) {
    var (:option, :input) = args.getOption('command');

    var cmd = runner.commands.firstWhere(
      (command) => command.name == input,
      orElse: () {
        throw ArgumentException(
          'Input $input is not a known command.',
          name,
        );
      },
    );

    return _renderCommandVerbose(cmd);
  }

  // Verbose is false and no arg was passed in, so print basic usage.
  for (var command in runner.commands) {
    buffer.writeln(command.usage);
  }

  return buffer.toString();
}

StringBuffer는 문자열을 효율적으로 만들 수 있게 해 주는 Dart 클래스예요. 특히 루프 안에서 많은 연결을 수행할 때 + 연산자를 쓰는 것보다 성능이 더 좋아요.

  • HelpCommand 클래스에 _renderCommandVerbose 비공개 헬퍼 메서드를 추가하세요. 이 메서드는 단일 명령의 상세 출력을 형식화해요.
String _renderCommandVerbose(Command cmd) {
  final indent = ' ' * 10;
  final buffer = StringBuffer();
  buffer.writeln(cmd.usage.instructionText); //abbr, name: description
  buffer.writeln('$indent ${cmd.help}');
  if (cmd.valueHelp != null) {
    buffer.writeln(
      '$indent [Argument] Required? ${cmd.requiresArgument}, Type: ${cmd.valueHelp}, Default: ${cmd.defaultValue ?? 'none'}',
    );
  }
  buffer.writeln('$indent Options:');
  for (var option in cmd.options) {
    buffer.writeln('$indent ${option.usage}');
  }
  return buffer.toString();
}

Task 2: onOutput 콜백 추가하기

다음으로 CommandRunneronOutput 인자를 추가해 유연한 출력 처리를 가능하게 해요.

  • command_runner/lib/src/command_runner_base.dart 파일을 여세요.

  • CommandRunner 생성자에 onOutput 인자를 추가하고, 클래스에 해당 onOutput 멤버를 추가하세요.

class CommandRunner {
  CommandRunner({this.onOutput, this.onError});

  /// If not null, this method is used to handle output. Useful if you want to
  /// execute code before the output is printed to the console, or if you
  /// want to do something other than print output the console.
  /// If null, the onInput method will [print] the output.
  FutureOr<void> Function(String)? onOutput;

  FutureOr<void> Function(Object)? onError;

  // ... rest of the class
}
  • run 메서드를 onOutput 인자를 사용하도록 갱신하세요.
  Future<void> run(List<String> input) async {
    try {
      final ArgResults results = parse(input);
      if (results.command != null) {
        Object? output = await results.command!.run(results);
        if (onOutput != null) {
          await onOutput!(output.toString());
        } else {
          print(output.toString());
        }
      }
    } on Exception catch (exception) {
      if (onError != null) {
        onError!(exception);
      } else {
        rethrow;
      }
    }
  }

이렇게 하면 run 메서드가 onOutput 함수가 제공되면 그것을 사용하고, 그렇지 않으면 콘솔에 출력하는 기본 동작을 해요.

Task 3: onOutput 콜백 사용하기

마지막으로 새 onOutput 기능을 사용하도록 주 애플리케이션을 갱신해요.

  • cli/bin/cli.dart 파일을 여세요.

  • CommandRunneronOutput 함수를 전달하도록 main 함수를 갱신하세요. write 함수를 사용할 수 있게 하려면 console.dart import도 추가해야 해요.

import 'package:command_runner/command_runner.dart';

const version = '0.0.1';

void main(List<String> arguments) {
  var commandRunner = CommandRunner(
    onOutput: (String output) async {
      await write(output);
    },
    onError: (Object error) {
      if (error is Error) {
        throw error;
      }
      if (error is Exception) {
        print(error);
      }
    },
  )..addCommand(HelpCommand());
  commandRunner.run(arguments);
}

Task 4: 변경 사항 테스트하기

개선된 HelpCommandonOutput 콜백을 테스트해요.

  • 터미널을 열고 cli 디렉토리로 이동하세요.

  • dart run bin/cli.dart help --verbose 명령을 실행하세요.

help 명령의 자세한 사용 정보가 사용자 지정 write 함수로 출력되는 걸 볼 수 있을 거예요.

Review

무엇을 해냈나요

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

  • 효율적인 문자열 구축에 StringBuffer 사용하기

    루프에서 문자열을 만들 때 더 효율적일 수 있는 StringBuffer로 문자열 연결을 바꿨어요. writeln()write() 같은 메서드가 내용을 추가하고, toString()이 최종 문자열을 만들어 내요.

  • 상세 출력으로 HelpCommand 개선하기

    옵션, 기본값, 설명을 포함한 상세 사용 정보를 보여 주도록 help 시스템을 개선했어요. --verbose 플래그와 --command 옵션이 사용자에게 세부 수준을 제어할 수 있게 해 줘요.

  • 유연한 처리를 위한 onOutput 콜백 인자 추가하기

    CommandRunneronOutput을 추가해 라이브러리 사용자가 출력 표시 방식을 사용자 지정할 수 있게 했어요. 이 패턴은 지연 출력, 로깅, 출력 리다이렉션을 가능하게 해 패키지를 더 유연하고 적응력 있게 만들어요.

Quiz

이해를 확인해 봐요

Dart에서 StringBuffer 클래스의 목적은 무엇인가요?

  • 여러 부분을 이어 붙여 문자열을 효율적으로 만드는 것 — 맞아요! StringBuffer는 연속적인 여러 번의 추가 동안 중간 문자열 객체를 만들지 않아 효율적인 문자열 연결을 제공해요.
  • 고정 크기 문자열을 저장하는 것 — 아니에요. Dart의 문자열은 불변이고 고정 크기라는 개념이 없어요. StringBuffer는 문자열 생성과 관련된 다른 목적을 가져요.
  • 문자열을 암호화하는 것 — 아니에요. 암호화는 전용 암호화 라이브러리가 필요해요. StringBuffer는 보안과 무관한 범용 유틸리티예요.
  • 두 문자열의 동등성을 비교하는 것 — 아니에요. 문자열 비교는 ==compareTo()로 해요. StringBuffer는 다른 목적을 가져요.

CommandRunner 클래스의 onOutput 인자로 무엇을 할 수 있나요?

  • 명령의 출력 처리를 사용자 지정한다 — 맞아요! onOutput은 형식화, 로깅, 다른 대상에 쓰기 같은 명령 출력의 사용자 지정 동작을 정의하게 해 줘요.
  • 명령의 입력을 지정한다 — 아니에요. 입력은 run()에 전달되는 커맨드라인 인자에서 와요. onOutput이라는 이름이 다른 무언가를 뜻한다는 걸 암시해요.
  • 명령의 오류 메시지를 설정한다 — 아니에요. 오류 처리는 별도의 메커니즘(onError)이 있어요. onOutput은 다른 종류의 결과를 위한 거예요.
  • 명령의 이름을 정의한다 — 아니에요. 명령 이름은 Command 클래스의 name getter에서 정의해요. onOutput은 다른 목적을 가져요.

다음 레슨

다음 레슨에서는 애플리케이션의 Wikipedia 부분을 준비할 거예요.

더 알아보기

  • StringBuffer — 문자열을 효율적으로 만드는 방법을 더 알아보세요.
  • 콜백 함수 — 함수를 인자로 전달하는 Dart의 함수 기능을 살펴보세요.