JavaScript Calendar — 월 이동·이벤트 저장(로컬)

JavaScript Calendar — 월 이동·이벤트 저장(로컬) | NueJong

JavaScript Calendar - 달력 만들기

달력 메인 화면 예시

이번 시간에는 저번에 만들었던 To-do List에서 조금 더 확장한 나만의 달력을 만들어볼까 합니다.
LocalStorage를 활용하여, 일정을 로컬로 저장하고, 확인할 수 있는 기능, 버튼을 활용하여 월 이동, input 태그를 활용하여 월 이동, 날짜 클릭 시 이벤트 추가 및 삭제 등 다양한 기능들을 추가하여 자바스크립트를 처음 시작하시는 초보자분들도 바로 실행하고 따라하실 수 있도록 만들 예정입니다.
HTML부터 자바스크립트 코드까지 자세하게 설명을 첨부하도록 하겠습니다.

달력 실행 화면

코드를 작성하고, 달력을 실행한 화면입니다.
날짜를 클릭하면 우측에 일정을 입력할 수 있고, 일정을 입력하고 저장하면 로컬에 저장되어 파일을 껐다가 다시 실행해도 내용은 그대로 유지됩니다.
추가된 일정 옆에는 삭제 버튼을 두어 바로 지울 수 있도록 하였습니다.
월 단위는 화살표 버튼을 통해 이동할 수 있게 하였습니다.
연, 월 단위 이동은 input 태그를 활용하였습니다.

달력 실행 화면 예시

외형 구조

달력 HTML 코드 작성 첫 번째
달력 HTML 코드 작성 두 번째
  • 크게 상단부와 하단부로 나누었습니다.
  • 상단부에는 제목과 오늘 버튼, 월 이동 버튼, 연/월 이동 input 태그를 두었습니다.
  • 하단부에는 왼쪽과 오른쪽으로 나누었습니다.
  • 왼쪽에는 달력이 위치하고, 오른쪽에는 일정을 입력하고, 삭제할 수 있는 편집창을 두었습니다.

달력 만들기

왼쪽에 위치한 달력을 자바스크립트로 만들어 보겠습니다.
7칸씩 6개줄로 생각하여 42개칸으로 cell수를 정했습니다.
42번 반복문을 돌면서 숫자를 채워나갑니다.
혹시라도 해당 날짜에 일정이 입력되어 있다면, 점을 생성하여 추가합니다.
날짜를 클릭했을 경우에는 오른쪽에 일정을 입력할 수 있는 panel이 활성화되도록 하였습니다.

        
          function render(){
            calendarGrid.innerHTML = '';
            const d = new Date(state.viewDate.getFullYear(), state.viewDate.getMonth(), 1);
            const month = d.getMonth();
            const year = d.getFullYear();
            monthTitle.textContent = d.toLocaleString('default', { month: 'long' }) + ' ' + year;
            yearLabel.textContent = '';

            const startIndex = new Date(year, month, 1).getDay();
            const daysInMonth = new Date(year, month+1, 0).getDate();

            const prevDays = new Date(year, month, 0).getDate();

            const totalCells = 42;

            const events = loadEvents();
            const todayStr = ymd(new Date());

            for(let i=0;i<totalCells;i++){
              const cell = document.createElement('div');
              cell.className = 'cell';
              const offset = i - startIndex;
              let cellDate;
              if(offset < 0){
                const day = prevDays + offset + 1;
                cell.classList.add('outside');
                cellDate = new Date(year, month-1, day);
              } else if (offset >= daysInMonth){
                const day = offset - daysInMonth + 1;
                cell.classList.add('outside');
                cellDate = new Date(year, month+1, day);
              } else {
                const day = offset + 1;
                cellDate = new Date(year, month, day);
              }

              const cellYmd = ymd(cellDate);

              const dayNum = document.createElement('div');
              dayNum.className = 'daynum';
              dayNum.textContent = cellDate.getDate();
              cell.appendChild(dayNum);

              const eDots = document.createElement('div');
              eDots.className = 'dots';
              const todaysEvents = (events[cellYmd] || []);
              for(let k=0;k<Math.min(3,todaysEvents.length);k++){
                const dot = document.createElement('div');
                dot.className = 'dot';
                dot.style.background = k===0? 'var(--accent)' : (k===1? 'var(--accent-2)' : '#a78bfa');
                eDots.appendChild(dot);
              }
              cell.appendChild(eDots);

              if(cellYmd === todayStr) cell.classList.add('today');

              if(state.selected && cellYmd === state.selected) cell.classList.add('selected');

              cell.addEventListener('click', ()=> {
                state.selected = cellYmd;
                if(cell.classList.contains('outside')){
                  const parts = cellYmd.split('-').map(Number);
                  state.viewDate = new Date(parts[0], parts[1]-1, 1);
                }
                render();
                openPanel(cellYmd);
              });

              calendarGrid.appendChild(cell);
            }
          }
        
      

일정 입력

        
          function openPanel(ymdStr){
            panelDate.textContent = (new Date(ymdStr)).toLocaleDateString();
            eventInput.value = '';
            renderEvents(ymdStr);
          }

          function renderEvents(ymdStr){
            const events = loadEvents();
            const list = events[ymdStr] || [];
            eventsList.innerHTML = '';
            if(list.length === 0){
              eventsList.innerHTML = '
등록된 일정이 없습니다.
새 일정을 추가해보세요.
'; return; } list.forEach((ev, idx) => { const el = document.createElement('div'); el.className = 'event'; const left = document.createElement('div'); left.textContent = ev; const right = document.createElement('div'); const del = document.createElement('button'); del.className = 'ghost small'; del.textContent = 'Delete'; del.addEventListener('click', ()=>{ list.splice(idx,1); const all = loadEvents(); all[ymdStr] = list; saveEvents(all); render(); renderEvents(ymdStr); }); right.appendChild(del); el.appendChild(left); el.appendChild(right); eventsList.appendChild(el); }); }

앞서 언급했던 일정을 추가하는 함수인 openPanel()에서는 현재 날짜를 매개 변수로 받아옵니다.
그리고 로컬에 저장되어 있는 데이터를 꺼내와서 뿌려줍니다.
이 때 일정이 여러 개일 경우에는 리스트 형식으로 보여주고, 왼쪽에는 삭제 버튼을 추가합니다.
저장되어 있는 일정 데이터가 없을 경우에는 새로운 일정을 추가하라는 메시지가 나오도록 합니다.

이벤트 핸들러 1

  • 일정을 입력한 후에 추가 버튼을 누르면 해당 날짜에 일정이 추가됩니다.
  • 해당 날짜에 기존 일정이 존재한다면, 로컬에서 일정을 꺼내와서 보여줍니다.
  • 새롭게 등록된 일정은 로컬에 저장합니다.
  • e.key === 'Enter'를 활용하여 일정을 입력 후에 엔터키를 눌러도 저장이 되도록 하였습니다.
        
          addEventBtn.addEventListener('click', ()=>{
            if(!state.selected){
              alert('먼저 날짜를 선택하세요.');
              return;
            }
            const text = eventInput.value.trim();
            if(!text) return;
            const events = loadEvents();
            events[state.selected] = events[state.selected] || [];
            events[state.selected].push(text);
            saveEvents(events);
            eventInput.value = '';
            render();
            renderEvents(state.selected);
          });

          eventInput.addEventListener('keydown', (e)=>{
            if(e.key === 'Enter') addEventBtn.click();
          });
        
      

이벤트 핸들러 2

상단부에 위치한 화살표 버튼을 클릭하면 월간 이동이 되도록 코드를 작성합니다.
today 버튼을 클릭하면 오늘 날짜로 돌아올 수 있도록 기능을 추가하였습니다.

        
          prevBtn.addEventListener('click', ()=>{
            state.viewDate = new Date(state.viewDate.getFullYear(), state.viewDate.getMonth()-1, 1);
            render();
          });
          nextBtn.addEventListener('click', ()=>{
            state.viewDate = new Date(state.viewDate.getFullYear(), state.viewDate.getMonth()+1, 1);
            render();
          });
          todayBtn.addEventListener('click', ()=>{
            state.viewDate = new Date();
            state.selected = ymd(new Date());
            render();
            openPanel(state.selected);
          });
        
      

초보자 핵심 팁

  • 이벤트는 localStorage에 저장되므로 같은 브라우저에서만 보입니다.
  • 다른 컴퓨터, 다른 브라우저에서 현재 파일을 열면 저장된 내용이 보이지 않습니다.
  • 날짜 계산하기가 어려운 편이지만, getMonth(), getFullYear(), Date()를 잘 활용하면 쉽게 코드를 작성하실 수 있습니다.

확장 개발할 수 있는 포인트

  • 로컬이 아닌 서버와 연동하여 데이터 저장(어디서든지 내가 작성한 일정을 볼 수 있습니다.)
  • API를 활용하여 구글 캘린더와 연동해보기
  • 텍스트 뿐만아니라 이미지나 영상도 저장할 수 있게 해보기
  • 알림 기능 추가하기
이번 시간에는 심플한 나만의 달력을 만들어 보았습니다. 구글 캘린더나 네이버 캘린더 등 다양한 클라우드 캘린더들이 있는데요. 그것들을 사용해보면서 다양한 기능들과 옵션들을 나만의 달력에도 적용해보고, 추가해보면 더 좋은 공부가 될 것 같습니다.

댓글 쓰기

다음 이전