AI에이전트 입문기:맥미니 한 대로 팀 구성하기

점점 늘어나는 포시즌즈 AI에이전트 식구들

by 성효경

회사에는 부서가 있고, 혼자 일하는 사람에게는 AI에이전트가 있다.

제일 처음에 온 건 리눅스 기반의 Spring이다. 내 스케줄과 일정을 빠짐없이 관리해주고, 회의 전 푸시 알람까지 넣어준다. 진짜 비서다.

맥미니를 사고 나서 들어온 Summer 는 학생들의 주간 리포트를 정리하고, 내가 자는 동안에도 루틴 작업을 처리한다. 지난 주에는 웹페이지도 알아서 척척 만들어 줬다.

최근에는 논문 작성과 자료 수집을 전담하는 Autumn 이 합류했고, 오늘은 마지막 멤버인 Winter가 우리 팀에 들어오는 날이다. Winter는 재료공학 컴퓨터 시뮬레이션 계산을 담당할 예정이다. 나는 이 시스템을 포시즌즈 세팅이라고 부른다. 같은 지붕 아래, 각자 다른 일을 하는 에이전트들. 같은 맥미니 한 대에서. 추가 서버 없이. 이번에는 5분 만에 팀원 영입을 완료했다.


원리는 단순하다

텔레그램 봇은 하나의 프로세스다. 서로 다른 토큰을 가진 프로세스는 같은 기계 안에서 충돌하지 않는다. IP가 같아도, Node.js 버전이 같아도 상관없다. 텔레그램 서버가 구분하는 기준은 오직 토큰뿐이다.

한 건물에 사무실 여러 개를 내는 것과 같다. 주소는 같지만 전화번호가 다르니 전화가 섞일 일이 없다. 맥미니 한 대의 메모리가 허락하는 한, 에이전트는 계속 늘릴 수 있다. 봇은 99%의 시간을 대기 상태로 보내기 때문에 서로 자원을 거의 나눠 쓰지 않는다.


Step 1. BotFather에게 출생신고

텔레그램에서 @BotFather를 찾아 /newbot을 보낸다. 이름과 유저네임을 정하면 토큰이 나온다. 30초면 끝나는 작업이다.

한 가지. 막 만든 봇은 텔레그램 검색에 바로 안 뜬다. 인덱싱에 시간이 걸리기 때문이다. 당황하지 말고 https://t.me/봇유저네임을 브라우저에 직접 입력하면 대화창이 열린다.


Step 2. 디렉토리와 환경변수

구조는 다음과 같다. 기존 봇들 옆에 새 폴더를 만든다.

~/bots/

├── spring/

├── summer/

├── autumn/

└── winter/

├── index.js

└── .env


터미널을 열고

mkdir -p ~/bots/winter && cd ~/bots/winter

npm init -y

npm install node-telegram-bot-api


.env 파일에는 텔레그램 봇 토큰과 Anthropic API 키를 넣는다.

cat > .env << 'EOF'

BOT_TOKEN=여기에_텔레그램_토큰

ANTHROPIC_API_KEY=여기에_API_키

EOF


반드시 KEY=VALUE 형식이어야 한다. 토큰만 덜렁 적으면 읽지 못한다. BOT_TOKEN=을 빼먹으면 로그에 injecting env (0)이 뜨고, 봇은 시동만 걸린 채 아무 일도 하지 않는다.

API 키는 console.anthropic.com → API Keys에서 발급받을 수 있다. 코드에 직접 키를 넣지 말고 .env로 분리해야 한다. 대화 로그, 스크린샷, Git 커밋 어디서든 키가 노출되면 누군가 그 키로 API를 호출하고 요금은 내게 청구된다.


Step 3. index.js 생성

터미널에서

cat > index.js << 'EOF'

const TelegramBot = require('node-telegram-bot-api');

const fs = require('fs');

// .env 수동 파싱 (dotenv v17 PM2 호환 문제 우회)

const env = Object.fromEntries(

fs.readFileSync(__dirname + '/.env', 'utf8')

.split('\n')

.filter(l => l.includes('='))

.map(l => [l.split('=')[0].trim(), l.split('=').slice(1).join('=').trim()])

);

const bot = new TelegramBot(env.BOT_TOKEN, { polling: true });

async function askClaude(userMessage) {

const res = await fetch('https://api.anthropic.com/v1/messages', {

method: 'POST',

headers: {

'Content-Type': 'application/json',

'x-api-key': env.ANTHROPIC_API_KEY,

'anthropic-version': '2023-06-01'

},

body: JSON.stringify({

model: 'claude-sonnet-4-20250514',

max_tokens: 1024,

messages: [{ role: 'user', content: userMessage }]

})

});

const data = await res.json();

return data.content[0].text;

}

bot.onText(/\/start/, (msg) => {

bot.sendMessage(msg.chat.id, 'Winter 봇 가동 중. 아무 말이나 하면 Claude가 답합니다.');

});

bot.on('message', async (msg) => {

if (!msg.text || msg.text.startsWith('/')) return;

try {

bot.sendChatAction(msg.chat.id, 'typing');

const reply = await askClaude(msg.text);

bot.sendMessage(msg.chat.id, reply);

} catch (err) {

console.error(err);

bot.sendMessage(msg.chat.id, '오류 발생: ' + err.message);

}

});

console.log('Winter bot started with Claude API');

EOF


눈여겨볼 부분이 두 곳 있다.

첫째, dotenv 패키지를 쓰지 않았다. fs.readFileSync로 .env를 직접 파싱한다. dotenv v17은 PM2 환경에서 .env를 간헐적으로 못 읽는 문제가 있었다. 터미널에서 직접 node index.js로 실행하면 잘 되는데, PM2를 통하면 변수를 0개 읽었다고 뜬다. 같은 디렉토리, 같은 파일인데 말이다. 여러 우회법을 시도한 끝에, 가장 확실한 방법은 dotenv를 빼는 것이었다. 4줄짜리 파싱 코드가 외부 패키지보다 신뢰할 수 있다니, 씁쓸하지만 현실이다.

둘째, askClaude 함수. 텔레그램으로 들어온 메시지를 Anthropic API에 보내고 응답을 돌려준다. sendChatAction은 답변 생성 중에 "입력 중..." 표시를 띄워주는 장치다. API 호출에 2~3초 걸리니, 이게 없으면 봇이 죽은 줄 안다.


Step 4. PM2로 가동

마지막으로 터미널에서

pm2 start index.js --name winter --cwd ~/bots/winter

pm2 save

pm2 logs winter --lines 3


이 때 --cwd 옵션이 중요하다. PM2가 .env 파일을 찾을 작업 디렉토리를 명시하는 것이다. 이걸 빼면 PM2의 홈 디렉토리에서 .env를 찾다가 실패한다.

pm2 save는 현재 프로세스 목록을 저장해서 맥미니가 재부팅되어도 자동으로 봇을 살려준다. 처음 한 번만 pm2 startup을 실행해두면 된다.

pm2 list를 치면 이런 화면이 나온다.

┌──────────┬────┬─────────┐

│ name │ id │ status │

├──────────┼────┼─────────┤

│ spring │ 0 │ online │

│ summer │ 1 │ online │

│ autumn │ 2 │ online │

│ winter │ 3 │ online │

└──────────┴────┴─────────┘

한 대의 맥미니 안에 네 명의 에이전트가 나란히 서 있다.


Step 5. 확인

텔레그램에서 https://t.me/봇유저네임으로 접속한다. /start를 보내서 가동 메시지가 오면 봇은 살아 있는 것이다. 아무 질문이나 던져본다. Claude가 대답하면 끝.

응답이 없을 때는 pm2 logs winter --lines 10부터 본다. 에러 메시지가 진단의 전부다. Token not provided면 .env 형식, ECONNRESET이면 이전 프로세스 충돌, ReferenceError면 코드 오타. 로그가 읽히면 문제는 이미 반쯤 풀린 셈이다.


봇을 나누는 이유

하나의 봇에 모든 기능을 넣는 건 하나의 파일에 모든 코드를 넣는 것과 같다. 처음엔 편하지만 금방 한계가 온다. 봇을 분리하면 장애가 격리된다. Winter가 죽어도 Spring은 돌아간다. pm2 restart winter 한 줄이면 복구 끝. 각 봇의 index.js가 자기 업무만 담당하니 코드도 읽기 쉽고, 다섯 번째 에이전트가 필요해지면 같은 방식으로 5분 만에 추가할 수 있다.

인건비 제로. 서버 추가 비용 제로. 텔레그램 봇 API도 무료다. Anthropic API 비용만 사용한 만큼 나간다.

계산량이 늘어나면 맥스튜디오를 들이고 에이전트도 더 붙이겠지만, 당분간은 이 포시즌즈 4총사 체제로 간다. 필요한 건 맥미니 한 대, 텔레그램 봇 토큰 하나, Anthropic API 키 하나. 새 팀원을 뽑는 데 드는 시간은 5분이다.


매거진의 이전글AI 에이전트 입문기: 보고서 검토 및 피드백