brunch

NextJS에서 D3 달력구현

D3로 그래프만 구현하는 것은 아닙니다.

by 전준형

달력 구현은 라이브러리로 해결하는 대표 UI요소 중 하나이다. 라이브러리로도 충분히 원하는 기능을 구현할 수 있을 뿐더러 디자인도 괜찮아서 굳이 구현하려고 하지 않는다. 그리고 매우 오래걸림. 하지만 프로젝트 전체 테마와 맞지 않을 때도 있고 원하는 기능을 일부 축소해야할 때도 있다. 이때도 d3가 좋은 솔루션이 될 수 있다. 이 글에서는 d3로 수치적 그래프 뿐만 아니라 달력과 같이 상호작용이 많은 UI요소를 구현하는 법을 소개한다.


구현 목표

스크린샷 2023-12-04 22.34.29.png
스크린샷 2023-12-04 22.47.13.png
프로젝트에 들어간 달력UI 모습

왼쪽 달력으로 시작일, 오른쪽 달력으로 종료일 설정

달력모달 여는 버튼, 달력 상단, 달력 하단의 날짜 연동

시작일과 종료일 선택시 서로를 넘어가지 않게 경계조건 추가

년/월 모달과 좌우버튼으로 년/월 이동

아래쪽 시작일과 종료일 표시, 및 클릭 시 해당 일이 있는 월로 이동(월 이탈시 파란색으로 변함)


달력 구현

이번 달력 구현은 하나의 달력 컴포넌트를 재활용 하여 두개의 달력을 하나의 달력 컨테이너 안에 담아 버튼요소등과 함께 배치 한다.

달력 컴포넌트 구성을 위해 우선 달력날짜는 7열6행을 기본으로 달력에 표시할 날짜를 생성할 함수를 만든다. 달력을 만들 때는 단 한가지 간단한 규칙만 지키면 수월하다. `Date`객체는 1월을 0으로 표시하기에 0~11월이 있다는 것이다. 함수에서는 0~11을 사용하여 계산하고 표기할 때만 달에 +1을 하여 표시해야 한다. 별거 아니지만 대충 생각하다간 나중에 고생할 수 있다. 달력 날짜 생성 함수는 gpt가 잘 만들어준다.

스크린샷 2023-12-04 23.10.44.png 달력 요일과 날짜 배치

날짜 생성함수를 얻었으면, 적당한 scale을 잡고 요일과 날짜만 배치한다면 달력 구현의 반은 끝난 셈이다. x축은 일곱 개의 열을 담당하고 y축은 6개의 행을 담당한다. (0,0)은 우측 상단에 위치하여 날짜 생성함수가 가르키는 위치에 하나하나 배치하면 된다. 최상단엔 -1로 요일을 설정하고, 0열부터는 날짜를 배치한다. 이 때, 각 날짜는 group으로 묶어줘야 이후에 효과 요소(선택 표시, 범위 표시 등)를 넣기 수월하다.


이번 달력 구현에서 선택된 날짜에 효과를 줄 때, 먼저 그려놓은 opacity가 0인 `rect`에 사용자 상호작용으로 선택된 날짜의 `rect`의 opacity를 1로 주면서 날짜가 선택된 효과를 주었다. 마우스 오버에도 같은 방법으로 효과를 입혔다. 여기서는 비교적 더 간단한 마우스 오버 효과를 소개한다.

스크린샷 2023-12-04 23.25.43.png hover 동작에서 미리 그려놓은 rect의 opacity를 1로 주며 마우스 오버를 나타냄.

이제 달력의 주 기능인 시작/종료일 설정 기능을 넣을 차례다. 범위 날짜 지정은 클릭 이벤트 안에서 몇가지 예외처리와 함께 구현해주면 된다.

스크린샷 2023-12-04 23.30.39.png click event

위 코드에서 `setter`함수는 calendar component가 전달받은 state setter 함수이며 이 함수 안에서 데이터 형식에 신경쓰며 날짜를 비교하거나 경계조건을 고려하여 setter 함수의 state인 시작/종료일의 업데이트 여부를 결정한다.


모달 구현

달력 안에서 모달이 많이 등장한다. 달력 자체를 띄우는 모달, 연도를 띄우는 모달(양쪽 2개), 달을 띄우는 모달(양쪽 두개) 이를 각각 구현할 때 모달이 열렸을 때 클릭 이벤트를 등록하여 닫아주면 쉽게 해결된다.

스크린샷 2023-12-04 23.39.07.png 이벤트캡처링 때문에 모달 작동이 방해받을 수 있으므로 capture: true를 설정해주자.

다만 이벤트를 등록 할 때, 클린업 함수를 등록하고 이벤트 캡처링에 주의 해야한다. click 이벤트는 등록과 동시에 캡처링이 이벤트리스너의 트리거가 되어 모달이 열리지 않을 수 있으므로 `{ capture: true }`와 같이 이벤트 캠처링 문제를 피할 수 있는 방법을 강구해야한다. (setTimeout으로 isOpen의 setter 함수를 이벤트루프에 묶어둘 수도 있다.)


날짜를 배치하고 날짜를 선택할 방법을 마련했다면, 나머지는 선택의 문제이다. 범위 내의 날짜들을 어떻게 지정할지, 시작/종료일 지정에 어떤 추가 조건들을 넣을지는 온전히 개발자의 몫이다.

keyword
작가의 이전글Next.js에서 D3.js 지도 그리기