통계 자료를 주 단위로 정리하여 차트로 보여 주고 있었는데 주별 자료를 보여 줄 때에 자꾸 몇 월 몇 주 차인지 알려주는 인덱스가 들쭉날쭉 어색하게 표기되고 있으니 수정해 달라는 요구사항을 받았다. 해당 코드는 다른 사람이 짜둔 부분이라 어떻게 구현되어 있었는지를 나도 그제서야 처음 봤는데, 해당 월 + 1일의 요일을 기준으로 단순 계산만 하고 있어서 일부 케이스에서는 '6월 6주차'와 같은 식으로 어색하게 계산되고 있었다.
이를 개선하기 위해서는 주 차 계산 방식의 기준부터 명확히 해야겠다는 생각이 들어서 ISO 표준을 따르기로 했다. ISO 8601의 정의에 따르면 한 해의 주 차 계산 방식은 다음과 같았다.
ISO 8601 표준
- 한 주는 월요일로 시작해서 일요일로 끝난다.
- 한 해의 첫 주는 한 주의 일 대부분(4일 이상)을 가진 첫번째 주로 정의한다. (= 첫번째 목요일이 존재하는 주, 1월 4일이 존재하는 주, ...)
ISO 8601 표준은 연도 별 주 계산만 정의하고 있긴 했지만, 위와 같이 계산하는 것이 일상에서도 일반적이므로 이에 맞게 한 달의 주 차 계산 방식을 정리하였다.
- 한 주는 월요일로 시작해서 일요일로 끝난다.
- 한 달의 첫 주는 첫번째 목요일이 존재하는 주로 정의한다. (= n월 4일이 존재하는 주, 4일 이상을 가진 첫번째 주, ...)
한 해에 대한 정보를 알려 주는 ISO week(e.g. 2024-02-23
은 2024년의 8주 차)를 계산하는 라이브러리는 많았는데, 월 별 주 차를 알려 주는 라이브러리는 잘 보이지 않아 호다닥 구현해 보기로 했다.
참고사항: 해당 프로젝트에서 이미 Day.js를 사용하고 있었으므로 나도 이를 이용하여 쉽게 계산하도록 구현했다.
const date = dayjs(target); // 계산할 날짜를 DayJS 객체로 변환 let targetMonth = date.month(); // 계산할 날짜의 월 정보 (0-11) const targetDate = date.date(); // 계산할 날짜의 일 정보 (1-31) // 월 시작일(n월 1일)의 요일 정보 (0-6, 0이 일요일) const startWeekDay = date.startOf("month").day(); // 월 시작일의 요일을 기반으로 단순 계산한 주 차 (0-6) const originalWeek = Math.ceil((targetDate + startWeekDay - 1) / 7); // 차후 보정치를 반영한 originalWeek + weekCorrection을 return let weekCorrection = 0;
해당 날짜로만 단순히 계산할 수 있는 정보는 위와 같고, 이제 이를 기반으로 각 상황에 맞게 실제 주 차 정보를 계산해 보자.
월 시작일(n월 1일)이 월요일~목요일이라면, 월을 시작하는 주가 해당 월(n월)의 1주 차가 된다. 이외의 경우, 월을 시작하는 주는 지난 달(n-1월)의 마지막 주 차가 된다.
해당하는 케이스를 달력으로 확인해 보면 다음과 같다.
Mo Tu We Th Fr Sa Su | Mo Tu We Th Fr Sa Su ~ Mo Tu We Th Fr Sa Su
1 2 3 4 5 6 7 : Week 01 | 1 2 3 4 5 6 ~ 1 2 3 4 :(1) Week 01
8 9 10 11 12 13 14 : Week 02 | 7 8 9 10 11 12 13 ~ 5 6 7 8 9 10 11 :(2) Week 02
15 16 17 18 19 20 21 : Week 03 | 14 15 16 17 18 19 20 ~ 12 13 14 15 16 17 18 :(3) Week 03
22 23 24 25 26 27 28 : Week 04 | 21 22 23 24 25 26 27 ~ 19 20 21 22 23 24 25 :(4) Week 04
29 30 31 : Week 01 | 28 29 30 31 ~ 26 27 28 29 30 31 :(5) Week 05/01
(*(n)
= originalWeek
값, Week n
= 실제 주 차 정보)
1주~4주 차까지는 단순 계산한 originalWeek
값과 동일하지만, 마지막 주의 경우 월의 종료일이 무슨 요일인지에 따라 [n월 5주]인지 [n+1월 1주]인지 달라진다. 따라서 originalWeek
가 5라면 월의 종료일이 월요일~수요일인지를 확인하여 수치를 보정해 주면 된다.
if (startWeekDay >= 1 && startWeekDay <= 4) {
if (originalWeek === 5) { // 마지막 주라면,
const endWeekDay = date.endOf("month").day(); // 월 종료일
if (endWeekDay >= 1 && endWeekDay <= 3) { // 월요일-수요일
targetMonth = date.add(1, "month").month(); // n+1월
weekCorrection = -4; // 1주 차 (5 - 4)
}
}
}
일요일인 경우 고려해야 할 사항 조건이 오히려 없어 별도로 분리했다. 해당하는 케이스를 달력으로 확인해 보면 다음과 같다.
Mo Tu We Th Fr Sa Su
1 :(0) Week 04/05
2 3 4 5 6 7 8 :(1) Week 01
9 10 11 12 13 14 15 :(2) Week 02
16 17 18 19 20 21 22 :(3) Week 03
23 24 25 26 27 28 29 :(4) Week 04
30 31 :(5) Week 01
(*(n)
= originalWeek
값, Week n
= 실제 주 차 정보)
1일의 경우 originalWeek
계산 식이 Math.ceil((1 + 0 - 1) / 7)
이므로 0이 되는데, 1일은 무조건 지난 달(n-1월)의 마지막 주 차가 되고, 2일부터 1주 차가 시작된다. 또한, 2월이 아니라면 월의 종료일이 30일 또는 31일일 텐데, 이는 무조건 다음 달(n+1월)의 첫 주 차가 된다. 이외 1주~4주 차까지는 i)과 마찬가지로 단순 계산한 originalWeek
값과 동일하다.
else if (startWeekDay === 0) {
if (originalWeek === 0) { // 1일이라면, 무조건 지난 달의 마지막 주
const lastDateOfPreviousMonth = date.subtract(1, "month").endOf("month"); // 지난 달(n-1월)의 종료일
const { week } = getWeekOfMonth(lastDateOfPreviousMonth); // 지난 달의 마지막 주 계산
targetMonth = lastDateOfPreviousMonth.month();
weekCorrection = week; // (0 + week)
}
else if (originalWeek === 5) { // Week 04를 넘는 주라면, 무조건 다음 달의 첫 주
targetMonth = date.add(1, "month").month(); // n+1월
weekCorrection = -4; // 1주 차 (5 - 4)
}
}
해당하는 케이스를 달력으로 확인해 보면 다음과 같다.
Mo Tu We Th Fr Sa Su | Mo Tu We Th Fr Sa Su
1 2 3 | 1 2 :(1) Week 04/05
4 5 6 7 8 9 10 | 3 4 5 6 7 8 9 :(2) Week 01
11 12 13 14 15 16 17 | 10 11 12 13 14 15 16 :(3) Week 02
18 19 20 21 22 23 24 | 17 18 19 20 21 22 23 :(4) Week 03
25 26 27 28 29 30 31 | 24 25 26 27 28 29 30 :(5) Week 04
| 31 :(6) Week 01
(*(n)
= originalWeek
값, Week n
= 실제 주 차 정보)
시작하는 주는 무조건 지난 달(n-1월)의 마지막 주 차가 되고, 그 이후에는 originalWeek
값보다 1씩 작은 값이 실제 주 차가 된다. 단, 1일이 토요일이고 31일까지 있다면 31일은 월요일이므로 예외적으로 다음 달(n+1월)의 첫 주가 된다.
else {
if (originalWeek === 1) { // 시작하는 주라면, 무조건 지난 달의 마지막 주
const lastDateOfPreviousMonth = date.subtract(1, "month").endOf("month"); // 지난 달(n-1월)의 종료일
const { week } = getWeekOfMonth(lastDateOfPreviousMonth);
targetMonth = lastDateOfPreviousMonth.month(); // 지난 달의 마지막 주 계산
weekCorrection = week - originalWeek; // (orinigalWeek + (week - originalWeek))
}
else if (originalWeek === 6) { // Week 04를 넘는 주라면, 무조건 다음 달의 첫 주
targetMonth = date.add(1, "month").month(); // n+1월
weekCorrection = -5; // 1주 차 (6 - 5)
}
else { // 이외에는 일괄 `originalWeek` 값보다 1씩 작은 값이 실제 주 차
weekCorrection = -1;
}
}
}
return { month: targetMonth + 1, week: originalWeek + weekCorrection };
마지막으로 실제 값을 return하기만 하면 되는데, Day.js의 month()
함수는 월을 0-11
의 범위로 알려 주므로 나중에 바로 사용하기 편하도록 1씩 더해 주었고, 주 차 정보는 기존에 생각해 둔 대로 단순 계산 값에 보정치를 더한 originalWeek + weekCorrection
를 return 하였다.
+) 사족
이런 저런 케이스를 고려하며 골머리를 썩이다 끝내고 나니 신나서 코드를 조금 정리해 npm 배포까지 해 봤는데, 예전에 사내용으로 private 배포했던 기억 더듬어가면서 했는데 생각보단 간단한데 생각보단 신경쓸게 많아서 이 내용도 나중에 정리해 봐도 괜찮을 것 같다는 생각이 들었다.
그나저나 일단 냅다 배포는 했는데 사용량... 나오긴 할까 ㅋㅋㅋㅋ 아니 이런 요구사항 받는 사람 별로 없나요 왜 서치해도 뭐가 잘 안 나와서 날 힘들게 해 ㅠㅠ