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:io HTTP 클라이언트를 쓰기보다 package:http 같은 상위 라이브러리를 권장하고, 서버 프레임워크는 shelf9 같은 커뮤니티 패키지를 기준으로 선택하는 흐름이 제시된다.

문서가 설명하는 범위#

  • CLI 앱에서 dart run으로 실행하고, 표준 입출력/파일/인자 파싱을 조합하는 방법
  • args 기반 ArgParser/ArgResults10 사용 예시
  • 서버·CLI에서 자주 쓰는 패키지 목록(명령줄/서버)과 선택 힌트

읽는 시간: 15분 | 난이도: 중급


참고 자료#


문제 상황#

입문자가 CLI 앱을 만들 때 가장 흔한 좌절은 “작은 기능도 코드가 커진다”는 점입니다.
예를 들어 파일 내용을 출력하는 앱만 해도, 파일/표준 입력/에러 출력/종료 코드/인자 파싱이 동시에 등장합니다.
이걸 그때그때 붙이면, 무엇이 핵심인지 보이지 않습니다.
그래서 이 글은 예시 앱 하나를 기준으로 “필수 뼈대”를 먼저 고정합니다.


해결 방법#

단계 1: CLI 앱의 흐름을 “인자 → 경로 → 처리”로 고정하기#

Why#

NOTE

CLI 앱은 입력이 “함수 호출”이 아니라 “커맨드라인 인자”로 들어옵니다.
그래서 앱 구조는 시작부터 “인자 처리”를 중심으로 잡는 편이 자연스럽습니다.

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 패키지는 원시 커맨드라인 인자를 옵션/플래그/값으로 바꾸는 파서를 제공합니다.
핵심 타입은 ArgParserArgResults입니다.

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#

NOTE

CLI의 입력은 파일이 될 수도 있고 표준 입력이 될 수도 있습니다.
둘을 분기 처리하더라도 “최종 출력”은 표준 출력으로 모으는 편이 단순합니다.

What#

NOTE

표준 입력을 그대로 표준 출력으로 전달하는 방식으로 stdin.pipe(stdout)가 제시됩니다.
파일 입력은 openRead()로 바이트 스트림을 만들고, utf8.decoderLineSplitter로 줄 단위 처리합니다.

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#

  1. args(args): 명령줄 인자를 옵션/플래그/값으로 파싱하는 패키지다.

  2. dart(dart): 파일/프로세스/표준 입출력 등 I/O를 제공하는 라이브러리다.

  3. dart(dart): UTF-8/JSON 등 변환을 제공하는 라이브러리다.

  4. ArgParser(아그 파서): 명령줄 옵션/플래그를 정의하고 파싱하는 타입이다.

  5. stdin.pipe(stdout)(파이프): 표준 입력 스트림을 표준 출력으로 그대로 전달하는 동작이다.

  6. openRead()(스트림 열기): 파일을 바이트 스트림으로 여는 메서드다.

  7. utf8.decoder(UTF-8 디코더): 바이트 스트림을 문자열 스트림으로 변환하는 디코더다.

  8. LineSplitter(라인 스플리터): 문자열 스트림을 줄 단위로 나누는 변환기다.

  9. shelf(shelf): 미들웨어 조합 모델로 HTTP 서버를 구성하는 패키지다.

  10. ArgResults(아그 리절트): 파싱 결과로, 옵션 값과 나머지 인자(rest)를 제공한다.

  11. Future(퓨처): 나중에 완료되는 작업의 결과를 표현하는 타입이다.

  12. Stream(스트림): 시간이 지나면서 여러 이벤트/값이 들어오는 흐름을 표현하는 타입이다.

  13. stderr(표준 에러): 오류 메시지를 출력하는 표준 에러 출력 스트림이다.

  14. exitCode(종료 코드): 프로세스 종료 상태를 나타내는 코드다(일반적으로 0은 성공).

  15. pubspec.yaml(pubspec): 패키지 메타데이터와 의존성을 선언하는 파일이다.

공유

이 글이 도움이 되었다면 다른 사람과 공유해주세요!

Dart 튜토리얼 22편: CLI 앱과 HTTP 서버(args·dart:io·서버 패키지)
https://moodturnpost.net/posts/dart/dart-cli-and-http-servers/
작성자
Moodturn
게시일
2026-01-04
Moodturn

목차