[Spring] 작성날짜 경과 시간으로 표시하기 (몇분전, 몇일전, 몇달전)

dondonee·2024년 5월 13일
0
post-thumbnail
post-custom-banner

문제

OKKY를 참고한 게시판을 만들고있다. OKKY의 게시글의 작성일을 보면 구체적 날짜가 아닌 '2분 전', '약 1시간 전', '약 1개월 전' 등으로 작성일로부터 얼마나 지났는지를 표시해주고 있다.


어떻게 하는 지 검색을 해 보니 Javascript 함수로 만든 블로그 글들이 가장 많이 보였다. 그러나 나는 JSP를 사용하고 있기 때문에 어떤 식으로 적용하는 것이 좋을 지 생각해보았다.

  1. Javascript : 컨트롤러에서 Model에 담아 JSTL <c:forEach> 반복문을 통해 게시글 목록을 출력하고 있기 때문에 적합하지 않다. JSP(JSTL) 동작이 완료된 뒤에 브라우저에서 Javascript가 동작하기 때문에 둘을 결합시키는 것이 적절하지 않아보였다.

  2. JSP 스크립트릿 : <%! %> 안에 getElapsedTime()메서드를 선언하고 <%= getElapsedTime(${post.createdDate}) %>와 같이 메서드를 선언할 수 있다고 생각했으나... JSTL 태그를 통해 메서드에 파라미터를 전달할 수가 없었다.

  3. 커스텀 JSTL 태그 : JSTL 태그 값을 사용하려면 커스텀 태그 라이브러리를 추가해야겠다고 생각했다. 그런데 나는 JSP 사용을 권장하지 않는 스프링 부트를 사용하고 있기 때문에 정보가 부족했다. 다른 방식을 찾는게 좋을 것 같았다.

  4. 컨트롤러에서 변환 : 작성일을 '5분 전'과 같은 형식으로 표시하는 것은 View의 역할이기 때문에 컨트롤러에서 변환해서 전달하는 것은 좋아보이지 않았다. 또한 다른 View에서는 동일한 값을 다른 형식으로 표시하고 싶을 수도 있기 때문이다. 컨트롤러는 원본을 주는 것이 맞다고 생각해서 이 방법은 소거했다.

  5. (해결!) Public 메서드 선언 : 사실 문제는 간단했다. 생각해보니 JSP에서 Member와 같은 클래스도 import도 가능하고 메서드도 쓸 수 있었다. 그래서 web 패키지에 JspFunction 클래스를 만들고 그 안에 getElapsedTime() 메서드를 선언했다.



과정

기간 계산하기 (Timestamp)

Javascript 예시에서는 날짜가 millisecond timestamp 형식이었다. 그런데 나는 DB에 작성일(createdDate)을 LocalDateTime 타입으로 저장하고 있었기 때문에 timestamp 형식으로 변환해야 했다.

Timestamp는 협정 세계시(UTC) 1970년 1월 1일 자정을 기준으로 얼마나 많은 초(혹은 밀리초)가 지났는 지를 표시한다. 음수를 사용하여 그 이전의 날짜도 표기할 수 있다. 단순한 숫자이기 때문에 기간을 계산하기에 편리하다. Epoch Time 이라고도 한다.

LocalDateTime은 어느 지역을 기준으로 했는지 타임존(time zone)에 대한 정보가 없기 때문에 epoch time으로 직접 변경할 수가 없다. 따라서 ZonedDateTime 형식으로 변환하고 타임존 정보를 주어야 한다. atZone()을 통해 타임존을 설정할 수 있고, toEpochSecond()로 epoch time으로 변경할 수 있으며 반환 타입은 long이다.


public class JspFunction {

    public String getElapsedTime(LocalDateTime createdTime) {

        LocalDateTime now = LocalDateTime.now();
        long nowMS = now.atZone(ZoneId.of("Asia/Seoul")).toEpochSecond();
        long createdMS = createdTime.atZone(ZoneId.of("Asia/Seoul")).toEpochSecond();
    }
}

경과 시간 계산하기 (초, 분, 시)

long seconds = nowMS - createdMS;
if (seconds < 60) {
    return "방금 전";
}

long minutes = seconds / 60;
if (minutes < 60) {
    return minutes + "분 전";
}

long hours = minutes / 60;
if (hours < 24) {
    return hours + "시간 전";
}

Javascript 코드의 경우 timestamp 타입이 밀리초 단위이기 때문에 1000으로 나누어 주었지만 Java는 초 단위이기 때문에 생략해도 된다.


경과 시간 계산하기 (주, 개월, 년)

long days = hours / 24;
long weeks = ChronoUnit.WEEKS.between(createdTime, now);
long months = ChronoUnit.MONTHS.between(createdTime, now);
long years = ChronoUnit.YEARS.between(createdTime, now);

if (days < 7) {
    return days + "일 전";
}

if (months < 1) {
    return "약 " + weeks + "주 전";
}

if (months < 12) {
    return "약 " + months + "개월 전";
}

return "약 " + years + "년 전";

시간을 계산하는 것 까지는 문제가 없지만, 주 단위부터는 문제가 생긴다. 달마다 일수가 다르고 매 해의 일수도 딱 떨어지는게 아니기 때문이다.

다행히도 ChronoUnit 클래스로 아주 쉽게 문제가 해결된다. between() 메서드를 사용하면 주, 개월, 년 단위로 기간을 쉽게 계산할 수 있다.



완성

JSP 출력

<jsp:useBean id="customFn" class="com.knou.board.web.JspFunction"/>

<jsp:useBean>액션태그를 통해 JspFunction 클래스를 JSP 파일로 가져온다.


<ul class="list-group list-group-flush list-unstyled">
    <c:forEach var="post" items="${posts}">
        <li class="py-3 border-top text-decoration-none">
            <div class="x-text-sm">
                <a href="#">${post.author.nickname}</a>
                <span class="mx-1">·</span>
                <span>${customFn.getElapsedTime(post.createdDate)}</span>
            </div>
            <div class="my-2">
                <a href="#"><span class="x-font-semibold">${post.title}</span></a>
            </div>
            <div class="d-flex justify-content-end x-text-md" style="line-height: 1.2rem">
                <i class="me-1 bi bi-eye" style="font-size: 1.2rem"></i><span
                    class="x-text-sm">${post.viewCount}</span>
            </div>
        </li>
    </c:forEach>
</ul>

${customFn.getElapsedTime(post.createdDate)}와 같이 JSTL을 사용하여 간편하게 출력 성공하였다.



JS 버전 함수

function getElapsedTime(createdTime) {
    const now = new Date();
    const created = new Date(createdTime);

    const seconds = Math.floor((now - created) / 1000); // Milliseconds -> Seconds

    if (seconds < 60) {
        return "방금 전";
    }

    const minutes = Math.floor(seconds / 60);
    if (minutes < 60) {
        return minutes + "분 전";
    }

    const hours = Math.floor(minutes / 60);
    if (hours < 24) {
        return hours + "시간 전";
    }

    const days = Math.floor(hours / 24);
    const weeks = Math.floor(days / 7);
    const months = Math.floor(days / 30.43685); // 한 달은 약 30.43685일
    const years = Math.floor(months / 12);

    if (days < 7) {
        return days + "일 전";
    }

    if (months < 1) {
        return "약 " + weeks + "주 전";
    }

    if (months < 12) {
        return "약 " + months + "개월 전";
    }

    return "약 " + years + "년 전";
}

Ajax 요청에 의한 응답 데이터를 가지고 View 처리를 하는 경우 <jsp:useBean>을 사용할 수 없다. 따라서 똑같은 기능을 하는 Javascript 함수를 만들어주었다.

Java의 toEpochSecond()는 EpochTime을 초 단위로 만들어주는 반면 Javascript의 Date 객체는 밀리초 단위의 EpochTime을 갖는다. 따라서 초 단위로 계산하기 위해서는 1000으로 나누어주는 단계가 필요했다.

또한 두 날짜 사이의 년수 및 개월수 계산의 경우 Java에서는 ChronoUnit을 사용했는데 JQuery에서는 이러한 기능을 제공해주는 메서드가 없는 것 같았다. 어차피 정확한 계산이 필요하지 않기 때문에 days를 월의 평균 일수로 나누어 months를 대략적으로 계산하였다. 월의 평균 일수는 1년 365.2422일을 12개월로 나누어 구했다.



궁금점

작성일을 경과 시간으로 표시해주는 것은 날짜 그대로 표시하는 것보다 얼마나 지난 글인지 직관적으로 알 수 있다는 점이 좋은 것 같다.

다만 OKKY는 글 상세보기를 해도 날짜가 표시되지 않는다. 게시글의 날짜를 정확하게 알아야 할 이유는 딱히 없지만 그래도 있는 것이 사용자 입장에서는 좋다. 너무 오래되어서 '21년 이상 전'과 같은 글을 보면 날짜가 너무 두루뭉술해서 답답함이 느껴지기도 한다.

아마도 DB와 사용자의 타임존 문제 때문이 아닐까 한다. 앞서 말한 것처럼 사용자가 게시글의 정확한 날짜를 알 필요도 없고 개발자 입장에서는 타임존을 통일하면 편하기 때문 아닐까...?




🔗 References

post-custom-banner

0개의 댓글