Use Cases (업데이트: 2026. 6. 2.)

Claude Code 날짜와 시간 처리 실전 가이드

Claude Code로 타임존, DST, Intl, DB 저장 정책을 안전하게 구현하고 테스트하는 실무 가이드.

Claude Code 날짜와 시간 처리 실전 가이드

날짜와 시간은 Claude Code에 한 줄로 맡기면 위험하다

날짜와 시간 버그는 코드 리뷰에서 놓치기 쉽지만 운영 영향은 큽니다. 한국이나 일본 시간으로는 정상처럼 보여도 미국 DST, 유럽 월말 정산, 인도의 30분 오프셋, 서버와 브라우저의 시간대 차이에서 바로 깨질 수 있습니다. Claude Code에 “날짜 처리 구현해줘”라고만 말하면 보기 좋은 포맷 코드는 나오지만 저장 정책, API 계약, 경계 테스트가 비어 있을 수 있습니다.

Claude Code는 단순 코드 생성기가 아니라 날짜/시간 명세를 함께 점검하는 페어 엔지니어로 써야 합니다. 이 글은 예약, 결제, 알림, 고객지원 SLA, 관리자 화면처럼 시간대 경계를 넘는 웹 애플리케이션을 기준으로 합니다. 테스트 설계는 Claude Code 테스트 전략, 스키마 변경은 데이터베이스 마이그레이션과 함께 보면 좋습니다.

구현 전에는 공식 문서를 확인합니다. 표시 포맷은 MDN Intl.DateTimeFormat, Temporal의 현재 상태와 개념은 TC39 Temporal proposalMDN Temporal, PostgreSQL 저장 동작은 PostgreSQL Date/Time Types, Claude Code 자동 검증은 Claude Code hooks를 기준으로 삼습니다.

먼저 용어와 저장 정책을 고정한다

Claude Code에 코드를 쓰게 하기 전에 팀의 단어를 고정해야 합니다. 그렇지 않으면 리뷰에서 “UTC로 저장하니 안전하다” 또는 “국내 서비스니까 KST 고정이면 된다” 같은 거친 판단이 나옵니다.

용어쉬운 의미저장 원칙
instant전 세계에서 하나로 정해지는 시점. 예: 2026-06-02T00:00:00ZUTC 기준 timestamp로 저장
local date사용자의 달력 날짜. 생일, 마감일, 영업일 등YYYY-MM-DD로 저장하고 시간과 섞지 않음
wall clock time사람이 시계에서 보는 시간. 예: 09:00 회의IANA 시간대 ID와 함께 저장
IANA timezoneAsia/Seoul, America/New_York 같은 지역명고정 offset으로 대체하지 않음
DST서머타임. 어떤 날은 23시간 또는 25시간전환일 테스트를 반드시 둠

실무에서는 다음 세 가지를 AGENTS.mdCLAUDE.md에 먼저 적어 둡니다.

  1. 이미 발생한 API timestamp는 Z 또는 명시적 offset이 있는 ISO 8601 문자열로 보낸다.
  2. 미래 예약과 알림은 localDate, localTime, timeZone을 분리해서 저장한다.
  3. 화면 표시에는 Intl.DateTimeFormatlocaletimeZone을 항상 전달하고 런타임 기본값에 맡기지 않는다.

경계는 다음처럼 나눕니다.

flowchart LR
  A["Client: local date/time input"] --> B["API contract: localDate + localTime + timeZone"]
  B --> C["Server: validate and convert when needed"]
  C --> D["Database: instant in timestamptz + original timeZone"]
  D --> E["UI: format with Intl.DateTimeFormat"]
  E --> A

Intl.DateTimeFormat으로 안전한 기반을 만든다

표시만 필요하다면 표준 API인 Intl.DateTimeFormat부터 검토합니다. MDN은 이 API를 언어에 맞는 날짜와 시간 포맷을 위한 내장 API로 설명하며, timeZone도 명시할 수 있습니다. Claude Code에는 toLocaleString()을 인자 없이 쓰지 말 것, 로컬 달력 날짜를 의미하는 YYYY-MM-DDDate로 바로 파싱하지 말 것을 제약으로 줍니다.

아래 모듈은 의존성 없이 쓸 수 있는 기본 정책입니다. src/lib/date-policy.ts에 두고 UI, API, 테스트에서 같은 함수를 사용합니다.

// src/lib/date-policy.ts
export const TIME_POLICY = {
  defaultLocale: 'ko-KR',
  defaultTimeZone: 'Asia/Seoul',
} as const;

type FormatOptions = {
  locale?: string;
  timeZone?: string;
  includeWeekday?: boolean;
};

function toDate(input: string | Date): Date {
  const date = input instanceof Date ? input : new Date(input);

  if (!Number.isFinite(date.getTime())) {
    throw new Error(`Invalid date value: ${String(input)}`);
  }

  return date;
}

export function toUtcIso(input: string | Date): string {
  if (typeof input === 'string' && !/(Z|[+-]\d{2}:?\d{2})$/i.test(input)) {
    throw new Error('Timestamp must include Z or an explicit UTC offset.');
  }

  return toDate(input).toISOString();
}

function dateParts(date: Date, timeZone: string): Record<string, string> {
  const parts = new Intl.DateTimeFormat('en-US', {
    timeZone,
    year: 'numeric',
    month: '2-digit',
    day: '2-digit',
  }).formatToParts(date);

  return Object.fromEntries(
    parts
      .filter((part) => part.type !== 'literal')
      .map((part) => [part.type, part.value]),
  );
}

export function dayKeyInTimeZone(
  input: string | Date,
  timeZone = TIME_POLICY.defaultTimeZone,
): string {
  const parts = dateParts(toDate(input), timeZone);
  return `${parts.year}-${parts.month}-${parts.day}`;
}

export function formatInstant(
  input: string | Date,
  {
    locale = TIME_POLICY.defaultLocale,
    timeZone = TIME_POLICY.defaultTimeZone,
    includeWeekday = true,
  }: FormatOptions = {},
): string {
  return new Intl.DateTimeFormat(locale, {
    timeZone,
    weekday: includeWeekday ? 'short' : undefined,
    year: 'numeric',
    month: 'short',
    day: '2-digit',
    hour: '2-digit',
    minute: '2-digit',
    hourCycle: 'h23',
    timeZoneName: 'short',
  }).format(toDate(input));
}

목표는 날짜 처리를 전부 유틸리티 뒤에 숨기는 것이 아닙니다. UTC 시점, 로컬 날짜 키, 표시 라벨을 분리하는 것입니다. Claude Code가 새 helper를 추가하려 하면 먼저 새 요구사항이 이 세 분류 중 어디에 속하는지 설명하게 하세요.

미래의 로컬 시간은 명시적으로 변환한다

Intl.DateTimeFormat은 출력에 강하지만 2026-11-01 01:30 America/New_York 같은 로컬 시간을 안전한 UTC instant로 바꾸는 도구는 아닙니다. 이 영역은 프로젝트에서 하나의 라이브러리로 통일하고 공식 문서를 확인합니다. LuxonAPI docs는 명시적인 시간대 API를 제공합니다. date-fns는 함수형 날짜 유틸리티에 강하지만 시간대 요구사항은 현재 공식 문서로 확인해야 합니다. Temporal은 TC39에서 Stage 4이지만 대상 브라우저와 Node 지원, polyfill 방침을 확인한 뒤 도입합니다.

예약 생성에서는 사용자가 입력한 로컬 날짜와 시간을 DB 저장용 instant로 변환합니다.

// src/lib/schedule-time.ts
import { DateTime } from 'luxon';

type LocalScheduleInput = {
  localDate: string; // YYYY-MM-DD
  localTime: string; // HH:mm
  timeZone: string; // IANA time zone, for example "America/New_York"
};

export function scheduleToUtcIso(input: LocalScheduleInput): string {
  const rawLocal = `${input.localDate}T${input.localTime}`;
  const local = DateTime.fromISO(rawLocal, { zone: input.timeZone });

  if (!local.isValid) {
    throw new Error(local.invalidExplanation ?? `Invalid local time: ${rawLocal}`);
  }

  const roundTrip = local.setZone(input.timeZone).toFormat("yyyy-MM-dd'T'HH:mm");
  if (roundTrip !== rawLocal) {
    throw new Error(`Nonexistent local time in ${input.timeZone}: ${rawLocal}`);
  }

  const iso = local.toUTC().toISO({ suppressMilliseconds: true });
  if (!iso) {
    throw new Error(`Could not convert local time to UTC: ${rawLocal}`);
  }

  return iso;
}

이 함수가 DST 종료일에 반복되는 01:00 시간을 모두 해결해 주지는 않습니다. 제품 정책으로 더 이른 offset을 택할지, 더 늦은 offset을 택할지, 사용자에게 확인할지 정해야 합니다. Claude Code에는 존재하지 않는 로컬 시간, 중복 시간, 월말, 윤일 테스트를 넣으라고 명시합니다.

서버, 클라이언트, DB 경계를 분리한다

가장 흔한 실패는 어떤 값이 서버 시간대인지 브라우저 시간대인지 결정하지 않고 Date에 넣는 것입니다. 브라우저 입력, API payload, DB 컬럼, 이메일, CSV 내보내기는 모두 다른 경계입니다.

설계를 리뷰할 때는 최소한 다음 세 가지 유스케이스로 점검합니다.

  • 예약 시스템: 입력은 장소의 로컬 시간, 저장은 UTC instant, 표시는 조회자 또는 장소 시간대.
  • 결제 마감: “월말 23:59”는 고객 계약 시간대의 local date 규칙이며 24시간 더하기로 다음 달을 만들면 안 됩니다.
  • 고객지원 SLA: 영업일, 휴일, 업무 시간이 지역마다 다르므로 마감 instant와 사람이 이해할 로컬 맥락을 모두 남깁니다.

PostgreSQL 공식 문서는 timestamp with time zone 입력이 UTC로 변환되고 원래 시간대가 보존되지 않는다고 설명합니다. 따라서 timestamptz만으로 사용자가 America/New_York을 선택했다는 사실을 알 수 없습니다. 제품에 필요하면 별도 컬럼에 저장합니다.

create table scheduled_events (
  id uuid primary key,
  title text not null,
  starts_at timestamptz not null,
  original_time_zone text not null check (original_time_zone <> ''),
  local_date date not null,
  local_time time not null,
  created_at timestamptz not null default now()
);

create index scheduled_events_starts_at_idx
  on scheduled_events (starts_at);

timestamp without time zone 컬럼에 2026-06-02T09:00:00+09:00을 넣으면 offset이 의미 있게 남는다고 가정하면 위험합니다. PostgreSQL은 타입을 먼저 결정하고, without time zone으로 결정된 값에서는 시간대 표시를 무시합니다. Claude Code 리뷰 체크리스트에 “날짜/시간 컬럼 타입과 API 계약이 일치하는가”를 넣어야 합니다.

DST와 경계일 테스트는 고정 instant로 작성한다

테스트는 “오늘”, 현재 시각, 개발자 PC의 시간대에 의존하면 안 됩니다. 입력을 UTC instant로 고정하고 기대되는 로컬 날짜나 라벨을 단언합니다. Vitest를 쓴다면 아래 테스트부터 시작하세요. 더 넓은 패턴은 Claude Code Vitest 고급 가이드를 참고하면 됩니다.

// src/lib/date-policy.test.ts
import { describe, expect, it } from 'vitest';
import { dayKeyInTimeZone, formatInstant, toUtcIso } from './date-policy';

describe('date/time policy', () => {
  it('requires an explicit offset for API timestamps', () => {
    expect(() => toUtcIso('2026-06-02T09:00:00')).toThrow(/offset/);
    expect(toUtcIso('2026-06-02T09:00:00+09:00')).toBe('2026-06-02T00:00:00.000Z');
  });

  it('calculates a local day key across the UTC date boundary', () => {
    expect(dayKeyInTimeZone('2026-03-31T15:01:00Z', 'Asia/Tokyo')).toBe('2026-04-01');
    expect(dayKeyInTimeZone('2026-04-01T00:30:00Z', 'America/Los_Angeles')).toBe('2026-03-31');
  });

  it('formats a DST transition instant in the requested time zone', () => {
    const label = formatInstant('2026-03-08T07:30:00Z', {
      locale: 'en-US',
      timeZone: 'America/New_York',
    });

    expect(label).toMatch(/03:30|3:30/);
    expect(label).toMatch(/EDT|GMT-4/);
  });
});

EDTGMT-4를 모두 허용하는 이유는 CI 환경의 ICU 데이터가 다를 수 있기 때문입니다. 반대로 local date key와 UTC 변환 결과는 비즈니스 로직이므로 엄격하게 고정합니다.

프롬프트와 hooks로 회귀를 막는다

Claude Code에는 구현 전에 제약을 전달합니다. 긴 프롬프트라도 위험한 경계를 명확히 적는 편이 낫습니다.

Before implementing date/time changes, read date-policy.ts and the database schema.

Constraints:
- API timestamps for completed events must be ISO strings with Z or an explicit offset
- Future bookings must keep localDate, localTime, and timeZone separate
- Intl.DateTimeFormat must always receive locale and timeZone
- Do not use new Date('YYYY-MM-DD') for local calendar dates
- Add Vitest cases for DST start, DST end, month end, and leap day
- Explain the PostgreSQL timestamp/timestamptz difference in the review notes

Done when:
- npm test -- --run date-time passes
- Updated API response examples are added to README

팀에서는 Claude Code hooks로 편집 후 날짜 테스트를 자동 실행할 수 있습니다. 공식 문서에 따르면 hooks는 Claude Code 라이프사이클 이벤트에서 shell command를 실행할 수 있습니다. 전체 운용은 Claude Code hooks 가이드를 참고하세요.

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [
          {
            "type": "command",
            "command": "npm test -- --run date-time"
          }
        ]
      }
    ]
  }
}

다루는 시간의 종류로 라이브러리를 고른다

날짜 라이브러리는 인기순이 아니라 제품이 다루는 시간의 종류로 골라야 합니다. 로케일 표시에는 Intl.DateTimeFormat, 순수 날짜 계산에는 date-fns, IANA 시간대 예약에는 Luxon, Instant, PlainDate, ZonedDateTime처럼 타입을 분리하려는 장기 설계에는 Temporal을 검토합니다. Temporal이 Stage 4여도 대상 런타임 지원과 polyfill 정책 확인은 필요합니다.

선택지적합한 경우주의점
Intl명시적 시간대가 있는 로케일 표시로컬 시간을 instant로 변환하는 도구는 아님
date-fns순수 날짜 계산, 함수 단위 import, 작은 유틸리티시간대 요구사항은 최신 공식 문서 확인
LuxonIANA 시간대 예약, 상대 시간, ISO 변환DST 중복 시간은 제품 규칙 필요
TemporalInstant, PlainDate, ZonedDateTime의 명확한 분리런타임 호환성과 polyfill 필요

콘텐츠와 수익화 관점에서도 단순 라이브러리 목록은 약합니다. 독자가 필요한 것은 무엇을 저장하고, 어디서 변환하고, 무엇을 테스트할지입니다. 이 정책을 CLAUDE.md, hooks, PR review, migration에 넣고 싶다면 Claude Code 교육 및 상담이 자연스러운 다음 단계입니다. 개인 개발자는 위 프롬프트를 무료 치트시트에 붙여 반복 가능한 가드레일로 쓰면 됩니다.

실제 검증 메모

Masa의 검증 메모: 가장 효과적인 확인은 UTC instant를 고정한 뒤 도쿄, 뉴욕, 로스앤젤레스에서 표시하고 날짜 경계를 넘는 local date key를 검증하는 것이었습니다. 가장 쉽게 재현된 버그는 YYYY-MM-DD를 사용자 로컬 날짜라고 생각하고 Date로 변환한 뒤 다른 시간대에서 전날로 표시되는 경우였습니다. 이 글의 정책은 local date를 문자열로 유지하고 저장된 instant와 표시 라벨만 명시적으로 변환하므로 Claude Code가 만든 diff도 리뷰하기 쉬워집니다.

#Claude Code #날짜 #시간대 #Intl #테스트
무료

무료 PDF: Claude Code 치트시트

이메일을 입력하면 명령, 리뷰 습관, 안전한 워크플로를 정리한 PDF를 받을 수 있습니다.

개인정보를 안전하게 관리하며 스팸을 보내지 않습니다.

Masa

작성자 소개

Masa

Claude Code 실무 워크플로와 팀 도입을 검증하는 엔지니어입니다.