패키지와 라이브러리로 Dart 코드 정리하기
패키지와 라이브러리로 Dart 코드 정리하기
Dart 코드를 재사용 가능한 라이브러리와 패키지로 구성하는 방법을 배워 봐요.
본문
이 장에서는 기본 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>
}
앞선 코드에서 눈여겨볼 점:
main을async로 선언하면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가 작업 2의CommandRunner클래스로 갱신됐는지 확인해 주세요.
기능에 관한 중요한 참고: 문서 가져오기 기능(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하는 것. — 아니에요.
import와export는 목적이 달라요.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 모범 사례가 그것을 권장하지 않을 뿐, 막지는 않아요.