외부 API 활용 & Webhook 응답 서버
실습 대상 — 포켓몬 무료 API (PokeAPI)
이번 강에서는 인증이 필요 없는 무료 API 로 PokeAPI 를 씁니다. 포켓몬 이름·타입·진화체인 등을 GET 요청 하나로 얻을 수 있어 API 학습용으로 딱입니다.
https://pokeapi.co/api/v2/evolution-chain/{id}/
https://pokeapi.co/docs/v2#evolution-chains
GET 방식은 URL 뒤 파라미터로 요청 정보를 담고, POST 방식은 요청 본문(body)에 정보를 담습니다. 이번 강에서는 GET 만 사용합니다.
실제로 브라우저 주소창에 GET URL 을 붙여넣기만 해도 API 가 응답합니다.
https://.../webhook/pokemon?id=10 을 열면 포켓몬 진화 카드가 HTML 로 렌더 되는 서비스를 만듭니다.HTTP Request 노드로 GET 요청
먼저 새 워크플로우를 만들고 Manual Trigger 뒤에 HTTP Request 노드를 붙입니다.
노드가 캔버스에 붙었으면 HTTP Request 노드를 클릭 해서 파라미터 세팅 화면을 엽니다.
메소드는 GET, URL 은 피카츄가 속한 진화체인(ID 10) 으로 하드코딩합니다. 피추 → 피카츄 → 라이츄 로 이어지는 체인입니다.
https://pokeapi.co/api/v2/evolution-chain/10/
Execute step 을 누르면 OUTPUT 에 진화체인 원본 JSON 이 그대로 담겨옵니다. baby_trigger_item / chain / evolves_to 필드가 보이면 성공.
기존 Manual Trigger 삭제
외부에서 호출 가능한 API 서버로 만들려면 수동 트리거를 없애고 Webhook 트리거로 교체 해야 합니다. 우선 기존 Manual Trigger 노드를 선택해서 삭제.
Webhook 트리거 노드 추가
빈 트리거 자리에 Webhook 노드를 검색해서 추가합니다.
Webhook 노드 설정. HTTP Method = GET, Path = pokemon, Respond = Using 'Respond to Webhook' Node 로 지정합니다. 상단에 Test URL / Production URL 두 개가 자동으로 만들어집니다.
HTTP 노드 URL 을 파라미터로 받도록 변경
이전엔 URL 에 25 를 하드코딩했지만 이제는 Webhook 요청 파라미터 id 를 URL 에 꽂아 씁니다. HTTP 노드의 URL 필드를 다음처럼 표현식(expression) 으로 변경합니다.
https://pokeapi.co/api/v2/evolution-chain/{{ $json.query.id }}/
$json.query.id 는 URL 뒤 ?id=25 같은 쿼리 파라미터를 그대로 꺼내온 값입니다. 이제 다른 ID 를 요청해도 HTTP 노드가 자동으로 대응.Code 노드 추가 — 진화체인 파싱 + HTML 생성
HTTP 노드 뒤에 Code 노드를 붙입니다. 이 노드가 원본 JSON 을 진화 트리로 파싱 하고, 그 결과를 HTML 문자열 로 만들어 다음 노드(Respond to Webhook)에 넘겨줍니다.
Code 노드 스크립트 (JavaScript)
Mode = Run Once for All Items, Language = JavaScript. 아래 스크립트를 통째로 붙여넣으세요. PokeAPI 의 chain 트리를 순회하며 노드 카드와 진화 조건 뱃지 를 HTML 로 조립합니다.
// PokéAPI evolution chain 데이터에서 시작 노드 찾기
const root = $json.chain ?? $json;
// 진화 조건을 읽기 쉽게 변환
function getEvolutionCondition(details = []) {
if (!details.length) return '';
const d = details[0];
const conditions = [];
if (d.min_level) {
conditions.push(`Lv.${d.min_level}`);
}
if (d.min_happiness) {
conditions.push(`친밀도 ${d.min_happiness}+`);
}
if (d.time_of_day) {
conditions.push(
d.time_of_day === 'day'
? '낮'
: d.time_of_day === 'night'
? '밤'
: d.time_of_day
);
}
if (d.item?.name) {
conditions.push(`아이템: ${d.item.name}`);
}
if (d.held_item?.name) {
conditions.push(`소지 아이템: ${d.held_item.name}`);
}
if (d.known_move?.name) {
conditions.push(`기술: ${d.known_move.name}`);
}
if (d.min_beauty) {
conditions.push(`아름다움 ${d.min_beauty}+`);
}
if (d.trigger?.name === 'trade') {
conditions.push('통신교환');
}
if (d.trigger?.name === 'use-item' && !d.item) {
conditions.push('아이템 사용');
}
return conditions.length
? conditions.join(' · ')
: '';
}
// 진화 트리를 HTML로 변환
function renderEvolution(node) {
const name = node.species?.name ?? 'unknown';
let html = `
<div class="pokemon">
<div class="pokemon-name">${name}</div>
</div>
`;
if (node.evolves_to?.length) {
for (const next of node.evolves_to) {
const condition = getEvolutionCondition(
next.evolution_details
);
html += `
<div class="arrow">
<div class="arrow-icon">↓</div>
${
condition
? `<div class="condition">${condition}</div>`
: ''
}
</div>
`;
html += renderEvolution(next);
}
}
return html;
}
const evolutionHtml = renderEvolution(root);
const html = `
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<title>포켓몬 진화 과정</title>
<style>
* {
box-sizing: border-box;
}
body {
margin: 0;
font-family:
Arial,
"Apple SD Gothic Neo",
"Noto Sans KR",
sans-serif;
background: #f5f7fb;
display: flex;
justify-content: center;
padding: 60px 20px;
}
.container {
width: 100%;
max-width: 600px;
background: white;
border-radius: 24px;
padding: 40px;
box-shadow:
0 12px 40px rgba(0,0,0,0.08);
text-align: center;
}
.title {
font-size: 28px;
font-weight: 800;
margin-bottom: 40px;
}
.evolution {
display: flex;
flex-direction: column;
align-items: center;
}
.pokemon {
width: 260px;
padding: 22px;
background:
linear-gradient(
135deg,
#fff7d6,
#fff
);
border: 2px solid #ffd84d;
border-radius: 18px;
box-shadow:
0 5px 15px rgba(0,0,0,0.07);
}
.pokemon-name {
font-size: 24px;
font-weight: 800;
text-transform: capitalize;
}
.arrow {
padding: 12px 0;
display: flex;
flex-direction: column;
align-items: center;
}
.arrow-icon {
font-size: 30px;
color: #888;
}
.condition {
margin-top: 5px;
padding: 6px 12px;
background: #eef2ff;
border-radius: 20px;
color: #4a5ab8;
font-size: 13px;
font-weight: 600;
}
</style>
</head>
<body>
<div class="container">
<div class="title">
Pokémon Evolution
</div>
<div class="evolution">
${evolutionHtml}
</div>
</div>
</body>
</html>
`;
return [
{
json: {
html
}
}
];
return [{ json: { html } }]. 다음 노드(Respond to Webhook) 에서 {{ $json.html }} 로 이 문자열을 그대로 응답 본문으로 씁니다.Respond to Webhook 노드 추가
Code 노드 뒤에 Respond to Webhook 노드를 붙입니다. 이 노드가 실제로 브라우저·클라이언트에게 반환할 HTTP 응답 을 만드는 곳입니다.
Respond With = Text 선택
Respond With 드롭다운에서 Text 를 선택합니다. HTML 문자열을 그대로 본문으로 넘길 거라 Text 가 적합.
Response Body — Code 노드 결과 참조
Response Body 에는 앞 Code 노드가 만든 HTML 문자열을 표현식 으로 꽂아 넣습니다.
{{ $json.html }}
Options → Response Headers 추가
Options 섹션에서 Response Headers 옵션을 추가합니다.
Content-Type: text/html 헤더 값 지정
Entries 에 새 헤더 하나를 추가하고 Name 과 Value 를 아래처럼 지정합니다.
Content-Type
text/html; charset=utf-8
워크플로우 Publish — 활성화
여기까지 세팅했으면 오른쪽 상단 Publish 를 눌러 워크플로우를 활성화합니다. 활성화 전에는 Test URL 로만 호출되고 매번 "Listen for test event" 를 눌러야 하지만, Publish 후에는 Production URL 이 상시 대기합니다.
/webhook/pokemon) 로 GET 요청이 오면 n8n 이 자동으로 워크플로우를 실행하고 HTML 응답을 돌려줍니다.Production URL 확인 · 기억
Webhook 노드를 다시 열고 Production URL 탭으로 전환하면 활성화된 서비스의 실제 접근 URL 이 나옵니다. 이 URL 을 복사해두세요.
브라우저에서 ?id=… 파라미터 붙여 호출
브라우저 주소창에 방금 복사한 Production URL 뒤에 ?id=10 을 붙여서 접속합니다. 10 은 피추(Pichu) → 피카츄 → 라이츄 진화체인 ID.
https://[내-n8n-도메인]/webhook/pokemon?id=10
- 다른 진화체인 ID 시도:
?id=1(이상해씨 체인),?id=2(파이리 체인),?id=10(피추/피카츄 체인) — 포켓몬 도감 번호가 아닌 체인 번호 임에 주의 - 확장 아이디어 ① 진화 체인 대신 포켓몬 상세 정보(
/api/v2/pokemon/{id}) 를 렌더 - 확장 아이디어 ② OpenAI 노드 를 붙여 각 포켓몬 소개문을 AI 로 자동 작성
- 확장 아이디어 ③ 응답을 JSON 으로 바꿔서 다른 프론트엔드 앱의 백엔드로 활용
- 확장 아이디어 ④ 인증 필요한 사내 API 를 붙여 내부 대시보드 로 만들기