가상의 습관 기록 앱을 생각해보겠습니다. 한국에서 9월 20일 00시 30분에 운동을 기록했는데, 목록에는 9월 19일이 나옵니다. 날짜에 하루를 더하기 전에 입력값, 서버에 보낸 값, 화면의 표시 규칙을 나란히 놓아야 합니다.
핵심은 달력에서 선택한 날짜와 어떤 일이 발생한 순간을 구분하는 것입니다. 둘을 같은 자료형과 함수로 처리하면 낮에는 멀쩡하던 코드가 자정 부근이나 다른 시간대에서 문제를 드러낼 수 있습니다. 작은 예제로 값이 이동하는 과정을 따라가보겠습니다.
먼저 “언제”가 무엇을 뜻하는지 정하기
“9월 20일에 할 일”은 달력의 날짜입니다. 사용자는 시·분을 고르지 않았고, 해외에서 앱을 열었다고 마감일이 19일로 바뀌기를 기대하지 않을 수도 있습니다. 반면 “9월 20일 00시 30분에 기록을 생성했다”는 특정 순간입니다. 같은 순간을 서울과 다른 지역의 시계로 표현하면 날짜까지 달라질 수 있습니다.
필드 이름부터 나누면 판단이 쉬워집니다. 할 일의 날짜는 dueDate, 실제 생성 시각은 createdAt처럼 구분합니다. 날짜만 필요한 값에는 날짜의 유효성을 검사하고, 발생 시각에는 어느 기준의 시각인지 분명히 남깁니다. 하나의 습관 기록에도 사용자가 선택한 활동 날짜와 저장 버튼을 누른 순간을 따로 둘 수 있습니다.
밤 12시가 지나 어제 운동한 내용을 입력했다면, 달력에는 어제의 활동으로 보여줄지 오늘 작성한 기록으로 보여줄지 먼저 정해야 합니다. 이 규칙이 없으면 정확한 시각을 저장해도 사용자가 원하는 날짜와 어긋납니다.
입력에서 저장, 화면까지 같은 순간 따라가기
JavaScript의 Date는 특정 순간을 나타냅니다. 원래 입력한 지역 시간대를 객체 안에 따로 보관하지는 않습니다. 예제의 2026-09-20T00:30:00+09:00에서 +09:00은 UTC보다 9시간 빠른 시각이라는 뜻입니다. 이를 UTC로 표현하면 전날인 9월 19일 15시 30분이 됩니다.
아래 그림은 발생 시각을 UTC 문자열로 저장·전달하기로 정한 앱의 예입니다. 입력과 저장값의 날짜 부분이 다르더라도 같은 순간을 가리킨다면 그 차이만으로 오류라고 판단할 수 없습니다. 서울 기준으로 보여줄 화면에서는 다시 서울 시간대를 적용해야 합니다.

API에서는 오프셋이 명확한 문자열로 시각을 주고받고, 저장소에서도 같은 순간이 유지되는지 확인합니다. UTC 문자열로 통일하는 방식은 한 가지 선택입니다. 저장소의 시각 자료형이나 설정은 제품마다 다르므로, 프런트엔드의 예제를 그대로 데이터베이스 설계 규칙으로 옮기지는 않습니다. 사용자의 활동 지역 자체도 필요하다면 지역 시간대는 별도 정보로 다룹니다.
문자열 앞부분을 잘랐을 때 하루가 밀리는 이유
toISOString()은 항상 UTC 기준의 문자열을 반환하며, 끝의 Z가 그 기준을 나타냅니다. 다음 코드는 같은 순간을 UTC 문자열과 서울 기준 날짜로 각각 출력합니다.
const recordedAt = new Date("2026-09-20T00:30:00+09:00");
console.log(recordedAt.toISOString());
// 2026-09-19T15:30:00.000Z
const displayDate = new Intl.DateTimeFormat("ko-KR", {
timeZone: "Asia/Seoul",
year: "numeric", month: "2-digit", day: "2-digit",
});
console.log(displayDate.format(recordedAt));
// 2026. 09. 20.
여기서 toISOString().slice(0, 10)을 쓰면 2026-09-19가 남습니다. 앞의 열 글자는 UTC에서 본 달력 날짜입니다. 서울의 기록 날짜를 만들려고 잘랐다면 잘못된 기준을 사용한 셈입니다. 문자열 자르기가 날짜 변환을 대신해주지는 않습니다.
화면에 표시할 때는 서비스가 정한 시간대를 Intl.DateTimeFormat에 전달합니다. ko-KR은 한국어권의 표시 형식에, timeZone은 시각을 어느 시간대로 표현할지에 관여합니다. 한국어 로캘을 선택하는 것과 서울 시간대를 선택하는 것은 별도입니다. 화면마다 이 규칙이 흩어지지 않도록 공통 표시 함수에서 관리하면 변경 지점을 줄일 수 있습니다.
날짜만 받은 값에 자정을 붙이면 생기는 문제
new Date("2026-09-20")처럼 날짜만 있는 표준 문자열은 UTC 자정으로 해석됩니다. 이 순간을 로스앤젤레스 시간대로 표시하면 9월 19일이 됩니다. 사용자는 달력에서 20일을 골랐지만 코드가 이를 UTC의 특정 순간으로 바꾼 뒤 다른 시간대로 옮긴 것입니다.
날짜만 필요한 필드는 유효한 YYYY-MM-DD 값으로 저장·전달하는 방법을 고려해보세요. API와 저장소까지 그 값이 달력 날짜라는 의미를 유지해야 합니다. 문자열 길이와 하이픈 위치가 맞는지만 검사하지 말고, 2월 30일처럼 실제로 없는 날짜도 거르도록 합니다. 화면의 연·월·일 표기 때문에 반드시 Date 객체로 바꿔야 하는 것은 아닙니다.
반대로 실제 마감이 “서울 시각 9월 20일 오후 6시”라면 날짜만으로 충분하지 않습니다. 시각과 적용할 시간대를 함께 정해야 합니다. “날짜는 문자열, 시각은 UTC”라는 문구를 무조건 적용하기보다, 그 필드가 무엇을 약속하는 값인지부터 확인하는 편이 좋습니다.
수정할 때는 한 건을 네 지점에서 비교하기
먼저 재현할 기록 하나를 고릅니다. 입력 직후에는 사용자가 고른 원본 값과 필드의 의미를 확인합니다. 다음으로 API 요청에서 오프셋이나 Z가 빠지지 않았는지 보고, 저장 후 다시 받은 응답이 같은 순간 또는 같은 날짜를 유지하는지 비교합니다. 마지막으로 화면에 전달한 로캘과 시간대 옵션을 확인합니다.
예를 들어 요청과 응답 모두 2026-09-19T15:30:00.000Z인데 화면만 19일이라면, 표시 규칙부터 살펴볼 수 있습니다. 반대로 요청에는 +09:00이 있었는데 응답에서 시간대 표기가 사라졌다면 서버나 저장 과정의 해석을 확인해야 합니다. 원본과 변환 후 값을 구분해 적어두면 수정할 위치가 좁혀집니다.
검증용 입력에는 자정 직전과 직후, 월말과 다음 달 첫날을 함께 넣어보세요. 호스트 시간대를 서울·UTC·로스앤젤레스로 바꿔도, 서울 기준 화면은 같은 날짜를 내야 한다는 기대값을 정합니다. 날짜 전용 필드는 어느 환경에서 읽든 사용자가 고른 날짜가 유지되는지도 별도로 확인합니다.
날짜가 어긋났다고 무조건 9시간을 더하면 이미 변환된 값에 보정을 중복 적용할 수 있습니다. AI에게 수정을 맡길 때도 원본 입력, 현재 출력, 원하는 출력, 기준 시간대와 필드의 의미를 함께 전달하세요. 이 예제라면 “서울에서 기록한 20일 활동을 20일로 보여주되, 생성 순간은 바꾸지 않는다”까지 적어두면 수정 목표가 분명해집니다.
본문 코드는 Node.js v24.19.0·ICU 78.3에서 실행해 결과를 확인했습니다. 확인 범위는 문자열 변환과 날짜 표시이며 실제 앱의 API·저장소 연동은 포함하지 않습니다.
'AI BOX' 카테고리의 다른 글
| 바이브 코딩이 막히는 건 모델이 아니라 지도다 (0) | 2026.08.18 |
|---|---|
| 🤖 생성형 AI 4종 비교! (3) | 2025.07.29 |