
이번 글에서는 별도의 지식이 없는 비개발자인 제가 Next.js를 이용해 페스티봄 달력을 만드는 과정을 설명드립니다.
처음 계획할 때부터 중심 화면은 목록이 아니라 달력이었습니다. 여러 페스티벌의 날짜를 한눈에 비교하면 좋겠다고 생각했습니다.
막상 모바일 화면에 달력을 넣어보니 생각처럼 모든 일정이 보이지 않았습니다. 주로 금요일부터 일요일까지 행사가 몰렸고, 작은 날짜 칸에 이름을 계속 쌓으면 글자가 읽히지 않았어요. 표시하지 못한 일정은 +3개처럼 남은 개수로 알리고, 원하는 페스티벌을 바로 찾는 검색 기능도 추가했습니다.
캘린더 라이브러리를 설치하면 더 빨리 시작할 수 있었겠지만 직접 수정할 때 구조를 이해하기 어렵다고 판단했습니다. 그래서 Next.js 프로젝트 안에서 날짜 계산부터 일정 막대 배치까지 직접 만들었어요.
이번 글에서는 Next.js가 무엇인지부터 설명하고, 페스티봄의 캘린더에서 어떤 역할을 맡겼는지 살펴봅니다. 월요일 시작 달력, 여러 날 이어지는 일정 막대, PC와 모바일의 다른 조작법, 색상과 글자 크기를 통일한 방법도 함께 정리했습니다.
Next.js란? 캘린더에서 어떤 역할을 했을까
Next.js는 React를 기반으로 웹사이트와 웹앱을 만드는 프레임워크입니다. 페이지 이동, 화면 렌더링, 서버와 브라우저에서 실행할 코드의 구분처럼 웹앱에 필요한 구조를 제공합니다. 캘린더 자체를 만들어주는 도구는 아닙니다.
페스티봄에서는 캘린더 전체를 하나의 거대한 파일로 두지 않았습니다. 월 이동과 검색이 들어간 헤더, 요일, 날짜 칸, 일정 막대, 선택한 날짜의 페스티벌 목록을 각각 컴포넌트로 나눴습니다. Next.js 화면에서는 이 컴포넌트를 조립해 하나의 캘린더로 보여줍니다.
"use client";
export default function Calendar() {
return (
<section>
<CalendarHeader />
<CalendarWeekdays />
<CalendarGrid />
<FestivalSidePanel />
</section>
);
}
코드 위의 use client는 이 화면에 브라우저 상호작용이 필요하다는 표시입니다. 달력에는 이전·다음 달 버튼, 검색, 날짜 선택, 마우스 휠과 모바일 스와이프가 들어갑니다. Next.js 공식 문서도 상태, 이벤트 처리, 브라우저 API가 필요한 화면에 클라이언트 컴포넌트를 사용하도록 설명합니다.[1]
현재 보고 있는 연도와 월은 ?year=2026&month=8처럼 주소에 기록했습니다. 이를 통해 새로고침하거나 뒤로 가기를 실행해도 같은 달을 다시 불러올 수 있습니다. 이 부분에서 Next.js의 useRouter, usePathname, useSearchParams를 활용했습니다.[2]
캘린더 라이브러리를 사용하지 않고 직접 만든 이유
라이브러리를 쓰지 않은 가장 큰 이유는 원하는 모양으로 계속 고칠 수 있어야 했기 때문입니다. 이미 정해진 달력 구조 위에 기능을 덧붙이면 처음에는 편하지만, 금·토·일을 잇는 막대나 모바일 전용 조작을 바꿀 때 라이브러리의 사용법부터 다시 찾아야 했습니다.
기존에 라이브러리를 사용하면 시작은 편하지만 공부하기에는 어렵다고 생각도 했어요. 그래서 직접 만들기로 했습니다.
페스티벌은 주말에 일정이 몰립니다. 일반적인 달력처럼 일요일부터 시작하면 한 주의 끝인 금요일과 토요일, 다음 줄의 일요일이 갈라집니다. 페스티봄은 월요일부터 일요일까지 한 줄로 배치해 금·토·일 일정을 이어서 볼 수 있도록 만들었습니다.
직접 구현했다고 해서 모든 코드를 한 번에 완성한 것은 아닙니다. 날짜 칸부터 만든 다음 일정 막대를 추가하고, 겹치는 순서를 정리한 뒤 모바일 조작을 붙였습니다. 문제가 생길 때마다 해당 계산이나 컴포넌트를 떼어 고치는 방식이었어요.
월요일 시작 달력과 여러 날짜의 페스티벌을 표시하는 방식
한 달은 매번 다른 요일에 시작하고 28일부터 31일까지 길이도 달라집니다. 페스티봄은 이전 달과 다음 달의 일부를 포함한 42개 날짜를 만들어 7열 6주 형태를 유지합니다. 자바스크립트에서 일요일은 0으로 계산되므로, 아래와 같이 값을 옮겨 월요일을 첫 번째 칸으로 맞췄습니다.
const startDayOffset = (firstDayOfMonth.getDay() + 6) % 7;
return Array.from({ length: 42 }, (_, index) => {
// 월요일부터 시작하는 6주 날짜를 계산합니다.
});
현재 화면에 필요한 페스티벌만 Supabase에서 가져온 뒤, 각 날짜가 시작일과 종료일 사이에 포함되는지 확인합니다. 하루짜리 행사는 한 칸에 머물고, 여러 날 진행되는 행사는 남은 날짜만큼 가로 막대를 늘립니다.
일정 막대 연결과 둥근 모서리 오류
여러 날 이어지는 막대는 달력의 한 줄을 넘어갈 수 없습니다. 일요일에 도착하면 막대를 끊고 다음 주 월요일부터 다시 그려야 합니다. 따라서 페스티벌의 남은 기간과 현재 주에 남은 칸 수 중 작은 값을 막대 길이로 사용했습니다.
처음에는 막대 끝을 둥글게 처리하는 조건이 제대로 맞지 않았습니다. 행사가 끝나는 날과 달력 줄이 끝나는 날을 같은 조건으로 다루면서 중간 막대에도 둥근 모서리가 붙거나, 마지막 막대가 각진 채로 남았어요.
이를 해결하려고 막대 길이와 함께 endsInThisRow 값을 계산하는 함수로 바꿨습니다. 실제 종료일이 현재 주 안에 있을 때만 오른쪽 끝을 둥글게 만들었습니다. 화면 모양의 문제처럼 보였지만 원인은 CSS보다 날짜 계산 함수에 있었습니다.
공간을 넘은 일정의 +개수 표시
같은 날짜에 페스티벌이 겹치면 먼저 시작한 일정을 위쪽 줄에 놓고, 기존 일정이 끝난 줄은 다음 행사에 다시 사용했습니다. 날짜 칸에 보이는 일정 막대는 최대 세 줄로 제한했습니다.
세 줄을 넘는 일정까지 억지로 넣으면 모바일에서 이름을 읽기 어렵습니다. 숨겨진 일정 수를 계산해 날짜 오른쪽 위에 +2개처럼 표시하고, 날짜를 누르면 전체 목록이 열리도록 구성했어요. 한눈에 모두 보여주고 싶었던 처음 목표를 완전히 달성하지는 못했지만, 일정이 더 있다는 사실은 놓치지 않게 했습니다.
색상·글자·여백을 디자인 토큰으로 통일한 방법
기능을 계속 추가하면서 또 다른 문제가 보였습니다. 컴포넌트마다 색상과 글자 크기를 따로 입력하니 화면이 복잡해졌고, 같은 역할의 글자도 크기와 진하기가 달랐습니다. 한 곳을 고친 뒤 비슷한 화면을 일일이 찾아 수정해야 했어요.
알아보니 색상이나 글자 크기에 이름을 붙여 미리 정하고, 각 컴포넌트가 그 값을 불러오게 만들 수 있었습니다. 이런 공통 값을 디자인 토큰이라고 부릅니다. 페스티봄에서는 캘린더 막대에 사용할 분홍·파랑·초록·보라·주황의 파스텔 색상 다섯 개를 정했어요.
festival: {
"bar-pink": "#FFD8E1",
"bar-blue": "#C0E6F4",
"bar-green": "#DEF4C5",
"bar-purple": "#DCC2EE",
"bar-orange": "#FFE7A3",
},
ink: {
DEFAULT: "#111111",
secondary: "#505050",
tertiary: "#767676",
}
캘린더 바의 배경은 다섯 색 안에서 고르고, 행사명은 공통 festival-night 색을 사용했습니다. 일반 글자는 중요도에 따라 ink, ink-secondary, ink-tertiary로 나눴습니다. 색상 이름이 역할을 나타내므로 코드에서 #111111 같은 값을 반복할 필요도 줄었습니다.
글자 크기는 calendarTitle, calendarDate, calendarEvent처럼 용도별로 묶었습니다. 여백도 컴포넌트마다 임의의 숫자를 계속 만들기보다 Tailwind CSS의 간격 단위를 반복해서 사용했습니다. Tailwind의 테마 값으로 색상과 간격을 정의하면 같은 이름을 여러 화면에서 반복해서 사용합니다.[3]
이 구조의 장점은 나중에 드러났습니다. 글씨가 너무 흐리거나 일정 막대 색이 강해 보일 때 공통 값을 한 번 바꾸면 이를 사용하는 화면도 함께 달라집니다. 비개발자인 제게는 디자인을 잘하는 것보다 선택지를 미리 줄여놓는 방법이 더 효과적이었어요.
PC와 모바일을 구분해 캘린더를 개발한 방법
PC 화면은 넓은 날짜 칸과 옆 패널로 구성
PC에서는 날짜 칸의 높이를 넉넉하게 두고 일정 막대를 세 줄까지 보여줍니다. 날짜를 선택하면 오른쪽에 해당 날짜의 페스티벌 목록과 상세 정보가 열립니다. 화면 폭을 활용해 달력과 정보를 동시에 볼 수 있도록 구성했습니다.
마우스 휠로 달력 위를 움직이면 이전 달이나 다음 달로 넘어가게 했습니다. 다만 일반 페이지 스크롤과 충돌하지 않도록 이동량이 기준을 넘었을 때만 한 달을 바꾸고, 한 번 이동한 직후에는 잠깐 입력을 잠갔습니다.
모바일은 검색과 가까운 페스티벌 목록으로 보완
모바일에서는 달력 한 칸의 폭이 좁아 일정 이름을 모두 보여줄 수 없습니다. 그래서 페스티벌이나 아티스트 이름을 검색하면 해당 달과 날짜로 바로 이동하는 기능을 추가했습니다. 날짜를 누르면 PC의 옆 패널 대신 화면 아래에서 목록이 올라옵니다.
달력만으로 찾기 불편한 상황을 보완하려고 별도의 페스티벌 목록 화면도 만들었습니다. 가까운 날짜의 행사부터 포스터와 함께 보여주기 때문에 달력에서 글자가 잘린 경우에도 일정을 다시 확인할 수 있습니다. 달력을 없애는 대신 서로 다른 방식의 화면을 추가해 보완했습니다.
모바일 스와이프 중단과 현재 월 초기화 문제
가장 오래 걸린 부분은 모바일 스와이프였습니다. 좌우로 넘길 때는 달이 바뀌어야 하지만 위아래 움직임은 페이지 스크롤로 남겨야 합니다. 기기마다 화면 크기와 터치 처리 방식이 달라 한 기기에서 잘 되던 동작이 다른 환경에서는 멈추기도 했어요.
달력을 여러 번 넘기다 제스처가 멈추거나 초기 화면으로 돌아가는 문제도 생겼습니다. 브라우저가 세로 스크롤을 처리하면 pointercancel 이벤트가 발생할 수 있기 때문에, 취소·화면 복귀·포인터 해제 시점마다 제스처 상태를 초기화했습니다.[4]
현재 월 자체가 초기화되는 문제는 터치 코드만 고쳐서는 해결되지 않았습니다. 화면 상태와 주소의 연도·월 값이 서로 어긋난 것이 원인이었어요. 월 이동 함수를 하나로 모은 뒤 스와이프와 버튼, 마우스 휠이 같은 함수를 사용하게 바꿨습니다. 뒤로 가기에서도 주소를 다시 읽어 화면의 월을 맞췄습니다.
직접 만든 Next.js 캘린더에서 남은 장점과 한계
직접 만든 덕분에 월요일 시작, 주말 일정 막대, 다섯 가지 색상, +개수, 모바일 검색처럼 페스티봄에 필요한 기준을 자유롭게 적용했습니다. 기능별 컴포넌트와 날짜 계산 함수를 나눠 놓으니 문제가 생겼을 때 확인할 범위도 전보다 분명해졌습니다.
그렇다고 모든 일정이 한눈에 들어오게 된 것은 아닙니다. 작은 모바일 화면에 많은 정보를 동시에 보여주는 데에는 물리적인 한계가 있었습니다. 막대를 더 작게 만들면 이름을 읽을 수 없고, 칸을 크게 만들면 한 달 전체가 보이지 않아요.
정확한 이름을 찾을 때는 검색을 사용하고, 가까운 행사를 둘러볼 때는 포스터 목록을 이용하도록 나눴습니다. 하나의 화면에 전부 넣으려 했지만 한계가 있었고 차선책으로 페이지를 나눠 각 화면에서 필요한 정보를 주기로 했습니다.
Next.js를 완전히 이해한 뒤 시작한 작업은 아니었습니다. 하지만 컴포넌트를 조립하고 브라우저에서 필요한 상태를 관리하는 구조를 실제 기능에 적용하면서 역할을 조금씩 파악했습니다. 이번 캘린더 작업에서 가장 크게 배운 점은 복잡한 기능도 날짜 계산, 화면 표시, 사용자 조작, 디자인 규칙으로 나누면 고칠 지점을 찾을 수 있다는 것이었습니다.
참고 자료
- Next.js Server and Client Components: 상태와 이벤트 처리가 필요한 클라이언트 컴포넌트의 역할을 확인했습니다.
- Next.js Adding Search and Pagination:
useRouter,usePathname,useSearchParams로 URL 상태를 다루는 방법을 참고했습니다. - Tailwind CSS Color: 테마에 공통 색상을 정의하고 유틸리티 이름으로 재사용하는 방식을 참고했습니다.
- MDN touch-action: 브라우저의 터치 처리와
pointercancel발생 조건을 확인했습니다.
함께 읽는 시리즈
비개발자의 바이브 코딩 도전기
-
1편
비개발자 바이브 코딩 후기: 어려웠던 이유 5가지
-
2편
LLM 코드 생성 원리: 2026 Codex는 코드를 어떻게 수정할까?
-
3편
2026 비전공자의 첫 바이브 코딩 MVP 만들기: 페스티봄 핵심 기능과 범위 정하기
-
4편
컴포넌트란? 2026 바이브 코딩 컴포넌트 비개발자가 부딪히며 배운 코드 재사용 방법
-
5편
데이터베이스 설계 방법: 2026 비개발자가 축제와 아티스트를 연결한 과정
-
6편
2026 Next.js·Supabase 관리자 페이지 만들기: JSON 등록과 승인 기능 구현
-
7편 · 현재 글
2026 비개발자 Next.js 캘린더 만들기: 라이브러리 없이 페스티벌 일정 구현하기
-
8편
리팩토링이란? 2026 비개발자가 바이브 코딩 코드 구조를 나눈 방법
-
9편
Next.js Supabase 보안 설정: 2026 비개발자가 RLS·MFA·서비스 테스트까지 한 과정