Unexpected token < in JSON — HTML 응답을 JSON으로 파싱할 때 원인·해결
fetch로 API를 부른 뒤 "SyntaxError: Unexpected token < in JSON at position 0"이 뜨면, JSON을 기대한 자리에 HTML이 왔다는 뜻이다. position 0, 즉 맨 첫 글자가 <라는 건 응답이 <!DOCTYPE html>이나 <html>로 시작하는 HTML 문서라는 신호다. JSON은 절대 <로 시작하지 않으니, 파서가 첫 글자에서 바로 막힌다. 프런트엔드에서 가장 자주 만나는 fetch 실수 중 하나다. 서버가 JSON 대신 오류 페이지·로그인 페이지·index.html을 돌려줬거나, 요청 URL이 잘못돼 엉뚱한 HTML을 받은 경우다. 참고로 이 문구는 엔진마다 조금씩 다르다. 최신 크롬·Node(V8)는 "Unexpected token '<', "<!DOCTYPE "... is not valid JSON"처럼 문제 부분을 함께 보여주고, 파이어폭스·사파리는 또 다른 표현을 쓰지만, "JSON 자리에 잘못된 문자가 왔다"는 의미는 같다. 이 글은 이 에러의 의미, 왜 HTML이 오는지, 그리고 URL과 Content-Type을 확인하는 해결법을 정리한다.

이 글의 구성
🔎 에러 메시지의 의미
이 에러는 JSON.parse가 던지는 SyntaxError다. fetch에서 await res.json()을 부르면 내부적으로 응답 본문을 JSON.parse하는데, 그 본문이 JSON이 아니라 HTML이면 첫 글자 <에서 막힌다. JSON 문법에서 값은 중괄호·대괄호·따옴표·숫자 등으로 시작하지 <로 시작하는 법이 없기 때문이다. position 0은 "맨 첫 글자에서 이미 틀렸다"는 뜻이라, 사실상 응답 전체가 JSON이 아니라는 신호다.
핵심은 "무엇이 왔는지"다. 코드가 잘못됐다기보다, 받은 데이터가 기대와 다른 것이다. 그래서 이 에러를 만나면 코드를 노려보기 전에 "실제로 서버가 무엇을 돌려줬는가"부터 봐야 한다. 응답 원문을 확인하면 대개 <!DOCTYPE html>로 시작하는 오류 페이지나 로그인 화면이 그대로 보인다.
핵심 — JSON 자리에 HTML이 왔다
position 0의 <는 응답이 HTML 문서로 시작한다는 확실한 단서다. 파싱 코드가 문제가 아니라, 받은 응답 자체가 JSON이 아니다. 그래서 해결의 출발점은 "이 요청이 실제로 어디로 가서 무엇을 받았는가"를 확인하는 것이다. res.json() 대신 res.text()로 원문을 먼저 찍어 보면 정체가 드러난다.
🧩 HTML이 오는 대표 원인 4가지
원인 1 — 잘못된 URL·상대경로
가장 흔하다. API 주소를 틀리게 적거나, 상대경로가 엉뚱하게 해석돼 API가 아니라 페이지(index.html)로 요청이 간 경우다. 존재하지 않는 경로라 서버가 HTML 404 페이지를 돌려준다.
원인 2 — SPA 서버의 index.html 폴백
싱글 페이지 앱 서버는 모르는 경로 요청에 index.html을 돌려주도록 설정된 경우가 많다. 그래서 잘못된 API 경로를 부르면 JSON 대신 앱의 HTML이 오고, 그걸 파싱하다 이 에러가 난다.
원인 3 — 인증 만료로 로그인 페이지 반환
세션이 만료됐거나 로그인이 필요한 상태에서, 서버가 JSON 에러 대신 로그인 페이지(HTML)로 리다이렉트하는 경우다. API를 불렀는데 로그인 화면 HTML이 돌아와 파싱에서 막힌다.
원인 4 — 개발 프록시 미설정·서버 오류 페이지
개발 환경에서 API 프록시를 설정하지 않아 요청이 프런트 개발 서버로 가 HTML을 받거나, 서버가 500 오류를 HTML 페이지로 돌려주는 경우다. 어느 쪽이든 응답이 JSON이 아니다.
💻 재현과 원문 확인
이 에러의 진단은 원문 확인이 8할이다. res.json() 대신 res.text()로 먼저 받아 응답이 무엇인지 눈으로 본다.
// JSON을 기대했지만 HTML이 옴
const res = await fetch("/api/user");
const data = await res.json();
// ✗ SyntaxError: Unexpected token '<' ... is not valid JSON
// ✓ 원문 확인 — HTML인지 눈으로
const text = await res.text();
console.log(text.slice(0, 100)); // "<!DOCTYPE html>..." 면 HTML
브라우저 개발자도구의 네트워크 탭도 강력하다. 문제의 요청을 클릭해 실제 요청 URL이 의도한 API가 맞는지, 응답의 Content-Type이 application/json인지 text/html인지 확인한다. Content-Type이 text/html이면 서버가 HTML을 보낸 것이 확실하다.
# 네트워크 탭에서 확인할 것
# - Request URL : 의도한 API 주소가 맞는가
# - Status : 404·500·302(리다이렉트)인가
# - Content-Type: application/json 인가 text/html 인가
🛠 해결 방법과 방어
근본 해결은 "왜 HTML이 오는가"를 고치는 것이고, 방어는 "파싱 전에 JSON인지 확인"하는 것이다.
| 원인 | 해결 |
|---|---|
| 잘못된 URL | 요청 URL·상대경로·API 베이스 주소를 실제 경로와 대조 |
| SPA 폴백 | API 경로가 index.html로 빠지지 않게 라우팅·프록시 정리 |
| 인증 만료 | 401을 JSON으로 응답하도록, 클라이언트는 로그인 유도 처리 |
| 개발 프록시 | 개발 서버 프록시 설정으로 API 요청을 백엔드로 전달 |
| 방어 코드 | res.ok·Content-Type 확인 후 파싱, 실패 시 원문 로깅 |
방어 코드의 핵심은 파싱 전에 "이게 JSON이 맞는가"를 확인하는 것이다. 상태 코드와 Content-Type을 보고, JSON이 아닐 때는 파싱을 시도하지 않고 원문을 남겨 원인 파악에 쓴다.
// ✓ 파싱 전에 JSON 여부 확인
const res = await fetch("/api/user");
const type = res.headers.get("content-type") || "";
if (!res.ok || !type.includes("application/json")) {
const text = await res.text();
throw new Error(`JSON 아님(${res.status}): ${text.slice(0, 80)}`);
}
const data = await res.json();
⚖️ end of input과의 차이
같은 JSON.parse의 SyntaxError지만, "Unexpected token <"와 "Unexpected end of JSON input"은 원인이 반대다. 헷갈리면 진단이 엉킨다.
| 메시지 | 무엇이 왔나 | 방향 |
|---|---|---|
| Unexpected token < | JSON 아닌 HTML이 옴 | 내용은 있는데 형식이 틀림 |
| end of JSON input | 빈·잘린 응답 | 내용이 부족(끝이 빨리 옴) |
정리하면, "Unexpected token <"는 응답이 HTML이라 첫 글자부터 형식이 틀린 것이고, "end of JSON input"은 응답이 비었거나 잘려서 내용이 부족한 것이다. 전자는 "무엇이 왔는지(URL·Content-Type)", 후자는 "내용이 있긴 한지(빈 응답)"부터 본다. 두 에러 모두 JSON.parse 단계에서 나지만, 손볼 곳이 다르다.
position 0의 <는 "JSON 자리에 HTML이 왔다"는 확실한 신호다. 코드가 아니라 응답부터 res.text()로 확인하라.
— URL과 Content-Type이 답을 준다
💬 자주 묻는 질문 5가지
Q1코드는 그대로인데 갑자기 나기 시작했어요.
응답이 바뀐 경우가 많습니다. 세션이 만료돼 서버가 로그인 페이지(HTML)로 리다이렉트하거나, 배포로 API 경로·프록시가 바뀌어 HTML이 오게 된 것이죠. res.text()로 원문을 찍고 네트워크 탭에서 URL·Content-Type을 확인하세요.
Q2왜 첫 글자가 < 인가요?
HTML 문서가 <!DOCTYPE html>이나 <html>로 시작하기 때문입니다. JSON은 <로 시작하는 법이 없어, 파서가 맨 첫 글자(position 0)에서 바로 막힙니다. 즉 응답 전체가 JSON이 아니라 HTML이라는 신호입니다.
Q3개발 환경에서만 나요.
개발 서버 프록시가 설정 안 된 경우가 흔합니다. API 요청이 백엔드가 아니라 프런트 개발 서버로 가서 index.html을 받는 것이죠. 개발 서버의 프록시 설정으로 API 요청을 백엔드 주소로 전달하면 해결됩니다.
Q4try-catch로 감싸면 되나요?
에러로 앱이 죽는 건 막지만 근본 해결은 아닙니다. HTML이 오는 원인(잘못된 URL·프록시·인증)이 그대로면 데이터를 못 받는 건 여전하죠. try-catch로 안전망을 두되, 파싱 전에 Content-Type을 확인하고 원문을 로깅해 왜 HTML이 오는지 고치세요.
Q5"end of JSON input"과는 다른 에러인가요?
둘 다 JSON.parse의 SyntaxError지만 원인이 반대입니다. "Unexpected token <"는 HTML이 와서 형식이 틀린 것이고, "end of JSON input"은 응답이 비거나 잘려 내용이 부족한 것입니다. 전자는 URL·Content-Type을, 후자는 빈 응답 여부를 확인하세요.
📌 결론
"Unexpected token < in JSON at position 0"은 JSON을 기대한 자리에 HTML이 왔을 때 나는 SyntaxError다. 맨 첫 글자 <가 응답이 HTML 문서라는 확실한 단서다. 코드가 아니라 받은 응답이 문제이므로, "무엇이 왔는지"부터 확인해야 한다.
실무 원칙은 다음과 같다.
🔤 <는 HTML 신호다. position 0의 <는 응답이 JSON이 아니라 HTML(오류·로그인·index.html)이라는 뜻이다. res.text()로 원문부터 본다.
🔗 URL·엔드포인트를 바로잡는다. 잘못된 주소·상대경로·SPA 폴백·프록시 미설정이 단골 원인이다. 네트워크 탭에서 실제 요청 URL을 확인한다.
🧾 Content-Type을 확인하고 방어한다. 파싱 전에 res.ok와 application/json 여부를 보고, 아니면 파싱하지 말고 원문을 로깅한다.
Unexpected token < 체크리스트
본 글은 자바스크립트 JSON.parse SyntaxError(HTML 응답)의 일반적 원인과 해결 방법을 정리한 자료다. 코드 수정은 영향 범위를 확인한 뒤 신중히 적용한다.
#JavaScript #JSON #JSONparse #Unexpectedtoken #자바스크립트에러 #fetch #resjson #HTML응답 #ContentType #SPA #개발프록시 #SyntaxError #프론트엔드 #JS디버깅 #웹개발
'IT' 카테고리의 다른 글
| Cannot access 'x' before initialization — TDZ 원인·해결·예방 (0) | 2026.07.02 |
|---|---|
| x is not iterable — 원인·해결·예방 (0) | 2026.07.01 |
| Assignment to constant variable — const 재할당 원인·해결·예방 (0) | 2026.07.01 |
| Unexpected end of JSON input — JSON.parse 원인·해결·예방 (1) | 2026.07.01 |
| Cannot set properties of undefined (setting 'x') — 원인·해결·예방 (0) | 2026.07.01 |
댓글