iBetter Books
수정

dart_frog 프로젝트 시작하기

dart_frog를 설치하고 todo_server 프로젝트를 생성합니다. 처음으로 실제 요청을 처리하는 핸들러를 작성해 봅니다.

dart_frog CLI 설치

dart pub global activate dart_frog_cli

설치 후 PATH에 ~/.pub-cache/bin이 포함되어 있어야 합니다.

dart_frog --version# dart_frog 0.3.x

프로젝트 생성

dart_frog create todo_servercd todo_server

생성된 프로젝트 구조입니다.

todo_server/
├── routes/
│   └── index.dart         ← GET / 핸들러
├── lib/
│   └── todo_server.dart
├── test/
│   └── routes/
│       └── index_test.dart
├── pubspec.yaml
└── analysis_options.yaml

pubspec.yaml 확인 및 설정

# 새 파일: pubspec.yamlname: todo_serverdescription: Todo REST API Server with dart_frog.version: 1.0.0publish_to: noneenvironment:  sdk: ">=3.0.0 <4.0.0"dependencies:  dart_frog: ^1.4.0dev_dependencies:  dart_frog_gen: ^1.6.0  lints: ^3.0.0  test: ^1.24.0  mocktail: ^1.0.3

나중에 SQLite, JWT 패키지를 추가할 예정입니다.

dart pub get

개발 서버 실행 — 핫 리로드

dart_frog dev
✓ Running on http://localhost:8080
✓ Hot reload enabled.
✓ Hot restart enabled.

다른 터미널에서 요청을 보냅니다.

curl http://localhost:8080# {"message": "Welcome to dart_frog!"}

routes/index.dart를 수정하면 서버를 재시작하지 않아도 변경이 반영됩니다.

기본 라우트 핸들러 이해

생성된 routes/index.dart를 살펴봅니다.

// 새 파일: routes/index.dart
import 'package:dart_frog/dart_frog.dart';

Response onRequest(RequestContext context) {
  return Response.json(body: {'message': 'Welcome to dart_frog!'});
}

onRequest(RequestContext context)가 핸들러 함수입니다. 모든 HTTP 메서드의 요청이 이 함수로 들어옵니다. RequestContext에서 요청 정보를 꺼내고, Response를 반환합니다.

파일 기반 라우팅 규칙

dart_frog의 라우팅은 파일 시스템 구조를 따릅니다.

routes/index.dart            → GET /
routes/todos/index.dart      → /todos
routes/todos/[id].dart       → /todos/:id (동적 세그먼트)
routes/auth/login.dart       → /auth/login

[id].dart 파일이 동적 세그먼트를 처리합니다. :id 값은 핸들러에서 context.request.uri.pathSegments로 꺼내거나, 함수 매개변수로 받습니다.

라우트 디렉토리 구성

todo_server에 필요한 라우트 파일을 만듭니다.

mkdir -p routes/todos routes/authtouch routes/todos/index.darttouch routes/todos/[id].darttouch routes/auth/login.darttouch routes/auth/register.dart

첫 번째 실제 핸들러 — /todos

// 새 파일: routes/todos/index.dart
import 'package:dart_frog/dart_frog.dart';

Future<Response> onRequest(RequestContext context) async {
  return switch (context.request.method) {
    HttpMethod.get => _getTodos(context),
    HttpMethod.post => _createTodo(context),
    _ => Response(statusCode: 405),
  };
}

Future<Response> _getTodos(RequestContext context) async {
  // Ch 05에서 데이터베이스 연동 후 교체
  final todos = [
    {'id': 1, 'title': '첫 번째 할 일', 'completed': false},
    {'id': 2, 'title': '두 번째 할 일', 'completed': true},
  ];

  return Response.json(body: todos);
}

Future<Response> _createTodo(RequestContext context) async {
  final body = await context.request.json() as Map<String, dynamic>;
  final title = body['title'] as String?;

  if (title == null || title.isEmpty) {
    return Response.json(
      statusCode: 400,
      body: {'error': 'title은 필수입니다.'},
    );
  }

  // Ch 05에서 실제 저장 구현
  final newTodo = {
    'id': 3,
    'title': title,
    'completed': false,
  };

  return Response.json(statusCode: 201, body: newTodo);
}
// 새 파일: routes/todos/[id].dart
import 'package:dart_frog/dart_frog.dart';

Future<Response> onRequest(RequestContext context, String id) async {
  final todoId = int.tryParse(id);
  if (todoId == null) {
    return Response.json(
      statusCode: 400,
      body: {'error': '유효하지 않은 ID입니다.'},
    );
  }

  return switch (context.request.method) {
    HttpMethod.get => _getTodo(context, todoId),
    HttpMethod.put => _updateTodo(context, todoId),
    HttpMethod.delete => _deleteTodo(context, todoId),
    _ => Response(statusCode: 405),
  };
}

Future<Response> _getTodo(RequestContext context, int id) async {
  // Ch 05에서 DB 조회로 교체
  if (id > 2) {
    return Response.json(
      statusCode: 404,
      body: {'error': '할 일을 찾을 수 없습니다.'},
    );
  }
  return Response.json(body: {'id': id, 'title': '할 일 $id', 'completed': false});
}

Future<Response> _updateTodo(RequestContext context, int id) async {
  final body = await context.request.json() as Map<String, dynamic>;
  return Response.json(body: {'id': id, ...body});
}

Future<Response> _deleteTodo(RequestContext context, int id) async {
  return Response(statusCode: 204);
}

RequestContext 주요 API

// HTTP 메서드 확인
context.request.method; // HttpMethod.get, .post, ...

// URL 정보
context.request.uri;            // Uri 객체
context.request.uri.path;       // '/todos/1'
context.request.uri.queryParameters; // {'limit': '10'}

// 헤더
context.request.headers;        // Map<String, String>

// 요청 본문
final body = await context.request.body();        // 원시 String
final json = await context.request.json();        // dynamic (Map 또는 List)
final bytes = await context.request.bytes();      // Uint8List

// 의존성 읽기 (미들웨어에서 주입한 값)
final db = context.read<Database>();

Response 생성

// 기본 응답
Response(statusCode: 200, body: 'Hello');

// JSON 응답 (Content-Type 자동 설정)
Response.json(body: {'key': 'value'});
Response.json(statusCode: 201, body: newItem);

// 에러 응답
Response(statusCode: 404);
Response.json(statusCode: 422, body: {'error': '유효성 오류'});

// 헤더 포함
Response(
  statusCode: 200,
  body: 'content',
  headers: {'X-Custom-Header': 'value'},
);

프로젝트 구조 설계

전체 todo_server 구조를 미리 설계합니다.

todo_server/
├── routes/
│   ├── index.dart
│   ├── _middleware.dart          ← 전역 미들웨어
│   ├── todos/
│   │   ├── _middleware.dart      ← /todos 인증 미들웨어
│   │   ├── index.dart            ← GET/POST /todos
│   │   └── [id].dart             ← GET/PUT/DELETE /todos/:id
│   └── auth/
│       ├── login.dart            ← POST /auth/login
│       └── register.dart         ← POST /auth/register
├── lib/
│   └── src/
│       ├── models/
│       │   ├── todo.dart
│       │   └── user.dart
│       ├── repositories/
│       │   ├── todo_repository.dart
│       │   └── user_repository.dart
│       ├── services/
│       │   ├── auth_service.dart
│       │   └── todo_service.dart
│       └── database.dart
├── test/
├── pubspec.yaml
└── Dockerfile

실행 확인

# 개발 서버 시작dart_frog dev# 다른 터미널에서 테스트curl -s http://localhost:8080/todos | python3 -m json.toolcurl -s -X POST http://localhost:8080/todos \  -H "Content-Type: application/json" \  -d '{"title": "첫 번째 할 일"}' | python3 -m json.toolcurl -s http://localhost:8080/todos/1 | python3 -m json.toolcurl -s http://localhost:8080/todos/99 | python3 -m json.tool

빌드

배포용 실행 파일을 만들 때는 dart_frog build를 사용합니다.

dart_frog build

.dart_frog/server.dart에 실행 가능한 서버 파일이 생성됩니다. dart compile exe와 동일한 방식으로 네이티브 바이너리로 컴파일할 수 있습니다.

정리

이번 챕터에서는 dart_frog 프로젝트를 생성하고 기본 구조를 잡았습니다.

  • dart_frog create로 프로젝트를 생성하고 dart_frog dev로 핫 리로드 서버를 실행합니다.
  • 파일 경로가 URL 경로입니다. [id].dart로 동적 세그먼트를 처리합니다.
  • onRequest(RequestContext context, String id) 함수가 핸들러입니다.
  • Response.json()으로 JSON 응답을 쉽게 만듭니다.

다음 챕터에서는 미들웨어로 CORS, 로깅, 인증을 처리합니다.