귀찮은개발자9 min

매일 아침 텔레그램이 브리핑을 보내준다

2026년 4월 · 귀찮은개발자 EP.13

2026년 4월 · 귀찮은개발자 EP.13

매일 아침 습관이 있었다. 눈 뜨면 앱 5개를 연다. Apsity 대시보드에서 매출을 보고, FeedMission에서 피드백을 확인하고, Gmail을 열고, 날씨 앱을 보고, 캘린더를 연다. 다 합치면 15분이다. 그 15분 동안 한 거라고는 숫자 읽기뿐이다.

EP.10에서 MCP로 50개 도구를 만들었다. EP.12에서 텔레그램 에이전트를 붙였다. 도구는 다 있는데 매일 아침 내가 직접 물어봐야 했다. “오늘 매출 어때?” “일정 뭐 있어?” 이것도 귀찮아졌다.

물어보지 않아도 알아서 보내주면 안 될까. 매일 아침 9시에 텔레그램이 울리고, 열어보면 오늘 필요한 정보가 다 정리돼 있으면 좋겠다. 만들기로 했다.

빠르게 보기

– 매일 아침 9시, 텔레그램 4개 채널로 브리핑 자동 발송
– 비즈니스/블로그/일상/차량 — 채널별로 정보를 분리
– 이상 감지: 매출 20% 변동, 저평점 리뷰, 키워드 순위 하락 시 즉시 알림
– node-cron으로 스케줄, pm2로 상시 실행, 맥미니에서 24시간 가동

왜 4개 채널인가

처음에는 채널 하나에 다 넣었다. 매출, 날씨, 일정, 차량 상태가 한 메시지에 쭉 나왔다. 이틀 써보고 바꿨다.

문제는 알림 피로였다. 차량 알림은 이상 있을 때만 보면 된다. 근데 매출 브리핑이 올 때마다 차량 정보도 같이 딸려왔다. 반대로 매출은 매일 꼭 봐야 하는데, 메시지가 길어지니까 스크롤하다 놓치는 항목이 생겼다.

정보 종류가 다르면 채널도 달라야 했다. 네 개로 쪼갰다.

biz — 비즈니스
Apsity 앱 매출, FeedMission 구독 통계. 매일 아침 꼭 확인해야 하는 숫자들이다.
blog — 검색/트래픽
Google Search Console 데이터. 어떤 키워드로 유입이 되고 있는지, 순위가 올랐는지 떨어졌는지.
life — 일상
날씨, Gmail 요약, 오늘 일정, 기술 트렌딩. 집 나서기 전에 한 번 훑어보는 용도다.
car — 차량
기아 커넥트에서 차량 상태를 가져온다. 연료 부족이나 경고가 있을 때만 메시지가 온다. 정상이면 아무것도 안 온다.
텔레그램 4개 채널에 브리핑 메시지가 분리 수신된 화면
4개 채널로 분리된 브리핑. 정보 종류별로 따로 온다 / GoCodeLab

이렇게 나누니까 확실히 편했다. 아침에 biz 채널만 보면 매출 파악이 끝난다. life 채널에서 날씨랑 일정을 확인한다. car 채널에 알림이 없으면 차는 괜찮다는 뜻이다. 채널마다 알림 설정도 다르게 걸어놨다. biz는 항상 울리게, car는 무음으로.

스케줄러: 알람 시계 코드 버전

node-cron이라는 도구를 썼다. 쉽게 말하면 스마트폰 알람이다. “매일 아침 9시에 이 코드를 실행해”라고 설정하면 된다. 스마트폰 알람이 정해진 시간에 소리를 울리듯, node-cron은 정해진 시간에 함수를 호출한다.

코드는 이렇게 생겼다.

// 모닝 브리핑: 매일 09:00 (서울 시간)
cron.schedule(‘0 9 * * *’, runMorningBriefing, {
  timezone: ‘Asia/Seoul’,
})

// 이상 감지: 하루 4번 (09, 13, 17, 21시)
cron.schedule(‘0 9,13,17,21 * * *’, runAnomalyCheck, {
  timezone: ‘Asia/Seoul’,
})

‘0 9 * * *’가 cron 표현식이다. 왼쪽부터 분, 시, 일, 월, 요일이다. “매일 9시 0분”이라는 뜻이다. 이 한 줄이면 매일 아침 9시에 브리핑 함수가 자동으로 실행된다.

이상 감지는 하루 4번 돌린다. 9시, 13시, 17시, 21시. 매출이 갑자기 떨어지거나 별점 1점 리뷰가 달리면 빨리 알아야 하니까. 4시간 간격이면 최대 4시간 안에는 알 수 있다.

모닝 브리핑이 하는 일

아침 9시가 되면 4개 채널에 메시지가 동시에 날아간다. 내부적으로는 이렇게 돌아간다.

// 4개 채널 병렬 발송
const results = await Promise.allSettled([
  sendBizBriefing(),    // Apsity + FeedMission
  sendBlogBriefing(),   // Search Console
  sendLifeBriefing(),   // 날씨 + Gmail + 캘린더 + 트렌딩
  sendCarBriefing(),    // 차량 상태 (이상 시에만)
])

Promise.allSettled를 쓴 이유가 있다. 4개를 동시에 보내는데, 하나가 실패해도 나머지는 그대로 간다. 날씨 API가 먹통이어도 매출 브리핑은 정상적으로 도착한다. 한 군데 고장났다고 전체가 멈추면 안 되니까.

biz 채널은 Apsity에서 앱 매출을 가져오고, FeedMission에서 구독 통계를 가져온다. 두 서비스에 동시에 접속해서 숫자를 모은 다음 한 메시지로 보내준다.

car 채널은 좀 다르다. 기아 커넥트 API로 차량 상태를 확인하는데, 연료 부족이나 경고 메시지가 있을 때만 발송한다. 차가 멀쩡하면 메시지가 안 온다. 매일 “차 정상입니다”를 받을 필요는 없으니까.

이상 감지: 평소랑 다르면 알려줘

이상 감지는 브리핑과 별개로 돌아간다. 하루 4번, 다섯 가지를 검사한다.

감시 항목 5가지
1. Apsity 매출 — 전일 대비 20% 이상 변동
2. FeedMission 매출 — 전일 대비 20% 이상 변동
3. 저평점 리뷰 — 별 1~2점 리뷰 신규 등록
5. 키워드 순위 — 5계단 이상 하락

원리는 단순하다. 어제 숫자를 저장해두고, 오늘 숫자랑 비교한다. 차이가 기준치를 넘으면 알림을 보낸다. 매출이 어제 $50이었는데 오늘 $38이면 -24%니까 알림이 간다.

여기서 중요한 게 중복 방지다. 같은 이상이 4시간마다 반복해서 울리면 짜증난다. memory 모듈이 이걸 해결한다.

// 이미 보낸 알림인지 확인
const alertId = `apsity_revenue_2026-04-07`
const alreadySent = await memoryTools
  .memory_check_alert.handler({ alertId })

if (alreadySent === ‘not_sent’) {
  // 텔레그램 발송 + 기록 저장
  await memoryTools.memory_log_alert.handler({
    alertId,
    type: ‘revenue_change’,
    expiresInHours: 24,
  })
}

memory 모듈은 data/alerts.json 파일에 알림 이력을 저장한다. “이 알림은 24시간 동안 유효하다”고 기록해두면, 다음 검사 때 같은 항목이 걸려도 이미 보낸 알림이니까 건너뛴다. 24시간이 지나면 만료돼서 다시 보낼 수 있게 된다.

이전 상태 저장도 같은 방식이다. data/state.json에 마지막으로 확인한 매출 금액, 키워드 순위를 기록해둔다. 다음 검사 때 이 파일을 읽어서 현재 값이랑 비교한다. 파일 기반이라 DB 없이도 돌아간다.

맥미니에서 24시간 돌리기

스케줄러는 컴퓨터가 켜져 있어야 돌아간다. 맥미니를 서버로 쓰고 있어서 여기에 올렸다.

pm2를 썼다. pm2는 프로그램 매니저다. 내 코드를 백그라운드에서 실행하고, 죽으면 자동으로 다시 살려준다. 컴퓨터를 껐다 켜도 알아서 복구된다.

터미널에서 스케줄러가 시작되고 cron 스케줄이 등록되는 로그
스케줄러 시작 로그. 브리핑 09:00, 이상 감지 4회가 등록된다 / GoCodeLab
# pm2로 스케줄러 등록
$ pm2 start dist/scheduler.js –name lazydev-scheduler

# 현재 상태 저장 (재부팅 후 복구용)
$ pm2 save

# 맥 시작 시 자동 실행 등록
$ pm2 startup

pm2 startup 명령이 핵심이다. 이걸 실행하면 macOS의 launchd에 등록된다. launchd는 맥의 자동 실행 시스템이다. 맥이 켜지면 launchd가 pm2를 살리고, pm2가 내 스케줄러를 살린다. 체인처럼 연결돼 있다.

한 가지 문제가 있었다. pm2로 띄우면 환경변수(.env)를 못 읽었다. 환경변수는 비밀번호 모음 파일이다. 텔레그램 봇 토큰, DB 접속 정보 같은 민감한 값들이 여기 들어 있다. 터미널에서 직접 실행하면 잘 되는데, pm2가 띄우면 빈 값이 나왔다.

원인은 간단했다. pm2는 새로운 프로세스를 만들 때 현재 터미널의 환경을 물려받지 않는다. 해결법도 간단했다. 코드 맨 위에 dotenv를 import하면 된다.

// scheduler.ts 맨 첫 줄
import ‘dotenv/config’

// 이 한 줄이 .env 파일을 읽어서
// process.env에 값을 채워준다

dotenv/config를 import하면 프로그램이 시작될 때 .env 파일을 읽어서 환경변수에 넣어준다. pm2든 직접 실행이든 상관없이 항상 값이 들어간다. 이거 한 줄 빼먹어서 30분 고민했다.

실제 하루

아침 9시에 폰이 울린다. 텔레그램 알림 4개.

biz 채널을 연다. “Apsity 오늘 매출 $52.40, 전일 대비 +18%.” FeedMission MRR $420, 신규 구독 2건. 5초면 읽는다.

life 채널을 본다. 서울 맑음 22도, 오후에 비 올 수 있음. 오늘 일정 3건. 읽지 않은 메일 2통. 우산 챙기고 나간다.

car 채널은 알림이 없다. 차는 괜찮다는 뜻이다.

오후 1시에 biz 채널에 알림이 하나 더 온다. “Apsity 매출 전일 대비 -22%.” 이상 감지가 잡았다. 대시보드를 열어보니 특정 앱의 키워드 순위가 떨어진 거였다. 바로 대응할 수 있었다.

예전이었으면 저녁에 대시보드 열어보고서야 알았을 거다.

안 된 것들

pm2 환경변수 문제. 위에서 말한 대로 dotenv/config import 한 줄로 해결했다. 근데 원인 찾는 데 시간이 걸렸다. 터미널에서는 되는데 pm2에서만 안 되니까 코드 문제가 아니라 환경 문제라는 걸 깨닫는 데 시간이 걸린 거다.

텔레그램 메시지 길이. 텔레그램 메시지는 4096자까지만 보낼 수 있다. 처음에 브리핑 전체를 한 메시지에 넣으려다가 잘렸다. 채널을 나누니까 자연스럽게 해결됐다. 채널당 메시지 길이가 줄어드니까.

첫 실행 시 이상 감지 오류. 이상 감지는 이전 상태랑 비교하는 건데, 첫 실행에는 이전 상태가 없다. state.json이 없으면 비교 자체를 건너뛰도록 처리했다. 두 번째 실행부터 정상 동작한다.

구조 정리

전체 흐름을 정리하면 이렇다.

스케줄
– 브리핑: 매일 09:00 KST
– 이상 감지: 09:00, 13:00, 17:00, 21:00 KST

브리핑 흐름
– scheduler.ts가 cron 시간에 맞춰 실행
– 각 채널 함수가 MCP 모듈을 호출해서 데이터 수집
– sendTelegram()으로 채널별 발송
– 4개 채널 병렬 처리 (하나 실패해도 나머지 정상)

이상 감지 흐름
– anomaly_check가 5개 항목 순차 검사
– data/state.json에서 이전 값 로드
– 현재 값과 비교, 기준치 초과 시 알림 후보 등록
– data/alerts.json에서 중복 확인 (이미 보냈으면 건너뜀)
– 이상이 있을 때만 biz 채널로 발송

상시 실행
– pm2가 프로세스 관리 (죽으면 자동 재시작)
– pm2 startup + launchd로 맥 재부팅 후 자동 복구

기술 선택 이유

왜 node-cron인가. 맥의 crontab을 쓸 수도 있었다. 근데 crontab은 Node.js 밖에 있다. 환경변수 설정도 따로 해야 하고, 에러 로그도 따로 잡아야 한다. node-cron은 코드 안에 있으니까 타입스크립트로 한 번에 관리된다.

왜 pm2인가. systemd도 있고 Docker도 있다. 근데 맥미니에서 Node.js 하나 돌리는 데 Docker는 과하다. pm2는 npm install 한 번이면 끝이다. 로그도 알아서 남기고, 재시작도 알아서 한다.

왜 파일 기반 메모리인가. Redis나 SQLite를 쓸 수도 있었다. 근데 저장하는 게 state.json, alerts.json 두 개뿐이다. 크기도 수 KB 수준이다. 파일로 충분하다. 나중에 커지면 그때 바꾸면 된다.

현재 상태

일주일째 돌리고 있다. 한 번도 안 죽었다. 매일 아침 9시에 4개 알림이 온다. 이상 감지가 실제로 잡은 건 3번이다. 매출 하락 1번, 저평점 리뷰 1번, 키워드 하락 1번. 셋 다 대시보드를 열기 전에 텔레그램에서 먼저 알았다.

아침 루틴이 바뀌었다. 앱 5개 여는 15분이 텔레그램 읽는 30초로 줄었다. 이상이 없으면 안 봐도 된다. 이상이 있으면 바로 안다.

다음 편에서는 기아 커넥트 API를 직접 연결해서 내 차 데이터를 Claude로 조회하는 이야기를 할 거다. EP.14에서 계속된다.

자주 묻는 질문

Q. node-cron이 뭔가요?

스마트폰 알람 같은 거다. “매일 아침 9시에 이 코드를 실행해”라고 설정하면, 서버가 켜져 있는 동안 정해진 시간에 자동으로 함수를 호출한다. npm 패키지 하나 설치하면 쓸 수 있다.

Q. pm2가 뭔가요?

프로그램 매니저다. 내 코드를 백그라운드에서 실행하고, 죽으면 자동으로 다시 살려준다. 맥을 껐다 켜도 알아서 다시 시작한다. Node.js 프로그램을 24시간 돌릴 때 쓴다.

Q. 환경변수(.env)가 뭔가요?

비밀번호 모음 파일이다. API 키나 토큰 같은 민감한 정보를 여기에 적어둔다. 코드에 직접 쓰면 위험하니까 별도 파일에 빼놓는 거다. dotenv라는 도구가 이 파일을 읽어서 프로그램에 넣어준다.

Q. 이상 감지(anomaly detection)가 뭔가요?

평소랑 다른 숫자가 나오면 알려주는 기능이다. 매출이 갑자기 20% 이상 떨어지거나, 별점 1점 리뷰가 달리거나, 키워드 순위가 5계단 이상 빠지면 텔레그램으로 알림이 온다.

Q. 서버 없이도 되나요?

스케줄러는 컴퓨터가 켜져 있어야 돌아간다. 맥미니나 라즈베리파이처럼 항상 켜놓을 수 있는 장비가 필요하다. 아니면 클라우드 서버를 써도 된다. AWS나 DigitalOcean 같은 곳에서 월 $5 정도면 충분하다.

관련 글