대시보드-따라하기 시리즈 1편. 카드를 스무 장 넘게 붙여도 버티는 뼈대를 만든다. 핵심은 "수집기 규약"과 "실패에 강한 스케줄러" 두 가지다.
폴더 구조
server.js HTTP 서버, /api/state, 정적 파일
lib/scheduler.js 수집기를 주기적으로 실행
lib/http.js 타임아웃·오류 처리를 넣은 fetch 래퍼
collectors/*.js 카드 하나당 파일 하나
config/*.json 종목·검색어 같은 설정
public/index.html 화면 전부
수집기 규약
수집기는 이름, 주기, 그리고 데이터를 돌려주는 함수만 내보낸다.
// collectors/weather.js
module.exports = {
name: 'weather',
title: '날씨',
intervalMs: 10 * 60e3,
timeoutMs: 20e3,
enabled: (env) => !!env.WEATHER_LAT, // 설정이 없으면 카드를 끈다
async fetch(ctx) {
const r = await ctx.http.json('https://api.example.com/forecast?...');
return { now: r.current, hourly: r.hourly.slice(0, 12) };
},
};
server.js 는 수집기를 폴더 스캔이 아니라 명시 목록으로 불러온다. 순서가 곧 화면 순서이고, 지운 파일이 실수로 다시 실행되는 일도 없다.
스케줄러
setInterval 대신 수집기마다 setTimeout 을 이어 붙인다. 한 번 끝난 뒤에 다음 실행을 잡으므로 느린 API 때문에 실행이 겹치지 않는다.
const MAX_BACKOFF = 30 * 60e3;
async function run(c) {
const s = state[c.name];
if (running[c.name]) return;
running[c.name] = true;
let timer;
try {
const data = await Promise.race([
c.fetch(ctx),
new Promise((_, rej) => { timer = setTimeout(() => rej(new Error('timeout')), c.timeoutMs || 60e3); }),
]);
Object.assign(s, { data, updatedAt: Date.now(), error: null, failures: 0 });
} catch (e) {
s.error = e.message; s.failures++; // data 는 건드리지 않는다 → 화면엔 직전 값
} finally {
clearTimeout(timer);
running[c.name] = false;
const delay = s.failures ? Math.min(c.intervalMs * 2 ** s.failures, MAX_BACKOFF) : c.intervalMs;
setTimeout(() => run(c), delay);
}
}
- 실패해도
data를 지우지 않는다. 화면에는 직전 값과 "몇 분 전 갱신"이 보인다. 카드가 텅 비는 것보다 조금 오래된 값이 낫다. - 지수 백오프. 실패가 이어지면 간격을 두 배씩 늘리되 30분을 넘기지 않는다. 죽은 API 를 두들겨서 차단당하는 일을 막는다.
- 첫 실행은 0~5초 사이 무작위 지연. 기동 직후 스무 개가 한꺼번에 나가면 같은 호스트의 호출 제한에 걸린다.
겪은 함정: "지금 실행"이 건너뛰어진다
화면에서 값을 저장한 직후 해당 수집기를 즉시 다시 돌리는 runNow() 를 만들었는데, 가끔 저장 전 값이 그대로 보였다. 마침 그 수집기가 실행 중이면 running 검사에 걸려 그냥 돌아와 버렸기 때문이다. 진행 중인 실행은 저장 전 데이터를 읽고 있었다.
async runNow(name) {
if (inflight[name]) await inflight[name]; // 진행 중인 실행이 끝나길 기다리고
await start(byName[name]); // 한 번 더 돌린다
return state[name];
}
"중복 실행 방지"와 "최신 값 보장"은 다른 요구다. 건너뛰기만 하면 앞의 것만 만족한다.
화면: 태블릿에서 배운 것
- 카드 안쪽 스크롤을 없애고 페이지 스크롤 하나만 남겼다. 터치로 넘기다 카드 안에 갇히는 일이 사라진다. 긴 목록은 "더보기"로 펼친다.
- 안쪽 스크롤을 없애면 줄바꿈 안 되는 긴 문구가 카드 밖으로 삐져나와 페이지가 가로로 넓어진다.
.card { overflow: hidden }과html, body { overflow-x: clip }로 막았다. - 60초마다 다시 그릴 때 펼침 상태, 선택한 탭이 초기화되지 않게 상태를 따로 기억한다.
- 지도 카드는 터치 기기에서 전체화면일 때만 드래그를 받는다. 그러지 않으면 페이지를 내리려다 지도만 움직인다.
- 새로고침하면 브라우저가 이전 스크롤 위치로 돌아가려 한다. 대시보드는 항상 맨 위에서 시작하는 편이 낫다:
history.scrollRestoration = 'manual'.
로컬에서 돌리기
node --env-file=.env server.js
수집기 하나만 따로 실행해 결과를 찍어 보는 스크립트를 만들어 두면 새 카드를 붙일 때 서버를 띄우지 않아도 된다.
다음 편은 카드에 넣을 데이터를 어디서 구하는지다. 시리즈 전체 목록은 여기.
댓글 없음:
댓글 쓰기