벨로그야 미안하다!! / velog dashboard 제작기 (3) - frontend & 이슈 & 앞으로

정현우·2023년 11월 22일
12

Project Records

목록 보기
5/7
post-thumbnail

Velog Dashboard

원래 Velog Dashboard 는 4편으로 이루어져 있었고, 개발과 글을 먼저 작성 한 뒤에 오픈과 글을 동시에 올리는 형태로 배포되었다.. 우려가 현실이 되었다..!

생각보다 많은 분들이 관심을 가져주셔서, 너무 감사드립니다!! 짧지만 굵었던 삼일천하 였습니다 :) 벨로그와 벨로퍼트님에게 심심한 사과를 올립니다.. 🙆‍🙇‍🙆‍🙇‍🙆‍🙇‍

"12월 중으로 레거시 통계 보기가 안정화 된 상태로 다시 제공" 된다고 하니, 그때 다시 살려보도록 하겠습니다!, 아래는 예정된 원래 글입니다! :') 아쉬운대로 올려봅니다!!

(통합 통계도 고려중이라고 하십니다!! 예쓰!!)


[ 해당 글은 "velog-dashboard" 프로젝트를 만들면서 기록한 DevLog 입니다! ]

일단 FE에 닿기까지 어찌 저찌 1차 user flow만 완성 시켜 보자고 코드 날먹을 하면서 달려온 것 같다. 왜인지 모르게 조급해졌다..! 의외로 가장 많은 시간을 투자한 FE 작업에 대한 기록이다..

일단 beta version (v0.1) 의 최소한의 통계 대시보드는 위 4가지 형태에 대해 표현해서 보여줘야 했다. (했었다)

1. frontend

1) 레퍼런스 및 써드파티

핀터레스트에서 무지성으로 일단 web dashboard dark 를 검색했다. 하나같이 구현난이도가 헬이다.. 까맣고 흰 글씨가 최고인 나에게 너무 어려운 과제다.

일단 살짝 비슷한 테마로 전체 틀을 잡고, wrapper - container - block - section 틀로 flex 만 사용해서 전체 틀만 잡았다.

그리고 css 야매 코딩을 위한 기깔나는 써드파티들을 기억한다.

  1. sweetalert2 : popup, modal 과 같은 모듈을 제공해 준다. 생각모다 엄청나게 많은 프로젝트에서 엿볼 수 있다.

  2. hover.css : 간단한 animation 및 event 들을 미리 다 세팅해 두어서 간단하게 class 추가하는 것으로 핸들링 할 수 있다. 진짜 짱편하다.

  3. chartjs : 차트 그리기! 60kb size로 기깔나는 chart를 data serving & object 정의만으로 쉽고 빠르게 그릴 수 있게 도와주는 라이브러리다!

  4. githun repo - HEAD : 조금 예전 레퍼런스라 최신식은 아닐 수 있으나 SEO 등을 위한 meta 해더를 포함한 html에서 세팅해야할 HEAD tag들에 대한 정보가 총망라되어 있다. 특히 Open Graph 에 대한 내용은 세팅하는 걸 추천한다.

  5. 구글 웹폰트 : 구글 웹폰트는 사실 이제 빼고 개발하는게 불가능할 정도..

  6. 웹빌더나 패키징해주는 써드파티 없이 pure html5, css3, javascript(ES5+) 만 사용했다. webpack 정도 까지는 쓸만하다고 생각했으나, 개인 취향도 있고, 페이지 수나 block 덩어리들이 딱히 많지 않을 것 같아서 사용하지 않았다.

  7. icon 등의 asset은 flaticon 과 같은 페이지를 활용했고, CDN 으로 가져오는 것을 적극(!?) 활용했다 ㅎ..

  8. 그리고 sentry, 사용하진 않았지만 부트스트랩이나 테일윈드 + chatGPT 의 조합이라면 이제 "꽤 그럴듯하게" 뭐든 만들 수 있다! (라고 생각했었다)

2) 전체 디렉토리 구조

일단 static file을 nginx 로 서빙할 예정이라 단순하다. url 파라미터가 곧 디렉토리고, 디렉토리 하위엔 html + css + js 가 한 세트를 이룬다. 이후 페이지가 늘어날 때 마다 디렉토리가 통으로 하나씩 늘어나면 된다! 와우! ㅎ

.
├── 404.html
├── 500.html
├── favicon.ico
├── global.css
├── global.js
├── dashboard
│   ├── index.css
│   ├── index.html
│   └── index.js
├── hover-min.css
├── imgs
│   ├── ...
├── index
│   ├── index.css
│   ├── index.html
│   └── index.js
├── index.css -> ./index/index.css
├── index.html -> ./index/index.html
└── index.js -> ./index/index.js

이렇게 해두고 nginx conf file에서 심플하게 server block 아래 하나면 된다. (ssl이나 port 등의 설정은 좀 생략해둔 설정값이다.)

server {
	...
    
    root /home/ubuntu/velog-dashboard/nginx/pages;

    # 기본 index 파일 처리
    location / {
        try_files $uri $uri/ $uri/index.html =404;
    }

    # 기본 요청 처리 (velog-dashboard.kro.kr -> index/index.html)
    location = / {
        try_files /index.html =404;
    }

    # api 요청 리버스 프록시 세팅
    location ^~ /api/ {
	proxy_set_header        Host $host;
	proxy_set_header        X-Real-IP $remote_addr;
	proxy_set_header        X-Forwarded-For $proxy_add_x_forwarded_for;
	proxy_set_header        X-Forwarded-Proto $scheme;
	proxy_pass http://localhost:...;
    }

    # 오류 페이지 처리
    error_page 404 /404.html;
    error_page 500 502 503 504 /500.html;
    
    ...
}

그리고 index file을 domain만 치고 들어오는, index로 제대로 세팅해주기 위해 아래 "심볼릭 링크" 들을 만들어 준다 :)

ln -s ./index/index.html index.html
ln -s ./index/index.js index.js
ln -s ./index/index.css index.css

이제 사실 무적이다! docker compose로 말때도 github에서 확인가능한 형태로 볼륨 잡아주면 끝이다! 여기까지 세팅하면 항상 마치 다 끝낸 것 처럼 기분이 좋다ㅎㅎ 이게 시작인데ㅎ

Vanilla JS file handling

바닐라js만 사용하다 보니 심플하고 통일된 구조로 만들었다.

크게 3가지 색션이다. (1) 동기식 main - js 실행 진입점, (2) 기타 function, (3) 바인딩 될 event 세팅 으로 나누어 진다. 동기식 진입점은 디버깅 및 의존성이 있는 실행에 (상대적으로) 유리하다. use strict엄격한 모드 실행 을 위해서 작성했다.

Vanilla JS file split

이건 생각보다 꽤 야매스러운 접근인데, 각 페이지 디렉토리의 index.html 에 import 할 때 아래와 같이 공통된 형태로 셋업한다.

그러면 global.js 를 root module 처럼 편하게 활용할 수 있다 ㅎ 여기서 fetch를 래퍼함수로 감싸서 새로 만들어서 활용한다든지 등을 편하게 했다. utils 성격도 활용이 가능하다 ㅎ

3) 실시간 데이터 동기화

dashboard는 실시간으로 데이터를 불러오는게 기본이다. 이를 위해 http request based polling vs websocket 을 고민했는데, 무자본 개발자에게 후자는 미래가 두렵기도하고, 구현복잡도와 보안에 대한 고려를 했을때 일단 시작은 http request polling을 선택하기로 했다.

/**
 * 주어진 함수를 정해진 간격으로 폴링하며, 특정 조건이 충족되면 폴링을 중지합니다.
 * 
 * @param {Function} fn - 폴링할 비동기 함수입니다.
 * @param {number} interval - 폴링 간격(밀리초 단위)입니다.
 * @param {Function} stopCondition - 폴링을 중지할 조건을 결정하는 함수입니다. 이 함수는 폴링 함수의 결과를 매개변수로 받아 boolean 값을 반환해야 합니다.
 * @returns {Function} 폴링을 중지하는 함수를 반환합니다. 이 함수를 호출하면 폴링이 중지됩니다.
 */
const polling = (fn, interval, stopCondition) => {
    let intervalId = setInterval(async () => {
        try {
            const result = await fn();
            // console.log('Polling result:', result);
            if (stopCondition(result)) {
                clearInterval(intervalId);
                console.log('Polling stopped.');
            }
        } catch (error) {
            console.error('Polling error:', error);
            clearInterval(intervalId); // 에러 발생 시 폴링 중지
        }
    }, interval);

    return () => {
        clearInterval(intervalId);
        // console.log('Polling has been manually stopped.');
    }; // 폴링을 수동으로 중지할 수 있는 함수 반환
};

특정 api call을 setInterval 로 polling을 구현한 wrapper function을 만들어서 사용했다. 실제 사용은 아래와 같이 사용했다.

30초 interval로 userInfo를 update한다. token-refresh의 영향으로 login해서 들어온 유저의 auth 정보마저도 바뀌기 때문에 저장된 token 값을 계속해서 refresh 해준다 :)

그리고 fetch web API를 wrapper function 하나 만들어 사용했다. (자세한 사항은 깃허브에서 더 확인 가능합니다! >> "velog-dashboard"

앞으로 해당 코드들을 조금 더 common module의 성격에 맞게, Object 성격을 살려서 리펙토링할 예정이다. DI 와 같은 부분도 고려해서 말이다. 그리고 DOM의 생명주기에 대한 고려가 깊게 되어 있지도 않다.

dynamic rendering

동적인 화면 랜더링은 innerHTML 을 활용한 고전적인 방법을 사용했다. 위에서 언급한 updateUserInfo 의 코드 전체 부분은 아래와 같다.

const updateUserInfo = async () => {
    const userInfo = JSON.parse(localStorage.getItem("userInfo"));
    const res = await getData(`/user/${userInfo.userId}`, {}, { accessToken: userInfo.accessToken, refreshToken: userInfo.refreshToken });
    const { koreanDate, koreanTime } = krDateAndTime(res.userInfo.lastScrapingAttemptTime);

    // userInfo를 가져와서 lastScrapingAttemptTime, lastScrapingAttemptResult 랜더링
    document.getElementById("userInfo-lastScrapingAttemptTime").innerHTML = `
        <div>
            <span>업데이트 시간</span>
            <span>
                ${koreanDate}</br>
                ${koreanTime}
            </span>
        </div>
    `;
    document.getElementById("userInfo-lastScrapingAttemptResult").innerHTML = `
        <div>
            <span>업데이트 결과</span>
            <span>${res.userInfo.lastScrapingAttemptResult}</span>
        </div>
    `;

    // 저장된 로컬스토리지도 업데이트 (토큰 리프레싱때문)
    localStorage.setItem("userInfo", JSON.stringify(res.userInfo));
    return res;
};

document.getElementById 으로 element를 가져와 innerHTMl 이라는 attribute 를 control해서 dynamic rendering을 한다. 개인적으로 simple 한 페이지는 어떤 라이브러리도 없이 이렇게 퓨어하게 만드는 것을 선호한다.

요즘 라이브러리를 사용하다보면 오히려 목적 보다 수단을 해결하는데에 피로함을 많이 느껴서, 규모가 어느정도 있는 작업이 아니고서야 react 와 같은 framework는 쉽게 손이 안간다.

4) 그래프 랜더링

chart.js 의 filter 와 css 세팅하는데 생각보다 고생했다... FE 어려워요... 일단 chart는 dynamic redering이 되어야 했다. 아니면 게시글이 많은 유저의 dashboard는 랜더링 되는데 까지 몇 시간이 걸릴 지도, 아니 컴퓨터가 멈출지도 모른다.

일단 그래프 관련해서는 크게 기본적으로 3가지 로직으로 나눠져야 했다.

1. data fetch 하는 부분

일단 post list 역시 동적으로 랜더링되고 있다. 랜더링될 때 onclick="updatePostListGraph(event)" 으로 이벤트를 바인딩한다. 즉 해당 버튼에 바인딩된 이벤트를 정의하는 함수가 위와 같다.

여기서 "날짜 필터" 역시 동적 랜더링 되고, 필터링 이벤트를 동적 바인딩 까지 해야한다!

2. data를 가지고 target element(dom, 주로 canvas)에 실제로 chart를 최초로 그리는 부분

앞선 클릭이벤트를 통해 최초 chart 가 그려지고, filtering 때문에 그려진 chart object를 return 한다.

3. 그려진 chart에 filter를 추가하는 부분

(여기서 주의할 점은 코드가 클린하지 않다는 점이다 ㅜㅠ) 우선 저렇게 단일 function에서 code 로 행위 자체가 바뀌면 클린하지 못한 코드이다. 그걸 알면서도 나..는.. total view 의 daily graph 때문에.. 끄흡.. 핑계

return 된 chart object 대상으로 filtering 하는 event를 바인딩하고, 그 바인딩 될 이벤트에 대한 코드다. 이미 받아오고 가져온 data 를 기반으로 다시 랜더링 한다.

여기서 멀티테넌시 + 더 큰 데이터 규모 였다면 프레임워크가 그리웠을 것이다.. 휴

5) 그 외

모달은 sweetalert2HTML 임베팅할 수 있는 attribute을 활용했고, css는 써드파티 없이 처음부터 flex 위주만 사용해서 세팅했다. velog의 black theme 디자인 시스템을 그대로 follow up 했다.

미디어쿼리 사용은 최대한 지양하려고 했으나, 대시보드 특성상 한 화면에 보여주는 정보가 많아서 사용했다. flex directionmax-width 정도만 컨트롤했다.

아 js로 Mobile 감지는 개인적으로 Window.matchMedia() 를 추천한다!

위 사진은 아쉬운대로 취합해서 묶어서 올려보는 사진이다 :) 지금은 아래와 같은 사진을 마주할 수 있다!


2. 짧지만 굵었던 이슈 체크

1) 로그인이 안돼요!

이메일 인증 또는 이메일 없이 회원가입이 가능하던 시절 가입했던 분들은 email이 없을 수 있다. 이 점을 간과했다.. email 을 당연하게 unique 로 박아두었더니 해당 시절 유저분들은 접근할 수 없었다.

2) worker (github action): "죽여줘...!"

이제는 Worker 로직, github action이 죽여줘를 외치기 시작했다. 이게 무..무슨 일이지..?!

다시 생각해 보는 test code의 중요성 ㅎ happy case 에 대한 고려 위주로 되어 있다 보니, key error, out of index 등의 아주 간단한 이슈에 대한 고려가 잘 안되어 있었다..

"게시글이 0개인 경우" 당연하게 list(posts.keys())[0] 에서 list out of index issue가 생긴다. 생각이 매우 짧았다 해당 issue handling이 잘 안되어있어서 exit 하지 못하고 action이 죽여줘를 외치다가 다음 스케쥴에 떠밀려 저 지경까지 되었다..!

3) 그 외

worker의 "살려줘...!" 이슈

생각보다 많은 사용자 분들이 등록해 주셨고, 통계 보기 API의 트랜잭션 타임이 튀기 시작했다. 해당 이슈가 velog 자체에 대한 영향을 주었고

velopert 님의 말을 조금 붙이자면, "애초에 통계 목적으로 저장 및 활용이 되던 데이터가 아니었기에 이번 기회에 통계에 맞추어 바꾸신다고" 말씀 주셨다. 감사합니다!


3. 앞으로

초기 세팅, 설정을 벗어나 이제 "신규 기능 개발""리팩토링 & QA & 이슈 핸들링에 대한 기록" 위주로 남기려고 했다. 그리고 추후 traffic 을 보고 도커라이징 이나 DevOps, SPOF 등의 조금 더 고도화된 얘기를 하려고 했다 ㅎㅎ

사실 velog dashboard는 앞으로 진행하려는 프로젝트의 시발점이었기 때문이고, 원래 계획으로는 velog-dashboard 는 이제 (1) global dashboard 기능과 (2) deep analyze (자연어 처리를 기반으로 한 키워드 추출, velog post data GPT 기반 데이터 분석) 기능의 신규 피쳐를 앞두고 있었다.

그리고 이를 기반으로 velog에 국한되지 않고 좀 다양한 플랫폼 대상으로 다양한 시도를 할 예정이었다. :)

2023년 11월 18일 오픈을 하고, 많이 부족하고 니치한 이 서비스에 생각보다 많은 분들이 테스트해주고 피드백을 주셨다.

압도적,, 감사합니다,, 감사합니다! 😭 피드백을 남겨주신 분들 모두 감사합니다. 특히 이슈 트래킹 및 체크까지 해주셨던 jaehojung94 님, chloe_ 님, 가장 많은 사용을 해주신 jochedda 님, 그 외 채널톡 및 메일로 의견 주신분들 모두 너무 감사합니다!!

profile
도메인 중심의 개발, 깊이의 가치를 이해하고 “문제 해결” 에 몰두하는 개발자가 되고싶습니다. 그러기 위해 항상 새로운 것에 도전하고 노력하는 개발자가 되고 싶습니다!

4개의 댓글

comment-user-thumbnail
2023년 11월 23일

와 재밌게 잘보고 있습니다!
저도 옆집에서 비슷한 뭐 뷰어쉽뭐시기 사이트를 만들어서 운영해보고 있는데요.. 이 제작기 시리즈를 보며 저 역시도 배우는게 많고 알아야하는 부분이 많은것 같아서 너무 좋은 글이라고 생각했습니다!

제가 개발적인 부분은 진짜 할말이 없는데 딱하나만 말씀드리고 싶은건..
폰트만.. 나눔고딕 대신에 Pretendard라도.. 흐긓ㄱㄱㅠㅠ
https://cactus.tistory.com/306

1개의 답글
comment-user-thumbnail
2023년 12월 4일

현우님 살릴수 있도록 기원드립니다!!! 기도하겠습니다!! 다음글 기대됩니다 ㅎㅎ

1개의 답글