iBetter Books
수정

완성과 코드 리뷰

모든 조각이 준비되었습니다. 이 챕터에서는 main() 함수를 완성하고, 앱 전체를 실행합니다. 그리고 코드를 돌아보며 어떤 개념이 어디에 쓰였는지, 무엇을 더 개선할 수 있는지 이야기합니다.

완성된 main() 함수

지금까지 만든 모든 파일을 연결하는 진입점입니다.

// 수정: bin/todo_app.dart

import 'dart:io';

import 'package:todo_app/command.dart';
import 'package:todo_app/todo_handler.dart';
import 'package:todo_app/todo_repository.dart';
import 'package:todo_app/todo_service.dart';

Future<void> main() async {
  final service = TodoService();
  final repo = TodoRepository();

  // 파일에서 데이터 불러오기
  final savedTodos = await service.load();
  repo.loadAll(savedTodos);

  print('=================================');
  print('  할 일 관리 CLI 앱');
  print('=================================');
  print('명령어: add <내용> | list | done <id> | delete <id> | exit');
  print('');

  while (true) {
    stdout.write('> ');
    final input = stdin.readLineSync() ?? '';

    if (input.trim().isEmpty) continue;

    final command = parseCommand(input);
    if (command == null) {
      print('알 수 없는 명령어입니다. (add, list, done, delete, exit)');
      continue;
    }

    if (command is ExitCommand) {
      await service.save(repo.getAll().toList());
      print('저장 후 종료합니다. 안녕히 가세요.');
      break;
    }

    final result = handleCommand(command, repo);
    if (result.isNotEmpty) print(result);
  }
}

while (true) 루프가 앱의 심장입니다. 사용자가 exit을 입력하기 전까지 계속 실행됩니다.

빈 입력은 continue로 건너뜁니다. 사용자가 실수로 엔터를 누른 경우에 오류 메시지를 보여줄 필요가 없습니다.

ExitCommand는 루프 밖에서 별도로 처리합니다. handleCommand는 저장 로직을 모르기 때문입니다. main()이 저장 책임을 직접 집니다.

완성된 파일 목록

앱을 구성하는 전체 파일입니다.

todo_app/
├── bin/
│   └── todo_app.dart         ← 진입점, 입력 루프
├── lib/
│   ├── todo.dart             ← Todo 데이터 모델
│   ├── command.dart          ← sealed class Command + 파싱 함수
│   ├── exceptions.dart       ← 커스텀 예외
│   ├── todo_repository.dart  ← 메모리 저장소
│   ├── todo_service.dart     ← 파일 I/O
│   └── todo_handler.dart     ← 명령어 처리
└── pubspec.yaml

여섯 개의 파일이 각자의 역할을 담당합니다. 의존 관계는 단방향입니다. bin/todo_app.dart가 최상위에서 모든 것을 연결하고, lib/ 내부 파일들은 서로 필요한 것만 참조합니다.

전체 실행 시연

앱을 실행한 전체 흐름입니다.

=================================
  할 일 관리 CLI 앱
=================================
명령어: add <내용> | list | done <id> | delete <id> | exit

> add Dart 교재 읽기
[1] Dart 교재 읽기 — 추가되었습니다.

> add 운동하기
[2] 운동하기 — 추가되었습니다.

> add 코드 리뷰
[3] 코드 리뷰 — 추가되었습니다.

> list
[1] [ ] Dart 교재 읽기
[2] [ ] 운동하기
[3] [ ] 코드 리뷰

> done 1
[1] Dart 교재 읽기 — 완료 처리했습니다.

> done 3
[3] 코드 리뷰 — 완료 처리했습니다.

> list
[1] [v] Dart 교재 읽기
[2] [ ] 운동하기
[3] [v] 코드 리뷰

> delete 2
[2] 운동하기 — 삭제했습니다.

> list
[1] [v] Dart 교재 읽기
[3] [v] 코드 리뷰

> done 99
[99] 번 할 일을 찾을 수 없습니다.

> add
입력 오류: 할 일 내용을 입력하세요.

> exit
저장 후 종료합니다. 안녕히 가세요.

다시 실행하면 이전 데이터가 그대로 남아 있습니다.

> list
[1] [v] Dart 교재 읽기
[3] [v] 코드 리뷰

코드 리뷰 — 어떤 개념이 어디에 쓰였나

PART 02부터 PART 09까지 배운 개념들이 이 앱 전체에 녹아 있습니다.

PART 02~03 — 변수, 타입, 흐름 제어. if, while, continue, breakmain() 입력 루프에 쓰였습니다. finalvar로 불변/가변을 구분했습니다.

PART 04 — 컬렉션. List<Todo>로 할 일 목록을 관리합니다. where(), map(), reduce(), sort(), 스프레드 연산자가 TodoRepository에 사용되었습니다.

PART 05 — 클래스와 OOP. Todo, TodoRepository, TodoService, TodoHandler 모두 클래스입니다. factory 생성자(Todo.fromJson)와 named 생성자, getter가 활용되었습니다.

PART 06 — Null Safety. stdin.readLineSync() ?? ''에서 ?? 연산자가 쓰였습니다. getById()가 예외를 던지도록 바꾸면서 호출 쪽에서 null 체크가 사라졌습니다. 흐름 분석으로 컴파일러가 null 안전성을 보장합니다.

PART 07 — 제네릭. List<Todo>, Map<String, dynamic>처럼 타입 파라미터가 곳곳에 쓰였습니다. List.unmodifiable()도 제네릭 메서드입니다.

PART 08 — 비동기와 예외. async/await로 파일 I/O를 처리합니다. TodoNotFoundException, InvalidInputException이 커스텀 예외입니다. on FormatException, on IOException 등 타입별 예외 처리가 TodoService에 있습니다.

PART 09 — 패턴과 레코드. Command가 sealed class입니다. switch 표현식에서 AddCommand(title: final t) 같은 구조 분해 패턴으로 필드를 추출했습니다. when 가드 절로 조건부 명령어 파싱을 구현했습니다.

리팩터링 포인트

이 앱은 학습용으로 단순하게 설계되었습니다. 더 나은 코드를 위한 개선 아이디어를 소개합니다.

자동 저장. 현재는 exit 시에만 저장합니다. add, done, delete 명령마다 자동 저장하면 예기치 않은 종료에도 데이터를 지킬 수 있습니다. Ch 03에서 언급한 레코드 반환 방식으로 구현할 수 있습니다.

정렬과 필터링. list 명령에 list --done, list --pending 같은 옵션을 추가하면 완료/미완료 항목만 볼 수 있습니다. parseCommand()에 옵션 파싱 로직을 추가하면 됩니다.

백업 파일. save() 전에 기존 파일을 todos.json.bak으로 복사해두면 저장 실패 시 복구할 수 있습니다.

테스트 추가. dart test 패키지로 TodoRepository의 각 메서드를 단위 테스트할 수 있습니다. 예외가 올바르게 발생하는지, 컬렉션 메서드 결과가 맞는지 확인할 수 있습니다.

확장 아이디어

이 CLI 앱을 발판 삼아 더 큰 프로젝트로 확장할 수 있습니다.

Flutter 앱으로 변환. TodoRepositoryTodo 클래스는 그대로 두고 UI 레이어만 Flutter로 교체할 수 있습니다. 핵심 로직을 UI와 분리한 덕분입니다.

HTTP API 서버. dart:ioHttpServershelf 패키지를 사용해 REST API 서버로 만들 수 있습니다. TodoRepository는 변경 없이 그대로 사용합니다.

SQLite 저장. JSON 파일 대신 sqlite3 패키지로 데이터베이스에 저장할 수 있습니다. TodoService만 교체하면 됩니다.

공유 할 일 목록. dart:io의 소켓으로 여러 사람이 같은 목록을 공유하는 기능도 만들 수 있습니다.

마무리

PART 10에서 만든 할 일 관리 CLI 앱은 작은 프로그램이지만, Dart의 여러 기능이 실제로 어떻게 함께 작동하는지 보여줍니다. 클래스로 데이터를 구조화하고, 컬렉션 메서드로 데이터를 조작하고, sealed class와 패턴 매칭으로 명령어를 처리하고, 비동기 I/O로 파일을 다루고, 커스텀 예외로 오류를 관리했습니다.

배운 개념들이 따로 노는 것이 아니라 하나의 완성된 프로그램 안에서 유기적으로 연결된다는 것을 느꼈으면 합니다.

Dart를 배우는 여정의 첫 번째 완성작을 만들었습니다. 여기서 멈추지 말고 이 앱을 자신만의 방식으로 발전시켜보세요. 코드를 직접 수정하고 실행해보는 것이 가장 좋은 학습입니다.