PHP REST API에서 응답 포맷이 일관되지 않거나 잘못된 형식으로 전송되는 문제는 클라이언트와의 통신에 심각한 문제를 일으킵니다. 다음은 주요 원인과 해결책들입니다.
문제: API 응답 형식이 엔드포인트마다 다름
해결책:
class ApiResponse {
public static function success($data = null, $message = 'Success', $code = 200) {
http_response_code($code);
header('Content-Type: application/json');
return json_encode([
'success' => true,
'code' => $code,
'message' => $message,
'data' => $data,
'timestamp' => date('c')
]);
}
public static function error($message = 'Error', $code = 400, $errors = null) {
http_response_code($code);
header('Content-Type: application/json');
return json_encode([
'success' => false,
'code' => $code,
'message' => $message,
'errors' => $errors,
'timestamp' => date('c')
]);
}
}
// 사용 예시
echo ApiResponse::success(['users' => $users], 'Users retrieved successfully');
echo ApiResponse::error('User not found', 404);
문제: JSON 데이터를 보내지만 Content-Type 헤더가 설정되지 않음
해결책:
function sendJsonResponse($data, $statusCode = 200) {
// 출력 버퍼 정리
while (ob_get_level()) {
ob_end_clean();
}
// 헤더 설정
header('Content-Type: application/json; charset=utf-8');
header('Cache-Control: no-cache, no-store, must-revalidate');
http_response_code($statusCode);
// JSON 인코딩 옵션
$jsonOptions = JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES;
if (defined('JSON_THROW_ON_ERROR')) {
$jsonOptions |= JSON_THROW_ON_ERROR;
}
echo json_encode($data, $jsonOptions);
exit();
}
문제: 특수문자나 인코딩 문제로 JSON 생성 실패
해결책:
function safeJsonEncode($data) {
// UTF-8 인코딩 확인 및 변환
$data = convertToUtf8($data);
$json = json_encode($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
if (json_last_error() !== JSON_ERROR_NONE) {
$error = json_last_error_msg();
// 일반적인 JSON 오류 처리
switch (json_last_error()) {
case JSON_ERROR_UTF8:
$data = utf8_encode_deep($data);
$json = json_encode($data);
break;
case JSON_ERROR_DEPTH:
throw new Exception('JSON encoding failed: Maximum stack depth exceeded');
case JSON_ERROR_SYNTAX:
throw new Exception('JSON encoding failed: Syntax error');
default:
throw new Exception('JSON encoding failed: ' . $error);
}
}
return $json;
}
function convertToUtf8($data) {
if (is_string($data)) {
return mb_convert_encoding($data, 'UTF-8', 'auto');
} elseif (is_array($data)) {
return array_map('convertToUtf8', $data);
}
return $data;
}
문제: 적절하지 않은 HTTP 상태 코드 사용
해결책:
class HttpStatus {
const OK = 200;
const CREATED = 201;
const NO_CONTENT = 204;
const BAD_REQUEST = 400;
const UNAUTHORIZED = 401;
const FORBIDDEN = 403;
const NOT_FOUND = 404;
const METHOD_NOT_ALLOWED = 405;
const CONFLICT = 409;
const UNPROCESSABLE_ENTITY = 422;
const INTERNAL_SERVER_ERROR = 500;
public static function getStatusText($code) {
$statusTexts = [
200 => 'OK',
201 => 'Created',
204 => 'No Content',
400 => 'Bad Request',
401 => 'Unauthorized',
403 => 'Forbidden',
404 => 'Not Found',
405 => 'Method Not Allowed',
409 => 'Conflict',
422 => 'Unprocessable Entity',
500 => 'Internal Server Error'
];
return $statusTexts[$code] ?? 'Unknown Status';
}
}
// 사용 예시
function createUser($userData) {
try {
$user = saveUser($userData);
echo ApiResponse::success($user, 'User created successfully', HttpStatus::CREATED);
} catch (ValidationException $e) {
echo ApiResponse::error('Validation failed', HttpStatus::UNPROCESSABLE_ENTITY, $e->getErrors());
}
}
문제: 에러 응답이 일관되지 않아 클라이언트에서 처리 어려움
해결책:
class ApiError {
public static function validationError($errors) {
return ApiResponse::error('Validation failed', 422, [
'validation_errors' => $errors
]);
}
public static function notFound($resource = 'Resource') {
return ApiResponse::error("$resource not found", 404);
}
public static function unauthorized($message = 'Unauthorized access') {
return ApiResponse::error($message, 401);
}
public static function forbidden($message = 'Access forbidden') {
return ApiResponse::error($message, 403);
}
public static function serverError($message = 'Internal server error') {
error_log("API Server Error: $message");
return ApiResponse::error('An error occurred. Please try again later.', 500);
}
}
// 사용 예시
if (!$user) {
echo ApiError::notFound('User');
exit();
}
if (!hasPermission($user, 'read')) {
echo ApiError::forbidden('You do not have permission to access this resource');
exit();
}
문제: 페이지네이션 정보가 일관되지 않게 제공됨
해결책:
function paginatedResponse($data, $page, $perPage, $total) {
$totalPages = ceil($total / $perPage);
return ApiResponse::success([
'items' => $data,
'pagination' => [
'current_page' => (int)$page,
'per_page' => (int)$perPage,
'total_items' => (int)$total,
'total_pages' => (int)$totalPages,
'has_next' => $page < $totalPages,
'has_prev' => $page > 1
]
]);
}
// 사용 예시
$page = $_GET['page'] ?? 1;
$perPage = $_GET['per_page'] ?? 10;
$users = getUsers($page, $perPage);
$totalUsers = getTotalUsers();
echo paginatedResponse($users, $page, $perPage, $totalUsers);
문제: 크로스 오리진 요청 시 응답이 차단됨
해결책:
function setCorsHeaders() {
$origin = $_SERVER['HTTP_ORIGIN'] ?? '*';
// 허용된 도메인 체크
$allowedOrigins = ['https://myapp.com', 'https://api.myapp.com'];
if (in_array($origin, $allowedOrigins) || $origin === 'http://localhost:3000') {
header("Access-Control-Allow-Origin: $origin");
}
header('Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type, Authorization, X-Requested-With');
header('Access-Control-Allow-Credentials: true');
header('Access-Control-Max-Age: 86400');
// OPTIONS 요청 처리
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
http_response_code(200);
exit();
}
}
// 모든 API 요청 시작 시 호출
setCorsHeaders();
문제: 민감한 정보가 API 응답에 포함됨
해결책:
class DataTransformer {
public static function filterUserData($user, $includePrivate = false) {
$publicFields = ['id', 'name', 'email', 'created_at'];
$privateFields = ['phone', 'address', 'last_login'];
$fields = $includePrivate ? array_merge($publicFields, $privateFields) : $publicFields;
return array_intersect_key($user, array_flip($fields));
}
public static function transformCollection($items, $transformer) {
return array_map($transformer, $items);
}
}
// 사용 예시
$users = getUsers();
$transformedUsers = DataTransformer::transformCollection($users, function($user) {
return DataTransformer::filterUserData($user, hasAdminPermission());
});
echo ApiResponse::success($transformedUsers);
문제: API 버전별로 다른 응답 포맷 필요
해결책:
class ApiVersionManager {
private $version;
public function __construct() {
$this->version = $_SERVER['HTTP_API_VERSION'] ?? 'v1';
}
public function formatResponse($data) {
switch ($this->version) {
case 'v1':
return $this->formatV1($data);
case 'v2':
return $this->formatV2($data);
default:
return ApiResponse::error('Unsupported API version', 400);
}
}
private function formatV1($data) {
return [
'status' => 'success',
'result' => $data
];
}
private function formatV2($data) {
return [
'success' => true,
'data' => $data,
'meta' => [
'version' => 'v2',
'timestamp' => time()
]
];
}
}
// 사용 예시
$versionManager = new ApiVersionManager();
echo json_encode($versionManager->formatResponse($userData));