
나는 여전히 코드를 작성할 줄 모른다. 하지만 이제 내 휴대폰은 홈 화면 위젯으로 등록한 에이전트를 실행하기 위해 오전 8시에 스스로 깨어난다.
요약
사용자가 잠든 사이에도 Android의 AlarmManager를 활용해 에이전트가 스스로 작업을 수행하도록 구현하는 기술적 방법을 다룹니다. Doze 모드 환경에서도 정확한 스케줄링을 위해 OS 레벨의 알람 시스템을 사용하는 원리와 주의사항을 설명합니다.
핵심 포인트
- AlarmManager를 사용하여 Doze 모드에서도 백그라운드 에이전트 실행 가능
- 단순한 시간 설정보다 명확한 작업(Task) 정의가 에이전트 품질의 핵심
- Android 제조사의 배터리 최적화 정책에 따른 실행 불확실성 주의 필요
- 정확한 알람 권한 및 OS 버전에 따른 동작 차이 고려
지난 포스트 이후, 에이전트는 더 이상 나를 기다리지 않는다
지난번에는 AI가 OS로 들어왔습니다. Codex 이벤트를 실시간으로 읽는 에이전트 채팅 (Agent Chat) 창, 할당량 바(quota bars)와 작성 상자(compose box)가 있는 홈 화면의 스카우터 (Scouter) 위젯, 카테고리별 알림 채널, 그리고 마침내 접히는 동작을 견뎌내는 Z Fold가 그것입니다.
이 모든 것들은 제가 나중에야 깨달은 하나의 가정을 공유하고 있었습니다. 이 모든 기능들은 제가 휴대폰을 보고 있을 때만 무언가를 수행한다는 점입니다. 위젯은 상태를 보여주었습니다. 채팅 창은 상태를 보여주었습니다. 알림은 무언가 이미 일어났음을 알려주었습니다. Shelly는 여전히 제가 시작해주기를 기다리는 프로세스를 들여다보는 아주 좋은 창(window)이 되어 있었습니다.
그래서 다음 질문은 명백했고, 저는 그것을 피해왔습니다. 내가 잠든 동안 에이전트가 실행되게 하려면 무엇이 필요할까?
@agent, AlarmManager, 그리고 Doze
당신은 Ask 박스에 평범한 영어로 이렇게 작성합니다:
@agent every day at 8am summarize the news and notify me
그다음에 일어나는 일이 제가 관심을 갖는 부분입니다. Shelly는 cron 데몬 (cron daemon)을 실행하지 않습니다. 시계를 폴링 (poll) 하기 위해 포그라운드 서비스 (foreground service)를 계속 유지하지도 않습니다. 대신 setExactAndAllowWhileIdle을 사용하여 AlarmManager를 통해 실제 Android 알람을 등록합니다. AlarmManager는 OS 자체의 스케줄러(scheduler)로, 당신의 알람 시계 앱이 사용하는 것과 동일하며 Doze를 통해서도 작동합니다. 화면이 꺼져 있고, 앱이 스와이프되어 종료되었으며, 휴대폰이 협탁 위에 뒤집혀 놓여 있어도 상관없습니다. 오전 8시에 에이전트는 깨어나서, 당신의 API 키와 도구(tools)를 사용하여 실행된 후 그 결과를 알림창 (notification shade)에 게시합니다.
파서 (parser)는 반론을 제기합니다. 만약 요청이 모호하다면, 파서는 제가 지금까지 사용해온 모든 스케줄링 UI가 하는 방식처럼 시간부터 묻지 않습니다. 대신 '무엇'을 할 것인지 묻습니다. "뉴스를 요약해줘"는 작업(task)이 아닙니다. "Hacker News 메인 페이지와 내가 별표를 표시한 RSS 피드를 요약해줘"가 작업입니다. 시계를 설정하기 전에 명사(noun)를 정확하게 정하는 것이 예상보다 훨씬 더 중요하다는 사실이 밝혀졌습니다. 왜냐하면 정해진 시간에 완벽하게 실행되지만 쓸모없는 결과물(mush)을 만들어내는 스케줄링된 에이전트는 에이전트가 아예 없는 것보다 더 나쁩니다. 매일 아침 영원히 쓸모없는 결과물만 만들어낼 것이기 때문입니다.
솔직한 주의 사항을 말씀드립니다. Doze 모드를 통한 정확한 알람 (Exact alarms)은 OS 계약 사항이며, Android OEM(제조사)들은 자체적인 배터리 최적화 도구로 이 계약을 조용히 위반해 온 길고 지저분한 역사를 가지고 있습니다. 제 Fold 기기에서는 작동합니다. 하지만 백그라운드 작업을 공격적으로 종료하는 제조사의 기기라면 결과가 정말 다를 수 있으며, 앱이 예외 처리를 요청하는 것 외에 이를 해결할 방법은 없습니다. 앱 내부에도 동일한 문제의 축소판이 존재합니다. 만약 정확한 알람 (exact-alarm) 권한이 부여되지 않았거나 Android 12 이전 버전의 기기를 사용 중이라면, 부정확한 알람 (inexact alarm)으로 대체됩니다. 즉, 알람이 울리기는 하되 반드시 오전 8시 0분 0초에 울린다는 보장은 없습니다. 스케줄이 완벽하다고 가장하기보다는 차라리 이렇게 솔직하게 말씀드리는 편이 낫겠습니다.
결과물이 전달되는 곳
휴대폰을 손에 들고 있을 때는 알림 (Notification)만으로도 충분합니다. 하지만 휴대폰을 들고 있지 않은 것이 핵심인 상황에서는 알림은 무용지물입니다.
따라서 실행 결과는 알림창보다 더 오래 지속되는 어딘가로 전달될 수 있습니다. 만약 노트에 기록되길 원한다면 Obsidian 초안으로 보낼 수 있습니다. 또는 Bluesky, Discord, Slack, Telegram, Mastodon, Misskey, WordPress로 직접 보낼 수도 있습니다. 혹은 직접 경로를 지정하고 싶다면 일반적인 웹훅 (webhook)으로 보낼 수도 있습니다.
실제로 검증된 부분은 다음과 같습니다. Bluesky는 엔드 투 엔드 (end to end)로 검증되었습니다. 스케줄링된 에이전트가 실행되어, 내용을 구성하고, 실제 계정에 게시하는 과정까지 인간의 개입 없이 이루어졌습니다. 나머지 6개의 소셜 및 퍼블리싱 커넥터들은 동일한 디스패치 (dispatch) 코드 경로를 따르고 동일한 방식으로 연결되어 있지만, 아직 각각의 커넥터를 실제 계정을 대상으로 개별 실행해 보지는 않았습니다. 저는 7개를 나열하고 여러분이 알아서 추측하게 만드는 대신, 제가 실제로 작동하는 것을 확인한 것이 무엇인지 말씀드리고 있는 것입니다.
기억하는 법을 배운 에이전트들
매일 아침 제로 상태에서 시작하는 스케줄링된 에이전트는 매우 비용이 많이 드는 cron 작업일 뿐입니다. 그래서 이제 세 가지 계층의 메모리 (memory)가 존재하며, 이들은 의도적으로 분리되어 있습니다.
실행 메모리 (Execution memory)는 에이전트별로 관리됩니다. 에이전트는 지난번에 무엇을 생성했는지 기억하고 다음 실행 시 이를 회상할 수 있습니다. 따라서 여러분이 어제의 내용을 다시 말할 필요 없이, "뉴스를 요약해줘"라는 명령이 "어제 이후로 새로 나온 뉴스를 요약해줘"가 될 수 있습니다.
Skills(기술)는 제가 예상했던 것보다 훨씬 더 좋아하게 된 레이어입니다. 다단계 실행(multi-step run)이 성공하면, 그 시퀀스는 레시피(recipe)로 저장됩니다. 다음에 유사한 작업이 발생하면, 에이전트가 동일한 다섯 단계를 처음부터 다시 발견하는 대신 해당 레시피가 자동으로 재사용됩니다. 이는 일주일 동안 이곳에 있었던 비서와 오늘 아침에 시작한 비서 사이의 차이와 같습니다.
Shared memory(공유 메모리)는 모든 에이전트가 볼 수 있는 컨텍스트이며, 제가 울타리를 쳐둔 부분입니다. 여기에 기록하려면 명시적인 트리거가 필요하며, 사용자가 확인하기 전까지는 기록이 반영되지 않습니다. 어떤 에이전트든 조용히 변형(mutate)할 수 있는 에이전트 간 상태(Cross-agent state)는 모든 에이전트를 한꺼번에 오염시킬 수 있는 아주 좋은 방법이며, 모든 에이전트가 이미 틀린 상태가 된 후에야 그 사실을 알게 됩니다.
Unattended mode(무인 모드)는 의도적으로 성능을 낮추었습니다
이것은 제가 가장 많은 반대에 부딪힐 것으로 예상되는 설계 결정이므로, 직접적으로 말씀드리겠습니다.
기본적으로 무인 상태(unattended)로 실행되는 에이전트는 Codex와 로컬 도구(local tools)만 사용할 수 있습니다. 당신이 잠든 동안 당신의 Gemini 키나 Perplexity 키에 접근할 수 없습니다. 만약 그것을 원한다면, Autonomous Cloud라는 설정이 있습니다. 이는 기본적으로 꺼져 있으며, 직접 가서 켜야 합니다.
아웃바운드 네트워크 호출(Outbound network calls)은 송신 허용 목록(egress allowlist)을 가진 기능 브로커(capability broker)를 통해 라우팅될 수 있습니다. 이는 사후 경고창이나 로그 한 줄이 아니라, 호출이 기기를 떠나기 전에 반드시 통과해야 하는 허용 목록입니다. 현재 상태를 솔직히 말씀드리자면: 이 브로커는 실행 과정에서 신뢰할 수 없는 웹 유래 콘텐츠(web-derived content)를 건드렸을 때 자동으로 활성화되어 강제 적용되는 strangler seam(점진적 교체 구조)이며, 그 외의 경우에는 플래그(flag) 뒤에 준비되어 있습니다. 아직 모든 아웃바운드 호출에 대한 무조건적인 경로로 구현된 것은 아닙니다. 그것이 제가 다음에 마무리하고 싶은 작업이지, 이미 완성되었다고 설명할 단계는 아닙니다.
자율 에이전트 (autonomous agent)의 핵심은 묻지 않고 행동한다는 점이며, 제가 방금 설명한 모든 제한 사항은 에이전트를 덜 자율적으로 느껴지게 만듭니다. 그럼에도 불구하고 저는 그 거래를 선택했습니다. 클라우드 자격 증명 (cloud credentials)을 보유한 채, 3주 전에 설정하고 잊어버린 일정에 따라 실행되며, 제한 없는 네트워크 송신 (network egress)을 수행하는 관리되지 않는 프로세스는 기능이 아닙니다. 그것은 하나의 잘못된 프롬프트 (prompt)를 청구서와 데이터 유출로 바꿔버리는 기계입니다. 저는 차라리 약간은 주저하는 것처럼 느껴지더라도, 실행 상태로 두어도 안전한 것을 출시하겠습니다.
위젯이 등록 터미널이 되다
지난 포스트에서 소개한 Scouter 위젯은 작성 상자 (compose box)가 있는 모니터였습니다. Codex 상태를 보여주고 프롬프트를 보낼 수 있게 해주었습니다.
그 후 런처 패드 (launchpad) 기능이 추가되었습니다. 앱을 실행할 필요 없이 홈 화면에서 이미 등록된 에이전트들을 위해 원터치 RUN 버튼을 사용할 수 있게 되었습니다.

그리고 가장 최근에는, 단순한 런처 패드에 머물지 않고 등록 지점 (registration point)이 되었습니다. 위젯의 ASK 상자에 직접 @agent ...라고 입력하거나, 그냥 말하는 것만으로 홈 화면에서 완전히 새로운 에이전트를 등록할 수 있습니다. 이는 AI 패인 (AI pane)에 입력할 때와 동일한 확인 흐름, 동일한 카드, 동일한 문구를 사용하며, 동기화가 어긋날 수 있는 별도의 불완전한 위젯 경로를 거치지 않습니다.

기본적으로는 꺼져 있는 '위젯 확인 생략 등록 (Widget No-Confirm Register)'이라는 선택적 설정이 있는데, 이 설정은 확인 절차를 건너뛰고 즉시 등록한 뒤 사후에 알림을 보냅니다. 이 설정은 위젯에서 전달된 명령에만 적용됩니다. AI 창(AI pane)에서 @agent를 입력하는 방식은 여전히 확인 카드가 나타나므로, 이 탈출구(escape hatch)가 조용히 전역적인 방식으로 확장될 수는 없습니다. 이 기능은 확인 카드가 안전장치가 아닌 번거로운 요소(friction)로 느껴질 만큼 작업을 50번 정도 반복했을 때를 위한 것입니다.
이 기능은 설정이 켜져 있을 때와 꺼져 있을 때의 동작 차이를 포함하여, 기기 상에서 두 가지 상태 모두 검증되었습니다. 앱을 열지 않고 위젯에서 내일 아침 휴대폰을 깨울 에이전트를 등록하는 것은 이번 릴리스에서 제가 가장 좋아하는 기능입니다.
하나의 CLI, 다양한 채팅
이전에 제가 했던 말과는 상반되는 수정 사항을 말씀드릴 차례입니다.
현재 유일한 퍼스트 클래스 (first-class) CLI 통합은 Codex입니다. 터미널에서 네이티브로 실행되는 실제 CLI 바이너리입니다. Claude Code CLI와 Gemini CLI 통합은 제거되었습니다. Claude Code는 아직 병합되지 않은 브랜치에서 다시 도입될 예정인데, 그 이유를 돌려 말하지 않겠습니다. claude -p가 여전히 내부 경로에서 SIGSEGV(세그멘테이션 오류)를 일으키고 있으며, 저는 더 긴 기능 목록을 유지하기 위해 세그폴트(segfault)가 발생하는 CLI를 출시하지 않을 것입니다.

반면 AI 창(AI pane)은 반대 방향으로 나아갔습니다. 채팅 스타일의 AI는 이제 Gemini, Cerebras, Groq, Perplexity, OpenRouter (신규), 그리고 llama.cpp를 통한 로컬 모델(local models)까지 지원합니다. 저는 실제로 기기에서 일상적으로 Qwen3.5-2B Q4_K_M을 사용합니다. 대화 중간에 @mention을 사용하여 이들 중 하나를 선택할 수 있습니다.
이 재편성의 핵심은 다음과 같습니다: 하나의 CLI를 견고하게 만드는 것, 그리고 채팅 AI에는 옵션을 줄이는 대신 더 많이 제공하는 것입니다. 이 두 인터페이스는 실패 모드(failure modes)가 완전히 다릅니다. 고장 난 CLI는 터미널 자체를 함께 망가뜨리지만, 고장 난 채팅 백엔드는 다른 모델을 @mention하여 계속 진행하면 그만이기 때문입니다.
탭이 사라지다
이전 UI는 탭(Tabs) 방식이었습니다. 이제는 아닙니다.
이제 모든 것은 하나의 ShellLayout 안에 존재합니다. 상단의 AgentBar, 왼쪽의 Sidebar, 중앙의 pane container, 그리고 cwd(현재 작업 디렉토리), git branch, 연결 상태를 보여주는 하단의 ContextBar가 그것입니다. 7가지의 pane 유형 — Terminal, Agent Chat, AI, Browser, Markdown, Preview, Ask — 이 있으며, 이들은 자유롭게 분할 및 크기 조절이 가능하며, 펼쳐진 디스플레이에서는 한 번에 최대 4개까지 표시할 수 있습니다.

터미널이 앱이었고 그 외의 모든 것이 우회 경로였을 때는 탭 방식이 합리적이었습니다. 하지만 에이전트(Agent), 채팅(Chat), 그리고 출력 미리보기(Output preview)가 모두 동시에 보여야 하게 되면서, 탭은 작업의 3분의 2를 숨기는 방식에 불과하게 되었습니다.
내가 여전히 할 수 없는 것
프로덕션급(Production-grade) Kotlin 작성하기. 문서를 세 번 읽고도 한 번은 틀리게 되는 AlarmManager의 정확한 알람 권한 모델(exact-alarm permission model)에 대해 추론하기. 어떤 OEM이 내 알람을 배터리 위협으로 간주할지 예측하기.
내가 할 수 있는 것은 자율 시스템(Autonomous system)의 형태를 평이한 영어로 설명하고, 기능 구현 전에 안전 펜스(Safety fence)를 요구하며, 세그폴트(Segfaulting, 세그멘테이션 오류)가 발생하는 버전은 배포를 거부하는 것입니다.
에이전트는 예전에 내가 여는 것이었습니다. 이제는 오전 8시에 스스로 열려 내가 무엇을 찾아냈는지 나에게 알려주는 존재입니다.
GitHub: github.com/RYOITABASHI/Shelly — GPLv3. 이슈(Issues), 수정 사항, 그리고 "내 폰에서는 알람이 울리지 않아요"라는 보고는 모두 진심으로 환영합니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기