OpenAI API를 붙이려는데 fetch가 뭐냐고 물어봤다
Cursor로 챗봇을 만들었다. UI는 그럴듯하게 나왔는데, OpenAI API를 연결하는 코드에서 fetch라는 게 등장한다. AI가 만들어준 코드를 보면 이런 식이다.
const response = await fetch('https://api.openai.com/v1/chat/completions', {
method: 'POST',
headers: {
'Authorization': 'Bearer sk-...',
'Content-Type': 'application/json'
},
body: JSON.stringify({ model: 'gpt-4', messages: [...] })
});
method, headers, body, JSON.stringify. 한 줄도 이해가 안 된다. AI한테 "이거 뭐야?"라고 물어보면 "HTTP POST 요청을 보내는 코드입니다"라고 답하는데, HTTP가 뭔지도 모르겠으니 설명이 설명이 아니다.
걱정할 것 없다. 원리는 식당 주문서다.
HTTP 요청은 식당 주문서다
식당에 가면 주문서를 쓴다. 뭘 원하는지 적어서 주방에 넘기면, 주방에서 음식을 만들어 내준다. 웹에서 일어나는 일도 똑같다.
브라우저(손님)가 서버(주방)에 주문서(HTTP 요청)를 보낸다. 서버가 요청을 처리해서 응답(음식)을 돌려준다. 이게 HTTP의 전부다. HyperText Transfer Protocol이라는 정식 이름은 잊어도 된다. "주문하고 받는 규칙"이라고 생각하면 충분하다.
주문서에는 몇 가지 항목이 있다.
**주문 종류(method)**가 있다. "메뉴판 보여주세요"는 GET이다. 데이터를 달라는 요청. "이거 주문할게요"는 POST다. 데이터를 보내는 요청. 웹에서 일어나는 대부분의 일이 이 두 가지로 돌아간다.
**주문 내용(body)**이 있다. POST로 뭔가를 보낼 때 실제 데이터를 담는 곳이다. "GPT-4 모델로 이 질문에 답해줘"라는 내용을 여기에 넣는다.
**메모(headers)**가 있다. 주문서 귀퉁이에 적는 부가 정보다. "알레르기 있으니 땅콩 빼주세요" 같은 것. HTTP에서는 "내 인증 토큰은 이거야", "데이터 형식은 JSON이야" 같은 정보가 여기 들어간다.

응답에도 규칙이 있다: Status Code
주방에서 주문서를 받으면 결과를 알려준다.
200: "음식 나왔습니다." 요청이 정상적으로 처리됐다는 뜻이다. 가장 보고 싶은 숫자.
404: "그 메뉴는 없습니다." 요청한 주소(URL)가 잘못됐거나 존재하지 않는다. API URL에 오타가 있으면 이게 뜬다.
401: "회원카드를 안 가져오셨네요." 인증이 안 됐다는 뜻이다. API 키가 빠졌거나 만료됐을 때 나온다.
500: "주방에 불이 났습니다." 서버 쪽에서 뭔가 터졌다. 내 코드 문제가 아니라 상대편 문제일 수 있다. 이럴 때는 잠시 후 다시 시도해보면 된다.

이 네 가지만 알아도 에러의 80%는 감이 잡힌다. 브라우저 개발자 도구(F12)를 열어서 Network 탭을 보면 모든 요청과 응답의 status code가 보인다. 뭔가 안 될 때 여기부터 확인하는 습관을 들이면 디버깅 시간이 확 줄어든다.
JSON: 택배 상자 안 포장 방식
주방에 "GPT-4로 이 질문에 답해줘"라고 보내려면, 그 내용을 정해진 형식으로 포장해야 한다. 아무렇게나 적어서 보내면 주방에서 읽을 수가 없으니까.
그 포장 형식이 JSON(JavaScript Object Notation)이다. 택배를 보낼 때 뽁뽁이로 감싸고 상자에 넣는 것처럼, 데이터를 정해진 규칙에 맞춰 담는 방식이다.
{
"model": "gpt-4",
"messages": [
{ "role": "user", "content": "서버리스가 뭐야?" }
]
}
중괄호 안에 "키": "값" 형태로 데이터를 넣는다. 이름표를 붙인 서랍장이라고 봐도 된다. "model"이라는 서랍에는 "gpt-4"가 들어있고, "messages"라는 서랍에는 대화 목록이 들어있다.
JavaScript에서 JSON으로 변환할 때 JSON.stringify()를 쓰고, JSON을 다시 JavaScript 객체로 변환할 때 JSON.parse()를 쓴다. fetch의 응답을 처리할 때는 response.json()이 이걸 대신해준다.
fetch()가 하는 일
fetch는 JavaScript에서 HTTP 요청을 보내는 함수다. 주문서를 작성해서 주방에 넘기는 역할이다.
// 가장 단순한 GET 요청 (메뉴판 보여줘)
const response = await fetch('https://api.example.com/data');
const data = await response.json();
// POST 요청 (이거 주문할게)
const response = await fetch('https://api.openai.com/v1/chat/completions', {
method: 'POST',
headers: {
'Authorization': 'Bearer sk-...',
'Content-Type': 'application/json'
},
body: JSON.stringify({
model: 'gpt-4',
messages: [{ role: 'user', content: '안녕' }]
})
});
const result = await response.json();
GET은 URL만 넘기면 된다. 주소만 알려주면 알아서 가져온다. POST는 method, headers, body를 같이 보내야 한다. 뭘 보낼지, 누구한테 보내는지, 어떤 형식인지를 전부 적어줘야 하기 때문이다.
await가 붙어있는 이유가 있다. fetch는 주문서를 보내고 음식이 나올 때까지 기다려야 한다. 인터넷을 통해 서버에 갔다 돌아오는 시간이 필요하니까. await는 "응답 올 때까지 기다려"라는 뜻이다. 이걸 빼면 응답이 오기 전에 다음 코드가 실행돼서 에러가 난다.
REST API: 주문 규칙
식당마다 주문 방식이 다르면 혼란스럽다. 어떤 집은 벨을 누르고, 어떤 집은 키오스크를 쓰고, 어떤 집은 소리를 질러야 한다. REST API는 "주문은 이렇게 통일하자"는 약속이다.
핵심 규칙은 세 가지다.
URL로 대상을 정한다. /users면 사용자 목록, /users/123이면 123번 사용자. 뭘 다루는지가 URL에 드러난다.
method로 동작을 정한다. 같은 /users/123이라도 GET이면 조회, PUT이면 수정, DELETE면 삭제다. URL은 "누구"이고 method는 "뭘 할지"다.
JSON으로 데이터를 주고받는다. 요청도 JSON, 응답도 JSON. 형식이 통일돼 있으니 어떤 API든 구조가 비슷하다.
바이브 코더가 만나는 대부분의 외부 서비스(OpenAI, Supabase, Stripe 등)가 REST API 방식이다. URL과 method의 조합만 익혀두면 어떤 API 문서를 열어도 읽는 법이 비슷하다.
프론트엔드 vs 백엔드: 누가 뭘 하나
여기까지 읽으면 하나 궁금해진다. "그러면 fetch로 API를 호출하는 코드는 어디에 두는 거지?"
답은 "경우에 따라 다르다"인데, 한 가지 원칙이 있다. API 키는 브라우저에 노출하면 안 된다.
프론트엔드는 브라우저에서 돌아가는 코드다. 사용자 눈에 보이는 화면을 그리고, 버튼 클릭 같은 상호작용을 처리한다. 문제는 브라우저에서 돌아가는 코드는 누구나 볼 수 있다는 점이다. 개발자 도구를 열면 JavaScript 코드가 전부 보인다.
백엔드는 서버에서 돌아가는 코드다. 사용자가 볼 수 없다. API 키를 숨기고, 민감한 로직을 처리하고, DB에 접근하는 일은 여기서 한다.
그래서 OpenAI API처럼 유료 API를 호출할 때는 이런 구조가 된다.
사용자가 질문 입력
→ 프론트엔드가 내 백엔드 서버에 fetch 요청
→ 백엔드가 API 키를 붙여서 OpenAI에 요청
→ OpenAI 응답을 백엔드가 받아서 프론트엔드에 전달
→ 화면에 답변 표시
프론트엔드에서 OpenAI를 직접 호출하면? API 키가 브라우저에 노출된다. 누군가 그 키를 가져다 쓰면 요금이 내 카드로 청구된다. Next.js의 API Routes나 Cloudflare Workers 같은 서버리스 함수가 여기서 역할을 한다. 프론트엔드와 외부 API 사이에 백엔드를 끼워넣는 것이다.
바이브 코더가 API에서 자주 막히는 곳
API 키를 코드에 직접 써놓는다. Cursor가 생성한 코드에 'Bearer sk-abc123...' 같은 게 하드코딩돼 있으면 즉시 환경변수로 빼야 한다. .env 파일에 넣고 process.env.OPENAI_API_KEY로 읽는 방식이다. 이 파일은 .gitignore에 반드시 추가한다. GitHub에 API 키가 올라가면 봇이 몇 분 안에 긁어간다.
response.ok를 확인하지 않는다. fetch는 404나 500 에러가 나도 에러를 던지지 않는다. 네트워크 연결 자체가 실패해야 에러가 난다. 그래서 응답을 받은 뒤 if (!response.ok) 체크를 반드시 넣어야 한다. 안 넣으면 에러 메시지를 정상 데이터인 줄 알고 처리하다가 이상한 곳에서 터진다.
CORS 에러에 당황한다. 브라우저에서 다른 도메인의 API를 호출하면 빨간 에러가 뜬다. "CORS policy" 어쩌고 하는 그거. 이건 보안 장치다. 해결법은 간단하다. 프론트엔드에서 직접 호출하지 말고 백엔드를 경유하면 된다. CORS는 브라우저에만 있는 제한이라 서버에서 서버로 요청할 때는 발생하지 않는다.
async/await를 빼먹는다. fetch 앞에 await를 안 붙이면 응답이 도착하기 전에 다음 줄이 실행된다. console.log(data)를 찍었는데 Promise { <pending> }이 나온다면 십중팔구 이 문제다.
정리: 이 글에서 나온 개념 한 장 요약
| 개념 | 식당 비유 | 실제 역할 |
|---|---|---|
| HTTP | 주문 시스템 | 브라우저와 서버가 데이터를 주고받는 규칙 |
| GET | "메뉴판 보여주세요" | 데이터를 요청 |
| POST | "이거 주문할게요" | 데이터를 전송 |
| Status Code | 주문 결과 안내 | 요청 처리 결과 (200, 404, 500...) |
| JSON | 포장 형식 | 데이터를 담는 표준 형식 |
| fetch() | 주문서 접수 창구 | JavaScript에서 HTTP 요청을 보내는 함수 |
| REST API | 주문 규칙 통일 | URL + method + JSON으로 통신하는 약속 |
| 프론트엔드 | 홀(고객 공간) | 브라우저에서 돌아가는 코드 |
| 백엔드 | 주방(비공개 공간) | 서버에서 돌아가는 코드 |
처음이 가장 어렵다
API 호출이 처음이면 모든 게 낯설다. URL, method, headers, body, await. 한꺼번에 나오니 복잡해 보인다. 하지만 결국은 주문서를 쓰고 응답을 받는 것이 전부다.
공개 API 하나를 골라서 GET 요청부터 보내보자. 날씨 API나 JSON Placeholder 같은 무료 API가 연습하기 좋다. 브라우저 주소창에 https://jsonplaceholder.typicode.com/posts/1을 입력해보면 JSON 데이터가 바로 뜬다. 방금 GET 요청을 보낸 거다.
그 감각이 잡히면 Cursor나 Claude Code가 만들어준 fetch 코드가 읽히기 시작한다. "아, 이 URL로 POST를 보내고 JSON으로 데이터를 담는 거구나." 코드 전체를 외울 필요 없다. 구조만 알면 AI가 생성한 코드의 어디가 URL이고 어디가 API 키인지 구분할 수 있다. 그 정도면 충분하다.