빨간 스택 트레이스를 보고 있습니다: SyntaxError: Unexpected token '<', "..." is not valid JSON. 아니면 Unexpected token u in JSON at position 0. 어느 쪽이든 JSON.parse() 가 실행을 거부하고 코드가 멈췄습니다. 이 가이드는 이 오류의 각 변형이 정확히 무엇을 의미하는지, 왜 발생하는지, 그리고 빠르게 고치는 법을 설명합니다.
오류의 실제 모습
표현은 JavaScript 엔진마다 다르지만 모두 같은 것을 의미합니다: 파서가 문자열의 특정 위치에서 예상하지 못한 문자를 만났다는 것.
// V8 (Chrome, Node.js, Edge)
SyntaxError: Unexpected token '<', "<html>..." is not valid JSON
SyntaxError: Unexpected token 'u', "undefined" is not valid JSON
SyntaxError: Unexpected token u in JSON at position 0 // 구버전 V8
SyntaxError: Expected ',' or '}' after property value in JSON at position 42
// Firefox (SpiderMonkey)
SyntaxError: JSON.parse: unexpected character at line 1 column 1 of the JSON data
SyntaxError: JSON.parse: unexpected non-whitespace character after JSON data
// Safari (JavaScriptCore)
SyntaxError: JSON Parse error: Unexpected identifier "undefined"
SyntaxError: JSON Parse error: Single quotes (') are not allowed in JSON핵심 정보는 항상 두 부분: 어떤 문자가 예상치 못했나 와 어디서. 문자는 파서가 실제로 받은 것을 알려 주고, 위치는 문자열의 어디를 보라고 알려 줍니다.
원인 1: 서버가 JSON 이 아니라 HTML 을 반환
Unexpected token '<' 의 가장 흔한 원인. fetch 나 AJAX 호출이 기대한 JSON 대신 HTML 페이지 —— 보통 404 페이지, 로그인 리다이렉트, 서버 오류 —— 를 받아왔습니다. < 는 <!DOCTYPE html> 나 <html> 의 여는 태그.
// ❌ 이건 "Unexpected token '<'" 로 죽습니다
const res = await fetch('/api/user');
const data = await res.json(); // 서버가 HTML 404 를 반환
// ✅ 응답 상태를 먼저 확인
const res = await fetch('/api/user');
if (!res.ok) {
throw new Error(`HTTP ${res.status}: ${res.statusText}`);
}
const data = await res.json(); res.json() 를 호출하기 전에 항상 res.ok (또는 res.status) 를 확인하세요. 원시 응답 텍스트를 먼저 들여다볼 수도 있습니다:
const text = await res.text();
console.log(text.slice(0, 200)); // 실제로 무엇을 받았는지 확인
const data = JSON.parse(text);원인 2: 값이 undefined
Unexpected token 'u' 가 위치 0 에 있는 건 거의 항상 JSON.parse(undefined) 를 호출했다는 뜻. 문자열 "undefined" 는 문자 u 로 시작하고, u 는 유효한 JSON 시작 문자가 아닙니다.
// ❌ 변수가 설정된 적 없음
const raw = localStorage.getItem('settings'); // 누락이면 null
JSON.parse(raw); // null 은 괜찮지만, raw 가 어쩌다 undefined 면…
// ❌ async 데이터가 아직 로드 되지 않음
JSON.parse(this.state.data); // 첫 렌더 때 data 는 undefined
// ✅ 파싱 전 가드
if (raw) {
const data = JSON.parse(raw);
} localStorage.getItem() 은 누락 키에 null (undefined 아님)을 반환하고, JSON.parse(null) 은 오류 없이 null 을 반환합니다. 흔한 범인은 초기화되지 않은 state 변수나 누락된 함수 인수.
원인 3: 이중 따옴표 대신 단일 따옴표
// ❌ SyntaxError: Unexpected token '''
JSON.parse("{'name': 'Ada'}");
// ✅ JSON 은 이중 따옴표가 필수
JSON.parse('{"name": "Ada"}')JavaScript 객체 리터럴은 단일 따옴표를 허용합니다. JSON 은 안 됩니다 —— 키를 포함한 모든 문자열은 이중 따옴표여야 합니다. 누가 Python 의 str() 함수를 json.dumps() 대신 써서 직렬화했거나, 객체 리터럴을 그대로 복사한 경우에 자주 발생합니다.
원인 4: 후행 쉼표
// ❌ SyntaxError: Unexpected token '}'
JSON.parse('{"name": "Ada", "score": 98,}');
// ^ 후행 쉼표
// ✅
JSON.parse('{"name": "Ada", "score": 98}')현대 JavaScript 는 배열과 객체에 후행 쉼표를 허용합니다. JSON 은 안 됩니다. 손으로 편집한 JSON 설정 파일이 이 오류의 가장 흔한 원천.
원인 5: 따옴표 없는 키
// ❌ SyntaxError: Unexpected token 'n'
JSON.parse("{name: 'Ada'}");
// ✅
JSON.parse('{"name": "Ada"}') JSON 의 객체 키는 항상 이중 따옴표 문자열이어야 합니다. name 같은 맨 식별자는 JavaScript 객체 리터럴에선 유효하지만 JSON 에선 아닙니다.
원인 6: JSON 안의 주석
// ❌ SyntaxError: Unexpected token '/'
JSON.parse(`{
// 사용자 레코드
"name": "Ada"
}`);
// ✅ 파싱 전 주석 제거 (또는 JSONC 파서 사용)
const stripped = raw.replace(///.*$/gm, '').replace(//*[sS]*?*//g, '');
JSON.parse(stripped);JSON 에는 주석 문법이 없습니다. 설정 파일(JSONC 포맷)에 주석이 자주 들어가지만 표준 JSON.parse() 는 즉시 거부합니다.
원인 7: 위치 0 의 BOM 또는 보이지 않는 문자
오류가 위치 0 인데 첫 보이는 문자가 문제없어 보이면, 문자열 앞에 보이지 않는 byte-order mark(BOM, U+FEFF) 나 다른 제어 문자가 붙어 있을 수 있습니다 —— 보통 Windows 에서 UTF-8-BOM 인코딩으로 저장된 파일을 읽을 때.
// ✅ 파싱 전 BOM 제거
const clean = raw.replace(/^/, '');
JSON.parse(clean);원인 8: 토큰 I 또는 N —— Infinity 와 NaN
오류가 Unexpected token I 또는 Unexpected token N 이라면 소스가 Infinity, -Infinity, NaN 을 내놓은 것 —— 모두 JavaScript 에선 유효하지만 JSON 에선 안 됩니다.
// ❌ 프로듀서 측 실수
const json = '{"ratio": NaN, "limit": Infinity}';
JSON.parse(json); // Unexpected token N (or I) ...
// ✅ 의도적으로 인코딩
const safe = JSON.stringify({ ratio: NaN, limit: Infinity });
// → '{"ratio":null,"limit":null}' (참고: 손실 —— 둘 다 null 이 됨)
// ✅ 무손실: 센티널 문자열로 인코딩 후 직접 다시 파싱
JSON.stringify({ ratio: 'NaN', limit: 'Infinity' });JSON.stringify 는 조용히 NaN / Infinity 를 null 로 변환합니다. 왕복이 필요하면 나갈 때 센티널 문자열로 바꾸고 들어올 때 다시 파싱하세요.
오류의 정확한 위치를 찾는 법
오류 메시지에 position 번호가 들어 있습니다. 그것으로 문자열을 슬라이스해 거기 무엇이 있는지 정확히 보세요:
function parseWithContext(text) {
try {
return JSON.parse(text);
} catch (err) {
// V8 오류 메시지에서 position 추출
const match = err.message.match(/position (\d+)/);
if (match) {
const pos = Number(match[1]);
const snippet = text.slice(Math.max(0, pos - 20), pos + 20);
console.error(`Error near: ..."${snippet}"...`);
console.error(` ${''.padStart(Math.min(pos, 20), ' ')}^`);
}
throw err;
}
}자주 묻는 질문
"Unexpected token in JSON" 이 뭔가요?
JSON.parse() 가 문법상 그 시점에 유효하지 않은 문자를 만났다는 뜻. 명시된 토큰은 무엇을 받았는지 알려 주고(< = HTML, u = undefined, ' = 단일 따옴표), 위치는 어디를 봐야 할지 알려 줍니다.
왜 fetch 에서 "Unexpected token '<'" 가 나오나요?
요청이 JSON 이 아니라 HTML 페이지 —— 404, 로그인 리다이렉트, 서버 오류 —— 를 반환했고, < 는 여는 HTML 태그입니다. response.json() 호출 전에 response.ok 를 확인하고 await response.text() 를 로그해 실제로 무엇이 돌아왔는지 보세요.
"Unexpected token u in JSON at position 0" 을 어떻게 고치나요?
undefined 를 JSON.parse() 에 넘겼습니다. 먼저 값을 가드(if (raw) JSON.parse(raw))하고 초기화되지 않은 state 나 누락 된 함수 인수를 확인하세요. 밀접한 token o 변형은 Unexpected token o in JSON 에서 다룹니다.
오류의 정확한 위치를 어떻게 찾나요?
오류 메시지에서 position 번호를 읽고 그 주변으로 문자열을 슬라이스(위 스니펫 참조)하거나 JSON 을 JSON Fix 에 붙여 넣으면 정확한 문제 줄을 강조해 줍니다.
가장 빠른 해결: 온라인으로 붙여넣고 수리
깨진 JSON 문자열을 지금 바로 고쳐야 한다면 JSON Fix 가 위의 모든 원인을 자동으로 처리합니다 —— 단일 따옴표, 후행 쉼표, 따옴표 없는 키, 주석, Python 리터럴 등. JSON 을 붙여 넣고 Repair & Format 을 누르면 한 단계로 깨끗한 유효한 JSON 을 돌려받습니다. 모든 게 브라우저에서 돌고, 어떤 서버로도 전송되지 않습니다.
- JSON Fix —— 깨진 JSON 을 자동 수리하고 정확한 오류 줄을 보여줌
- JSON Unexpected Token 오류 고치기 —— 모든 unexpected token 변형 빠른 참조
- "[object Object] is Not Valid JSON" 고치기 —— 파싱 전에 객체가 문자열로 강제 변환됐을 때의 관련 오류
- JSON Diff —— 수정 전/후 JSON 을 비교해 의도치 않은 변경이 없는지 확인