개발
자바스크립트로 만드는 유저 조회수 카운터 기능


사이트를 운영하는 입장에서 사이트의 활성도를 보는 가장 기본적인 척도는 바로 '조회수'이다.
말그대로 조회수는 내 사이트의 특정 페이지의 조회가 얼마나 되었는지 보여주는 정량적 수치로 '방문자수'와 더불어 중요한 지표로 사용된다.
방문자수의 경우는 일반적으로 해당 사이트의 방문에 대해서 1회만 카운트하는 방식이어서 세션이나 쿠키로 구현하나 조회수의 경우는 특정 게시글의 조회수를 일일히 카운트 하기때문이 방문자수보다 일반적으로 조회수의 카운트량이 어마어마하게 많다.
전통적인 조회수 처리 방법
보통 이런 조회수 카운터는 내가 이전에 썻던 글처럼 해당 글을 백단에서 꺼내줄때 조회수 컬럼에 +1 처리를 해주는것으로 쉽게 구현이 가능하다.
하지만 이런 방식의 백단 처리면 해당페이지에서 무한 새로고침을 하던가, 봇이나 크롤러가 해당페이지를 퍼갈때도 카운터가 +1이 되기 마련이어서 '실제 유저의 조회수'를 얻어내는건 거의 불가능에 가깝다.
물론 나는 기존에 이를 어떻게든 처리해보기위해서 Jenssegers\Agent 패키지를 이용해 아래와 같이 봇을 걸러내는 작업을 했음에도 불구하고 실제로는 저 조건식을 뚫고 진짜 여러 요청이 조회수로 카운트 되고 있었다.
$agent = new Agent();
if(!$agent->isRobot()) {
$post->timestamps = false;
$post->increment('hit');
$post = $post->fresh();
}
Copy
내가 이에 대해서 자각을 하게된 계기는 내 사이트에 연동해둔 구글 애널리틱스와 구글 애드센스의 보고서를 보고서 느낀것이다.
일례로 내 사이트에서 일일조회수가 총합 1000이 찍혀있던날, 막상 애널리틱스로 가져오는 방문자는 300명밖에안되고 애드센스로 볼수있는 페이지뷰수는 400회밖에 안되었던 사례가 있다.
물론 해당 데이터들은 구글의 빡센 유저 조건에 의해서 허수데이터도 거르고 거른 진짜 데이터여서 내가 만든 '서버입장에서의 방문자수'와는 당연히 차이가 있겠다만, 그럼에도 불구하고 데이터 편차가 100%가 넘어가는건 좀 아니다 싶었다.
그래서 이 부분에 대해서 더 좋은방법이 없을까 생각하다가 기존의 백단에서만 수행하던 조회수 작업을 앞단(자바스크립트)과 연동하는 방법을 구상해보았다.
자바스크립트 조회수 카운터
말은 거창하게해도 결국 페이지가 랜더링된 이후 백단으로 비동기 요청을 넣는 간단한 방법이다.
function hitPost(postId, isLoggedIn) {
if (!postId || isLoggedIn) return;
const [nav] = performance.getEntriesByType("navigation");
if (nav && nav.type === 'reload') return; // Prevent hit on reload
fetch(`/${postId}/hit`, {
method: 'POST',
headers: {
'X-CSRF-TOKEN': '{{ csrf_token() }}',
'Content-Type': 'application/json'
}
}).catch(e => console.error('Failed to hit a post:', e));
}
Copy
하지만 이 방법이 기존의 백단기준 조회수보다 훨씬 신뢰도가 높은 이유는 바로 '페이지 랜더링이 끝난 후 작동되는 함수'라는 부분에서 있다.
대부분의 봇이나 크롤러는 자바스크립트를 실행하지않고 정적인 HTML 데이터만 긁어가는 경우가 태반이다.
이런 '봇'들의 조회수가 나는 백단에서 충분히 걸러질줄 알았는데 구글부터도 '실제 유저데이터'를 수집한다는 명목으로 User-Agent 값을 속이고 들어오는 경우도 많다고한다. (구글 페이지스피드도 같은 맥락)
결국 이런 로봇들과 사람의 차이를 둔다면 실제 페이지를 보는 유저입장에서는 스타일, 스크립트 처리가 완료된 페이지를 보냐 안보냐의 차이가 아닐까 싶다.
그래서 위의 hitPost 함수를 만들어놓고 이를 본문이 랜더링이 완료되었을때 작동시키는 방식으로하면 대부분의 가짜 페이지 요청은 걸러낼 수 있는 것이다.
document.addEventListener('DOMContentLoaded', function() {
window.postId = '{{ $post->id }}';
window.isLoggedIn = {{ auth()->check() ? 'true' : 'false' }};
hitPost(window.postId, window.isLoggedIn);
});
Copy
그리고 백단보다는 앞단에서 유저의 행위를 파악하기가 쉬워서 나는 새로고침의 경우에는 조회수 상승 요청이 안가도록 예외처리를 걸었다.
이런 부분을 이용해서 굳이 조회수뿐만이 아니라 사용자의 DOM 크기에 기반한 플래그처리나 기록을 하는 방향으로 응용도 충분히 가능하다.
백단 메소드 (PHP + Laravel)
그럼 이런 요청을 처리해주는 백단은 어떻게 구성해야할까?
public function hitPost(Request $request, $id)
{
$post = Post::find($id);
$agent = new Agent();
// 로그인한 사용자는 제외
if (Auth::check() || $agent->isRobot()) {
return response()->noContent();
}
$post->timestamps = false;
$post->increment('hit');
return response()->noContent();
}
Copy
의외로 간단하다.
여기서도 기존 에이전트 방식의 로봇을 제거하고 평범하게 조회수를 1 올려주면 되는일이다.
(나는 내가 로그인한경우는 조회수를 올리면안되서 별도의 로그인 사용자는 조회수가 안올라가도록 예외처리를 했다.)
그리고 위 컨트롤러 메소드를 찾아갈 수 있게 라우터를 설정해주면 작업은 끝이다.
Route::post('{id}/hit', 'HomeController@hitPost');
Copy
결론
기존에 백단에서만 체크하던 조회수 방식을 앞단 -> (비동기 요청) -> 백단에서 더블체크한 후 조회수를 올리는 방식이다.
정상적인 유저라면 페이지로드 후 스크립트가 실행되는것을 기반으로하는 트릭 논리라고 볼 수 있다.
적용한지 하루가 지난 이후 실제 조회수가 어떻게 쌓이는지 비교해봤다.


차이가 10%이내로 줄었다!
이 10%도 어떤 로직상의 오차가 아니라 '애드블럭'같은 광고 차단을 쓰는 방문자의 경우에는 내 조회수에선 올라가지만 애드센스 조회수에서 안올라가는 경우라고 볼 수 있다.
그래도 기존의 100%오차보다는 무려 90%정도로 확 줄인상황이라 정말 마음에 드는 결과라고 볼 수 있다.
끝!
#문제해결 #Laravel #php #JavaScript #블로그
0
개의 댓글
개발 카테고리의 다른 글
04/29
파이썬 리스트 슬라이싱 [:], [::] 정리 (콜론, 더블콜론)
난 실무에 파이썬을 잘 이용하지는 않지만 정보처리기사를 준비하는도중 파이썬 관련 문법이 몇번 제법 나와서 알게된 사항이다. 개인적으로 공부도하고 정리도 할겸 포스팅을 남겨본다. 파이썬 리스트 슬라이싱리스트 슬라이싱(List Slicing)이라고 파이썬에서 리스트를 다룰때 사용하는 선택자 기법이 있다.우리가 흔히...

04/22
nginx 캐싱 설정 (Cache-Control)
앞전의 이야기랑 연계되는 이야기이긴한데,S3서버를 통해서 제공되는 파일은 그쪽에서 캐싱처리를 해도되지만, 웹서버에 올려서 호스팅되는 파일은 웹서버쪽에서 캐싱처리를 해야한다. 더군다나 나는 Docker 환경에 Proxy 방식으로 웹서버를 운용중이라 적용하는데 좀 버거운 과정이 있었다. 쉽게 상황을 풀어보면 도커에서...

04/21
AWS S3 캐싱 정책 일괄변경 작업 + Laravel 옵션
나는 AWS S3를 통해서 게시글 본문 이미지를 호스팅 하고 있다.이 S3를 최대한 잘 사용해보기 위해서 그위에 Cloud Front를 얹어서 CDN 처럼 지역 배포하는 기능을 구현해놨는데, 이게 캐싱설정이 제대로 먹지 않는다는걸 알게되었다. 무슨말이냐면 아무리 CloudFront에 캐시정책이 제대로 설정되어있다하더라고 S3에 설정...