시리야, 응급실 병상 몇 개 남았어? — n8n을 iOS 단축어 음성 에이전트로 쓰기
이 글과 연결된 글 보기
드래그로 이동 · 휠/두 손가락으로 확대 · 방향키로 글 선택 · 0으로 맞춤. 실선: 본문 링크, 점선: 시리즈·주제의 대표 연결.표시된 글 7편 목록
아이폰의 "시리에게 말하기" 단축어로 질문을 던지면, n8n의 Webhook이 음성 텍스트를 받아 AI Agent에게 넘기고, Agent가 필요하다고 판단한 도구 — 공공데이터 응급실 API — 를 스스로 호출해 음성으로 읽어줄 답을 돌려줍니다. 이 워크플로우를 뜯어보면 브라우저나 슬랙 없이 "말"만으로 n8n을 실행하는 구조를 손에 익히게 되고, 응급실 API 자리에 다른 도구를 끼우면 그 구조를 그대로 재사용할 수 있습니다.
이 워크플로우 하나로 보여줄 수 있는 것
면접에서 "n8n으로 뭘 만들어봤어요?"라는 질문에 "말로 질문하면 AI Agent가 도구를 스스로 판단해서 부르고, 그 답을 음성으로 돌려받는 구조를 만들었습니다"라고 답할 수 있는 워크플로우입니다. 들어간 역량은 세 가지입니다.
- 브라우저 밖에서 트리거하기 — 슬랙이나 캔버스 버튼이 아니라 iOS 단축어가 보내는 POST 요청으로 워크플로우를 시작해본 경험
- Agent의 도구 호출 설계 — AI Agent에 Tool Workflow를 연결해, Agent가 스스로 도구 호출 여부를 판단하게 만든 경험
- 응답 형식을 소비자에 맞추기 — JSON이 아니라 시리가 그대로 읽을 평문(text/plain)을 돌려주도록 Respond to Webhook을 설정한 경험
세 가지 모두 "말로 실행하는 자동화" 전체에 재사용됩니다. 응급실 API 자리에 날씨, 사내 문서 검색, 캘린더 조회 같은 다른 도구를 끼우면 같은 뼈대로 다른 음성 비서를 만들 수 있습니다.
배경 — 화면을 볼 수 없을 때도 n8n을 쓰고 싶다면
n8n 워크플로우는 대부분 브라우저에서 시작합니다. 캔버스를 열고 버튼을 누르거나, 정해진 시간에 스스로 돌거나, 슬랙 메시지가 트리거가 됩니다. 운전 중이거나 손이 바쁠 때는 이 중 어느 것도 쓸 수 없습니다. 이 워크플로우는 그 순간에 "시리에게 말하기"만으로 n8n을 실행할 수 있게 만든 것입니다.
이 시리즈의 MCP 서버 편에서 AI Agent에 Tool을 연결하는 법을, NFC 웹훅 편에서 GET 방식으로 Webhook을 여는 법을 다뤘다면, 이번 편은 그 둘을 합칩니다. 시리가 받아 적은 음성 텍스트를 POST로 Webhook에 보내고, AI Agent가 그 텍스트를 해석해 도구를 직접 판단해 부르는 구조입니다. 설치와 기본 노드 설명은 n8n으로 반복업무 자동화 포트폴리오 만들기에 정리돼 있어, 이 글에서는 워크플로우 자체만 다룹니다.
이 워크플로우가 부르는 도구는 보건복지부 응급의료포털이 제공하는 공공데이터 API로, 지역별 응급실의 실시간 병상 현황을 돌려줍니다. 원본 캔버스에는 특정 종합병원 하나가 이름 그대로 하드코딩돼 있었는데, 이 글에서는 그 이름을 옮기지 않고 "한 종합병원"으로만 부릅니다. 뒤에서 보듯 이 값은 원하는 병원 이름으로 바로 바꿔 쓸 수 있는 자리입니다. 그대로 따라 하려면 아이폰(단축어 앱)이 필요하고, 나머지는 사실상 무료입니다. data.go.kr 키는 무료로 발급되고, Agent가 쓰는 gpt-4.1-mini는 저가형 모델이라 비용 부담도 크지 않습니다.
구조 — Trigger / Task / Target
이 워크플로우는 입구가 하나가 아닙니다. 사람이 들어오는 문(Webhook)과, Agent가 도구를 부를 때 스스로 다시 들어오는 문(Execute Workflow Trigger)이 같은 캔버스 안에 있습니다.
- Trigger — 사용자 쪽에서는 iOS 단축어가 보내는 POST 요청(Webhook), Agent 쪽에서는 도구가 필요할 때마다 워크플로우 자신을 다시 부르는 Execute Workflow Trigger, 그리고 캔버스에서 직접 테스트할 Manual Trigger — 입구가 셋입니다.
- Task — AI Agent가 질문을 해석 → 도구가 필요하면 같은 워크플로우를 다시 호출해 공공데이터 API를 조회 → 여러 병원 중 하나로 좁히기
- Target — Respond to Webhook이 평문 텍스트를 돌려주고, 단축어가 그 텍스트를 시리 음성으로 읽습니다.
만들기
1. iOS 단축어에서 Siri 음성을 POST로 보내기
단축어 앱에서 "시리에게 말하기"로 음성을 텍스트로 받고, "URL의 콘텐츠 가져오기" 액션의 메서드를 POST로, URL은 이 워크플로우 Webhook 노드의 Production URL로 둡니다. 뒤에서 볼 AI Agent 노드는 {{ $json.body.parseJson().input }}으로 텍스트를 꺼내는데, 요청 본문이 문자열로 들어와 다시 JSON으로 파싱한 뒤 input 필드를 읽는다는 뜻입니다. 즉 단축어가 보내는 본문은 {"input": "시리가 받아 적은 문장"} 모양이어야 합니다. 단축어 쪽 액션 구성 자체는 이 워크플로우 JSON에 남아 있지 않으므로, 위 모양의 본문을 만든다는 원칙만 지키면 됩니다.
GET이 아니라 POST를 쓴 이유도 짚어볼 만합니다. 시리가 받아 적는 문장은 길이가 들쭉날쭉하고 물음표·쉼표 같은 문장부호와 한글이 섞여 있어, GET 쿼리 파라미터로 보내려면 URL 인코딩을 거쳐야 하고 길이 제한에도 걸릴 수 있습니다. POST로 JSON body에 담으면 인코딩 걱정 없이 문장을 그대로 실어 보낼 수 있어, 형식이 정해지지 않은 음성 텍스트에는 더 안전합니다.
2. Webhook — POST만 받고, 응답은 나중에
Webhook 노드는 HTTP Method를 POST, Path를 hospital로, Respond를 Using 'Respond to Webhook' Node로 둡니다. 이 설정 덕분에 Webhook 노드가 바로 응답하지 않고 뒤쪽의 Respond to Webhook 노드가 실행될 때까지 기다립니다. AI Agent가 답을 만드는 데 시간이 걸리므로 이 대기가 필요합니다.
3. AI Agent — 텍스트를 읽고, 도구를 판단해서 부르기
AI Agent 노드(원본 이름: Check Availablity Room Voice AI Agent)는 Prompt Type을 Define으로, Text를 {{ $json.body.parseJson().input }}으로 둡니다. Chat Model에는 OpenAI(gpt-4.1-mini)를, Tool에는 응급실 API를 호출하는 Tool Workflow 노드를 연결합니다.
시스템 프롬프트는 응급실 API 응답의 필드 30개 가량 — 병상 종류별 가용 수(hvec, hvicc 등), 장비 가용 여부(hvctayn, hvmriayn 등), 당직의 연락처(hv1~hv12) — 을 하나하나 설명하고, 값을 음성으로 어떻게 바꿔 읽을지 규칙도 정해뒀습니다.
당신은 음성으로 답변을 전달하는 도우미입니다. 사용자가 모바일에서
음성으로 듣기 때문에 답변은 짧고 간결해야 합니다. 줄바꿈 기호 같은
특수문자는 쓰지 마세요.
숫자·기호 값을 말로 바꾸는 규칙, 그리고 도구를 언제 부를지도 프롬프트에 직접 적혀 있습니다.
"Y": 현재 사용 가능 → 음성으로는 "사용 가능하다" 또는 "있다"고 짧게 답한다.
"N1": 현재 사용 불가 → 음성으로는 "현재 사용 불가능하다" 또는 "없다"고 답한다.
응급실 병동현황에 대해 물어보면 getEmrrmRltmUsefulSckbdInfoInqire
도구를 사용해서 답변해주세요.
이 두 조각만 봐도 프롬프트의 역할이 분명해집니다. 원시 API 값을 사람 말로 바꾸는 규칙을 정하고, 도구를 언제 쓸지 판단 기준을 주는 것입니다.
4. HTTP Request — 공공데이터 API를 직접 부르기
Get Emergency API1 노드는 https://apis.data.go.kr/B552657/ErmctInfoInqireService/getEmrrmRltmUsefulSckbdInfoInqire를 GET으로 호출합니다. Query Parameters는 다음과 같습니다.
serviceKey— data.go.kr 인증키. 원본은 값을 파라미터에 직접 적어뒀지만, 옮기지 않습니다. Authentication을 Generic Credential Type → Query Auth로 바꾸면 export한 JSON에 키가 남지 않습니다.STAGE1,STAGE2— 지역을 좁히는 파라미터입니다. 서울특별시 강남구를 보고 싶다면 각각서울특별시,강남구를 넣습니다.pageNo,numOfRows— 페이지네이션 값입니다. 원본은numOfRows를 500으로 크게 잡아 해당 지역 응급실을 한 번에 받아옵니다.
Header에는 accept: application/json을 추가합니다. 입구는 둘입니다. 캔버스에서 바로 테스트하는 Manual Trigger와, AI Agent가 도구를 부를 때 실행되는 Execute Workflow Trigger입니다.
5. Split Out + Filter — 병원 하나만 골라내기
API 응답은 response.body.items.item 배열 안에 병원 여러 개를 한 번에 담아 옵니다. Split Out 노드에서 Field To Split Out을 response.body.items.item으로 두면 병원 하나가 n8n 아이템 하나가 됩니다.
이어지는 Filter 노드가 조건 하나로 그중 하나만 남깁니다.
- 왼쪽 값:
{{ $json.dutyName }} - 연산자: String → is equal to
- 오른쪽 값: 확인하고 싶은 병원 이름
원본에는 특정 종합병원 이름이 문자 그대로 박혀 있지만, API 응답의 dutyName 필드와 정확히 같은 표기여야 필터가 걸린다는 점만 유의하면 원하는 병원 이름으로 바꿔 쓸 수 있습니다. Filter를 통과한 아이템이 이 서브플로우에서 마지막으로 연결된 노드의 출력이 되고, 이 값이 곧 도구 호출의 반환값이 됩니다. Tool Workflow 노드는 실행된 경로의 마지막 노드 출력을 그대로 돌려주기 때문입니다.
6. 워크플로우가 자기 자신을 도구로 부르는 구조
getEmrrmRltmUsefulSckbdInfoInqire라는 이름의 Tool Workflow 노드가 AI Agent에 도구로 연결돼 있는데, 이 노드가 가리키는 워크플로우는 다른 파일이 아니라 지금 보고 있는 워크플로우 자신입니다. 캔버스 하나가 두 역할을 동시에 합니다. Webhook으로 들어오면 "질문에 답하는 에이전트"가 되고, Execute Workflow Trigger로 들어오면 "API를 조회해 병원 하나를 돌려주는 도구"가 됩니다. 서브워크플로우를 별도 파일로 만들지 않아도 되니 캔버스도, 고칠 지점도 하나로 줄어듭니다. When Executed by Another Workflow 트리거 이름과 Tool Workflow 노드의 workflowId를 나란히 보면 셀프 호출이라는 걸 확인할 수 있습니다.
7. Respond to Webhook — 시리가 읽을 수 있는 평문으로
Respond to Webhook 노드는 Respond With를 Text로, Response Body를 {{ $json.output }}으로, Content-Type 헤더를 text/plain; charset=utf-8로 둡니다. output은 AI Agent의 기본 출력 필드 이름입니다. 이 값을 JSON으로 감싸지 않고 평문으로 돌려주는 게 중요합니다. 단축어의 "말하기" 액션이 응답 문자열을 그대로 읽으려면, 앞에 {"output": "..."} 같은 JSON 껍데기가 없어야 하기 때문입니다.
원본 캔버스에 남아 있는 흔적
이 워크플로우는 처음부터 새로 만든 게 아닙니다. 캔버스의 첫 스티키 노트는 n8n이 공식 템플릿 갤러리에 올려둔 "Hey Siri, Ask Agent" 템플릿 설명 그대로입니다. Apple 단축어로 시리에게 말하면 Webhook을 거쳐 AI Agent가 답하고, 시리가 그 답을 다시 읽어주는 골격이 이 템플릿에서 왔다는 뜻입니다. 만든 사람은 그 위에 응급실 API를 부르는 도구 하나를 얹었습니다.
캔버스에는 본 흐름과 연결되지 않은 노드도 셋 남아 있습니다. Extract hvec1이라는 Set 노드는 hospital_id, dutyName, hvec, hvidate 네 필드만 뽑아 정리해두지만 지금은 어디에도 연결돼 있지 않습니다. 필요한 필드만 추려 넘기는 방법을 시도했다가, Filter를 통과한 아이템을 그대로 돌려주는 더 단순한 방식을 택한 흔적으로 보입니다. 이름이 그냥 "AI Agent"인, Aggregate 노드에 연결된 두 번째 Agent 노드도 연결이 끊겨 있는데, 결과를 별도 Agent에 한 번 더 넘기는 방법을 시도했다가 지금 쓰는 Tool Workflow 자기 호출 방식으로 정리된 것으로 보입니다.
한계
- 병원 하나로 고정 — Filter가 병원 이름 하나만 통과시켜, "다른 병원은?"이라고 물어도 데이터가 넘어오지 않습니다. 여러 병원을 비교하려면 Filter 대신 Agent가 질문에서 병원명·지역을 직접 추출해 API 파라미터로 넘기게 바꿔야 합니다.
- Webhook에 인증이 없음 — Production URL만 알면 누구나 실행할 수 있습니다. 응급실 병상 정보는 공공데이터라 문제는 적지만, 다른 API로 바꿔 쓴다면 Header Auth를 걸어두는 편이 안전합니다.
- 단축어 쪽 설정은 캔버스에 남지 않음 — 이 JSON에는 단축어의 요청 구성이 없고, Webhook이 받는 값의 모양만 유추할 수 있습니다.
- API 실패 처리 없음 —
Get Emergency API1이 실패하거나 응답이 비어 있어도 별도 에러 처리가 없어, Agent는 빈 값을 "모른다"로 답하게 됩니다.
포트폴리오·면접 포인트
같은 뼈대에서 응급실 API 자리를 날씨 API, 사내 문서 검색, 캘린더 조회 같은 다른 도구로 바꾸면 "말로 실행하는 비서" 여러 개를 같은 패턴으로 만들 수 있습니다. Filter로 병원 하나만 고정하는 대신 Agent가 질문에서 조건을 직접 추출하게 바꾼 버전을, "처음 버전의 한계를 이렇게 고쳤다"는 한 문단과 함께 포트폴리오에 붙이면 좋습니다.
면접에서 "왜 GET이 아니라 POST로 웹훅을 받았나요?"라는 질문에는 음성 텍스트는 길이와 문장부호가 들쭉날쭉해 URL 인코딩보다 JSON body가 안전하다는 점을, "왜 서브워크플로우를 따로 만들지 않았나요?"라는 질문에는 워크플로우가 자기 자신을 도구로 부르게 해서 관리할 캔버스와 고칠 지점을 하나로 줄인 선택이었다는 점을 답하면 됩니다.
다음 액션
다음 편에서는 다시 사람이 말을 걸지 않아도 스스로 도는 방향으로 넘어갑니다. 카드뉴스 이미지를 만들어 인스타그램에 자동으로 발행하는 워크플로우인데, 사람이 그때그때 트리거하는 대신 정해진 주기로 콘텐츠를 만들어 내보내는 흐름을 다룹니다.
