[개발 공부] / HTTP 204 응답: 성공인데 본문이 없는 상태.md

HTTP 204 응답: 성공인데 본문이 없는 상태

조회

<!doctype html>

2026년 6월 19일 | 개발 공부


HTTP 204 No Content는 요청이 성공했지만 응답 본문을 보내지 않는다는 뜻이다. 상태 코드로는 분명 성공 계열인데, 클라이언트 코드에서는 의외로 자주 에러처럼 보인다. 특히 fetch나 테스트 헬퍼에서 습관처럼 JSON 파서를 먼저 호출하면, 서버가 실패한 게 아닌데도 Unexpected end of JSON input 같은 메시지를 만난다.

나는 이 상태 코드를 볼 때 성공 여부데이터가 있는지를 일부러 분리해서 생각하려고 한다. 둘을 한 변수에 섞어 두면 삭제 요청은 성공했는데 화면에는 실패 토스트가 뜨고, 패치 요청은 처리됐는데 로컬 캐시는 엉뚱하게 비워지는 식의 작은 버그가 생긴다. 204는 어려운 개념은 아니지만, API 클라이언트의 반환 계약을 대충 잡았을 때 바로 티가 나는 지점이다.

HTTP 204 응답에서 서버 상태 코드, 헤더, 클라이언트 분기를 분리해 보는 순서
204 응답은 성공 신호와 빈 본문을 따로 읽어야 클라이언트 분기가 덜 흔들린다.

204는 실패가 아니라 빈 성공이다

204는 HTTP의 성공 응답 범위인 2xx에 들어간다. 의미는 간단하다. 서버가 요청을 처리했고, 지금 응답으로 돌려줄 표현 본문은 없다는 것이다. 그래서 삭제 API의 성공 응답, 토글 저장, 서버 설정 변경, 긴 작업 시작 요청처럼 “처리됐다는 사실”만 알려 주면 되는 곳에서 자주 등장한다.

헷갈림은 보통 200과 비교할 때 나온다. 200 OK는 성공과 본문을 같이 기대하게 만든다. 반면 204 No Content는 성공은 맞지만 본문은 기대하지 말라는 계약에 가깝다. 그러니 204를 만났을 때 첫 질문은 “왜 실패했지?”보다 이 엔드포인트가 성공 후 무엇을 돌려주기로 했는가에 가까워야 한다.

반대로 서버가 검증 실패나 권한 문제를 알리고 싶다면 204 대신 4xx 계열을 써야 한다. “실패 이유를 감추려고 204를 보낸다”는 식으로 쓰면 클라이언트는 성공으로 처리할 수밖에 없다. 상태 코드는 감정 표현이 아니라 계약이라서, 본문이 없다는 사실과 실패했다는 사실을 섞어 쓰면 뒤쪽 디버깅이 더 비싸진다.

JSON 파서가 먼저 터지는 이유

프론트엔드나 자동화 코드에서 가장 흔한 실수는 상태 코드를 보기 전에 본문 파서를 먼저 부르는 것이다. 예를 들어 요청을 보낸 뒤 항상 response.json()을 호출하는 공통 헬퍼가 있으면, 204 응답에서는 파싱할 문자열 자체가 없다. 서버 로그에는 정상 처리로 남았는데 클라이언트에서는 JSON 파싱 실패가 떠서, 처음 보면 서버가 이상한 JSON을 보낸 것처럼 보인다.

const res = await fetch('/items/42', { method: 'DELETE' });
if (res.status === 204) return { ok: true, data: null };
const data = await res.json();

이 분기는 단순해 보이지만 꽤 중요하다. 성공이면 무조건 JSON을 받는다고 가정하지 않고, 상태 코드가 본문 계약을 먼저 정하게 만든다. 나는 공통 API 래퍼를 만들 때 204, 205, 304처럼 본문이 비어야 자연스러운 응답을 먼저 따로 빼 둔다. 그다음에야 Content-Type이 JSON인지 보고 파서를 태운다.

테스트에서도 같은 문제가 난다. 단위 테스트가 “응답을 JSON으로 파싱할 수 있다”만 확인하면 204 엔드포인트는 늘 실패한다. 이럴 때 테스트 이름을 “삭제 성공은 204와 빈 본문을 반환한다”처럼 바꾸면 기대가 훨씬 선명해진다. 테스트가 데이터 모양 확인에서 멈추지 않고 본문이 없어야 하는 성공까지 확인하게 되는 셈이다.

헤더와 메서드가 같이 보여야 한다

204를 디버깅할 때는 본문만 보려고 하면 얻는 정보가 거의 없다. 대신 헤더를 본다. Content-Length: 0이 있거나 아예 본문 관련 헤더가 빠져 있는지, Content-Type: application/json을 괜히 붙여 두지는 않았는지, 캐시나 재검증과 관련된 헤더가 있는지 확인한다. 본문이 없는데 JSON 타입만 붙어 있으면 클라이언트 헬퍼가 잘못된 기대를 하기 쉽다.

curl -i -X DELETE http://localhost:8000/items/42

메서드도 같이 봐야 한다. DELETE 뒤 204는 자연스러운 편이다. PATCHPUT 뒤에도 “수정은 됐고 최신 표현은 다시 보내지 않는다”는 정책이면 말이 된다. 하지만 조회성 GET에서 204가 자주 나온다면, 빈 목록을 200과 빈 배열로 보낼지, 정말 표현할 리소스가 없는 상태인지 팀 안에서 한 번은 정리하는 편이 낫다.

여기서 중요한 건 204를 피하는 일이 아니다. 204를 쓸 자리와 200에 빈 배열이나 빈 객체를 줄 자리를 나누는 것이다. 목록 조회에서 결과가 0개인 상황은 대개 “본문 없는 성공”보다 “빈 컬렉션이라는 데이터”에 가깝다. 반면 삭제나 저장 완료처럼 결과 데이터보다 처리 완료 신호가 중요한 곳은 204가 깔끔할 수 있다.

클라이언트 반환 타입을 따로 잡는다

204를 안정적으로 다루려면 클라이언트 함수의 반환 타입부터 나눠 두는 게 좋다. 모든 API 함수를 Promise<T>처럼 잡으면 204도 억지로 어떤 T를 만들어야 한다. 그러다 {}, null, undefined가 호출부마다 섞인다. 차라리 삭제 함수는 Promise<void>나 성공 플래그만 반환하게 두면 기대가 작아진다.

화면 상태도 마찬가지다. 204가 오면 새 데이터를 받지 않았다는 뜻이지, 현재 화면 데이터를 마음대로 지워도 된다는 뜻은 아니다. 삭제 요청이면 로컬 목록에서 해당 항목을 제거할 수 있고, 설정 저장이면 기존 폼 값을 유지한 채 저장됨 표시만 바꾸면 된다. 서버 응답 본문이 비었다는 사실화면에서 무엇을 갱신할지는 별도 판단이다.

나는 API 래퍼를 볼 때 반환값 이름도 신경 쓴다. data 하나로 모든 성공을 표현하면 204에서는 의미가 흐려진다. ok, status, data를 분리하거나, 아예 204 전용 함수는 데이터를 반환하지 않게 만드는 쪽이 낫다. 그러면 호출부에서 “데이터가 없으니 실패” 같은 잘못된 조건문이 줄어든다.

내가 남기는 확인 순서

204 관련 버그를 만나면 나는 네 가지만 본다. 첫째, 실제 상태 코드가 204인지 확인한다. 둘째, 응답 헤더에 본문을 기대하게 만드는 값이 붙어 있는지 본다. 셋째, 클라이언트 공통 헬퍼가 상태 코드 분기 전에 JSON 파서를 호출하는지 확인한다. 넷째, 호출부가 빈 본문을 실패처럼 다루고 있지는 않은지 본다.

이 순서가 잡히면 문제 범위가 빨리 줄어든다. 서버가 204를 잘못 보낸 것인지, 헤더가 모호한 것인지, 클라이언트 래퍼가 너무 단순한 것인지, 화면 갱신 로직이 과하게 반응한 것인지가 갈린다. 204는 조용한 응답이라 로그에 남는 정보가 적다. 그래서 더더욱 상태 코드, 헤더, 파서, 호출부를 나눠서 봐야 한다.

작은 상태 코드 하나지만, 이 구분을 해 두면 API 클라이언트가 꽤 단단해진다. 성공은 성공대로 처리하고, 빈 본문은 빈 본문대로 받아들이면 된다. 서버가 아무것도 안 보냈다는 사실을 에러로 착각하지 않는 것만으로도, 삭제 버튼이나 저장 버튼 주변의 애매한 실패 메시지가 많이 줄어든다.

댓글

홈으로 돌아가기

검색 결과

"" 검색 결과입니다.