SyntaxError: Unexpected token '<', "<!DOCTYPE "... is not valid JSON —— 거의 항상 한 가지를 의미합니다: 코드가 JSON이 아니라 HTML 페이지 에 대해 JSON.parse()(또는 res.json())을 호출했다는 뜻. 위치 0의 < 는 <!DOCTYPE html> 또는 <html> 의 시작입니다. 정확히 왜 발생하고 어떻게 고치는지 설명합니다.
에러의 모습
// V8 (Chrome / Node / Edge)
SyntaxError: Unexpected token '<', "<!DOCTYPE "... is not valid JSON
// 더 오래된 V8
SyntaxError: Unexpected token < in JSON at position 0
// Firefox
SyntaxError: JSON.parse: unexpected character at line 1 column 1 of the JSON data
// Safari
SyntaxError: JSON Parse error: Unexpected identifier "<"위치 0 이 핵심 단서: 첫 바이트부터 이미 틀렸음 —— 응답은 처음부터 JSON이 아니었습니다.
왜 발생하는가: HTML을 받았다
fetch 나 AJAX 호출이 기대한 JSON 대신 HTML 문서를 돌려줬습니다. 흔한 원인:
- 프레임워크나 웹 서버가 HTML로 제공하는 404 / 500 에러 페이 지
- HTML 로그인 페이지로 가는 로그인/인증 리다이렉트(세션 만료)
- 잘못된 URL —— API 엔드포인트 대신 app/SPA 라우트에 도달
- 프록시, CDN, 캡티브 포털 이 자체 HTML 페이지를 반환
깨진 예
const res = await fetch('/api/user'); // 서버가 404 HTML 페이지를 돌려줌
const data = await res.json(); // ❌ Unexpected token '<' …
// res.json() 이 "<!DOCTYPE html>…" 을 JSON으로 파싱하려 함수정된 예
const res = await fetch('/api/user');
// 1) 파싱 전에 상태 확인
if (!res.ok) {
throw new Error(`HTTP ${res.status} ${res.statusText}`);
}
// 2) 선택: 정말 JSON인지 확인
const type = res.headers.get('content-type') ?? '';
if (!type.includes('application/json')) {
const text = await res.text();
throw new Error(`Expected JSON, got: ${text.slice(0, 80)}…`);
}
const data = await res.json(); // ✅ 안전알아 둘 만한, 덜 분명한 두 가지 원인
- WAF 차단/요율 제한 응답. WAF, CDN 또는 DDoS 보호 계층이 호출을 가로채 자체 HTML 챌린지/차단 페이지를 돌려줄 수 있습니다 —— Cloudflare 인터스티셜, AWS WAF 거부 페이지, 공공 Wi-Fi의 캡티브 포털. 상태는 종종
403/429/503에 HTML 본문. Network 탭에서 응답을 확인하고CF-Ray나X-Amz-Cf-Id같은 헤더로 식별하세요. - SPA 히스토리 폴백이
index.html을 돌려줌. 정적 호스트와 개발 서버는 클라이언트 라우터가 처리하도록 알 수 없는 경로를 모두/index.html로 재작성하는 경우가 흔합니다. 프런트엔드가 실수로/api/user를 상대 경로로 fetch하고 프록시가 설정되지 않았다면, 서버는 SPA의 HTML 셸을200으로 조용히 돌려줍니다 —— '200 OK인데 HTML' 인 혼란스러운 버전의 같은 에러가 됩니다.
고치는 단계
- 원시 본문 로깅:
console.log(await res.clone().text()).<!DOCTYPE나<html>로 시작하면 HTML. res.json()호출 전에res.ok/res.status확인 —— 4xx/5xx 는 보통 HTML 에러 페이지를 돌려줍니다.- Network 탭에서 URL 확인. 상대 경로가 API가 아니라 SPA의 index.html 로 풀릴 수 있음.
- 인증 확인 —— 로그인 페이지로의 리다이렉트는 토큰/세션 만료를 의미.
Content-Type검사 —— 이에 따라 분기해 비-JSON 응답을 맹목적으로 파싱하지 마세요.
자주 묻는 질문
'Unexpected token < in JSON at position 0' 이 무슨 뜻?
파서가 < 를 첫 문자로 발견. 이는 어떤 합법한 JSON에서도 불가능. 응답은 코드가 기대한 JSON이 아니라 HTML(<!DOCTYPE> 또는 <html> 페이지)이었습니다.
왜 내 fetch 가 JSON 대신 HTML 을 돌려주나요?
흔한 원인: 404/500 에러 페이지, 로그인 페이지로의 인증 리다이렉트, 또는 API가 아니라 프런트엔드로 풀리는 잘못된 URL. res.ok 를 확인하고 await res.text() 를 로그해 무엇이 돌아왔는지 보세요.
내 앱이 죽지 않게 하려면?
파싱 전에 가드: res.ok 와 Content-Type 헤더를 검사하고 JSON.parse()/res.json() 을 try/catch 로 감싸 HTML 응답을 예외가 아닌 처리 가능한 에러로 만드세요.
지금 고치기
확신이 없는 응답 본문이 있다면 JSON Fix 에 붙여 넣으세요 —— 그게 유효한 JSON인지 다른 것(예: HTML)인지 즉시 알려 줍니다. 모든 처리는 브라우저에서.
- JSON Fix —— 브라우저에서 JSON 검증·수리
- JSON.parse 의 Unexpected Token 에러 해결법 —— 모든 token 변형
- Unexpected token u in JSON at position 0 ——
undefined형제 에러 - '[object Object] is not valid JSON' 해결 —— 종합 문법 오류 레퍼런스