본 콘텐츠의 이미지 및 내용은 AI로 생성되었습니다.
본 콘텐츠의 이미지 및 내용을 무단으로 복제, 배포, 수정하여 사용할 경우 저작권법에 의해 법적 제재를 받을 수 있습니다.
이미지 로딩 중...
AI Generated
2025. 12. 12. · 11 Views
Human-in-the-Loop Interrupts 완벽 가이드
LangGraph의 Human-in-the-Loop Interrupts는 AI 에이전트가 중요한 결정을 내리기 전에 사람의 승인을 받을 수 있게 해주는 강력한 기능입니다. interrupt() 함수 사용법부터 승인/거부 워크플로우, 상태 검토 및 편집, 도구 실행 전 승인, 입력 유효성 검증까지 실무에서 바로 활용할 수 있는 패턴을 배워봅니다.
목차
1. interrupt() 함수 사용법
어느 날 김개발 씨가 AI 챗봇 프로젝트를 진행하던 중 난감한 상황에 부딪혔습니다. 고객의 주문을 자동으로 처리하는 AI 에이전트를 만들었는데, 가끔 엉뚱한 상품을 주문하는 문제가 발생했습니다.
"AI가 스스로 판단하기 전에 사람이 확인할 수 있는 방법이 없을까요?" 선배 개발자 박시니어 씨가 웃으며 대답했습니다. "그럴 때 쓰라고 있는 게 바로 interrupt() 함수예요."
interrupt() 함수는 LangGraph 워크플로우의 실행을 일시 중지하고 사람의 개입을 기다리는 함수입니다. 마치 자동차의 브레이크처럼, 필요한 순간에 AI의 실행을 멈추고 사람이 확인할 시간을 제공합니다.
이를 통해 AI가 중요한 결정을 내리기 전에 반드시 사람의 승인을 받도록 설계할 수 있습니다.
다음 코드를 살펴봅시다.
from langgraph.checkpoint.memory import MemorySaver
from langgraph.graph import StateGraph, START
from langgraph.types import interrupt, Command
# 상태 정의
class State(dict):
order_amount: int
approved: bool
def check_order(state: State):
# 10만원 이상 주문은 승인 필요
if state["order_amount"] > 100000:
# 여기서 실행을 중지하고 사람의 승인을 기다립니다
approval = interrupt("고액 주문입니다. 승인하시겠습니까?")
state["approved"] = approval
return state
# 그래프 구성
graph = StateGraph(State)
graph.add_node("check", check_order)
graph.add_edge(START, "check")
app = graph.compile(checkpointer=MemorySaver())
김개발 씨는 입사 6개월 차 AI 개발자입니다. 최근 회사에서 고객 주문을 자동으로 처리하는 AI 에이전트를 개발하는 프로젝트를 맡았습니다.
처음에는 모든 게 순조로웠습니다. AI가 주문을 받고, 재고를 확인하고, 배송까지 자동으로 처리했습니다.
그런데 문제가 발생했습니다. 어느 고객이 실수로 100대의 노트북을 주문했는데, AI가 그대로 처리해버린 것입니다.
1억 원이 넘는 주문이었습니다. 회사는 큰 손실을 입었고, 김개발 씨는 상사에게 불려갔습니다.
박시니어 씨가 김개발 씨의 코드를 살펴보더니 말했습니다. "AI가 모든 걸 자동으로 처리하면 편하긴 하지만, 위험할 수도 있어요.
특히 중요한 결정은 사람이 확인해야 합니다." 그렇다면 interrupt() 함수란 정확히 무엇일까요? 쉽게 비유하자면, interrupt() 함수는 마치 중요한 서류에 서명을 받는 것과 같습니다.
직원이 모든 서류를 처리할 수 있지만, 계약서처럼 중요한 문서는 반드시 상사의 서명을 받아야 합니다. AI 에이전트도 마찬가지입니다.
대부분의 작업은 자동으로 처리하지만, 중요한 결정은 사람의 승인이 필요합니다. interrupt() 함수가 없던 시절에는 어땠을까요?
개발자들은 AI의 모든 결정을 신뢰하거나, 아예 자동화를 포기해야 했습니다. 자동화를 하면 편하지만 위험했고, 수동으로 처리하면 안전하지만 비효율적이었습니다.
특히 금융, 의료, 법률처럼 실수가 치명적인 분야에서는 AI 도입 자체를 꺼렸습니다. 더 큰 문제는 제어권 상실이었습니다.
AI가 한번 실행되면 멈출 수 없었습니다. 마치 브레이크 없는 자동차처럼, AI가 잘못된 방향으로 가도 개입할 방법이 없었습니다.
바로 이런 문제를 해결하기 위해 Human-in-the-Loop Interrupts가 등장했습니다. interrupt() 함수를 사용하면 워크플로우의 특정 지점에서 실행을 일시 중지할 수 있습니다.
AI는 그 지점에서 멈춰서 사람의 입력을 기다립니다. 사람이 승인하면 다시 실행되고, 거부하면 다른 경로로 진행됩니다.
또한 사람과 AI의 협업도 가능해집니다. AI가 할 수 있는 일은 AI가 하고, 판단이 필요한 부분은 사람이 개입합니다.
이렇게 하면 효율성과 안전성을 모두 확보할 수 있습니다. 무엇보다 신뢰성이 크게 향상된다는 큰 이점이 있습니다.
중요한 결정은 반드시 사람이 검토하므로, AI의 실수로 인한 피해를 예방할 수 있습니다. 위의 코드를 한 줄씩 살펴보겠습니다.
먼저 class State(dict) 부분을 보면 워크플로우에서 사용할 상태를 정의하는 것을 알 수 있습니다. order_amount는 주문 금액, approved는 승인 여부를 저장합니다.
다음으로 check_order 함수에서는 주문 금액을 확인합니다. 10만 원을 넘으면 interrupt() 함수를 호출하여 실행을 중지하고 사람의 승인을 기다립니다.
메시지 "고액 주문입니다. 승인하시겠습니까?"가 사용자에게 표시됩니다.
interrupt() 함수는 사용자의 입력을 받을 때까지 기다립니다. 사용자가 True(승인) 또는 False(거부)를 입력하면 그 값이 approval 변수에 저장되고, 상태의 approved 필드가 업데이트됩니다.
마지막으로 graph.compile(checkpointer=MemorySaver())에서 체크포인터를 설정합니다. 이것이 핵심입니다.
interrupt() 함수를 사용하려면 반드시 체크포인터가 필요합니다. 체크포인터는 워크플로우의 상태를 저장하여, 중단된 지점에서 다시 시작할 수 있게 해줍니다.
실제 현업에서는 어떻게 활용할까요? 예를 들어 고객 서비스 챗봇을 개발한다고 가정해봅시다.
대부분의 문의는 AI가 자동으로 답변하지만, 환불이나 계정 삭제처럼 중요한 요청은 interrupt() 함수로 상담원의 승인을 받도록 설계할 수 있습니다. 많은 금융 기업에서 이런 패턴을 적극적으로 사용하고 있습니다.
또 다른 예로, 의료 AI를 생각해봅시다. AI가 환자의 증상을 분석하고 진단을 제안하지만, 최종 처방은 반드시 의사가 검토하고 승인해야 합니다.
interrupt() 함수를 사용하면 이런 워크플로우를 안전하게 구현할 수 있습니다. 하지만 주의할 점도 있습니다.
초보 개발자들이 흔히 하는 실수 중 하나는 체크포인터 없이 interrupt()를 사용하는 것입니다. interrupt() 함수는 상태를 저장했다가 복원해야 하므로, 반드시 체크포인터가 필요합니다.
체크포인터 없이 사용하면 에러가 발생합니다. 또 다른 실수는 너무 자주 interrupt()를 호출하는 것입니다.
사용자가 매번 승인해야 한다면 오히려 불편해집니다. 정말 중요한 결정에만 사용해야 합니다.
다시 김개발 씨의 이야기로 돌아가 봅시다. 박시니어 씨의 설명을 들은 김개발 씨는 코드를 수정했습니다.
10만 원 이상의 주문은 반드시 관리자의 승인을 받도록 했습니다. 그 후로 비정상적인 주문은 사라졌고, 상사에게 칭찬까지 받았습니다.
interrupt() 함수를 제대로 이해하면 안전하고 신뢰할 수 있는 AI 시스템을 구축할 수 있습니다. 여러분도 오늘 배운 내용을 실제 프로젝트에 적용해 보세요.
실전 팁
💡 - interrupt() 함수는 반드시 체크포인터와 함께 사용해야 합니다
- 정말 중요한 결정에만 사용하여 사용자 경험을 해치지 않도록 주의하세요
- interrupt()의 메시지는 명확하고 구체적으로 작성하여 사용자가 쉽게 판단할 수 있도록 하세요
2. Command(resume=) 재개
interrupt() 함수로 워크플로우를 멈추는 법을 배운 김개발 씨는 이제 새로운 궁금증이 생겼습니다. "멈춘 워크플로우를 다시 시작하려면 어떻게 해야 하죠?" 박시니어 씨가 화면을 가리키며 설명했습니다.
"바로 Command 객체를 사용하는 거예요. resume 값으로 사용자의 결정을 전달하면 됩니다."
**Command(resume=)**은 interrupt()로 중단된 워크플로우를 재개할 때 사용하는 객체입니다. 마치 일시정지된 영화를 다시 재생하는 것처럼, 멈춰있던 워크플로우에 사용자의 입력을 전달하여 다음 단계로 진행시킵니다.
resume 파라미터에 값을 담아 보내면, 그 값이 interrupt() 함수의 반환값이 됩니다.
다음 코드를 살펴봅시다.
from langgraph.types import Command
# 워크플로우 실행
config = {"configurable": {"thread_id": "order_123"}}
initial_state = {"order_amount": 150000, "approved": False}
# 첫 실행 - interrupt()에서 멈춤
result = app.invoke(initial_state, config)
print(f"워크플로우 중단됨: {result}")
# 사용자가 승인 결정
user_approved = True # 실제로는 UI에서 입력받음
# Command로 재개 - resume에 승인 결과 전달
app.invoke(
Command(resume=user_approved),
config # 같은 thread_id 사용
)
# 거부하려면 False 전달
# app.invoke(Command(resume=False), config)
김개발 씨가 interrupt() 함수를 성공적으로 적용한 후, QA 팀에서 테스트를 진행했습니다. 그런데 테스터 이지원 씨가 질문을 던졌습니다.
"워크플로우가 멈추는 건 확인했는데, 승인 버튼을 누르면 어떻게 다시 실행되나요?" 김개발 씨는 당황했습니다. interrupt() 함수로 멈추는 것까지만 생각했지, 재개하는 방법은 깊이 고민하지 않았던 것입니다.
박시니어 씨가 다가와 코드를 보여주었습니다. 그렇다면 Command 객체란 정확히 무엇일까요?
쉽게 비유하자면, Command 객체는 마치 우편 엽서와 같습니다. 편지를 보낸 사람(AI)이 답장(사용자의 결정)을 기다리고 있을 때, 엽서에 답을 적어서 보내면 됩니다.
Command 객체도 마찬가지로, interrupt()로 멈춰있는 워크플로우에 사용자의 결정을 전달하는 수단입니다. Command 객체가 없다면 어떻게 될까요?
워크플로우는 영원히 멈춰있을 것입니다. interrupt()로 중단된 지점에서 계속 대기만 하고, 사용자의 입력을 받을 방법이 없습니다.
마치 편지를 보냈는데 답장을 받을 주소가 없는 것과 같습니다. 또 다른 문제는 상태의 일관성입니다.
만약 새로운 워크플로우를 시작한다면, 이전에 처리했던 모든 상태가 사라집니다. 주문 정보, 고객 정보, 진행 상황 등 모든 것을 처음부터 다시 시작해야 합니다.
바로 이런 문제를 해결하기 위해 Command 객체가 존재합니다. Command(resume=) 패턴을 사용하면 중단된 지점에서 정확하게 재개할 수 있습니다.
이전 상태가 모두 보존되어 있으므로, 사용자의 입력만 받으면 바로 다음 단계로 진행됩니다. 또한 같은 thread_id를 사용하여 워크플로우의 연속성을 보장합니다.
thread_id는 워크플로우의 고유 식별자입니다. 같은 thread_id로 요청하면 LangGraph는 이전에 저장된 상태를 불러와서 재개합니다.
무엇보다 간단한 API라는 큰 이점이 있습니다. Command(resume=값) 형태로 매우 직관적으로 사용할 수 있습니다.
복잡한 설정이나 코드가 필요 없습니다. 위의 코드를 한 줄씩 살펴보겠습니다.
먼저 config = {"configurable": {"thread_id": "order_123"}}를 보면 워크플로우의 고유 ID를 설정하는 것을 알 수 있습니다. 이 ID로 나중에 같은 워크플로우를 식별합니다.
다음으로 app.invoke(initial_state, config)를 실행하면 워크플로우가 시작됩니다. check_order 함수에서 주문 금액이 10만 원을 넘으므로 interrupt()가 호출되고, 워크플로우는 여기서 멈춥니다.
이 시점에서 프로그램은 대기 상태입니다. 사용자가 UI에서 "승인" 또는 "거부" 버튼을 클릭하기를 기다립니다.
사용자가 승인 버튼을 클릭하면 user_approved = True가 설정됩니다. 그리고 app.invoke(Command(resume=user_approved), config)를 호출합니다.
여기서 핵심은 같은 config 객체를 사용한다는 것입니다. LangGraph는 config의 thread_id를 보고, "아, 이건 아까 멈춘 order_123 워크플로우구나"라고 인식합니다.
그리고 저장된 상태를 불러와서, interrupt()가 호출된 지점부터 재개합니다. resume=user_approved 값이 interrupt() 함수의 반환값이 됩니다.
따라서 approval = interrupt(...)에서 approval 변수는 True를 받게 되고, state["approved"] = True가 실행됩니다. 실제 현업에서는 어떻게 활용할까요?
예를 들어 웹 애플리케이션을 개발한다고 가정해봅시다. 사용자가 "계정 삭제" 버튼을 클릭하면, 백엔드에서 워크플로우가 시작되고 interrupt()로 멈춥니다.
프론트엔드는 "정말 삭제하시겠습니까?" 다이얼로그를 표시합니다. 사용자가 "확인"을 클릭하면, 프론트엔드는 POST /workflow/resume에 {thread_id: "user_456", resume: true}를 전송합니다.
백엔드는 이 요청을 받아 app.invoke(Command(resume=True), config)를 호출하여 계정 삭제를 진행합니다. 또 다른 예로, 승인 시스템을 생각해봅시다.
직원이 휴가를 신청하면 워크플로우가 시작되고, 관리자의 승인을 기다립니다. 관리자가 승인 이메일의 링크를 클릭하면, 서버는 Command(resume=True)로 워크플로우를 재개하여 휴가를 등록합니다.
하지만 주의할 점도 있습니다. 초보 개발자들이 흔히 하는 실수 중 하나는 다른 thread_id로 재개하려는 것입니다.
thread_id가 다르면 LangGraph는 완전히 새로운 워크플로우로 인식하여, 이전 상태를 찾을 수 없습니다. 반드시 같은 thread_id를 사용해야 합니다.
또 다른 실수는 resume에 잘못된 타입을 전달하는 것입니다. interrupt()에서 기대하는 값의 타입과 resume에 전달하는 값의 타입이 일치해야 합니다.
예를 들어 interrupt()가 불리언을 기대하는데 문자열을 보내면 예상치 못한 동작이 발생할 수 있습니다. 다시 김개발 씨의 이야기로 돌아가 봅시다.
박시니어 씨의 설명을 들은 김개발 씨는 프론트엔드와 백엔드를 연결했습니다. 사용자가 승인 버튼을 클릭하면 Command(resume=True)가 전송되어 주문이 처리되고, 거부 버튼을 클릭하면 Command(resume=False)가 전송되어 주문이 취소됩니다.
테스터 이지원 씨가 다시 테스트를 진행했고, 이번에는 완벽하게 작동했습니다. "이제 제대로 작동하네요!" 김개발 씨는 뿌듯한 미소를 지었습니다.
Command 객체를 제대로 이해하면 사용자와 AI가 자연스럽게 협업하는 시스템을 만들 수 있습니다. 여러분도 오늘 배운 내용을 실제 프로젝트에 적용해 보세요.
실전 팁
💡 - 반드시 같은 thread_id를 사용하여 워크플로우를 재개하세요
- resume에 전달하는 값의 타입이 interrupt()에서 기대하는 타입과 일치하는지 확인하세요
- 프론트엔드에서 thread_id를 안전하게 관리하여 보안 이슈가 없도록 주의하세요
3. 승인 거부 워크플로우
Command 객체로 워크플로우를 재개하는 법을 배운 김개발 씨는 실제 비즈니스 로직을 구현하기 시작했습니다. "승인하면 A 작업을, 거부하면 B 작업을 해야 하는데, 어떻게 구현하죠?" 박시니어 씨가 화이트보드에 플로우차트를 그리며 설명했습니다.
"간단해요. interrupt()의 반환값으로 분기 처리하면 됩니다."
승인/거부 워크플로우는 사용자의 결정에 따라 다른 경로로 진행되는 패턴입니다. interrupt()의 반환값을 확인하여 True면 승인 경로, False면 거부 경로로 분기합니다.
이를 통해 사용자의 선택에 따라 완전히 다른 비즈니스 로직을 실행할 수 있습니다.
다음 코드를 살펴봅시다.
from langgraph.graph import StateGraph, START, END
from langgraph.types import interrupt
class OrderState(dict):
order_id: str
amount: int
status: str
def request_approval(state: OrderState):
# 승인 요청
approved = interrupt(
f"주문 #{state['order_id']}: {state['amount']}원을 승인하시겠습니까?"
)
# 승인 여부에 따라 상태 업데이트
if approved:
state["status"] = "approved"
else:
state["status"] = "rejected"
return state
def process_order(state: OrderState):
# 승인된 주문 처리
print(f"주문 {state['order_id']} 처리 중...")
state["status"] = "completed"
return state
def cancel_order(state: OrderState):
# 거부된 주문 취소
print(f"주문 {state['order_id']} 취소됨")
return state
# 조건부 라우팅
def route_by_approval(state: OrderState):
if state["status"] == "approved":
return "process"
else:
return "cancel"
# 그래프 구성
graph = StateGraph(OrderState)
graph.add_node("request", request_approval)
graph.add_node("process", process_order)
graph.add_node("cancel", cancel_order)
graph.add_edge(START, "request")
graph.add_conditional_edges("request", route_by_approval, {
"process": "process",
"cancel": "cancel"
})
graph.add_edge("process", END)
graph.add_edge("cancel", END)
김개발 씨의 주문 시스템이 점점 복잡해지고 있었습니다. 단순히 승인만 받는 것이 아니라, 승인되면 결제를 진행하고, 거부되면 고객에게 알림을 보내야 했습니다.
"이렇게 경로가 나뉘는 건 어떻게 구현하죠?" 박시니어 씨가 대답했습니다. "실제 비즈니스 로직은 대부분 이런 식이에요.
사용자의 선택에 따라 완전히 다른 프로세스가 진행되죠. LangGraph의 조건부 라우팅을 사용하면 깔끔하게 구현할 수 있어요." 그렇다면 승인/거부 워크플로우란 정확히 무엇일까요?
쉽게 비유하자면, 승인/거부 워크플로우는 마치 갈림길과 같습니다. 등산로를 걷다가 갈림길을 만나면, 왼쪽으로 가면 정상으로, 오른쪽으로 가면 계곡으로 내려갑니다.
워크플로우도 마찬가지로, 사용자가 승인하면 한 경로로, 거부하면 다른 경로로 진행됩니다. 조건부 분기가 없다면 어떻게 될까요?
모든 케이스를 하나의 함수에서 처리해야 합니다. if-else 문이 중첩되고, 코드가 복잡해집니다.
승인 로직과 거부 로직이 뒤섞여서 가독성이 떨어지고, 유지보수가 어려워집니다. 더 큰 문제는 확장성입니다.
나중에 "보류" 상태가 추가되면 어떻게 할까요? 기존 함수를 또 수정해야 합니다.
코드가 점점 스파게티처럼 얽혀갑니다. 바로 이런 문제를 해결하기 위해 조건부 라우팅이 존재합니다.
조건부 라우팅을 사용하면 각 경로를 독립적인 노드로 분리할 수 있습니다. request_approval 노드는 승인만 요청하고, process_order 노드는 주문 처리만, cancel_order 노드는 취소만 담당합니다.
각 노드가 하나의 책임만 가지므로 단일 책임 원칙을 지킵니다. 또한 경로 추가가 쉽습니다.
"보류" 상태를 추가하고 싶다면, hold_order 노드를 만들고 라우팅 함수에 조건 하나만 추가하면 됩니다. 기존 코드를 거의 수정하지 않아도 됩니다.
무엇보다 테스트가 간단하다는 큰 이점이 있습니다. 각 노드를 독립적으로 테스트할 수 있습니다.
process_order 함수만 따로 떼어내서 단위 테스트를 작성할 수 있습니다. 위의 코드를 한 줄씩 살펴보겠습니다.
먼저 class OrderState에서 주문의 상태를 정의합니다. order_id, amount, status 필드를 가집니다.
status는 "approved", "rejected", "completed" 등의 값을 가질 수 있습니다. 다음으로 request_approval 함수에서는 interrupt()로 승인을 요청합니다.
사용자의 응답(True/False)을 받아서, True면 status를 "approved"로, False면 "rejected"로 설정합니다. process_order와 cancel_order 함수는 각각 승인과 거부 경로의 비즈니스 로직을 처리합니다.
실제로는 여기서 결제 API를 호출하거나, 이메일을 발송하거나, 데이터베이스를 업데이트하는 등의 작업을 수행합니다. 핵심은 route_by_approval 함수입니다.
이 함수는 라우팅 결정을 담당합니다. state의 status를 확인하여, "approved"면 "process" 노드로, 그렇지 않으면 "cancel" 노드로 보냅니다.
add_conditional_edges("request", route_by_approval, {...})에서 조건부 엣지를 추가합니다. request 노드 다음에 route_by_approval 함수를 실행하여 다음 노드를 결정합니다.
딕셔너리 {"process": "process", "cancel": "cancel"}는 함수의 반환값을 실제 노드 이름으로 매핑합니다. 실제 현업에서는 어떻게 활용할까요?
예를 들어 콘텐츠 검토 시스템을 개발한다고 가정해봅시다. 사용자가 블로그 글을 작성하면, AI가 자동으로 욕설이나 부적절한 내용을 검사합니다.
문제가 발견되면 관리자의 검토를 요청합니다. 관리자가 승인하면 글이 게시되고, 거부하면 작성자에게 수정 요청 알림이 갑니다.
보류하면 다른 관리자에게 재검토를 요청합니다. 이런 복잡한 워크플로우를 조건부 라우팅으로 깔끔하게 구현할 수 있습니다.
또 다른 예로, 구매 승인 시스템을 생각해봅시다. 직원이 물품을 구매하려면 금액에 따라 팀장, 부서장, 임원의 순차적 승인이 필요합니다.
각 단계에서 거부되면 구매가 취소되고, 모두 승인되면 구매가 진행됩니다. 하지만 주의할 점도 있습니다.
초보 개발자들이 흔히 하는 실수 중 하나는 라우팅 함수에 비즈니스 로직을 넣는 것입니다. route_by_approval 함수는 순수하게 경로 결정만 해야 합니다.
데이터베이스를 업데이트하거나, API를 호출하는 등의 사이드 이펙트가 있으면 안 됩니다. 또 다른 실수는 상태를 제대로 업데이트하지 않는 것입니다.
interrupt()의 반환값을 받았으면, 반드시 상태에 저장해야 합니다. 그래야 나중에 라우팅 함수에서 올바른 경로를 선택할 수 있습니다.
다시 김개발 씨의 이야기로 돌아가 봅시다. 박시니어 씨의 설명을 들은 김개발 씨는 코드를 리팩토링했습니다.
승인 로직과 거부 로직을 별도의 노드로 분리하고, 조건부 라우팅으로 연결했습니다. 코드가 훨씬 깔끔해졌습니다.
각 함수가 하나의 책임만 가지므로 이해하기 쉬워졌고, 새로운 기능을 추가하는 것도 간단해졌습니다. 팀원들도 "이제 코드가 읽기 편하네요"라고 칭찬했습니다.
승인/거부 워크플로우 패턴을 제대로 이해하면 복잡한 비즈니스 로직도 명확하게 구조화할 수 있습니다. 여러분도 오늘 배운 내용을 실제 프로젝트에 적용해 보세요.
실전 팁
💡 - 라우팅 함수는 순수 함수로 작성하여 사이드 이펙트가 없도록 하세요
- 각 노드가 하나의 책임만 가지도록 설계하여 단일 책임 원칙을 지키세요
- 상태 필드 이름을 명확하게 지어서 코드의 가독성을 높이세요
4. 상태 검토 및 편집
승인/거부 워크플로우를 완성한 김개발 씨는 또 다른 요구사항을 받았습니다. "사용자가 주문 내용을 수정할 수 있게 해주세요." 박시니어 씨가 고개를 끄덕이며 말했습니다.
"그럴 때는 interrupt()에서 단순히 True/False만 받지 말고, 수정된 상태 전체를 받으면 됩니다."
상태 검토 및 편집은 사용자가 워크플로우 중간에 상태를 확인하고 수정할 수 있게 하는 패턴입니다. interrupt()의 반환값으로 불리언 대신 딕셔너리나 객체를 받아서, 사용자가 입력한 새로운 값으로 상태를 업데이트합니다.
이를 통해 AI가 제안한 내용을 사람이 검토하고 수정하는 협업이 가능해집니다.
다음 코드를 살펴봅시다.
from langgraph.types import interrupt
class DraftState(dict):
title: str
content: str
tags: list
def generate_draft(state: DraftState):
# AI가 초안 작성
state["title"] = "LangGraph 완벽 가이드"
state["content"] = "LangGraph는 복잡한 AI 워크플로우를 구축하는..."
state["tags"] = ["AI", "LangGraph", "Python"]
return state
def review_draft(state: DraftState):
# 사용자에게 초안 검토 요청
reviewed = interrupt({
"message": "초안을 검토하고 수정해주세요",
"current_draft": state
})
# 사용자가 수정한 내용으로 상태 업데이트
if reviewed:
state["title"] = reviewed.get("title", state["title"])
state["content"] = reviewed.get("content", state["content"])
state["tags"] = reviewed.get("tags", state["tags"])
return state
def publish(state: DraftState):
print(f"발행: {state['title']}")
return state
김개발 씨의 시스템이 점점 고도화되고 있었습니다. 이번에는 AI가 블로그 초안을 자동으로 작성하는 기능을 추가하기로 했습니다.
하지만 문제가 있었습니다. AI가 작성한 글을 그대로 발행하면 품질이 들쭉날쭉했습니다.
제품 기획자 최기획 씨가 제안했습니다. "AI가 초안을 작성하면, 사용자가 한번 검토하고 수정할 수 있게 해주세요.
제목도 바꾸고, 내용도 고치고, 태그도 추가할 수 있어야 해요." 김개발 씨는 고민에 빠졌습니다. "지금까지는 True/False만 받았는데, 여러 필드를 어떻게 받죠?" 박시니어 씨가 힌트를 주었습니다.
"interrupt()는 어떤 값이든 받을 수 있어요. 딕셔너리를 받으면 되죠." 그렇다면 상태 검토 및 편집이란 정확히 무엇일까요?
쉽게 비유하자면, 상태 검토 및 편집은 마치 작가와 편집자의 협업과 같습니다. 작가(AI)가 초고를 작성하면, 편집자(사용자)가 검토하면서 문장을 다듬고, 단어를 바꾸고, 문단을 재배치합니다.
최종 원고는 둘의 협업으로 완성됩니다. 상태 편집 기능이 없다면 어떻게 될까요?
사용자는 AI의 결과를 받아들이거나 거부하는 것만 가능합니다. 마음에 들지 않으면 처음부터 다시 실행해야 합니다.
AI가 10개 항목 중 9개는 완벽하게 작성했지만 1개만 잘못되었다면? 전체를 거부하고 다시 생성해야 합니다.
더 큰 문제는 비효율성입니다. AI가 몇 분에 걸쳐 복잡한 분석을 수행했는데, 사소한 부분 하나 때문에 전체를 다시 실행하는 것은 시간과 비용 낭비입니다.
바로 이런 문제를 해결하기 위해 상태 검토 및 편집 패턴이 존재합니다. 이 패턴을 사용하면 부분적인 수정이 가능합니다.
AI가 생성한 결과 중 마음에 드는 부분은 그대로 두고, 수정하고 싶은 부분만 바꿀 수 있습니다. 효율적이고 유연합니다.
또한 사람과 AI의 진정한 협업이 가능해집니다. AI는 빠르게 초안을 작성하고, 사람은 전문성과 창의성을 더합니다.
각자의 강점을 살린 시너지를 낼 수 있습니다. 무엇보다 사용자 경험이 크게 향상된다는 큰 이점이 있습니다.
사용자는 AI의 결과를 수동적으로 받아들이는 것이 아니라, 능동적으로 개선할 수 있습니다. 통제감과 만족도가 높아집니다.
위의 코드를 한 줄씩 살펴보겠습니다. 먼저 generate_draft 함수에서 AI가 블로그 초안을 작성합니다.
제목, 본문, 태그를 자동으로 생성하여 상태에 저장합니다. 다음으로 review_draft 함수에서 핵심 로직이 실행됩니다.
interrupt()에 딕셔너리를 전달합니다. "message"는 사용자에게 표시할 안내 메시지이고, "current_draft"는 현재 상태를 보여줍니다.
사용자는 UI에서 제목을 수정하고, 본문 일부를 고치고, 태그를 추가합니다. 그리고 수정된 내용을 딕셔너리 형태로 반환합니다.
예를 들어 {"title": "LangGraph 심화 가이드", "tags": ["AI", "LangGraph", "Advanced"]}처럼요. reviewed에 사용자의 수정 사항이 담겨 있습니다.
reviewed.get("title", state["title"]) 부분은 "사용자가 title을 수정했으면 새 값을 사용하고, 수정하지 않았으면 기존 값을 유지한다"는 의미입니다. 이렇게 각 필드를 업데이트합니다.
마지막으로 publish 함수에서 최종 승인된 내용을 발행합니다. 이때 상태는 AI의 초안과 사용자의 수정 사항이 결합된 최종 버전입니다.
실제 현업에서는 어떻게 활용할까요? 예를 들어 이메일 자동화 시스템을 개발한다고 가정해봅시다.
AI가 고객에게 보낼 이메일 초안을 작성합니다. 제목, 본문, 첨부 파일 목록을 생성합니다.
직원은 이 초안을 검토하면서 톤을 조정하고, 개인화된 인사말을 추가하고, 불필요한 첨부 파일을 제거합니다. 수정이 완료되면 이메일이 발송됩니다.
AI의 효율성과 사람의 섬세함이 결합된 결과물입니다. 또 다른 예로, 코드 리뷰 시스템을 생각해봅시다.
AI가 풀 리퀘스트를 분석하여 잠재적 버그, 성능 이슈, 코딩 컨벤션 위반을 찾아냅니다. 리뷰어는 AI의 지적 사항을 검토하면서 일부는 승인하고, 일부는 "이건 의도한 것이므로 문제없음"이라고 수정합니다.
하지만 주의할 점도 있습니다. 초보 개발자들이 흔히 하는 실수 중 하나는 기본값 처리를 빠뜨리는 것입니다.
사용자가 일부 필드만 수정하고 나머지는 그대로 두고 싶을 때, 수정하지 않은 필드는 기존 값을 유지해야 합니다. .get(key, default) 패턴을 꼭 사용하세요.
또 다른 실수는 타입 검증을 하지 않는 것입니다. 사용자가 잘못된 형식의 데이터를 보낼 수 있습니다.
예를 들어 tags는 리스트여야 하는데 문자열을 보낼 수 있습니다. 반드시 타입을 검증하고, 잘못된 입력은 에러 메시지와 함께 다시 요청하세요.
다시 김개발 씨의 이야기로 돌아가 봅시다. 박시니어 씨의 조언을 받은 김개발 씨는 상태 검토 기능을 구현했습니다.
AI가 초안을 작성하면, 사용자는 웹 에디터에서 자유롭게 수정할 수 있습니다. 제품 기획자 최기획 씨가 직접 테스트해보고 감탄했습니다.
"완벽해요! AI가 빠르게 초안을 만들어주고, 제가 세부 사항을 다듬으니까 생산성이 3배는 올라간 것 같아요." 상태 검토 및 편집 패턴을 제대로 이해하면 AI와 사람이 진정으로 협업하는 시스템을 만들 수 있습니다.
여러분도 오늘 배운 내용을 실제 프로젝트에 적용해 보세요.
실전 팁
💡 - 사용자가 수정하지 않은 필드는 기존 값을 유지하도록 .get(key, default) 패턴을 사용하세요
- 사용자 입력의 타입과 형식을 반드시 검증하여 예상치 못한 에러를 방지하세요
- UI에서는 현재 상태를 명확하게 보여주어 사용자가 쉽게 검토하고 수정할 수 있도록 하세요
5. 도구 실행 전 승인
블로그 자동화 시스템이 잘 작동하던 어느 날, 큰 사고가 발생했습니다. AI가 실수로 모든 게시글을 삭제하는 API를 호출한 것입니다.
김개발 씨는 식은땀을 흘리며 데이터를 복구했습니다. 박시니어 씨가 진지하게 말했습니다.
"위험한 도구는 실행 전에 반드시 사람의 승인을 받아야 해요."
도구 실행 전 승인은 AI 에이전트가 외부 도구나 API를 호출하기 전에 사람의 허가를 받는 패턴입니다. 특히 데이터 삭제, 결제, 이메일 발송처럼 되돌릴 수 없는 작업은 반드시 승인 절차를 거쳐야 합니다.
interrupt()를 도구 호출 직전에 배치하여 도구 이름과 매개변수를 보여주고, 승인받은 후에만 실행합니다.
다음 코드를 살펴봅시다.
from langgraph.types import interrupt
class ToolState(dict):
tool_name: str
tool_args: dict
result: str
# 위험한 도구 목록
DANGEROUS_TOOLS = ["delete_database", "send_bulk_email", "charge_payment"]
def execute_tool(state: ToolState):
tool_name = state["tool_name"]
tool_args = state["tool_args"]
# 위험한 도구는 승인 필요
if tool_name in DANGEROUS_TOOLS:
approved = interrupt({
"message": f"도구 '{tool_name}' 실행을 승인하시겠습니까?",
"tool_name": tool_name,
"arguments": tool_args,
"warning": "이 작업은 되돌릴 수 없습니다!"
})
if not approved:
state["result"] = "사용자가 거부함"
return state
# 도구 실행 (실제로는 여기서 API 호출)
if tool_name == "delete_database":
state["result"] = f"데이터베이스 {tool_args['db_name']} 삭제 완료"
elif tool_name == "send_email":
state["result"] = f"{tool_args['to']}에게 이메일 발송 완료"
return state
김개발 씨는 AI 에이전트에 점점 더 많은 권한을 부여하고 있었습니다. 처음에는 데이터를 읽기만 했지만, 이제는 쓰기도 하고, 삭제도 하고, 외부 API도 호출합니다.
편리했지만 위험도 컸습니다. 어느 날 재난이 찾아왔습니다.
AI가 "오래된 게시글 정리"라는 작업을 수행하다가, 버그로 인해 모든 게시글을 삭제해버렸습니다. 3년간 쌓인 소중한 콘텐츠가 한순간에 사라졌습니다.
다행히 백업이 있어서 복구했지만, 김개발 씨는 큰 충격을 받았습니다. CTO가 긴급 회의를 소집했습니다.
"AI에게 무제한 권한을 주면 안 됩니다. 위험한 작업은 반드시 사람이 승인해야 합니다." 그렇다면 도구 실행 전 승인이란 정확히 무엇일까요?
쉽게 비유하자면, 도구 실행 전 승인은 마치 은행의 이중 인증과 같습니다. 고액 송금을 하려면 비밀번호를 입력하는 것만으로는 부족합니다.
추가로 SMS 인증이나 보안카드 확인이 필요합니다. AI 도구 호출도 마찬가지로, 위험한 작업은 이중 확인이 필요합니다.
승인 절차가 없다면 어떻게 될까요? AI가 잘못된 판단을 내려도 막을 방법이 없습니다.
데이터가 삭제되고, 잘못된 이메일이 발송되고, 부정확한 결제가 진행됩니다. 사고가 발생한 후에야 알게 되고, 그때는 이미 늦었습니다.
더 큰 문제는 신뢰 상실입니다. 한번 큰 사고가 나면, 사용자들은 AI 시스템을 신뢰하지 않게 됩니다.
"AI가 또 무슨 짓을 할지 몰라"라는 두려움이 생깁니다. 결국 자동화의 이점을 포기하고 수동 프로세스로 되돌아가게 됩니다.
바로 이런 문제를 해결하기 위해 도구 실행 전 승인 패턴이 존재합니다. 이 패턴을 사용하면 위험한 작업을 사전에 차단할 수 있습니다.
AI가 도구를 호출하기 직전에 멈춰서, 사람에게 "정말 이 작업을 해도 될까요?"라고 물어봅니다. 사람이 확인하고 승인해야만 실행됩니다.
또한 투명성이 확보됩니다. 사용자는 AI가 어떤 도구를 어떤 매개변수로 호출하려는지 정확하게 볼 수 있습니다.
블랙박스가 아니라 화이트박스입니다. 무엇보다 규정 준수가 가능하다는 큰 이점이 있습니다.
금융, 의료, 법률 분야에서는 중요한 결정에 사람의 승인이 법적으로 요구되는 경우가 많습니다. 이 패턴을 사용하면 규정을 준수하면서도 자동화의 이점을 누릴 수 있습니다.
위의 코드를 한 줄씩 살펴보겠습니다. 먼저 DANGEROUS_TOOLS 리스트에 위험한 도구들을 정의합니다.
데이터베이스 삭제, 대량 이메일 발송, 결제 처리처럼 되돌릴 수 없는 작업들입니다. execute_tool 함수에서 상태에서 도구 이름과 매개변수를 가져옵니다.
그리고 if tool_name in DANGEROUS_TOOLS로 위험한 도구인지 확인합니다. 위험한 도구라면 interrupt()를 호출합니다.
단순히 메시지만 보내는 것이 아니라, 도구의 상세 정보를 함께 전달합니다. 도구 이름, 매개변수, 경고 메시지를 모두 포함합니다.
사용자는 UI에서 이 정보를 보고 판단합니다. "delete_database 도구가 'production_db'를 삭제하려고 한다"라는 정보를 보고, "아, 이건 위험한데?"라고 판단하여 거부할 수 있습니다.
승인되지 않으면 state["result"] = "사용자가 거부함"으로 설정하고 도구를 실행하지 않습니다. 승인되었을 때만 실제로 도구를 호출합니다.
실제 현업에서는 어떻게 활용할까요? 예를 들어 고객 지원 챗봇을 개발한다고 가정해봅시다.
챗봇은 고객의 질문에 답하고, 주문 상태를 조회하고, FAQ를 검색합니다. 이런 읽기 작업은 자동으로 처리됩니다.
하지만 환불 처리나 계정 삭제는 위험한 작업입니다. 챗봇이 이런 작업을 수행하려고 하면, 상담원에게 알림이 갑니다.
상담원은 고객과의 대화 내역을 검토하고, 정말 환불이 필요한지 확인한 후 승인합니다. 또 다른 예로, 마케팅 자동화 시스템을 생각해봅시다.
AI가 고객 세그먼트를 분석하고, 이메일 캠페인을 기획하고, 메시지를 작성합니다. 하지만 실제로 10만 명에게 이메일을 발송하기 전에는 마케터의 최종 승인이 필요합니다.
하지만 주의할 점도 있습니다. 초보 개발자들이 흔히 하는 실수 중 하나는 모든 도구에 승인을 요구하는 것입니다.
데이터 읽기처럼 안전한 작업까지 승인받게 하면, 사용자가 계속 클릭만 하는 불편한 시스템이 됩니다. 정말 위험한 작업만 선별해야 합니다.
또 다른 실수는 충분한 정보를 제공하지 않는 것입니다. "도구를 실행하시겠습니까?"만 물으면 사용자는 판단할 수 없습니다.
어떤 도구를, 어떤 매개변수로, 어떤 영향을 미칠지 상세하게 보여줘야 합니다. 다시 김개발 씨의 이야기로 돌아가 봅시다.
사고 이후 김개발 씨는 모든 위험한 도구에 승인 절차를 추가했습니다. 삭제, 결제, 대량 발송 등의 작업은 반드시 관리자의 승인을 거치도록 했습니다.
몇 주 후, AI가 또다시 버그로 인해 모든 데이터를 삭제하려고 시도했습니다. 하지만 이번에는 실행되기 전에 멈췄습니다.
관리자에게 알림이 갔고, 관리자는 "이건 이상한데?"라고 판단하여 거부했습니다. 대형 사고를 미연에 방지한 것입니다.
CTO가 김개발 씨를 불러 칭찬했습니다. "이제야 안전한 시스템이 되었군요.
자동화와 안전성을 모두 확보했어요." 도구 실행 전 승인 패턴을 제대로 이해하면 안전하고 신뢰할 수 있는 AI 시스템을 구축할 수 있습니다. 여러분도 오늘 배운 내용을 실제 프로젝트에 적용해 보세요.
실전 팁
💡 - 위험한 도구 목록을 명확하게 정의하고, 정기적으로 검토하여 업데이트하세요
- 사용자에게 도구의 이름, 매개변수, 예상 영향을 상세하게 보여주세요
- 승인 로그를 저장하여 나중에 감사(audit)할 수 있도록 하세요
6. 입력 유효성 검증 패턴
도구 승인 시스템을 구축한 김개발 씨는 또 다른 문제를 발견했습니다. 사용자가 승인은 했지만, 잘못된 값을 입력하는 경우가 생겼습니다.
예를 들어 이메일 주소에 "@"가 없거나, 금액에 음수를 입력하는 경우였습니다. 박시니어 씨가 조언했습니다.
"interrupt()와 반복문을 결합하면, 올바른 입력을 받을 때까지 계속 요청할 수 있어요."
입력 유효성 검증 패턴은 사용자로부터 올바른 형식의 입력을 받을 때까지 반복적으로 요청하는 패턴입니다. interrupt()를 while 루프 안에 배치하고, 입력값을 검증하여 유효하면 루프를 빠져나가고, 유효하지 않으면 오류 메시지와 함께 다시 요청합니다.
이를 통해 잘못된 데이터가 시스템에 들어오는 것을 원천 차단할 수 있습니다.
다음 코드를 살펴봅시다.
from langgraph.types import interrupt
import re
class FormState(dict):
email: str
age: int
valid: bool
def validate_email(email: str) -> tuple[bool, str]:
# 이메일 형식 검증
pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
if re.match(pattern, email):
return True, ""
return False, "유효한 이메일 주소를 입력해주세요 (예: user@example.com)"
def validate_age(age: int) -> tuple[bool, str]:
# 나이 범위 검증
if 0 < age < 150:
return True, ""
return False, "나이는 1~149 사이의 숫자여야 합니다"
def collect_user_info(state: FormState):
# 이메일 검증 루프
while True:
user_email = interrupt("이메일 주소를 입력해주세요:")
is_valid, error_msg = validate_email(user_email)
if is_valid:
state["email"] = user_email
break
else:
# 오류 메시지와 함께 다시 요청
interrupt({"error": error_msg, "retry": True})
# 나이 검증 루프
while True:
user_age = interrupt("나이를 입력해주세요:")
is_valid, error_msg = validate_age(user_age)
if is_valid:
state["age"] = user_age
state["valid"] = True
break
else:
interrupt({"error": error_msg, "retry": True})
return state
김개발 씨의 시스템이 점점 완성도를 높여가고 있었습니다. 하지만 사용자들이 예상치 못한 방식으로 시스템을 사용하면서 새로운 문제가 발생했습니다.
어느 사용자가 이메일 주소에 "abc"만 입력하고 승인했습니다. 시스템은 이 값을 그대로 받아서 이메일 발송 API를 호출했고, 당연히 실패했습니다.
또 다른 사용자는 나이에 "-5"를 입력했고, 데이터베이스에 잘못된 값이 저장되었습니다. QA 팀의 이지원 씨가 버그 리포트를 작성했습니다.
"사용자 입력에 대한 검증이 없어서 잘못된 데이터가 계속 들어오고 있어요." 김개발 씨는 고개를 끄덕였습니다. "입력값을 검증해야 하는데, 어떻게 하면 좋을까요?" 박시니어 씨가 화이트보드에 그림을 그리며 설명했습니다.
"간단해요. 검증 로직과 interrupt()를 반복문으로 묶으면 됩니다.
올바른 값을 받을 때까지 계속 요청하는 거죠." 그렇다면 입력 유효성 검증 패턴이란 정확히 무엇일까요? 쉽게 비유하자면, 입력 유효성 검증은 마치 출입 보안 검색대와 같습니다.
공항에서 보안 검색을 통과하려면 금속 물체를 제거해야 합니다. 검색대가 울리면 통과할 수 없고, 다시 돌아가서 물건을 빼고 재검색을 받아야 합니다.
입력 검증도 마찬가지로, 기준을 통과할 때까지 받아주지 않습니다. 입력 검증이 없다면 어떻게 될까요?
시스템은 쓰레기가 들어가면 쓰레기가 나오는 상태가 됩니다. 잘못된 데이터로 계산하면 잘못된 결과가 나옵니다.
이메일 주소가 올바르지 않으면 발송이 실패하고, 금액이 음수면 회계가 꼬입니다. 더 큰 문제는 보안 취약점입니다.
악의적인 사용자가 SQL 인젝션이나 XSS 공격을 시도할 수 있습니다. 입력 검증 없이 사용자 입력을 그대로 데이터베이스나 웹페이지에 사용하면 큰 보안 사고가 발생합니다.
바로 이런 문제를 해결하기 위해 입력 유효성 검증 패턴이 존재합니다. 이 패턴을 사용하면 데이터 품질이 보장됩니다.
시스템에 들어오는 모든 데이터가 정해진 규칙을 만족합니다. 이메일은 올바른 형식이고, 나이는 합리적인 범위이고, 전화번호는 숫자로만 구성됩니다.
또한 사용자 경험이 향상됩니다. 잘못된 입력을 하면 즉시 피드백을 받아서 바로 수정할 수 있습니다.
나중에 "이메일 발송 실패"라는 에러를 보는 것보다, 입력 시점에 "이메일 형식이 올바르지 않습니다"라는 메시지를 보는 것이 훨씬 좋습니다. 무엇보다 시스템 안정성이 크게 향상된다는 큰 이점이 있습니다.
예상치 못한 입력으로 인한 크래시나 버그가 줄어듭니다. 방어적 프로그래밍의 핵심입니다.
위의 코드를 한 줄씩 살펴보겠습니다. 먼저 validate_email 함수는 정규표현식으로 이메일 형식을 검증합니다.
유효하면 (True, "")를 반환하고, 유효하지 않으면 (False, 오류메시지)를 반환합니다. 이런 패턴은 튜플 언패킹으로 간편하게 사용할 수 있어서 자주 쓰입니다.
validate_age 함수는 나이가 1~149 사이인지 확인합니다. 0 이하나 150 이상은 현실적이지 않으므로 거부합니다.
collect_user_info 함수의 핵심은 while True 루프입니다. 무한 루프로 시작하여, 올바른 입력을 받을 때까지 계속 반복합니다.
user_email = interrupt("이메일 주소를 입력해주세요:")로 사용자에게 입력을 요청합니다. 그리고 validate_email(user_email)로 검증합니다.
검증을 통과하면 state["email"] = user_email로 상태에 저장하고 break로 루프를 빠져나갑니다. 검증 실패 시에는 interrupt({"error": error_msg, "retry": True})로 오류 메시지를 보여줍니다.
이때 사용자는 "오류: 유효한 이메일 주소를 입력해주세요"라는 메시지를 보고, 다시 입력 화면으로 돌아갑니다. 루프가 계속되므로, 올바른 값을 입력할 때까지 반복됩니다.
나이 입력도 같은 패턴으로 처리합니다. 모든 입력이 유효하면 state["valid"] = True로 설정하고 함수를 종료합니다.
실제 현업에서는 어떻게 활용할까요? 예를 들어 회원가입 폼을 개발한다고 가정해봅시다.
사용자는 이름, 이메일, 비밀번호, 전화번호를 입력해야 합니다. 각 필드마다 검증 규칙이 있습니다.
이메일은 올바른 형식이어야 하고, 비밀번호는 8자 이상에 특수문자를 포함해야 하고, 전화번호는 010으로 시작하는 11자리 숫자여야 합니다. 입력 유효성 검증 패턴을 사용하면, 각 필드를 순차적으로 검증하면서 실시간 피드백을 제공할 수 있습니다.
또 다른 예로, 설문조사 시스템을 생각해봅시다. 질문마다 답변 형식이 다릅니다.
객관식은 1~5 사이의 숫자, 주관식은 최소 10자 이상, 이메일 항목은 올바른 이메일 형식이어야 합니다. 각 질문에 맞는 검증 로직을 적용하여 데이터 품질을 보장할 수 있습니다.
하지만 주의할 점도 있습니다. 초보 개발자들이 흔히 하는 실수 중 하나는 너무 엄격한 검증입니다.
예를 들어 이름에 한글과 영문만 허용하면, "김-철수"나 "O'Brien" 같은 합법적인 이름이 거부됩니다. 검증 규칙은 현실을 반영해야 합니다.
또 다른 실수는 사용자를 무한 루프에 갇히게 하는 것입니다. 검증 규칙이 너무 복잡하거나 오류 메시지가 불친절하면, 사용자는 계속 실패만 하고 앞으로 나아가지 못합니다.
명확한 오류 메시지와 예시를 제공해야 합니다. 다시 김개발 씨의 이야기로 돌아가 봅시다.
박시니어 씨의 조언을 받은 김개발 씨는 모든 입력 필드에 검증 로직을 추가했습니다. 이메일, 전화번호, 나이, 금액 등 각 필드마다 적절한 규칙을 적용했습니다.
QA 팀의 이지원 씨가 다시 테스트를 진행했습니다. 이번에는 일부러 잘못된 값을 입력해봤지만, 시스템이 정확하게 잡아냈습니다.
"유효한 이메일을 입력해주세요", "나이는 1~149 사이여야 합니다"라는 친절한 메시지가 표시되었습니다. 몇 주 후 데이터베이스를 확인해보니, 잘못된 데이터가 거의 사라졌습니다.
사용자 만족도도 올라갔습니다. "이제 뭘 입력해야 하는지 명확하게 알려줘서 좋아요"라는 피드백이 들어왔습니다.
입력 유효성 검증 패턴을 제대로 이해하면 견고하고 사용자 친화적인 시스템을 구축할 수 있습니다. 여러분도 오늘 배운 내용을 실제 프로젝트에 적용해 보세요.
실전 팁
💡 - 검증 규칙은 너무 엄격하지 않게, 현실적인 사례를 모두 수용할 수 있도록 설계하세요
- 오류 메시지는 구체적이고 친절하게 작성하여 사용자가 쉽게 수정할 수 있도록 하세요
- 정규표현식 같은 복잡한 검증 로직은 별도 함수로 분리하여 테스트하고 재사용하세요
이상으로 학습을 마칩니다. 위 내용을 직접 코드로 작성해보면서 익혀보세요!
댓글 (0)
함께 보면 좋은 카드 뉴스
Helm 마이크로서비스 패키징 완벽 가이드
Kubernetes 환경에서 마이크로서비스를 효율적으로 패키징하고 배포하는 Helm의 핵심 기능을 실무 중심으로 학습합니다. Chart 생성부터 릴리스 관리까지 체계적으로 다룹니다.
보안 아키텍처 구성 완벽 가이드
프로젝트의 보안을 처음부터 설계하는 방법을 배웁니다. AWS 환경에서 VPC부터 WAF, 암호화, 접근 제어까지 실무에서 바로 적용할 수 있는 보안 아키텍처를 단계별로 구성해봅니다.
AWS Organizations 완벽 가이드
여러 AWS 계정을 체계적으로 관리하고 통합 결제와 보안 정책을 적용하는 방법을 실무 스토리로 쉽게 배워봅니다. 초보 개발자도 바로 이해할 수 있는 친절한 설명과 실전 예제를 제공합니다.
AWS KMS 암호화 완벽 가이드
AWS KMS(Key Management Service)를 활용한 클라우드 데이터 암호화 방법을 초급 개발자를 위해 쉽게 설명합니다. CMK 생성부터 S3, EBS 암호화, 봉투 암호화까지 실무에 필요한 모든 내용을 담았습니다.
AWS Secrets Manager 완벽 가이드
AWS에서 데이터베이스 비밀번호, API 키 등 민감한 정보를 안전하게 관리하는 Secrets Manager의 핵심 개념과 실무 활용법을 배워봅니다. 초급 개발자도 쉽게 따라할 수 있도록 실전 예제와 함께 설명합니다.