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(문자열) 이 보장하는 건 ISO 형식(2022-04-09T12:00:00)뿐이고, 공백으로 가른 2022-04-09 12:00:00 은 구현 정의라 엔진마다 다르다.1

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

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

isAfter(new Date(), parse(DATE, 'yyyy-MM-dd HH:mm:ss', new Date()))

Footnotes

  1. Date.parse() - MDN: “The date time string format (produced by toISOString()) must be supported.” / “Other formats are implementation-defined and may not work across all browsers. A library can help if many different formats are to be accommodated.” ↩

#159

어느 캘린더 라이브러리를 쓸까.

상황 추천
완전 커스텀 UI 필요 react-aria + 직접 구현
빠르게 기본 기능 필요 react-big-calendar
모던한 DX, 가벼움 중시 Schedule-X
엔터프라이즈급 기능 FullCalendar

라이브러리 특징:

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

Footnotes

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

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

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

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

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

Flaky 테스트 방지 — 기본 매개변수는 호출할 때마다 평가된다. 인자를 안 넘기면 new Date(), 넘기면 그 값이라 Date·Math.random 같은 비순수 값을 props 기본값으로 주입한다1

  • new Date(), Math.random() 같은 비순수 함수는 매번 다른 결과 → 테스트 불안정
  • 해결: 기본 매개변수로 주입하면 프로덕션 동작 유지 + 테스트에서 제어 가능
function DateInput({ now = new Date() }) {
  const [date, setDate] = React.useState(now.toLocaleDateString('sv-SE')) // 로컬 YYYY-MM-DD (toISOString 은 UTC 라 KST 오전 9시 전엔 하루 밀린다)

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

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

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

테스트:

describe('DateInput', () => {
  it('주입한 날짜로 렌더한다', () => {
    render(<DateInput now={new Date(2023, 0, 1)} />)

    const input = screen.getByLabelText('날짜')

    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()
  })
})

Footnotes

  1. Default parameters - MDN: “Default function parameters allow named parameters to be initialized with default values if no value or undefined is passed.” · “The default argument is evaluated at call time.” ↩

#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

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

// proxy.js (Next.js 16 — 예전 middleware.js) - 타임존 감지
export function proxy(request) {
  const timezone =
    request.cookies.get('user-timezone')?.value ||
    request.headers.get('x-vercel-ip-timezone') || // Vercel. request.geo 는 15 에서 제거
    'UTC'

  const requestHeaders = new Headers(request.headers)
  requestHeaders.set('x-user-timezone', timezone)
  // 응답 헤더가 아니라 요청 헤더로 넘겨야 서버 컴포넌트의 headers() 가 읽는다
  return NextResponse.next({ request: { headers: requestHeaders } })
}

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

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

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


대안: 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 발생 — getServerSnapshot 은 서버 렌더와 하이드레이션 때만 쓰이고, 그 뒤 getSnapshot 값으로 다시 그린다.4


Footnotes

  1. proxy.js – Next.js: “The middleware file convention is deprecated and has been renamed to proxy.” / “NextResponse.next({ request: { headers: requestHeaders } }) to make requestHeaders available upstream” ↩

  2. How to upgrade to version 15 – Next.js: “The geo and ip properties on NextRequest have been removed as these values are provided by your hosting provider.” / “Previously synchronous Request-time APIs that rely on request information are now asynchronous” ↩

  3. Request headers – Vercel: “The name of the time zone for the location of the requester’s public IP address in ICANN Time Zone Database name format such as America/Chicago.” ↩

  4. useSyncExternalStore – React: “optional getServerSnapshot: A function that returns the initial snapshot of the data in the store. It will be used only during server rendering and during hydration of server-rendered content on the client.” ↩

#509

dayjs로 특정 날짜가 포함된 주의 모든 날짜 가져오기 — week 는 로케일을 따르고 isoWeek 는 ISO 8601(월요일 시작)을 따른다.1

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

// startOf('week') → 로케일의 주 시작 (기본 en·ko 는 일요일, en-gb 는 월요일)
// 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']

Footnotes

  1. StartOf - Day.js: week — “the first day of this week, 00:00 (locale aware)” / isoWeek — “the first day of this week according to ISO 8601, 00:00” ↩

#513

Date.getDay() - 로컬 시간 기준 요일 반환 (0=일요일, 6=토요일)1

new Date().getDay() // 0~6
new Date(2024, 11, 25).getDay() // 3 (수요일) — 월은 0부터 시작
new Date('2024-12-25').getDay() // ⚠ 날짜만 쓴 문자열은 UTC 자정 → UTC보다 느린 시간대(미국 등)에선 2

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

Footnotes

  1. Date.prototype.getDay() - MDN: “returns the day of the week for this date according to local time, where 0 represents Sunday.” / Date time string format - MDN: “When the time zone offset is absent, date-only forms are interpreted as a UTC time and date-time forms are interpreted as a local time.” ↩

#519

현재 시간부터 목표 시간까지 남은 시간 계산 — Date 끼리 빼면 숫자 문맥에서 타임스탬프(ms)로 바뀌어 차가 밀리초로 나온다.1

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 }

Footnotes

  1. Date — MDN: “The valueOf() and Symbol.toPrimitive (when passed “number”) methods — which are automatically called in number coercion — return the timestamp, causing Date objects to behave like their timestamps when used in number contexts.“ ↩

#531