TL;DR
- 날짜 칸을 비워 둔 건만 저장하면 서버가 500을 뱉는다. 메시지에 단서가 없다.
- 빈 날짜의 기본값이
19000101이었다. 1908년 이전 한국은 표준시가 아니라 지방 평균시 +08:27:52였다. - UTC로 바꾸면 32분 8초가 붙는다. 빈 날짜엔 기본값을 주지 않거나
moment.utc로 만든다.
환경
| 항목 | 값 |
|---|---|
| 프론트 | SAPUI5 1.108.7, moment.js |
| 백엔드 | OData v2 Gateway |
| 필드 형식 | Edm.DateTime, Edm.Time |
| 브라우저 시간대 | Asia/Seoul |
🚨 문제 상황
신청 화면에서 저장을 누르면 대부분 잘 된다. 그런데 어떤 건만 500이 떨어진다.
HTTP/1.1 500 Internal Server Error
{"error":{"code":"SY/530",
"message":{"value":"An exception was raised."}}}
메시지에 단서가 하나도 없다. 어느 필드가 문제인지, 값이 뭐가 잘못됐는지 아무것도 알려 주지 않는다.
성공한 건과 실패한 건을 나란히 놓고 비교했다. 차이는 하나였다.
실패한 건은 날짜 칸 하나를 비워 둔 건이었다.

원인 추적
처음 의심한 것
500이니 서버부터 봤다. 네 가지를 차례로 지웠다.
| 의심 | 확인 | 결과 |
|---|---|---|
| 권한이나 잠금 | 같은 사용자로 다른 건 저장 | 성공 |
| 필수값 누락 | 서버 검증 로직 확인 | 그 필드는 선택값 |
| CSRF 토큰 만료 | 그럼 403이어야 함 | 아님 |
| 요청 본문 형식 | 성공 건과 한 글자씩 비교 | 날짜 필드 하나만 다름 |
마지막 비교에서 나왔다. 빈 날짜 칸에 코드가 넣어 준 기본값이다.
// 실패한 요청 본문
{
"StartDate": "/Date(-2208996472000)/",
...
}
타임스탬프가 음수인 건 1970년보다 앞이니 당연하다. 이상한 건 끝자리다.
자정 정각이면 000으로 끝나야 하는데 472000으로 끝난다. 초 단위가 붙어 있다.
진짜 원인
빈 날짜의 기본값으로 19000101을 쓰고 있었다. 그리고 이 코드를 거쳤다.
// 자정으로 맞춘다고 생각한 코드
const dDate = moment(sValue, 'YYYYMMDD').hours(0).minutes(0).seconds(0);
mPayload.StartDate = dDate.toDate(); // ← 여기서 UTC 로 바뀐다
브라우저 시간대는 Asia/Seoul이다. 그런데 IANA 시간대 데이터는 연도마다 다른 오프셋을 들고 있다.
한국이 표준시 UTC+09:00을 쓰기 시작한 건 1908년이다. 그 전 서울 시각은 지방 평균시 +08:27:52였다.
그래서 1900-01-01 00:00 +08:27:52를 UTC로 바꾸면 1899-12-31 15:32:08Z가 된다. 자정이 아니라 32분 8초가 붙은 시각이다.

직접 찍어 보면 바로 보인다.
moment('20260101', 'YYYYMMDD').hours(10).toISOString();
// '2026-01-01T01:00:00.000Z' ← 09:00 차이. 깔끔하다
moment('19000101', 'YYYYMMDD').hours(10).toISOString();
// '1900-01-01T01:32:08.000Z' ← 08:27:52 차이. 32분 8초가 붙었다
OData의 날짜 형식은 이 초 단위 잔여를 그대로 백엔드 변환에 넘긴다. 거기서 예외가 나고, Gateway는 그걸 뭉뚱그려 500으로 내려 보낸다.
메시지에 단서가 없었던 이유가 이것이다.
왜 하필 32분 8초인가
지방 평균시는 그 지역 경도로 계산한 태양시다.
서울이 동경 약 126.97도이고 경도 15도가 1시간이니 126.97 ÷ 15 ≈ 8.465시간. 8시간 27분 52초다. 표준시 9시간과의 차이가 32분 8초다.
moment 버그가 아니다. IANA 시간대 데이터가 역사적 사실을 그대로 들고 있어서 그렇다.
new Date('1900-01-01T00:00:00+08:27:52').toISOString();
// '1899-12-31T15:32:08.000Z'
같은 함정이 있는 나라가 또 있다.
- 일본
Asia/Tokyo— 1888년 이전 +09:18:59 - 중국
Asia/Shanghai— 1901년 이전 +08:05:43
옛 날짜를 기본값으로 쓰는 코드는 어느 나라에서든 똑같이 걸린다.
해결
1. 빈 날짜엔 기본값을 안 준다 (권장)
애초에 빈 값에 19000101을 끼워 넣을 이유가 없었다.
const toDate = (s) => (s ? moment(s, 'YYYYMMDD').toDate() : null);
// 비었으면 null
mPayload.StartDate = toDate(oModel.getProperty('/startDate'));
2. 시간대를 안 타게 UTC로 만든다
날짜만 의미 있는 필드라면 로컬 시간대를 거칠 이유가 없다.
const toUtcDate = (s) =>
(s ? moment.utc(s, 'YYYYMMDD').toDate() : null);
toUtcDate('19000101').toISOString();
// '1900-01-01T00:00:00.000Z' ← 자정 정각
3. 기본값을 꼭 써야 하면 1908년 이후로
// '1969-12-31T15:00:00.000Z'
moment('19700101', 'YYYYMMDD').toISOString();
// '9999-12-30T15:00:00.000Z'
moment('99991231', 'YYYYMMDD').toISOString();
둘 다 정확히 9시간 차이라 안전하다.
확인
- UTC 끝자리 —
toISOString()결과가:00:00.000Z로 끝나는지 - 요청 본문 —
/Date(…)값이000으로 끝나는지 - 빈 값 저장 — 날짜 칸 비운 채로 저장해서 200이 오는지
- 되읽기 — 저장한 건을 다시 열었을 때 칸이 비어 있는지
💡 배운 것
500을 받으면 서버부터 의심하게 된다. 메시지에 단서까지 없으면 더 그렇다.
그런데 결정적이었던 건 서버 로그가 아니라 성공 요청과 실패 요청의 본문을 한 글자씩 비교한 것이었다. 프론트에서 할 수 있는 걸 다 해 보기 전에 서버로 넘기면 왕복만 길어진다.
hours(0).minutes(0).seconds(0)은 로컬 시간대의 자정이다. 그 로컬 시간대가 연도마다 다를 수 있다는 건 생각해 본 적이 없었다.
이제 날짜를 payload에 넣는 길목은 함수 하나로 모았다. 전에는 컨트롤러마다 각자 변환했다. 같은 실수를 다시 하려면 그 함수를 고쳐야 한다.
참고
- IANA Time Zone Database —
Asia/Seoul의 1908년 전후 오프셋 - moment.js 문서 —
moment.utc()와moment()의 차이
'Front > SAPUI5' 카테고리의 다른 글
| renderType="Bare" 로 바꾸자 라벨이 가운데로 몰렸다 (0) | 2026.10.30 |
|---|---|
| belize 테마에서 Select 와 ComboBox 드롭다운 폭이 다른 이유 (0) | 2026.10.30 |
| SAPUI5 ComboBox 에서 selectedKey="" 가 먹지 않는 이유 (0) | 2026.10.29 |
| SAPUI5 JSONModel (0) | 2026.06.12 |
| SAPUI5 Event Handling 가이드 (0) | 2026.06.12 |