본 콘텐츠의 이미지 및 내용은 AI로 생성되었습니다.
본 콘텐츠의 이미지 및 내용을 무단으로 복제, 배포, 수정하여 사용할 경우 저작권법에 의해 법적 제재를 받을 수 있습니다.
이미지 로딩 중...
AI Generated
2026. 2. 3. · 2 Views
UX와 협업 패턴 완벽 가이드
AI 에이전트와 사용자 간의 효과적인 협업을 위한 UX 패턴을 다룹니다. 프롬프트 핸드오프부터 인터럽트 처리까지, 현대적인 에이전트 시스템 설계의 핵심을 배웁니다.
목차
1. 프롬프트 핸드오프 설계
김개발 씨는 AI 에이전트를 활용한 코드 생성 도구를 만들고 있었습니다. 그런데 이상한 일이 벌어졌습니다.
에이전트가 사용자의 요청을 처리하다가 갑자기 엉뚱한 방향으로 작업을 진행하는 것이었습니다. "왜 이렇게 된 거지?" 김개발 씨는 고개를 갸웃거렸습니다.
프롬프트 핸드오프란 한 에이전트에서 다른 에이전트로, 또는 에이전트에서 사용자로 작업 맥락을 전달하는 설계 패턴입니다. 마치 릴레이 경주에서 바통을 넘기듯이, 현재까지의 작업 상태와 의도를 정확하게 다음 단계로 전달해야 합니다.
이것을 제대로 설계하면 작업의 연속성을 유지하면서도 각 단계에서 필요한 확인을 받을 수 있습니다.
다음 코드를 살펴봅시다.
// 프롬프트 핸드오프를 위한 컨텍스트 구조 정의
interface HandoffContext {
taskId: string;
originalIntent: string;
completedSteps: string[];
pendingActions: Action[];
userDecisionRequired: boolean;
}
// 핸드오프 매니저 클래스
class PromptHandoffManager {
// 현재 작업 상태를 다음 에이전트에게 전달
async handoff(context: HandoffContext, nextAgent: Agent): Promise<void> {
const summary = this.buildContextSummary(context);
// 핵심: 의도와 완료된 작업을 명확히 전달
await nextAgent.receive({
previousContext: summary,
actionItems: context.pendingActions,
requiresApproval: context.userDecisionRequired
});
}
}
김개발 씨는 입사 6개월 차에 접어든 주니어 개발자입니다. 요즘 회사에서 가장 핫한 프로젝트인 AI 에이전트 기반 개발 도구를 담당하게 되었습니다.
처음에는 단순해 보였습니다. 사용자가 요청하면 AI가 코드를 생성하고, 끝.
하지만 현실은 그렇게 단순하지 않았습니다. "김개발 씨, 이 에이전트 왜 이래요?
제가 분명히 로그인 기능 만들어달라고 했는데, 갑자기 회원가입 페이지를 수정하고 있어요." 기획자 이서비스 씨의 항의 전화가 걸려왔습니다. 문제를 살펴보니 원인이 보였습니다.
에이전트가 작업 중간에 다른 서브 에이전트를 호출했는데, 원래 의도가 제대로 전달되지 않은 것이었습니다. 마치 전화 게임에서 메시지가 왜곡되는 것처럼요.
프롬프트 핸드오프란 정확히 이 문제를 해결하기 위한 패턴입니다. 릴레이 경주를 생각해보세요.
선수가 아무리 빨리 달려도 바통을 제대로 전달하지 못하면 실격입니다. 마찬가지로 에이전트 시스템에서도 작업 맥락을 정확하게 전달하는 것이 핵심입니다.
핸드오프 설계에서 가장 중요한 것은 세 가지입니다. 첫째, 원래 의도를 명확히 기록해야 합니다.
사용자가 처음에 무엇을 원했는지, 그 핵심 목표가 무엇인지 잃어버리면 안 됩니다. 둘째, 완료된 작업 목록을 유지해야 합니다.
다음 에이전트가 이미 끝난 일을 다시 하지 않도록요. 셋째, 아직 남은 작업과 그 우선순위를 전달해야 합니다.
위의 코드에서 HandoffContext 인터페이스를 보면 이 세 가지가 모두 담겨있습니다. originalIntent는 원래 의도, completedSteps는 완료된 작업, pendingActions는 남은 작업입니다.
특히 userDecisionRequired 플래그는 사용자의 확인이 필요한 시점을 표시합니다. 실제 현업에서는 이 패턴이 어떻게 쓰일까요?
예를 들어 코드 리팩토링 에이전트를 생각해봅시다. 사용자가 "이 파일의 성능을 개선해줘"라고 요청합니다.
에이전트는 먼저 분석을 수행하고, 여러 개선 방안을 찾아냅니다. 이 중 일부는 자동으로 적용할 수 있지만, 일부는 사용자의 판단이 필요합니다.
이때 핸드오프 패턴을 사용하면 "분석 완료, 3가지 개선안 발견, 2번 항목은 사용자 승인 필요"라는 맥락을 깔끔하게 전달할 수 있습니다. 주의할 점도 있습니다.
핸드오프 컨텍스트가 너무 비대해지면 오히려 혼란을 줍니다. 필요한 정보만 간결하게 담아야 합니다.
또한 각 에이전트가 컨텍스트를 수정할 때는 변경 이력을 남겨야 나중에 문제가 생겼을 때 추적이 가능합니다. 다시 김개발 씨 이야기로 돌아가면, 핸드오프 패턴을 적용한 뒤 에이전트는 더 이상 엉뚱한 방향으로 빠지지 않았습니다.
각 단계에서 원래 목표를 확인하고, 필요하면 사용자에게 되묻기 때문입니다. 이서비스 씨의 항의 전화도 뚝 끊겼습니다.
실전 팁
💡 - 핸드오프 컨텍스트에는 반드시 원래 의도를 불변값으로 유지하세요
- 각 에이전트가 컨텍스트를 수정할 때 타임스탬프와 함께 변경 이력을 남기세요
- 사용자 확인이 필요한 시점을 명확히 플래그로 표시하세요
2. 스테이징 커밋 워크플로우
박시니어 씨는 후배들의 코드 리뷰를 하다가 한숨을 쉬었습니다. AI 에이전트가 생성한 코드가 바로 프로덕션에 반영되어 장애가 발생한 것입니다.
"에이전트가 만든 코드도 검증 단계가 필요해요. 바로 적용하면 위험합니다." 그때부터 팀에서는 스테이징 커밋 워크플로우를 도입하기로 했습니다.
스테이징 커밋 워크플로우는 에이전트가 생성한 변경사항을 바로 적용하지 않고, 중간 단계에 임시 저장하여 사용자가 검토할 수 있게 하는 패턴입니다. 마치 출판사에서 원고를 바로 인쇄하지 않고 교정 단계를 거치는 것과 같습니다.
이를 통해 실수를 미리 발견하고, 안전하게 변경사항을 적용할 수 있습니다.
다음 코드를 살펴봅시다.
// 스테이징 영역에서 변경사항을 관리하는 시스템
interface StagedChange {
id: string;
filePath: string;
originalContent: string;
proposedContent: string;
diffSummary: string;
status: 'pending' | 'approved' | 'rejected';
}
class StagingCommitManager {
private stagedChanges: Map<string, StagedChange> = new Map();
// 에이전트의 변경사항을 스테이징 영역에 추가
async stageChange(change: Omit<StagedChange, 'status'>): Promise<string> {
const staged: StagedChange = { ...change, status: 'pending' };
this.stagedChanges.set(change.id, staged);
// 사용자에게 리뷰 요청 알림 발송
await this.notifyForReview(staged);
return change.id;
}
// 승인된 변경사항만 실제로 적용
async commitApproved(): Promise<void> {
for (const [id, change] of this.stagedChanges) {
if (change.status === 'approved') {
await this.applyChange(change);
}
}
}
}
박시니어 씨는 개발팀의 리더로서 매일 수많은 코드 리뷰를 합니다. 최근에는 AI 에이전트가 생성한 코드까지 검토해야 하니 업무량이 두 배로 늘었습니다.
그런데 어느 날, 검토 없이 바로 적용된 에이전트 코드가 문제를 일으켰습니다. "에이전트가 API 응답 형식을 바꿔버렸어요.
프론트엔드가 전부 깨졌습니다." 급하게 롤백을 하고 나서야 팀은 근본적인 해결책이 필요하다는 것을 깨달았습니다. 스테이징 커밋 워크플로우의 핵심은 간단합니다.
에이전트가 만든 모든 변경사항은 일단 대기 상태로 들어갑니다. 사용자가 확인하고 승인해야만 비로소 실제로 적용됩니다.
출판사에서 책을 만들 때를 떠올려보세요. 작가가 원고를 보내면 바로 인쇄기로 가지 않습니다.
편집자가 검토하고, 교정을 보고, 최종 승인이 나야 인쇄가 시작됩니다. 이 패턴에서 가장 중요한 것은 diff 정보입니다.
사용자가 수백 줄의 코드를 전부 읽을 수는 없습니다. 그래서 무엇이 바뀌었는지, 왜 바뀌었는지를 한눈에 보여주는 요약이 필요합니다.
위 코드에서 diffSummary 필드가 바로 그 역할을 합니다. 또한 상태 관리가 필수적입니다.
pending은 검토 대기, approved는 승인됨, rejected는 거부됨을 나타냅니다. 이렇게 명확한 상태를 두면 어떤 변경사항이 어떤 단계에 있는지 한눈에 파악할 수 있습니다.
실무에서는 이 패턴을 어떻게 확장할까요? 먼저 변경사항의 위험도를 자동으로 분류할 수 있습니다.
설정 파일 변경은 고위험, 주석 추가는 저위험으로 태그를 붙입니다. 고위험 변경은 반드시 사용자 승인을 받고, 저위험 변경은 자동 승인 옵션을 제공할 수 있습니다.
주의할 점도 있습니다. 스테이징 영역에 변경사항이 너무 오래 쌓이면 문제가 됩니다.
나중에 한꺼번에 적용하려고 하면 충돌이 발생할 수 있기 때문입니다. 따라서 주기적으로 검토하고 처리하는 습관이 필요합니다.
또한 각 변경사항에 만료 기간을 설정하는 것도 좋은 방법입니다. 박시니어 씨 팀에서는 이 패턴을 도입한 뒤로 에이전트 관련 장애가 80% 줄었습니다.
처음에는 "검토가 귀찮다"는 불만도 있었지만, 장애 대응에 쓰던 시간이 줄어드니 오히려 전체 업무 효율이 올랐습니다.
실전 팁
💡 - 변경사항의 위험도를 자동 분류하여 고위험 항목에 집중하세요
- 스테이징 영역에 만료 기간을 설정하여 방치되는 변경사항을 방지하세요
- diff 요약을 자연어로 제공하면 비개발자도 검토에 참여할 수 있습니다
3. 비동기 배경 에이전트
김개발 씨는 에이전트에게 대용량 로그 분석을 요청했습니다. 그런데 화면이 멈춰버렸습니다.
5분이 지나도 응답이 없습니다. "이거 죽은 건가?" 브라우저를 새로고침 했더니 작업이 처음부터 다시 시작되었습니다.
그제야 깨달았습니다. 오래 걸리는 작업은 다르게 처리해야 한다는 것을요.
비동기 배경 에이전트는 시간이 오래 걸리는 작업을 백그라운드에서 실행하고, 사용자는 다른 작업을 계속할 수 있게 하는 패턴입니다. 마치 세탁기에 빨래를 돌려놓고 다른 집안일을 하는 것과 같습니다.
작업이 완료되면 알림을 통해 사용자에게 알려줍니다.
다음 코드를 살펴봅시다.
// 배경 작업을 관리하는 에이전트 러너
interface BackgroundTask {
taskId: string;
status: 'queued' | 'running' | 'completed' | 'failed';
progress: number;
result?: unknown;
error?: string;
}
class BackgroundAgentRunner {
private tasks: Map<string, BackgroundTask> = new Map();
// 작업을 백그라운드 큐에 추가하고 즉시 반환
async submitTask(taskConfig: TaskConfig): Promise<string> {
const taskId = generateUniqueId();
const task: BackgroundTask = {
taskId,
status: 'queued',
progress: 0
};
this.tasks.set(taskId, task);
// 비동기로 작업 실행 시작 (await 없이 즉시 반환)
this.executeInBackground(taskId, taskConfig);
return taskId; // 사용자는 이 ID로 상태 조회 가능
}
// 작업 상태 조회
getTaskStatus(taskId: string): BackgroundTask | undefined {
return this.tasks.get(taskId);
}
}
현대 웹 애플리케이션에서 사용자 경험의 핵심은 응답성입니다. 아무리 좋은 기능도 화면이 멈추면 사용자는 떠나버립니다.
김개발 씨가 겪은 문제가 바로 이것입니다. 로그 분석, 대용량 데이터 처리, 복잡한 코드 생성 같은 작업은 몇 분씩 걸릴 수 있습니다.
이런 작업을 동기적으로 처리하면 두 가지 문제가 생깁니다. 첫째, 사용자 인터페이스가 멈춥니다.
둘째, 네트워크 타임아웃이 발생할 수 있습니다. 비동기 배경 에이전트 패턴은 이 문제를 우아하게 해결합니다.
세탁기를 떠올려보세요. 빨래를 넣고 시작 버튼을 누르면, 우리는 그 자리에 서서 기다리지 않습니다.
다른 집안일을 하거나 TV를 보다가 알림음이 울리면 빨래를 꺼내면 됩니다. 코드에서 핵심은 submitTask 메서드입니다.
이 메서드는 작업을 큐에 넣고 즉시 taskId를 반환합니다. 실제 작업은 executeInBackground에서 별도로 진행됩니다.
사용자는 이 taskId를 가지고 언제든지 진행 상황을 확인할 수 있습니다. 상태 관리도 중요합니다.
queued는 대기 중, running은 실행 중, completed는 완료, failed는 실패를 나타냅니다. 특히 progress 필드는 0에서 100까지의 진행률을 나타내어 사용자에게 현재 얼마나 진행되었는지 알려줍니다.
실제 서비스에서는 이 패턴을 어떻게 구현할까요? 보통 메시지 큐 시스템과 함께 사용합니다.
Redis, RabbitMQ, AWS SQS 같은 서비스가 대표적입니다. 작업 요청이 들어오면 큐에 넣고, 별도의 워커 프로세스가 큐에서 작업을 꺼내 처리합니다.
이렇게 하면 서버가 재시작되어도 작업이 유실되지 않습니다. 주의할 점도 있습니다.
배경 작업이 실패했을 때의 처리가 중요합니다. 자동 재시도 횟수를 정하고, 최종 실패 시 사용자에게 알림을 보내야 합니다.
또한 오래된 작업 결과는 주기적으로 정리해야 메모리 누수를 방지할 수 있습니다. 김개발 씨는 이 패턴을 적용한 뒤 로그 분석 기능을 다시 만들었습니다.
이제 사용자가 분석을 요청하면 즉시 "분석이 시작되었습니다. 완료되면 알려드릴게요"라는 메시지가 뜹니다.
사용자는 다른 작업을 하다가 알림을 받고 결과를 확인하면 됩니다.
실전 팁
💡 - 작업 결과에 TTL(만료시간)을 설정하여 자동으로 정리되게 하세요
- 실패한 작업은 일정 횟수까지 자동 재시도하고, 최종 실패 시 알림을 보내세요
- 동시 실행 작업 수를 제한하여 시스템 과부하를 방지하세요
4. 사용자 알림 전략
이서비스 씨는 요즘 스트레스를 받고 있습니다. AI 에이전트가 작업을 완료해도 알림이 너무 늦게 오거나, 반대로 사소한 것까지 알림을 보내 집중을 방해하기 때문입니다.
"중요한 건 바로 알려주고, 안 중요한 건 나중에 모아서 알려주면 안 되나요?" 합리적인 요청이었습니다.
사용자 알림 전략은 에이전트의 다양한 이벤트를 사용자에게 효과적으로 전달하기 위한 패턴입니다. 마치 좋은 비서가 긴급한 전화는 바로 연결하고, 일반 우편은 모아서 한 번에 보고하는 것과 같습니다.
알림의 중요도와 긴급도에 따라 전달 방식을 달리하여 사용자 경험을 향상시킵니다.
다음 코드를 살펴봅시다.
// 알림 우선순위와 채널을 관리하는 시스템
type NotificationPriority = 'critical' | 'high' | 'medium' | 'low';
type NotificationChannel = 'push' | 'email' | 'inApp' | 'digest';
interface NotificationConfig {
priority: NotificationPriority;
channels: NotificationChannel[];
aggregatable: boolean; // 다른 알림과 묶을 수 있는지
}
class NotificationStrategy {
// 우선순위에 따른 기본 설정
private configs: Record<NotificationPriority, NotificationConfig> = {
critical: { priority: 'critical', channels: ['push', 'inApp'], aggregatable: false },
high: { priority: 'high', channels: ['push', 'inApp'], aggregatable: false },
medium: { priority: 'medium', channels: ['inApp'], aggregatable: true },
low: { priority: 'low', channels: ['digest'], aggregatable: true }
};
// 알림 발송 결정
async notify(event: AgentEvent): Promise<void> {
const config = this.configs[event.priority];
if (config.aggregatable) {
await this.addToDigest(event); // 요약 알림에 추가
} else {
await this.sendImmediate(event, config.channels); // 즉시 발송
}
}
}
알림은 양날의 검입니다. 적절한 알림은 사용자에게 유용한 정보를 제공하지만, 과도한 알림은 오히려 피로감을 줍니다.
이서비스 씨의 불만은 많은 사용자가 공감하는 문제입니다. 좋은 비서를 상상해보세요.
CEO에게 긴급한 전화가 오면 회의 중이라도 메모를 전달합니다. 하지만 광고 전화나 일반 우편은 하루에 한 번 모아서 보고합니다.
사용자 알림 전략도 이와 같은 원리입니다. 알림을 설계할 때 가장 먼저 고려해야 할 것은 우선순위입니다.
위 코드에서는 네 단계로 나누었습니다. critical은 시스템 장애나 보안 문제처럼 즉각 대응이 필요한 것입니다.
high는 중요하지만 몇 분 여유가 있는 것입니다. medium은 알면 좋지만 급하지 않은 것입니다.
low는 참고용 정보입니다. 다음으로 고려할 것은 채널입니다.
push는 모바일 푸시 알림으로 사용자의 즉각적인 주의를 끕니다. email은 기록이 필요한 공식적인 알림에 적합합니다.
inApp은 앱 내 알림 센터에 표시됩니다. digest는 하루에 한 번 모아서 보내는 요약 알림입니다.
aggregatable 플래그도 중요합니다. true로 설정된 알림은 비슷한 알림끼리 묶을 수 있습니다.
예를 들어 "파일 3개 분석 완료"처럼요. 이렇게 하면 알림 수를 줄이면서도 정보는 모두 전달할 수 있습니다.
실무에서는 사용자별 설정도 지원해야 합니다. 어떤 사용자는 모든 알림을 즉시 받고 싶어하고, 어떤 사용자는 정말 긴급한 것만 받고 싶어합니다.
기본 설정을 제공하되, 사용자가 커스터마이징할 수 있게 해야 합니다. 또한 시간대를 고려해야 합니다.
사용자가 자정에 잠들어 있을 때 중요하지 않은 알림을 보내는 것은 좋지 않습니다. 방해 금지 시간대를 설정하고, 이 시간에는 critical 알림만 전달하는 것이 바람직합니다.
이서비스 씨의 요청대로 알림 전략을 개선한 뒤, 사용자 만족도가 크게 올랐습니다. "이제 중요한 건 바로 알 수 있고, 자잘한 건 퇴근 전에 한 번만 확인하면 돼서 좋아요." 바로 이것이 좋은 알림 전략의 효과입니다.
실전 팁
💡 - 사용자별 알림 설정을 지원하여 개인화된 경험을 제공하세요
- 방해 금지 시간대에는 critical 알림만 전달하세요
- 비슷한 알림은 묶어서 보내 알림 피로도를 낮추세요
5. 진행 상황 표시
신입사원 최주니어 씨가 AI 에이전트에게 대용량 파일 처리를 요청했습니다. 화면에는 "처리 중..."이라는 문구만 덩그러니 떠 있습니다.
5분이 지났습니다. "이게 10%인 건지 90%인 건지 모르겠어요.
다 된 건가요?" 아무런 정보도 없이 기다리는 것은 고문과 같습니다.
진행 상황 표시는 에이전트가 작업을 수행하는 동안 현재 상태와 예상 완료 시점을 사용자에게 알려주는 패턴입니다. 마치 택배 조회 시스템이 "집화 완료 → 배송 중 → 배달 출발"처럼 단계를 보여주는 것과 같습니다.
사용자의 불안감을 줄이고 신뢰를 높여줍니다.
다음 코드를 살펴봅시다.
// 진행 상황을 세분화하여 표시하는 시스템
interface ProgressInfo {
taskId: string;
currentStep: string;
totalSteps: number;
completedSteps: number;
percentComplete: number;
estimatedTimeRemaining?: number; // 초 단위
subProgress?: string; // 현재 단계의 세부 진행 상황
}
class ProgressTracker {
// 진행 상황 업데이트 및 브로드캐스트
updateProgress(taskId: string, update: Partial<ProgressInfo>): void {
const current = this.getProgress(taskId);
const updated = { ...current, ...update };
// 예상 시간 계산 (과거 속도 기반)
updated.estimatedTimeRemaining = this.calculateETA(updated);
this.progressStore.set(taskId, updated);
// 실시간으로 클라이언트에 전송
this.broadcastToClient(taskId, updated);
}
// 현재 단계에 대한 세부 정보 표시
setSubProgress(taskId: string, subProgress: string): void {
this.updateProgress(taskId, { subProgress });
}
}
인간의 심리에서 가장 견디기 어려운 것 중 하나는 불확실성입니다. 병원 대기실에서 "곧 불러드리겠습니다"라고만 하면 10분도 길게 느껴집니다.
하지만 "현재 3번째이시고, 약 15분 후에 진료받으실 수 있습니다"라고 하면 같은 시간도 덜 답답합니다. 최주니어 씨가 느낀 답답함이 바로 이것입니다.
"처리 중..."이라는 메시지는 아무런 정보도 제공하지 않습니다. 1%일 수도 있고 99%일 수도 있습니다.
사용자는 기다려야 할지, 새로고침해야 할지, 포기해야 할지 판단할 수 없습니다. 진행 상황 표시 패턴은 이 문제를 해결합니다.
핵심은 세 가지 정보입니다. 첫째, 현재 어떤 단계인지입니다.
"파일 분석 중", "데이터 변환 중", "결과 저장 중"처럼요. 둘째, 전체 대비 진행률입니다.
"3/5 단계" 또는 "60%"처럼 숫자로 보여줍니다. 셋째, 예상 남은 시간입니다.
"약 2분 남음"처럼요. 위 코드에서 ProgressInfo 인터페이스를 보면 이 세 가지가 모두 포함되어 있습니다.
currentStep은 현재 단계, completedSteps과 totalSteps는 진행률, estimatedTimeRemaining은 예상 시간입니다. 특히 subProgress 필드가 유용합니다.
예를 들어 "파일 분석 중" 단계가 오래 걸린다면, "파일 분석 중 - report_2024.xlsx (5/20)"처럼 세부 정보를 보여줄 수 있습니다. 이렇게 하면 화면이 멈춘 것이 아니라 실제로 작업이 진행되고 있다는 것을 사용자가 확인할 수 있습니다.
예상 시간 계산은 어떻게 할까요? 가장 간단한 방법은 지금까지의 속도를 기반으로 추정하는 것입니다.
10%를 처리하는 데 1분이 걸렸다면, 나머지 90%는 약 9분이 걸릴 것으로 예상합니다. 물론 이것은 추정치이므로 "약 9분"처럼 표현해야 합니다.
실무에서 주의할 점이 있습니다. 진행률이 거짓말을 하면 안 됩니다.
90%에서 한참 멈춰있으면 사용자는 더 답답해합니다. 정확한 추정이 어려우면 차라리 "처리 중 (속도가 느려질 수 있습니다)"라고 솔직하게 표현하는 것이 낫습니다.
최주니어 씨가 사용하는 도구에 진행 상황 표시를 추가한 뒤, 같은 작업이라도 훨씬 덜 답답하게 느껴졌습니다. "지금 3단계 중 2단계이고, 약 3분 남았구나.
커피 한 잔 타와야겠다." 정보가 있으면 기다림을 계획할 수 있습니다.
실전 팁
💡 - 예상 시간은 정확하지 않을 수 있으므로 "약"이라는 표현을 사용하세요
- 진행률이 오래 멈추면 사용자에게 설명을 제공하세요
- 세부 진행 상황을 표시하여 작업이 실제로 진행 중임을 보여주세요
6. 인터럽트 및 취소 처리
김개발 씨가 에이전트에게 코드 리팩토링을 요청했습니다. 그런데 작업이 시작된 직후, 요구사항이 바뀌었다는 연락을 받았습니다.
급하게 취소 버튼을 눌렀지만 에이전트는 아랑곳하지 않고 계속 작업합니다. "멈춰!
제발 멈춰!" 하지만 이미 늦었습니다. 파일 절반이 수정되어 버렸습니다.
인터럽트 및 취소 처리는 진행 중인 에이전트 작업을 안전하게 중단하고, 시스템을 일관된 상태로 유지하는 패턴입니다. 마치 은행 거래에서 중간에 취소해도 잔액이 꼬이지 않는 것처럼, 작업 도중 중단되어도 데이터가 손상되지 않아야 합니다.
이를 위해 체크포인트와 롤백 메커니즘이 필요합니다.
다음 코드를 살펴봅시다.
// 안전한 인터럽트와 롤백을 지원하는 시스템
interface Checkpoint {
id: string;
timestamp: Date;
state: unknown;
canRollback: boolean;
}
class InterruptibleAgent {
private checkpoints: Checkpoint[] = [];
private isCancelled: boolean = false;
// 각 주요 단계 전에 체크포인트 생성
async createCheckpoint(state: unknown): Promise<string> {
const checkpoint: Checkpoint = {
id: generateUniqueId(),
timestamp: new Date(),
state: structuredClone(state), // 깊은 복사
canRollback: true
};
this.checkpoints.push(checkpoint);
return checkpoint.id;
}
// 취소 요청 처리
async requestCancel(): Promise<void> {
this.isCancelled = true;
// 마지막 안전 체크포인트로 롤백
const lastSafe = this.findLastSafeCheckpoint();
if (lastSafe) {
await this.rollbackTo(lastSafe);
}
}
// 작업 중간에 취소 여부 확인
shouldContinue(): boolean {
return !this.isCancelled;
}
}
은행에서 송금을 한다고 상상해보세요. A 계좌에서 출금한 직후, 시스템 오류가 발생했습니다.
이때 B 계좌에 입금되지 않았다면 돈은 어디로 갔을까요? 이런 일이 발생하지 않도록 은행 시스템은 트랜잭션이라는 개념을 사용합니다.
작업이 완전히 완료되거나, 아예 시작 전 상태로 돌아가거나 둘 중 하나입니다. 에이전트 시스템에서도 마찬가지입니다.
김개발 씨가 겪은 상황처럼, 작업 중간에 취소했는데 파일 절반만 수정된 상태로 남아있으면 큰 문제입니다. 어디까지 수정되었는지 파악하기도 어렵고, 수동으로 복구하기도 어렵습니다.
인터럽트 및 취소 처리 패턴의 핵심은 체크포인트입니다. 게임에서 세이브 포인트를 생각해보세요.
보스 몬스터와 싸우다 죽으면 마지막 세이브 포인트에서 다시 시작합니다. 에이전트도 마찬가지입니다.
중요한 작업 전에 체크포인트를 만들어두면, 문제가 생겼을 때 그 지점으로 돌아갈 수 있습니다. 위 코드에서 createCheckpoint 메서드는 현재 상태의 스냅샷을 저장합니다.
structuredClone을 사용하여 깊은 복사를 하는 것이 중요합니다. 얕은 복사를 하면 원본이 수정될 때 체크포인트도 같이 바뀌어버립니다.
requestCancel 메서드가 호출되면 먼저 isCancelled 플래그를 true로 설정합니다. 그리고 마지막 안전한 체크포인트를 찾아 롤백합니다.
shouldContinue 메서드는 에이전트가 각 단계를 실행하기 전에 호출하여 취소 요청이 있는지 확인합니다. 실무에서 주의할 점이 있습니다.
모든 작업을 체크포인트로 만들면 성능이 떨어집니다. 중요한 단계 앞에서만 체크포인트를 만들어야 합니다.
또한 체크포인트 데이터가 너무 크면 메모리 문제가 생길 수 있으므로, 오래된 체크포인트는 주기적으로 정리해야 합니다. 또한 사용자에게 취소의 의미를 명확히 전달해야 합니다.
"취소하시겠습니까? 이미 수정된 파일 3개는 원래 상태로 복구됩니다"처럼 무슨 일이 일어날지 알려주어야 합니다.
사용자가 실수로 취소 버튼을 눌렀을 수도 있으니까요. 김개발 씨가 사용하는 도구에 이 패턴을 적용한 뒤, 취소 버튼을 누르면 에이전트가 즉시 멈추고 이전 상태로 깔끔하게 복구됩니다.
"휴, 큰일 날 뻔했네요. 이제 안심하고 취소할 수 있어요."
실전 팁
💡 - 체크포인트는 중요한 단계 앞에서만 생성하여 성능 저하를 방지하세요
- 취소 시 무슨 일이 일어나는지 사용자에게 명확히 안내하세요
- 오래된 체크포인트는 주기적으로 정리하여 메모리 누수를 방지하세요
이상으로 학습을 마칩니다. 위 내용을 직접 코드로 작성해보면서 익혀보세요!
댓글 (0)
함께 보면 좋은 카드 뉴스
자가 치유 및 재시도 패턴 완벽 가이드
AI 에이전트와 분산 시스템에서 필수적인 자가 치유 패턴을 다룹니다. 에러 감지부터 서킷 브레이커까지, 시스템을 스스로 복구하는 탄력적인 코드 작성법을 배워봅니다.
Feedback Loops Human-in-the-Loop 완벽 가이드
AI 시스템에서 인간의 판단을 효과적으로 통합하는 Human-in-the-Loop 패턴을 다룹니다. 인간 검토 루프 설계부터 신뢰도 기반 자동화까지, 안전하고 효율적인 AI 시스템 구축 방법을 실무 예제와 함께 설명합니다.
Feedback Loops 컴파일러와 CI/CD 완벽 가이드
컴파일러 피드백 루프부터 CI/CD 파이프라인, 테스트 자동화, 자가 치유 빌드까지 현대 개발 워크플로우의 핵심을 다룹니다. 초급 개발자도 쉽게 이해할 수 있도록 실무 예제와 함께 설명합니다.
실전 MCP 통합 프로젝트 완벽 가이드
Model Context Protocol을 활용한 실전 통합 프로젝트를 처음부터 끝까지 구축하는 방법을 다룹니다. 아키텍처 설계부터 멀티 서버 통합, 모니터링, 배포까지 운영 레벨의 MCP 시스템을 구축하는 노하우를 담았습니다.
MCP 동적 도구 업데이트 완벽 가이드
AI 에이전트의 도구를 런타임에 동적으로 로딩하고 관리하는 방법을 알아봅니다. 플러그인 시스템 설계부터 핫 리로딩, 보안까지 실무에서 바로 적용할 수 있는 내용을 다룹니다.