나갈까? 하나의 야외 계획을 추천하고 휴대폰을 치우라고 알려주는 로컬 Gemma 에이전트
요약
본 글은 Gemma를 활용하여 사용자가 외출할 계획을 추천하고 휴대폰 사용을 자제하도록 유도하는 웹 앱 '나갈까?' 개발 과정을 소개합니다. 이 앱은 날씨, 공기 질, 주변 공원 정보를 종합 분석하여 최적의 야외 활동 계획(장소, 경로, 복장 등)을 제시하며, 사용자에게 디지털 디톡스를 권유하는 것이 특징입니다.
핵심 포인트
- Gemma를 활용한 로컬 에이전트 개발 사례
- 날씨/공기질 기반 맞춤형 외출 계획 추천 기능 구현
- 사용자에게 휴대폰 사용 자제를 유도하는 독특한 UX 설계
본 글은 Hacktoberfest Open-Source AI Challenge Week 1: Touch Grass에 제출하는 내용입니다.
내가 만든 것 (What I Built)
_나갈까?(Should I go out?)_는 하나의 질문에 답하고 휴대폰을 치우라고 알려주는 웹 앱입니다.
한 번 터치하기만 하면, 이 앱은 날씨, 공기 질, 그리고 주변의 공원들을 확인하고, 사용자의 컴퓨터에서 실행되는 Gemma가 이용 가능한 시간에 맞는 야외 계획 하나를 제안합니다. 그 계획에는 공원이나 명소, 왕복 경로, 할 만한 활동 몇 가지, 그리고 무엇을 입어야 하는지에 대한 정보가 포함됩니다. 그런 다음 화면은 사용자에게 휴대폰을 주머니에 넣으라고 알려줍니다.
사용자를 밖에 보내는 앱들은 보통 피드(feeds), 스트릭(streaks), 리더보드(leaderboards)를 통해 여전히 사용자의 관심을 끌려고 합니다. 저는 이 화면이 나들이의 가장 짧은 부분이 되기를 바랐습니다. 사용자에게 계획 하나만 제공하고, 마음에 들지 않으면
야외로 나가다
저는 집돌이(homebody)라서 외출을 거의 하지 않습니다. 앱을 만들면서 휴대폰으로 테스트해 보았습니다. 그러자 근처에 있었지만 제가 알지 못했던 작은 공원인 까치 어린이공원을 추천했습니다.

걸어가서 체크인하고 첫 배지를 받았습니다. 제가 직접 만든 앱 덕분에 어딘가에 간다는 느낌이 신선했습니다.
한 가지 이상한 점이 있었습니다. 체크인을 한 후,
날씨와 공기, 그리고 주변 공원을 확인하고, 사용자의 컴퓨터에서 구동되는 Gemma가 주어진 시간에 맞는 야외 활동 계획을 하나 추천해 줍니다. 그러고는 휴대폰을 치우라고 알려주죠.
- 피드가 아닌 하나의 계획. 공원이나 명소, 그곳에서 할 몇 가지 활동, 주어진 시간에 맞는 왕복 경로, 그리고 무엇을 입어야 하는지까지 안내합니다.
- 사용자 맞춤형. 첫 방문 시 네 가지 간단한 질문에 답하거나 AI가 결정하게 할 수 있습니다. 장소가 마음에 들지 않나요? '다른 장소'를 누르세요.
- 실제 방문 시에만 카운트. '여기에 왔어요(I'm here)' 기능을 통해 해당 장소에 체크인합니다. 목표와 배지는 실제로 어딘가에 갔을 때만 움직이며, 점수나 리더보드는 없습니다.
- 산책 미리보기. 실제 거리를 비행하는 약 30초 분량의 뮤직비디오를 제공하여 저장하거나...
이 프로젝트는 apps/server (Node에서 Hono 및 Mastra 사용)와 apps/web (Vite 및 React 사용)을 가진 pnpm 모노레포입니다. README에는 간단한 설정 방법이 있으며, AGENTS.md 파일은 모델 다운로드까지 포함하여 코딩 에이전트가 모든 것을 설정할 수 있게 해줍니다.
제작 과정
| 구성 요소 | 여기서 하는 역할 |
|---|---|
| Gemma 3 4B (오픈 웨이트) | 장소를 선택하고, 이유와 할 활동을 작성하며, 복장을 추천하고, 답변을 한국어로 번역합니다 |
| ... | |
| 모든 과정은 무료이며 API 키가 필요 없습니다. |
서버의 역할과 Gemma의 역할
첫 번째 버전에서는 Gemma에게 공원 목록을 주고 계획을 짜도록 했습니다. 하지만 Gemma는 임의로 걷는 시간을 만들었고, 그 결과 카드는 지도와 일치하지 않았습니다. 이제 서버가 측정 가능한 모든 것을 처리하고, Gemma는 판단이 필요한 부분을 담당합니다. 각 요청은 네 단계 Mastra 워크플로우를 거칩니다:
- Open-Meteo, Nominatim, Overpass에서 조건을 수집하고 하나의 OSRM 요청으로 모든 공원까지의 실제 왕복 거리를 측정합니다. 시간 기준에 맞지 않는 공원은 제외됩니다.
- Gemma에게 질문합니다. Gemma는 조건에 맞는 공원만 확인하며, 각 공원의
roundTripMin과 특징을 보고받고 JSON 형식으로 답변합니다: ID별 공원 하나, 이유, 할 만한 활동, 그리고 고정된 카탈로그에서 선택된 복장입니다. Gemma는 좌표나 옷을 임의로 만들어낼 수 없습니다. - 답변을 확인합니다. zod가 구조를 검증합니다. 규칙(Rules)은 공원이 가지고 있지 않은 시설 이름이 포함된 할 만한 활동이나, 날씨와 일치하지 않는 이유를 가진 항목들을 제외시킵니다. 만약 "아이들과 함께"라고 요청했고 Gemma가 놀이터가 없는 공원을 선택했지만 다른 후보지가 놀이터를 갖추고 있다면, 서버는 공원지를 변경합니다. Gemma가 두 번 실패하면 간단한 규칙(simple rules)이 대신 답변합니다.
- 경로를 계획합니다. 단계 1에서 이미 측정을 완료했기 때문에 캐시에서 가져옵니다.
한국어 답변은 두 번째 Gemma 호출을 통해 검증된 영어 답변을 번역하여 얻으며, 장소 이름은 변경되지 않도록 마스킹 처리됩니다.
Sentry로 추적하기 (Tracing with Sentry)
SENTRY_DSN이 설정되면, 각 요청은 하나의 Sentry 트레이스(trace)가 됩니다: 네 단계의 과정, 에이전트 실행 내용, 토큰을 포함한 모든 Gemma 호출, 그리고 나가는 모든 API 요청입니다.
공개 Overpass 서버는 종종 느리기 때문에, 앱은 최대 3초 동안 기다린 후 공원 특징을 하루 동안 캐싱합니다. 이 트레이스에서 경로 단계(route step)가 1ms가 걸린 것은 해당 대기 시간 동안 경로 측정이 이루어졌기 때문입니다.
대부분의 대기 시간은 Gemma에 의해 발생합니다. 모델이 이미 로드된 M3 Pro MacBook에서 세 번의 요청을 처리했을 때:
| Input tokens | Output tokens | Time | |
|---|---|---|---|
| Gemma가 장소를 선택하고 계획을 작성함 | 2,151 ~ 2,592 | 218 ~ 236 | 9.0 ~ 9.7초 |
| ... | |||
| 이는 모든 요청의 75~80%를 차지하며 비용은 $0입니다. Gemma가 유효하지 않은 JSON을 반환하면, 재시도(retry) 내용이 같은 단계에 표시됩니다: |
Open Innovation이 중요한 이유
이 앱은 사용자가 어디에 있는지, 언제 외출하는지, 누구와 함께 가는지, 그리고 어떤 공원을 방문했는지를 알고 있습니다. 폐쇄형 모델 API를 사용할 경우, 이 모든 정보가 다른 사람의 로그에 남게 됩니다. 하지만 여기서는 프롬프트가 제 Mac을 벗어나지 않습니다. 지도 및 날씨 서비스는 필요한 좌표만 받으며, 카카오맵은 한국에서 길찾기를 탭할 때 시작점과 목적지만 받고, 체크인은 브라우저 내에 유지됩니다.
운영 비용이 무료이기 때문에, 유료 API를 사용했다면 아마도 잘라냈을 호출들까지 유지했습니다. '다른 장소'는 Gemma에게 다시 질문하고, 모든 답변은 한국어 처리를 위해 두 번째 호출을 받으며, 깨진 JSON은 재시도를 합니다.
작은 로컬 모델 또한 디자인에 영향을 미쳤습니다: 측정된 경로, 고정된 의상 카탈로그, 규칙 확인 및 폴백(fallback) 기능입니다. 모델을 교체하는 것은 환경 변수(OLLAMA_MODEL) 하나만 변경하면 되며, 동일한 검사가 모든 오픈 모델에 적용됩니다. 가입 절차나 API 키가 필요 없습니다. 클론하여 모델을 가져오기만 하면 OpenStreetMap, Open-Meteo, OSRM이 커버하는 어디서든 작동합니다. 단점은 Mac이 켜져 있어야 하고, Gemma가 로드되는 동안 첫 답변을 받는 데 10초에서 20초가 걸린다는 것입니다.
나의 에이전트 세션
저는 Cursor agents를 사용하여 이것을 구축했으며, Entire가 세션을 기록하고 각 세션을 커밋에 연결했습니다: 126개의 커밋 중 104개에 Entire-Checkpoint: 트레일러가 포함되어 있습니다. 이 기록들은 제 작업 환경 설정까지 포함하고 있기 때문에 비공개 저장소에 보관되어 있습니다.
DEV의 파서는 아직 Cursor 기록을 읽지 못하기 때문에, 저는 세션을 Claude Code 형식으로 변환했습니다. 그래서 클로드 코드 아이콘이 표시되는 것입니다. 세션 1부터 5까지는 Entire의 기록에서 왔고, 세션 6은 Cursor 채팅에서 온 것입니다. Cursor는 도구 결과를 저장하지 않기 때문에, 여러분은 제가 입력한 프롬프트와 에이전트의 답변, 그리고 에이전트가 실행한 명령을 보게 될 것입니다. 제 프롬프트는 한국어였기 때문에 영어 번역을 추가했고, 로컬 경로와 계정 이름은 삭제했습니다.
1. "카드와 경로는 다른 시간을 보여요. 수정해 주세요."
카드가 산책에 대해 지도와 의견이 달랐는데, 이는 Gemma가 분(minute)을 추정했기 때문입니다. 에이전트는 경로 측정을 Gemma 앞에 배치했습니다: OSRM은 모든 후보의 왕복 거리를 측정하고, 맞지 않는 공원은 제외되며, Gemma는 각 공원의 roundTripMin 값을 받게 됩니다. 그 후 저는 4B 모델에게 제가 직접 측정할 수 있는 숫자를 요청하는 것을 중단했습니다.
커밋: b94354b "왕복 거리가 맞는 공원만 유지"
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기

