본문으로 건너뛰기
취준생을 위한 n8n 실전 워크플로우 레시피 · 17/44시리즈 보기 →

호텔 가격 추이를 AI가 요약해주는 n8n 워크플로우 — OAuth 토큰이 필요한 API 다루기

· 약 13분
Datapopcorn CEO / AI automation educator
이 글과 연결된 글 보기
7편 · 20개 연결
100%
n8n Data Table 처음 써보기 — 트렌드 3개를 합쳐 매일 아침 분석 주제 뽑는 워크플로우호텔 가격 추이를 AI가 요약해주는 n8n 워크플로우 — OAuth 토큰이 필요한 API 다루기n8n으로 정적 웹서버 운영하기 — 수료증 발급부터 진위 확인까지, 멱등 처리로 안전하게벡터DB 없이 실시간으로 사이트를 읽는 챗봇 — 경복대학교 안내 챗봇 해부n8n으로 반복업무 자동화 포트폴리오 만들기구글시트에 쌓인 데이터를 인스타그램 카드뉴스로 — n8n 시트 인스타그램 자동발행서브워크플로우를 Tool로 감싸 AI Agent에게 서버 점검시키기 — 그리고 시스템 프롬프트만으로는 부족한 이유

드래그로 이동 · 휠/두 손가락으로 확대 · 방향키로 글 선택 · 0으로 맞춤. 실선: 본문 링크, 점선: 시리즈·주제의 대표 연결.

표시된 글 7편 목록
전체 글 연결 탐색

날씨 API처럼 키 하나를 URL에 붙이면 끝나는 API가 있고, 호텔·항공 같은 여행 API처럼 client_id와 client_secret으로 access token을 먼저 받아야 열리는 API가 있습니다. 이 글은 서울 호텔 5곳의 일주일치 가격을 API로 모아 구글시트에 쌓고 Gemini가 트렌드를 요약해 메일과 디스코드로 보내주는 워크플로우를 해부하면서, 토큰 발급 노드를 두고 뒤 노드에서 그 토큰을 참조하는 패턴을 익히는 편입니다. 이 패턴을 한 번 손에 익히면 "인증이 복잡해서 못 붙이는 API"가 사라집니다.

이 워크플로우 하나로 보여줄 수 있는 것​

날씨 알림 편과 경제지표 편에서 다룬 API는 발급받은 키를 URL 파라미터나 헤더에 한 번 넣으면 끝나는 종류였습니다. 이 편의 API는 한 단계가 더 있습니다. 키와 시크릿으로 토큰을 발급받고, 그 토큰을 매 요청의 Authorization 헤더에 실어 보내야 합니다. 이 한 단계를 워크플로우 안에서 처리해본 경험이 이 편의 핵심이고, 거기에 세 가지가 따라옵니다.

  • OAuth client credentials 처리 — 토큰 발급용 HTTP 노드를 따로 두고, 뒤 노드에서 $('노드 이름') 표현식으로 토큰을 꺼내 쓰는 구조를 설계한 경험
  • test·production 환경 구분 — 무료 test 환경과 과금되는 production 환경의 URL과 데이터 차이를 알고 개발한 경험
  • 여러 호텔 × 여러 날짜 조합을 만들어 API를 반복 호출하고, 결과를 하나로 모아 AI에게 요약을 시킨 경험
  • 핀 데이터(pinned data)로 개발 — API를 매번 부르지 않고 저장해 둔 응답으로 뒤 노드를 만드는 습관

이 워크플로우는 데이터팝콘 n8n 인스턴스에 2025년 12월에 교육용으로 만들어 둔 것입니다. 캔버스에 API 안내, 토큰 발급 curl, 각 엔드포인트의 파라미터 설명이 스티키 노트로 붙어 있어서 구조를 읽기 좋습니다. 설치나 기본 노드 설명은 n8n으로 반복업무 자동화 포트폴리오 만들기에 정리돼 있으니 여기서는 워크플로우 자체만 다룹니다.

배경 — 호텔 가격은 날짜마다 다르고, 비교는 손으로 하기 싫습니다​

같은 호텔이라도 체크인 날짜에 따라 가격이 꽤 다르고, 호텔 여러 곳을 날짜별로 비교하려면 예약 사이트를 수십 번 열어야 합니다. 이 워크플로우는 그 비교를 API로 대신하게 한 것입니다. 서울 시내 호텔 목록을 받아 앞의 5곳만 고르고, 일주일 뒤부터 7일간의 1박 가격을 호텔마다 조회해 시트에 쌓은 뒤, Gemini에게 "가격 트렌드, 가성비 호텔, 인사이트 3가지"를 500자 안에 요약하게 합니다.

데이터 출처는 여행 API를 제공하는 Amadeus for Developers의 Self-Service API입니다. 이 워크플로우를 만들던 시점에는 무료 test 환경에서 키를 발급받아 바로 쓸 수 있었습니다. 다만 지금은 사정이 달라졌습니다.

2026년 8월 확인 기준 — Amadeus Self-Service API는 종료됐습니다

Amadeus는 2026년 2월에 Self-Service 개발자 포털 종료를 공지했고, 신규 가입을 먼저 중단한 뒤 2026년 7월 17일자로 포털과 API 키를 모두 비활성화했습니다(PhocusWire 보도, 2026년 2월). 이 글을 쓰는 시점에 test.api.amadeus.com은 응답하지 않습니다. 그래서 이 편은 그대로 따라 만드는 레시피가 아니라 client credentials 방식 API를 n8n에서 다루는 패턴을 배우는 해부 글로 읽어주세요. 토큰 발급 노드, 토큰 참조 표현식, 환경 구분, 핀 데이터 개발은 같은 방식의 인증을 쓰는 다른 API에 그대로 옮겨집니다.

선행조건은 원래 Amadeus 무료 test 계정과 Gemini API 키 두 가지였습니다. 지금은 앞의 것을 만들 수 없으므로, 이 글의 구조를 여러분이 쓰려는 client credentials 방식 API로 바꿔 읽으면 됩니다. 토큰 발급 엔드포인트와 응답 필드 이름만 다를 뿐 뼈대는 같습니다.

구조 — Trigger / Task / Target​

  • Trigger — 수동 실행. 실습용이라 스케줄은 붙이지 않았습니다
  • Task — 토큰 발급 → 호텔 목록 → 5곳 × 7일 조합 생성 → 가격 조회 → 시트 적재 → 전체 합쳐 AI 요약
  • Target — Gmail 한 통 + Discord 웹훅 한 건

만들기​

1. Access_Token — API 키 하나로 안 되는 API의 첫 노드​

Amadeus는 OAuth 2.0의 client credentials 방식을 씁니다. 캔버스 스티키 노트에 만든 사람이 적어 둔 발급 명령이 이렇습니다.

curl -X POST "https://test.api.amadeus.com/v1/security/oauth2/token" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=client_credentials&client_id=YOUR_API_KEY&client_secret=YOUR_API_SECRET"

이걸 HTTP Request 노드로 옮긴 것이 Access_Token 노드입니다.

  • Method: POST
  • URL: https://test.api.amadeus.com/v1/security/oauth2/token
  • Body Content Type: Form Urlencoded
  • Body Parameters: grant_type = client_credentials, client_id = API Key, client_secret = API Secret

응답으로 access_token과 expires_in이 옵니다. 원본의 핀 데이터를 보면 expires_in이 1799, 즉 약 30분입니다. 당시 공식 가이드도 토큰 만료를 다루는 방법으로 "매 호출 전에 남은 시간을 확인"하거나 "unauthorized 에러를 잡아서 재발급"하는 두 가지를 안내했습니다(Amadeus 개발자 가이드 저장소, authorization). 이 워크플로우는 실행 한 번이 30분 안에 끝나기 때문에 실행 시작 시 토큰을 한 번 발급받고 끝까지 그 토큰을 씁니다. 스케줄로 오래 도는 흐름이라면 두 방법 중 하나를 워크플로우 안에 넣어야 합니다.

원본은 client_id와 client_secret을 Body Parameters에 직접 적어 두었습니다. 혼자 실습할 때는 되지만, 공유하거나 포트폴리오로 올릴 워크플로우라면 이 값은 노드 안에 남기면 안 됩니다. HTTP Request 노드의 Authentication을 Generic Credential Type → OAuth2 API로 두고 Grant Type을 Client Credentials로 고르면 n8n이 토큰 발급과 갱신을 대신해 주고, 키·시크릿은 credential에만 저장됩니다. 그래도 이 편에서는 수동 발급 노드를 먼저 만들어보길 권합니다. 토큰이 어디서 나와 어디로 가는지를 눈으로 본 사람과 credential이 알아서 해주는 것만 본 사람은 에러가 났을 때 대처가 다릅니다.

2. 호텔 목록 API — 토큰을 헤더에 실어 보내기​

Amadeus API 노드는 도시 코드로 호텔 목록을 받는 GET 요청입니다.

  • URL: https://test.api.amadeus.com/v1/reference-data/locations/hotels/by-city
  • Query Parameters: cityCode = SEL, radius = 5, radiusUnit = KM, hotelSource = ALL
  • Header Parameters: Authorization = Bearer {{ $json.access_token }}

cityCode는 IATA 도시 코드입니다. 스티키 노트에 "도시 코드를 넣으면 도시 중심 기준으로 검색하고, radius는 중심에서의 최대 거리, 단위는 KM 또는 MILE"이라고 파라미터 설명이 적혀 있는데, 공식 문서의 설명을 그대로 옮겨 둔 것입니다. 이 노드는 Access_Token 바로 다음에 연결돼 있어서 $json.access_token으로 앞 노드의 출력을 직접 받습니다.

토큰 참조 방식은 노드 위치에 따라 두 가지를 씁니다.

  • 토큰 노드 바로 다음 노드: Bearer {{ $json.access_token }}
  • 토큰 노드에서 여러 노드 떨어진 노드: Bearer {{ $('Access_Token').first().json.access_token }}

뒤에 나오는 가격 조회 노드는 중간에 Split Out, Limit, Code를 거친 뒤라 $json에는 이미 다른 데이터가 들어 있습니다. 그래서 노드 이름으로 거슬러 올라가 토큰을 꺼냅니다. .first()를 붙인 이유는 토큰 노드의 출력이 아이템 1개인데 가격 조회 노드는 아이템 35개를 처리하기 때문입니다. .item으로 짝을 맞추려 하면 아이템 수가 달라 꼬이니, 하나뿐인 토큰은 .first()로 고정해 두는 게 안전합니다.

3. test와 production — URL이 다르고 데이터가 다릅니다​

당시 문서 기준으로 Amadeus는 두 환경을 나눠 두었습니다. test.api.amadeus.com은 매달 무료 요청 할당량이 있고 실제 데이터의 일부를 캐시해 둔 환경이며, api.amadeus.com은 실시간 전체 데이터에 호출당 과금하는 환경입니다. 초당 요청 제한도 test 10회, production 40회로 달랐습니다(개발자 가이드 저장소, test data).

여기서 배울 점은 Amadeus에 한정되지 않습니다. 결제·여행·광고 API는 대부분 이런 구분이 있고, test 환경은 데이터가 적거나 오래됐으며 production으로 옮기는 순간 URL과 키가 바뀝니다. 워크플로우의 URL을 노드마다 하드코딩하면 환경을 바꿀 때 노드를 전부 열어야 하니, 실무용이라면 base URL을 Set 노드 하나에 두고 뒤에서 참조하는 편이 낫습니다. 이 원본은 실습용이라 세 노드 모두 test URL이 직접 박혀 있습니다.

4. Split Out → Limit → Code — 호텔 5곳 × 7일 조합 만들기​

호텔 목록 응답은 data 배열 안에 호텔이 수십 개 들어 있습니다. Split Out 노드로 data를 풀어 호텔 하나를 아이템 하나로 만들고, Limit 노드에서 Max Items를 5로 잘라 앞의 5곳만 남깁니다. 무료 할당량을 생각하면 처음부터 전부 조회할 이유가 없습니다.

그다음 Code 노드가 호텔마다 일주일 뒤부터 7일간의 체크인·체크아웃 날짜를 만듭니다. 캔버스 스티키에는 "7일간 날짜 배열 생성"이라고만 적혀 있고, 코드는 이렇습니다.

const allItems = $input.all();
const results = [];
const spreadsheetInfo = $('Create spreadsheet').first().json;

allItems.forEach((item, itemIndex) => {
const hotel = item.json;

// n주 후부터 n+1주 후까지 (7일간)
for (let i = 7; i < 14; i++) {
const today = new Date();
const checkIn = new Date(today);
checkIn.setDate(today.getDate() + i);

const checkOut = new Date(checkIn);
checkOut.setDate(checkIn.getDate() + 1);

results.push({
json: {
hotelId: hotel.hotelId,
hotelName: hotel.name,
chainCode: hotel.chainCode || '',
checkInDate: checkIn.toISOString().split('T')[0],
checkOutDate: checkOut.toISOString().split('T')[0],
dayOffset: i - 14,
spreadsheetId: spreadsheetInfo.spreadsheetId,
sheetTitle: spreadsheetInfo.sheets[0].properties.title
},
pairedItem: { item: itemIndex }
});
}
});

return results;

호텔 5개가 들어오면 35개 아이템이 나갑니다. 즉 다음 노드는 API를 35번 호출합니다. pairedItem을 붙여 두면 뒤 노드에서 어느 호텔에서 나온 아이템인지 n8n이 추적할 수 있습니다. 호텔 수나 날짜 범위를 늘리기 전에 할당량을 먼저 계산해보는 습관이 여기서 생깁니다.

5. hotel-offers API — 가격 조회와 "없는 방은 조용히 버리기"​

Amadeus hotel-offers API 노드는 호텔 ID와 날짜로 1박 가격을 조회합니다.

  • URL: https://test.api.amadeus.com/v3/shopping/hotel-offers
  • Query Parameters: hotelIds = {{ $json.hotelId }}, adults = 2, checkInDate = {{ $json.checkInDate }}, checkOutDate = {{ $json.checkOutDate }}
  • Header Parameters: Authorization = Bearer {{ $('Access_Token').first().json.access_token }}

이 노드에는 스티키 노트에 "Setting"이라는 제목으로 설정 메모가 붙어 있습니다. "Always output data = on, On Error = Continue. 호텔 중 유효하지 않은 호텔 코드가 있거나 예약 가능한 방이 없는 경우를 제외하고 유효한 호텔 데이터만 사용"이라는 내용입니다. 노드 설정을 열어보면 실제로 On Error가 "Continue (using error output)"으로, Always Output Data가 켜져 있습니다. 35번 호출 중 몇 번은 그 날짜에 방이 없거나 test 환경에 없는 호텔이라 에러가 나는데, 이 설정 덕분에 워크플로우가 거기서 멈추지 않고 성공한 호출만 다음 노드로 넘어갑니다. 에러 출력 쪽에는 아무 노드도 연결하지 않아서 실패한 조합은 그냥 사라집니다.

응답에서 필요한 값만 Edit Fields(Set) 노드로 추립니다. 호텔 ID, 호텔 이름, 체크인 날짜, 체크아웃 날짜, 가격, 객실 카테고리, 인원 일곱 개이고, 값은 {{ $json.data[0].offers[0].price.total }}처럼 응답의 첫 오퍼에서 꺼냅니다. 응답 한 건에 오퍼가 여러 개 올 수 있지만 트렌드를 보는 데는 첫 오퍼의 총액이면 충분합니다.

6. 시트 적재 → Aggregate → AI Agent — 35행을 요약 한 통으로​

Google Sheets 노드(Append or Update)가 한 행씩 시트에 씁니다. 시트는 워크플로우 맨 앞의 Create spreadsheet 노드가 "Hotel List"라는 이름으로 새로 만든 것이고, 뒤 노드는 {{ $('Create spreadsheet').item.json.spreadsheetId }}로 그 ID를 참조합니다.

시트에 쓴 뒤 Aggregate 노드(All Item Data)로 35개 아이템을 아이템 하나의 data 배열로 합칩니다. AI Agent에게 호텔 하나씩 35번 물어보는 게 아니라 전체를 한 번에 주고 비교를 시키기 위해서입니다. AI Agent 노드의 프롬프트는 원본 그대로 이렇습니다.

다음은 호텔 가격 정보입니다:

{{ JSON.stringify($json.data, null, 2) }}

분석 요청:
1. 가격 트렌드 분석
2. 가장 가성비 좋은 호텔 추천
3. 주요 인사이트 3가지

마크다운 형식으로 500자 이내로 간결하게 답변해주세요.

Chat Model에는 Google Gemini가 연결돼 있습니다. JSON.stringify(..., null, 2)로 배열을 들여쓰기된 문자열로 바꿔 넣는 것이 요령인데, 객체를 그냥 표현식에 넣으면 [object Object]로 들어가 모델이 아무것도 못 읽습니다. 결과 {{ $json.output }}은 Gmail 노드(제목 "호텔 가격 트렌트 분석")와 Discord 노드(웹훅)로 동시에 나갑니다. AI Agent의 출력이 두 노드에 병렬로 연결돼 있어서 하나가 실패해도 다른 쪽은 갑니다.

원본 캔버스에 남아 있는 흔적 — 핀 데이터로 개발한 자국​

원본 캔버스에는 본 흐름과 연결되지 않은 HTTP Request 노드가 세 개 떠 있습니다. 토큰 발급, 도시별 호텔 목록, 그리고 본 흐름에는 끝내 들어가지 않은 호텔 리뷰 점수(hotel-sentiments) API입니다. 세 노드 모두 출력이 핀(pin)돼 있습니다. 즉 한 번 실제로 호출해 받은 응답을 노드에 고정해 두고, 그 뒤로는 API를 부르지 않고 저장된 응답으로 다음 노드를 만들었다는 뜻입니다.

이게 핀 데이터를 쓰는 이유입니다. 뒤 노드의 표현식 하나 고칠 때마다 토큰을 새로 받고 API를 다시 부르면 할당량이 줄고 속도도 느립니다. 응답 구조가 확정된 노드는 핀해 두고, 뒤 노드가 완성되면 핀을 풀고 실제로 한 번 돌려보는 순서가 효율적입니다. 리뷰 API 노드가 핀만 된 채 남아 있는 것도, 시도는 했지만 이번 워크플로우에는 넣지 않기로 한 흔적으로 읽힙니다.

다만 핀 데이터에는 주의할 점이 하나 있습니다. 핀 데이터는 워크플로우를 export한 JSON 파일에 같이 저장됩니다. 토큰 발급 노드를 핀해 두면 access_token이, 계정 정보가 응답에 섞인 API라면 그 정보가 파일에 그대로 남습니다. 워크플로우를 깃허브에 올리거나 포트폴리오로 공유하기 전에는 인증 관련 노드의 핀을 풀고 export하세요. 이 원본도 그 점검이 필요한 상태였습니다.

캔버스 한쪽에는 Loop Over Items와 Replace Me 노드가 연결되지 않은 채 남아 있습니다. 처음엔 호텔마다 루프를 돌리려다 Code 노드에서 조합을 한 번에 만드는 쪽으로 바꾼 흔적입니다. 35개 아이템을 HTTP 노드에 그대로 넣으면 n8n이 알아서 아이템마다 호출하므로, 이 규모에서는 루프 노드가 없어도 됩니다.

한계​

  • API 자체가 종료됨 — 위에서 말한 대로 2026년 7월 이후 Amadeus Self-Service는 쓸 수 없습니다. 패턴을 옮길 대상 API를 따로 정해야 합니다.
  • 실행마다 새 스프레드시트 — Create spreadsheet가 맨 앞에 있어서 돌릴 때마다 "Hotel List" 파일이 하나씩 늘어납니다. 시트를 한 번 만든 뒤에는 이 노드를 빼고 기존 시트 ID를 참조하게 바꿔야 추이가 한 파일에 쌓입니다.
  • 토큰 재발급 없음 — 실행 시작 시 한 번만 발급합니다. 호출 수를 늘려 실행이 30분을 넘기면 뒤쪽 호출부터 인증 에러가 납니다.
  • 실패한 조합이 보이지 않음 — On Error Continue로 에러를 넘기지만 에러 출력에 아무것도 붙이지 않아, 35개 중 몇 개가 실패했는지 알 수 없습니다. 에러 출력에 Set 노드 하나를 붙여 실패 건수를 세는 것만으로도 리포트의 신뢰도가 올라갑니다.
  • 첫 오퍼만 사용 — 응답의 offers[0]만 쓰기 때문에 같은 호텔의 다른 객실 타입은 비교에서 빠집니다.

포트폴리오·면접 포인트​

이 워크플로우의 포트폴리오 가치는 호텔이 아니라 인증 패턴에 있습니다. "토큰 발급 노드 → 뒤 노드에서 $('노드').first().json.access_token 참조 → 만료 시간 확인" 구조를 다른 client credentials API로 다시 만들어 보고, 캔버스 스크린샷과 토큰 노드의 출력(토큰 값은 가린 채) 캡처를 함께 올리면 됩니다. 거기에 실행마다 새 시트가 생기는 문제나 실패 건수가 안 보이는 문제를 고친 버전을 붙이면, 따라 만든 사람이 아니라 운영을 생각한 사람으로 보입니다.

면접에서는 "API 키를 노드에 직접 넣으면 왜 안 되나요?"나 "토큰이 30분 뒤 만료되면 어떻게 하나요?" 같은 질문이 올 수 있습니다. 앞의 질문에는 credential과 export 파일의 관계를, 뒤의 질문에는 실행 길이가 만료보다 짧으면 실행당 1회 발급으로 충분하고 길어지면 남은 시간 확인 또는 401 재발급 중 하나를 넣는다고 답하면 됩니다. 핀 데이터가 export에 저장된다는 사실까지 말할 수 있으면 보안을 신경 쓰는 사람이라는 인상을 줍니다.

다음 액션​

인증 방식은 다뤘으니, 다음에는 API 응답을 오래 쌓는 쪽으로 넘어갑니다. 농지 데이터를 스케줄로 수집해 시트에 계속 쌓아 가는 편인데, 이 편의 "실행마다 새 시트" 문제를 처음부터 피한 구조라 두 편을 나란히 놓고 보면 시트를 어떻게 잡아야 추이 데이터가 되는지 분명해집니다.