X시간전, X분전, X초전 실시간 시간표현 자바스크립트 구현하기. While이 에 작성. 3,406번 읽힘.
최근 SNS의 영향으로 많은 사이트에서 댓글이나 간단한 게시글의 작성 시간을 표현할때 몇시간전, 몇분전, 몇초전 등의 훨씬 인식하기 쉬운 시간표현을 하고 있다. 저번에 썼던, ‘최근에 내가 만드는 CMS’에서도 시간 표현에 그런 표현을 아주 많이 썼다. 많이 쓰다보니 쉽게 적용할 방법이 필요했고 그러다보니 쉽게 함수로 만들었다.
이 함수는 한 6개월전에 만들었다. 첨에 이런 함수를 쓰는 법에 대한 가이드 같은게 구글링을 해봐도 어디도 없었다. 지금도 별로 보이지 않는 것 같다. 그래서 이것저것 생각을 해봐서 제일 편하게 쓸 수 있는 방법이 span 태그에 timestamp 라는 attribute 를 추가해서 하는게 편할 것 같아서 이렇게 만들었다. 이것저것 수정을 하다보니 소스는 조금 조잡해져서 수정이 필요하다. 타 라이브러리에서 범용성을 위해 지원하는 함수들을 의존하면서 구현한거라 성능이 조금 구리긴하다. 그래도 우선 지금 쓰는 스크립트 정도는 공개 한다.
원리
<span class="_timestamp" timestamp="1312265402">2011년 8월 2일 15시 10분 02초</span>
위와 같이 html을 입력하면 자동으로 몇초전 등의 표현으로 바뀐다. 자바스크립트로 바꾸는 것이며 검색엔진 봇이 인식 할 수는 없는 방식이다. 그렇기 때문에 span 태그의 innerHTML에 저렇게 실제 날짜 표현을 해줘야 검색엔진 크롤러 봇들이 제대로 시간을 퍼갈 수 있다.
몇초전 등의 표현으로 바뀌는 원리는, 우선 setInterval 등의 함수로 페이지내에서 시간을 계산해주는 함수가 정기적으로 실행된다. 그 함수는 현재 페이지에서 ‘span._timestamp’라는 요소가 존재하는지 찾고, 존재하면 그 요소의 ‘timestamp’라는 Attribute(속성)이 있는지 한번 더 찾는다. 속성이 있으면 timestamp 를 기반으로 현재 시간과 비교하여 몇초 다른지 몇시간 다른지 계산을 해준다. 계산을 하면 그 span._timestamp 에 innerHTML을 몇초전 등의 시간으로 표현해준다.
의존성 함수
jQuery
jQuery 는 1.6으로 테스트 됐다. 다른 버전도 잘 될지는 테스트를 안해봐서 모르겠으며, 만약 잘 되지 않더라도 이 함수는 간단한 놈이기 때문에 약간의 함수 수정만 거치면 어떤 버전에서도 가능하다.
php.js
phpjs는 php의 함수들을 javascript 에서 사용 하도록 구현해놓은 라이브러리이다. 여기서는 2개의 함수가 필요하다. date() 함수와 strtotime() 함수를 쓴다. 두 함수는 아래의 링크에서 받을 수 있다.
자바스크립트 Date Object 에 메소드 추가
Date.prototype.getDOY = function() { var onejan = new Date(this.getFullYear(),0,1); return Math.ceil((this - onejan) / 86400000); }
Day Of Year 이라는 함수이며 이 함수가 필요하다.
실시간 시간표현 함수
우선 시간 계산을 하는 자바스크립트이다. 아래는 date() 함수에만 의존성이 있다.
function dynamicDate(dt, option, nocalcserverdif){ if (typeof timeDif == "undefined") timeDif = 0; if (!nocalcserverdif) { dt += timeDif; } var dif = Math.floor(new Date().getTime() / 1000) - dt; switch(option) { case "half": if (dif >= 0) { var today = new Date(); var seconds = dif%60, minutes = ((dif-seconds)%3600)/60, hours = ((dif-(minutes*60+seconds))%86400)/3600; var days = today.getDOY() - date("z", dt), months = (today.getFullYear()*12 + today.getMonth() + 1) - (parseInt(date("Y",dt)*12) + parseInt(date("n", dt))), years = today.getFullYear() - date("Y", dt); if (years == 0 || (years == 1 && months <= 1)) { if (months == 0 || (months == 1 && days < 7)) { if (days == 0) { if (hours == 0) { if (minutes == 0) { if (seconds <= 2) { return "방금 전"; } else { return seconds+"초 전"; } } else { return minutes+"분 "+seconds+"초 전"; } } else { return hours+"시간 "+minutes+"분 전"; } } else if (days == 1) { return "어제 "+date("H시", dt); } else if (days == 2) { return "그저께 "+date("H시", dt); } else if (days == 3) { return "그끄저께 "+date("H시", dt); } else if (days < 7) { var weekStr = ['일','월','화','수','목','금','토']; return weekStr[date('w', dt)]+"요일 "+date("H시", dt); } else { return days+"일 전 "+date("H시", dt); } } else if (months == 1) { return "저번달 "+date("j일", dt); } else if (months < 6) { return months+"달 전 "+date("j일", dt); } else { return date("n월 j일", dt); } } else if (years == 1) { return "작년 "+date("n", dt)+"월 "+date("j일", dt); } else if (years == 2) { return "재작년 "+date("n", dt)+"월 "+date("j일", dt); } else { return years+"년 전 "+date("n", dt)+"월 "+date("j일", dt); } } else { dif = dif * -1; var today = new Date(); var seconds = dif%60, minutes = ((dif-seconds)%3600)/60, hours = ((dif-(minutes*60+seconds))%86400)/3600; var days = date("z", dt) - today.getDOY(), months = (parseInt(date("Y",dt)*12) + parseInt(date("n", dt))) - (today.getFullYear()*12 + today.getMonth() + 1), years = date("Y", dt) - today.getFullYear(); if (years == 0 || (years == 1 && months <= 1)) { if (months == 0 || (months == 1 && days < 7)) { if (days == 0) { if (hours == 0) { if (minutes == 0) { if (seconds <= 2) { return "곧"; } else { return seconds+"초 후"; } } else { return minutes+"분 "+seconds+"초 후"; } } else { return hours+"시간 "+minutes+"분 후"; } } else if (days == 1) { return "내일 "+date("H시", dt); } else if (days == 2) { return "모레 "+date("H시", dt); } else { return days+"일 후 "+date("H시", dt); } } else if (months == 1) { return "다음달 "+date("j일", dt); } else if (months < 6) { return months+"달 후 "+date("j일", dt); } else { return date("n월 j일", dt); } } else if (years == 1) { return "내년 "+date("n", dt)+"월 "+date("j일", dt); } else if (years == 2) { return "내후년 "+date("n", dt)+"월 "+date("j일", dt); } else { return years+"년 후 "+date("n", dt)+"월 "+date("j일", dt); } } break; default: if (dif >= 0) { var today = new Date(); var seconds = dif%60, minutes = ((dif-seconds)%3600)/60, hours = ((dif-(minutes*60+seconds))%86400)/3600; var days = today.getDOY() - date("z", dt), months = (today.getFullYear()*12 + today.getMonth() + 1) - (parseInt(date("Y",dt)*12) + parseInt(date("n", dt))), years = today.getFullYear() - date("Y", dt); if (years == 0 || (years == 1 && months <= 1)) { if (months == 0 || (months == 1 && days < 7)) { if (days == 0) { if (hours == 0) { if (minutes == 0) { if (seconds <= 2) { return "방금 전"; } else { return seconds+"초 전"; } } else { return minutes+"분 "+seconds+"초 전"; } } else { return hours+"시간 "+minutes+"분 전"; } } else if (days == 1) { return "어제 "+date("H시 i분", dt); } else if (days == 2) { return "그저께 "+date("H시 i분", dt); } else if (days == 3) { return "그끄저께 "+date("H시 i분", dt); } else if (days < 7) { var weekStr = ['일','월','화','수','목','금','토']; return weekStr[date('w', dt)]+"요일 "+date("H시 i분", dt); } else { return days+"일 전 "+date("H시 i분", dt); } } else if (months == 1) { return "저번달 "+date("j일 H시", dt); } else if (months < 6) { return months+"달 전 "+date("j일 H시", dt); } else { return date("n월 j일 H시", dt); } } else if (years == 1) { return "작년 "+date("n", dt)+"월 "+date("j일 H시", dt); } else if (years == 2) { return "재작년 "+date("n", dt)+"월 "+date("j일 H시", dt); } else { return years+"년 전 "+date("n", dt)+"월 "+date("j일 H시", dt); } } else { dif = dif * -1; var today = new Date(); var seconds = dif%60, minutes = ((dif-seconds)%3600)/60, hours = ((dif-(minutes*60+seconds))%86400)/3600; var days = date("z", dt) - today.getDOY(), months = (parseInt(date("Y",dt)*12) + parseInt(date("n", dt))) - (today.getFullYear()*12 + today.getMonth() + 1), years = date("Y", dt) - today.getFullYear(); if (years == 0 || (years == 1 && months <= 1)) { if (months == 0 || (months == 1 && days < 7)) { if (days == 0) { if (hours == 0) { if (minutes == 0) { if (seconds <= 2) { return "곧"; } else { return seconds+"초 후"; } } else { return minutes+"분 "+seconds+"초 후"; } } else { return hours+"시간 "+minutes+"분 후"; } } else if (days == 1) { return "내일 "+date("H시 i분", dt); } else if (days == 2) { return "모레 "+date("H시 i분", dt); } else if (days < 7) { var weekStr = ['일','월','화','수','목','금','토']; return weekStr[date('w', dt)]+"요일 "+date("H시 i분", dt); } else { return days+"일 후 "+date("H시 i분", dt); } } else if (months == 1) { return "다음달 "+date("j일 H시", dt); } else if (months < 6) { return months+"달 후 "+date("j일 H시", dt); } else { return date("n월 j일 H시", dt); } } else if (years == 1) { return "내년 "+date("n", dt)+"월 "+date("j일 H시", dt); } else if (years == 2) { return "내후년 "+date("n", dt)+"월 "+date("j일 H시", dt); } else { return years+"년 후 "+date("n", dt)+"월 "+date("j일 H시", dt); } } } }
잘 되는지 테스트해보려면 아래와 같이 테스트하면 된다. 1312265402는 Unix Timestamp 값이다.
alert(dynamicDate(1312265402));
그리고 실시간으로 업데이트 해주는 함수는 아래와 같다. strtotime 함수와 jQuery 에 의존성이 있다.
jQuery(function(){ refreshDate = function(){ $("span._timestamp").each(function(i,o){ var oT = $(o); var timestamp = oT.attr("timestamp"); var date = oT.attr("date"); if (date) timestamp = strtotime(date); var nocalcserverdif = false; timestamp = parseInt(timestamp); if (isNaN(timestamp)) return true; if (oT.attr("clientside")) nocalcserverdif = true; oT.html(dynamicDate(timestamp, oT.attr('option'), nocalcserverdif)); }); } refreshDate(); setInterval(function(){refreshDate()}, 5000); });
이런식으로 하면 된다. 바로 위의 이것은 jQuery(function(){})에 꼭 넣어서 써야 한다. 페이지와 jQuery가 로딩완료 된 후 실행되어야 하는 함수이다. 5초마다 시간을 갱신해준다.
사용법
이제 어디서든 아래의 php 소스를 입력하면 시간이 자동으로 표현된다. option이라는 attribute 는 지정을 안해도 되고 half 등을 쓸 수 있다.
$time = time(); echo '<span class="_timestamp" timestamp="'.$time.'" option="half">'.date("Y/m/d H:i:s", $time).'</span>
혹은 아래와 같이 써도 표현이 된다. 여기서 strtotime() 함수를 쓰게 된다.
<span class="_timestamp" date="2011-08-02 15:34:02">2011-08-02 15:34:02</span>
표현 예제
2011-08-02 15:34:02는 지금으로부터 입니다.
추가 조치
그리고 span 태그를 쓰게 되면 간혹 어떤 브라우저에서는 css 를 바로 윗 요소의 style 을 잘 반영 해주지 않는 경우가 있다. 또는 이니셜라이즈 CSS를 쓸 경우, 모든 요소에 기본 글자 색깔과 font-size 를 지정해주는 경우도 있어서 아래의 css 를 추가해주면 좀더 유연한 작동을 바랄 수 있다.
span._timestamp { color: inherit; font-family: inherit; font-size: inherit; }
파일 다운로드 및 데모 보기
데모페이지: http://awhile.us/demo/dynamicdate/
다운로드: [다운로드]
도메인과 라이브러리 While이 에 작성. 1,075번 읽힘.
나는 지금 시점에서 가지고 있는 도메인이 40개가 넘는다.
일부는 무료도메인 서비스 (oa.to) 를 위해서 산것도 있고, 내가 보기에 좋은 도메인이라 가지고 놀려고 산 것도 있고 또 무료도메인 서비스같이 특정 서비스를 하려고, 아니면 홈페이지를 운영하려고 산 도메인도 있다.
무튼 지금은 내가 보기에 좋은 도메인이라고 생각하는거에 대해서 글을 쓸 생각인데, 요즘에 만들고 있는 사이트에서는 이미지나 자바스크립트, CSS등을 모두 한곳에 몰아놓고 쓴다. 뭐 맨날 이것저것 홈페이지 관련된걸 만들다보니 매번 공통적으로 쓰는게 있었다. 정말로 내가 만든 페이지에서는 빠지지 않는 이것!
어떤 사이트에서도 공통적으로 적용 할 수 있는 ‘이니셜라이저 CSS’이다. 웹표준 사이트를 만드려고 하기 위해서는 어떤 사이트든 무조건 CSS에 대해서 이니셜라이징을 해둬야 많은 브라우저에서 잘보이기 마련이다. 그 말썽쟁이 IE라는 놈 때문에.. 저기엔 내가 정말로 자주 쓰고 항상 쓰는 속성만 싹 모아놨다. 그게 바로 “라이브러리”이다. 자기가 많이 쓰는 기능들을 한데모아 편하게 쓸 수 있도록 모아두는 걸 라이브러리라고 한다. 어떤 프로그래머든 자신이 자주이용하는 함수를 모아둔 라이브러리를 만들고 싶은 마음이 없는 사람은 거의 없을거다. 그냥 나도 차곡차곡 정리하다보니 아래에 나올 라이브러리들이 모아졌다.
근데 뭔가 특이한게 있지 않은가? 위에 보이는 저 도메인은 ‘라이브러리’전용 도메인이다. 실제로는 내가 zz.gg 라는 도메인의 주인이고, 거기에 이미지 서버등의 서브도메인으로 많이 쓰는 ”static”의 약자인 s를 붙여 s.zz.gg 라고 지었다. ss.gg 뭐 요정도면 더 좋았겠지만 이런건 구하기 힘드니까.. 이게 뭔 의미냐 싶겠지만 저렇게 하면 입력하기 상당히 쉽다. 그런데 어차피 복사 붙여넣기로 css랑 js 붙여넣어 쓰면서 뭘 편하길 바라냐고? 아 그것까지는 대답 못해주겠음! 사실 그것까지는 나도잘.. 그냥 간지나게 짧은 도메인 붙여봤음.
내가 젤 자주 쓰는 공개 ajax 라이브러리가 jquery 이다. 이것도 역시 아래와 같이 주소를 구성하여 쓰고있다.
http://s.zz.gg/jquery/jquery.placeholder.js
다른 것들과 구분의 핵심이 s.zz.gg/ 뒤에 나오는 폴더이름이다. 저 폴더를 중심으로 구분하고 있다. 이런거 하다보면 대충대충 이리저리 껴넣다가 나중에 구조가 파산신청하고 싶어버리게 될때가 상당히 많은데 첨부터 차곡차곡.. 이외에도 phpjs, sound manager2, codemirror, extjs 등을 모두 넣어놨다. 이렇게 쓴지 반년은 된거 같은데 엄청 편하다.
그리고 위에 css 라이브러리 뿐만 아니라 js 라이브러리도 쌓고 있다. 요즘에 js 에 재미가 들어서 전면 ajax 적용된 CMS를 만들고 있다. 코드네임은 infiniteboard.
http://s.zz.gg/infinite/func.js
위는 가장 자주 쓰는 함수 모음의 일부이다.
여러가지가 있다. form 태그에 여러 간단한 attribute 만 추가해주고 아주 쉽게 ajax 처리를 끝내버릴 수 있는, 추가적인 스크립트 따위도 코딩이 필요 없이 그냥 간단한 ajax 폼를 한방에 만들어버릴 수 있는 함수가 있다.
둘째로 빙빙 돌아가는 로딩이미지를 보여주는 함수가 있다.
셋째로 a href 와 비슷한 기능을 하면서 특정 키를 누르고 있을때는 새창이 열리게 하는 등의 기능이 있는 함수가 있다. 대체적으로 button 에다가 a href 거는 것은 비표준이니, onclick로 링크를 걸고 싶을 때 사용한다.
넷째로 실시간 시간을 보여주는 처리를 하는 함수이다. phpjs 의 time() 함수에 의존성이 있고 <span class=’_timestamp’ timestamp=’타임스탬프값’></span> 라고 본문에 적어두면 특정 시간마다 갱신하는 기능을 넣을 수 있다. 그렇게 되면 저 타임스탬프값과 클라이언트의 시간 값을 비교해서 몇초 전에 작성된 글인가? 요런걸 갖다 보여준다. 서버와 클라이언트간의 시간 차가 있을 수 있으니 이것도 처리를 해주는데, 이건 서버마다 다르니까 웹페이지를 만들때 따로 처리를 한다. 저기 보이는 전역 변수 timeDif 가 그 비밀이다.
또 다섯째로 instantForm 이라는 부분이다. 이건 일반적으로 inline 으로 보이는 “텍스트”를 클릭을 하면 갑자기 “input 상자”로 바뀌면서 바로 그 자리에서 수정을 할 수 있도록 하는 기능이다. 아직 만든지 1-2주밖에 되지 않았고 좀더 확실하고 범용적이게 개선이 되려면 아직 몇주 더 걸릴것이다. 참고로 요건 uibutton 이라는 함수에 의존성이 있다. 그냥 꾸밈용 함수인데 이건 나~중에 설명하기로 한다. 페이스북의 버튼 기능에 감동받아서 만든 함수이다.