Dart 튜토리얼 22편: CLI 앱과 HTTP 서버(args·dart:io·서버 패키지)
요약
핵심 요지
- 문제 정의: CLI 앱은 입력/출력/에러 처리/인자 파싱이 한 번에 등장해서, 코드가 길어지고 흐름이 끊기기 쉽다.
- 핵심 주장: Dart CLI는
args1/dart:io2/dart:convert3를 조합해 “인자 파싱 → 입력 스트림 처리 → 출력 스트림 출력 → 실패 시 에러 출력” 흐름을 만들 수 있다. - 주요 근거:
ArgParser4로-n플래그를 추가하고,stdin.pipe(stdout)5로 표준 입력을 그대로 출력하고,File(path).openRead()6 +utf8.decoder7 +LineSplitter8로 파일을 줄 단위로 처리하는 예시가 제시된다. - 실무 기준: 서버 개발에서는 직접
dart:ioHTTP 클라이언트를 쓰기보다package:http같은 상위 라이브러리를 권장하고, 서버 프레임워크는shelf9 같은 커뮤니티 패키지를 기준으로 선택하는 흐름이 제시된다.
문서가 설명하는 범위
- CLI 앱에서
dart run으로 실행하고, 표준 입출력/파일/인자 파싱을 조합하는 방법 args기반ArgParser/ArgResults10 사용 예시- 서버·CLI에서 자주 쓰는 패키지 목록(명령줄/서버)과 선택 힌트
읽는 시간: 15분 | 난이도: 중급
참고 자료
문제 상황
입문자가 CLI 앱을 만들 때 가장 흔한 좌절은 “작은 기능도 코드가 커진다”는 점입니다.
예를 들어 파일 내용을 출력하는 앱만 해도, 파일/표준 입력/에러 출력/종료 코드/인자 파싱이 동시에 등장합니다.
이걸 그때그때 붙이면, 무엇이 핵심인지 보이지 않습니다.
그래서 이 글은 예시 앱 하나를 기준으로 “필수 뼈대”를 먼저 고정합니다.
해결 방법
단계 1: CLI 앱의 흐름을 “인자 → 경로 → 처리”로 고정하기
Why
NOTECLI 앱은 입력이 “함수 호출”이 아니라 “커맨드라인 인자”로 들어옵니다.
그래서 앱 구조는 시작부터 “인자 처리”를 중심으로 잡는 편이 자연스럽습니다.
What
NOTE예시 앱
dcat은 “인자로 받은 파일 경로들을 읽어서 출력한다”는 목표를 갖습니다.
경로가 없으면 표준 입력을 그대로 받아 표준 출력으로 흘려보내는 동작이 제시됩니다.
How
TIP다음 코드는 예시 앱의 전체 흐름(인자 파싱 → 경로 목록 → 처리)을 보여줍니다.
코드 블록 안 주석은 한글로 맞췄습니다.import 'dart:convert';import 'dart:io';import 'package:args/args.dart';const lineNumber = 'line-number';void main(List<String> arguments) {exitCode = 0; // 성공을 기본값으로 둔다.final parser = ArgParser()..addFlag(lineNumber, negatable: false, abbr: 'n');ArgResults argResults = parser.parse(arguments);final paths = argResults.rest;dcat(paths, showLineNumbers: argResults[lineNumber] as bool);}Future<void> dcat(List<String> paths, {bool showLineNumbers = false}) async {if (paths.isEmpty) {// 인자로 파일이 없으면 표준 입력을 받아서 그대로 출력한다.await stdin.pipe(stdout);} else {for (final path in paths) {var lineNumber = 1;final lines = utf8.decoder.bind(File(path).openRead()).transform(const LineSplitter());try {await for (final line in lines) {if (showLineNumbers) {stdout.write('${lineNumber++} ');}stdout.writeln(line);}} catch (_) {await _handleError(path);}}}}Future<void> _handleError(String path) async {if (await FileSystemEntity.isDirectory(path)) {stderr.writeln('error: $path is a directory');} else {exitCode = 2;}}
Watch out
WARNING이 흐름을 그대로 따라가면 “비동기”(
Future11,Stream12)가 자연스럽게 섞입니다.
따라서async/await가 익숙하지 않다면, 비동기 기초를 먼저 짧게 복습하고 오는 편이 좋습니다.
결론: CLI 앱은 “인자 → 경로 목록 → 처리 함수” 구조로 시작하면, 기능이 늘어도 흐름이 유지됩니다.
단계 2: args로 인자 파싱을 “표준화”하기
Why
NOTE인자 파싱을 문자열로 직접 처리하면 옵션이 늘어날수록 버그가 늘어납니다.
그래서 인자 파싱 전용 패키지를 쓰는 흐름이 제시됩니다.
What
NOTE
args패키지는 원시 커맨드라인 인자를 옵션/플래그/값으로 바꾸는 파서를 제공합니다.
핵심 타입은ArgParser와ArgResults입니다.
How
TIP예시에서는
-n플래그를 추가하고, 파싱 결과의 나머지 값을rest로 읽습니다.void main(List<String> arguments) {exitCode = 0; // 성공을 기본값으로 둔다.final parser = ArgParser()..addFlag(lineNumber, negatable: false, abbr: 'n');ArgResults argResults = parser.parse(arguments);final paths = argResults.rest;dcat(paths, showLineNumbers: argResults[lineNumber] as bool);}
Watch out
WARNING파싱 결과는 “플래그/옵션”과 “나머지 인자(rest)”를 분리합니다.
따라서 파일 경로처럼 “옵션이 아닌 값”은rest로 읽는 규칙을 코드로 고정해야 합니다.
결론: 인자 파싱은 args로 표준화하고, “옵션 vs rest” 경계를 명확히 둡니다.
단계 3: 입력은 스트림으로 받고, 출력은 표준 출력으로 “흘려보내기”로 고정하기
Why
NOTECLI의 입력은 파일이 될 수도 있고 표준 입력이 될 수도 있습니다.
둘을 분기 처리하더라도 “최종 출력”은 표준 출력으로 모으는 편이 단순합니다.
What
NOTE표준 입력을 그대로 표준 출력으로 전달하는 방식으로
stdin.pipe(stdout)가 제시됩니다.
파일 입력은openRead()로 바이트 스트림을 만들고,utf8.decoder와LineSplitter로 줄 단위 처리합니다.
How
TIP표준 입력을 받는 경우는 한 줄로 처리합니다.
await stdin.pipe(stdout);파일을 줄 단위로 처리하는 흐름은 다음과 같습니다.
final lines = utf8.decoder.bind(File(path).openRead()).transform(const LineSplitter());await for (final line in lines) {stdout.writeln(line);}
Watch out
WARNING파일이 디렉터리이거나 읽기 실패가 발생하면 예외가 발생할 수 있습니다.
예시는 실패 시stderr13로 메시지를 출력하고,exitCode14를 바꾸는 방식으로 처리합니다.
결론: 입력은 스트림으로 받고, 출력은 표준 출력으로 모으되, 실패는 표준 에러와 종료 코드로 표현합니다.
단계 4: 서버 개발은 패키지 생태계를 기준으로 “재사용”을 선택한다
Why
NOTE서버는 인증/라우팅/미들웨어 같은 기능을 반복해서 필요로 합니다.
이 기능을 매번 직접 구현하면 유지보수가 어려워집니다.
What
NOTE서버·CLI 개발에서 유용한 커뮤니티 패키지 목록이 제시됩니다.
예를 들어 서버에서는shelf같은 미들웨어 모델을 제공하는 패키지가 언급됩니다.
또한 HTTP 서버 샘플로shelf기반 샘플과, Google APIs를 사용하는 샘플이 소개됩니다.
How
TIP필요한 기능 키워드로 pub.dev에서 검색하고, 플랫폼 지원 조건(서버/CLI)을 기준으로 선택합니다.
예를 들어 CLI에서는args, 서버에서는shelf같은 패키지가 대표로 제시됩니다.
Watch out
WARNING패키지를 가져다 쓰면 편해지는 만큼, “플랫폼 지원”과 “버전/보안” 관점의 관리가 필요합니다.
따라서 의존성은pubspec.yaml15로 명시하고, 설치/업데이트 흐름을 고정해야 합니다.
결론: 서버·CLI는 표준 라이브러리 + 커뮤니티 패키지를 조합하되, 의존성 관리를 함께 고정해야 합니다.
Footnotes
-
args(args): 명령줄 인자를 옵션/플래그/값으로 파싱하는 패키지다. ↩
-
dart
(dart ): 파일/프로세스/표준 입출력 등 I/O를 제공하는 라이브러리다. ↩ -
dart
(dart ): UTF-8/JSON 등 변환을 제공하는 라이브러리다. ↩ -
ArgParser(아그 파서): 명령줄 옵션/플래그를 정의하고 파싱하는 타입이다. ↩
-
stdin.pipe(stdout)(파이프): 표준 입력 스트림을 표준 출력으로 그대로 전달하는 동작이다. ↩
-
openRead()(스트림 열기): 파일을 바이트 스트림으로 여는 메서드다. ↩
-
utf8.decoder(UTF-8 디코더): 바이트 스트림을 문자열 스트림으로 변환하는 디코더다. ↩
-
LineSplitter(라인 스플리터): 문자열 스트림을 줄 단위로 나누는 변환기다. ↩
-
shelf(shelf): 미들웨어 조합 모델로 HTTP 서버를 구성하는 패키지다. ↩
-
ArgResults(아그 리절트): 파싱 결과로, 옵션 값과 나머지 인자(rest)를 제공한다. ↩
-
Future(퓨처): 나중에 완료되는 작업의 결과를 표현하는 타입이다. ↩
-
Stream(스트림): 시간이 지나면서 여러 이벤트/값이 들어오는 흐름을 표현하는 타입이다. ↩
-
stderr(표준 에러): 오류 메시지를 출력하는 표준 에러 출력 스트림이다. ↩
-
exitCode(종료 코드): 프로세스 종료 상태를 나타내는 코드다(일반적으로 0은 성공). ↩
-
pubspec.yaml(pubspec): 패키지 메타데이터와 의존성을 선언하는 파일이다. ↩
공유
이 글이 도움이 되었다면 다른 사람과 공유해주세요!