javascript Date() 전전달 말일, 다음달 말일, 다음주, 윤년, 만나이, 경과 날짜 수, 날짜 연산하기

자바스크립트로 고급 날짜 이동 기능을 구현하는 방법을 배웁니다. 전월 말일, 전전월 말일, 다음달 말일, 윤년 계산, 만나이 계산, 두 날짜 사이의 날짜 차이, 두 날짜의 차이를 원하는 단위로 출력하는 방법을 배울 수 있습니다.

Date 객체의 기본적인 사용법과 날짜를 문자열로 포매팅 출력을 하는 기초 사용 방법은 다음 글을 먼저 보시기 바랍니다.

>> javascript Date() 현재 날짜, 오늘, 어제, 전월 말일 날짜 계산과 출력 포매팅

날짜 계산에 사용하는 Date 객체의 주요 함수

  • getFullYear() : 4자리 년도 반환
  • getMonth() : 월 반환. 1월이 0에서 시작하며, 12월은 11이 됨.
  • getDate() : 일 반환.
  • setFullYear() : 4자리 정수로 년도 설정
  • setMonth() : 월 설정
  • setDate() : 일 설정

그밖에 요일 함수가 더 있습니다. setDate()/getDate()와 혼동하지 않도록 주의해야 합니다.

  • getDay() : 요일을 정수 값으로 반환. 0이 일요일, 6이 토요일이 됨.
  • setDay() : 요일을 정수 값으로 설정.

getDay()/setDay(), getMonth()/setMonth() 함수는 0에서 시작하는 값을 반환합니다. 주의해야 합니다.

2자리 정수 년도 값을 얻거나 설정하는 getYear()/setYear() 함수는 사라질 예정이므로 사용하면 안됩니다.

처음 언급한 함수 6개만 사용해서 날짜 계산을 하게 됩니다. 또한 날짜 함수들은 반환값이 밀리세컨드 정수 값이기 때문에 함수 체인을 사용할 수 없습니다.

가능은 하지만 날짜 객체를 반복해서 생성해야 해서 런타임 코스트가 올라가고, 사이드 이펙트가 발생할 수 있기 때문에 여러 행으로 나눠서 각각 사용합니다.

let d = new Date()
d.setDate(1)
d.setMonth(d.getMonth()+2)

앞서처럼 두 줄로 구현한 코드를 한 줄로 구현하면 다음처럼 작성할 수도 있습니다. 코드량이 줄어들지도 않을 뿐더러 실행 속도면에서도 잇점이 없습니다.

let d = new Date()
d = new Date(new Date(d.setDate(1)).setMonth(d.getMonth()+2))

이달 말일, 전달 말일, 전전달 말일, 다음달 말일 구하기

월을 이동해 말일을 구할때는 항상 간접 방식으로 구해야 합니다. 말일은 28, 29, 30, 31일 중 하나가 될 수 있기 때문에 하나의 값으로 날짜를 특정할 수 없습니다.

그래서 항상 고정된 "1일의 전날"을 구하는 방식으로 말일을 구하게 됩니다.

setDate() 함수에 인자 값으로 0을 넣으면 현재 날짜의 전일이 되는 특징을 이용해서 말일을 구하게 됩니다.

먼저 이달 말일을 구해보겠습니다.

날짜를 1일로 먼저 바꾸는 것은 앞서 설명했지만 말일이 가변이기 때문입니다. 오늘이 3월 31인데 setMonth()로 1달을 더하면 4월 31일이 됩니다. 4월 31일은 존재하지 않는 날짜이고 4월 말일인 4월 30일의 다음날, 그러니까 5월 1일이 됩니다.

5월 1일의 전날을 구하면 4월 30일이 되면서 이달 말일 아닌 다음달 말일이 최종 날짜가 되게 됩니다. 혼동하지 않도록 주의해야 합니다.

let d = new Date()
console.log(d.toLocaleDateString())
d.setDate(1) // 이달 1일
d.setMonth(d.getMonth()+1) // 다음달 1일
d.setDate(0) //다음달 1일의 전일인 이달 말일
console.log(d.toLocaleDateString())
----------------------------------
> 2024. 6. 6.
> 2024. 6. 30.

보기는 쉽지만 코드가 너무 길어지는 문제가 있습니다. Date 객체를 생성할 때 인자 3개(년, 월, 일)을 넣어서 날짜 객체를 생성하는 방법을 이용해 코드를 조금 더 간결하게 다음처럼 만들 수 있습니다.

앞으로는 이 방법을 사용한다고 생각하면 됩니다.

let d = new Date()
d = new Date(d.getFullYear(), d.getMonth()+1, 0)
console.log(d.toLocaleDateString())
----------------------------------
> 2024. 6. 30.

날짜 입력 인자 값으로 "0"을 넣는 것이 이 방식의 핵심입니다. 인자 값으로 생성되는 날짜는 "2024년 7월 0일"이 되면서 6월 말일이 Date 객체의 날짜 값이 됩니다.

같은 방법으로 다음 달 말일은 "Date(d.getFullYear(), d.getMonth()+2, 0)"로 표현하면 됩니다. 월을 입력하는 두 번째 인자값만 바꾸면 됩니다.

전월, 전전월은 반대로 월 입력 값만 바꾸면 됩니다. "Date(d.getFullYear(), d.getMonth(), 0)"는 전월 말일이 됩니다. 전전월 말일은 "Date(d.getFullYear(), d.getMonth()-1, 0)" 이 됩니다.

이런 트릭을 이용하면 작년 12월 31일은 1월 1일의 전날이 되므로 "Date(d.getFullYear(), 0, 0)"가 됩니다.

let d = new Date()
d = new Date(d.getFullYear(), 0, 0)
console.log(d.toLocaleDateString())
----------------------------------
> 2023. 12. 31.

윤년(윤달) 처리

말일을 구할 때는 날짜에 0을 입력해서 손쉽게 가변인 말일을 구했지만, 다음 달 오늘이 되면 문제가 조금 달라집니다.

앞서 언급한 문제인 오늘이 3월 31이고 다음달 오늘을 구한다고 하면 실제로는 4월 31일이 아닌 5월 1일이 되게 됩니다. 따라서 4월의 마지막 날인 4월 30일로 보정을 하는 과정이 필요합니다.

윤달이 있는 경우에는 반대의 경우가 생깁니다. 1월 29일의 다음달 오늘은 2월 29일이 되어어 2월 28일로 보정을 해야 하지만 윤년에는 2월 29일로 사용해야 합니다.

년도를 바꿔서 작년 오늘 날짜를 적용할 때도 동일합니다. 올해가 윤년이고 2월 29일이면 작년 오늘을 구하기 위해 "Date(d.getFullYear()-1, d.getMonth(), d.getDate())를 하면 작년 3월 1일이 되게 됩니다.

우리가 구현하는 기능의 대부분은 이런 문제가 생기게 되면 말일로 끌어당겨서 보정을 하는 처리를 해야 합니다. 그래야 말도 안되는 기능상의 오류가 생기는 것을 막을 수 있습니다.

let d = new Date()
console.log(d.toLocaleDateString())
let dd = new Date(d.getFullYear(), d.getMonth()+1, d.getDate())
console.log(dd.toLocaleDateString())
d = dd.getMonth() === d.getMonth()+1 ? dd : new Date(dd.getFullYear(), dd.getMonth(), 0)
console.log(d.toLocaleDateString())
----------------------------------
> 2023. 03. 31.
> 2023. 05. 01.
> 2023. 04. 30.

3월 31일인 날짜 객체를 조작하지 않고 dd 변수에 이동하는 다음달 오늘 날짜 객체를 만든 수 원래 객체의 월+1이 dd 객체의 월과 같은지 비교를 해서 같으면 dd가 정상 날짜이므로 d에 대힙을 하고, 같지 않으면 다음 다음 달로 날짜가 넘어간 것이므로 다음 달 말일로 날짜를 끌어 당겨오는 처리를 해야 합니다.

날짜 객체에 addMonth() 이름으로 된 프로토타입 함수를 하나 만들어서 윤년과 가변 말일이 반영된 날짜가 설정되는 월 이동 함수를 만들 수 있습니다.

0, 양수, 음수, 그리고 +/-12의 값을 모두 처리할 수 있어야 합니다.

Date.prototype.addMonth = function(moveMonth){
    if(moveMonth === 0) return this // 이동 없으면 불필요한 실행 없이 리턴
    let dd = new Date(this.getFullYear(), this.getMonth()+moveMonth, this.getDate()) //가변 말일, 윤년 보정 안된 날짜 객체

    let mm = (this.getMonth()+moveMonth)%12
    mm = Math.abs(mm+(mm < 0 ? 12:0)) // 가변 말일, 윤년 보정된 월

    // 년, 월, 일 설정  
    this.setFullYear(dd.getFullYear())
    this.setMonth(dd.getMonth())
    if(dd.getMonth() !== mm){
        this.setDate(0) // 월 보정
    }
    return this
}
let d = new Date(2024, 0, 31)
console.log(d.addMonth(-4).toLocaleDateString())
----------------------------------
> 2023. 9. 30

구현한 함수는 날짜 객체를 반환하도록 해서 Date 객체의 내장 함수들을 체인으로 연결해서 사용할 수 있도록 했습니다.

만 나이 구하기

만 나이 계산은 생일을 기준으로 오늘 날짜와 년, 월, 일 값을 비교해서 계산합니다. 조건은 2가지를 체크해야 합니다.

  1. 생일 월이 지났나?
  2. 월이 같으면 생일 날짜가 지났나?

생일 날짜를 "YYYY-MM-DD" 포맷 문자열 인자 값으로 받아서 계산한 만 나이를 정수로 반환하는 함수를 하나 만들어서 재사용 할 수 있도록 하겠습니다.

function calcAge(day10){
    let birthday = new Date(day10)
    let today = new Date()
    // 년도 계산 나이에서 생일 월/일이 오늘 날짜보다 작으면 1살을 뺌
    return today.getFullYear() - birthday.getFullYear() + (today.getMonth() - birthday.getMonth() < 0 ? -1 : 0) + (today.getMonth() === birthday.getMonth() && today.getDate() - birthday.getDate() < 0 ? -1 : 0)
}
console.log(calcAge('2001-06-08'))
----------------------------------
> 23

두 날짜 사이 경과 날짜 수 구하기

언뜻 앞서의 나이 구하는 방식으로 년/월/일 차이로 구하면 될 것 같지만, 윤달이나 제각각인 월별 날짜 때문에 불가능합니다.

Date 객체의 내장 함수인 getTime() 함수는 날짜 객체가 가지고 있는 밀리세컨트 시간 값을 반환합니다. 이 값은 1970-01-01 00:00:00.000 부터 경과한 밀리세컨드 값입니다.

이 값의 차이를 가지고 날짜 수를 역산을 하게 됩니다.

function calcBetweenDay(startday, endday){
    let sd = new Date(startday)
    let ed = new Date(endday)
    return Math.ceil(Math.abs(sd - ed) / (24 * 60 * 60 * 1000)) // 1일 밀리세컨드 값으로 나눈 후 자투리 시간도 1일로 계산되도록 올림 처리를 함
}
console.log(calcBetweenDay('2024-05-01','2024-06-07')) // 37일
----------------------------------
> 37

날짜 차이 계산하기

앞서 만들었던 경과 날짜 수를 계산하는 방식과 유사하게 두 날짜 사이의 차이를 계산하는 범용 함수를 만들 수 있습니다.

두 날짜/시간 값 사이의 차이를 일, 시간, 분, 초 단위로 환산한 값을 얻을 수 있도록 추가 파라미터로 반환할 환산 단위를 정해줍니다.

파라미터로 받는 날짜/시간 값은 Date 객체로 받습니다. 문자열도 받아도 내부에서 Date 객체로 변환해주면 되지만, 편의상 Date 객체로 받아서 처리합니다.

function calcTimeDifference(do1, do2, dtype){
    // 출력 타입은 문자 1개로 d:날짜, h:시간, m:분, s:초 로 구분해서 타입에 맞게 환산해서 반환
    let divider = ('dhms'.indexOf(dtype) >= 0 ? 1000 : 1) * ('dhm'.indexOf(dtype) >= 0 ? 60 : 1) * ('dh'.indexOf(dtype) >= 0 ? 60 : 1) * ('d' == dtype ? 24 : 1)
    console.log(divider)
    return divider > 1 ? Math.round(Math.abs(do1.getTime() - do2.getTime()) / divider) : 0 // 소수점 값은 반올림하며, 출력 타입이 없으면 0 반환
}
let day1 = new Date('2024-06-01 02:45:17')
let day2 = new Date('2024-06-08 23:05:10')
console.log(calcTimeDifference(day1, day2, 'h'))
----------------------------------
> 188

윤년 판단하기

윤년을 판단하는 방법은 알려진 함수가 있습니다. 4로 나누어 떨어지지만 100으로는 나누어 떨어지지 않거나, 400으로 나누어 떨어지면 윤년이 됩니다.

function leapYear(year4)
{
    return ((year4 % 4 == 0) && (year4 % 100 != 0)) || (year4 % 400 == 0);// 윤년이면 true, 아니면 false
}
console.log(leapYear(2024))
----------------------------------
> true

윤년은 2월 29일이 있는 해입니다. 꼼수지만 다음처럼 날짜 객체로 확인하기도 합니다. 윤년이면 2월 29일이 있으므로 getMonth() 반환 값이 1(2월)이 됩니다.

function leapYear2(year4)
{
    let d = new Date(year4,1,29)
    return d.getMonth() == 1;// 윤년이면 true, 아니면 false
}

지난주/다음주 구하기

같은 요일인 전주/다음주, 또는 전전주와 같은 개별 날짜의 이동은 setDate() 함수로 일주일인 7*"이동할 주수" 만큼 더하거나 빼면 됩니다.

let nextWeekDay = new Date(d.getFullYear(), d.getMonth(), d.getDate()+7) // 지난주 같은 요일
let prevWeekDay = new Date(d.getFullYear(), d.getMonth(), d.getDate()-7) // 다음주 같은 요일
console.log(prevWeekDay.toLocaleDateString(), nextWeekDay.toLocaleDateString())
----------------------------------
2024. 5. 31. 2024. 6. 14.

전주, 또는 다음 주 일주일의 시작과, 끝 날짜를 구하려면 getDay() 함수를 사용해서 오늘이 주의 어떤 요일인지를 알고, 이 값을 사용해 다음주(월~일), 전주(월~일)의 시작과 끝 날짜를 구합니다.

let pWeekStart = new Date(d.getFullYear(), d.getMonth(), d.getDate()-(d.getDay()+7+1)) // 전주 월요일
let pWeekEnd = new Date(d.getFullYear(), d.getMonth(), d.getDate()-d.getDay()) // 전주 일요일
console.log(pWeekStart.toLocaleDateString(), pWeekEnd.toLocaleDateString())

let nWeekStart = new Date(d.getFullYear(), d.getMonth(), d.getDate()+(7-d.getDay()+1)) // 다음주 월요일
let nWeekEnd = new Date(d.getFullYear(), d.getMonth(), d.getDate()+14-d.getDay()) // 다음주 일요일
console.log(nWeekStart.toLocaleDateString(), nWeekEnd.toLocaleDateString())
----------------------------------
2024. 5. 25. 2024. 6. 2.
2024. 6. 10. 2024. 6. 16.