React에서 달력을 직접 구현하는 것은 다양한 기술적 요소를 익히기에 좋은 연습이 될 수 있습니다. 달력은 단순한 UI 요소처럼 보일 수 있지만, 날짜 연산, 상태 관리, 사용자 입력 처리 등 여러 가지 개념이 포함됩니다.
본 글에서는 React를 활용하여 달력 컴포넌트를 직접 만들어보고, 구현 과정을 정리했습니다.
React 달력 컴포넌트의 기능 구성
React로 구현할 달력 컴포넌트의 주요 기능은 다음과 같습니다.
- 현재 년도와 월을 표시
- 이전 달, 다음 달로 이동할 수 있는 버튼 제공
- 달력을 6주(42일) 기준으로 구성하여 이전 달, 현재 달, 다음 달의 날짜를 표시
- 표시된 년월이 현재 날짜와 일치하는 경우, 오늘 날짜를 강조
- 특정 날짜 클릭 시 해당 날짜를 강조
달력 구현을 위한 날짜 연산 정리
- 년, 월, 일, 요일 가져오기
- 특정 달의 첫 번째 요일 구하기
- 특정 달의 마지막 날짜 구하기
- 특정 연도, 월, 일에 해당하는 날짜 객체를 가져오기
- 이전/다음 달 이동 시 연도와 월 조정하기
- 달력에 표시되는 이전 달 구하기
- 달력에 표시되는 다음 달 구하기
년, 월, 일, 요일 가져오기
const today = new Date(); // 현재 날짜 객체 생성
console.log(today.getFullYear()); // 현재 연도 출력
console.log(today.getMonth() + 1); // 현재 월 (0부터 시작하므로 +1 필요)
console.log(today.getDate()); // 현재 일(day)
console.log(today.getDay()); // 요일 (0 = 일요일, 1 = 월요일 ...)
- getFullYear() → 연도 가져오기
- getMonth() → 월 가져오기 (0부터 시작하므로 +1 필요)
- getDate() → 일(day) 가져오기
- getDay() → 요일 가져오기 (일요일: 0, 월요일: 1 …)
특정 달의 첫 번째 요일 구하기
해당 함수는 현재 선택된 달의 첫 번째 날짜(1일)의 요일을 반환합니다. 예를 들어, 2025년 2월이 선택된 경우, 2월 1일은 토요일이므로 [6, “토”] 값을 반환합니다. 이 값을 활용하여 달력의 시작 위치를 정할 수 있습니다.
const DAY_OF_WEEK = "일,월,화,수,목,금,토".split(","); // ["일", "월", "화", "수", "목", "금", "토", "일"]
function getFirstDayOfWeekOfCurrentMonth(
year: number,
month: number
): [number, string] {
const date = new Date(`${year}/${month}/1`);
const index = date.getDay();
return [index, DAY_OF_WEEK[index]];
}
console.log(getFirstDayOfWeek(2025, 2)); // 2025년 2월 1일의 요일 [6, "토"]
console.log(getFirstDayOfWeek(2025, 4)); // 2025년 4월 1일의 요일 [2, "화"]
특정 달의 마지막 날짜 구하기
특정 달의 첫 번째 요일을 구한 후, 마지막 날짜를 계산하면 그 달의 전체 날짜를 구성할 수 있습니다. 이를 활용하면 해당 월을 달력에 표시할 수 있습니다.
function getMonthLastDay(year, month) {
return new Date(year, month, 0).getDate();
}
console.log(getMonthLastDay(2024, 2)); // 2024년 2월의 마지막 날짜 (29)
console.log(getMonthLastDay(2024, 4)); // 2024년 4월의 마지막 날짜 (30)
특정 연도, 월, 일에 해당하는 날짜 객체 가져오기
이 함수는 주어진 연도(year), 월(month), 일(day)에 해당하는 Date 객체를 생성합니다. 달력의 날짜 데이터를 생성할 때 사용되며, 잘못된 날짜(예: 2월 30일) 입력 시 자동으로 보정됩니다.
function getDate(year: number, month: number, day: number) {
return new Date(year, month - 1, day);
}
이전/다음 달 이동 시 연도와 월 조정하기
이 함수는 이전 달 또는 다음 달로 이동할 때 연도와 월을 올바르게 조정합니다. 예를 들어, 12월에서 다음 달로 이동하면 연도가 증가하고 1월로 변경되며, 1월에서 이전 달로 이동하면 연도가 감소하고 12월로 변경됩니다.
function adjustYearMonth(
year: number,
month: number
): { year: number; month: number } {
if (month > 12) {
year += 1;
month -= 12;
} else if (month < 1) {
year -= 1;
month += 12;
}
return { year, month };
}
달력 컴포넌트 구현
지금까지 달력 구현에 필요한 기본적인 날짜 연산 및 조작 함수를 작성했습니다. 이제 이 함수들을 활용하여 실제 달력 컴포넌트를 구성하고, 화면에 표시하는 작업을 진행하겠습니다.
전체 코드
import clsx from "clsx";
import { useState } from "react";
import { twMerge } from "tailwind-merge";
// 요일을 한국어로 정의
const DAY_OF_WEEK = "일,월,화,수,목,금,토".split(",");
// 총 6주간의 캘린더 항목 길이를 정의
const CALENDAR_ITEM_LENGTH = 42;
// 이전 달 또는 다음 달로 이동할 때 연도와 월을 조정하는 함수
function adjustYearMonth(
year: number,
month: number
): { year: number; month: number } {
if (month > 12) {
year += 1;
month -= 12;
} else if (month < 1) {
year -= 1;
month += 12;
}
return { year, month };
}
// 현재 월의 첫 번째 요일을 얻는 함수
function getFirstDayOfWeekOfCurrentMonth(
year: number,
month: number
): [number, string] {
const date = new Date(`${year}/${month}/1`);
const index = date.getDay();
return [index, DAY_OF_WEEK[index]];
}
// 현재 월의 마지막 날짜를 얻는 함수
function getMonthLastDay(year: number, month: number) {
const date = new Date(year, month, 0);
return date;
}
// 주어진 연도, 월, 일에 해당하는 날짜 객체를 얻는 함수
function getDate(year: number, month: number, day: number) {
return new Date(year, month - 1, day);
}
// 날짜 상태를 위한 타입 정의
type Date = {
year: number;
month: number;
};
function App() {
// 현재 날짜를 가져오기
const today = new Date();
// 선택된 날짜와 포커스를 위한 상태 초기화
const [date, setDate] = useState<Date>({
year: new Date().getFullYear(),
month: new Date().getMonth() + 1,
});
const [focus, setFocus] = useState(0);
// 현재 월의 첫 번째 요일을 가져오기
const day = getFirstDayOfWeekOfCurrentMonth(date.year, date.month);
// 캘린더에 표시할 이전 달의 날짜들 가져오기
const prevMonthDays = Array.from({ length: day[0] }, (v, k) => {
return getDate(date.year, date.month, -k);
}).reverse();
// 현재 월의 날짜들 가져오기
const monthDays = Array.from(
{ length: getMonthLastDay(date.year, date.month).getDate() },
(v, k) => {
return getDate(date.year, date.month, k + 1);
}
);
// 캘린더에 표시할 다음 달의 날짜들 가져오기
const nextMonthDays = Array.from(
{ length: CALENDAR_ITEM_LENGTH - prevMonthDays.length - monthDays.length },
(v, k) => {
return getDate(date.year, date.month + 1, k + 1);
}
);
// 모든 날짜들을 합쳐서 캘린더에 표시할 날짜 배열 생성
const days = prevMonthDays.concat(monthDays).concat(nextMonthDays);
// 이전 달로 이동하는 함수
function prevMonth() {
setDate((prev) => {
return {
...prev,
...adjustYearMonth(prev.year, prev.month - 1),
};
});
}
// 다음 달로 이동하는 함수
function nextMonth() {
setDate((prev) => {
return {
...prev,
...adjustYearMonth(prev.year, prev.month + 1),
};
});
}
// 현재 상태에 따라 클래스 이름을 생성하는 함수
function classNames(time: number) {
return twMerge(
clsx("flex items-center justify-center p-3 bg-slate-200 h-[50px]", {
"border border-red-500": time === focus,
"bg-red-500 text-white":
time ===
new Date(
today.getFullYear(),
today.getMonth(),
today.getDate()
).getTime(),
})
);
}
// 포커스를 처리하는 함수
function handleFocus(time: number) {
setFocus(time);
}
return (
<div className="h-screen flex justify-center items-center">
<div className="max-w-[600px] w-full">
<div className="mb-2">
{date.year}년 {date.month}월
</div>
<div className="border p-1 rounded-md">
<header>
<ul className="grid grid-cols-7 gap-1">
{DAY_OF_WEEK.map((day) => (
<li
key={day}
className="p-3 flex items-center justify-center h-[50px]"
>
{day}
</li>
))}
</ul>
</header>
<ul className="grid grid-cols-7 gap-1">
{days.map((date) => (
<li
key={date.getTime()}
className={classNames(date.getTime())}
onClick={() => handleFocus(date.getTime())}
>
{date.getDate()}
</li>
))}
</ul>
</div>
<div className="flex justify-end gap-2 mt-2">
<button onClick={prevMonth}>이전달</button>
<button onClick={nextMonth}>다음달</button>
</div>
</div>
</div>
);
}
export default App;
현재 날짜 및 상태 관리
function App() {
// 현재 날짜를 가져오기
const today = new Date();
// 선택된 연도와 월을 관리하는 상태
const [date, setDate] = useState<Date>({
year: new Date().getFullYear(),
month: new Date().getMonth() + 1,
});
// 클릭된 날짜를 강조하기 위한 상태
const [focus, setFocus] = useState(0);
useState를 사용하여 현재 연도와 월을 상태로 관리합니다.
focus 상태를 추가하여 사용자가 선택한 날짜를 강조할 수 있도록 합니다.
캘린더의 날짜 데이터 생성
1일의 요일을 기준으로 이전 달의 일부 날짜를 계산
const day = getFirstDayOfWeekOfCurrentMonth(date.year, date.month);
const prevMonthDays = Array.from({ length: day[0] }, (v, k) => {
return getDate(date.year, date.month, -k);
}).reverse();
현재 월의 첫 번째 요일을 기준으로 이전 달의 날짜를 계산하여 채움
현재 월의 날짜 생성
const monthDays = Array.from(
{ length: getMonthLastDay(date.year, date.month).getDate() },
(v, k) => {
return getDate(date.year, date.month, k + 1);
}
);
현재 월의 마지막 날짜를 구한 후, 1일부터 해당 월의 마지막 날까지의 데이터를 생성
다음 달의 날짜를 6주(42일) 기준으로 생성
const nextMonthDays = Array.from(
{ length: CALENDAR_ITEM_LENGTH - prevMonthDays.length - monthDays.length },
(v, k) => {
return getDate(date.year, date.month + 1, k + 1);
}
);
6주(42일) 기준으로 부족한 날짜를 다음 달의 날짜로 채움
모든 날짜를 하나의 배열로 결합
const days = prevMonthDays.concat(monthDays).concat(nextMonthDays);
이전 달, 현재 달, 다음 달의 날짜를 하나의 배열로 합쳐 달력 데이터를 완성
이전/다음 달 이동 기능
function prevMonth() {
setDate((prev) => {
return {
...prev,
...adjustYearMonth(prev.year, prev.month - 1),
};
});
}
function nextMonth() {
setDate((prev) => {
return {
...prev,
...adjustYearMonth(prev.year, prev.month + 1),
};
});
}
<button>을 클릭하면 setDate()를 사용해 연도/월 상태를 변경
달력 UI 렌더링
연도 및 월을 표시하는 헤더
<div className="mb-2">
{date.year}년 {date.month}월
</div>
요일 표시 영역
<ul className="grid grid-cols-7 gap-1">
{DAY_OF_WEEK.map((day) => (
<li key={day} className="p-3 flex items-center justify-center h-[50px]">
{day}
</li>
))}
</ul>
달력 날짜 출력 영역
<ul className="grid grid-cols-7 gap-1">
{days.map((date) => (
<li key={date.getTime()} className={classNames(date.getTime())} onClick={() => handleFocus(date.getTime())}>
{date.getDate()}
</li>
))}
</ul>
이전/다음 달 이동 버튼
<div className="flex justify-end gap-2 mt-2">
<button onClick={prevMonth}>이전달</button>
<button onClick={nextMonth}>다음달</button>
</div>