Footnotes

  1. 날짜 및 시간 작업을 위한 Temporal API 공식 문서

  2. 날짜 및 시간을 사용하여 작업할 때의 문제를 논의하고 솔루션으로 Temporal API를 제안하는 블로그 게시물

  3. Date 개체, 날짜 형식, 시간대 및 날짜 라이브러리를 포함하여 JavaScript에서 날짜 작업에 대한 포괄적인 설명

  4. 타임존 작업의 기본 사항을 설명

#11
import isAfter from 'date-fns/isAfter';

isAfter(new Date(), new Date(DATE))

날짜 비교 할 일이 있어서 별 생각 없이 new Date를 때렸는데 safari에서 안되는 문제가 발견되었다. 콘솔을 확인해보니 yyyy-MM-dd HH:mm:ss 해당 형태의 포멧 에서는 안된다. 평소에 new Date 보다는 moment나 date-fns같은 라이브러리를 당연하게 써오다 보니 몰랐다. 그런데 또 다른 생각을 해보자면 저런 문제가 있기 때문에 더 적극적으로 라이브러리를 사용해야 한다는 게 함정.

import isAfter from 'date-fns/isAfter';
import format from 'date-fns/format';

isAfter(new Date(), format(DATE))
#159

라이브러리 특징:

  • react-aria - Adobe Spectrum 팀의 headless. 스타일 없이 로직과 접근성만. useCalendar, useDatePicker 훅 기반. ARIA 구현 꼼꼼. 일정 표시는 직접 구현.
  • Schedule-X - 최신. React/Vue/Angular 어댑터. 드래그/리사이즈 지원. 월/주/일 뷰. 가벼움. 레퍼런스 적음.
  • FullCalendar - 가장 오래됨. 플러그인 아키텍처. 타임라인, 리소스 뷰 등 고급 기능. 일부 유료, 번들 큼.
  • react-big-calendar - 순수 React. Google Calendar 스타일. moment/date-fns/dayjs 선택 가능. 타입 지원 아쉬움.
상황추천
완전 커스텀 UI 필요react-aria + 직접 구현
빠르게 기본 기능 필요react-big-calendar
모던한 DX, 가벼움 중시Schedule-X
엔터프라이즈급 기능FullCalendar

Footnotes

  1. headless 모드가 있으면 좋을 것 같은데 찾기 어렵다. 이런게 있다…정도로만 생각하자. 실제 갖다 써보면 뭔말인지 알 수 있을거다.

  2. 네이티브로 날짜 계산(?)을 구현하려면 참고

#28
import {
  addDays,
  addMonths,
  eachDayOfInterval,
  eachWeekOfInterval,
  startOfMonth,
} from 'date-fns'

const startOfMonthDate = startOfMonth(new Date())
const matrix = eachWeekOfInterval({
  start: startOfMonthDate,
  end: addMonths(startOfMonthDate, 1),
}).map((weekDay) => {
  const startDate = new Date(weekDay)

  return eachDayOfInterval({
    start: startDate,
    end: addDays(startDate, 6),
  })
})
#29

Flaky 테스트 방지 - Date, Math.random 같은 비순수 함수를 props로 주입

  • new Date(), Math.random() 같은 비순수 함수는 매번 다른 결과 → 테스트 불안정
  • 해결: 기본 매개변수로 주입하면 프로덕션 동작 유지 + 테스트에서 제어 가능
function Date({ date = new Date() }) {
  const [date, setDate] = React.useState(date)

  return (
    <input
      type="date"
      onChange={(e) => setDate(e.target.value)}
      defaultValue={date}
    />
  )
}

function Random({ randomizer = Math.random }) {
  const [state, setState] = React.useState(randomizer())

  return <div>{state}</div>
}

테스트:

describe('Date Component', () => {
  it('should update state on date change', () => {
    render(<Date date={new Date('2023-01-01')} />)

    const input = screen.getByRole('textbox')

    expect(input.value).toBe('2023-01-01')
  })
})

describe('Random Component', () => {
  it('should render a random number', () => {
    const mockRandomizer = () => 0.5

    render(<Random randomizer={mockRandomizer} />)

    const div = screen.getByText('0.5')

    expect(div).toBeInTheDocument()
  })
})
#314

vitest와 jest에서 가짜 타이머를 사용하는 방법. 내부적으로는 @sinonjs/fake-timers를 사용. vitest와 jest 모두 시스템 시간을 조작할 수 있도록 하여 테스트가 일관되게 실행되도록 하고 다양한 시간과 날짜를 시뮬레이션하여 다양한 조건에서 코드가 예상대로 작동하는지 확인할 수 있음.

describe('isSameDate', () => {
  beforeEach(() => {
    vi.useFakeTimers()
    vi.setSystemTime('2024-07-20')
  })

  afterEach(() => {
    vi.useRealTimers()
  })

  it('현재 날짜와 동일한 날짜를 전달하면 true를 반환한다', () => {
    expect(isSameDate('2024-07-20')).toBe(true)
  })

  it('현재 날짜와 다른 날짜를 전달하면 false를 반환한다', () => {
    expect(isSameDate('2024-07-19')).toBe(false)
    expect(isSameDate('2024-07-21')).toBe(false)
  })
})

#315

RSC에서 날짜 처리: 쿠키 기반 타임존

문제: Date 객체 직렬화, 서버/클라이언트 타임존 불일치, 하이드레이션 FOUT

해결: 서버에서 타임존 적용해서 렌더링

// middleware.js - 타임존 감지
export function middleware(request) {
  const timezone =
    request.cookies.get('user-timezone')?.value ||
    request.geo?.timezone || // Vercel
    'UTC'

  const response = NextResponse.next()
  response.headers.set('x-user-timezone', timezone)
  return response
}

// 서버 컴포넌트
const getUserTimezone = cache(() => {
  return headers().get('x-user-timezone') || 'UTC'
})

// 클라이언트 - 타임존 자동 감지 후 쿠키 저장
useEffect(() => {
  const tz = Intl.DateTimeFormat().resolvedOptions().timeZone
  document.cookie = `user-timezone=${tz}; path=/; max-age=31536000`
}, [])

첫 방문은 인프라 추정값 사용, 이후 정확한 타임존 적용. FOUT 없음.


대안: useSyncExternalStore (client component 전용)

'use client'

const timezoneStore = {
  getSnapshot: () => Intl.DateTimeFormat().resolvedOptions().timeZone,
  getServerSnapshot: () => 'UTC',
  subscribe: () => () => {},
}

function useTimezone() {
  return useSyncExternalStore(
    timezoneStore.subscribe,
    timezoneStore.getSnapshot,
    timezoneStore.getServerSnapshot
  )
}

서버: UTC → 클라이언트: 실제 타임존. 하이드레이션 에러 없음, 대신 FOUT 발생.

#509

dayjs로 특정 날짜가 포함된 주의 모든 날짜 가져오기

import dayjs from 'dayjs'
import isoWeek from 'dayjs/plugin/isoWeek'
dayjs.extend(isoWeek)

// startOf('week') → 일요일 시작
// startOf('isoWeek') → 월요일 시작 (캘린더 UI에 주로 사용)
function getWeekDays(date, iso = true) {
  const start = dayjs(date).startOf(iso ? 'isoWeek' : 'week')
  return Array.from({ length: 7 }, (_, i) =>
    start.add(i, 'day').format('YYYY-MM-DD')
  )
}

getWeekDays('2025-10-28') // ['2025-10-27', ..., '2025-11-02']
#513

Date.getDay() - 요일 반환 (0=일요일, 6=토요일)

new Date().getDay() // 0~6
new Date('2024-12-25').getDay() // 3 (수요일)
new Date(2024, 11, 25).getDay() // 월은 0부터 시작

const days = ['일', '월', '화', '수', '목', '금', '토']
days[new Date().getDay()] // 오늘 요일
#519

현재 시간부터 목표 시간까지 남은 시간 계산

function calculateRemainingTime(targetTime) {
  const now = new Date()
  const [h, m, s] = targetTime.split(':').map(Number)

  const target = new Date()
  target.setHours(h, m, s)

  const diff = target - now
  if (diff < 0) return null

  return {
    hours: Math.floor(diff / 3600000) % 24,
    minutes: Math.floor(diff / 60000) % 60,
    seconds: Math.floor(diff / 1000) % 60,
  }
}

calculateRemainingTime('18:30:00')
// { hours: 2, minutes: 15, seconds: 30 }
#531