라이브러리 특징:

  • 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

캘린더 이벤트 겹침 처리 알고리즘

겹치는 이벤트들을 나란히 배치하기 위한 레인 할당:

interface Event {
  id: string
  start: number
  end: number
}
interface PositionedEvent extends Event {
  lane: number
  totalLanes: number
}

function calculatePositions(events: Event[]): PositionedEvent[] {
  // 1. 시작 시간 순 정렬
  const sorted = [...events].sort((a, b) => a.start - b.start || a.end - b.end)

  // 2. 겹침 그룹 찾기 + 레인 할당
  const result: PositionedEvent[] = []
  let group: Event[] = []
  let groupEnd = 0

  sorted.forEach((event) => {
    if (group.length === 0 || event.start < groupEnd) {
      group.push(event)
      groupEnd = Math.max(groupEnd, event.end)
    } else {
      processGroup(group, result)
      group = [event]
      groupEnd = event.end
    }
  })
  if (group.length) processGroup(group, result)

  return result
}

function processGroup(group: Event[], result: PositionedEvent[]) {
  const activeLanes: number[] = []

  group.forEach((event) => {
    // 사용 가능한 가장 낮은 레인 찾기
    let lane = activeLanes.findIndex((end) => end <= event.start)
    if (lane === -1) lane = activeLanes.length

    activeLanes[lane] = event.end
    result.push({ ...event, lane, totalLanes: 0 })
  })

  // totalLanes 업데이트
  const maxLane =
    Math.max(...result.slice(-group.length).map((e) => e.lane)) + 1
  result.slice(-group.length).forEach((e) => (e.totalLanes = maxLane))
}

// CSS 적용: left = lane/totalLanes * 100%, width = 100%/totalLanes
#510

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