1. 라이브러리
  2. HTTP와 웹
  3. HTTP 상태 코드

업데이트됨 1개월 전

서버가 201 Created를 반환하면, 서버는 하나의 분명한 선언을 하고 있는 겁니다: 전에는 없던 무언가가 이제 생겨났다는 것입니다. 단순한 성공 응답이 아닙니다—일종의 탄생 신고서입니다. 서버가 이렇게 말하는 겁니다: "제가 무언가를 만들었습니다. 전에는 없었습니다. 이제 있습니다. 여기 있습니다."

200 OK와는 다릅니다. 200 OK는 "요청이 성공했고 결과는 이것입니다"라는 뜻입니다. 201은 여러분의 요청이 새로운 무언가를 세상에 만들어냈다는 뜻입니다.

Location 헤더: 새 리소스가 있는 곳

201 응답에는 새로 생성된 리소스를 가리키는 Location 헤더가 포함되어야 합니다:

HTTP/1.1 201 Created
Location: /api/users/124
Content-Type: application/json

{
    "id": 124,
    "name": "Alice",
    "email": "alice@example.com",
    "createdAt": "2024-10-21T14:22:00Z"
}

Location 헤더는 당연히 따라오는 질문에 답합니다: "좋아요, 뭔가를 만들었다는 건 알겠는데—어디 있나요?" 이 헤더가 없으면 클라이언트는 응답 본문을 직접 파싱하고, ID를 찾아내고, URL을 스스로 구성해야 합니다. Location 헤더는 그 수고를 덜어줍니다.

응답 본문에 무엇을 담을 것인가

서버가 생성한 모든 내용을 포함하여, 완성된 리소스 전체를 반환하세요:

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

{
    "name": "Alice",
    "email": "alice@example.com"
}

HTTP/1.1 201 Created
Location: /api/users/124
Content-Type: application/json

{
    "id": 124,
    "name": "Alice",
    "email": "alice@example.com",
    "createdAt": "2024-10-21T14:22:00Z",
    "updatedAt": "2024-10-21T14:22:00Z",
    "emailVerified": false
}

클라이언트는 두 개의 필드를 보냈습니다. 서버는 여섯 개를 돌려줍니다. 그 네 개의 추가 필드—id, createdAt, updatedAt, emailVerified—는 서버가 채워 넣은 값들입니다. 클라이언트는 별도 요청 없이 이 값들을 바로 받아볼 수 있어야 합니다.

빈 본문을 반환하고 클라이언트가 Location URL에서 직접 GET 요청을 보내게 할 수도 있습니다. 하지만 그러면 한 번으로 끝날 일이 두 번의 왕복이 됩니다. 리소스를 바로 반환하세요.

201 vs. 200: 생성 vs. 처리

이 구분은 중요합니다:

201 Created: 이전에 없던 새 리소스가 생겨났습니다.

200 OK: 요청이 성공했습니다—무언가 일이 일어났고, 데이터를 돌려받을 수도 있지만, 새로운 주소를 가진 무언가가 새로 태어난 것은 아닙니다.

기존 사용자를 업데이트하는 경우? 200입니다:

PUT /api/users/124 HTTP/1.1
{"name": "Alice Updated"}

HTTP/1.1 200 OK

새 사용자를 생성하는 경우? 201입니다:

POST /api/users HTTP/1.1
{"name": "Alice"}

HTTP/1.1 201 Created
Location: /api/users/124

PUT가 드러내는 묘한 사실

PUT는 리소스의 존재 여부에 따라 생성하거나 업데이트할 수 있습니다:

PUT /api/users/999 HTTP/1.1
{"name": "Charlie"}

사용자 999가 없으면, 서버는 새로 만들고 201을 반환합니다. 이미 있으면, 서버는 업데이트하고 200을 반환합니다.

여기서 묘한 일이 생깁니다: 이 요청을 보낼 때, 실제로 어떤 일이 일어날지 알 수 없습니다. 여러분의 패킷은 탄생인지 수정인지도 모른 채 서버로 향합니다. 응답의 상태 코드를 받는 그 순간이야말로—여러분을 포함해 누구도—실제로 무슨 일이 일어났는지 처음으로 알게 되는 순간입니다.

첫 번째 요청: 201 (새로운 것이 생겼습니다). 같은 요청을 다시: 200 (기존 것이 업데이트되었습니다). 바이트는 똑같았습니다. 의미는 달랐습니다.

POST는 매번 새로 만듭니다

POST는 멱등하지 않습니다. 요청할 때마다 새 리소스가 생성됩니다:

POST /api/users → 201 Created, Location: /api/users/124
POST /api/users → 201 Created, Location: /api/users/125
POST /api/users → 201 Created, Location: /api/users/126

세 번의 요청, 세 명의 새 사용자, 세 개의 다른 Location 헤더. 각각의 201은 고유한 생성을 알립니다.

클라이언트 처리

const response = await fetch('/api/users', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ name: 'Alice', email: 'alice@example.com' })
});

if (response.status === 201) {
    const location = response.headers.get('Location');
    const user = await response.json();
    // user.id, user.createdAt 등을 바로 사용할 수 있습니다
}

상태 코드를 먼저 확인하세요. 201이면 생성이 성공한 겁니다. 그런 다음 Location 헤더(어디서 찾을 수 있는지)와 응답 본문(리소스 자체) 모두를 꺼내 쓰세요.

흔한 실수들

업데이트에 201 반환하기:

PUT /api/users/124  // 사용자가 이미 존재함
HTTP/1.1 201 Created  // 잘못됨—200 OK여야 합니다

생성 실패 시 201 반환하기:

POST /api/users
HTTP/1.1 201 Created
{"error": "Email already registered"}  // 잘못됨—409 Conflict여야 합니다

Location 헤더 생략하기:

HTTP/1.1 201 Created
{"id": 124, "name": "Alice"}  // 이 리소스는 어디 있나요?

항상 Location을 포함하세요. 이것이 201의 핵심입니다—단순히 "무언가를 만들었습니다"가 아니라 "무언가를 만들었고, 그것이 어디 있는지도 알려드립니다."

201 Created에 대해 자주 묻는 질문

항상 본문에 생성된 리소스를 반환해야 하나요?

그렇습니다. 기술적으로는 빈 본문을 반환하고 클라이언트가 Location URL에서 가져오도록 할 수 있지만, 왕복을 한 번 더 낭비하게 됩니다. ID나 타임스탬프처럼 서버가 생성한 모든 필드를 포함하여 리소스 전체를 반환하세요.

POST 요청에서 201과 200의 차이는 무엇인가요?

POST가 자체 URL을 가진 새 리소스를 만들었을 때 201을 쓰세요. POST가 다른 작업을 했을 때—데이터 처리, 특정 동작 트리거, 계산 결과 반환 등—200을 쓰세요. URL로 조회할 수 있는 새로운 것이 생겼다면, 그게 201입니다.

여러 리소스를 한 번에 생성할 때도 201을 쓸 수 있나요?

애매합니다. 201은 단일 Location 헤더를 전제로 하지만, 일괄 생성은 여러 리소스를 만들어냅니다. 200이든 201이든 어느 쪽이든 충분히 합리적인 선택입니다. 201을 사용한다면 단일 Location을 제공할 수 없으니, 각 리소스의 개별 URL과 함께 생성된 리소스들을 본문에 담으세요.

참고 자료

이 페이지가 도움이 되었나요?

😔
🤨
😃