오늘은 velog에서 기본적으로 제공해주지 않는 기능을 평소에 자주쓰던 velog-readme-stats 를 이용하여 구현해보려고합니다.
제가 이 강화를 왜 시작했을까요? 이유는 다음과 같습니다.
x스토리, x이버 블로그 등은 아래와 같이 방문자수 ( 조회수 ) 를 제공하지만, velog는 제공해주지 않습니다.
대신, 각 글마다 아래와 같은 통계를 보여줍니다.
하지만! 저는 제가 쓴 글들의 조회수와 받은 좋아요의 총합이 궁금합니다.
그래서 저는 다음과 같은 생각을 했습니다.
벨로그는 오픈소스로 공개 되어있고, 이걸 활용할 수 있게 해놨으니까 코드를 분석해서 직접 만들어보자!
근데, api를 분석하는 것도 분석하는 것이지만 이걸 시각화해서 보여줄 방법이 제일 중요했습니다.
저 혼자 봐도 괜찮지만, 그래도 다른 사람도 내 통계를 간단하게 볼 수 있으면 좋을 것 같았거든요.
그래서 저는 평소에 github readme에 사용하던 velog-readme-stats를 활용하기로 마음먹었습니다.
velog-readme-stats는 eungyeole 님이 만들어주신 오픈소스 프로젝트로, github readme에 사용할 수 있는 뱃지입니다.
위 stats 뱃지는 벨로그의 api를 이용하여 만들었으며, 정말 깔끔하고 이쁘게 잘만들어져있습니다.
이제 남에게 보여줄 프론트엔드(..?) 는 해결됐으니, api를 만들어야합니다.
velog는 정말 감사하게도(?) client와 server 가 다음과 같은 레포에 오픈소스로 공개되어있습니다.
프론트엔드 : https://github.com/velopert/velog-client
백엔드 : https://github.com/velopert/velog-server
그렇기 때문에, server 레포만 잘 살펴봐도 api를 쉽게 이용할 수 있습니다.
엥 근데...!!!! api 서버 코드를 분석하려고 열어보니 이게 웬걸..! graphql과 typescript로 코드가 구성이 되어있습니다.
요즘 "자바 혹은 코틀린 + Spring + HTTP API" 를 개발하는 저에겐 외계어나 다름이 없을뻔했습니다만,
그래도 이 기술 저 기술 찍먹을 평소에 많이 해봤더니 "express + graphql + apollo"의 조합을 써봤다는 놀라운 사실.
시도해본지 2년이 넘어가지만, 역시 경험은 언제나 도움이 됩니다.
https://github.com/kshired/instaclone-backend
그래서, 이해를 할 수 있을거라는 어느정도의 확신을 갖고 분석을 시작하기로합니다.
직접 특정 사용자의 모든 글을 긁어와서 조회수와 좋아요의 합을 구하는 무식한 방법을 처음에는 생각했었습니다만, 숨겨진 통계를 위한 api가 있을 수도 있다는 생각해서 찾아보기로 했습니다.
이 무식한 방법이 어떨 때는 가장 좋은 방법일 수도 있지만, 일단 "서버에 영향을 주는 행위일 수도 있지 않을까?" 라는 생각을 했기에 첫 시도로는 보류했습니다.
하지만, 열심히 graphql 코드를 읽어보고 분석한 결과..
제공해주지 않는 것 같더라구요..
근데, 숨겨진 API가 있을 수도 있겠다는 생각을 왜 했을까요?
실제로 velog는 공식적으로 안내하지는 않지만, rss 같은 기능을 제공하고 있기 때문에 혹시나해서 찾아봤습니다.
rss 기능은 다음과 같은 방식으로 사용할 수 있습니다.
https://v2.velog.io/rss/{유저이름}
첫 번째 시도는 아쉽게도 실패했으니, 처음에 생각해낸 모든 글을 긁어와서 조회수와 좋아요의 합을 구하는 무식한 방법을 해보기로했습니다.
대충보니까 이 코드인 것 같더라구요.
posts로 graphql 쿼리를 날리면, paginataion을 통해 각 post들의 정보를 가져올 수 있겠다 싶었습니다.
이제 어떤 query를 날려야하는지도 알아냈으니,
이제 고민 할 것은 아래 코드를 보면 알 수 있듯이 post를 1번 요청당 최대 100개씩 밖에 못가져오는데 어떻게 하는게 좋을까라는 생각이였습니다.
// 실제로 limit이 100이 넘으면 bad request 에러가 throw됩니다. ㅠㅠ
if (limit > 100) {
throw new ApolloError('Max limit is 100', 'BAD_REQUEST');
}
생각보다 멍청하지만 간단한 방법으로 해결했습니다.
바로, 100개씩 반복해서 전부 가져오자는 생각이였습니다.
그래서 코드를 다음같이 짜게 되었습니다.
const { request } = require("../utils");
const fetcher = (username, cursor = null) => {
return request({
query: `query Posts($cursor: ID, $username: String, $temp_only: Boolean, $tag: String, $limit: Int) {
posts(cursor: $cursor, username: $username, temp_only: $temp_only, tag: $tag, limit: $limit) {
id
likes
views
}
}`,
variables: {
username,
cursor,
limit: 100,
},
});
};
async function getStats(name) {
let response;
let posts = [];
while (true) {
try {
if (response && response.data.data.posts.length >= 100) {
response = await fetcher(name, posts[posts.length - 1].id);
} else {
response = await fetcher(name);
}
} catch(e) {
throw new Error(e);
}
posts = [...posts, ...response.data.data.posts];
if (response.data.data.posts.length < 100) break;
}
const totalLikes = posts.map(post => post.likes)
.reduce((prev,cur) => prev + cur,0)
const totalViews = posts.map(post => post.views)
.reduce((prev,cur) => prev + cur,0)
return {
totalLikes,
totalViews
}
}
module.exports = getStats;
( 참고로 이 코드는, velog-backup 의 코드를 참고해서 짰습니다. 오픈소스 + 오픈소스 == 흑마법 )
100개씩 가져오는 요청은 서버에 부담을 주지 않을까요?
graphql 의 장점은 여러개가 있는데, 간단하게 정리하면 다음과 같습니다.
그렇기 때문에, 2번의 장점 덕분에 실제 코드에서도 아래와 같이 필수적으로 필요한 id, likes, views 만 요청함으로써 서버의 부담을 어느정도는 줄일 수 있었습니다.
그래도 사실.. 어느정도의 부하를 줄 수도 있을거라 생각합니다.
그래서 이 코드를 사용하여 만든 velog-readme-stats는 일단 저만 사용하고 있습니다.
( 이게 큰 부하를 주지 않는다고 생각했다면, velog-stats에 feature req후 PR을 올리거나 했을 거 같습니다. )
그래서 만들어진 결과는? 다음과 같습니다.
실제 github 프로필에는 아래처럼 적용되어 있습니다.
원래 사용되던 svg 코드에, 조금 수정을 가해서 픽셀 노가다를 마친 끝에 위와 같은 svg 뱃지를 만들 수 있었습니다.
아래의 코드를 보시면, (처절한) 픽셀 노가다의 흔적을 볼 수 있습니다.
const createBadge = (name, likes, views) => {
const name_size = (name.length-1) * 9;
const likes_size = (likes.toString().length + 2)* 9;
const views_size = (views.toString().length + 2) * 9;
const total_size = 33+name_size+likes_size+views_size
return `
<svg width="${total_size}" height="25" viewBox="0 0 ${total_size} 25" fill="none" xmlns="http://www.w3.org/2000/svg">
<style>
.name{ fill: #ffffff; font-weight: 500; font-size: 14px;}
</style>
<g>
<rect x="2" width="${total_size}" height="25" fill="#20C997"/>
<text x="30" y="17" class="name">${name}</text>
<text x="${30+name_size}" y="17" class="name">♡ ${likes}</text>
<text x="${48+name_size+likes_size}" y="17" class="name">${views}</text>
</g>
<path d="M3.125 0H21.875C23.6009 0 25 1.39911 25 3.125V21.875C25 23.6009 23.6009 25 21.875 25H3.125C1.39911 25 0 23.6009 0 21.875V3.125C0 1.39911 1.39911 0 3.125 0Z" fill="#20C997"/>
<path d="M18.6199 8.526V7.54163C17.9949 7.3385 17.2605 7.11975 16.4167 6.88538C15.573 6.63538 15.0027 6.51038 14.7058 6.51038C14.0496 6.51038 13.6589 6.82288 13.5339 7.44788L12.0105 16.0963C11.5261 15.4557 11.1277 14.9166 10.8152 14.4791C10.3308 13.7916 9.8855 13.0026 9.47925 12.1119C9.05737 11.2213 8.84644 10.4244 8.84644 9.72131C8.84644 9.29944 8.96362 8.9635 9.198 8.7135C9.41675 8.44788 9.83081 8.11194 10.4402 7.70569C9.81519 6.90881 9.03393 6.51038 8.09643 6.51038C7.59644 6.51038 7.18237 6.65881 6.85425 6.95569C6.5105 7.25256 6.33862 7.69006 6.33862 8.26819C6.33862 9.23694 6.74487 10.4479 7.55737 11.901C8.35425 13.3385 9.89331 15.5026 12.1746 18.3932L14.4949 18.5573L16.2761 8.526H18.6199Z" fill="white"/>
<svg x="${30+name_size+likes_size}" y="4.5" width="14px" height="14px" viewBox="0 -0.778 14 14" xmlns="http://www.w3.org/2000/svg" fill="white">
<path d="M7 3.5a2.696 2.696 0 0 0 -0.759 0.122 1.347 1.347 0 0 1 0.176 0.656 1.361 1.361 0 0 1 -1.361 1.361 1.347 1.347 0 0 1 -0.656 -0.176A2.715 2.715 0 1 0 7 3.5zm6.915 2.367C12.597 3.296 9.988 1.556 7 1.556S1.402 3.297 0.085 5.868a0.786 0.786 0 0 0 0 0.709C1.403 9.149 4.012 10.889 7 10.889s5.598 -1.741 6.915 -4.312a0.786 0.786 0 0 0 0 -0.709zM7 9.722c-2.398 0 -4.596 -1.337 -5.783 -3.5C2.404 4.059 4.602 2.722 7 2.722s4.596 1.337 5.783 3.5C11.596 8.385 9.398 9.722 7 9.722z"/>
</svg>
</svg>
`;
};
네! 놀랍게도 심심해서 다크모드도 만들었습니다!
간단하게 테스트 해보시려면 아래와 같이 사용하면 됩니다.
[![Velog's GitHub stats](https://velog-readme-2.vercel.app/api/badge-stats?color=dark&name=아이디)](https://velog.io/@아이디)
사실 이 코드로 만든 뱃지는, 100개의 요청을 여러번 보내다 보니 렌더링 속도도 많이 늦고 서버에 영향을 줄 가능성이 다분합니다.
그래서 공개적으로 배포하기엔 어려울 것 같아서 개인적으로 쓰고 있습니다.
추가적으로, 간단하게 좋아요와 조회수만 보여주기 때문에 통계로 보기에는 조금 아쉽습니다.
그래서 저는 velopert님이 시간이 있으시다면...
간단하게라도 전체 통계 기능 + 외부로 보여줄 수 있는 화면을 만들어주시면 정말 좋을 것 같습니다! 만들어주세요!!!!!
추가로, 이렇게 적은 리소스를 들여서 새로운 것을 만들 수 있다는 것이 오픈소스의 장점이라고 생각합니다.
오픈소스로 이렇게 velog-readme-stats를 공개해주신 eungyeole 님께 감사함을 표합니다!