iBetter Books
수정

라우팅과 미들웨어

미들웨어는 요청과 응답 사이에서 공통 작업을 처리합니다. 로깅, CORS, 인증 토큰 검증이 대표적인 예입니다. dart_frog의 미들웨어는 _middleware.dart 파일로 관리합니다.

미들웨어 개념

미들웨어는 요청을 받아 다음 핸들러로 전달하거나, 직접 응답을 반환하는 함수입니다.

요청 → 미들웨어 1 → 미들웨어 2 → 핸들러
                                    ↓
응답 ← 미들웨어 1 ← 미들웨어 2 ←  응답

dart_frog에서 미들웨어는 Middleware 타입입니다. 핸들러를 감싸는 함수입니다.

Middleware myMiddleware() {
  return (Handler handler) {
    return (RequestContext context) async {
      // 요청 전 처리
      print('요청 시작: ${context.request.uri}');

      // 다음 핸들러 호출
      final response = await handler(context);

      // 응답 후 처리
      print('응답 완료: ${response.statusCode}');

      return response;
    };
  };
}

_middleware.dart 파일

routes/_middleware.dart는 전체 라우트에 적용되는 글로벌 미들웨어를 정의합니다.

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

Handler middleware(Handler handler) {
  return handler
      .use(requestLogger())
      .use(corsHeaders());
}

requestLogger()dart_frog가 기본 제공하는 로깅 미들웨어입니다. .use()로 미들웨어를 체이닝합니다.

CORS 미들웨어 직접 구현

dart_frogcorsHeaders()를 기본 제공하지만, 세밀하게 제어하려면 직접 구현합니다.

// 새 파일: lib/src/middleware/cors_middleware.dart
import 'package:dart_frog/dart_frog.dart';

/// CORS 헤더를 추가하는 미들웨어.
Middleware cors({
  List<String> allowedOrigins = const ['*'],
  List<String> allowedMethods = const [
    'GET', 'POST', 'PUT', 'DELETE', 'OPTIONS',
  ],
  List<String> allowedHeaders = const [
    'Content-Type', 'Authorization',
  ],
}) {
  return (Handler handler) {
    return (RequestContext context) async {
      final origin = context.request.headers['Origin'] ?? '*';
      final isAllowed =
          allowedOrigins.contains('*') || allowedOrigins.contains(origin);

      final corsResponseHeaders = {
        'Access-Control-Allow-Origin': isAllowed ? origin : '',
        'Access-Control-Allow-Methods': allowedMethods.join(', '),
        'Access-Control-Allow-Headers': allowedHeaders.join(', '),
        'Access-Control-Max-Age': '86400',
      };

      // OPTIONS(preflight) 요청 처리
      if (context.request.method == HttpMethod.options) {
        return Response(
          statusCode: 204,
          headers: corsResponseHeaders,
        );
      }

      final response = await handler(context);

      // 기존 헤더에 CORS 헤더 추가
      return response.copyWith(
        headers: {...response.headers, ...corsResponseHeaders},
      );
    };
  };
}

로깅 미들웨어 직접 구현

요청과 응답을 구조화된 형태로 로깅합니다.

// 새 파일: lib/src/middleware/logging_middleware.dart
import 'dart:io';

import 'package:dart_frog/dart_frog.dart';

/// 요청/응답을 로깅하는 미들웨어.
Middleware requestLogging() {
  return (Handler handler) {
    return (RequestContext context) async {
      final start = DateTime.now();
      final request = context.request;

      final response = await handler(context);

      final elapsed = DateTime.now().difference(start).inMilliseconds;
      final method = request.method.value.padRight(7);
      final status = response.statusCode;
      final path = request.uri.path;
      final statusColor = _statusColor(status);

      stderr.writeln(
        '$statusColor[$status]\x1B[0m $method $path  ${elapsed}ms',
      );

      return response;
    };
  };
}

String _statusColor(int status) {
  if (status < 300) return '\x1B[32m'; // 초록
  if (status < 400) return '\x1B[33m'; // 노랑
  return '\x1B[31m';                   // 빨강
}

글로벌 미들웨어 업데이트

// 수정: routes/_middleware.dart
import 'package:dart_frog/dart_frog.dart';

import '../lib/src/middleware/cors_middleware.dart';
import '../lib/src/middleware/logging_middleware.dart';

Handler middleware(Handler handler) {
  return handler
      .use(requestLogging())
      .use(cors(
        allowedOrigins: ['http://localhost:3000', 'https://yourapp.com'],
      ));
}

에러 처리 미들웨어

예외가 발생했을 때 일관된 에러 응답을 반환하는 미들웨어입니다.

// 새 파일: lib/src/middleware/error_middleware.dart
import 'package:dart_frog/dart_frog.dart';

import '../exceptions.dart';

/// 예외를 잡아 표준화된 에러 응답으로 변환합니다.
Middleware errorHandler() {
  return (Handler handler) {
    return (RequestContext context) async {
      try {
        return await handler(context);
      } on AppException catch (e) {
        return Response.json(
          statusCode: e.statusCode,
          body: {
            'error': e.message,
            'code': e.code,
          },
        );
      } on FormatException catch (e) {
        return Response.json(
          statusCode: 400,
          body: {'error': '잘못된 요청 형식: ${e.message}'},
        );
      } catch (e, st) {
        // 예상치 못한 에러
        print('내부 서버 오류: $e\n$st');
        return Response.json(
          statusCode: 500,
          body: {'error': '내부 서버 오류가 발생했습니다.'},
        );
      }
    };
  };
}
// 새 파일: lib/src/exceptions.dart

/// 앱 전용 기본 예외 클래스.
class AppException implements Exception {
  const AppException({
    required this.message,
    required this.statusCode,
    this.code,
  });

  final String message;
  final int statusCode;
  final String? code;
}

class NotFoundException extends AppException {
  const NotFoundException(String message)
      : super(message: message, statusCode: 404, code: 'NOT_FOUND');
}

class ValidationException extends AppException {
  const ValidationException(String message)
      : super(message: message, statusCode: 422, code: 'VALIDATION_ERROR');
}

class UnauthorizedException extends AppException {
  const UnauthorizedException([String message = '인증이 필요합니다.'])
      : super(message: message, statusCode: 401, code: 'UNAUTHORIZED');
}

class ForbiddenException extends AppException {
  const ForbiddenException([String message = '권한이 없습니다.'])
      : super(message: message, statusCode: 403, code: 'FORBIDDEN');
}

글로벌 미들웨어 최종 구성

// 수정: routes/_middleware.dart
import 'package:dart_frog/dart_frog.dart';

import '../lib/src/middleware/cors_middleware.dart';
import '../lib/src/middleware/error_middleware.dart';
import '../lib/src/middleware/logging_middleware.dart';

Handler middleware(Handler handler) {
  return handler
      .use(errorHandler())    // 가장 바깥: 에러 처리
      .use(requestLogging())  // 로깅
      .use(cors());           // CORS
}

미들웨어 순서가 중요합니다. .use()는 안쪽부터 적용됩니다. 위 코드에서 실행 순서는 cors → requestLogging → errorHandler → handler입니다.

/todos 전용 인증 미들웨어 뼈대

routes/todos/_middleware.dart/todos 경로에만 적용됩니다.

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

import '../../lib/src/exceptions.dart';

/// /todos 경로에 인증을 적용하는 미들웨어.
/// JWT 검증은 Ch 06에서 구현합니다.
Handler middleware(Handler handler) {
  return handler.use(_authMiddleware());
}

Middleware _authMiddleware() {
  return (Handler handler) {
    return (RequestContext context) async {
      final authHeader = context.request.headers['Authorization'];

      if (authHeader == null || !authHeader.startsWith('Bearer ')) {
        throw const UnauthorizedException();
      }

      // Ch 06에서 실제 JWT 검증으로 교체
      final token = authHeader.substring(7);
      if (token.isEmpty) {
        throw const UnauthorizedException('유효하지 않은 토큰입니다.');
      }

      return handler(context);
    };
  };
}

실행 확인

dart_frog dev
# CORS preflight 요청curl -s -X OPTIONS http://localhost:8080/todos \  -H "Origin: http://localhost:3000" \  -H "Access-Control-Request-Method: POST" \  -v 2>&1 | grep -i "access-control"# 인증 없이 /todos 접근 (401 예상)curl -s http://localhost:8080/todos# 인증 헤더와 함께 접근 (토큰 값은 아직 더미)curl -s http://localhost:8080/todos \  -H "Authorization: Bearer dummy-token"

정리

이번 챕터에서는 미들웨어로 공통 기능을 구현했습니다.

  • _middleware.dart 파일이 해당 디렉토리 라우트 전체에 적용됩니다.
  • CORS, 로깅, 에러 처리 미들웨어를 직접 구현했습니다.
  • AppException 계층으로 에러 코드를 표준화했습니다.
  • routes/todos/_middleware.dart로 특정 경로에만 인증을 적용합니다.

다음 챕터에서는 요청 처리와 응답 설계를 다듬습니다. DTO 패턴과 유효성 검사를 구현합니다.