서버 운영
프론트와 서버의 상태 코드 계약이 어긋날 때
클라이언트는 200을 기대하는데 서버가 400을 보내거나, 에러 형식이 다르면 앱이 깨진다. 하나의 규칙으로 통일하자.
프론트엔드가 예상하는 상태 코드와 서버가 실제로 보내는 상태 코드가 다르다. 또는 에러 응답 형식이 다르다. 이런 계약 어긋남은 보통 개발 초기에 정하지 않아서 생긴다.
첫 번째: 응답을 직접 본다
# 에러 케이스
curl -i -X POST https://api.example.com/login \
-H 'Content-Type: application/json' \
-d '{"email": "invalid"}'
# 상태 코드, 헤더, body를 모두 본다
브라우저 DevTools의 Network 탭에서도 확인할 수 있다. 실패 요청의 상태 코드와 응답 body를 본다.
두 번째: 성공/실패 기준을 정한다
// 서버 응답 타입
type ApiResponse<T> =
| { success: true; data: T; status: 200 | 201 }
| { success: false; error: string; status: 400 | 401 | 500 };
// 또는 HTTP 상태 코드 기준으로
// 200-299: 성공
// 400: 클라이언트 오류 (유효하지 않은 입력)
// 401: 인증 실패
// 403: 권한 없음
// 500-599: 서버 오류
팀이 동의하는 규칙을 정한다. 보통 HTTP 표준을 따르는 게 최선이다.
세 번째: 클라이언트의 요청 처리 로직을 정한다
async function fetchData(url) {
const response = await fetch(url);
if (response.ok) {
// 2xx 상태 코드
const data = await response.json();
return { success: true, data };
}
if (response.status === 401) {
// 토큰 만료
window.location.href = '/login';
return { success: false, error: '인증이 필요합니다' };
}
if (response.status === 400) {
// 유효성 검사 실패
const error = await response.json();
return { success: false, error: error.message };
}
if (response.status >= 500) {
// 서버 오류
return { success: false, error: '서버 오류가 발생했습니다' };
}
// 예상 밖의 상태 코드
console.error(`Unexpected status: ${response.status}`);
return { success: false, error: '알 수 없는 오류' };
}
각 상태 코드별로 명확한 처리 로직을 정한다.
네 번째: 에러 응답 형식을 통일한다
// 옵션 1: 간단한 형식
{
"error": "Invalid email format"
}
// 옵션 2: 구조화된 형식
{
"errors": [
{
"field": "email",
"message": "Invalid email format",
"code": "INVALID_EMAIL"
}
]
}
// 옵션 3: JSON:API 형식
{
"errors": [
{
"status": "400",
"code": "INVALID_EMAIL",
"title": "Invalid Email",
"detail": "The provided email address is not valid"
}
]
}
팀이 동의하는 형식으로 통일한다. 모든 에러 응답이 같은 구조를 따르면 클라이언트 처리가 훨씬 쉽다.
다섯 번째: 계약을 테스트한다
// Jest 예제
describe('API Contracts', () => {
test('200 response should have data field', async () => {
const response = await fetch('/api/users/me');
expect(response.status).toBe(200);
const json = await response.json();
expect(json).toHaveProperty('data');
});
test('400 response should have error message', async () => {
const response = await fetch('/api/login', {
method: 'POST',
body: JSON.stringify({ email: 'invalid' })
});
expect(response.status).toBe(400);
const json = await response.json();
expect(json).toHaveProperty('error');
});
});
API 계약을 테스트 코드로 명시하면, 나중에 어긋나는 걸 방지할 수 있다.
체크리스트
- 성공/실패 상태 코드를 정한다 (HTTP 표준 따르기)
- 각 상태 코드별 응답 형식을 정한다
- 클라이언트의 상태 코드별 처리 로직을 구현한다
- 에러 응답 형식을 통일한다
- 계약을 테스트 코드로 검증한다
- 새로운 엔드포인트를 추가할 때는 기존 규칙을 따른다
프론트와 서버의 상태 코드 계약은 개발 초기에 정하고 문서화하는 게 중요하다. 한 번 정하면 모든 엔드포인트가 같은 규칙을 따르도록 강제하자.