빌드노트
undefined로 비운 객체 필드가 JSON 요청에서 사라진다
undefined를 넣은 객체 속성은 JSON에서 키째 빠진다. 전송 문자열과 hasOwn으로 누락 지점을 확인하고, 명시적 null로 초기화를 요청하려면 서버와 어떤 계약이 필요한지 설명한다.
수정 폼에서 닉네임을 지우려고 payload.nickname = undefined로 바꿔도, 서버에는 지우기 의도가 전달되지 않을 수 있다. JSON.stringify가 객체 속성의 undefined를 만나면 키와 값을 함께 생략하기 때문이다. 받은 JSON이 {}라면 서버는 사용자가 닉네임을 지웠는지, 처음부터 수정하지 않았는지 구별할 수 없다.
먼저 요청 본문을 만드는 위치에서 직렬화된 문자열을 확인한다. 초기화를 전달할 표현으로는 명시적 null을 쓸 수 있지만, 서버가 그것을 초기화로 처리한다는 API 계약이 있어야 한다.
객체에는 있는데 전송 문자열에는 없다
다음 코드는 원본 객체의 필드가 JSON을 거치며 사라지는지 확인한다. Node.js가 설치된 환경에서 새 파일 json-check.cjs에 저장하고, 그 폴더에서 node json-check.cjs로 실행한다. 추가 패키지는 필요 없다.
const payload = { nickname: 'Bae' };
payload.nickname = undefined;
const body = JSON.stringify(payload);
console.log(Object.hasOwn(payload, 'nickname'), body);
console.log(Object.hasOwn(JSON.parse(body), 'nickname'));
Windows의 Node.js 22.23.2에서 실행한 출력은 첫 줄이 true {}, 둘째 줄이 false였다. 원본에는 nickname 속성이 남아 있지만, 만들어진 문자열은 {}다. 그 문자열을 다시 파싱한 객체에는 속성이 없다. 처음부터 빈 객체를 직렬화해도 같은 {}가 나오므로, JSON을 받은 쪽에서는 두 입력을 구별할 수 없다.
값만 읽으면 원본과 파싱한 객체 모두 undefined여서 차이를 놓치기 쉽다. Object.hasOwn (새 탭에서 열림)은 값과 별개로 객체에 직접 정의된 속성이 있는지 확인한다. 위 입력에서 원본의 속성을 삭제한 것은 직렬화가 아니다. JSON 문자열을 만들 때 그 속성을 생략했다.
별도 합성 실험에서는 같은 문자열을 127.0.0.1에만 연 Node HTTP 수신기로 보냈다. 파싱 전 수집한 요청 본문은 정확히 2바이트인 {}였다. 서버에 도착하기 전, 문자열을 만드는 단계에서 이미 필드가 빠진 것이다. 수신기는 본문을 읽어 돌려주기만 했고 저장 기능은 없었다.
자기 요청도 전송 직전 문자열과 개발자 도구 Network의 요청 Payload 원문을 비교한다. 문자열부터 키가 없다면 요청 객체를 만드는 코드를 확인한다. 문자열에는 키가 있는데 실제 요청에는 없다면 다른 객체를 보내는지, 요청 라이브러리가 본문을 다시 만드는지 확인한다.
null을 보내는 것과 서버에서 초기화하는 것은 별개다
앞 파일 끝에 다음 두 줄을 붙이면, 지우기 의도를 표현할 수 있는 JSON이 남는지 비교할 수 있다. 같은 node json-check.cjs 명령으로 다시 실행한다.
payload.nickname = null;
console.log(JSON.stringify(payload));
앞의 두 출력 뒤에 {"nickname":null}이 추가됐다. 별도 전송에서도 이 문자열 그대로 17바이트가 도착했고, 파싱한 객체는 nickname 속성과 null 값을 모두 보존했다. 누락된 필드와 명시적 null은 JSON에서 구별된다. MDN의 JSON.stringify 설명 (새 탭에서 열림)에서 이 직렬화 규칙을 확인할 수 있다.
다만 null 자체에 삭제 명령이라는 뜻은 없다. 예를 들어 수정 API가 다음 계약을 지원한다면, 사용자가 선택한 동작에 맞춰 요청을 만들 수 있다.
{}: 닉네임을 수정하지 않고 기존 값을 유지한다.{"nickname":null}: 닉네임을 초기화한다.{"nickname":"New"}: 닉네임을 새 문자열로 바꾼다.
이 계약을 적용한 별도의 메모리 모델에서는 기존 값 'Bae'가 각각 'Bae', null, 'New'가 됐다. 이는 예제로 정한 규칙이며 실제 API의 동작을 검증한 결과는 아니다. 사용하는 API가 null을 거절하거나 다른 의미로 처리한다면, 초기화용 필드나 별도 동작 등 지원할 표현부터 정해야 한다. 빈 문자열을 허용할지도 별도 규칙이다.
화면 상태에서도 수정하지 않음과 사용자가 지우기를 선택함을 구별한다. 지우기일 때만 계약에 맞는 null을 넣고, 새 값으로 바꾸는 동작에 예상하지 않은 undefined가 들어오면 검증 오류로 처리한다. 이를 조용히 누락시키면 잘못된 입력이 수정하지 않음으로 바뀐다.
서버 쪽도 필드의 존재 여부를 보존해야 한다. JavaScript 객체로 받는다면 Object.hasOwn(received, 'nickname')으로 적용 여부를 판단한 뒤 값을 검증할 수 있다. 다른 프레임워크에서는 JSON을 DTO에 바인딩하는 과정에서 누락과 명시적 null을 같은 값으로 합치지 않는지 확인한다. 클라이언트가 구별해 보내도 이 단계에서 합쳐지면 의도를 잃는다.
모든 undefined를 null로 바꾸면 다른 필드도 영향을 받는다
공통 replacer로 undefined를 전부 null로 바꾸면 지우려던 필드 외의 값도 바뀐다. MDN의 replacer 설명 (새 탭에서 열림)처럼 함수는 중첩된 객체의 속성에도 적용된다. 합성 입력의 contact.phone은 기본 직렬화에서 빠졌지만, 전역 변환을 붙이자 null로 남았다. 서버가 null을 초기화로 해석한다면 미입력 전화번호까지 지울 수 있다. 필드별 수정 의도에 맞춰 요청을 만드는 편이 범위를 판단하기 쉽다.
배열과 최상위 값의 규칙도 객체에 그대로 대입하지 않는다. 별도 비교에서 [undefined]는 [null]이 됐고, 파싱한 배열은 길이 1과 0번 요소를 유지했다. 처음부터 [null]을 넣어도 같은 JSON이므로 원래 값은 복원할 수 없다. 반면 JSON.stringify(undefined)는 문자열이 아닌 JavaScript 값 undefined를 반환했고, 이를 JSON.parse에 넘기면 SyntaxError가 났다. 이 위치별 차이는 ECMAScript의 JSON.stringify 정의 (새 탭에서 열림)에도 명시돼 있다.
직접 실행한 범위는 Node.js 22.23.2의 일반 객체·배열·원시값, 합성 메모리 모델과 loopback 전송이다. 기본 비교에는 사용자 정의 toJSON이나 replacer가 없었고, 전역 변환은 별도 조건으로 비교했다. 브라우저 UI, 요청 라이브러리의 변환, 실제 서버의 DTO 바인딩과 DB 저장은 실행하지 않았다. 전송 문자열에서 필드가 보존되는지 확인한 뒤, 누락과 null을 구별하는 API 계약까지 점검해야 한다.