Zum Hauptinhalt springen

🚦 HTTP 상태 코드

📖 정의

HTTP 상태 코드는 서버가 클라이언트의 요청을 어떻게 처리했는지 알려주는 3자리 숫자입니다. 첫 번째 자리 숫자에 따라 5개의 그룹으로 분류됩니다.

🎯 비유로 이해하기

레스토랑 주문 시스템

2xx (성공) = "주문하신 음식 나왔습니다!"
├─ 200: 음식 완성, 바로 드세요
├─ 201: 새로운 메뉴 등록 완료
└─ 204: 음식 치웠습니다 (테이블 정리 완료)

3xx (리다이렉션) = "다른 곳으로 안내해드릴게요"
├─ 301: 매장이 영구적으로 이전했습니다
├─ 302: 임시로 다른 곳에서 서빙합니다
└─ 304: 이미 드신 음식이라 새로 안 나옵니다 (캐시)

4xx (클라이언트 오류) = "손님 실수입니다"
├─ 400: 주문을 이해할 수 없어요
├─ 401: 회원만 주문 가능합니다
├─ 403: 이 메뉴는 주문 불가능합니다
├─ 404: 그런 메뉴 없습니다
└─ 429: 주문을 너무 많이 하셨어요

5xx (서버 오류) = "저희 실수입니다"
├─ 500: 주방에 문제가 생겼습니다
├─ 502: 주방과 연결이 안 됩니다
├─ 503: 지금 너무 바빠서 주문 받을 수 없어요
└─ 504: 주방 응답이 너무 느립니다

💡 상태 코드 그룹

┌─────┬─────────────┬────────────────────┐
│ 코드│ 분류 │ 의미 │
├─────┼─────────────┼────────────────────┤
│ 1xx │ 정보 │ 요청 처리 중 │
│ 2xx │ 성공 │ 요청 성공 │
│ 3xx │ 리다이렉션 │ 추가 조치 필요 │
│ 4xx │ 클라이언트 │ 클라이언트 오류 │
│ 5xx │ 서버 오류 │ 서버 오류 │
└─────┴─────────────┴────────────────────┘

✅ 2xx - 성공

200 OK

가장 일반적인 성공 응답

요청:
GET /api/users/123 HTTP/1.1

응답:
HTTP/1.1 200 OK
Content-Type: application/json

{
"id": 123,
"name": "홍길동",
"email": "hong@example.com"
}
fetch('https://api.example.com/users/123')
.then(response => {
if (response.status === 200) {
return response.json();
}
})
.then(data => console.log(data));

사용 사례:

  • GET 요청 성공
  • PUT, PATCH 요청 성공
  • 데이터가 포함된 모든 성공 응답

201 Created

새로운 리소스 생성 성공

요청:
POST /api/users HTTP/1.1
Content-Type: application/json

{
"name": "홍길동",
"email": "hong@example.com"
}

응답:
HTTP/1.1 201 Created
Location: /api/users/123
Content-Type: application/json

{
"id": 123,
"name": "홍길동",
"email": "hong@example.com"
}
fetch('https://api.example.com/users', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
name: '홍길동',
email: 'hong@example.com'
})
})
.then(response => {
if (response.status === 201) {
console.log('사용자 생성 성공!');
// Location 헤더에서 새 리소스 URL 확인
console.log(response.headers.get('Location'));
return response.json();
}
});

204 No Content

성공했지만 반환할 내용이 없음

요청:
DELETE /api/users/123 HTTP/1.1

응답:
HTTP/1.1 204 No Content
fetch('https://api.example.com/users/123', {
method: 'DELETE'
})
.then(response => {
if (response.status === 204) {
console.log('삭제 성공!');
// 응답 바디 없음
}
});

사용 사례:

  • DELETE 요청 성공
  • PUT, PATCH 후 반환 데이터 없을 때
  • 작업 성공했지만 클라이언트에게 전달할 내용 없을 때

기타 2xx 코드

202 Accepted
├─ 요청 접수됨, 처리는 비동기로 진행
└─ 예: 대용량 파일 처리, 이메일 전송

206 Partial Content
├─ 일부 콘텐츠만 반환
└─ 예: 동영상 스트리밍, 대용량 파일 다운로드

🔀 3xx - 리다이렉션

301 Moved Permanently

리소스가 영구적으로 이동

요청:
GET /old-page HTTP/1.1

응답:
HTTP/1.1 301 Moved Permanently
Location: https://example.com/new-page
// 브라우저가 자동으로 리다이렉트
fetch('https://api.example.com/old-endpoint')
.then(response => {
if (response.status === 301) {
console.log('영구 이동:', response.headers.get('Location'));
}
});

사용 사례:

  • 사이트 주소 변경
  • API 엔드포인트 영구 변경
  • SEO: 검색 엔진이 새 URL 저장

302 Found (Temporary Redirect)

리소스가 일시적으로 이동

응답:
HTTP/1.1 302 Found
Location: https://example.com/temp-page

사용 사례:

  • 임시 페이지로 이동
  • 로그인 페이지로 리다이렉트
  • A/B 테스팅

304 Not Modified

캐시된 데이터 사용 가능

요청:
GET /api/users/123 HTTP/1.1
If-None-Match: "abc123"

응답:
HTTP/1.1 304 Not Modified
ETag: "abc123"
// 브라우저가 자동으로 캐시 사용
fetch('https://api.example.com/users/123')
.then(response => {
if (response.status === 304) {
console.log('캐시 사용 중');
}
});

장점:

  • 네트워크 대역폭 절약
  • 응답 속도 향상
  • 서버 부하 감소

기타 3xx 코드

303 See Other
├─ 다른 URL에서 GET으로 조회
└─ 예: POST 후 결과 페이지로 이동

307 Temporary Redirect
├─ 302와 유사하지만 메서드 유지 보장
└─ 예: POST → POST로 리다이렉트

308 Permanent Redirect
├─ 301과 유사하지만 메서드 유지 보장
└─ 예: POST → POST로 영구 리다이렉트

❌ 4xx - 클라이언트 오류

400 Bad Request

잘못된 요청 형식

요청:
POST /api/users HTTP/1.1
Content-Type: application/json

{
"name": "", // 빈 이름
"email": "invalid-email" // 잘못된 이메일 형식
}

응답:
HTTP/1.1 400 Bad Request
Content-Type: application/json

{
"error": "Validation Error",
"message": "Invalid request data",
"details": [
{
"field": "name",
"message": "Name is required"
},
{
"field": "email",
"message": "Invalid email format"
}
]
}
fetch('https://api.example.com/users', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
name: '',
email: 'invalid-email'
})
})
.then(response => {
if (response.status === 400) {
return response.json();
}
})
.then(error => {
console.error('유효성 검사 실패:', error);
});

401 Unauthorized

인증 필요

요청:
GET /api/profile HTTP/1.1

응답:
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer realm="example"
Content-Type: application/json

{
"error": "Unauthorized",
"message": "Authentication required"
}
fetch('https://api.example.com/profile', {
headers: {
'Authorization': 'Bearer YOUR_TOKEN_HERE'
}
})
.then(response => {
if (response.status === 401) {
console.error('인증 필요 - 로그인 페이지로 이동');
window.location.href = '/login';
}
});

사용 사례:

  • 로그인하지 않은 사용자
  • 만료된 토큰
  • 잘못된 인증 정보

403 Forbidden

권한 없음

요청:
DELETE /api/users/999 HTTP/1.1
Authorization: Bearer user_token

응답:
HTTP/1.1 403 Forbidden
Content-Type: application/json

{
"error": "Forbidden",
"message": "You don't have permission to delete this user"
}
fetch('https://api.example.com/admin/users/999', {
method: 'DELETE',
headers: {
'Authorization': 'Bearer USER_TOKEN'
}
})
.then(response => {
if (response.status === 403) {
console.error('권한이 없습니다');
alert('관리자만 삭제할 수 있습니다');
}
});

401 vs 403:

401 Unauthorized (인증 실패)
├─ "당신이 누구인지 모르겠습니다"
├─ 로그인하지 않음
├─ 토큰 없음/만료
└─ 해결: 로그인 필요

403 Forbidden (인가 실패)
├─ "당신이 누구인지는 알지만 권한이 없습니다"
├─ 로그인은 됨
├─ 권한 부족
└─ 해결: 관리자에게 권한 요청

404 Not Found

리소스를 찾을 수 없음

요청:
GET /api/users/999999 HTTP/1.1

응답:
HTTP/1.1 404 Not Found
Content-Type: application/json

{
"error": "Not Found",
"message": "User with ID 999999 not found"
}
fetch('https://api.example.com/users/999999')
.then(response => {
if (response.status === 404) {
console.error('사용자를 찾을 수 없습니다');
// 404 페이지 표시
}
return response.json();
});

사용 사례:

  • 존재하지 않는 페이지
  • 삭제된 리소스
  • 잘못된 URL

기타 4xx 코드

405 Method Not Allowed
├─ 허용되지 않는 HTTP 메서드
└─ 예: GET만 가능한데 POST 요청

409 Conflict
├─ 요청이 현재 서버 상태와 충돌
└─ 예: 이미 존재하는 이메일로 가입 시도

422 Unprocessable Entity
├─ 문법은 맞지만 의미상 처리 불가
└─ 예: 날짜 형식은 맞지만 미래 날짜

429 Too Many Requests
├─ 요청 횟수 제한 초과
└─ 예: API 호출 제한 (Rate Limiting)

429 Too Many Requests 처리

async function fetchWithRetry(url, options = {}, maxRetries = 3) {
for (let i = 0; i < maxRetries; i++) {
const response = await fetch(url, options);

if (response.status === 429) {
const retryAfter = response.headers.get('Retry-After') || 60;
console.log(`Too many requests. Retrying after ${retryAfter}s...`);
await new Promise(resolve => setTimeout(resolve, retryAfter * 1000));
continue;
}

return response;
}

throw new Error('Max retries exceeded');
}

🔥 5xx - 서버 오류

500 Internal Server Error

서버 내부 오류

요청:
GET /api/users HTTP/1.1

응답:
HTTP/1.1 500 Internal Server Error
Content-Type: application/json

{
"error": "Internal Server Error",
"message": "An unexpected error occurred"
}
fetch('https://api.example.com/users')
.then(response => {
if (response.status === 500) {
console.error('서버 오류 발생');
alert('일시적인 오류입니다. 잠시 후 다시 시도해주세요.');
}
});

원인:

  • 코드 버그
  • 처리되지 않은 예외
  • 데이터베이스 오류
  • 서버 설정 문제

502 Bad Gateway

게이트웨이 오류

응답:
HTTP/1.1 502 Bad Gateway
Content-Type: text/html

<html>
<body>
<h1>502 Bad Gateway</h1>
<p>The server received an invalid response from the upstream server</p>
</body>
</html>

원인:

  • 프록시 서버와 백엔드 서버 간 통신 실패
  • 백엔드 서버 다운
  • 네트워크 문제

503 Service Unavailable

서비스 이용 불가

응답:
HTTP/1.1 503 Service Unavailable
Retry-After: 3600
Content-Type: application/json

{
"error": "Service Unavailable",
"message": "Server is under maintenance",
"retryAfter": 3600
}
fetch('https://api.example.com/users')
.then(response => {
if (response.status === 503) {
const retryAfter = response.headers.get('Retry-After');
console.log(`서비스 점검 중. ${retryAfter}초 후 재시도`);
}
});

원인:

  • 서버 점검
  • 과부하
  • 일시적 중단

504 Gateway Timeout

게이트웨이 타임아웃

응답:
HTTP/1.1 504 Gateway Timeout
Content-Type: application/json

{
"error": "Gateway Timeout",
"message": "The server did not respond in time"
}

원인:

  • 백엔드 서버 응답 지연
  • 네트워크 지연
  • 데이터베이스 쿼리 타임아웃

📊 상태 코드 치트시트

┌─────┬────────────────────┬─────────────────────────┐
│ 코드│ 이름 │ 언제 사용? │
├─────┼────────────────────┼─────────────────────────┤
│ 200 │ OK │ 성공 (일반) │
│ 201 │ Created │ 생성 성공 │
│ 204 │ No Content │ 성공 (응답 없음) │
├─────┼────────────────────┼─────────────────────────┤
│ 301 │ Moved Permanently │ 영구 이동 │
│ 302 │ Found │ 임시 이동 │
│ 304 │ Not Modified │ 캐시 사용 │
├─────┼────────────────────┼─────────────────────────┤
│ 400 │ Bad Request │ 잘못된 요청 │
│ 401 │ Unauthorized │ 인증 필요 │
│ 403 │ Forbidden │ 권한 없음 │
│ 404 │ Not Found │ 리소스 없음 │
│ 429 │ Too Many Requests │ 요청 제한 초과 │
├─────┼────────────────────┼─────────────────────────┤
│ 500 │ Internal Error │ 서버 오류 │
│ 502 │ Bad Gateway │ 게이트웨이 오류 │
│ 503 │ Service Unavailable│ 서비스 불가 │
│ 504 │ Gateway Timeout │ 게이트웨이 타임아웃 │
└─────┴────────────────────┴─────────────────────────┘

🛠️ 실전 에러 처리

기본 에러 처리

async function fetchData(url) {
try {
const response = await fetch(url);

// 상태 코드별 처리
switch (response.status) {
case 200:
return await response.json();

case 401:
console.error('인증 필요');
window.location.href = '/login';
break;

case 403:
console.error('권한 없음');
alert('접근 권한이 없습니다');
break;

case 404:
console.error('리소스 없음');
return null;

case 500:
case 502:
case 503:
case 504:
console.error('서버 오류');
alert('일시적인 오류입니다. 잠시 후 다시 시도해주세요');
break;

default:
console.error('알 수 없는 오류:', response.status);
}
} catch (error) {
console.error('네트워크 오류:', error);
}
}

재시도 로직

async function fetchWithRetry(url, options = {}, retries = 3) {
for (let i = 0; i < retries; i++) {
try {
const response = await fetch(url, options);

// 성공 또는 클라이언트 오류 (재시도 불필요)
if (response.ok || response.status < 500) {
return response;
}

// 서버 오류 - 재시도
if (i < retries - 1) {
console.log(`서버 오류. ${i + 1}번째 재시도...`);
await new Promise(resolve => setTimeout(resolve, 1000 * (i + 1)));
}
} catch (error) {
if (i === retries - 1) throw error;
await new Promise(resolve => setTimeout(resolve, 1000 * (i + 1)));
}
}
}

글로벌 에러 핸들러

// axios interceptor
axios.interceptors.response.use(
response => response,
error => {
const status = error.response?.status;

switch (status) {
case 401:
// 토큰 갱신 또는 로그인 페이지로
refreshToken().catch(() => {
window.location.href = '/login';
});
break;

case 403:
alert('접근 권한이 없습니다');
break;

case 404:
console.error('리소스를 찾을 수 없습니다');
break;

case 429:
alert('요청이 너무 많습니다. 잠시 후 다시 시도해주세요');
break;

case 500:
case 502:
case 503:
case 504:
alert('서버 오류가 발생했습니다');
break;

default:
console.error('알 수 없는 오류:', status);
}

return Promise.reject(error);
}
);

🤔 자주 묻는 질문

Q1. 200과 201의 차이는?

A:

200 OK
├─ 일반적인 성공
├─ GET, PUT, PATCH에 주로 사용
└─ 예: 데이터 조회 성공

201 Created
├─ 새로운 리소스 생성 성공
├─ POST에 주로 사용
├─ Location 헤더에 새 리소스 URL 포함
└─ 예: 회원 가입 성공

실전 예시:
GET /api/users/123 → 200 OK
POST /api/users → 201 Created
PUT /api/users/123 → 200 OK

Q2. 401과 403을 언제 사용하나?

A:

401 Unauthorized (인증 문제)
├─ 로그인하지 않음
├─ 토큰 없음
├─ 토큰 만료
└─ 해결: 로그인 필요

403 Forbidden (권한 문제)
├─ 로그인은 됨
├─ 일반 사용자가 관리자 기능 접근
├─ 계정 정지/차단
└─ 해결: 권한 획득 필요

플로우:
1. 토큰 있나? → 없음 → 401
2. 토큰 유효한가? → 만료 → 401
3. 권한 있나? → 없음 → 403
4. 모두 OK → 200

Q3. 404 vs 410 차이는?

A:

404 Not Found
├─ 리소스를 찾을 수 없음
├─ 일시적일 수도, 영구적일 수도
└─ 예: 잘못된 URL, 존재하지 않는 ID

410 Gone
├─ 리소스가 영구적으로 삭제됨
├─ 의도적으로 제거됨
├─ 다시는 사용 불가
└─ 예: 만료된 프로모션, 삭제된 계정

실전에서는 대부분 404 사용:
- 404가 더 범용적
- 410은 명시적으로 "영구 삭제"를 알려야 할 때만

Q4. 500 vs 503 차이는?

A:

500 Internal Server Error
├─ 예상치 못한 서버 오류
├─ 코드 버그, 예외 처리 실패
├─ 재시도해도 계속 발생
└─ 개발자가 수정 필요

503 Service Unavailable
├─ 일시적으로 서비스 불가
├─ 서버 점검, 과부하
├─ 재시도하면 성공할 수 있음
├─ Retry-After 헤더 포함 가능
└─ 곧 정상화될 예정

언제 사용?
500: 코드 버그 발생 시
503: 의도적 중단, 과부하 시

Q5. 상태 코드를 잘못 사용하면?

A:

❌ 나쁜 예:

// 모든 에러를 200으로 반환
HTTP/1.1 200 OK
{
"success": false,
"error": "User not found"
}

문제점:
├─ HTTP 캐싱 방해
├─ 에러 추적 어려움
├─ 클라이언트 처리 복잡
└─ HTTP 표준 위반

✅ 좋은 예:

// 적절한 상태 코드 사용
HTTP/1.1 404 Not Found
{
"error": "Not Found",
"message": "User not found"
}

장점:
├─ HTTP 의미론 준수
├─ 캐싱, 로깅 자동화
├─ 클라이언트 처리 간단
└─ 디버깅 용이

🎓 실습하기

1. 모든 상태 코드 처리

async function handleResponse(response) {
// 2xx
if (response.ok) {
if (response.status === 204) {
return null; // No Content
}
return await response.json();
}

// 3xx
if (response.status >= 300 && response.status < 400) {
const location = response.headers.get('Location');
console.log('리다이렉트:', location);
return null;
}

// 4xx
if (response.status >= 400 && response.status < 500) {
const error = await response.json();
throw new Error(error.message || 'Client error');
}

// 5xx
if (response.status >= 500) {
throw new Error('Server error');
}
}

2. 상태 코드 테스트

// httpstat.us를 사용한 테스트
async function testStatusCodes() {
const codes = [200, 201, 204, 400, 401, 403, 404, 500, 503];

for (const code of codes) {
try {
const response = await fetch(`https://httpstat.us/${code}`);
console.log(`${code}: ${response.statusText}`);
} catch (error) {
console.error(`${code}: Error`, error);
}
}
}

🔗 관련 문서

🎬 마치며

HTTP 상태 코드는 클라이언트와 서버 간의 커뮤니케이션을 명확하게 해줍니다. 적절한 상태 코드를 사용하면 디버깅이 쉬워지고, 에러 처리가 간단해지며, HTTP 표준을 준수하는 견고한 API를 만들 수 있습니다!

다음 단계: HTTP 헤더를 읽어보며 Content-Type, Authorization 등의 헤더를 알아보세요.