패키지와 라이브러리로 Dart 코드 정리하기

패키지와 라이브러리로 Dart 코드 정리하기

Dart 코드를 재사용 가능한 라이브러리와 패키지로 구성하는 방법을 배워 봐요.

출처: Organize Dart code with packages and libraries

본문

이 장에서는 기본 Dart 문법에서 한 단계 나아가 "Dart다운 방식(The Dart way)"으로 명령줄 애플리케이션을 만들며 모범 사례를 받아들여 볼 거예요. 명령줄 인자를 다루는 전용 패키지를 만들어 코드를 재사용 가능한 구성 요소로 리팩터링하는 방법을 배워요.

이 단계는 앞으로 나올 장들에서 더 고급스러운 명령줄 애플리케이션을 만들기 위한 준비가 돼요. 앞으로는 Wikipedia 로직용 전용 패키지와 견고한 command_runner 프레임워크를 통합할 거예요. 이 장은 Dart 라이브러리, export 문, 그리고 더 나은 조직과 유지보수를 위한 프로젝트 구조화 방법을 이해하는 데 도움을 줘요.

  • 재사용 가능한 Dart 패키지를 만들어요.
  • export로 공개 API를 정의해요.
  • 로컬 경로 의존성(path dependency)을 추가해요.

사전 준비 (Prerequisites)

  • 비동기 프로그래밍과 HTTP 요청을 다룬 3장을 마쳐야 해요.

작업 (Tasks)

이 장에서는 기존 dartpedia CLI 애플리케이션을 리팩터링해서 명령줄 인자 파싱 로직을 command_runner라는 별도 패키지로 추출할 거예요. 이렇게 하면 프로젝트 구조가 개선되어 더 모듈화되고 유지보수하기 쉬워져요.

참고: 공식적으로 유지되는 args 패키지에는 command_runner 클래스가 있어요. 이 튜토리얼에서는 우리만의 command_runner 클래스를 만들지만, 실제 프로젝트에서는 args의 클래스를 사용하게 될 가능성이 높아요.

작업 1: command_runner 패키지 만들기 (Create the command_runner package)

먼저 명령줄 인자 파싱 로직을 담을 새 Dart 패키지를 만들어요.

  • 프로젝트의 루트 디렉터리(/dartpedia)로 이동해 주세요.

  • 터미널에서 다음 명령을 실행해 주세요:

dart create -t package command_runner

이 명령은 Dart 패키지의 기본 구조를 갖춘 command_runner라는 새 디렉터리를 만들어요. 이제 프로젝트 루트에 cli 패키지 옆에 새 command_runner 폴더가 있는 게 보일 거예요.

작업 2: CommandRunner 클래스 구현하기 (Implement the CommandRunner class)

이제 command_runner 패키지를 만들었으니, 결국 명령줄 인자 파싱 로직을 다룰 자리 표시자(placeholder) 클래스를 추가해 주세요.

  • command_runner/lib/command_runner.dart 파일을 열어 주세요. 기존 자리 표시자 코드를 제거하고 다음을 추가해 주세요:
/// A simple command runner to handle command-line arguments.
///
/// More extensive documentation for this library goes here.
library;

export 'src/command_runner_base.dart';
// TODO: Export any other libraries intended for clients of this package.

앞선 코드에서 눈여겨볼 점:

  • library;는 이 파일을 라이브러리로 선언해요. 라이브러리는 재사용 가능한 Dart 코드 단위의 경계와 공개 인터페이스를 정의해요.

  • export 'src/command_runner_base.dart';는 중요한 줄이에요. command_runner 패키지를 import하는 다른 패키지가 command_runner_base.dart의 선언을 사용할 수 있게 해 줘요. 이 export 문이 없으면 command_runner_base.dart 안의 클래스와 함수는 command_runner 패키지에만 비공개가 되어 dartpedia 애플리케이션에서 사용할 수 없어요.

  • command_runner/lib/src/command_runner_base.dart 파일을 열어 주세요.

  • 기존 자리 표시자 코드를 제거하고 CommandRunner 클래스를 command_runner/lib/src/command_runner_base.dart에 추가해 주세요:

class CommandRunner {
  /// Runs the command-line application logic with the given arguments.
  Future<void> run(List<String> input) async {
    print('CommandRunner received arguments: $input');
  }
}

앞선 코드에서 눈여겨볼 점:

  • CommandRunner는 지금은 단순화된 대용(stand-in) 역할을 하는 클래스예요. 이 run 메서드는 현재 받은 인자를 그냥 출력해요. 이후 장에서는 이 클래스를 확장해 복잡하고 구성 가능한 명령 파싱을 처리할 거예요.
  • Future<void>는 이 메서드가 비동기 연산을 수행할 수도 있지만 값을 반환하지는 않는다는 것을 나타내는 반환 타입이에요.

작업 3: command_runner를 의존성으로 추가하기 (Add command_runner as a dependency)

이제 command_runner 패키지를 만들고 자리 표시자 CommandRunner 클래스를 추가했으니, cli 애플리케이션에 command_runner에 의존한다는 것을 알려야 해요. command_runner 패키지는 프로젝트 안에 로컬로 있으므로 경로 의존성(path dependency)을 사용할 수 있어요.

  • cli/pubspec.yaml 파일을 열어 주세요.

참고: 올바른 /dartpedia/cli/pubspec.yaml 파일을 여는지 확인해 주세요. command_runner 패키지를 만들 때 /dartpedia/command_runner/pubspec.yaml 파일도 함께 만들어졌어요.

  • cli 패키지는 애플리케이션 패키지로 pub.dev에 게시할 의도가 없으므로 name 필드 뒤에 publish_to: none을 추가해 주세요. 이렇게 하면 실수로 게시하는 것을 막고, 경로 의존성을 추가했을 때 분석기 경고가 나오는 것도 예방할 수 있어요.
name: cli
publish_to: none
# ...
  • dependencies 섹션을 찾아 다음 줄을 추가해 주세요:
dependencies:
  http: ^1.3.0 # Keep your existing http dependency
  command_runner:
    path: ../command_runner # Points to your local command_runner package

이 섹션은 cli 애플리케이션이 command_runner 패키지에 의존한다는 것과, 그 패키지가 ../command_runner 디렉터리(cli 디렉터리 기준 상대 경로)에 있다는 것을 지정해요.

  • 터미널의 /dartpedia/cli 디렉터리에서 dart pub get을 실행해 새 의존성을 가져와 주세요.

작업 4: command_runner 패키지 import하고 사용하기 (Import and use the command_runner package)

이제 command_runner를 의존성으로 추가했으니 cli 애플리케이션에 import하고, 기존 인자 처리 로직을 새 CommandRunner 클래스로 교체할 수 있어요.

  • cli/bin/cli.dart 파일을 열어 주세요.

  • 다른 import들과 함께 파일 맨 위에 다음 import 문을 추가해 주세요:

import 'package:command_runner/command_runner.dart';

이 문은 command_runner 패키지를 import해서 CommandRunner 클래스를 사용할 수 있게 해 줘요.

  • main 함수 리팩터링하고 기존 로직 제거하기: 현재 3장의 main 함수는 version, help, wikipedia 같은 명령을 직접 처리한 다음 searchWikipedia를 호출해요. 이제 이 모든 사용자 정의 명령 처리 로직을 새 CommandRunner 클래스에 대한 단일 호출로 교체할 거예요.

cli/bin/cli.dart 파일(3장에서 온 것)은 현재 다음과 같아야 해요:

import 'dart:io';
import 'package:http/http.dart' as http;
import 'package:command_runner/command_runner.dart';

const version = '0.0.1';

void main(List<String> arguments) {
  if (arguments.isEmpty || arguments.first == 'help') {
    printUsage();
  } else if (arguments.first == 'version') {
    print('Dartpedia CLI version $version');
  } else if (arguments.first == 'wikipedia') {
    final inputArgs = arguments.length > 1 ? arguments.sublist(1) : null;
    searchWikipedia(inputArgs);
  } else {
    printUsage();
  }
}

void searchWikipedia(List<String>? arguments) async { /* ... existing logic ... */ }
void printUsage() { /* ... existing logic ... */ }
Future<String> getWikipediaArticle(String articleTitle) async { /* ... existing logic ... */ }

이제 cli/bin/cli.dart의 전체 내용(http import 제외)을 다음 갱신 버전으로 교체해 주세요:

import 'dart:io';
import 'package:http/http.dart' as http;
import 'package:command_runner/command_runner.dart';

void main(List<String> arguments) async { // main is now async and awaits the runner
  var runner = CommandRunner(); // Create an instance of your new CommandRunner
  await runner.run(arguments); // Call its run method, awaiting its Future<void>
}

앞선 코드에서 눈여겨볼 점:

  • mainasync로 선언하면 await를 쓸 수 있어요. runner.run()Future를 반환하므로, 그것을 await하면 main이 runner의 완료를 명시적으로 기다려 로직 순서가 명확해지고 오류 전파도 개선돼요.
  • var runner = CommandRunner();는 새 command_runner 패키지의 CommandRunner 클래스 인스턴스를 만들어요.
  • await runner.run(arguments);CommandRunner 인스턴스의 run 메서드를 호출하면서 명령줄 인자를 전달해요.

제거된 함수들:

  • printUsage, searchWikipedia, getWikipediaArticle 함수는 이제 cli/bin/cli.dart에서 완전히 제거돼요. 이 함수들의 로직은 이후 장에서 전체 명령줄 프레임워크를 만드는 과정의 일부로 재설계되어 command_runner 패키지로 옮겨질 거예요.

작업 5: 애플리케이션 실행하기 (Run the application)

이제 코드를 리팩터링하고 cli 애플리케이션을 command_runner 패키지를 사용하도록 갱신했으니, 이 단계에서 모든 것이 올바르게 작동하는지 확인하기 위해 애플리케이션을 실행해 봐요.

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

  • wikipedia 명령을 실행해 주세요:

dart run bin/cli.dart wikipedia Computer_programming
  • 애플리케이션이 이제 오류 없이 실행되고 인자를 콘솔에 출력하는지 확인해 주세요. 이는 제어가 새 command_runner 패키지로 성공적으로 넘어갔다는 것을 보여 줘요.
CommandRunner received arguments: [wikipedia, Computer_programming]

문제 해결: cli.dart를 실행할 때 Dart가 Method not found: 'CommandRunner' 오류를 보고하면, command_runner/lib/src/command_runner_base.dart작업 2CommandRunner 클래스로 갱신됐는지 확인해 주세요.

기능에 관한 중요한 참고: 문서 가져오기 기능(3장에서 온 것)이 더는 활성화되지 않는 것을 눈치챌 거예요. 이것은 예상된 동작이에요! 이 장에서 명령 처리 책임을 옮기며 프로젝트 구조를 리팩터링했기 때문이에요. 다음 장들은 command_runner 패키지 안에서 그 핵심 애플리케이션 로직을 다시 만들고 강화하는 데 집중할 거예요.

복습 (Review)

배운 내용 (What you accomplished)

이번 강의에서 만들고 배운 내용을 정리해 드릴게요.

  • 재사용 가능한 Dart 패키지 만들기: dart create -t package command_runner로 다른 패키지에서 사용할 새 라이브러리 패키지를 뼈대로 잡았어요. 라이브러리 패키지는 재사용 가능한 Dart 라이브러리를 프로젝트 작업공간 안에서든 pub.dev에 게시해서든 조직하고 공유하는 표준 방식이에요.
  • export로 공개 API 정의하기: 라이브러리 파일에 export 문을 추가해서 특정 라이브러리와 그 정의를 소비자에게 노출했어요. lib 루트 디렉터리의 파일에서 export되지 않으면 lib/src/의 코드는 패키지에 비공개로 남아요. 이렇게 하면 구현 세부사항을 완전히 통제하면서도 남을 위한 공개 API를 신중하게 만들 수 있어요.
  • 로컬 경로 의존성 추가하기: pubspec.yaml을 구성해 경로 의존성을 사용해 로컬 패키지에 의존하게 했어요. 이는 패키지들이 각자 게시되기 전에 함께 진화할 수 있는 다중 패키지 프로젝트를 가능하게 해 줘요.

퀴즈 (Quiz)

이해도를 확인해 봐요 (Check your understanding)

Q1. Dart 라이브러리에서 export 문의 목적은 무엇인가요?

  • 다른 파일의 선언을 라이브러리의 공개 API를 통해 사용할 수 있게 하는 것. — 맞아요! export는 선언을 다시 노출시켜 줘요. 예를 들어 export 'src/command.dart';Command를 여러분 패키지를 import하는 누구든 접근할 수 있게 해요.
  • 다른 라이브러리에서 구현 세부사항을 숨기는 것. — 아니에요. export 문은 숨기는 것과 반대 역할을 해요. 구현 전용 멤버를 숨기려면 어떤 export 문에도 포함하지 않으면 돼요.
  • 단일 문으로 여러 라이브러리를 import하는 것. — 아니에요. importexport는 목적이 달라요. import는 코드를 여러분 라이브러리에서 사용할 수 있게 하고, export는 여러분 라이브러리의 소비자를 위해 무언가를 해요.
  • 패키지가 지원하는 Dart SDK 버전을 지정하는 것. — 아니에요. SDK 제약은 Dart 코드가 아니라 pubspec.yaml에 들어가요. export는 구성 옵션이 아니라 Dart 언어 기능이에요.

Q2. 로컬 패키지(파일 시스템에 있는 것)를 어떻게 의존성으로 추가하나요?

  • command_runner: {path: ../command_runner} 같은 경로 의존성을 사용해요. — 맞아요! 경로 의존성은 로컬 디렉터리를 가리켜요. 다중 패키지 프로젝트나 게시 전에 패키지를 개발할 때 아주 적합해요.
  • command_runner: ^1.0.0 같은 버전 제약을 사용해요. — 아니에요. 버전 제약은 pub.dev에 게시된 패키지를 위한 거예요. 로컬 패키지에는 다른 종류의 의존성 지정이 필요해요.
  • 패키지 폴더를 프로젝트의 lib/ 디렉터리에 복사해요. — 아니에요. 복사는 중복과 버전 관리 문제를 만들어요. Dart는 복사하지 않고 위치로 패키지를 참조하는 방법을 제공해요.
  • dart pub add command_runner --local을 실행해요. — 아니에요. dart pub add에는 --local 플래그가 없어요. pubspec.yaml에서 위치를 다르게 지정하거나 dart pub add에 패키지 설명자를 전달해야 해요.

Q3. 패키지에 lib/src/parser.dart 코드가 있어요. 왜 export 'src/parser.dart';만 담긴 lib/parser.dart를 만들까요?

  • 구현 파일을 src/에 조직하면서 사용자가 깔끔한 경로에서 import할 수 있게 하기 위해서요. — 맞아요! 사용자는 src/에 직접 들어가지 않고 import 'package:mylib/parser.dart';라고 쓸 수 있어요. 이렇게 하면 공개 API와 내부 조직을 분리할 수 있어요.
  • import 깊이를 줄여 코드를 더 빠르게 실행하기 위해서요. — 아니에요. export는 런타임 성능에 영향을 주지 않아요. import 경로의 디렉터리 수는 코드가 얼마나 빨리 실행되는지에 영향을 주지 않아요.
  • Dart가 모든 공개 파일이 lib/ 루트 디렉터리에 있어야 하기 때문이에요. — 아니에요. Dart는 이 구조를 요구하지 않아요. 언어 요구사항이 아니라 관례예요.
  • 다른 패키지가 src/ 파일을 직접 import하는 것을 막기 위해서요. — 아니에요. src/의 파일은 기술적으로 여전히 직접 import될 수 있어요. Dart 모범 사례가 그것을 권장하지 않을 뿐, 막지는 않아요.

더 알아보기